문제점
Redis 구성을 위해 application.yml에서 host, port, node 주소 등의 환경변수를 가져오는데 있어 @ConfigurationProperties
는 Setter를 기반으로 바인딩을 하게 됩니다. 이로 인해 외부에서 node 값에 대해 조작해 데이터가 변경될 가능성이 있었습니다.
또한 @Value
로 변수를 가져오는 방식 또한 중복된 주소값을 적어야 되고 final
변수로 설정하지 않아 immutable한 성격을 가지고 있다고는 보기 힘들었습니다.
그래서 이러한 데이터 변경의 위험을 제거하기 위한 고민을 할 필요가 있었습니다.
RedisConfig 클래스
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
@Getter
@Setter // Setter로 인해 데이터가 변경 가능성이 존재
@Configuration
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class RedisConfig {
private List<String> nodes;
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
RedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration(nodes);
return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
}
}
Application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
redis:
host: localhost
port: 7001
# local 환경에서 redis cluster 활용시 사용하는 변수
cluster:
nodes:
- 127.0.0.1:7001
- 127.0.0.1:7002
- 127.0.0.1:7003
- 127.0.0.1:7004
- 127.0.0.1:7005
- 127.0.0.1:7006
해결방법
@ConstructorBinding
Spring Boot 2.3 버전 이후 생성자 주입방식으로 불변성을 가지고 Properties 파일을 만들 수 있는 방식이 추가되었습니다.
@ConstructorBinding
어노테이션을 이용하면 final
필드에 대해 값을 주입할 수 있고 중첩 클래스가 있다면 자동으로 중첩 클래스의 final 필드 또한 자동으로 값을 주입하는 대상이 됩니다.
final 키워드를 명시하지 않는다면 setter를 이용해서 값을 binding 하려하기 때문에 setter가 없다는 exception이 발생하게 됩니다.
다만, 이 방식을 사용하면 Properties 클래스에 직접적으로 @Configuration
을 이용해서 직접적으로 Spring Bean으로 만들어 주지 않습니다. 그래서 다른 클래스에서 @EnableConfigurationProperties
을 이용해서 생성할 Properties 클래스의 클래스 타입을 명시해주면 Spring Bean으로 등록해야 합니다.
변경된 코드
RedisPropertiesConfig & RedisProperties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableConfigurationProperties(value = RedisProperties.class) // Bean 등록을 위한 어노테이션
public class RedisPropertiesConfig {
@Getter
@RequiredArgsConstructor // 생성자 추가
@ConstructorBinding // 생성자로 바인딩한다는 것을 명시
@ConfigurationProperties(prefix = "spring.redis") // 접두사 지정
public static class RedisProperties {
// final 변수로 구성
private final List<String> nodes;
private final String host;
private final int port;
}
}
우선 RedisPropertiesConfig
클래스에 @EnableConfigurationProperties
를 추가해 빈으로 등록해 줄 내부 클래스 타입을 지정해줍니다.
그리고 내부 클래스로 구성된 RedisProperties
에서 Application.yml을 통해 가져올 환경변수들을 final 변수로 추가하고 @ConstructorBinding
과 @RequiredArgsConstructor
를 추가해 생성자를 통해서 변수가 바인딩 되도록 구성했습니다.
Application.yml
1
2
3
4
5
6
7
8
9
10
11
12
spring:
redis:
host: localhost
port: 7001
nodes: # local 환경에서 redis cluster 활용시 사용하는 변수
- 127.0.0.1:7001
- 127.0.0.1:7002
- 127.0.0.1:7003
- 127.0.0.1:7004
- 127.0.0.1:7005
- 127.0.0.1:7006
Application.yml에서도 하나의 prefix로 필요한 변수를 호출할 수 있도록 수정했습니다.
RedisConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
private final RedisProperties properties; // RedisProperties 타입을 DI
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
RedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration(properties.getNodes()); // RedisProperties 변수에서 Getter를 통해 원하는 변수를 호출
return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
// return new LettuceConnectionFactory(properties.getHost(), properties.getPort());
}
}
그리고 실제 환경변수를 필요로 하는 RedisConfig에서 빈으로 등록된 RedisProperties 객체를 주입받아 저장된 변수들을 Getter를 통해 호출합니다.
마치며
이처럼 @ConstructorBinding를 통해 Setter가 아닌 생성자를 통해 바인딩이 되도록 수정하면서 외부에서 변수 조작의 위험을 줄이고 고정 상수에 대한 안정성을 확보했습니다.
참고자료
- https://tecoble.techcourse.co.kr/post/2020-09-29-spring-properties-binding/
- https://cheese10yun.github.io/immutable-properties/
- https://velog.io/@lovi0714/%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-%EC%84%B8%EC%85%98-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81