1장에서 알아본 Application을 여러가지 환경에 배포하기 위한 Profile설정에 대해서 정리에 이어서
Profile을 활용한 실행 환경제어에 대해서 정리한다.

1장: Profile의 활용 - Gradle, Maven Build Profile과 Spring Profile

1장 마지막에 언급한 것처럼 각각의 Region에서 Api 호출에 대한 권한 데이터 확인을 서로 다른 방식을 사용한다 요구사항을 Profile로 해결해 보자.

갑자기 왜 필요했나?

먼저 이와 같은 요구사항이 생겨난 것은 Containerize하여 배포하기 위해 준비하면서 이다. 기존 서버에 구동시키는 Application과는 달리 Container는 외부 연동 시스템과 다같이 묶어서 배포하지 않는 이상 외부 종속성 문제가 발생할 수 있다. 예를 들어 DB를 접속하는 Application을 컨테이너 배포를 할때 DB도 같이 배포하거나 기존 구축되어 있는 DB를 연결하거나 또 다른 방식을 사용해야만 한다.

우리는 Container환경에서의 권한제어는 dynamic한 데이터가 아닌 정해진 데이터를 기반으로 간단한 수준으로 하면 될것 같다 라는 전제조건에 동의하고 아래 두가지 경우에 대해서 개발을 진행했다.

  • DB에 저장된 데이터를 기반으로 하는 권한제어
  • File에 Static하게 작성된 데이터를 기반으로 하는 권한제어

기술 스택

  • spring-boot
  • webflux
  • reactor
  • cassandra

기존 구현

기존 DB에 저장된 데이터를 기반으로 하는 권한제어는 아래와 같이 작성되었다.

public interface CassandraAuthRepository extends ReactiveCassandraRepository<Auth, String> {
    Mono<Auth> findById(String appkey);
}

위의 Repository를 이용하여 Authentication을 제공하는 AuthService는 Repository를 이렇게 활용한다.

@Component
public class AuthRepositoryService {

    private final CassandraAuthRepository cassandraAuthRepository;

    @Autowired 
    public AuthRepositoryService(CassandraAuthRepository cassandraAuthRepository) {
        this.cassandraAuthRepository = cassandraAuthRepository
    }

    public Mono<Boolean> isAuthenticated(String appkey, ...) {
        Mono<Auth> auth = cassandraAuthRepository.findById(appkey)
        ...
    }

가장 단순한 spring-data를 활용한 cassandra db 접근, 또 authentication을 수행하는 함수의 구현이다.

데이터 소스 추가, @Profile

위 두가지 방식의 권한제어에 대해서 개발하여 적용하기 위해서는 일단 가장 기본적으로 인터페이스를 기반으로 하는 추상화가 필요하다.
Service 레벨의 인터페이스로도 가능하지만 실제 데이터를 읽어내는 Repository 레벨의 인터페이스를 구현했다. 아래의 간단한 인터페이스를 예시로 들어본다.

public interface AuthRepository {
   Mono<Auth> findById(String appkey);
}

위 인터페이스에 대한 구현체를 2가지 만들텐데, profile이름을 할당하고 둘중 하나의 구현체만 항상 생성되도록 설정할 예정이다.
conditional하게 bean을 만들지 말지에 대한 결정은 spring에서 제공하는 @Profile 어노테이션으로 가능하다.

  • cassandra
    • profile: cassandra-auth
@Profile("cassandra-auth")
public interface CassandraAuthRepository extends ReactiveCassandraRepository<Auth, String>, AuthRepository {
    Mono<Auth> findById(String appkey);
}
  • file
    • profile: local-auth
@Profile("local-auth")
@Component
public class LocalAuthRepository implements AuthRepository {

    private final LocalAuthProperty localAuthProperty; // property 파일을 위한 component
    public LocalAppkeyAuthRepository(LocalAuthProperty localAuthProperty) {
        this.localAuthProperty = localAuthProperty;
    }
    @Override
    public Mono<AppkeyAuth> findById(String appkey) {
        ...
    }
}

위와 같이 AuthRepository에 대한 두개의 구현체가 생기게 되고 이를 활용하는 Service에서는 아래와 같이 변경될 수 있다.

@Component
public class AuthRepositoryService {

    private final AuthRepository authRepository;

    public AuthRepositoryService(
        @Autowired(required = false) CassandraAuthRepository cassandraAuthRepository, 
        @Autowired(required = false) LocalAuthRepository localAuthRepository) {
        if ((Objects.nonNull(cassandraAuthRepository) && Objects.nonNull(localAuthRepository))
            || (Objects.isNull(cassandraAuthRepository) && Objects.isNull(localAuthRepository))) {
            throw new RuntimeException("Multiple repository profile specified or repository profile not specified. only one of 'local-auth' and 'cassandra-auth' must be enabled");
        }

        if (Objects.nonNull(cassandraAuthRepository)) {
            this.authRepository = cassandraAuthRepository;
        } else {
            this.authRepository = localAuthRepository;
        }
    }

