우당탕탕 삽질기

Read-Only 사내 라이브러리 component와의 빈 생성 순서 조절하기

해리리_ 2023. 7. 2. 23:07

들어가는 말


최근 회사에서 solid 원칙을 적용하여 리팩터링을 진행했다. 워낙 의존성이 꼬여 있어서 무언가 로직을 바꾸기 보다는 최대한 기존의 로직을 유지하면서 진행해야 했다. 그래서 기존 로직을 유지한다는 요구사항을 만족하지만 더 적합한 기술(?)로 바꾸는 작업만 했는데 그 과정에서 직면한 문제가 있었다. 일단 문제에 들어가기 앞서 스프링 빈 콜백에 대해 훑어보고 들어가자.

 

스프링 빈 콜백 리마인드


스프링 빈으로 등록되는 클래스 안에서 어떤 멤버변수가 딱 한번만 초기화되게 하려면 어떻게 해야할까? 일단 스프링 빈은 기본적으로 싱글톤이기 때문에 딱 한번만 초기화해놓고 계속 그때 초기화된 그대로 멤버변수를 반환하는 것이 가능하다.

 

기존 로직은 아래처럼 매번 그 멤버변수의 size를 확인하여 원하는 값으로 초기화되었는지를 확인하고 이를 그대로 반환하는 방식이었다. 의도는 결국 저 멤버변수를 초기화하기 위한 redis 커넥션이 있어서 이 빈도를 줄이기 위해 최초에 한번만 세팅하려는 것이었다. 이걸 기존의 방식으로 유지하자니 좀 찝찝해서 더 적합한 다른 방식으로 바꿨다.

@Service
public class Test {
    private Set<String> stringSet = Collections.emptySet();
    
    public Set<String> getStringSet() {
    	if(stringSet.size > 0) {
        	return stringSet;
        }
        stringSet = redisConnection.getStringSet();
        return stringSet;
    }
}

 

스프링의 싱글톤 빈이 최초로 컨테이너에 등록되고 의존관계까지 주입된 이후에 이 멤버변수를 초기화하는 방법은 두 가지다.

 

  1. 생성자로 의존성 주입할 때 내부 멤버변수도 같이 초기화시키자. 그럼 빈 생성될 때 딱 한번만 redis의 값을 가져온 뒤 쭉 들고 있을 수 있음.
  2. 하지만 생성자는 필수 파라미터 정보를 받고 객체를 생성하는 책임을 진다. 내부 멤버변수를 초기화하는 책임은 분리하고 싶다. 스프링 콜백을 활용하자.

 

스프링 빈 생명주기 콜백을 알고는 있었지만 실제로 자주 쓰진 않았어서 좀 늦게 떠올리게 됐다.

 

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


스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 콜백 (빈 생성 & 의존관계 주입 끝났을 때 호출) → 빈 사용 → 소멸 전 콜백 → 스프링 종료

 

스프링은 의존관계 주입이 완료되었을 때 호출되는 콜백 메소드를 지원한다.

 

  • 인터페이스 (InitializingBean, DisposableBean)
  • 설정정보에 어노테이션 선언할 때 설정 @Bean(initMethod = "init", destroyMethod = "close")
  • @PostConstruct, @PreDestroy
    • 초기화 콜백 : 지금 이 빈 생성 됐고 의존관계도 주입 완료됐음을 알려줌.
    • 최신 스프링이 권장하는 방식.

 

문제 발생


그래서 콜백으로 빈 등록 & 의존관계 주입이 끝난 뒤에 초기화를 잘 해내는 줄 알았는데, 그 초기화 메소드 안에서 호출하는 외부 컴포넌트(회사 자체 라이브러리)에서 문제가 발생했었다. 회사 코드라 여기에는 못 쓰고 상황 묘사만 하자면 내 @Service가 외부 라이브러리의 @Component 클래스의 한 메소드를 호출하는데 그 외부 라이브러리의 @Component가 아직 생성되지 않은 상태였다. 

 

  • 내 @Service가 먼저 컴포넌트 스캔이 됨. 그 이후에 외부 라이브러리 @Component가 스캔됨.
  • 내 @Service의 @PostConstruct 메소드에서 외부 @Component를 참조함.
  • 외부 @Component가 생성되기 전이라 NPE 발생

 

더 자세하게 풀자면 redis에서 값을 가져오는 일은 모두 숙박대전 스위치가 켜져 있을 때에 실행된다.

  • 요구사항 : 숙박대전 스위치를 확인해서 켜져있다면 → 딱 한번만 redis의 값을 가져와서 내 멤버변수에 저장해두고 싶다.

숙박대전은 11번가 외부 라이브러리를 참조하는 Sauron 스위치를 활용한다. DB에 있는 값임.

로컬에서도 스위치는 계속 true 상태인데 시점에 따라 Y / N 여부가 달랐다.

  • 스프링이 다 올라오고 나서 postman으로 스위치 상태를 호출하면 스위치가 true 이지만
  • 빈 생성 시점에는 스위치가 false라고 나옴. 😱

 

원인


내 @Service가 외부 라이브러리의 @Component 클래스의 한 메소드를 호출하는데 그 외부 라이브러리의 @Component가 아직 생성되지 않은 상태였다.

 

