Spring Core/02. Spring Container와 DI

05. Profile

  • -
반응형

이번 포스트에서는 Profile에 대해 살펴보자.

 

환경의 분리와 프로파일

실제 프로젝트를 진행하다보면 다양한 개발 환경의 복잡성을 경험하는 것은 불가피하다. 일반적으로 DB를 이용해서 개발할 때 개발자들은 자신의 로컬 DB에서 작업을 시작해서 테스트 서버에서 기능을 검증하고 최종적으로 운영 환경에서 애플리케이션을 서비스한다. 이 과정에서 비지니스 로직 자체는 거의 변하지 않지만 DB 주소, 비밀번호등 인프라 관련 설정은 각 단계(환경)마다 크게 달라질 수 있다. 이런 상황에서 각 환경에 맞춰 설정 파일을 수시로 수정하는 작업은 상당히 번거롭다.

환경이 바뀔 때마다 코드를 바꾸기는 너무 어렵다.

 

이런 문제를 해결하기 위해 환경에 따른 설정을 미리 준비해두고, 필요에 따라 적절한 설정으로 손쉽게 교체할 수 있다면 개발 및 배포 과정이 훨씬 간결하고 효율적으로 이뤄질 수 있다.

필요한 설정 파일을 바꿔치기!

하지만 여전히 여러 환경 설정을 관리하는 과정 자체가 복잡하고 번거로울 수 있다. 이때 유용하게 활용할 수 있는 개념이 바로 '프로파일(profile)'이다.

 

프로파일

프로파일이란 설정(또는 빈)을 목적에 맞게 분리하고 그룹화 해서 관리하기 위한 개념이다. 프로파일은 이름 없는 default 프로파일이름 있는 프로파일로 나눌 수 있다. default 프로파일은 모든 프로파일에 적용될 수 있고 이름있는 프로파일은 해당 프로파일 사용이 선언될 경우만 적용된다.

아래 그림은 dev profile,  test profile, oper profile로 프로파일을 구성하고 각각의 설정 조합을 묶은 형태를 보여준다.

선택한 프로파일에 따라서 적용되는 설정과 빈이 달라진다.

만약 여러 개의 profile이 사용되면 뒤에 선언된 profile 내용이 앞의 내용을 덮어쓴다. 예를 들어 dev, test, oper 순으로 사용 선언을 하면 default > dev > test > oper 의 순으로 속성들이 재정의 될 수 있다.

이런 프로파일을 사용하면 다음의 장점을 얻을 수 있다.

  • 환경별 설정의 명확한 분리: 환경별로 다른 설정을 명확하게 분리함으로써 설정 관리의 복잡성을 줄이고 실수를 방지한다.
  • 유연한 환경 전환: 개발, 테스트, 운영 등 다양한 환경으로 쉽게 전환할 수 있어 개발 및 배포 프로세스가 유연해진다.
  • 보안의 강화: 비밀번호, API 키 등 중요한 설정 정보를 소스코드와 분리하여 관리할 수 있어 보안성이 향상된다.

 

프로파일을 통한 설정 분리

개발  환경과 운영 환경을 아래와 같이 정의하고 이를 profile에 적용해보자.

profile 분리

 

설정 파일 작성

프로파일 별로 설정을 분리할 때는  application-{profile_name}.properties  형태로  해당 profile에서 사용할 내용을 별도의 파일로 작성한다.  그냥 application.properties 파일은 default profile이 적용되며 모든 profile에서 사용된다. 나머지 이름있는 프로파일은 런타임에 해당 프로파일이 적용될 경우만 동작하게 된다. 

위 내용을 처리하기 위해 다음의 설정 파일을 추가해보자.

# application-oper.yml
server:
  db:
    ip: 192.168.0.1
    user: admin
# application-dev.yml
server:
  db:
    ip: 192.168.0.9
    user: test
logging:
  level:
    "[com.doding]": trace

 

사용할 프로파일 설정 - 단위테스트

사용할 profile은 runtime에 설정할 수 있다. 먼저 단위 테스트를 수행할 때에는 테스트 클래스@ActiveProfiles를 이용해서 profile을 지정한다. 

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ActiveProfiles {
    @AliasFor("value")
    String[] profiles() default {};
}

