스프링부트로 개발하다가, 빈을 분명히 등록했는데 실행만 시키면 빈을 찾을 수 없다는 에러가 떠서 한참을 고민하다 보니, 빈 등록 시점의 문제라는 걸 알게 되었습니다.
빈을 순차적으로 등록하는 과정 중 아직 등록되기 전의 빈을 사용해야 하는 코드가 먼저 실행되었던 것입니다.
빈의 로딩을 사용시점으로 미루는 @Lazy 어노테이션으로 해결했는데, 그래서 이번 글에서는 @Lazy에 대해 적어보았습니다.
일반적으로 Spring은 애플리케이션이 시작될 때 모든 빈을 생성하고 초기화합니다.
이때 애플리케이션의 규모가 커지면 빈의 수가 많아지고, 초기화 시간이 길어져 앱의 시작 속도가 느려질 수 있습니다.
이러한 문제를 해결하는 방법 중 하나가 바로 @Lazy
어노테이션을 활용하는 것입니다.
@Lazy
어노테이션을 사용하면, 해당 빈의 초기화를 애플리케이션이 실제로 그 빈을 필요로 할 때까지 지연시킬 수 있습니다.
즉, 빈이 처음 요청될 때 초기화가 이루어지므로, 애플리케이션이 시작할 때 불필요한 리소스를 사용하지 않게 되어 초기화 시간을 단축할 수 있습니다.
이 방식은 주로 애플리케이션이 시작될 때 많은 리소스를 사용하는 복잡한 빈들이 있을 때 유용합니다.
@Lazy
를 사용하면, 애플리케이션을 시작하고 바로 사용자에게 서비스를 제공하는 동안 불필요한 초기화 작업을 미루고, 실제로 해당 빈이 필요한 시점에 초기화가 이루어지도록 할 수 있습니다.
예를 들어, 데이터베이스 연결을 담당하는 빈이 있다고 가정할 때, 애플리케이션이 시작되자마자 데이터베이스 연결이 이루어지면 초기화 시간이 길어질 수 있습니다. 또, 빈 로딩 시점이 꼬여 빈이 등록되는 시점보다 빈 호출 시점이 선행하게 되면 빈을 찾을 수 없다는 에러가 나기도 하겠지요.
그러나 @Lazy
를 사용하면 실제로 데이터베이스 연결이 필요한 순간까지 빈의 초기화를 지연시킬 수 있어, 애플리케이션의 초기 성능을 개선할 수 있습니다.
@Lazy의 구동 원리
Spring에서 빈은 기본적으로 애플리케이션 컨텍스트가 시작될 때 모두 초기화됩니다.
이는 "Eager Initialization"이라고 불리며, 모든 빈이 애플리케이션 시작 시점에 바로 생성되고, 필요에 따라 의존성 주입도 함께 이루어지는 방식입니다.
이 방식은 모든 의존성 관계가 미리 주입되기 때문에, 빈의 상태가 완전히 준비된 후 애플리케이션이 실행되도록 보장합니다.
하지만 @Lazy
어노테이션을 사용하면, Spring은 해당 빈을 처음으로 사용할 때까지 초기화를 지연시킵니다.
그럼, 구체적으로 빈을 지연 로딩하는 과정을 알아봅시다.
1. 빈 프록시 등록
@Lazy
어노테이션이 적용된 빈은 애플리케이션이 시작되면서 바로 초기화되지 않습니다.
Spring은 애플리케이션 컨텍스트가 로드될 때 해당 빈을 프록시(Proxy) 형태로 등록합니다.
즉, 실제 객체는 생성되지 않고, 프록시 객체만 마치 대리 객체처럼 생성되어 의존성 주입을 받을 수 있는 상태가 됩니다.
Spring에서는 프록시 객체가 실제 빈의 기능을 대신하되, 해당 빈이 실제로 필요할 때 초기화하도록 합니다.
이 과정은 Java의 동적 프록시나 CGLIB 프록시를 통해 구현될 수 있습니다.
- 동적 프록시 (JDK Proxy): 빈이 인터페이스를 구현한 경우, Spring은 JDK 동적 프록시를 사용하여 @Lazy 어노테이션이 적용된 빈을 프록시로 만들어 둡니다.
- CGLIB 프록시: 빈이 인터페이스를 구현하지 않은 경우, Spring은 CGLIB을 사용하여 해당 클래스의 서브클래스를 동적으로 생성합니다.
이렇게 생성된 프록시 객체는 실제 객체가 초기화되기 전에 먼저 요청을 받습니다.
2. 처음 사용될 때 실제 초기화
빈이 실제로 처음 사용될 때 (어딘가로부터 호출될 때가 되겠죠), Spring은 해당 빈을 초기화합니다.
이 시점에서 프록시 객체가 실제 객체로 교체되며, 빈의 의존성들이 주입되고 초기화 과정이 진행됩니다.
만약 그 빈이 다른 빈에 의존성을 가지고 있다면, 그 의존성 역시 처음 사용되는 시점에 초기화됩니다.
실제 사용 예시
import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Component
@Component
class ServiceA(
@Lazy private val serviceB: ServiceB
) {
fun doSomething() {
serviceB.performAction()
}
}
@Component
class ServiceB {
init {
println("ServiceB is initialized!")
}
fun performAction() {
println("ServiceB action performed!")
}
}
위의 코드에서 ServiceA
는 ServiceB
를 의존성으로 가집니다.
@Lazy
어노테이션이 ServiceB
에 적용되어 있기 때문에, ServiceA
가 처음 호출될 때까지 ServiceB
는 초기화되지 않습니다.
ServiceA
의 doSomething()
메서드가 호출될 때에야 비로소 ServiceB
의 초기화가 이루어집니다.
'Backend' 카테고리의 다른 글
DB Connection Pool에 대해 (0) | 2025.01.31 |
---|---|
사람들은 왜 자바가 아닌 코틀린에 열광할까? (0) | 2025.01.05 |
Nest.js에서 prisma exception 데코레이터로 깔끔하게 핸들링하기 (0) | 2024.11.24 |
WebFlux에서 chunked 스트리밍 request 받기 (3) | 2024.10.27 |
Next.js 14와 Firebase로 간단하게 백엔드 API 만들기 (1) | 2023.12.22 |