Spring

[Spring] Bean에 대한 공부

하고있자 2024. 5. 27. 21:19

Bean

스프링 컨테이너에서 객체를 생성하고 관리되는 객체이다. 

 

스프링 컨테이너

Bean의 인스턴스화, 구성, 생명 주기, 제거 등 Bean을 관리하는 기능을 한다. 스프링 Bean은 DI와 IoC를 이용하여 외부에서 객체를 주입받는다. 이때 외부라는 게 스프링 컨테이너를 의미한다. 

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class)

ApplicationContext를 스프링 컨테이너라 부른다. 스프링 컨테이너는 IoC 컨테이너, DI 컨테이너라고도 한다. ApplicationContext는 인터페이스이다. 위 코드는 스프링 컨테이너를 생성하고 구성 정보인 AppConfig.class를 넣어 스프링 컨테이너를 생성한다. 

 

BeanFactory와 ApplicationContext

위에서 ApplicationContext는 스프링 컨테이너라 부른다고 했는데 ApplicationContext는 BeanFactory를 상속한다. BeanFactory은 스프링 컨테이너의 최상위 인터페이스다. BeanFactory는 스프링 빈을 관리하고 조회하는 역할을 담당한다.

 

BeanFactory만으로는 스프링의 기능이 부족한 경우가 많다. 그렇기에  BeanFactory뿐 아니라 다른 인터페이스도 상속하는 ApplicationContext를 주로 사용한다.

위 그림처럼 ApplicationContext는 여러 인터페이스를 상속한다. 

상속하는 인터페이스의 기능을 살짝 보면

  • ResourceLoader : Classpath 또는 file system으로부터 리소스를 읽어 들이기 위한 인터페이스이다.
  • MessageSource : message.properties에 등록된 메시지를 읽는 인터페이스다. 이를 통해서 다른 여러 언어의 메시지를 읽을 수 있다.
  • ApplicationEventPublisher : 이벤트를 발생시키고, 발생시킨 이벤트를 핸들링하는 기능을 제공한다.

ApplicationContext는 여러 가지 기능을 상속받아서 스프링 컨테이너의 기능을 수행한다.

 

Componentscan

스프링에서 자동으로 빈을 등록하기 위해 사용하는 어노테이션은 @Component와 @Componentscan이다. 여기서 @Componentscan은 @Component애노테이션이 있는 Class들을 스캔하여 Bean으로 등록해 준다. @Component은 자동으로 Bean을 등록해 주는 애노테이션이다.

@Component
public class MemberRepositoryImpl implements MemberRepository {
...
}

위 코드처럼 @Component를 붙여두면 @Componentscan을 통해서 자동으로 스프링 빈으로 등록된다.

 

웹 공부를 하면서 @Controller, @Service, @Repository, @SpringBootApplication 애노테이션들을 사용했다면 이 애노테이션 안에는 @Component와 @Componentscan 존재한다. 즉 @Controller, @Service, @Repository가 Componentscan의 대상이 되어 자동으로 빈을 등록한다.

애노테이션 내에 @Component과 @Componentscan 존재한다.

 

@Configuration, @Bean

스프링에서 수동으로 Bean을 등록하기 위한 애노테이션이다. 

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository(){
        return new MemberRepositoryImpl();
    }
}

빈을 수동으로 등록이 가능하다. 빈을 수동으로 등록하면 명시적으로 표기하기에는 좋다. 하지만 개수가 많아지면 귀찮아지거나 누락에 가능성도 있고 실수할 수 도 있다. 빈을 자동으로 등록이 가능한데 왜 이렇게 수동으로 빈을 등록할까?

 

스프링을 공부하면 스프링에서는 객체를 싱글톤으로 관리한다. 싱글톤은 객체 인스턴스를 1개만 생성하고 관리하는 것을 말한다. 위 코드를 보면 이상하다. 분명 스프링은 객체를 1번만 생성하는 싱글톤으로 객체를 관리한다고 하는데 memberRepository에서 객체를 생성해서 호출하고 memberService에서도 객체를 생성하고 호출한다. 객체를 호출할 때마다. 객체를 생성하고 호출한다. 위 코드만 봐도 싱글톤이 깨져 보인다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
    	System.out.println("memberService 호출");
        return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository(){
    	System.out.println("memberRepository 호출");
        return new MemberRepositoryImpl();
    }
}

코드를 바꿔서 실행하면 싱글톤이기에 memberRepository는 2번 호출되어야 맞다. 

실제 결과는 1번이다. 왜 memberRepository는 2번 호출되지 않았는가? 좀 더 깊게 공부해 보자

@Test
void deepClass(){
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
}

위 코드는 AppConfig를 스프링 빈으로 등록하고 그 Bean의 정보를 확인하는 코드이다.

Bean의 정보를 보는데 이상하다. CGLIB? 위에 어떤 코딩에서도 CGLIB를 붙인 적이 없다. 어떻게 된 걸까?

 

스프링에서 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받는 임의의 다른 클래스를 만들고, 다른 클래스를 스프링 빈으로 등록한 것이다. CGLIB Bean의 로직을 살짝 설명하면 @Bean이 붙은 메서드이고 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환한다. 이렇게 사용하기에 스프링에서는 싱글톤이 보장된다.

 

수동, 자동 빈 등록 충돌이 일어나면?

코드를 짜다보면 메서드의 이름이 같아지는 경우가 있다. 이때 빈으로 등록하면 문제가 발생한다. 빈이 자동으로 Bean을 등록할 때와 수동으로 Bean을 등록할 때 차이가 있다.

 

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 ConflictingBeanDefinitionException 예외가 발생한다. 

 

수동 빈 등록 vs 자동 빈 등록

