[Spring] Spring Bean의 초기화와 소멸 메서드, Bean Scope

벼랑 끝 코딩·2025년 3월 18일

Spring

목록 보기
2/16

Spring 시작으로 스프링 컨테이너와 스프링 빈 등록 그리고 의존 관계 주입을 알아봤다.

이번에는 스프링 컨테이너에 등록되는 대상이자 의존 관계 주입의 대상인
Spring Bean에 대해 자세히 알아보자.

Spring Bean 초기화, 소멸 메서드

Spring Container → Spring Bean → Dependency Injection
→ initialization Method Callback → Bean Using → close method Callback → terminated

Spring을 시작하면 먼저 Spring Container가 생성되고,
Spring Bean 생성(객체 생성 포함) 후 의존 관계가 주입되어 구조가 완성된다.
그 후 Bean은 곧바로 사용되는게 아니라 초기화 메서드가 실행되고
Spring 종료 전에는 소멸 메서드가 호출된 뒤 종료된다.

Spring Bean 생성 직후와 소멸 직전에 각각 메서드가 1회씩 호출되는 것이다!
그렇다면 Spring은 Bean의 수 많은 메서드 중에
어떤 것이 초기화 메서드이고 어떤 것이 소멸 메서드인 줄 알고 실행하는 것일까?

참고로 초기화 메서드의 경우, 보통 외부 커넥션을 연결하는 등 무거운 동작을 수행해서
객체를 생성하는 부분과 초기화하는 부분을 분리하는 것이 좋다.

InitializingBean, DisposableBean 구현

class Service implements InitializingBean, DisposableBean {
	
    @Override  // ** InitializingBean 구현 오버라이딩 메서드 **
    public void afterPropertiesSet() throws Exception {
    	// 코드
    }
    
    @Override  // ** Disposable 구현 오버라이딩 메서드 **
    public void destroy() throws Exception {
    	// 코드
    }
}

InitializingBean 인터페이스 구현 시,
afterPropertiesSet() 메서드를 오버라이딩해야 한다.
Spring Bean 생성 후 의존 관계 주입까지 끝나면 Spring은 해당 메서드를 실행시킨다.

DisposableBean 인터페이스 구현 시,
destroy() 메서드를 오버라이딩해야 한다.
Spring은 Spring 종료 전에 해당 메서드를 실행시킨다.

이 방식은 Spring 전용 인터페이스에 의존하고 메서드 이름이 고정되어 있다.
또한 외부 라이브러리에 적용이 불가능해 사실상 사용하지 않는다.

@Bean(initMethod, destroyMethod)

class Service {
	
    public void initializationMethod() {
    	// 코드
    }
    
    public void closeMethod() {
    	// 코드
    }
}

class AppConfig {
	
    //  ** initMethod, destroyMethod 지정 **
    @Bean(initMethod = "initializationMethod", destroyMethod = "closeMethod")
    public Service service() {
    	return new Service();
    }

@Bean을 사용하여 객체를 수동으로 스프링 컨테이너에 등록할 때,
initMethod, destroyMethod element에 각각 메서드 이름을 선언하면
Spring은 의존 관계 주입이 끝나고 initMethod에 해당하는 메서드를,
Spring 종료 전에 destroyMethod에 해당하는 메서드를 각각 실행시킨다.

보통 소멸 메서드는 자원의 정리에서 사용된다.
자원 정리 메서드의 경우 대부분 close() 또는 shutdown() 메서드를 사용하는데,
destroyMethod는 close(), shutdown() 메서드를 추론하여 자동으로 호출할 수 있다.
따라서 메서드 이름이 close(), shutdown()인 경우,
destroyMethod에 별도로 선언하지 않아도 실행 가능하다.

이 방식은 인터페이스를 구현하던 이전 방식과는 달리
메서드 이름도 지정이 가능하고 클래스가 Spring 코드에 의존하지 않아
외부 라이브러리에도 적용 가능하다는 장점이 있다.

@PostConstruct, @PreDestroy

class Service {
	
    @PostConstruct  // ** 의존 관계 주입 후 호출 **
    public void initializationMethod() {
    	// 코드
    }
    
