티스토리 뷰

Spring Configuration을 구성할 때 @Configuration 클래스 내부의 메서드에다가 @Bean을 선언하여 구성합니다.

만일 동일한 타입의 @Bean이 여러 개이고 환경별로 다르게 동작하게끔 구성하기 위해서는 @Profile을 사용해 처리할 수 있습니다. 그런데 이 @Profile을 사용할 때 주의해야 할 점이 있는데요. 예제코드와 함께 살펴보겠습니다.

 

예제코드

위 코드는 AWS SES를 이용하기 위해 AmazonSimpleEmailService 타입의 Bean을 생성하는 코드입니다.

local 환경에서는 설정 파일에 있는 access-key, secret-key 정보로 credentials 정보를 생성하여 이용할 수 있도록,

dev 환경은 EC2에 배포되니 IMDS를 이용하여 EC2에 부여된 IAM 역할로 이용할 수 있도록 구성하였습니다.

 

 

ApplicationContextRunner를 사용하여 해당 이름의 Bean이 존재하는지 체크해보겠습니다.

ApplicationContextRunner에 대해 궁금하신 분은 여기에서 확인해보실 수 있습니다.

 

 

local 환경에서는 Bean이 존재한 걸 확인하였습니다. 

 

 

dev 환경으로 테스트를 해보겠습니다.

 

 

dev 환경에서는 해당 이름의 Bean을 찾을 수 없다고 나옵니다.

위 @Configuration 클래스 내부에서는 동일한 타입의 Bean을 2개를 만들고 @Profile을 이용해서 환경별로 어떤 Bean이 동작되어야 하는가를 명시해줬는데 Bean을 찾을 수 없다고 합니다. 이제 왜 찾을 수 없는지 원인을 파악해보겠습니다.

 

원인파악

@Profile의 설명입니다. 번역하자면 다음과 같습니다.

 

@Profile on @Bean 메서드를 사용하면 다음과 같은 특수 시나리오가 적용될 수 있습니다: 동일한 Java 메서드 이름의 @Bean 메서드가 오버로드된 경우(생성자 오버로드와 유사함), @Profile 조건은 오버로드된 모든 메서드에서 일관되게 선언되어야 합니다. 조건이 일치하지 않으면 과부하된 메서드 중 첫 번째 선언의 조건만 문제가 됩니다. @따라서 프로파일은 특정 인수 서명이 있는 오버로드된 메소드를 다른 메소드보다 선택하는 데 사용할 수 없다. 프로필 조건이 다른 대체 빈을 정의하려면 동일한 빈 이름을 가리키는 고유한 Java 메서드 이름을 사용하십시오. 

즉, 위에 언급된 두 어노테이션을 오버로딩된 메서드에 사용할 경우 오버로딩된 메서드 내부는 일관되게 선언이 되어야 합니다. 만일 조건이 일치하지 않을 경우 오버로딩된 메서드 중 첫 번째로 선언된 메서드가 적용됩니다.

 

내부 구현이 다르게 되있는 오버로딩된 메서드들 중에서 @Profile로 특정 메서드를 선택하는 데 사용할 수 없으며

만약에 @Profile 조건이 다른 빈을 정의하려면 동일한 빈 이름을 가리키는 고유한 메서드 이름을 사용해야 합니다.

 

동일한 빈을 가리키는 방법은 @Bean의 name 속성에 이름을 명시해주면 됩니다.

 

 

@Configuration의 내용을 살펴보면 @Profile에 관한 두 가지 예제가 나옵니다.

@Configuration 클래스별로 나눠서 작성하거나 또는 @Profile 조건이 다른 동일한 이름의 Bean을 선언하고 메서드 이름을 다르게 해줍니다.

 

 

아까 위에서 정리했던 내용 중에 '만일 조건이 일치하지 않을 경우 오버로딩된 메서드 중 첫번째로 선언된 메서드가 적용됩니다.' 라는 문구가 있었습니다. 오버로딩된 메서드 중에서 첫 번째는 어떻게 알 수 있을까요? 위에 정답이 나와있습니다.

 

ConfigurationClassBeanDefinitionReader 클래스는 @Configuration이 선언된 클래스를 읽어드린 후 @Bean이 선언된 메서드들을 순회하면서 Spring Bean으로 등록합니다.  그리고 getBeanMethods 메서드는 내부적으로 LinkedHashSet으로 구현돼 있어 순서가 보장됩니다. 아마 여기서 말하는 순서는 선언순서를 얘기하는 것 같습니다.

 

 

@Profile은 내부적으로 @Conditional을 참조하고 있습니다. 다시 말해 @Profile + @Bean을 혼용해서 사용한다는 건

결국 @Bean + @Conditional을 같이 사용한다는 말과 같습니다.

 

문제해결

먼저 AWSCredentialsProvider를 한 단계 추상화합니다. 그런 다음 구현 클래스에서 환경별로 분기 처리하려고 합니다.

 

 

구현클래스를 환경별로 분리하여 처리하도록 하였습니다.

 

 

코드가 처음 봤던 것보다 조금 깔끔해졌습니다. 이제 테스트를 해보겠습니다.

 

 

기존에 Configuration 클래스는 아무런 Bean도 주입받지 않았기 때문에 구현 클래스를 주입받도록 지정해뒀습니다.

테스트는 정상적으로 잘 되었습니다.

 

 

@Configuration 주석에 설명되있는 방법이 아닌 위 방법으로 해결하려고 했던 이유는 다음과 같습니다.

1. 해당 Configuration 설정은 AWS SDK를 이용하여 SES Client Bean을 생성하는데 S3, SNS, SQS 등 액세스해야 할 AWS 서비스가 많아지면 서비스당 Configuration 파일을 두 개씩 만들어야 합니다. 그래서 하나로 관리하기 위해 이렇게 구성하였습니다.

2. @Bean을 이용하여 Spring Bean을 설정할 경우 기본적으로 Bean의 고유 식별자는 메서드 이름이 됩니다. 그리고 Bean의 식별자는 Bean의 타입 클래스 이름의 첫 글자를 소문자로 변환한 이름을 가지고 구성됩니다.
@Bean의 name 속성을 이용해서도 식별자를 지정해줄 순 있지만 개인적으로 위처럼 하니까 코드의 가독성이 더 좋은 것 같습니다.

3. 추후에 @Profile의 조건이 추가되고 조건마다 내부 구현의 동작이 달라질 경우 Configuration 클래스에 추가하는 것 보다 @Profile 조건에 의해 동작하는 걸 클래스로 빼두어 그 클래스에서 구현해서 넘겨주는 게 더 깔끔하다고 판단했기 때문입니다.

 

참고

@Profile은 Spring Boot 애플리케이션을 GraalVM 네이티브 이미지로 빌드할 경우 조건별 분기가 지원되지 않습니다.

https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html

 

GraalVM Native Image Support

When writing native image applications, we recommend that you continue to use the JVM whenever possible to develop the majority of your unit and integration tests. This will help keep developer build times down and allow you to use existing IDE integration

docs.spring.io

 

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함