기본 콘텐츠로 건너뛰기

스프링 `@Value`를 안전하게 쓰는 방법: 주입 시점부터 설정 바인딩 선택 기준까지

스프링 @Value를 안전하게 쓰는 방법: 주입 시점부터 설정 바인딩 선택 기준까지

스프링 부트에서 설정값을 코드 밖으로 빼는 일은 거의 필수에 가깝습니다. 외부 API 주소, 타임아웃, 토큰, 기능 플래그처럼 환경마다 달라지는 값을 소스 코드에 직접 넣어두면 배포와 운영이 빠르게 꼬입니다. 이때 가장 먼저 떠올리기 쉬운 도구가 @Value입니다.

문제는 @Value가 간단해 보인다는 점입니다. 한 줄로 값을 넣을 수 있다 보니 동작 조건과 주입 시점을 놓치기 쉽고, 설정이 커졌을 때도 계속 같은 방식으로 밀어붙이기 쉽습니다. @Value는 분명 유용하지만, 어디까지가 적절한 사용 범위인지 알아두지 않으면 디버깅 비용이 커집니다.

@Value는 어떤 문제를 해결할까

@Value는 외부 설정값 하나를 스프링 빈에 주입할 때 가장 빠르게 적용할 수 있는 방법입니다. 예를 들어 base-url, timeout, enabled 같은 단일 값을 서비스나 컴포넌트 안으로 가져와야 할 때 코드 양이 적고 이해하기 쉽습니다.

핵심은 @Value가 “설정 한두 개를 직접 꺼내 쓰는 도구”라는 점입니다. 값이 적고 맥락이 단순하면 충분히 좋은 선택입니다. 반대로 같은 접두사 아래에 여러 설정이 묶여 있고, 그 설정들이 하나의 의미 있는 그룹을 이룬다면 @Value보다 @ConfigurationProperties가 더 자연스럽습니다.

@Value가 동작하는 조건과 주입 시점

가장 먼저 기억해야 할 사실은 @Value가 스프링이 관리하는 객체에서만 동작한다는 점입니다. @Component, @Service, @Configuration이 붙어 있거나 @Bean으로 등록된 객체여야 합니다. 직접 new로 생성한 객체에서는 스프링이 개입하지 않으므로 @Value도 작동하지 않습니다.

주입 시점도 중요합니다. 생성자 파라미터에 붙인 @Value는 객체를 만들기 전에 해석되어 전달됩니다. 반면 필드 주입과 세터 주입은 객체 생성 이후에 적용됩니다. 그래서 필드 주입 값을 필드 초기화 구문이나 생성자 안에서 먼저 사용하려고 하면 기대한 값이 아직 들어오지 않았을 수 있습니다.

실무에서는 이 차이 때문에 생성자 주입이 가장 안전합니다. 생성 시점에 필요한 값이 명확하고, final 필드로 유지할 수 있으며, 테스트에서도 다루기 쉽기 때문입니다. 반대로 필드 주입은 코드가 짧아 보이지만 객체가 어떤 설정에 의존하는지 숨기기 쉽고, 초기화 순서 문제를 만들기 쉽습니다.

또 하나 자주 헷갈리는 지점은 static 필드입니다. @Value는 일반적인 빈 주입 메커니즘을 따르므로 static 필드에 기대하는 방식으로 쓰는 패턴은 피하는 편이 낫습니다. 정적 접근이 필요해 보인다면 설계 자체를 다시 보는 쪽이 보통 더 낫습니다.

application.yml, 프로파일, 환경 변수는 어떻게 합쳐질까

스프링 부트는 여러 설정 소스를 하나의 환경처럼 합쳐서 사용합니다. 보통 기본값은 src/main/resources/application.yml에 두고, 환경별 차이는 application-local.yml, application-prod.yml, 환경 변수, 커맨드라인 인자로 덮어씁니다.

아래는 가장 흔한 기본 설정 예시입니다.

# src/main/resources/application.yml
external-api:
  base-url: https://dev-api.example.com
  connect-timeout-ms: 1500
  read-timeout-ms: 3000
  enabled: true

