스프링 빈
스프링 빈
스프링 컨테이너에서 관리하는 자바 객체를 스프링 빈이라고 한다.
조금 더 자세하게 설명하자면, 기존에 Java 프로그램에서는 객체를 생성할 때 우리가 직접 new 생성자
를 입력해 객체를 생성하였지만, 우리가 직접 객체를 생성하는 것이 아닌 Spring에 의해 생성되고 관리되는 자바 객체를 스프링 빈이라고 한다.
그럼 스프링 컨테이너에는 빈을 등록하는 방법들에 대해 알아보자.
스프링 빈 등록하기
스프링 빈을 스프링 컨테이너에 등록하는 방법으로는 여러가지 방법들이 존재한다. 그 중 대표적으로 가장 자주 사용하는 자바 설정 파일을 이용하는 방법과 컴포넌트 스캔을 이용하는 방법에 대해 소개해보겠다.
자바 설정 파일
@Configuration과 @Bean 어노테이션을 이용하여 Bean을 등록하는 방법이다. 스프링 컨테이너는 @Configuration이 붙은 클래스를 설정 정보로 사용한다. 이 클래스에 정의된 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
- 스프링 컨테이서 생성 및 설정 파일 등록
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
5개의 빈이 등록된 것을 확인할 수 있다.
빈 이름은 @Bean(name="xxxxx")
와 같이 name 속성을 사용해서 직접 부여할수 있고 기본 값은 메서드 명을 사용한다.
빈 이름이 중복이 되면, 예외가 발생할수도 있고 중복된 빈이 무시될 수도 있다.
컴포넌트 스캔
어플리케이션이 커져 등록해야 할 스프링 빈이 많아지게 된다면, 자바 설정 파일에 일일히 등록하기 힘들 것이다. 그래서 스프링은 설정 정보 없이 자동으로 스프링 빈을 등록할 수 있는 컴포넌트 스캔이라는 기능을 제공한다. @ComponentScan 어노테이션은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다. 위 클래스의 빈 이름은 memberServiceImpl이 된다. 빈 이름을 직접 지정하기 위해서는 @Component("xxxxx")
와 같이 지정할 수 있다.
@ComponentScan 어노테이션은 여러가지 속성들을 사용하여 스캔 옵션을 지정할 수 있다.
- 탐색 패키지 시작 지점 지정하기
@ComponentScan(
basePackages = "hello.core"
)
basePackages 는 탐색할 패키지의 시작 위치를 지정한다. 선언한 패키지를 포함해서 하위 패키지를 모두 탐색하고 basePackages를 여러 시작 위치를 지정할 수도 있다.
- 컴포넌스 스캔 대상 필터링하기
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
includeFilters는 탐색 지점부터 컴포넌트 스캔 대상에 존재하지 않은 파일을 추가로 지정할 수 있고 excludeFilters는 컴포넌트 스캔에서 제외할 대상을 지정한다.
자동 빈 등록 vs 수동 빈 등록
스프링 MVC의 @Controller, @Service, @Repository 어노테이션에도 @Component가 포함되어 있고, 우리는 자동 빈 등록을 한다.
그렇다면 수동 빈 등록은 언제 사용하면 좋을까?
어플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있는데 기술 지원 빈을 등록할 때 사용하면 좋을 것 같다. 일단 업무 로직 빈과 기술 지원 빈에 대해 알아보자.
- 업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 레포지토리등이 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
- 기술 지원 빈 : 기술적인 문제나 공통 관심사를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 있다. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽다.
기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 어플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 업무 로직은 문제가 발생했을 대 어디가 문제인지 잘 들어나지만, 기술 지원 로직은 적용이 잘되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.
하지만 비즈니스 로직 중에서 다형성을 활용할 때는 자동 빈 등록보다 수동 빈 등록으로 하는 것이 가독성에 좋을 수도 있다.
빈 중복 등록
빈을 중복 등록되면 예외가 발생하는 경우도 있고 중복된 빈이 무시되는 경우도 존재하는데 세 가지 상황에 대해 살펴보자.
- 수동 빈 등록 vs 수동 빈 등록
@Configuration
public class AppConfig {
@Bean(name = "memberService")
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
위 설정 파일을 컨테이너에 등록하면 어떻게 될까?
1번째, 2번째 빈의 이름이 memberService로 중복이 되었다. 예외는 발생하지 않고 둘 중 하나만 빈으로 등록이 되었다. 결과는2번째 선언한 빈이 무시가 되었다. memberService의 빈 타입을 출력해보니 첫번째 선언한 빈의 타입과 일치하다.
1번째와 2번째 메서드 순서를 바꿔보니 결과가 바뀌었다.
수동 빈 등록을 할 때 빈 이름이 충돌하게 되면 가장 먼저 등록된 빈 이외에는 무시되는 것을 확인할 수 있다.
- 자동 빈 등록 vs 자동 빈 등록
컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록될 때, 이름이 같은 경우에 ConflictingBeanDefinitionException이 발생한다.
- 수동 빈 등록 vs 자동 빈 등록
이 경우에는 수동 빈 등록이 우선권을 가진다. 수동 빈 등록이 자동 빈을 오버라이딩 해버린다.
개발자가 의도적으로 이런 결과를 기대한 것이라면 좋지만 보통 여러 설정들이 꼬여서 이런 결과가 만들어진다고 한다. 예외가 발생하지 않는 예외상황은 정말 잡기 어려운 버그가 된다. 그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 변경했다고 한다.
만약 수동 빈 등록이 자동 빈 등록을 오버라이딩하는 것을 허용하게 설정을 바꾸고 싶다면 application.properties에서 spring.main.allow-bean-definition-overriding=true
을 작성하면 된다.
빈 생명주기 콜백
스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다. 그런데 개발자가 의존관계 주입이 모두 완료된 시점을 알 수 없으니, 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 컨테이너가 종료되기 직전에 소멸 콜백을 준다.
스프링 빈의 이벤트 라이프 사이클
- 스프링 컨테이너 생성
- 스프링 빈 생성
- 의존관계 주입
- 초기화 콜백
- 사용
- 소멸전 콜백
- 스프링 종료
스프링이 지원하는 빈 생명주기 콜백 방법 3가지
InitializingBean, DisposableBean
public class NetworkClient implements InitializingBean, DisposableBean {
...
@Override
public void afterPropertiesSet() throws Exception {
connect();
call("초기화 메시지");
}
@Override
public void destroy() throws Exception {
disconnect();
}
}
- 스프링 전용 인터페이스이므로 스프링에 의존적이다.
- 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
설정 정보에 빈 등록시 초기화 메서드, 종료 메서드 지정
@Configuration
public class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
public class NetworkClient {
public void init() {
connect();
call("초기화 메시지");
}
public void close() {
disconnect();
}
}
@Bean의 destoryMethod 속성에는 메서드 추론 기능이 있다. 라이브러리에서는 대부분 close, shutdown이라는 종료 메서드를 사용한다. 추론 기능은 @Bean에 destoryMethod를 명시하지 않아도 close, shutdown라는 이름의 메서드를 자동으로 호출해준다.
추론 기능을 사용하기 싫으면 detstoryMethod=”“처럼 빈 공백을 지정하면 된다.
@PostConstruct, PreDestroy 어노테이션 지원
public class NetworkClient {
@PostConstruct
public void init() {
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
disConnect();
}
}
위 어노테이션들의 패키지를 잘 보면 javax.annotation이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
또한 어노테이션 하나만 붙이면 되므로 매우 편리하다.
참고
스프링 핵심 원리 - 기본편 (김영한 님) 토비의 스프링 - vol.1 (이일민 님)