728x90
반응형

[Spring] @Value 주입이 null인 경우

환경 : Spring Boot 3.x, Kotlin 1.9



1. 원인

  • static 필드에 직접 사용 — Spring이 주입 불가
  • new 키워드로 생성한 객체 — Spring 빈이 아님
  • 생성자 호출 시점에 다른 빈에서 해당 필드 참조 — 초기화 순서 문제
  • property 키 오타 또는 application.yml 누락
  • SpEL 표현식 오류


2. 해결 방법

2.1) static 필드 금지 — 생성자 주입 사용

Spring은 인스턴스 필드에만 의존성 주입 가능. static 필드는 클래스 레벨이라 주입 안 됨

// 동작 안 함
@Component
class AppConfig {
    companion object {
        @Value("\${app.name}")
        lateinit var appName: String // null
    }
}

// 정상
@Component
class AppConfig(
    @Value("\${app.name}") val appName: String
)

어쩔 수 없이 static처럼 써야 하면 setter trick

@Component
class AppConfig {
    companion object {
        lateinit var appName: String
            private set
    }

    @Value("\${app.name}")
    fun setAppNameStatic(value: String) {
        appName = value
    }
}

하지만 위 방식보다는 @ConfigurationProperties로 객체로 관리하는 게 더 깔끔


2.2) new 키워드 금지 — 빈으로 등록

new로 만든 객체는 Spring 컨테이너 밖이라 @Value 동작 안 함

// 동작 안 함
class EmailService {
    @Value("\${mail.from}")
    lateinit var from: String
}

@Service
class NotificationService {
    private val emailService = EmailService() // new와 동일 → @Value 무시
}
// 정상 — 빈으로 등록
@Component
class EmailService(
    @Value("\${mail.from}") val from: String
)

@Service
class NotificationService(
    private val emailService: EmailService // DI
)

2.3) 초기화 순서 문제 — @PostConstruct에서 사용

생성자 시점에 다른 필드를 참조하면 주입 전 상태일 수 있음

// 동작 안 함
@Component
class AppConfig {
    @Value("\${app.name}")
    lateinit var appName: String

    val fullName = "App: $appName" // 생성자 시점 → null
}

// 정상 — @PostConstruct 사용
@Component
class AppConfig {
    @Value("\${app.name}")
    lateinit var appName: String

    lateinit var fullName: String

    @PostConstruct
    fun init() {
        fullName = "App: $appName" // 주입 완료 후 실행
    }
}

2.4) property 키 확인 — default 값 설정

오타나 키 누락 시 기본값으로 방어

// 키 오타나 누락 시 예외 발생
@Value("\${app.nmae}") // 오타
lateinit var appName: String

// default 값 사용
@Value("\${app.name:DefaultApp}")
lateinit var appName: String

숫자나 boolean도 동일

@Value("\${app.timeout:30}")
var timeout: Int = 0

@Value("\${app.enabled:true}")
var enabled: Boolean = false

2.5) SpEL 표현식 오류

#{...}는 SpEL, ${...}는 property placeholder. 헷갈리지 않기

// property 값
@Value("\${app.name}")
lateinit var appName: String

// SpEL 표현식
@Value("#{T(java.lang.Math).random() * 100}")
var randomValue: Double = 0.0

// property를 SpEL에서 사용
@Value("#{\${app.timeout} * 1000}")
var timeoutMs: Long = 0

SpEL 문법 오류 시 앱 시작 자체가 실패하므로 쉽게 발견됨


2.6) @ConfigurationProperties 권장

여러 property를 다룰 때는 @Value보다 @ConfigurationProperties가 안전

// application.yml
app:
  name: MyApp
  timeout: 30
  mail:
    from: no-reply@example.com
    host: smtp.example.com
@ConfigurationProperties(prefix = "app")
@ConstructorBinding
data class AppProperties(
    val name: String,
    val timeout: Int,
    val mail: MailProperties
)

data class MailProperties(
    val from: String,
    val host: String
)

@EnableConfigurationProperties(AppProperties::class)
@SpringBootApplication
class Application
@Service
class NotificationService(
    private val appProperties: AppProperties
) {
    fun send() {
        println("앱: ${appProperties.name}, 발신: ${appProperties.mail.from}")
    }
}

타입 안전성 + IDE 자동완성 + 검증 통합 가능



3. 디버깅

3.1) Environment에서 직접 확인

property가 실제로 로드됐는지 확인

@Component
class DebugConfig(private val env: Environment) {
    @PostConstruct
    fun check() {
        val appName = env.getProperty("app.name")
        println("app.name = $appName") // null이면 yml 파일 확인
    }
}

3.2) default 값으로 누락 확인

@Value("\${app.name:NOT_FOUND}")
lateinit var appName: String

@PostConstruct
fun check() {
    if (appName == "NOT_FOUND") {
        println("app.name property 누락")
    }
}

3.3) 주입 시점 로그

@Component
class AppConfig(
    @Value("\${app.name}") val appName: String
) {
    init {
        println("생성자 시점 appName: $appName")
    }

    @PostConstruct
    fun postConstruct() {
        println("@PostConstruct 시점 appName: $appName")
    }
}

init@PostConstruct 모두에서 찍히면 정상. init에서만 찍히고 값이 이상하면 주입 실패


※ 생성자 주입 + @ConfigurationProperties가 가장 안전. @Value는 간단한 값 하나만 쓸 때 사용


728x90
반응형

+ Recent posts