    public Mono<Boolean> isAuthenticated(String appkey, ...) {
        Mono<Auth> auth = authRepository.findById(appkey)
        ...
    }

실행

위와 같이 작성된 Application을 정상적으로 수행시키기 위해서는 Repository profile인 cassandra-authlocal-auth profile을 active해야 한다. profile을 active하는 방법은 2가지가 존재한다.

  • Application을 실행 할 때 option으로 전달.
  • 기본적으로 로드되는 application.yml 파일에 active또는 include로 작성.

Option으로 Profile active.

spring application을 구동시킬때 -Dspring.profiles.active라는 옵션을 전달하면 해당 profile을 active할수 있다. option명에서도 보이듯이 여러개의 profile을 동시에 active시킬수 있으며 콤마로 구분한다.

-Dspring.profiles.active=local-auth,container

##application-container.yml
server.port: 8080
##application-local-auth.yml
auth:
  id: test
  role: admin

위와 같이 2개의 profile을 active하게 되면 spring은 기본적으로 classpath에 존재하는 application-{profile}.yml 파일을 property로 로드하게 된다.

property파일로 active

또는 container환경에 대해서는 항상 local-auth를 enable하고 싶다면 container profile이 active될때 로드되는 application-container.yml property파일로 local-auth profile을 active 시킬 수 있다.

-Dspring.profiles.active=container

##application-container.yml
server.port: 8080
spring:
  profiles:
    active: container
    include:
      - local-auth
##application-local-auth.yml
auth:
  id: test
  role: admin

property파일 내에서 환경 분리

위의 두가지 방법과 더불어 추가로 로드된 property파일 내에서 또 한번 더 환경분리가 가능하다.
만일 cassandra db 정보를 kr region에 배포를 할 예정인데 독립된 두가지 환경을 만들 예정이라면 아래와 같이 kr1, kr2로 분리하여 환경 설정이 가능하다.

##application-common.yml
server.port: 8080
##application-kr1.yml
spring:
  profiles:
    active: kr1
    include:
      - common
      - cassandra-auth
      - kr1
##application-kr2.yml
spring:
  profiles:
    active: kr2
    include:
      - common
      - cassandra-auth
      - kr2
##application-cassandra-auth.yml
spring:
  profiles: kr1
cassandra:
  hosts:
    - 1.1.1.1
  port: 11042
  keyspace: kr1
  user: kr1
  password: kr1
---
spring:
  profiles: kr2
cassandra:
  hosts:
    - 1.1.1.1
  port: 11042
  keyspace: kr2
  user: kr2
  password: kr2

위와 같이 작성된 property파일이 있을 때 -Dspring.profiles.active=kr1 이라는 옵션으로 Application을 구동시키면 아래와 같은 property파일을 읽는것과 같은 효과를 볼수 있다.

server.port: 8080
spring:
  profiles:
    active: kr1
    include:
      - common
      - cassandra-auth
      - kr1
cassandra:
  hosts:
    - 1.1.1.1
  port: 11042
  keyspace: kr1
  user: kr1
  password: kr1

여기서 spring.profiles.include 가장 마지막에 한번더 작성된 kr1이라는 profile은 약간의 꼼수(?) 일지도 모르겠다.. 실제 테스트 도중에 application-cassandra-auth.yml에 분리해 놓은 spring profile이 제대로 적용되지 않는 것 같아서 이것저것 시도해 보다가, cassandra-auth프로파일 아래에 kr1 프로파일을 한번 더 적어주니 정상적으로 로드 되었다. include된 profile의 순서가 뭔가 property파일을 읽어 처리하는데에 영향이 있는것 같다.

결론

지금까지 작성한 Application을 여러가지 환경으로 배포하는 방법에 대해서 정리하면 다음과 같다.

  • build profile을 위한 리소스 폴더 분리
  • spring profile을 이용한 로드되는 리소스 파일 분리
  • spring profile을 이용한 생성되는 Component분리
  • spring profile을 이용한 하나의 리소스 파일 내의 환경 분리

위에 나열된 모든 방법을 이용하여 최근 서로 다른 요구사항의 7가지 환경에 대해 동일한 코드 베이스의 Application을 배포하였다. DB가 다른 경우도 있고, Authentication 방식이 다른 경우(DB, File), 외부 Api의존성을 가지는 경우와 아닌경우 등등에 대해서 모두 만족할 수 있는 배포를 할 수 있었다.

이러한 배포과정에서 Spring Application을 Fully Executable한 jar로 빌드하여 실행하였는데, 따로 background process를 만들기 위해 스크립트를 짜거나 하지 않아도 되고, 내장 톰켓을 사용하며, 서비스로 등록하여 서버 리붓과 같은 상황에서도 Application이 재구동 될 수 있는 환경을 만들기에 아주 편리했다.

추후 시간이 나면 정리해야 겠다.

BELATED ARTICLES

more