달록 기술 블로그 방문하기 (opens new window)

이 글은 우테코 달록팀 크루 '매트 (opens new window)'가 작성했습니다.

# properties 객체로 다루기

Spring에서 application.yml이나 application.properties에 존재하는 값을 불러오는 방법에는 대표적으로 @Value 애노테이션을 사용한 방법과 @ConfigurationProperties를 사용한 방법이 존재한다. 두 방식을 직접 적용해 본 뒤 차이와 이점에 대해 알아보려 한다.

# @Value 사용하기

@Value는 기본적으로 설정 정보를 단일값으로 주입 받기 위해 사용된다. 아래는 실제 달록 프로젝트에서 적용한 예시이다.

@Component
public class GoogleOAuthClient implements OAuthClient {

    private static final String JWT_DELIMITER = "\\.";

    private final String clientId;
    private final String clientSecret;
    private final String grantType;
    private final String redirectUri;
    private final String tokenUri;
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    public GoogleOAuthClient(@Value("${oauth.google.client-id}") final String clientId,
                             @Value("${oauth.google.client-secret}") final String clientSecret,
                             @Value("${oauth.google.grant-type}") final String grantType,
                             @Value("${oauth.google.redirect-uri}") final String redirectUri,
                             @Value("${oauth.google.token-uri}") final String tokenUri,
                             final RestTemplateBuilder restTemplateBuilder, final ObjectMapper objectMapper) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.grantType = grantType;
        this.redirectUri = redirectUri;
        this.tokenUri = tokenUri;
        this.restTemplate = restTemplateBuilder.build();
        this.objectMapper = objectMapper;
    }
		...
}

간단하게 적용이 가능하지만 공통으로 묶인 프로퍼티가 많아질 경우 코드가 지저분해진다. 이러한 프로퍼티 값들을 객체로 매핑하여 사용하기 위한 애노테이션으로 @ConfigurationProperties가 존재한다.

# @ConfigurationProperties

우리는 때때로 DB 설정을 작성하기 위해 application.yml을 통해 관련 정보를 작성하곤 한다. 아래는 간단한 h2 DB를 연결하기 위한 설정을 적은 예시이다.

spring:
  datasource:
    url: jdbc:h2:~/dallog;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa

이러한 설정들은 어디서 어떻게 활용되고 있을까? 실제 바인딩 되고 있는 객체를 따라가보자.

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
	
    private ClassLoader classLoader;
    private boolean generateUniqueName = true;
	private String name;
	private Class<? extends DataSource> type;
	private String driverClassName;
	private String url;
    ...
}

DataSourceProperties는 우리가 application.yml에 작성한 설정 정보를 기반으로 객체로 추출하고 있다. 이것은 Spring Boot의 자동설정으로 DataSource가 빈으로 주입되는 시점에 설정 정보를 활용하여 생성된다.

간단히 디버깅을 진행해보면 Bean이 주입되는 시점에 아래와 같이 application.yml에 명시한 값들을 추출한 DataSourceProperties를 기반으로 생성하고 있다.

정리하면 우리는 Spring Boot를 사용하며 자연스럽게 @ConfigurationProperties를 활용하여 만든 객체를 사용하고 있는 것이다.

이제 우리가 작성한 설정 값을 기반으로 객체를 생성해서 활용해보자. 아래는 실제 프로젝트에서 사용하고 있는 application.yml의 일부를 가져온 것이다.

...
oauth:
  google:
    client-id: ${GOOGLE_CLIENT_ID}
    client-secret: ${GOOGLE_CLIENT_SECRET}
    redirect-uri: ${GOOGLE_REDIRECT_URI}
    oauth-end-point: https://accounts.google.com/o/oauth2/v2/auth
    response-type: code
    scopes:
        - https://www.googleapis.com/auth/userinfo.profile
        - https://www.googleapis.com/auth/userinfo.email
    token-uri: ${GOOGLE_TOKEN_URI}
    grant-type: authorization_code
...

이것을 객체로 추출하기 위해서는 아래와 같이 작성해야 한다.

@ConfigurationProperties("oauth.google")
@ConstructorBinding
public class GoogleProperties {

    private final String clientId;
    private final String clientSecret;
    private final String redirectUri;
    private final String oAuthEndPoint;
    private final String responseType;
    private final List<String> scopes;
    private final String tokenUri;
    private final String grantType;

    public GoogleProperties(final String clientId, final String clientSecret, final String redirectUri,
                            final String oAuthEndPoint, final String responseType, final List<String> scopes,
                            final String tokenUri, final String grantType) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.redirectUri = redirectUri;
        this.oAuthEndPoint = oAuthEndPoint;
        this.responseType = responseType;
        this.scopes = scopes;
        this.tokenUri = tokenUri;
        this.grantType = grantType;
    }

    public String getClientId() {
        return clientId;
    }

    public String getClientSecret() {
        return clientSecret;
    }

    public String getRedirectUri() {
        return redirectUri;
    }

    public String getoAuthEndPoint() {
        return oAuthEndPoint;
    }

    public String getResponseType() {
        return responseType;
    }

    public List<String> getScopes() {
        return scopes;
    }

    public String getTokenUri() {
        return tokenUri;
    }

    public String getGrantType() {
        return grantType;
    }
}
  • @ConfigurationProperties: 프로퍼티에 있는 값을 클래스로 바인딩하기 위해 사용하는 애노테이션이다. @ConfigurationProperties는 값을 바인딩하기 위해 기본적으로 Setter가 필요하다. 하지만 Setter를 열어둘 경우 불변성을 보장할 수 없다. 이때 생성자를 통해 바인딩 하기 위해서는 @ConstructorBinding을 활용할 수 있다.
  • @ConstructorBinding: 앞서 언급한 것 처럼 생성자를 통해 바인딩하기 위한 목적의 애노테이션이다.
@Configuration
@EnableConfigurationProperties(GoogleProperties.class)
public class PropertiesConfig {
}
  • @EnableConfigurationProperties: 클래스를 지정하여 스캐닝 대상에 포함시킨다.

# 개선하기

@Component
public class GoogleOAuthClient implements OAuthClient {

    private static final String JWT_DELIMITER = "\\.";

    private final GoogleProperties googleProperties;
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    public GoogleOAuthClient(final GoogleProperties googleProperties, final RestTemplateBuilder restTemplateBuilder,
                             final ObjectMapper objectMapper) {
        this.googleProperties = googleProperties;
        this.restTemplate = restTemplateBuilder.build();
        this.objectMapper = objectMapper;
    }
    ...
}

이전 보다 적은 수의 필드를 활용하여 설정 정보를 다룰 수 있도록 개선되었다.

# 정리

우리는 application.yml 혹은 application.properties에 작성하여 메타 정보를 관리할 수 있다. 클래스 내부에서 관리할 경우 수정하기 위해서는 해당 클래스에 직접 접근해야 한다. 하지만 설정 파일로 분리할 경우 우리는 환경에 따라 유연하게 값을 설정할 수 있다. 또한 @ConfigurationProperties 애노테이션을 사용할 경우 클래스로 값을 바인딩하기 때문에 연관된 값을 한 번에 바인딩할 수 있다.

# References.

달록 repository (opens new window)
[Spring] @Value와 @ConfigurationProperties의 사용법 및 차이 - (2/2) (opens new window)
appendix.configuration-metadata.annotation-processor (opens new window)

#우아한테크코스 #달록 #properties
last updated: 7/27/2022, 8:41:24 AM