security:
  api-token: local-dev-token

실무에서 자주 체감하는 우선순위만 단순하게 정리하면 이렇습니다.

  • 커맨드라인 인자는 매우 높은 우선순위로 기존 값을 덮어씁니다.
  • 환경 변수와 시스템 프로퍼티는 일반적인 설정 파일보다 우선합니다.
  • application-prod.yml 같은 프로파일별 파일은 기본 application.yml 위에 덮어씌워집니다.
  • ${key:default} 형태의 기본값은 해당 키가 없을 때만 사용됩니다.

application.yml에 분명 값이 있는데도 다른 값이 들어온다면, 파일이 무시된 것이 아니라 더 높은 우선순위의 설정이 덮어썼을 가능성을 먼저 봐야 합니다.

@Value를 안전하게 쓰는 가장 무난한 패턴

설정이 꼭 한두 개 수준이고, 아직 별도의 설정 객체까지 만들 필요가 없다면 생성자 주입으로 @Value를 쓰는 방식이 가장 안정적입니다. 기본값이 필요한 항목만 : 문법으로 명시하고, 필수 값은 기본값 없이 두어 애플리케이션 시작 시점에 빠르게 실패하게 만드는 편이 낫습니다.

아래 예시는 실무에서 자주 보는 형태입니다.