이 경우 수동 빈 등록이 우선권을 가진다. 수동 빈이 자동 빈을 오버라이딩한다.

 

의존관계 주입

의존관계 주입을 자세히 보고 싶다면 쓴이가 쓴 글을 읽자. 글을 간단하게 이야기하면 스프링에서는 필드 주입, 생성자 주입, setter 주입이 있다. 스프링은 싱글톤을 유지하려 하고 이를 권장한다. 즉 스프링의 의존관계 주입은 한번 일어나고 변경하는 일이 없다. 따라서 불변해야 하기에 생성자 주입을 사용하는 것을 스프링에서는 권장한다.

 

@Autowired

스프링에서 의존관계를 자동으로 주입받는 어노테이션이다. 스프링 빈의 생성자가 1개라면 생략이 가능하다. 

 

빈의 우선순위 

빈을 자동으로 생성하면 빈이 2개 이상일 때 타입을 조회하면서 문제가 발생한다. 

@Autowired
private Car car;

위 인스턴스를 조회할 때 아래와 같이 선택된 빈이 2개 이상이면 문제가 발생한다.

@Component
public class HydrogenCar implements Car {}
@Component
public class ElectricCar implements Car {}

위처럼 코드를 설정하고 실행하면 NoUniqueBeanDefinitionException가 발생한다.

 

이를 해결하기 위해서 3가지 방법이 있다.

 

@Autowired 필드 명

타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

@Autowired
private Car car; //기존 코드
@Autowired
private Car electricCar; // 변경 코드

먼저 타입을 매칭하고 타입 매칭에서 결과가 2개 이상일 경우 위 코드처럼 필드 이름, 파라미터 이름으로 빈 이름을 매칭한다.

 

@Qualifier

추가 구분자를 붙여주는 방법이다. 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

코드를 보면 이해가 가능한다. 코드를 보자

@Component
@Qualifier("hydrogenCar")
public class HydrogenCar implements Car {}

위 코드처럼 구분자를 빈 이름과 같이 할 수도 있다.

@Component
@Qualifier("mainCar")
public class electricCar implements Car {}

위 코드처럼 구분자의 이름을 변경하여 사용할 수도 있다.

@Service
public class CarService {

    private final Car car;
    
    @Autowired
    public CarService(@Qualifier("mainCar") Car car) {
            this.car = car;
    }
}

위 코드처럼 생성자 자동 주입을 할 때 Qualifier를 활용해야 어떤 빈이 들어가야 하는지 정할 수 있다.

생성자 주입, 수정자 주입에서 사용이 가능하다. 만약 mainCar라는 구분자를 가진 빈이 없다면 mainCar라는 이름을 가진 스프링 빈이 있는지 추가로 찾는다. 

 

@Primary

우선순위를 정하는 방법이다. @Autowired에서 여러 빈이 조회되면 @Primary을 가진 빈이 우선권을 가진다.

@Component
public class HydrogenCar implements Car {}

@Component
@Primary 
public class ElectricCar implements Car {}

생성자 주입, 수정자 주입을 하는 코드에서는 무조건 electricCar가 @Primary를 가지고 있기에 우선권을 가져서 코드에서 호출되게 된다.

 

만약 빈을 조회할 때@Qualifier, @Primary가 두 개가 동시에 있다면 더 좁은 범위의 선택권을 가지는 @Qualifier이 우선권을 가진다.

 

빈 생명주기 콜백

애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려다. 객체의 초기화와 종료 작업이 필요하다. 

 

콜백이란

주로 콜백함수를 부를 때 사용되는 용어다. 콜백함수를 등록하면 특정 이벤트가 발생했을 때 해당 메서드가 호출된다.

 

스프링 빈의 이벤트 라이프사이클

스프링 컨테이너 생성 > 스프링 빈 생성 > 의존관계 주입 > 초기화 콜백 메서드 호출 > 사용 > 소멸 전 콜백 메서드 호출 > 스프링 종료

여기서 초기화 콜백에서는 빈이 생성되고, 의존관계 주입이 완료된 후 호출한다. 소멸전 콜백에서는 빈이 소멸되기 직전에 호출한다.

 

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.

 

인터페이스(InitializingBean, DisposableBean)

public class CarBean implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() throws Exception {
    	// 초기화 콜백 (의존관계 주입이 끝나면 호출)
    }
	
    @Override
    public void destroy() throws Exception {
    	// 소멸 전 콜백 (메모리 반납, 연결 종료와 같은 과정)
    }
}

 

특징

  • InitializingBean은 afterPropertiesSet 메서드로 초기화를 지원한다
  • DisposableBean은 destroy 메서드로 소멸을 지원한다
  • 이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 외부 라이브러리에는 적용할 수 없다

 

설정 정보에 초기화 메서드, 종료 메서드 지정

public class CarBean {
    public void init() {
    	// 초기화 콜백 (의존관계 주입이 끝나면 호출)
    }
    
    public void close() {
    	// 소멸 전 콜백 (메모리 반납, 연결 종료와 같은 과정)
    }
} 

@Configuration
class LifeCycleConfig {
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
    }
}

 

특징 

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 동작한다.

 

@PostConstruct, @PreDestroy 애노테이션 지원

public class CarBean {
    @PostConstruct
    public void init() {
    	 // 초기화 콜백 (의존관계 주입이 끝나면 호출)
    }
    
    @PreDestroy
    public void close() {
    	// 소멸 전 콜백 (메모리 반납, 연결 종료와 같은 과정)
    }
}

 

특징 

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 외부 라이브러리에는 적용하지 못한다.

 

출처

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/

https://ittrue.tistory.com/221

https://dev-coco.tistory.com/170

https://mangkyu.tistory.com/151

https://castleone.tistory.com/2

https://woooongs.tistory.com/99