만약 ActiveProfiles에 oper를 선언하면 default + oper 프로파일이 사용되고 dev를 선언하면 default + dev 프로파일이 사용된다. 

@SpringBootTest
@ActiveProfiles({ "oper" })
public class OperProfileTest {

  @Test
  public void dbInfoTest(
      @Value("${server.db.ip}") String ip, 
      @Value("${server.db.user}") String user) {
    assertEquals(ip, "192.168.0.1");
    assertEquals(user, "admin");
  }
}
@SpringBootTest
@ActiveProfiles({ "dev" })
public class DevProfileTest {

  @Test
  public void dbInfoTest(
      @Value("${server.db.ip}") String ip, 
      @Value("${server.db.user}") String user) {
    assertEquals(ip, "192.168.0.9");
    assertEquals(user, "test");
  }
}

 

테스트를 실행할 때 log를 살펴보면 어떤 프로파일로 테스트가 동작하는지 알 수 있다.

14:09:04 [INFO ] c.d.di.pfofile.OperProfileTest.logStartupProfileInfo.660 
 - The following 1 profile is active: "oper"

 

일반적으로 대형의 애플리케이션의 경우 위와 같이 설정 파일을 분리해서 사용할 수 있지만 예제처럼 작은 애플리케이션에서는 다중문서 모드를 이용해 하나의 설정 파일을 쪼개서 여러 프로파일의 내용을 담을 수도있다. 각 하위 문서가 어떤 프로파일인지를 지정하기 위해서는 spring.config.activate.on-profile을 이용한다. 이 설정이 없는 경우는 default profile에 해당한다.

# default profile
logging:
  pattern:
    console: "%clr(%d{HH:mm:ss} [%-5level] %c{20}.%M.%L - %m%n)"
  level:
    com.doding: debug
    org.springframework: info
spring.output.ansi.enabled: always
---
spring:
  config:
    activate:
      on-profile: oper # oper profile
server.db:
  ip: 192.168.0.1
  user: admin
---
spring:
  config:
    activate:
      on-profile: dev # dev profile
server.db:
  ip: 192.168.0.9
  user: test
logging:
  level:
    "[com.doding]": trace
application.properties 에서는 #--- 를 삽입하고 profile 특화 내용을 작성한다. 주의할 점은 #이 profile 구분으로 사용되기 때문에 일반적인 line comment 용도로는 #이 아닌 !을 사용한다. profile을 안쓸꺼면 상관은 없다.
더보기

spring.profiles.group

profile을 세분화해서 사용하다가 여러 개의 profile을 적용해야하는 경우 여러개를 하나의 그룹으로 설정해서 재사용할 수 있는데 이때는 spring.profiles.group을 사용한다.

다음의 설정은 oper과 dev를 묶어서 testing이라는 profile을 구성한다. 

spring:
  profiles:
    group:
      testing: oper, dev   # oper와 dev를 묶어서 testing을 만든다.

따라서 @ActiveProfiles에 사용하려는 profile을 지정할 때 testing만 설정해주면 된다.

@ActiveProfiles(profiles = { "testing" })
public class ProfileOperDevTest { ..}

 

기타

 

Profile을 통한 상황 별 빈 구성

자동차를 운전하면서 상황에 따라 연습용 차, 시험용 차, 실제로 사용하는 차 등 여러 종류의 차를 사용하는 것처럼, 우리의 애플리케이션에서도 상황에 따라 다른 빈이 필요할 수 있다.

이러한 상황에 따라 어떤 빈을 생성할지 결정하기 위해 스프링 프레임워크에서는 @Profile 애노테이션을 제공한다. 이 애노테이션을 사용하면, 활성화된 프로파일에 따라 특정 빈의 생성 여부를 제어할 수 있다.

@Profile 클래스 레벨 또는 메서드 레벨에 선언할 수 있는 애너테이션으로 해당 빈이 어떤 프로파일에 속하는지 지정한다. @Profile을 생략하면 default 즉 모든 profile에서 사용 가능하다.

@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Profile {
  String[] value();
}

 

묵시적 빈 선언에서 프로파일을 지정하는 경우는 다음과 같다.

@Component
@Profile("dev")  // HulkBuster는 dev profile에서만 동작한다.
public class HulkBuster extends IronMan{}

 

