들어가기 전에
때는 작년 어느 날, CloudWatch에 요청 및 응답 로그를 적재하도록 구현하고 로컬에서 테스트를 모두 마친 후 코드를 업로드 한 날이었다. 메인에 머지가 되면서 자동으로 서버에 배포가 됐고 잠시 숨을 돌리던 중 들려온 말.
"배포 에러났는데요?"
아니, 에러가 났다고? 내가 테스트 할 때 뭘 놓쳤나? 바로 보내주신 에러 로그를 확인했다.
[ERROR] [main] org.springframework.boot.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name '' // 이하 생략
확실히 로컬에서는 보지 못했던 에러였다. 서버에 배포되면서 빈의 초기화 순서가 바뀌었는데 그게 내 코드에 영향을 준 것이다. 어떤 이유로 초기화 순서가 바뀌었는지 정확히 파악하지는 못했지만 이런 배포 실수는 내지 말아달라는 따끔한 피드백을 받고 그 다음부터는 빈의 생성 순서까지 생각하며 코드를 작성하게 됐다.
그래서 이 글에서는 빈의 개념부터 초기화 순서를 제어하는 방법 그리고 내가 겪은 트러블 슈팅 경험에 대해 적어보고자 한다.
빈이 도대체 뭔데?
개발자가 구현한 클래스가 어떤 과정을 통해 스프링이 인식할 수 있게 되는 순간 바로 빈이 된다. 이미 스프링 프레임워크에 익숙하다면 @Component, @Service와 같이 스프링이 해당 클래스의 존재를 알 수 있게 해주는 어노테이션을 많이 사용해왔을 것이다. 기본적으로 이렇게 빈이 등록되면 스프링은 개발자가 신경쓰지 않아도 알아서 각 빈들의 생명 주기를 관리하고 초기화 순서를 조정해 준다.
스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(Bean)이라고 부른다.
스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다.
<토비의 스프링 3.1> 1장 오브젝트와 의존관계 중
참고로 쉽게 스프링이라고 표현했지만 정확한 명칭은 스프링 컨테이너 또는 애플리케이션 컨텍스트이다. 별도의 설정 정보들을 통해 해당 작업들을 수행하게 되며 이렇게 프레임워크에 의해 관리되는 것을 제어의 역전(IoC)이라고 한다.
때에 따라 나의 경우처럼 개발자가 직접 빈의 초기화 순서를 지정해줘야 하는 일이 생기는데 @DependsOn 어노테이션을 활용하는 방법을 소개하고자 한다.
@DependsOn
빈의 초기화 순서를 커스텀하기 위해 제공되는 어노테이션이다. 스프링에 등록된 빈이라면 이 어노테이션을 통해 각 클래스들 간의 의존 관계를 명시해 줄 수 있기 때문에 초기화 순서를 제어할 수 있다.
[1] 간단 사용 예제
예를 들어, VariableInitializer라는 클래스를 만들고 빈으로 등록했다고 하자.
@Component
public class VariableInitializer {
// 구현 코드
}
다음으로 CommonClient 클래스를 만들어 내부에서 VariableInitializer의 값을 사용하려고 한다.
@Component
@DependsOn("variableInitializer")
public class CommonClient {
// 구현 코드
}
이때, @DependsOn에 의존하고 있는 VariableInitializer를 명시해주면 반드시 해당 클래스가 먼저 초기화 된 후에 CommonClient 클래스가 로드되는 것을 보장할 수 있기 때문에 빈 생성 순서에 의해 발생하는 문제점을 방지할 수 있게 되는 것이다.
만약, 의존하고 있는 클래스가 두 개 이상이라면 아래와 같이 쓸 수 있다는 점을 참고하자.
@DependsOn({"variableInitializer", "commonVariable"})
[2] 주의할 점
❶ 스프링에 등록된 빈만을 제어할 수 있다.
앞서 설명했듯이 스프링이 인식할 수 있도록 어노테이션을 선언한 클래스만이 이 어노테이션에 의해 순서를 제어할 수 있다.
❷ 순환 의존성 문제를 주의해야 한다.
서비스 단에서도 잘못 관계를 설정하다보면 순환 참조 문제가 발생하는 것처럼 순환 의존성 문제가 발생할 수 있다. 빈 초기화 순서를 입맛대로 제어한다 하더라도 그 과정 중에 빈들의 의존이 순환이 되어버리면 아예 초기화가 이루어지지 않기 때문에 의존 관계를 명확히 파악하고 사용해야 한다.
나의 트러블 슈팅 경험기
[1] 의존 관계에 있던 클래스 정보
내가 구현한 코드에서 의존 관계에 포함된 클래스는 다음과 같다.
A -> B -> C
A 클래스
yml 파일에서 설정 값들을 @Value를 통해 가져오고 전역 변수 선언 및 초기화를 수행하는 클래스이다.
public static String cloudWatchAccessKeyId; // AWS CloudWatch Access Key ID
@Value("${aws.cloudwatch.access-key-id}")
public void setCloudWatchAccessKeyId(String cloudWatchAccessKeyId) {
A.cloudWatchAccessKeyId = cloudWatchAccessKeyId;
}
B 클래스
A 클래스의 변수 정보를 통해 CloudWatch에 접근하기 위한 객체(이하 CloudWatchLogsClient)를 생성하는 클래스이다.
@Configuration
public class B {
@Bean
public CloudWatchLogsClient cloudWatchLogsClient() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
A.cloudWatchAccessKeyId, A.cloudWatchSecretAccessKey
);
// 생략
}
}
C 클래스
B 클래스에 생성한 CloudWatchLogsClient를 통해 로그를 적재하는 기능을 구현한 클래스이다.
@Service
@RequiredArgsConstructor
@Slf4j
public class C {
private final CloudWatchLogsClient cloudWatchLogsClient;
// 생략
}
[2] 에러 로그 파악하기
다시 에러 로그를 살펴보면 결국 C 클래스를 빈으로 생성하지 못해 UnsatisfiedDependencyException이 발생한 문제임을 알 수 있다. 해당 예외는 의존성 주입이 실패했다는 것인데 알다시피 C 클래스 내부에 의존성으로 주입받는 객체는 딱 하나, B 클래스에서 생성하는 CloudWatchLogsClient였다.
[ERROR] [main] org.springframework.boot.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'C클래스' defined in URL ... //생략
그러니까 이 문제는 A 클래스의 설정 값을 B 클래스에서 활용하지 못해 CloudWatchLogsClient 객체를 생성하지 못한 것에서부터 시작된 것이다.
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate
[software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient]:
Factory method 'cloudWatchLogsClient' threw exception with message:
Access key ID cannot be blank.
[3] 해결 방법
A 클래스를 B 클래스보다 먼저 초기화되게 만들기 위해 @DependsOn 어노테이션을 선언해 해당 에러를 해결했다.
B 클래스
A 클래스가 초기화 된 이후에 B 클래스의 초기화가 이루어질 수 있게 함과 더불어 누가 봐도 A 클래스와 의존 관계임을 알 수 있게 되었다.
@Configuration
@DependsOn("a")
public class B {
@Bean
public CloudWatchLogsClient cloudWatchLogsClient() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
A.cloudWatchAccessKeyId, A.cloudWatchSecretAccessKey
);
// 생략
}
}
정리
- 스프링 빈으로 등록된 오브젝트들의 초기화 순서와 생명 주기 관리는 모두 스프링 컨테이너의 몫이다.
- 각 클래스 간의 의존 관계가 명확할 때 빈의 초기화 순서를 명시하고 제어하면 자동으로 순서가 변경되는 경우에 발생할 수 있는 문제를 해결할 수 있다.
- 스프링에 등록된 빈이라면 @DependsOn 어노테이션을 활용해서 빈의 초기화 순서를 제어할 수 있다.
- 빈의 초기화 순서를 제어할 때는 순환 의존 문제를 주의해야 한다.
참고
- Controlling Bean Creation Order with @DependsOn Annotation
- Understanding the @DependsOn Annotation in Spring
- 토비의 스프링 3.1 Vol.1 스프링의 이해와 원리 - 1장 오브젝트와 의존관계
'Spring' 카테고리의 다른 글
[Spring] Custom Filter로 로그 파밍하기 (CloudWatch X Logback) (4) | 2024.10.09 |
---|---|
[Spring] Server-Sent Events로 알람 서비스 개선하기 (0) | 2023.08.28 |
[Spring] 로컬 캐시를 활용한 Refresh Token 구현기 (feat. Caffeine) (0) | 2023.05.28 |
[Spring Security] POST 테스트 : 403 Forbidden 에러 해결 (0) | 2023.05.17 |
[Spring] 쉽게 이해하는 Spring Security + JWT 로그인 구현기 (0) | 2023.05.12 |