eunnnn 2023. 4. 24. 03:06

Spring Bean

Spring IoC 컨테이너가 관리하는 자바 객체를 빈(Bean)이라고 한다.

 
Spring Ioc (제어의 역전, Inversion Of Control)

일반적으로 처음에 배우는 자바 프로그램에서는 각 객체들이 프로그램의 흐름을 결정하고 각 객체를 직접 생성하고 조작하는 작업(객체를 직접 생성하여 메소드 호출)을 했다. 예를 들어 A 객체에서 B 객체에 있는 메소드를 사용하고 싶으면, B 객체를 직접 A 객체 내에서 생성하고 메소드를 호출하는 것이다. 이때는 모든 작업을 사용자가 제어한다.


하지만 IoC가 적용된 경우, 객체의 생성을 특별한 관리 위임 주체에게 맡기게 된다. 이 경우 사용자는 객체를 직접 생성하지 않고, 객체의 생명주기를 컨트롤하는 주체는 다른 주체가 됩니다.


요약하면 Spring의 Ioc란 클래스 내부의 객체 생성 -> 의존성 객체의 메소드 호출이 아닌, 스프링에게 제어를 위임하여 스프링이 만든 객체를 주입 -> 의존성 객체의 메소드를 호출하는 구조다. 스프링에서는 모든 의존성 객체를 스프링이 실행될때 만들어주고 필요한 곳에 주입해준다.

 

Spring Container (=IoC Container)

스프링 컨테이너는 자바 객체(Bean)의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공하는 역할을 한다. 개발자는 new 연산자, 인터페이스 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸시킬 수 있는데, 스프링 컨테이너가 이 역할을 대신하며 제어 흐름을 외부에서 관리한다. 또한, 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 설정해준다.

최종적으로 정리한 스프링 컨테이너의 역할은 객체(빈)의 생성과 관계설정, 사용, 제거 등의 작업이다.

 

스프링 컨테이너는 BeanFactory와 ApplicationContext로 나뉜다.

  • BeanFactory
    • 스프링 컨테이너의 최상위 인터페이스
    • 스프링 빈을 관리하고 조회하는 역할
  • ApplicationContext 
    • BeanFactory 기능을 모두 상속받아서 제공한다.
    • 다음과 같은 부가기능들을 제공한다.
      • 메시지 소스를 활용한 국제화 기능
      • 환경변수 - 로컬, 개발, 운영 등을 구분해서 처리
      • 애플리케이션 이벤트 관리
      • 편리한 리소스 조회

 

또한 스프링 컨테이너는 객체의 인스턴스를 싱글톤으로 관리하므로 싱글톤 컨테이너라고도 불린다. 이에 대한 정보는 아래 게시글에서 더욱 자세히 알 수 있다.

 


Bean 등록 방법

1. 어노테이션 사용
@ComponentScan 어노테이션과 @Component 어노테이션을 사용해서 빈을 등록할 수 있다.
간단히 말하면 @ComponentScan 어노테이션은 어느 지점부터 컴포넌트를 찾으라고 알려주는 역할을 하고 @Component는 실제로 찾아서 빈으로 등록할 클래스를 나타내는 역할을 한다. 클래스 위에 @Component를 붙이면 스프링이 알아서 스프링 컨테이너에 빈을 등록한다.
 

@Component 외에도 @Controller, @Service, @Repository, @Configuration는 @Component의 상속을 받고 있으므로 모두 컴포넌트 스캔의 대상이다.


 
2. Bean 설정 파일에 직접 등록하는 방법

@Configuration과 @Bean Annotation 을 이용하여 Bean을 등록할 수 있다.

자바 설정파일은 자바 클래스를 생성해서 작성할 수 있으며 일반적으로 xxxxConfiguration와 같이 명명하고, @Configuration을 붙여  Spring Project 에서의 Configuration 역할을 하는 Class를 지정할 수 있다. 그리고 해당 File 하위에 Bean 으로 등록하고자 하는 Class에 @Bean Annotation을 사용해주면 간단하게 Bean을 등록할 수 있다.

// Hello.java
@Configuration
public class HelloConfiguration {
    @Bean
    public HelloController sampleController() {
        return new SampleController;
    }
}

 

@Bean vs @Component

  • @Bean은 메소드 레벨에서 선언하며, 반환되는 객체(인스턴스)를 개발자가 수동으로 빈으로 등록하는 애노테이션이다. 반면 @Component는 클래스 레벨에서 선언함으로써 스프링이 런타임시에 컴포넌트 스캔을 하여 자동으로 빈을 찾고(detect) 등록하는 애노테이션이다.
  • @Bean은 위와 같이 ElementType 설정이 METHOD 혹은 ANNOTATION_TYPE이므로 메소드나 어노테이션에만 붙일 수 있다. 그러나 @Component는 위와 같이 ElementType.TYPE 설정이 있으므로 Class 혹은 Interface에만 붙일 수 있다.
  • @Bean은 개발자가 컨트롤이 불가능한 외부 라이브러리들을 Bean으로 등록하고 싶은 경우에 사용된다. 반면@Component는 개발자가 직접 컨트롤이 가능한 클래스들의 경우에 사용된다.    

 

스프링 Bean Scope

Spring Bean Scope는 말그대로 빈이 존재할 수 있는 범위, 생성부터 소멸까지의 범위를 가리킨다.

