프로젝트 기술 스택
spring boot 2.5.4
gradle 7.1.1
mybatis
지난 포스팅에서 언급했 듯이 설정 파일을 분리해서 읽는 방식에는 문제가 있었고, 이를 해결하기 위해 Spring Cloud Config를 적용하게 되었다.
사실 이전에도 Spring Cloud Config에 대해서 알고 있었고, 지난 여름 방학동안 다른 프로젝트에 이를 적용해보려고 했었는 데 적용하는 데 실패했었다.
당시에는 해결방안을 몰라서 적용하지 못했었고, 이번에도 그에 대한 두려움이 있어서 다른 방식을 이용하게 된 것이였다. 하지만 이번 기회에 다시 재도전하여 설정을 적용해봤다.
Spring Cloud Config란?

Spring Cloud Config는 외부 설정 정보를 서버에 올려두고, 클라이언트 어플리케이션에서 이를 사용할 수 있는 기능이다. 예를 들어 github의 private repository에 default, dev, prod의 설정 정보를 올려두고, 이 repository를 Cloud Config Server와 연결하면, client application에서 profile에 따라 Clould Config Server의 설정 파일을 이용할 수 있는 것이다. 당연히 서로 다른 client application에서도 같은 서버를 이용한다면, 같은 설정 정보를 이용하는 것도 가능하다.
1. 개발 환경 분리 및 설정 파일 작성
개발 환경(Phase)이란?
개발 환경이란 Application이 동작하는 환경을 의미한다. 일반적으로 프로그램을 개발할 때, 우선 local에서 application을 실행하면서 디버깅이나 테스틀 진행하고, 실제 운영 환경과 비슷한 dev 환경이나, beta, rc 환경에 순차적으로 배포하면서 테스트를 진행하게 된다. 이러한 테스트를 통과한 코드를 운영 환경(prod)에서 배포하게 된다.
개발 환경 마다 다른 설정을 적용할 수 있도록 Spring Boot에서는 Profile 기능을 제공하고 있다.
각 개발환경에 대한 특징이 궁금하다면 아래의 블로그를 참고하면 좋을 것이다.
[참고하기 좋은 내용] 개발 환경 분리
1. 개발 환경 분리 1) Local 말 그대로 개발할 때의 각자 개발자 PC 환경을 뜻한다. 이 때 중요한 점은 코드를 합칠 때의 문제가 발생하지 않도록 모든 개발자가 동일한 개발 환경을 사용해야한 다는
ozofweird.tistory.com
https://jungseob86.tistory.com/11
[기타] 개발 환경(Phase)이란 무엇일까?
개발 환경(Phase)란? 개발 환경( Phase )은 Application이 동작하는 환경을 의미한다. 따라서 대부분의 Framework는 각각의 개발 환경마다 설정 값을 다르게 셋팅할 수 있는 기능이 있다. 대표적으로 Spring B
jungseob86.tistory.com
내가 우리팀에 적용하기 위해 구상한 개발환경은 다음과 같다.
defualt(local)
: 우리팀의 경우 개발 단계에서 나는 local DB에서 개발을 진행한 다음 서버의 DB에 통합하는 방법을 이용하고 있고, 다른 팀원의 경우 서버의 DB에서 개발을 진행하고 있다. 따라서 defualt 환경의 경우 개발자 마다 별도로 설정 파일을 작성하고, 이를 이용할 수 있도록 구성했다.
또한 일반적으로도 개발자마다 local DB의 계정명과 비밀번호가 다르다는 점을 고려하면 Spring Cloud Config의 설정 정보를 공통적으로 이용하긴 힘들다는 것이 나의 생각이다.
결국은 "설정 정보의 민감한 정보 숨기기 1"에서 사용했던 설정 정보를 2가지로 나누는 방식을 유지할 것이다.
그러나, DB 외의 다른 설정 정보 (예를 틀어 jwt 암호화 키, Open API의 키 값과 비밀번호 등 )은 Spring Cloud Config의 설정을 이용할 수 있도록 구성했다.
wholeinone-defualt.yml
(이때 각 환경별 설정 정보의 파일 이름은 {어플리케이션 이름}-{환경 이름}.yml 형식으로 작성한다. )
config-name: default
searchapi:
naverid: {client 키 값}
naversecret: {secret 값}
jwt:
secret:
user_info_password_key: {암호화 키}
token_validity_in_seconds: {만료 시간}
앞서 말했 듯이 DB 정보 외의 설정 정보를 작성한다.
dev
: 운영환경과 유사하게 만든 개발 서버, 서버의 DB를 이용한다.
wholeinone-dev.yml
config-name: dev
spring:
datasource:
hikari:
driver-class-name : {driver}
jdbc-url : {url}
user-name : (계정명)
password : {비밀번호}
searchapi:
naverid: {client 키}
naversecret: {secret}
jwt:
secret:
user_info_password_key:{암호화 키}
token_validity_in_seconds: {만료 시간}
참고로 localhost를 기준으로 jdbc-url를 작성하는 방법은 다음과 같다.
aws의 RDS를 이용하는 경우 localhost대신 RDS의 endpoint를 작성하면된다.
jdbc-url: jdbc:mysql://localhost:3306/{DB이름}?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf8&useSSL=false
allowPublicKeyRetrieval=true: mysql 8.0이후로 외부에서 mysql db 접속시 필수로 필요 (기본 false)
useUnicode=true: unicode로 전송한다.
characterEncoding=utf8: utf8로 인코딩한다.
useSSL=false: ssl 접속을 사용하지 않는다 (기본 값 true)
이외에도 다양한 옵션을 추가할 수 있다.
prod
: 원래라면 운영 환경이지만, 우선은 front 분들이 이용하는 환경이라고 생각하고 구성했다. dev와 마찬가지로 개발 서버를 이용하지만, 오류 발생시 slack을 통해 오류 메시지를 출력한다는 점이 다르다. 또한 서버에서 배포시 사용하는 환경이다.
(slack 연동 방법은 추후 소개)
wholeinone-prod.yml
config-name: prod
spring:
datasource:
hikari:
driver-class-name : {driver}
jdbc-url : {url}
username : {계정명}
password : {비밀번호}
searchapi:
naverid: {client 키}
naversecret: {secret}
jwt:
secret:
user_info_password_key: {client 키}
token_validity_in_seconds: {만료시간}
logging:
slack:
webhook-uri: {url}
config: classpath:logback-spring.xml
jdbc-url에 서버의 DB 연결정보를 적는다.
이렇게 작성한 wholeinone-default.yml, wholeinone-dev.yml, wholeinone-prod.yml 파일을 깋허브 private 저장소에 올린다. 나는 wholeinone-config라는 이름의 저장소에 올렸다.
2. Clould Config Server Application 구현
의존성 추가(bulid.gradle)
dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
그리고 본인의 Server 프로젝트에서 main 함수가 있는 클래스에 @EnableConfigServer를 추가한다.
아래는 예시
@EnableConfigServer
@SpringBootApplication
public class WholeinoneConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(WholeinoneConfigServerApplication.class, args);
}
}
application.yml
server에서 활용할 설정정보가 저장된 깃허브 계정과 연결을 위한 설정을 작성한다.
spring:
application:
name: configserver
cloud:
config:
server:
git:
uri: https://github.com/{깃허브 계정}/{설정 정보를 올린 깃허브 저장소}
username: {깃허브 계정}
password: {깃허브 계정의 키값}
default-label: master //main이 되는 브랜치 (개인마다 다를 수 있으니 확인 필요)
server:
port: 8082 // Server가 실행될 포트
여기서 password는 깃허브의 personal access token을 입력하면 되는데 발급 방법은 다음과 같다.
https://record-of-coding.tistory.com/24
Github의 access token 발급하기 (원격으로 깃허브 로그인하기)
Settings > Developer Settings > Personal access tokens Github의 우측 상단의 프로필 사진을 클릭하고 Settings 클릭 Settings 페이지의 좌측 사이드바에서 Developer Settings 클릭 Developer settings에서..
record-of-coding.tistory.com
서버를 실행하고,
http://localhost:8082/wholeinone/defualt 등으로 get 요청을 보냈을 때 wholeinone-default.yml의 값이 정상적으로 출력된다면 config server가 정상적으로 작동하고 있는 것이다.
({어플리케이션 이름}-{환경 이름}.yml 형식으로 설정 정보의 파일명을 작성하게 되는 데, http://localhost:8082/{어플리케이션 이름}/{환경 이름}과 같은 url로 get요청을 보내면 된다. )
3. Client Application 구현
설정정보를 이용하는 스프링 프로젝트에 spring cloud config의 의존성을 추가한다.
build.gradle
{
//Config Server Client
implementation 'org.springframework.cloud:spring-cloud-starter-config'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
스프링 프로젝트에서 client의 설정 정보를 읽기 전에 server의 설정정보를 읽어와야 하는데, 이를 위해서 bootstrap.yml을 작성한다. bootstrap.yml은 스트링 부트 프로젝트가 시작할 때 application.yml 보다 먼저 로드된다.
그런데 bootstrap.yml을 자동으로 읽어오는 기능이 최신의 spring cloud에서 빠지게 되어 의존성을 추가해줘야 한다.
(사실 전에 구현에 실패했던 이유가 이 의존성을 추가해야 한다는 것을 몰라서였다..)
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
bootstrap.yml
spring:
cloud:
config:
uri: {spring cloud server가 실행되고 있는 uri}:{포트}
name: {프로젝트 이름}
설정 정보 읽기
로컬의 설정 정보를 읽는 방법과 spring cloud config를 사용하는 방법 2가지를 이용하기 때문에 설정 정보를 읽는 방법도 약간 복잡하게 구성하게 되었다.
설정 정보를 읽는 클래스를 3가지로 구성하게 되었는 데 각 클래스의 기능은 다음과 같다.
DefaultGlobalPropertyConfig
: defualt 프로필에서 local의 설정 정보를 통해 개발자들마다 서로 다른 설정 정보를 구성하고 읽어 올 수 있다.
GlobalPropertyConfig
: dev, prod 프로필에서 사용하는 서버의 DB 정보를 읽어온다.
SecretPropertyConfig
: default, dev, prod 에서 공통으로 사용하는 secret 값을 설정 정보에서 읽어온다 (예를 들어 jwt 암호화 키 등)
DefaultGlobalPropertyConfig
전체 코드는 "설정 정보의 민감한 정보 숨기기 1"과 동일한데, Profile 설정과 RefreshScope 어노테이션만 추가한다.
@Profile("default")는 default 프로필에서 이 빈을 사용하겠다는 의미이다.
@RefreshScope은 서버의 설정 정보에 변경이 생겼을 때 변경 정보를 갱신할 수 있다.
@Configuration
@Profile("default")
@RefreshScope
@PropertySources({
@PropertySource( value = "classpath:/properties/config.yaml", ignoreResourceNotFound = true )//db 설정 파일 경로
// @PropertySource( value = "file:${user.home}/env/dev-config.properties", ignoreResourceNotFound = true) // 배포시 배포 환경의 디렉토리 주소
})
@Getter
public class DefaultGlobalPropertyConfig {
@Value("${driver-class-name}")
private String driverClassName;
@Value("${jdbc-url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
}
GlobalPropertyConfig
!default는 default가 아닌 profile에서 이 빈을 이용하겠다는 의미이다.
@Configuration
@Profile("!default")
@RefreshScope
@Getter
public class GlobalPropertyConfig {
@Value("${spring.datasource.hikari.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.hikari.jdbc-url}")
private String url;
@Value("${spring.datasource.hikari.username}")
private String username;
@Value("${spring.datasource.hikari.password}")
private String password;
}
SecretPropertyConfig
모든 프로필에서 공통적으로 사용할 설정 정보를 주입받는다.
@Configuration
@Getter
@RefreshScope
public class SecretPropertyConfig {
@Value("${jwt.secret.user_info_password_key}")
private String userInfoPasswordKey;
@Value("${jwt.secret.token_validity_in_seconds}")
private String tokenValidityInSeconds;
@Value("${searchapi.naverid}")
private String naverId;
@Value("${searchapi.naversecret}")
private String naverSecret;
}
문제는 DBConfig 인데,
원래는 하나의 DBConfig에서 프로필마다 다른 클래스를 이용하도록 구현하고 싶었는 데, 방법을 모르겠어서 그냥 DBConfig도 2가지를 만들게 되었다.
Default 프로필에서 사용하는 DB 설정
@Configuration
@EnableTransactionManagement
@Profile("default")
public class DefaultDBConfig {
@Autowired
DefaultGlobalPropertyConfig defaultGlobalPropertyConfig;
@Bean
@ConfigurationProperties(prefix = "dev.spring.datasource.hikari") //다음의 prefix로 시작하는 설정을 이용해서 hikariCP의 설정 파일을 만듦
public HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
@Primary
public DataSource customDataSource() { // 위에서 만든 설정 파일을 이용해서 디비와 연결하는 데이터 소스를 생성
return DataSourceBuilder
.create()
.url(defaultGlobalPropertyConfig.getUrl())
.driverClassName(defaultGlobalPropertyConfig.getDriverClassName())
.username(defaultGlobalPropertyConfig.getUsername())
.password(defaultGlobalPropertyConfig.getPassword())
.build();
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception { //DataSource를 참조하여 MyBatis와 Mysql 서버를 연동시켜준다.
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(customDataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:/mapper/**/*Mapper.xml"));
//Mapper 파일 위치를 설정. [classpath]: resource 폴더 의미, [/mapper/**/]: mapp 폴더 밑의 모든 폴더를 의미, [*Mapper.xml]: 이름이 Mapper로 끝나고 확장자가 xml인 모든 파일을 의미
factoryBean.setTypeAliasesPackage("com.naturemobility.seoul.domain");
factoryBean.setConfiguration(mybatisConfig()); //Mybatis의 설정파일의 위치를 참조
return factoryBean.getObject();
}
@Bean
@ConfigurationProperties(prefix = "dev.mybatis.configuration")
public org.apache.ibatis.session.Configuration mybatisConfig() {
return new org.apache.ibatis.session.Configuration();
}
@Bean
public SqlSessionTemplate sqlSession() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(customDataSource());
}
}
dev, prod에서 사용하는 DB설정
@Profile("!default")
@Configuration
@EnableTransactionManagement
public class DBConfig {
@Autowired
GlobalPropertyConfig globalPropertyConfig;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari") //다음의 prefix로 시작하는 설정을 이용해서 hikariCP의 설정 파일을 만듦
public HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
@Primary
public DataSource customDataSource() { // 위에서 만든 설정 파일을 이용해서 디비와 연결하는 데이터 소스를 생성
return DataSourceBuilder
.create()
.url(globalPropertyConfig.getUrl())
.driverClassName(globalPropertyConfig.getDriverClassName())
.username(globalPropertyConfig.getUsername())
.password(globalPropertyConfig.getPassword())
.build();
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception { //DataSource를 참조하여 MyBatis와 Mysql 서버를 연동시켜준다.
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(customDataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:/mapper/**/*Mapper.xml"));
//Mapper 파일 위치를 설정. [classpath]: resource 폴더 의미, [/mapper/**/]: mapp 폴더 밑의 모든 폴더를 의미, [*Mapper.xml]: 이름이 Mapper로 끝나고 확장자가 xml인 모든 파일을 의미
factoryBean.setTypeAliasesPackage("com.naturemobility.seoul.domain");
factoryBean.setConfiguration(mybatisConfig()); //Mybatis의 설정파일의 위치를 참조
return factoryBean.getObject();
}
@Bean
@ConfigurationProperties(prefix = "mybatis.configuration")
public org.apache.ibatis.session.Configuration mybatisConfig() {
return new org.apache.ibatis.session.Configuration();
}
@Bean
public SqlSessionTemplate sqlSession() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(customDataSource());
}
}
문제점
앞서 잠깐 언급했듯이 설정 정보를 private 저장소에 올렸음에도 http://localhost:8082/{어플리케이션 이름}/{환경명} 으로 get 요청을 보내면 설정 정보가 그대로 노출되게 된다.
따라서 이러한 문제점을 해결하기 위해 spring clould config에서는 암호화 기능을 제공한다.
설정 정보를 암호화하는 방법은 다음에 소개하도록 한다.
후기
지난 포스팅에서 설정 파일을 2개로 나누는 경우 생기는 문제(1. 설정 방법이 다소 복잡함, 2. 설정 정보의 변경이 어렵고 배포 시 파일 동기화가 불편함)가 있어서 이와 같은 방법을 적용하게 되었는 데, 적어도 설정 정보의 변경 및 동기화가 어렵다는 문제를 해결할 수 있었다.
다만, 개발자마다 다른 설정 정보를 사용할 수 있도록 설계하면서 설정 방법이 더 복잡하게 된 것같다...
만약에 다음에 다른 프로젝트를 하게 된다면 spring cloud config만 이용하여 설정 방법을 간단하게 구성하게 될 것같다.
그럼에도 지난번에 적용에 실패했던 방법을 이번에 시도하여 설공했다는 것이 개인적으로 의미가 있다.
참고 블로그
Spring Cloud Config
[Spring Cloud Config] Application의 설정 정보 (application.yml) 를 중앙에서 관리하기 (by native repository)
해당 글은 Spring Cloud Netflix Eureka 와 Spring Cloud Gateway 의 Built-in Route로 Predicates와 Filter 조작하기)에 의존하는 글입니다. 실습 환경을 따라하시려면 Eureka와 Gateway 글에 나온 실습을 따라하..
wonit.tistory.com
https://madplay.github.io/post/introduction-to-spring-cloud-config
Spring Cloud Config: 소개와 예제
스프링 설정이 바뀌었을 때 빌드, 배포없이 갱신하려면 어떻게 해야할까? 스프링의 설정 파일들을 어떻게 외부로 분리시킬 수 있을까?
madplay.github.io
https://pjh3749.tistory.com/276
Spring Cloud Config를 활용하여 설정값(properties), 비밀번호 숨겨서 배포하기
Spring Cloud Config Server 구축하기 Spring Cloud Config 를 사용하여 비밀번호나 민감한 key들을 숨겨서 관리하는 방법을 알아보겠습니다. 전체적인 구상도는 다음과 같습니다. 깃허브 private 저장소에 yml
pjh3749.tistory.com
'프로젝트 > 스크린 골프장' 카테고리의 다른 글
| 전시회 후기 (0) | 2022.06.16 |
|---|---|
| [Spring Boot] 설정 파일의 민감한 정보 숨기기 1 (0) | 2022.01.15 |
| DB 및 전체 프로젝트 구조 (0) | 2022.01.09 |
| UI 프레임워크 (0) | 2022.01.09 |
| 경쟁 앱 분석 (0) | 2022.01.09 |


















