[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는 간단한 값 하나만 쓸 때 사용
'TroubleShooting' 카테고리의 다른 글
| [Spring] @Scheduled 멀티 인스턴스 중복 실행 (0) | 2026.05.06 |
|---|---|
| [Spring] @Async 동작하지 않는 경우 (0) | 2026.04.29 |
| [JPA] save()가 INSERT 대신 SELECT를 날리는 경우 (0) | 2026.04.15 |
| [JPA] MultipleBagFetchException (0) | 2026.04.04 |
| [vue] CORS 해결 (1) | 2022.06.19 |