지난 포스팅에서 언급했 듯이 설정 파일을 분리해서 읽는 방식에는 문제가 있었고, 이를 해결하기 위해 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 기능을 제공하고 있다.
: 우리팀의 경우 개발 단계에서 나는 local DB에서 개발을 진행한 다음 서버의 DB에 통합하는 방법을 이용하고 있고, 다른 팀원의 경우 서버의 DB에서 개발을 진행하고 있다. 따라서 defualt 환경의 경우 개발자 마다 별도로 설정 파일을 작성하고, 이를 이용할 수 있도록 구성했다.
또한 일반적으로도 개발자마다 local DB의 계정명과 비밀번호가 다르다는 점을 고려하면 Spring Cloud Config의 설정 정보를 공통적으로 이용하긴 힘들다는 것이 나의 생각이다.
결국은 "설정 정보의 민감한 정보 숨기기 1"에서 사용했던 설정 정보를 2가지로 나누는 방식을 유지할 것이다.
그러나, DB 외의 다른 설정 정보 (예를 틀어 jwt 암호화 키, Open API의 키 값과 비밀번호 등 )은 Spring Cloud Config의 설정을 이용할 수 있도록 구성했다.
wholeinone-defualt.yml
(이때 각 환경별 설정 정보의 파일 이름은 {어플리케이션 이름}-{환경 이름}.yml 형식으로 작성한다. )
원래는 하나의 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만 이용하여 설정 방법을 간단하게 구성하게 될 것같다.
그럼에도 지난번에 적용에 실패했던 방법을 이번에 시도하여 설공했다는 것이 개인적으로 의미가 있다.
classpath는 resources 폴더까지를 의미하므로 그 아래부터의 설정 파일의 경로를 적으면 Spring에서 설정파일을 읽을 수 있다.
그리고 @Value를 통해 값을 주입한다.
@Configuration
@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("${user-name}")
private String username;
@Value("${password}")
private String password;
}
설정정보 이용하기
@Configuration
@EnableTransactionManagement
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();
}
// 커스텀한 DataSource를 이용해서 MyBatis와 DataSource 연결
@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());
}
}
문제점
그런데 이와 같이 설정을 했는 데 발생했던 문제는 다음과 같다.
1. 다른 팀원이 DB 설정 정보를 작성하기 어려움
설정파일을 하나 만들고 임의의 폴더 위치에 둔 다음, @PropertySource에 폴더의 주소를 적기만 하면되기 때문에 설정하기 쉬울 것이라고 생각했었는데, 나름 쉽게 설명드렸음에도 다른 팀원 분이 DB 설정을 하기 힘들어 하셨다...
git ignore로 인해 파일이 공유되지 않다보니 상대방 입장에서는 이해하기 힘들다는 것을 고려하지 못했다.
이는 아예 설정이 완료된 프로젝트 전체(config.yml 포함)을 따로 전달해드려서 해결하긴 했지만, 처음 의도대로 username이 개인 PC의 username으로 설정되는 바람에 한 번 더 어려움을 겪어야 했다.
이 문제도 root 계정에서 개인 PC의 username의 계정을 하나 만드는 것으로 해결했었다.
2. 배포시 설정파일 관리의 어려움
API를 서버에 올릴 때, jenkins와 github web hook를 이용해서 배포를 했었다.
그런데 git ignore로 인해 설정파일이 빠지게 되면서 빌드 시에 에러가 발생했다.
이는 약간의 꼼수로 서버에 동일한 설정파일을 만들어두고, jenkins에서 빌드 시에 복사해서 사용하도록 했었다.
그러나 이렇게 관리를 하면 매번 새로운 설정 정보가 생길 경우, 직접 server에 있는 파일을 수정해야해서 매우 번거로웠다.
프로젝트의 전체 DB의 모습이다. 지금 서버에 올라간 DB는 총 19개이지만 프로젝트를 진행하면서 기능의 변경이 일어났고, 그로 인해 사용하지 않는 DB 3개를 제외하여 16개만 나타냈다. 그러나 프로젝트를 진행하면서 DB가 추가되고 있어 최종적으로는 20개 가까이의 DB 테이블이 생성될 것으로 예상하고 있다.
클라이언트 페이지 설계
전체적인 클라이언트 페이지 간의 관계를 나타낸 것이다. 실제로는 각 페이지에서 더 세부적인 기능이 있으며, 로그인과 회원가입 기능도 제공한다.
사장님 페이지 설계
마찬가지로 사장님 페이지 간의 관계를 나타낸 것이다.
실제로는 사이드 메뉴를 통해 모든 페이지를 접근할 수 있기 때문에 거의 모두 동등한 깊이를 가진다고 볼 수 있다.
유즈 케이스 다이어그램이라던가 다양한 방식으로 현재 프로젝트의 구조를 설명할 수 있겠지만, 프로젝트 규모가 꽤 커서 혼자 한 번에 정리하기 어려운 점이 있어 우선은 이정도로 설명하고 각 기능을 구현한 방법을 포스팅하면서 (가능하다면) 자세히 설명할 것이다.