외부 라이브러리는 gradle에 의존성으로 넣어둔 것이고, 이렇게 gradle로 넣어준 외부 의존성의 컴포넌트 스캔이 먼저 될 거라 생각했어서 더 의아했다. 그런데 보니까 외부 @Component가 속한 패키지가 내 @Service보다 더 하위에 있었다. 컴포넌트 스캔은 basePackage를 먼저 스캔하고 그 이후로 하위패키지를 쭉 스캔한다고 한다. 그래서 basePackage에 있는 내 @Service가 먼저 스캔되어 빈 등록이 먼저 되고 바로 콜백이 실행된 것이었다.

https://reflectoring.io/spring-component-scanning/

 

내가 원하는 것


내가 만든 컴포넌트(@Service)가 특정 외부 라이브러리 Component 보다 늦게 생성됐으면 좋겠다! 정상적으로 참조할 수 있게.

외부 Component 생성 → 그 이후에 내 Service 생성

근데 외부 라이브러리가 read-only 라서 건드릴 수가 없음. 이걸 안 건드리고 빈 생성 시점을 조절할 수 있을까?

  1. 외부 Component 생성, 의존관계 주입이나 초기화 메소드 완료.
  2. 내 Service 생성, 의존관계 주입 완료
  3. 내 Service의 초기화 콜백 호출 (PostConstruct)
  4. 정상적으로 외부 Component의 메소드를 호출

 

해결: 외부 컴포넌트가 더 늦게 생성되게 하는 법


일단 여러가지 빈 생성 시점을 조절할 수 있는, 뭔가 내 Service의 생성을 늦출 수 있을 것 같은 어노테이션을 알아봤다.

 

@Lazy

  • 누군가 필요로 하는 시점에 생성함. 지금 원하는 요구사항과 무관한데 이름 땜에 한번 들춰봄.

@Order

  • 특정 빈에 Order(1), Order(-23) 등을 붙여서 생성 순서 조절 가능. 음수도 설정 가능
  • 숫자가 커야 우선순위가 낮으니까 내 Service가 외부 컴포넌트보다 value가 커야 함. 제일 커야 함.
  • 내 Service의 우선순위를 낮추려고 아무리 큰 값을 설정하더라도 default가 제일 큰 값이이니까 뭘 해도 외부 컴포넌트의 value가 더 큼. 즉 외부 컴포넌트의 우선순위가 더 낮아서 결국 내 service가 먼저 생성됨.
  • 이건 순서를 컨트롤하려는 두 Bean 모두에 write할 수 있어야 하는데 외부 component는 read-only라 불가능

@DependsOn("외부 컴포넌트이름") 

  • 외부 컴포넌트 이름을 넣으면 그 컴포넌트 생성 이후에 만들어진다는 의미.
  • 이러면 위에서 의도한 순서대로 잘 된다.

ConditionalOnBean의 elements.

 

이번에 한번 더 확인한 것들


@Component, @Controller, @Service, @Repository

다들 기능 상 차이가 없 기능 상 차이가 있는 것도 있고 아닌 것도 있었다.

  • component와 service는 기능 상 차이는 없다.
  • controller는 component 기능 + 추가 기능이 있다.
    • controller는 component와는 다르게 url과 클래스를 이어주는 @RequestMapping의 기능을 추가로 가지고 있다.
    • 누가 친절히 실험해주신걸 읽어보니 controller 자리에 @Component로 대체해서 사용했더니 404 에러가 나고, @Component + @RequestMapping를 썼더니 잘 나왔다.
  • repository는 component 기능 + 추가 기능이 있다.
    • 아레 첫번째 사진에서는 it activates persistence exception translation for all beans annotated with @Repository 라고 되어 있다.
    • 그리고 두번째 사진에는 non-spring exception을 spring exception으로 자동으로 전환해주기 위해서는 @Respository 어노테이션이 필요하다고 한다. 그래야 PersistenceExceptionTranslationProcessor가 persistence exception translation을 해서 원래 스프링 예외가 아닌 애를 spring의 DataAccessException으로 번역해준다고 한다.

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

repository 설명, 블로그에서 가져옴

참고

https://javarevisited.blogspot.com/2017/11/difference-between-component-service.html#axzz86HwlLlf2

 

Difference between @Component, @Service, @Controller, and @Repository in Spring

A blog about Java, Programming, Algorithms, Data Structure, SQL, Linux, Database, Interview questions, and my personal experience.

javarevisited.blogspot.com

https://velog.io/@soup930/Component%EC%99%80-Controller%EC%9D%98-%EC%B0%A8%EC%9D%B4


빈 등록, 의존성 주입, 콜백

다수의 빈이 있고 각 빈의 의존관계 주입 방식이 BeanA는 생성자 주입, BeanB는 setter 주입, BeanC도 setter 주입이라면 아래 순서였는데 한번 더 확인할 예정...

  1. BeanA 생성과 의존성 주입이 동시에 이뤄짐.
  2. BeanA에 PostConstruct가 있다면 호출됨.
  3. BeanB 생성 (생성자 호출)
  4. BeanB 의존관계 주입 (setter 호출)
  5. BeanB에 PostConstruct가 있다면 호출됨.
  6. BeanC 생성 (생성자 호출)
  7. BeanC 의존관계 주입 (setter 호출)
728x90