수동 빈 등록은 자바 코드의 @Bean
이나 XML의 <bean>
등을 통해서 등록할 스프링 빈을 직접 설정 정보에 적어주는 방식이다.
@Configuration
이 달린 클래스는 빈 설정을 담당하는 설정 정보 클래스가 된다.@Configuration
이 붙은 클래스를 설정 정보로 사용한다.@Configuration
이 달린 클래스 내에서, 메소드에 @Bean
을 적용하면 메소드가 반환하는 객체가 스프링 빈으로 등록된다.스프링은 설정 정보가 없어도 자동으로 스프링 빈으로 등록해주는 @ComponentScan
기능과 의존관계도 자동으로 주입해주는 @Autowired
기능을 제공한다.
@ComponentScan
은 @Component
가 붙은 모든 클래스를 스프링 빈으로 등록한다.
@Configuration
이 붙은 설정 정보가 스프링 빈으로 등록되었던 이유도, @Configuration
소스코드 내부에 @Component
가 붙어있기 때문이다.
이때 클래스 명의 앞글자를 소문자로 바꾼 이름이 빈 이름으로 등록된다.
(ex, MemberService
-> memberService
)
@Component("memberService2")
와 같이 스프링 빈 이름을 직접 지정할 수도 있다.
탐색 위치
@ComponentScan
이 적용된 클래스의 패키지와 그 하위 패키지에 있는 @Component
가 적용된 클래스들이 빈으로 등록된다.
다음과 같이 탐색 위치를 지정할 수 있다.
basePackages
: 해당 패키지를 포함해서 그 하위 패키지를 모두 탐색한다.
ex, @ComponentScan(basePackages = "hello.core")
ex, @ComponentScan(basePackages = "hello.core", "hello.service")
basePackagesClasses
: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
ex, @ComponentScan(basePackagesClasses = AppConfig.class)
만약 탐색 위치를 지정하지 않으면, @ComponentScan
이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
권장: 패키지 위치를 지정하지 않고, 설정 정보 클래스를 프로젝트의 최상단에 두는 방식
예를 들어서 프로젝트가 다음과 같은 구조로 되어 있으면
com.hello
com.hello.service
com.hello.repository
com.hello
위치에 AppConfig 같은 설정 정보 클래스를 두고,@ComponentScan
애노테이션을 붙이고,basePackages
지정은 생략한다. 그러면 프로젝트의 최상단부터 그 하위 패키지까지 모두 컴포넌트 스캔의 대상이 된다.
스프링 부트 사용 시
스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인
@SpringBootApplication
을 프로젝트의 시작 위치에 두는 것이 관례이다.
그리고@SpringBootApplication
안에@ComponentScan
이 들어있다. 따라서 별도의 설정 정보를 만들지 않아도 스프링 부트가 알아서 컴포넌트 스캔을 통해 스프링 빈을 등록한다.
다음 애노테이션은
@Component
를 가지고 있으면서 추가로 부가 기능을 수행한다.
@Controller
: 스프링 MVC 컨트롤러로 인식@Repository
: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.@Configuration
: 스프링 설정 정보로 인식하고, 스프링 빈을 싱글톤으로 관리해준다.@Service
: 특별한 처리를 하지는 않지만, 비즈니스 계층을 인식하는데 도움이 된다.
생성자를 통한 의존관계 주입 시 사용하는 애노테이션이다.
생성자에 @Autowired
를 붙이면, 스프링 컨테이너가 등록된 빈 중에 해당 빈을 찾아서 자동으로 의존관계를 주입한다.
이때 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
예를 들어, public MemberServiceImpl(MemberRepository memberRepository) {}
생성자에 @Autowired
가 붙어 있는 경우, getBean(MemberRepository.class)
로 동작한다.
생성자가 한 개인 경우 생략 가능하다.
롬복의 @RequiredArgsConstructor
를 적용하면 final
이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. 따라서 생성자를 한 개만 두어 @Autowired
를 생략하고, @RequiredArgsConstructor
까지 적용하면 다음과 같이 코드를 깔끔하게 사용할 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@Autowired
의 기본 값이 required = true
이기 때문에 자동 주입할 대상이 없으면 오류가 발생한다. 이때 오류가 발생하지 않고 제대로 동작하기 위해서는 다음과 같은 자동 주입 대상을 옵션을 사용하면 된다.
@Autowired(required = false)
: 자동 주입할 대상이 없으면 해당 메소드 자체가 호출되지 않는다.
@Autowired(required = false)
public void NoBean(Member member) {...} //Member는 스프링 빈이 아니다.
//위 메소드는 호출되지 않는다.
@Nullable
: 자동 주입할 대상이 없으면 null
이 입력된다.
@Autowired
public void NoBean(@Nullable Member member) {...}
//member에는 null이 입력된다.
Optional<>
: 자동 주입할 대상이 없으면 Optional.empty
가 입력된다.
@Autowired
public void NoBean(Optional<Member> Member member) {...}
//member에는 Optional.empty가 입력된다.
@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번만 호출되는 것이 보장된다. 따라서 의존관계가 불변하며 필수인 경우 사용된다.
가장 권장되는 의존관계 주입 방식이다. 그 이유는 다음과 같다.
대부분의 의존관계는 불변해야 한다.
만약 수정자 주입을 사용하면, setter 메소드를 public으로 열어두기 때문에 의존관계가 변경될 수 있다. 반면 생성자 주입을 사용하면, 객체를 생성할 때 생성자가 딱 1번만 호출되므로 한 번 설정된 의존관계는 변하지 않는다.
NullPointerException이 발생하지 않는다.
생성자를 통해 초기에 의존관계가 할당되기 때문에, NullPointerException
이 발생하지 않는다. 그리고 만약 new OrderServiceImpl()
와 같이 객체 생성 시점에 주입 데이터를 누락한다면, 이때 컴파일 오류가 발생하기 때문에 NullPointerException
이 발생하지 않도록 데이터 누락을 바로 잡을 수 있다.
final 키워드를 사용할 수 있다
생성자 주입 방식을 사용하면 final
키워드를 사용할 수 있다. 따라서 생성자를 정의할 때 의존관계 주입 코드가 빠진 경우, 컴파일 오류가 발생하여 바로 잡을 수 있다.
* 생성자 주입만 final
키워드를 사용할 수 있다. 수정자 주입과 필드 주입은 모두 생성자 이후에 호출되기 때문이다.
@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;
}
}
setter 메소드를 통해 의존관계를 주입하는 방식이다.
선택, 변경 가능성이 있는 의존관계에 사용된다.
final
키워드를 사용할 수 없으며, 의존관계가 변경될 수 있다.
실수로 해당 메소드 호출을 빼먹어 의존관계 주입이 누락된 경우, NullPointerException
이 발생한다.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
}
빈으로 등록된 객체를 사용하고자 하는 클래스에 필드로 선언한 뒤 @Autowired
를 붙여주면 자동으로 의존관계가 주입된다.
코드가 간결하다는 장점이 있다.
final
키워드를 사용할 수 없으며, 의존관계가 변경될 수 있다.
참조 관계를 눈으로 확인하기 어렵다.
ConflictingBeanDefinitionException
예외가 발생한다. 수동 빈이 자동 빈을 오버라이딩 하며, 수동 빈 등록이 우선권을 갖는다.
ex, Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
만약 의도적인 설정이 아니라 여러 설정들이 꼬여서 이런 결과가 나온 경우에는 잡기 어려운 버그가 만들어진다. 그래서 스프링 부트는 수동 빈 등록과 자동 빈 등록이 충돌되는 경우에 오류가 발생하도록 기본 값을 설정해두었다.
ex, Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
@Autowired
는 타입으로 조회한다.
따라서 아래 코드의 경우 ac.getBean(DiscountPolicy.class)
와 유사하게 동작한다.
@Autowired
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
따라서 만약 DiscountPolicy
의 하위 타입인 FixDiscountPolicy
, RateDiscountPolicy
둘 다 스프링 빈으로 등록되어 있다면,
위 코드 실행 결과, NoUniqueBeanDefinitionException
오류가 발생한다.
해결 방법은 다음과 같다.
@Autowired 필드명 매칭
@Qualifier
@Primary
@Autowired
는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
타입 매칭 시도
타입 매칭 결과가 2개 이상인 경우 필드명, 파라미터 명으로 빈 이름 매칭
//필드 이름을 빈 이름으로 변경
@Autowired
private DiscountPolicy rateDiscountPolicy;
}
//파라미터 이름을 빈 이름으로 변경
@Autowired
public OrderServiceImpl(DiscountPolicy rateDiscountPolicy) {
discountPolicy = rateDiscountPolicy;
}
@Qualifier
는 추가 구분자를 붙여주는 방법이다.
* 빈 이름 자체를 변경하는 것은 아니다.
//추가 구분자 붙여주기
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{}
//추가 구분자 붙여주기
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
//주입 시에 등록한 이름을 적어주기
@Component
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Qualifier("mainDiscountPolicy")
이렇게 문자를 적으면, 오타가 있어도 컴파일 시 체크가 안된다. 따라서 애노테이션을 만들어서 문제를 해결할 수 있다.
//애노테이션 생성
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy") //이때 여기서도 추가 구분자를 지정한다.
public @interface MainDiscountPolicy {
}
//애노테이션 적용
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{}
//주입 시에 등록한 애노테이션 적어주기
@Component
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy rateDiscountPolicy;
@Autowired
public OrderServiceImpl(@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
@Primary
는 우선순위를 정하는 방법이다. 의존관계 주입시에 여러 빈이 매칭되면 @Primary
가 우선권을 갖는다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy; //@Primary가 붙은 RateDiscountPolicy가 주입된다.
}
@Primary, @Qualifier
- 활용
자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고,
특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있는 경우, 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은@Primary
를 적용해서 조회하는 곳에@Qualifier
지정 없이 편리하게 조회하고, 서브 데이터베이스의 커넥션을 획득할 때는@Qualifier
를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.
- 우선순위
@Primary
는 기본값처럼 동작하고@Qualifier
는 매우 상세하게 동작한다.
스프링은 자동보다 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 여기서도@Qualifier
가 우선순위가 높다.
RateDiscountPolicy
와 FixDiscountPolict
둘 다 스프링 빈에 등록되어야 한다. public class AllBeanTest {
@Test
public void findAllBean(){
ApplicationContext ac = new AnnotationConfigApplicationContext(DiscountService.class, AutoAppConfig.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
int fixDiscountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
Assertions.assertThat(fixDiscountPrice).isEqualTo(1000);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
Assertions.assertThat(rateDiscountPrice).isEqualTo(2000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}
DiscountService
가 스프링 빈으로 등록될 때 생성자가 호출되고,
이때 DiscountPolicy
가 주입되면서 Map과 List에 모든 할인 정책(FixDiscountPolicy
, RateDiscountPolicy
)이 주입된다.
discount
메소드는 discountCode로 "fixDiscountPolicy" 또는 "rateDiscountPolicy"가 넘어오면 map에서 해당 이름의 스프링 빈을 찾아서 실행한다.
이렇게 동적으로 빈을 선택해야 하는 경우, Map 또는 List를 통해 스프링 빈을 받으면, 상황에 따라 빈을 찾아 사용하기 좋다.
편리한 자동 등록을 기본으로 사용하자
스프링은 @Component
뿐만 아니라 @Controller
, @Service
, @Repository
처럼 계층에 맞춰 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 게다가 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계되어 있다.
설정 정보를 기반으로 애플리케이션을 구성하는 부분과 동작하는 부분을 명확하게 나누는 것이 이상적이지만 일일이 설정 정보 클래스를 정의하고, @Bean
을 적고, 객체를 생성하고, 주입할 대상을 적어주는 과정은 상당히 번거롭다. 그리고 관리할 빈이 많아지면 설정 정보를 관리하는 것 자체가 부담이 된다.
따라서 편리한 자동 등록을 기본으로 사용하자. 자동 등록을 사용해도 OCP, DIP를 지킬 수 있다.
직접 등록하는 기술 지원 객체는 수동 등록
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다.
기술 지원 빈: 기술적인 문제나 공통 관시사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
기술 지원 로직은 업무 로직에 비해 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 기술 지원 로직이 적용이 잘 되고 있는지 아닌지 파악하기 어려운 경우가 많다. 따라서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 게 좋다.
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자
DiscountService
가 자동 의존관계 주입으로 Map<String, DiscountPolicy>
에 주입 받는 상황을 생각해보자. 이때 여기에 어떤 빈들이 주입될지 코드만 보고 한 번에 파악하기 힘들다. 자동 등록을 사용하고 있기 때문에 파악하려면 여러 코드를 찾아봐야 한다.
이런 경우 설정 정보를 만들고 수동으로 빈을 등록하면 한 눈에 파악하기 좋다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
또는 자동 등록을 할 경우에는, DiscountPolicy
의 구현 빈들만 따로 모아서 특정 패키지에 함께 묶어두는 것이 좋다.