Spring Core/02. Spring Container와 DI

07. 빈의 생명주기

  • -
반응형

이번 포스트에서는 Spring이 관리하는 빈의 생명주기(Life Cycle)에 대해 살펴보자.

빈의 생명 주기

 

빈의 생명 주기

스프링의 빈은 다음과 같은 생명 주기를 갖는다.

  1. 생성자를 호출하여 객체를 생성한다.
  2. 1의 과정을 통해 생성자 주입을 하거나 별도의 setter 메서드를 이용해서 의존성을 주입한다.
  3. 아직 빈을 활용하기 이전에 초기화 메서드를 실행한다.
  4. 빈을 사용한다.
  5. 빈을 다 사용한 후 소멸 메서드를 실행한다.

 

초기화 및 소멸 메서드

초기화 메서드와 소멸 메서드에 대해 살펴보자.

  초기화 메서드 소멸 메서드
주요 용도 빈에서 사용하려는 자원의 초기화 빈에서 사용한 자원의 정리
호출 시점 의존성 주입 후 비지니스 로직이 호출되기 전 모든 비지니스 로직이 종료된 후
작성 방법 명시적 빈 등록: @PostConstruct 적용 
묵시적 빈 등록: @Bean의 initMethod 속성
명시적 빈 등록: @PreDestroy 적용 
묵시적 빈 등록: @Bean의 destroyMethod 속성
주의사항 메서드는 파라미터가 없어야 한다.

다음은 묵시적 빈 등록에서는 @PreDestory(소멸 메서드), @PostConstruct(초기화 메서드)를 사용한 예이다.

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@Data
@Component
@Slf4j
public class CoffeeShop {
  private final Barista barista;

  @PostConstruct
  public void setup() {
    log.debug("오늘 장사 시작: 집기 준비, 재료 준비");
  }

  @PreDestroy
  public void cleanup() {
    log.debug("오늘 장사 마감: 정리하고 정산하고~");
  }

  public Coffee orderCoffee(String client, String name) {
    return barista.makeCoffee(client, name);
  }
}

또는 빈에서 InitializingBean과 DisposableBean 인터페이스를 구현하고 필요한 메서드를 재정의할 수도 있다.

@Component
@Slf4j
public class CoffeeShop implements InitializingBean, DisposableBean {
	  
  @Override          // @PostConstruct 대용
  public void afterPropertiesSet() {
    log.debug("늘 장사 시작: 집기 준비, 재료 준비");
  }
  
  @Override          // @PreDestroy 대용 
  public void destroy() {
    log.debug("오늘 장사 마감: 집기 정리하고 정산하고~");
  }
}

만약 명시적 빈 등록을 사용하는 경우는 @Bean의 initMethod 속성과 destroyMethod 속성을 사용해서 메서드를 지정한다.

@Bean(initMethod = "setup", destroyMethod = "cleanup")
public CoffeeShop coffeeShop(Barista barista) {
  return new CoffeeShop(barista)
}

 

앞서 prototype 빈을 사용하면 스프링의 편의 기능을 사용할 수 있다고 했는데 위와 같은 초기화 메서드 지원도 그 예이다. 다음의 코드는 Coffee가 빈으로 만들어지면 자동으로 손님에게 서비스가 진행된다. 이때 주의 사항은 prototype 빈은 객체 생성, 의존성 주입 후 초기화 메서드까지만 container가 관리한다는 점이다. 따라서 소멸 메서드는 직접 호출해 줘야 한다.

@Data
@Component
@Scope("prototype")
@Slf4j
public class Coffee {
  // 어떤 손님이 주문한 어떤 커피 : Coffee의 상태
  private final String client;
  private final String coffee;

  @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
@Lazy       // 이제 Barista가 필요한 시점에 빈을 생성한다.
@Slf4j
public class Barista {
    private final ApplicationContext ctx;

    public Barista(ApplicationContext ctx){ // 로그 출력을 위해 직접 생성자 생성
        this.ctx = ctx;
        log.debug("바리스타 준비 완료");
    }
}

기존의 CoffeeShop은 Barista를 의존성으로 갖는다. 결국 처음부터 필요한 상황이 되어버린다. 이런 상황에서 ObjectProvider<T> interface를 사용할 수 있다.

@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를 가져온다.
  }

}

 하지만 너무 무거운 빈을 애플리케이션 동작 중에 초기화 시킨다면 오히려 성능에 문제가 될 수도 있다.

다시 빈들의 동작을 테스트 해보자.  만약 CoffeeShop 만 만들고 orderCoffee를 호출하지 않으면 이제 Baristar는 생성되지 않는다.

@Test
public void coffeeShopTest() {
  assertNotNull(shop);
}
오늘 장사 시작: 집기 준비, 재료 준
// 다른 로그들
오늘 장사 마감: 정리하고 정산하고~

orderCoffee를 호출하면 비로소 Baristar가 생성되고 Coffee를 만드는 것을 볼 수 있다.

 

애플리케이션 라이프사이클

 

애플리케이션 실행 후 처리

간혹 애플리케이션 시작 시점에 특정 작업을 수행할 때가 있다. 예를 들어 애플리케이션의 상태를 체크하거나 배치작업을 수행하는 경우를 생각해볼 수 있다. 이를 위해 Spring이 생성한 빈들을 이용하고 싶다. 이런 코드는 어디다 작성해야 할까?

프로그램의 출발은 main 메서드이다. 그런데 main 메서드는 static 영역이기 때문에 생성된 빈을 주입받아서 사용하는 등의 작업이 불가능하다.

이런 경우 ApplicationRunner나 CommandLineRunner  인터페이스를 활용할 수 있다. 이 interface들의 콜백은 모든 스프링 빈들이 로드된 후에 동작하는데 실행시 main 메서드로 전달된 argument를 받아서 활용할 수 있다.

java -jar myapp.jar --spring.profiles.active=dev arg1 arg2

이때 --spring.profiles.active=dev를 option arguments라고 하고 arg1 arg2는 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와 DI' 카테고리의 다른 글

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
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.