import java.net.URI;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class ExternalApiClient {

    private final URI baseUrl;
    private final Duration connectTimeout;
    private final boolean enabled;

    public ExternalApiClient(
            @Value("${external-api.base-url}") URI baseUrl,
            @Value("${external-api.connect-timeout-ms:1500}") long connectTimeoutMs,
            @Value("${external-api.enabled:false}") boolean enabled
    ) {
        this.baseUrl = baseUrl;
        this.connectTimeout = Duration.ofMillis(connectTimeoutMs);
        this.enabled = enabled;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public URI getBaseUrl() {
        return baseUrl;
    }

    public Duration getConnectTimeout() {
        return connectTimeout;
    }
}

이 방식의 장점은 명확합니다. 객체 생성에 필요한 값이 생성자에 모두 드러나고, 필드가 불변으로 유지됩니다. 반대로 필드 주입을 선택하면 생성자나 필드 초기화 로직에서 값을 먼저 쓰지 않도록 더 조심해야 합니다.

또한 기본값은 신중하게 써야 합니다. 선택적 설정에는 유용하지만, 원래 반드시 있어야 하는 값에 기본값을 넣어두면 설정 누락을 너무 늦게 발견할 수 있습니다. 예를 들어 API 토큰처럼 꼭 필요한 값은 애플리케이션이 시작 단계에서 바로 실패하도록 두는 편이 운영 안정성에 유리합니다.

실행 환경에 따라 값이 바뀌는 방식 이해하기

@Value를 붙인 코드가 같아도 실제 주입 결과는 실행 환경에 따라 달라집니다. 그래서 설정 이슈를 볼 때는 자바 코드만 보지 말고, 어떤 프로파일과 어떤 환경 변수로 실행했는지도 함께 봐야 합니다.

예를 들어 아래처럼 실행하면 application.yml의 기본값 대신 환경 변수와 실행 인자가 우선 적용됩니다.

export SPRING_PROFILES_ACTIVE=prod
export EXTERNAL_API_BASE_URL=https://api.example.com
export SECURITY_API_TOKEN=prod-secret-token

java -jar app.jar --external-api.connect-timeout-ms=2500

이 경우 external-api.base-url은 환경 변수 값으로, external-api.connect-timeout-ms는 커맨드라인 인자 값으로 해석됩니다. 특히 환경 변수는 external-api.base-urlEXTERNAL_API_BASE_URL처럼 대문자와 밑줄 형태로 매핑된다는 점을 기억해두면 배포 설정을 읽을 때 훨씬 편합니다.

민감한 값도 주의해야 합니다. 토큰이나 비밀번호가 잘 들어왔는지 확인하고 싶더라도 전체 문자열을 로그로 남기면 안 됩니다. 길이만 확인하거나 앞 몇 글자만 마스킹해서 점검하는 편이 안전합니다.

@Value 사용 시 자주 만나는 문제와 점검 순서

@Value 문제는 대부분 몇 가지 패턴으로 반복됩니다. 에러를 만나면 아래 순서대로 확인하는 것이 가장 빠릅니다.

  • 이 객체가 정말 스프링 빈인지 확인합니다.
  • 어디선가 new로 직접 생성하고 있지 않은지 확인합니다.
  • 프로퍼티 키 오타가 없는지 확인합니다.
  • 활성 프로파일이 예상과 같은지 확인합니다.
  • 필드 주입 값을 생성자나 정적 컨텍스트에서 먼저 사용하지 않았는지 확인합니다.
  • 필수 설정인데 기본값으로 숨겨두지 않았는지 확인합니다.

설정 키가 없으면 보통 시작 단계에서 아래와 비슷한 오류가 보입니다.

Could not resolve placeholder 'security.api-token' in value "${security.api-token}"

이 메시지가 나오면 원인은 대개 명확합니다. application.yml 또는 환경 변수에 값이 없거나, 키 이름이 다르거나, 다른 프로파일 파일에서 예상과 다르게 덮어쓰였을 가능성이 큽니다. 문제를 빨리 찾으려면 코드만 보지 말고 실제 실행 커맨드와 배포 환경 변수도 함께 확인해야 합니다.

언제 @ConfigurationProperties로 넘어가야 할까

@Value는 단일 값 주입에는 편하지만, 설정이 조금만 커져도 코드가 금방 흩어집니다. 예를 들어 external-api 아래에 주소, 연결 타임아웃, 읽기 타임아웃, 재시도 횟수, 인증 토큰, 기능 플래그까지 붙기 시작하면 @Value를 여러 줄 늘어놓는 방식은 유지보수성이 급격히 떨어집니다.

이럴 때는 관련 설정을 하나의 타입으로 묶는 @ConfigurationProperties가 더 적합합니다.

import java.net.URI;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {
}

@ConfigurationProperties(prefix = "external-api")
public record ExternalApiProperties(
        URI baseUrl,
        int connectTimeoutMs,
        int readTimeoutMs,
        boolean enabled
) {
}

이 접근의 장점은 분명합니다. external-api 설정이 하나의 객체로 모이고, connect-timeout-msconnectTimeoutMs에 바인딩되는 식의 유연한 바인딩을 활용할 수 있습니다. 서비스에서는 여러 개의 문자열과 숫자를 따로 주입받는 대신 ExternalApiProperties 하나만 받으면 되므로 코드가 훨씬 읽기 쉬워집니다.

선택 기준은 단순하게 잡아도 충분합니다.

  • 값이 한두 개이고 사용 위치가 분명하면 @Value
  • 같은 접두사 아래 설정이 여러 개 모이면 @ConfigurationProperties
  • 설정 객체를 재사용하거나 검증하고 싶으면 @ConfigurationProperties
  • 임시로 빠르게 단일 문자열, 숫자, 불리언을 주입할 때는 @Value

정리

@Value는 작은 설정을 다룰 때 여전히 좋은 도구입니다. 다만 스프링 빈에서만 동작하고, 주입 위치에 따라 시점이 다르며, 설정 우선순위를 모르면 예상과 다른 값을 읽을 수 있다는 점을 함께 이해해야 합니다. 특히 “왜 이 클래스만 값이 안 들어오지?”라는 문제는 대부분 빈 등록 여부와 생성 방식에서 시작합니다.

반대로 설정이 늘어나고 의미 있는 그룹이 만들어지기 시작했다면, @Value를 계속 늘리는 것보다 @ConfigurationProperties로 옮기는 편이 더 안정적입니다. 기준은 복잡하지 않습니다. 단일 값이면 @Value, 설정 묶음이면 @ConfigurationProperties입니다. 이 기준만 분명해도 설정 관련 코드는 훨씬 덜 흔들립니다.

원문 참고

https://www.maeil-mail.kr/question/7

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...