명시적 빈 선언에서는 @Configuration 클래스에 선언하면 해당 클래스에서 선언된 모든 빈이 해당 프로파일에 적용된다. @Bean에 지정한 profile은 해당 빈에만 영향을 준다. 다음의 예에서 sPhone은 oper 프로파일이 적용되며 lPhone은 dev 프로파일이 적용된다.

@Configuration // 여기는 설정 정보가 담겨있습니다.
@Slf4j
public class PhoneConfig {
  @Bean // 빈 생성 선언
  @Profile("oper")
  public SPhone sPhone() { // SPhone 타입의 빈 생성, 빈 이름은 sPhone
    log.debug("sphone 빈 생성");
    return new SPhone(); // 실제로 사용될 빈 객체 반환
  }

  @Bean(name = "mylPhone") // 빈 생성 선언: 빈의 이름은 mylPhone
  @Profile("dev")
  public LPhone lPhone() {
    log.debug("lphone 빈 생성");
    return new LPhone();
  }
}

 

앞서 살펴본 예제에서는 SPhone과 LPhone이 동시에 존재하는 경우 발생하는 문제를 확인했습니다. 그러나 실제로는 스마트폰 하나만 사용하는 것이 일반적입니다. 이를 예로 들면, 운영 프로파일에서는 SPhone만 필요하고, 개발 프로파일에서는 LPhone만 필요하다는 것입니다. 이전에는 프로파일 개념 없이 빈을 만들어 두 개의 빈이 동시에 생성되어, 이름 기반으로 빈을 주입하는 방식을 사용하였습니다. 그러나 실제로는 프로파일을 이용하여 필요한 빈만 생성하는 것이 훨씬 효율적인 방법입니다.

package com.doding.di.car;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

public interface Car {}

@Component
@Profile("dev")
@Slf4j
class CarForExercise implements Car {
  public CarForExercise() {
    log.debug("연습용 차량 생성");
  }
}

@Component
@Profile("oper")
@Slf4j
class CarForLife implements Car {
  public CarForLife() {
    log.debug("실생활용 차량 생성");
  }
}

 

위와 같이 빈이 구성되었을 때 Car를 주입 받는 테스트를 해보자. 이때 @ActiveProfiles의 내용을 default, dev, oper일 때 @Test의 assert 문장을 어떻게 구성해야 할지 고민해보자.

package com.doding.di.car;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import lombok.extern.slf4j.Slf4j;

@SpringBootTest
@ActiveProfiles({ "oper" })
@Slf4j
public class CarTest {

  @Autowired(required = false)
  Car car;

  @Test
  public void carTest() {
    //assertNull(car);
    assertNotNull(car);
    log.debug("car: {}", car.getClass().getName());
  }
}

 

사용할 프로파일 설정 - 애플리케이션

애플리케이션을 실행할 때에는 java 실행 시 spring.profiles.active 설정으로 사용할 profile을 지정하면 된다.

java -jar -Dspring.profiles.active=[프로파일명] jar_file_name.jar # -D는 시스템 property 사용
java -jar --spring.profiles.active=[프로파일명] jar_file_name.jar # --는 명령줄 인자 사용

이 옵션을 주는 방법이 개발 툴마다 조금 다를 수 있는데 vscode의 경우 [project_root]/.vscode/launch.json 파일에 args 옵션을 추가해준다.

{
  "configurations": [
    {
      "type": "java",
      "name": "Spring Boot-DitestApplication<ditest>",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "mainClass": "com.doding.di.DitestApplication",
      "args": "--spring.profiles.active=oper,dev",
      "projectName": "ditest",
      "envFile": "${workspaceFolder}/.env"
    }
  ]
}

 

참고로 eclipse 기반에서 실행할 때에는 Run Configuration에서 Arguments > VM arguments에 아래와 같이 추가해주면 된다.

반응형

'Spring Core > 02. Spring Container와 DI' 카테고리의 다른 글

07. 빈의 생명주기  (0) 2024.02.23
06. 빈의 스코프  (0) 2024.02.22
04. DI 처리 - 묵시적 DI  (0) 2020.06.17
03. DI 처리 - 명시적 DI  (0) 2020.06.16
02. Dependency와 Dependency Injection  (0) 2020.06.15
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.