Read-Only 사내 라이브러리 component와의 빈 생성 순서 조절하기
들어가는 말
최근 회사에서 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;
}
}
스프링의 싱글톤 빈이 최초로 컨테이너에 등록되고 의존관계까지 주입된 이후에 이 멤버변수를 초기화하는 방법은 두 가지다.
- 생성자로 의존성 주입할 때 내부 멤버변수도 같이 초기화시키자. 그럼 빈 생성될 때 딱 한번만 redis의 값을 가져온 뒤 쭉 들고 있을 수 있음.
- 하지만 생성자는 필수 파라미터 정보를 받고 객체를 생성하는 책임을 진다. 내부 멤버변수를 초기화하는 책임은 분리하고 싶다. 스프링 콜백을 활용하자.
스프링 빈 생명주기 콜백을 알고는 있었지만 실제로 자주 쓰진 않았어서 좀 늦게 떠올리게 됐다.
스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 콜백 (빈 생성 & 의존관계 주입 끝났을 때 호출) → 빈 사용 → 소멸 전 콜백 → 스프링 종료
스프링은 의존관계 주입이 완료되었을 때 호출되는 콜백 메소드를 지원한다.
- 인터페이스 (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 라서 건드릴 수가 없음. 이걸 안 건드리고 빈 생성 시점을 조절할 수 있을까?
- 외부 Component 생성, 의존관계 주입이나 초기화 메소드 완료.
- 내 Service 생성, 의존관계 주입 완료
- 내 Service의 초기화 콜백 호출 (PostConstruct)
- 정상적으로 외부 Component의 메소드를 호출
해결: 외부 컴포넌트가 더 늦게 생성되게 하는 법
일단 여러가지 빈 생성 시점을 조절할 수 있는, 뭔가 내 Service의 생성을 늦출 수 있을 것 같은 어노테이션을 알아봤다.
@Lazy
- 누군가 필요로 하는 시점에 생성함. 지금 원하는 요구사항과 무관한데 이름 땜에 한번 들춰봄.
@Order
- 특정 빈에 Order(1), Order(-23) 등을 붙여서 생성 순서 조절 가능. 음수도 설정 가능
- 숫자가 커야 우선순위가 낮으니까 내 Service가 외부 컴포넌트보다 value가 커야 함. 제일 커야 함.
- 내 Service의 우선순위를 낮추려고 아무리 큰 값을 설정하더라도 default가 제일 큰 값이이니까 뭘 해도 외부 컴포넌트의 value가 더 큼. 즉 외부 컴포넌트의 우선순위가 더 낮아서 결국 내 service가 먼저 생성됨.
- 이건 순서를 컨트롤하려는 두 Bean 모두에 write할 수 있어야 하는데 외부 component는 read-only라 불가능
@DependsOn("외부 컴포넌트이름")
- 외부 컴포넌트 이름을 넣으면 그 컴포넌트 생성 이후에 만들어진다는 의미.
- 이러면 위에서 의도한 순서대로 잘 된다.
이번에 한번 더 확인한 것들
@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/
참고
https://javarevisited.blogspot.com/2017/11/difference-between-component-service.html#axzz86HwlLlf2
https://velog.io/@soup930/Component%EC%99%80-Controller%EC%9D%98-%EC%B0%A8%EC%9D%B4
빈 등록, 의존성 주입, 콜백
다수의 빈이 있고 각 빈의 의존관계 주입 방식이 BeanA는 생성자 주입, BeanB는 setter 주입, BeanC도 setter 주입이라면 아래 순서였는데 한번 더 확인할 예정...
- BeanA 생성과 의존성 주입이 동시에 이뤄짐.
- BeanA에 PostConstruct가 있다면 호출됨.
- BeanB 생성 (생성자 호출)
- BeanB 의존관계 주입 (setter 호출)
- BeanB에 PostConstruct가 있다면 호출됨.
- BeanC 생성 (생성자 호출)
- BeanC 의존관계 주입 (setter 호출)