    @PreDestroy  // **  종료 직전 호출 **
    public void destroy() throws Exception {
    	// 코드
    }
}

메서드에 @PostConstruct 애노테이션을 선언하면
의존 관계 주입 직후 해당 메서드가 호출되어 실행된다.

메서드에 @PreDestroy 애노테이션을 선언하면
Spring 종료 직전 해당 메서드가 호출되어 실행된다.

이 방식의 경우 매우 간편하기 때문에 권장하는 방법이다.
또한 자바 표준 언어로 Spring 언어에 의존하지 않는다.
하지만 외부 라이브러리에는 적용이 불가능하기 때문에,
외부 라이브러리에 적용할 때에는 @Bean(initMethod, destroyMethod) 방법을 사용한다.

@PostConstruct와 @Transactional

// 불가
@PostConstruct
@Transactional
public void method() {
	// 메서드 바디
}


// 개선
@PostCounstruct
public void method() {
	Service service = new Service();
    service.method();
}

class Service {
	
    @Transactional
    public void method() {
    	// 메서드 바디
    }

이후에 배울 Transaction은 @PostConstruct 애노테이션과 동시에 선언할 수 없다.
이 경우 Transaction을 적용할 메서드를 별도의 클래스를 생성한 뒤 분리해서 사용한다.

Bean Scope

Spring 시작 시, 스프링 컨테이너가 생성되고 Spring Bean을 등록하면
보통은 Spring 종료 시까지 스프링 컨테이너에서 Bean이 관리된다.
하지만 변수에도 생명주기인 Scope가 있는 것처럼,
Bean에도 스프링 컨테이너에서 관리되는 생명주기인 Bean Scope가 있다.

Bean이 어디까지 살아남을 수 있을지 알아보자.

Singleton

스프링 컨테이너가 Bean을 관리하는 Bean Scope의 default 값이다.
스프링 종료까지 관리되기 때문에, 스프링 컨테이너는 싱글톤 컨테이너라고도 불린다.

Singleton Pattern

스프링 종료까지 생존하는 특징 외에도 싱글톤은
빈에 등록된 객체 요청 시 항상 동일한 객체를 반환한다는 특징이 있다.
여기서 클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴을
Singleton Pattern이라 한다.

static 키워드로 메서드 영역에 객체 생성

class Service {

	//  ** static 키워드로 객체 생성 **
	private static final Service service = new Service();
} 

클래스의 인스턴스가 1개만 생성되기 위해서는 인스턴스가 공유되어 관리되어야 한다.
그렇기 때문에 객체를 static 키워드로 선언하여 메서드 영역에서 관리하도록 한다.

static get() 메서드로 인스턴스 생성 없이 전역 접근

class Service {

	private static final Service service = new Service();
    
    //  ** static get 메서드 **
    public static Service getService() {
    	return service;
    }
} 

static 키워드로 생성한 인스턴스를 얻을 수 있는 get() 메서드를
static 키워드로 선언하여 전역에서 인스턴스 생성 없이
get() 메서드에 접근이 가능하도록 선언한다.

private 생성자

class Service {

	private static final Service service = new Service();
    
    public static Service getService() {
    	return service;
    }
    
    // ** private 생성자 **
    private Service() {
    }
} 

생성자를 private로 선언하여 외부에서 객체 생성이 불가능하도록 설정한다.

Singleton Pattern 단점

싱글톤 패턴을 직접 구현하기에는 객체마다 작성해야할 코드가 너무 많다.
또한 static 객체를 생성할 때, 구체 클래스에 의존하게 되어
OCP, DIP를 준수하지 못한 코드를 작성하게 된다.

Spring Container Singleton

Spring은 Bean을 기본적으로 Singleton으로 생성하는데,
Spring Container로 복잡한 코드 구현 없이 객체지향적으로 Singleton Pattern을 사용한다.
Spring을 시작할 때, Spring Container가 생성되고
Spring Bean을 등록하면서 생성된 객체를 스프링 컨테이너가 보관하고 있다가,
객체 요청 시 이미 만들어진 동일한 객체를 전달하여 1개의 인스턴스가 재사용되도록 한다.

class Service {

