목차
들어가기 전에
스프링부트 프로젝트를 개발하다 보면 운영/개발 등의 환경에 따라 코드에 들어가야 하는 값이 달라지는 경우가 있다. 단순히 현재 서버가 어떤 서버인지를 확인해줄 문자열이 될 수도 있고, 다른 API를 통해 서비스를 제공하는 프로젝트라면 해당 API를 제공하는 서버 호스트가 될 수도 있다.
이렇게 구동 환경에 따라 달라지는 값들은 yml
파일 또는 properties
파일에서 프로파일을 나눠서 관리하고 실제 코드에서는 @Value
로 값을 바인딩해서 사용하게 된다.
그런데 @Value
로는 final
변수에 값을 바인딩하려면 롬복으로 생성자를 선언하는 방식을 사용할 수 없기 때문에 변경사항이 발생했을 때 굉장히 귀찮아진다. 이 경우 생성자 바인딩이라는 방식을 통해 프로퍼티 설정 값을 수월하게 final
변수로 사용할 수 있다.
@Value
로 프로퍼티 값 바인딩
사실 코틀린에서는 자바의 final
에 대응하는 val
변수에 @Value
로 값을 바인딩하는게 어렵지 않다. 코틀린의 문법 상 프로퍼티를 선언할 때 동시에 생성자가 만들어지기 때문이다.
@Service
class TokenProvider(
@Value("\${secret-key}")
private val secretKey: String,
@Value("\${expiration-hours}")
private val expirationHours: Long,
@Value("\${issuer}")
private val issuer: String
) {
...
}
하지만 자바에서 final
변수에 @Value
로 값을 바인딩하려면 롬복 없이 보일러 플레이트 코드로 생성자를 만들어야 한다. @RequiredArgsConstructor
는 final
로 선언되어있는 필드에 스프링 빈을 주입하려고 하기 때문이다.
결국 보일러 플레이트 코드로 생성자를 만들어서 생성자의 파라미터에 @Value
를 붙여줘야 final
변수에 프로퍼티 값을 바인딩할 수 있다.
@Service
public class TokenProvider {
private final String secretKey;
private final long expirationHours;
private final String issuer;
public TokenProvider(
@Value("${secret-key}") String secretKey,
@Value("${expiration-hours}") long expirationHours,
@Value("${issuer}") String issuer
) {
this.secretKey = secretKey;
this.expirationHours = expirationHours;
this.issuer = issuer;
}
...
}
@Value
의 단점
자바에서 @Value
를 사용할 때 발생하는 단점은 다음과 같이 정리할 수 있다.
- 일반 바인딩 방식이나 롬복을 사용한 생성자 바인딩 방식은
final
을 사용할 수 없기 때문에 값을 변경해버릴 수 있다. - 생성자를 사용해서 바인딩하면
final
을 사용할 수 있으나 보일러 플레이트 코드를 사용해야 해서 해당 클래스에서 주입받는 스프링 빈 등이 추가되거나 제거되면 수정하기가 매우 귀찮다.
이 문제에서 비교적 자유로운 코틀린이라고 하더라도 다음의 단점은 자바와 공유하게 된다.
- 여러 클래스에서 동일한 프로퍼티 값을 바인딩해서 사용하게 된다면 중복 코드가 발생하고,
@Value
로 프로퍼티 값의 키를 매번 작성해가며 바인딩하다보니 코드가 지저분해질 수 있다.
생성자 바인딩을 사용해보자
Constructor Binding. 단어 그대로 생성자 바인딩이다. 이 방법을 사용하면 프로퍼티 값을 통째로 클래스의 필드 또는 프로퍼티에 바인딩할 수 있다. 이로 인해 여러 클래스에서 동일한 프로퍼티 값을 사용하는 경우에도 또한 해당 이렇게 프로퍼티 값이 통째로 바인딩된 객체를 주입받아서 사용해서 중복 코드를 줄일 수 있다.
아래와 같은 프로퍼티를 클래스의 필드에 바인딩하는 경우를 생각해보자.
file-path:
log-path: /home/centos/demo/logs
download-path: /home/centos/demo/download
api-config:
host: https://www.demo.com
authorization: eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJjb2xhYmVhcjc1NCIsImlhdCI6MTcwMjYyNzE5MSwiZXhwIjoxNzAyNzEzNTkxfQ.-VRK5VRvaX4vvWl0q-LnCiThZNqMGThPX5RZ5stDbFj0pnnaCmtDzeVHMi0ttSR1ZVBpbAMtPX-0wWzbKdTePg
우선 값을 바인딩 받을 클래스를 작성한다.
@Getter
@ConfigurationProperties("file-path")
@RequiredArgsConstructor
@ConstructorBinding // 스프링부트 3.0 이상에서는 제외
public class FilePathProperties {
private final String logPath;
private final String downloadPath;
}
@Getter
@RequiredArgsConstructor
@ConfigurationProperties("api-config")
@ConstructorBinding // 스프링부트 3.0 이상에서는 제외
public class ApiConfigProperties {
private final String host;
private final String authorization;
}
@ConfigurationProperties
의 값에는 yml
또는 properties
파일에서 바인딩하고자 하는 값의 키에서 상위 레벨의 키를 모두 작성해주면 된다. (aaa.bbb.ccc
의 값을 바인딩하고 싶으면 @ConfigurationProperties("aaa.bbb")
와 같은 식으로)
클래스의 필드에는 바인딩하고자 하는 값의 최하위 키를 필드명과 매칭해서 작성해주면 된다. 이 때 하이픈으로 구분된 단어들은 자동으로 카멜케이스로 치환된다.(위 사진에서 FilePathProperties와 같은 경우)
@ConstructorBinding
어노테이션은 스프링부트 3.0 미만에서만 붙여준다. 스프링부트 3.0 이상일 경우에는 컴파일 에러가 발생한다.
그리고 스프링부트 메인 클래스(일반적으로는 프로젝트명Application)에 아래와 같이 어노테이션을 추가해준다.
@EnableConfigurationProperties({FilePathProperties.class, ApiConfigProperties.class}) // 추가
@SpringBootApplication
public class PropertiesDemoApplication {
public static void main(String[] args) {
SpringApplication.run(PropertiesDemoApplication.class, args);
}
}
@EnableConfigurationProperties
의 값으로는 프로퍼티 값을 바인딩해줄 클래스의 타입 배열을 넣어주면 된다.
이렇게 생성자 바인딩을 통해 프로퍼티 값을 바인딩 받는 클래스는 일반 스프링 빈과 동일한 방식으로 주입받아서 사용할 수 있다.
이제 테스트 코드를 작성해서 제대로 바인딩이 되었는지 확인해보자.
@SpringBootTest
public class ConstructorBindingTest {
private final FilePathProperties filePathProperties;
private final ApiConfigProperties apiConfigProperties;
@Autowired
public ConstructorBindingTest(FilePathProperties filePathProperties, ApiConfigProperties apiConfigProperties) {
this.filePathProperties = filePathProperties;
this.apiConfigProperties = apiConfigProperties;
}
@Test
void 생성자_바인딩_테스트() {
// given
String logPath = filePathProperties.getLogPath();
String downloadPath = filePathProperties.getDownloadPath();
String host = apiConfigProperties.getHost();
String authorization = apiConfigProperties.getAuthorization();
// when
// then
assertThat(logPath).isEqualTo("/home/centos/demo/logs");
assertThat(downloadPath).isEqualTo("/home/centos/demo/download");
assertThat(host).isEqualTo("https://www.demo.com");
assertThat(authorization).isEqualTo("eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJjb2xhYmVhcjc1NCIsImlhdCI6MTcwMjYyNzE5MSwiZXhwIjoxNzAyNzEzNTkxfQ.-VRK5VRvaX4vvWl0q-LnCiThZNqMGThPX5RZ5stDbFj0pnnaCmtDzeVHMi0ttSR1ZVBpbAMtPX-0wWzbKdTePg");
}
}
테스트가 잘 통과하는 것을 볼 수 있다.
번외1 - 클래스가 많아지면 어떡하지?
이 방법은 프로퍼티 값을 바인딩 받을 클래스가 늘어날수록 스프링부트 메인 클래스의 @EnableConfigurationProperties
에 클래스를 계속 추가해야 한다는 문제가 있다. 이렇게 되면 번거로울 뿐만 아니라 코드가 굉장히 지저분해질 수 있다.
이 때 @ConfigurationPropertiesScan
을 사용할 수 있다. 이 어노테이션을 사용하면 @ConfigurationProperties
가 붙은 클래스를 자동으로 탐색해서 스프링 빈으로 등록해준다.
스프링부트 메인 클래스의 내용만 아래처럼 수정해준다.
//@EnableConfigurationProperties({FilePathProperties.class, ApiConfigProperties.class}) 얘를 지우고
@ConfigurationPropertiesScan // 이걸 추가해준다
@SpringBootApplication
public class PropertiesDemoApplication {
public static void main(String[] args) {
SpringApplication.run(PropertiesDemoApplication.class, args);
}
}
이 때, @ConfigurationPropertiesScan
의 값으로 탐색하고자 하는 패키지를 지정할 수 있다. 이렇게 하면 해당 패키지 내부의 클래스만 탐색한다.(기본값 : 프로젝트 전체)
인텔리제이에서 어노테이션 왼쪽의 아이콘을 클릭해서 탐색된 클래스들을 확인할 수도 있다.
테스트 코드를 실행해보면 동일하게 테스트를 잘 통과하는 걸 볼 수 있다.
번외2 - 코틀린이라면?
코틀린도 자바 동일한 방법으로 생성자 바인딩을 통한 프로퍼티 값을 가져올 수 있다.
@ConfigurationProperties("file-path")
class FilePathProperties(
val logPath: String,
val downloadPath: String
)
@ConfigurationProperties("api-config")
class ApiConfigProperties(
val host: String,
val authorization: String
)
문법의 차이로 인해 롬복이 사라졌을 뿐 동일한 구조라는 것을 알 수 있다.
@ConfigurationPropertiesScan
@SpringBootApplication
class PropertiesDemoKtApplication
fun main(args: Array<String>) {
runApplication<PropertiesDemoKtApplication>(*args)
}
메인 클래스에도 동일하게 @EnableConfigurationProperties
또는 @ConfigurationPropertiesScan
을 붙여준다.
@EnableConfigurationProperties(ApiConfigProperties::class, FilePathProperties::class)
@SpringBootApplication
class PropertiesDemoKtApplication
fun main(args: Array<String>) {
runApplication<PropertiesDemoKtApplication>(*args)
}
또는
@ConfigurationPropertiesScan
@SpringBootApplication
class PropertiesDemoKtApplication
fun main(args: Array<String>) {
runApplication<PropertiesDemoKtApplication>(*args)
}
테스트 코드를 작성해서 실행해보면 정상적으로 바인딩 된 것을 볼 수 있다.
@SpringBootTest
class ConstructorBindingTest @Autowired constructor(
val apiConfigProperties: ApiConfigProperties,
val filePathProperties: FilePathProperties
) {
@Test
fun `생성자 바인딩 테스트`() {
// given
val logPath = filePathProperties.logPath
val downloadPath = filePathProperties.downloadPath
val host = apiConfigProperties.host
val authorization = apiConfigProperties.authorization
// when
// then
assertThat(logPath).isEqualTo("/home/centos/demo/logs")
assertThat(downloadPath).isEqualTo("/home/centos/demo/download")
assertThat(host).isEqualTo("https://www.demo.com")
assertThat(authorization).isEqualTo("eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJjb2xhYmVhcjc1NCIsImlhdCI6MTcwMjYyNzE5MSwiZXhwIjoxNzAyNzEzNTkxfQ.-VRK5VRvaX4vvWl0q-LnCiThZNqMGThPX5RZ5stDbFj0pnnaCmtDzeVHMi0ttSR1ZVBpbAMtPX-0wWzbKdTePg")
}
}
여담
자바에서 @Value
로 final
변수에 프로퍼티 값을 바인딩하려면 보일러 플레이트 코드를 사용해야 해서 수정이 필요할 때 번거로워지는 문제가 있는데 이 방법은 롬복을 사용하면서도 손쉽게 final
변수에 프로퍼티 값을 바인딩할 수 있다는 장점이 있다고 생각한다.
코틀린은 그냥 기본 생성자의 프로퍼티에 @Value
를 붙여주기만 해도 val
변수에 스프링부트 프로퍼티 값을 바인딩할 수 있지만 생성자 바인딩을 사용해서 중복되는 @Value
를 제거하고 코드를 좀 더 깔끔하게 작성할 수 있다고 생각한다.
참조 링크
'Development > Spring & Spring Boot' 카테고리의 다른 글
[Spring Boot] 스프링 인터셉터(Spring Interceptor)를 활용해서 API 로그를 DB에 기록해보자 (0) | 2024.03.01 |
---|---|
[Spring Boot] AOP를 이용해서 DB에 API 로그를 남겨보자 (2) | 2024.02.06 |
[Spring Boot] H2 DB Embedded Mode 사용하기 (0) | 2023.10.04 |
[Spring Boot] 스프링이 제공하는 다양한 어노테이션을 통한 유효성 검사와 응답 처리 (0) | 2023.08.28 |
[Spring Security] 스프링 시큐리티를 적용하고 예외가 발생했을 때 403 Forbidden이 발생하는 원인과 처리 방안 (1) | 2023.08.05 |
댓글