07. 빈의 생명주기
- -
이번 포스트에서는 Spring이 관리하는 빈의 생명주기(Life Cycle)에 대해 살펴보자.
빈의 생명 주기
빈의 생명 주기
스프링의 빈은 다음과 같은 생명 주기를 갖는다.
- 생성자를 호출하여 객체를 생성한다.
- 1의 과정을 통해 생성자 주입을 하거나 별도의 setter 메서드를 이용해서 의존성을 주입한다.
- 아직 빈을 활용하기 이전에 초기화 메서드를 실행한다. 초기화 메서드에서는 빈에서 사용하려는 자원을 초기화 할 수 있다.
- 빈을 사용한다.
- 빈을 다 사용한 후 소멸 메서드를 실행한다. 소멸 메서드에서는 빈에서 사용했던 자원의 정리 작업을 진행할 수 있다.
초기화 및 소멸 메서드
초기화 메서드와 소멸 메서드에 대해 살펴보자.
초기화 메서드 | 소멸 메서드 | |
주요 용도 | 빈에서 사용하려는 자원의 초기화 | 빈에서 사용한 자원의 정리 |
호출 시점 | 의존성 주입 후 비지니스 로직이 호출되기 전 | 모든 비지니스 로직이 종료된 후 |
작성 방법 | 묵시적 빈 등록: @PostConstruct 적용 명시적 빈 등록: @Bean의 initMethod 속성 |
묵시적 빈 등록: @PreDestroy 적용 명시적 빈 등록: @Bean의 destroyMethod 속성 |
주의사항 | 메서드는 파라미터가 없어야 한다. |
다음은 묵시적 빈 등록에서는 @PreDestory(소멸 메서드), @PostConstruct(초기화 메서드)를 사용한 예이다.
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Component
@RequiredArgsConstructor
@Slf4j
public class CoffeeShop {
private final Barista barista;
@PostConstruct
public void init() {
log.debug("오늘 장사 시작: 집기 준비, 재료 준비");
}
@PreDestroy
public void destroy() {
log.debug("오늘 장사 마감: 정리하고 정산하고~");
}
public Coffee orderCoffee(String client, String menu) {
return barista.makeCoffee(client, menu);
}
}
만약 명시적 빈 등록을 사용하는 경우는 @Bean의 initMethod 속성과 destroyMethod 속성을 사용해서 메서드를 지정한다.
@Bean(initMethod = "init", destroyMethod = "destroy")
public CoffeeShop coffeeShop(Barista barista) {
return new CoffeeShop(barista)
}
또는 빈에서 InitializingBean과 DisposableBean 인터페이스를 구현하고 필요한 메서드를 재정의할 수도 있다.
@Component
@Slf4j
public class CoffeeShop implements InitializingBean, DisposableBean {
@Override // @PostConstruct 대용
public void afterPropertiesSet() {
log.debug("오늘 장사 시작: 집기 준비, 재료 준비");
}
@Override // @PreDestroy 대용
public void destroy() {
log.debug("오늘 장사 마감: 집기 정리하고 정산하고~");
}
}
prototype 빈의 초기화/소멸메서드
앞서 prototype 빈을 사용하는 목적중의 하나로 스프링의 빈이 되면 스프링의 편의 기능을 사용할 수 있다고 했는데 위와 같은 초기화 메서드 지원도 그 예이다. 이때 주의 사항은 prototype 빈은 객체 생성, 의존성 주입 후 초기화 메서드까지만 container가 관리한다는 점이다. 따라서 소멸 메서드는 직접 호출해 줘야 한다.
다음의 코드는 Coffee가 빈으로 만들어지면 자동으로 손님에게 서비스가 진행된다.
@Data
@Component
@Scope("prototype")
@Slf4j
public class Coffee {
private String client;
private String coffee;
public Coffee(@Value("${null}") String client,@Value("${null}") String menu) {
this.client = client;
this.menu = menu;
}
@PostConstruct
public void setup() {
log.debug("컵에 예쁘게 담아서 나갑니다.");
}
// @PreDestroy: prototype 빈은 빈 생성 --> 의존성 주입 --> @PostConstruct 까지만 진행하고
// 따라서 소멸자는 명시적 호출 필요
public void cleanUp() {
log.debug("설겆이 필요하죠");
}
}
이제 두 잔의 커피를 시켜보면 CoffeeShop의 생성과 소멸에 필요한 일은 추가적으로 코드가 없지만 자동으로 처리되고 prototype 빈인 Coffee의 소멸 메서드는 명시적으로 호출해야 함을 알 수 있다.
@Test
public void makeCoffeeTest() {
Coffee coffee1 = shop.orderCoffee("hong", "latte");
coffee1.cleanUp(); // prototpye의 소멸자는 명시적 호출
Coffee coffee2 = shop.orderCoffee("hong", "latte");
coffee2.cleanUp();
}
게으른 초기화(지연 초기화)
Spring Container는 시작하면서 등록된 모든 빈에 대한 초기화를 진행한다.
우리 CoffeeShop은 오픈하긴 했는데 아직 인지도가 없어서 손님이 없는 상황이다. 이때 Barista를 미리 고용해 놓고 있다면 인건비만 나갈 뿐이다. 처음 손님이 커피를 주문할 때 Barista를 고용하면 어떨까?
이처럼 초기화의 시점을 최초 애플리케이션이 실행할 때가 아니라 그 빈이 필요할 때 처리하는 것을 게으른 초기화라고 하고 이를 위해 @Lazy를 사용한다. 게으른 초기화를 사용하면 리소스를 효율적으로 관리하고 애플리케이션의 시작 시간을 단축시킬 수 있다.
@Component
@Scope("singleton")
@RequiredArgsConstructor
@Lazy // 이제 Barista가 필요한 시점에 빈을 생성한다.
@Slf4j
public class Barista {
private final ApplicationContext ctx;
@PostConstruct
public void inti(){
log.debug("바리스타 준비 완료!");
}
}
하지만 지금은 Barista를 게으르게 만들어도 부지런하게 동작한다. 왜냐하면 CoffeeShop이 Barista를 의존성으로 갖고 있고 결국 CoffeeShop이 만들어질 때부터 Barista가 필요한 상황이 되어버린다.
이런 경우에는 ObjectProvider<T>라는 interface를 사용할 수 있다. ObjectProvider<T>는 특정 타입의 빈을 지연 방식으로 검색하고 주입받는다.
@Component
@Data
@Scope("singleton")
public class CoffeeShop {
private final ObjectProvider<Barista> baristaProvider; // 객체가 없을 수도 있다.
// do something
public Coffee orderCoffee(String name) {
return baristaProvider.getObject().makeCoffee(name); // provider를 통해 Baristar를 가져온다.
}
}
이제 비용이 비싼 Barista는 주문이 없을 때는 동작하지 않는 것을 알 수 있다.
@Test
public void coffeeShopTest() {
assertNotNull(shop);
}
오늘 장사 시작: 집기 준비, 재료 준
// 다른 로그들
오늘 장사 마감: 정리하고 정산하고~
게으른 초기화는 얼핏 효율적으로 보이지만 너무 무거운 빈을 애플리케이션 동작 중에 초기화 시킨다면 오히려 성능에 문제가 될 수도 있다. 역시 상황에 따라 사용해야 한다.
애플리케이션 라이프사이클
애플리케이션 실행 후 처리
간혹 애플리케이션 시작 시점에 특정 작업을 수행 해야 할 때가 있다. 예를 들어 빈들의 상태를 체크하거나 빈들이 동작하기 전에 배치작업을 수행하는 경우를 생각해볼 수 있다. 이를 위해 모든 빈들이 만들어지고 아직 서비스하기 전에 동작하고 싶다면 해당 코드를 어디다 작성해야 할까?
쉽게 떠오르는 부분은 프로그램의 출발점인 main 메서드이다. 그런데 main 메서드는 static 영역이기 때문에 생성된 빈을 주입받아서 사용하는 등의 작업이 불가능하다.
이런 경우 ApplicationRunner나 CommandLineRunner 인터페이스를 활용할 수 있다. 이 interface들의 콜백은 모든 스프링 빈들이 로드된 후에 동작하는데 실행시 main 메서드로 전달된 argument를 받아서 활용할 수도 있다.
java -jar myapp.jar --spring.profiles.active=dev arg1 arg2
이때 전달되는 argument는 두 종류로 구분할 수 있다.
- option arguments: 일반적으로 -로 시작하며 key=value 형태로 옵션 정보를 전달하는 용도
- non-option arguments: 단순한 입력 값
CommandLineRunner
CommandLineRunner는 String ... args를 받는 run 메서드 하나를 갖는 @FunctionalInterface이다. 따라서 다음과 같이 생성하면서 빈으로 등록할 수 있다. 이때 args에는 main 메서드에 전달되는 argument 들이 전달되는데 특별히 구분하지 않는다.
@Bean
public CommandLineRunner cRunner() {
return args -> {
// --spring.profiles.active=dev arg1 arg2
System.out.println(" - args: " + Arrays.toString(args));
};
}
ApplicationRunner
ApplicationRunner는 CommandRunner와 비슷한데 다만 파라미터로 ApplicationArguments를 받는 점이 다르다. ApplicationArguments는 option args와 non-option args를 구분해서 사용할 수 있다.
@Bean
public ApplicationRunner runner() {
return (args) -> {
// --spring.profiles.active=dev
System.out.println(" - option args: " + args.getOptionNames());
// arg1 arg2
System.out.println(" - non option args: " + args.getNonOptionArgs());
};
}
@Order
만약 여러 개의 Runner를 사용한다면 @Order를 이용해서 동작 순서를 지정할 수 있다. 하지만 순서 기반으로 동작하는 애플리케이션을 구성하는 것은 바람직하지 않다.
@Bean
@Order(2)
public CommandLineRunner cRunner() {
return args -> {
// do something
};
}
@Bean
@Order(1)
public ApplicationRunner runner() {
return (args) -> {
// do something
};
}
'Spring Core > 02. Spring Container와 빈 관리' 카테고리의 다른 글
06. 빈의 스코프 (0) | 2024.02.22 |
---|---|
05. Profile (0) | 2020.06.17 |
04. DI 처리 - 묵시적 DI (0) | 2020.06.17 |
03. DI 처리 - 명시적 DI (0) | 2020.06.16 |
02. Dependency와 Dependency Injection (0) | 2020.06.15 |
소중한 공감 감사합니다