	String name;  // ** 필드가 있으면 안된다! **
    
    // 코드
} 

Singleton에서 주의해야할 점은 객체가 공유되기 때문에 필드가 없어야 한다!
내가 저장한 값이 다른 요청에서 변경될 수 있기 때문에,
싱글톤은 무상태(Stateless)로 필드 없이 만들어져야 한다.

@Bean, @Component 애노테이션으로 생성된 객체는 싱글톤을 보장하는데,
실제 객체가 아닌 CGLIB 기술이 사용된 Proxy 객체가 생성되어,
요청 시 Bean에 등록되어 있으면 반환, 없으면 생성 후 반환하여 싱글톤을 유지한다.
이렇게 싱글톤으로 객체를 생성하고 관리하는 것을 Singleton Registry라고 한다.

Prototype

Bean Scope에서 Prototype이란 의존 관계 주입 후 초기화 콜백까지
Spring Bean이 스프링 컨테이너에 생존하는 것을 의미한다.
그렇기 때문에 종료 전에 호출되는 소멸 메서드 콜백은 작동하지 않는다.

// Bean 자동 등록
@Component
@Scpoe("prototype")  // ** prototype Bean Scope 지정 **
class Service {
	// 코드
}

// Bean 수동 등록

class AppConfig {
	
    @Bean
    @Scope("prototype")  // ** prototype Bean Scope 지정 **
    public Service service() {
    	return new Service();
    }

Prototype으로 Bean Scope를 설정하고 싶은 경우,
@Component 또는 @Bean으로 Bean 등록 시에 @Scope("prototype")을 함께 선언한다.
prototype으로 설정한 Bean은 스프링 시작과 동시에 생성되는 것이 아닌,
Bean을 요청할 때 인스턴스를 생성하여 항상 새로운 객체를 반환한다.

Dependency Lookup(DL)

싱글톤, 프로토타입. 이렇게 이쁘게만 분리해서 생성하면 얼마나 좋을까.
하지만 싱글톤 Bean에서 Prototype Bean을 생성하는 상황이 있다고 해보자.
Singleton Bean은 Spring 시작할 때 생성되어 의존관계 주입도 수행된다.

내부에 Prototype Bean이 있는 경우 의존관계 주입을 위해 객체가 생성된 후 주입되어
Prototype으로 매번 새로운 객체가 생성되길 기대했지만,
Singleton 내부의 Prototype Bean은 동일한 객체가 유지되어 버린다!

이 문제를 어떻게 해결하면 좋을까?
이럴땐 의존관계를 외부에서 주입받는 것이 아닌, 직접 찾아야 한다.
이것을 의존관계 조회(DL, Dependency Lookup)라고 한다.

어떻게 DL을 수행할 수 있을지 그 방법을 살펴보자.

Spring Container 직접 요청

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

PrototypeBean pb = ac.getBean(PrototypeBean.class);

스프링 컨테이너 객체를 생성하고, getBean() 메서드를 통해
직접 PrototypeBean을 생성하여 사용한다.
하지만 이 방식은 Spring에 종속적인 코드이기 때문에 사실상 사용하지 않는다.

ObjectProvider

이전에는 ObjectFactory를 사용했으나 기능이 추가되어 ObjectProvider를 사용한다.
ObjectProvider란 스프링 컨테이너에서 Bean을 대신 찾아주는 DL 도구이다.

@Component
@Scope("prototype")
class PrototypeBean {
	// 코드
}

@Component
class Service {

	private final ObjectProvider<PrototypeBean> op;
	
    //  ** ObjectProvider 의존관계 생성자 주입 **
    @Autowired
    public Service(ObjectProvider<PrototypeBean> op) {
    	this.op = op;
    }
    
