1. 의존관계 주입 방법
생성자 주입
생성자를 통해 의존관계를 주입 받는 방법
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 생성자가 1개이므로 생략해도 자동 주입 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
- 생성자 호출 시점에 딱 1번만 호출되는 것이 보장됨
- 불변, 필수 의존관계에 사용
- 💫생성자가 딱 하나만 존재한다면 @Autowired를 생략해도 자동 주입됨
수정자 주입 (setter 주입)
필드의 값을 변경하는 수정자 메서드 setter를 통해 의존관계를 주입하는 방법
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public MemberRepository getMemberRepository() {
return this.memberRepository;
}
}
- 자바 빈 프로퍼티 규약의 수정자 메서드 방식(setter)을 사용하는 방법
- 참고: 자바 빈 프로퍼티 규약 = 필드의 값을 직접 변경하지 않고, setAbc, getAbc 메서드를 통해 값을 수정하거나 읽는 규칙
- 선택, 변경 가능성이 있는 의존관계에서 사용
- @Autowired의 기본 동작은 주입할 대상이 없으면 오류 발생
- 주입할 대상이 없어도 동작하도록 하려면 @Autowired(required = false)로 지정해야 함
필드 주입
필드에 바로 주입하는 방법
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
- 코드가 간결하지만 외부에서 변경이 불가능해 테스트하기 힘들고, DI 프레임워크가 없으면 아무것도 할 수 없기 때문에 사용하지 않는 것이 좋음
- 순수한 자바 테스트 코드에는 @Autowired가 동작하지 않으며 @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능함
- 애플리케이션의 실제 코드와 관계 없는 테스트 코드나, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용해야 함
@Bean
OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy discountPolicy) {
return new OrderServiceImpl(memberRepository, discountPolicy);
}
- @Bean에서 파라미터에 의존관계는 자동 주입됨
- 수동 등록 시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결 가능
일반 메서드 주입
일반 메서드를 통해 의존관계를 주입하는 방법
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
- 한 번에 여러 필드를 주입 받을 수 있음
- 일반적으로 잘 사용 X
- 참고: 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작함
- 스프링 빈이 아닌 Member 같은 class에서 @Autowired를 적용해도 아무 기능도 동작하지 않음
옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있지만, @Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어 자동 주입 대상이 없으면 오류가 발생한다. 이때, 자동 주입 대상을 옵션으로 처리하는 방법은 3가지가 있다.
a. @Autowired(required=false)
자동 주입할 대상이 없으면 수정자 메서드 자체가 호출이 안 됨
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
- Member라는 class는 스프링 빈이 아님
- setNoBean1 메서드는 @Autowired(required=false)이므로 호출 자체가 안 됨
b. org.springframework.lang.@Nullable
자동 주입할 대상이 없으면 null이 입력됨
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member); // 반환 값 null
}
c. Optional<>
자동 주입할 대상이 없으면 Optional.empty가 입력됨
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member); // 반환 값 Optional.empty
}
참고: @Nullable, Optional은 스프링 전반에 걸쳐서 지원됨
- 예를 들어, 생성자 자동 주입에서 특정 필드에서만 사용해도 됨
생성자 주입을 선택해야 하는 이유
최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장함
- 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법임
- 기본으로 생성자 주입 사용 + 필수 값이 아닌 경우 수정자 주입 방식을 옵션으로 부여 (동시 사용 가능)
a. 불변
- 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료 시점까지 변경할 일이 없음 (오히려 불변해야 함)
- 수정자 주입을 사용한다면, setAbc 메서드를 public으로 열어둬야 함
- 누가 실수로 변경할 수 있기 때문에 불변해야 하는 메서드를 public으로 열어두는 건 좋은 설계 방법이 아님
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 불변하게 설계할 수 있음
b. 누락
- 프레임워크 없이 순수한 자바 코드를 유닛 테스트할 때 아래 수정자 의존관계인 경우, NullPointException이 발생함 (의존관계 주입 누락)
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
//...
}
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
- 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생함
- IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있음
c. final 키워드
생성자 주입 사용 시 필드에 final 키워드를 사용할 수 있음
- 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에서 막아줌
java: variable discountPolicy might not have been initialized
2. 롬복과 최신 트렌드
롬복 라이브러리 적용 방법
build.gradle에 라이브러리 및 환경 추가
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor('org.projectlombok:lombok')
}
- [File → Settings] → plugin → lombok 검색 설치 실행 (재시작)
- Spring Initializr에서 Dependency에 Lombok 추가
- [File → Settings] → Annotation Processors 검색 → Enable annotation processing 체크 (재시작)
롬복
a. 설명
getter, setter, toString 등의 메서드 작성 코드를 줄여주는 라이브러리로, 긴 코드를 짧게 줄일 수 있음 (기능은 동일)
- 아래 5개의 어노테이션을 @Data 하나만 붙여 나타낼 수도 있음
- 이외에도 여러 어노테이션을 제공하며, 이를 기반으로 코드를 컴파일 과정에서 생성해 주는 방식으로 동작함
- 코딩 과정에서는 어노테이션만 보이지만, 실제로 컴파일된 결과물(.class)에는 코드가 생성돼 있음
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructorpublic
class CategoryModel {
private String id;
private String parentId;
private String name;
private Long depthLevel;
private Long seq;
private String userYn;
}
- 장점과 주의사항
- 복잡하고 반복되는 코드를 줄여 가독성과 생산성을 높일 수 있음
- 이해하지 않고 사용하면 무한 재귀 호출 또는 순환 참조의 문제 등이 발생할 수 있음
b. @RequiredArgsConstructor
final 키워드가 붙은 필드를 모아 생성자를 자동으로 만들어 줌
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
- 생성자를 딱 1개 두고, @Autowired를 생략하는 방법을 주로 사용함
- 여기에 @RequiredArgsConstructor를 사용하면 코드를 더 깔끔하게 작성할 수 있음
3. 조회 빈이 2개 이상일 때
문제
@Autowired는 ac.getBean(Abc.class)와 유사하게 동작함 = 타입으로 조회
- '스프링 빈 조회'에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생함
NoUniqueBeanDefinitionException: No qualifying bean of type
...
expected single matching bean but found 2
...
조회 대상 빈이 2개 이상일 때 해결 방법
- 하위 타입으로 지정 → DIP 위반 및 유연성 저하 & 이름만 다르고 타입이 똑같은 빈이 있으면 해결 불가
- 스프링 빈 수동 등록
- ✔️의존관계 자동 주입에서 해결하는 방법
- @Autowired 필드 명 매칭
- @Qualifier → @Qualifier끼리 매칭 → 빈 이름 매칭
- @Primary 사용
@Autowired 필드 명, @Qualifier, @Primary
a. @Autowired 필드 명 매칭
@Autowired의 동작 방식
- 타입 매칭 시도
- 타입이 같은 빈이 여러 개 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭 (필드 명 매칭)
@Autowired
private MemberService memberService;
// 필드 명을 빈 이름으로 변경
@Autowired
private MemberService memberServiceA;
b. @Qualifier 사용
추가 구분자를 붙여주는 방법
- 주입 시 추가적인 방법을 제공하는 것 (빈 이름 변경 X)
- 빈을 직접 등록할 때도 사용 가능
- 모든 코드에 어노테이션을 붙여야 한다는 단점이 존재함
// 빈 등록 시 @Qualifier를 붙여 줌
@Component
@Qualifier("memberServiceA")
public class MemberServiceA implements MemberService {}
@Component
@Qualifier("memberServiceB")
public class MemberServiceB implements MemberService {}
- 주입 시 @Qualifier을 붙이고 등록한 이름을 적음
- 등록한 이름(abc)에 대한 빈을 찾을 수 없다면 abc라는 이름의 스프링 빈을 추가로 찾음
- @Qualifier를 찾는 용도로만 사용하는 게 좋음
// 생성자 자동 주입 예시
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("MemberServiceA") MemberService memberService) {
this.memberRepository = memberRepository;
this.memberService = memberService;
}
// 수정자 자동 주입 예시
@Autowired
public void setMemberService(@Qualifier("MemberServiceA") MemberService memberService) {
this.memberService = memberService;
}
- @Qualifier끼리 매칭
- 빈 이름 매칭
- NoSuchBeanDefinitionException 예외 발생
c. @Primary 사용
우선순위를 정하는 방법
- @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가짐
@Component
@Primary // 우선권
public class MemberServiceA implements MemberService {}
@Component
public class MemberServiceB implements MemberService {}
d. @Primary와 @Qualifier의 활용 예시
코드에서 자주 쓰는 메인 DB의 커넥션을 획득하는 스프링 빈과 코드에서 특별한 기능으로 가끔 쓰는 서브 DB의 커넥션을 획득하는 스프링 빈이 있다고 가정
- 메인 DB 커넥션을 얻는 스프링 빈은 @Primary 적용 (@Qualifier 적용해도 되긴 함)
- @Qualifier 지정 없이 편리하게 조회
- 서브 DB 커넥션을 얻는 스프링 빈은 @Qualifier 적용
@Primary는 기본값처럼 동작하고, @Qualifier는 매우 상세하게 동작함
- 스프링의 우선순위: 자동 < 수동, 넓은 범위의 선택권 < 좁은 범위의 선택권
- 따라서, @Qualifier가 우선!
어노테이션 직접 만들기
@Qualifier("memberServiceA")처럼 문자를 적으면 컴파일 시 타입 체크가 안 되기 때문에 커스텀 어노테이션을 만들어 해결할 수 있음
- 코드 용
@Component
@MainMemberService
public class MemberServiceA implements MemberService {}
4. 조회한 빈이 모두 필요할 때, List, Map
코드 분석
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
}
static class DiscountService {
// Map으로 모든 DiscountPolicy를 주입 받음 (fixDiscountPolicy, rateDiscountPolicy)
// key: 스프링 빈 이름, value: DiscountPolicy 타입으로 조회한 모든 스프링 빈
private final Map<String, DiscountPolicy> policyMap;
// DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담는 List
private final List<DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
// map에서 넘어오는 String으로 해당 스프링 빈을 찾아서 실행
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
5. 자동, 수동의 올바른 실무 운영 기준
편리한 자동 기능을 기본으로 사용하기
스프링은 점점 자동을 선호하는 추세임
- @Component, @Controller, @Service, @Repository처럼 계층에 맞춰 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있음
- 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계함
설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이긴 하지만, 자동 빈 등록만 사용해도 OCP와 DIP를 지킬 수 있음
직접 지원하는 기술 지원 객체는 수동 등록
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있음
a. 업무 로직 빈
- 웹을 지원하는 Controller, 핵심 비즈니스 로직이 있는 Service, 데이터 계층의 로직을 처리하는 Repository 등
- 비즈니스 요구사항을 개발할 때 추가되거나 변경됨
- 숫자도 매우 많고, 한 번 개발하면 유사한 패턴이 보이기 때문에 자동 기능을 적극 사용하는 것이 좋음
b. 기술 지원 빈
- 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용됨
- DB 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술
- 업무 로직에 비해 수가 적고, 애플리케이션 전반에 걸쳐 광범위하게 영향을 미치며 문제가 발생했을 때 파악하기 어려운 경우가 많기 때문에 수동 빈 등록을 사용해 명확하게 드러내는 것이 좋음 (유지 보수를 위해)
다형성 적극 활용하는 비즈니스 로직은 수동 등록 고민하기
'조회한 빈이 모두 필요할 때, List, Map'의 경우, 자동 등록을 사용하고 있기 때문에 어떤 빈들이 주입될 지, 각 빈들의 이름은 뭘지 코드만 보고 파악할 수 없음
- 이런 경우 수동 빈 등록을 사용하거나 자동으로 하려면 특정 패키지에 같이 묶어두는 것이 파악하기 쉬움
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// 수동 빈 등록 예시
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
return configuration.getAuthenticationManager();
}
....
}
참고: 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외!
- 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해 명확하게 드러내는 것이 좋음