지금까지 우리가 스프링 빈을 등록할 때 자바 코드에 @Bean을 통해 설정 정보에 직접 등록한 스프링 빈들을 나열했다.
이렇게 등록해야할 스프링 빈이 수십, 수백 개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고 누락하는 문제도 발생한다.
그렇기에 스프링은 설정 정보가 없어도, 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
@Configuration
@ComponentScan
public class AutoAppConfig{
}
@Component
public class MemoryMemberRepository implements MemberRepository{}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
이전에 AppConfig에서는 @Bean으로 직접 설정 정보를 작성했고 의존관계도 명시했다.
하지만 이젠 이런 설정 정보가 없기 때문에 의존 관계 주입도 이 클래스 안에서 해결해야 한다.
1. @ComponentScan
@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
이 때, 빈의 기본 이름은 클래스명에서 맨 앞글자를 소문자로 바꾸고 사용한다.
2. @Autowired 의존관계 자동 주입
생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
하지만 생각해보면 모든 자바 클래스를 다 컴포넌트 스캔한다고 하면 시간이 오래 걸린다.
그렇기에, 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정하면 시간을 절약할 수 있다
@ComponentScan(
basePackages = "hello.core",
)
basePackages는 탐색할 패키지의 시작 위치를 지정한다.
이 패키지를 포함한 하위 패키지를 모두 탐색한다.
만약 지정하지 않는다면, @ComponentScan이 붙은 클래스의 패키지가 시작 위치가 된다.
최근 트렌드는
패키지 위치를 지정하지 않고, @ComponentScan을 붙이는 위치를 프로젝트 최상단에 두는 것이다.
컴포넌트 스캔은 @Component 뿐만이 아니라,
@Controller, @Service, @Repository, @Configuration 도 포함해서 진행한다.
이러한 어노테이션들도 잘 보면 @Component를 상속해서 받고 있다.
의존 관계 주입은 2가지가 주로 쓰인다.
- 생성자 주입
생성자를 통해 의존 관계를 주입받는 방법이다.
생성자를 통해 하므로, 불변, 필수 의존관계에 사용해야 한다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
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;
}
}
과거에는 Setter 주입과 필드 주입을 많이 사용했지만 최근에는 대부분의 프레임워크가 생성자 주입을 권장한다.
왜냐하면
1. 불변
대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다.
오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안되는 것이 맞다.
만약 Setter을 사용하려면 Setter에 public으로 공개해야하고 이렇게 되면 누군가가 실수로 변경하는 문제가 생길 수 있다.
2. 누락
Setter함수는 필수 동작해야하는 함수가 아니다.
그렇기에 해당 메서드를 호출하지 않는다 해서 문제가 발생하지 않는다.
하지만 우리가 Setter함수를 통해 의존관계 주입을 하고 있기 때문에 문제가 발생한다.
생성자 주입을 사용하면, 주입할 데이터를 누락하면 컴파일 오류가 발생하기 때문에 누락 문제를 해결할 수 있다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
만약 저렇게 생성자가 딱 한 개 있으면 @Autowired를 생략할 수 있다 했다.
코드를 간결히 해보자.
롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면
final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
최근에는 생성자를 딱 1개 두고, @Autowired를 생략하는 방법을 주로 사용한다.
그리고 나서 @RequiredArgsConstructor를 사용하면 깔끔하게 사용할 수 있다.
@Autowired는 타입(type)으로 조회한다.
@Autowired
private DiscountPolicy discountPolicy
그렇기에 아래 코드와 유사하게 동작한다.
ac.getBean(DiscountPolicy.class)
타입으로 조회하게 되면 선택된 빈이 2개 이상일 때 문제가 생긴다.
@Autowired
private DiscountPolicy discountPolicy
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
해결책 :
1. @Autowired 필드명 매칭
@Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있으면 필드 이름과 파라미터 이름으로 빈 이름을 추가 매칭한다.
2. @Qualifier 사용
@Qualifier는 추가 구분자를 붙여주는 방법이다. 주입 시 추가적인 방법을 제공하는 것일뿐, 빈 이름을 변경하는 것은 아니다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
3. @Primary 사용
우선순위를 정하는 방법이다. @Primary가 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
코드에서 자주 사용하는 메인 데이터베이스의 경우엔 @Primary로 사용하고
코드에서 특별한 기능으로 가끔 사용하는 경우엔 @Qualifier를 사용하면 좋다.
우선순위의 경우 @Primary는 기본값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작하기 때문에
@Qualifier가 우선권이 높다.
8. 생명주기 콜백)
데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고,
애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.
외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정하자.
이 NetworkClient는 애플리케이션 시작 전에 connect()를 호출해서 연결을 맺어야하고, 애플리케이션이 종료되면 disConnect()를 호출해서 연결을 끊어야한다.
public class NetworkClient{
private String url;
public NetworkClient(){
System.out.println("생성자 호출 url = " + url);
connect();
}
public void connect{
System.out.println("connect: "+ url);
}
public void disconnect(){
System.out.println("close : "+ url);
}
}
생성자 부분을 url 정보 없이 connect가 호출되고 있다.
객체를 생성하는 단계에 url이 없고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해 setUrl()을 해야 url이 존재하겠지.
스프링 빈은 다음과 같은 라이프 사이클을 가진다.
객체 생성 -> 의존관계 주입
스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다.
따라서 초기화 작업은 의존관계 주입이 끝나고 난 다음에 호출해야한다.
그렇기에 개발자 입장에서는 의존관계 주입이 모두 완료된 시점을 알아야한다.
스프링은 의존관계 주입이 완료되면 스프링 빈에게 '콜백 메서드'를 통해서 초기화 시점을 알려준다.
또한 스프링은 스프링 컨테이너가 종료되기 직전에 '소멸 콜백'을 알려준다.
즉 정리하자면
<스프링 빈의 이벤트 라이프사이클>
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 종료
스프링은 3가지 방법으로 빈 생명주기 콜백을 지원한다.
1. 인터페이스 (InitializingBean, DisposableBean)
public class NetworkClient implements InitializingBean, DisposableBean{
private String url;
@Override
public void afterPropertiesSet() throws Exception{
connect();
}
@Override
public void destory() throws Exception{
disconnect();
}
}
해당 인터페이스들은 스프링 전용 인터페이스이다. 그렇기에 해당 코드가 스프링 전용 인터페이스에 의존한다.
또 인터페이스이므로, 초기화랑 소멸 메소드의 이름을 변경할 수 없다.
해당 방법들은 스프링 초창기에 사용한 방법들이고, 지금은 거의 사용하지 않는다.
2. 빈 등록 초기화, 소멸 메서드 지정
설정 정보에
@Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메소드를 지정할 수 있다.
public class NetworkClient
private String url;
public void init() {
connect();
}
public void close() {
disConnect();
}
}
이렇게 되면 메소드 이름을 우리가 원하는 대로 지정할 수 있다.
또 스프링 빈이 스프링 코드에 의존하지 않고, 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에서도 초기화랑 종료 메서드를 적용할 수 있다.
3. @PostConstruct, @PreDestroy 애노테이션 특징
public class NetworkClient
private String url;
@PostConstruct
public void init() {
connect();
}
@PreDestroy
public void close() {
disConnect();
}
}
최신 스프링에서 가장 권장하는 방법이다.
패키지가 javax.annotation.PostConstruct이다. 즉, 스프링에 종속적이지 않고 자바 표준이다.
컴포넌트 스캔과도 잘 어울린다.
단점은, 외부 라이브러리에서는 적용하지 못한다.
그렇기에 그럴땐 2번 설명인 @Bean의 initMethod, destroyMethod를 활용해야한다.