    public void prototypeBeanMethod() {
    	PrototypeBean pb = op.getObject();  // ** Prototype Bean 생성 **
}

ObjectProvider는 스프링 컨테이너에서 제공하는 기능으로 Bean으로 등록하지 않고
의존관계 주입을 통해 사용할 수 있다.
ObjectProvider의 getObject() 메서드를 통해 Prototype Bean을 생성할 수 있다.
하지만 이 방식은 Spring에 종속된 코드라는 단점이 있다.

jakarta.inject.Provider

Spring에 종속하지 않고 자바 표준 코드를 작성하는 방법이 있다.
바로 jakarta.inject.Provider를 사용하는 것이다.
이 방식을 사용하기 위해서는 build.gradle에 라이브러리를 추가해야 한다.

build.gradle

dependencies {
	implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
}

Provider 역시 스프링이 자동으로 구현하고 관리하여 Bean으로 등록할 필요가 없다.

@Component
@Scope("prototype")
class PrototypeBean {
	// 코드
}

@Component
class Service {

	private final Provider<PrototypeBean> provider;
	
    //  ** Provider 의존관계 생성자 주입 **
    @Autowired
    public Service(Provider<PrototypeBean> provider) {
    	this.provider = provider;
    }
    
    public void prototypeBeanMethod() {
    	PrototypeBean pb = prodier.get();  // ** Prototype Bean 생성 **
}

Provider을 생성자 주입을 통해 의존 관계를 주입한 뒤,
get() 메서드를 사용하면 Prototype Bean을 생성할 수 있다.

proxyMode

ObjectProvider이 Spring에 종속되어 Provider를 생성하였으나,
의존관계를 주입하기 위해 코드를 작성하는 것은 번거롭다.
proxyMode를 사용하면 Provider 없이도 DL을 수행할 수 있다.

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
class PrototypeBean {
	// 코드
    
    public void prototype() {
    	// 메서드 바디
    }
}

@Component
class Service {

	private final PrototypeBean pb;
    
    //  ** PrototypeBean 의존관계 생성자 주입 **
    @Autowired
    public Service(PrototypeBean pb) {
    	this.pb = pb;
    }
    
    public void prototypeBeanMethod() {
    	pb.prototype();
}

prototype으로 생성할 Bean의 @Scope 애노테이션에
proxyMode = ScopedProxyMode.TARGET_CLASS를 함께 선언하면
(인터페이스라면 ScopedProxyMode.INTERFACES 사용)
Provider과 같은 DL 도구 없이도 Prototype Bean을 의존관계 주입하여 사용할 수 있다.

proxyMode가 뭐길래 이게 가능한걸까?
proxyMode를 설정할 경우, 스프링 컨테이너는 PrototypeBean을
스프링 컨테이너에 CGLIB 기술을 사용하여 Proxy 객체로 등록한다.
그리고 요청이 왔을 때 prototype의 Bean을 생성하여 반환한다.

Provider이나 proxyMode 모두 Prototype Bean의 생성을
요청하는 시점까지 지연할 수 있다는 특징을 가지고 있다.

Web 관련 Bean Scope

Spring 시작, 초기화 메서드 호출 이후와 같이
Spring과 관련된 생명주기 외에 Web과 관련되어
스프링 컨테이너의 Bean 생명주기 Scope를 지정할 수 있다.

Web 관련 Bean Scope를 지정하려면 먼저 build.gradle에 라이브러리를 추가해야 한다.

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
}

@Scope 애노테이션과 함께 web 관련 Scope를 지정할 수 있다.

@Scope("request")  // ** request 요청 시작과 종료동안 생존
@Scope("session")  // ** HTTP Session과 동일하게 생존
@Scope("application")  // ** ServletContext와 동일하게 생존
@Scope("webSocket")  //  ** 웹 소켓과 동일하게 생존
class Service {
	// 코드
}

web 관련 Scope에는 request, session, application, webSocket이 있다.
web 관련 Scope도 prototype과 동일하게 proxyMode를 적용할 수 있다.

마무리

Spring을 사용하기 전 Spring 구조에 이어서
Spring 구조의 핵심이 되는 Bean에 대해 알아봤다.
Spring 시작, 종료 사이에서 Bean의 동작에 대해 이해하고,
Bean이 스프링 컨테이너에서 생존하는 Scope에 대해 유의하자.

profile
복습에 대한 비판과 지적을 부탁드립니다

0개의 댓글