Spring의 Bean은 별다른 설정이 없으면 Singleton Scope로 생성된다. 그러나 이는 동시성 문제를 유별하여 위험한 상황을 초래할 수 있다. 그리고 요구사항과 구현 기능 등의 필요에 따라서 비싱글톤이 필요한 경우도 많다. 그리고 이를 명시적으로 구분하기 위해서 scope라는 키워드를 제공한다. Spring 에는 다음과 같은 Scope들이 존재한다.

  • 싱글톤
    • Spring 프레임워크에서 기본이 되는 스코프
    • 스프링 컨테이너의 시작과 종료까지 1개의 객체로 유지됨
  • 프로토타입
    • 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프 (Bean의 소멸에는 스프링 컨테이너가 더이상 관여하지 않고, gc에의해 빈이 제거된다.   
    • 요청이 오면 항상 새로운 인스턴스를 생성하여 반환하고 이후에 관리하지 않음
    • 프로토타입을 받은 클라이언트가 객체를 관리해야 함(종료메서드에 대한 호출도 클라이언트가 직접 해야한다) 
    • request: 각각의 요청이 들어오고 나갈때가지 유지되는 스코프
    • session: 세션이 생성되고 종료될 때 까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

스프링 Bean의 Life Cycle 

Spring Container는 자바 객체(Bean)의 생성과 소멸 같은 생명주기(Life Cycle)를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공하는 역할을 한다.

  • Spring의 Bean은 Java 또는 XML bean 정의를 기반으로 컨테이너가 시작될 때 인스턴스화 되어야 한다.
  • Bean을 사용 가능한 상태로 만들기 위해 사전, 사후 초기화 단계를 수행해야할 수도 있다.
  • 그 후 Bean이 더 이상 필요하지 않으면 Ioc Container에서 제거된다.
  • 다른 시스템 리소스를 해제하기 위해 사전 및 사후 소멸 단계를 수행해야할 수도 있다.

Spring Container는 이런 빈 객체의 생명주기를 컨테이너의 생명주기 내에서 관리하고 객체 생성이나 소멸 시 호출될 수 있는 콜백 메서드를 제공하고 있습니다.

 

이를 토대로 Spring Bean 라이프 사이클을 정리하면 다음과 같다.

  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성
  3. 의존 관계 주입
  4. 초기화 콜백 (빈이 생성되고 빈의 의존관계 주입이 완료된 후 호출되는 것)   
  5. 사용
  6. 소멸 전 콜백 (빈이 소멸되기직전에 호출)  
  7. 스프링 종료

그리고 이 때 쓰이는 콜백 함수는 다음의 세 가지 방식으로 구현될 수 있다.

 

인터페이스(InitializingBean, DisposableBean)

public class NetworkClient implements InitializingBean, DisposableBean {
    private String url;

    public NetworkClient()  {
        System.out.println("생성자 호출, url = " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    //서비스 시작시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

    public void call(String message) {
        System.out.println("call: " + url + " message = " + message);
    }

    //서비스 종료시 호출
    public void disconnect() {
        System.out.println("close: " + url);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("NetworkClient.afterpropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}

위와 같이, InitializingBean 인터페이스를 상속받아 afterPropertieSet() 메소드를 오버라이드하여 초기화 콜백 함수를 구현할 수 있다.
또한 DisposableBean 인터페이스를 상속받고, destroy() 메소드를 오버라이드해 소멸 전 콜백함수를 구현할 수 있다.

하지만, 해당 방식의 경우 아래 3가지 단점이 있다.

  • 스프링 전용 인터페이스들을 상속하므로, 코드가 스프링 인터페이스에 의존적
  • 메소드 이름 변경 불가
  • 코드가 공개되지않은 외부 라이브러리에 적용 불가

설정 정보에 초기화, 종료 메소드 지정 방식

class NetworkClient {
  void init(){}
  void close(){}
}
@Configuration
static class LifeCycleConfig {
	@Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
    	NetworkClient networkClient = new NetworkClient();
    	networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

위와 같이, 클래스 내부에 초기화/종료 메소드를 구현해놓고 @Bean(initMethod = "init", destroyMethod = "close") 처럼 Bean Annotation에 추가 아규먼트들을 설정해주는 방식으로 콜백 메소드들을 사용할 수 있다.

해당 방식의 장점은 아래와 같다.

  • 메소드 이름을 자유롭게
  • 스프링 빈이 스프링 코드에 의존하지 않음
  • 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메소드를 적용 가능

어노테이션 @PostConstruct, @PreDestroy

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@PostConstruct
public void init() throws Exception {
	System.out.println("NetworkClient.afterpropertiesSet");
    connect();
    call("초기화 연결 메시지");
}
@PreDestroy
public void close() throws Exception {
	System.out.println("NetworkClient.destroy");
    disconnect();
}

단순 어노테이션을 통해 콜백 함수들을 커스터마이징해준다.
해당 방식의 장점은 아래와 같다.

  • import문에서 알 수 있듯이, 스프링 인터페이스에 의존적이지 않으며, 최신 스프링에서 권장하는 방식이다.
  • 컴포넌트 스캔과 활용도가 높다.

하지만 아래와 같은 단점이 있다.

  • 코드 수정이 불가능한 외부 라이브러리에는 적용할 수 없다.

결론

코드 수정이 불가능한 외부 라이브러리에는 @Bean(initMethod="XX", destroyMethod="XX") 방식을 사용하자.
그 외의 경우엔 @PostConstruct, @PreDestroy를 사용하자.

더보기