Spring 기초
Spring에서 다루고 있는 주요 개념들을 올바르게 이해한다.
Spring Context
Spring 애플리케이션이 실행될 때, 기본적으로 하나의 IoC Container가 생성되고, 하나의 ApplicationContext 또는 BeanFactory가 이 역할을 수행한다.
Spring Boot의 자동 구성
@SpringBootApplication
annotation을 통해 자동 구성을 설정할 수 있으며, 이는 다음을 포함한다.
@EnableAutoConfiguration
: Spring Boot의 자동 구성을 활성화한다.@ComponentScan
: 패키지 내의 stereotype(ex.@Component
,@Service
,@Repository
,@Controller
) 등의 annotation으로 선언된 클래스를 찾고 IoC Container에 Bean으로 등록한다.@Configuration
: 클래스가 하나 이상의 @Bean 메소드를 포함하고, Spring 컨테이너에 의해 빈 설정을 제공할 수 있음을 나타낸다.
Bean
별다른 이유가 없는 한, Bean에서는 상태를 가지지 않도록 한다.
IoC Container가 관리하는 객체이다. Bean의 생명주기는 애플리케이션의 생명주기와 일치한다.
Bean이 등록될 때 metadata들도 포함되는데, BeanDefinition
이라고 하는 객체에 class type, scope, 의존성 정보 등이 포함된다.
특히 각 Component 타입(ex. @Service
, @Component
, @Repository
, @Controller
)의 Bean들은 Singleton(Singleton Pattern과는 다소 차이가 있음)으로 인스턴스가 관리되며, 애플리케이션 전체에서 이를 공유한다.
Bean은 unique한 id를 가지게 되는데, Container가 해당 id로 Bean을 찾고 관리한다.
IoC(Inversion of Control) Container
Spring에서 객체의 생성(또는 인스턴스화)을 관리하거나 구성하고 조직하는 역할을 한다.
"The IoC container then injects those dependencies when it creates the bean"
ApplicationContext
Spring에서는 기본적으로 즉시 로딩 방식으로 객체를 생성한다. 가령, Spring의 자동 구성요소인 stereotype(ex. @Service
, @Component
, @Repository
, @Controller
annotation으로 선언된 Bean들)은 기본적으로 ApplicationContext가 초기화될 때 즉시 생성되며, 의존 관계에 있는 Bean 역시 함께 생성된다.
또는 생성 시점을 @Lazy
annotation으로 지연 로딩되도록 변경할 수도 있다.
BeanFactory
DI와 지연 로딩 방식으로 객체의 생성을 관리한다. DI를 통해 객체를 구성하고, 지연 로딩 방식으로 필요한 시점에 객체를 인스턴스화 한다.
즉, BeanFactory는 Bean을 처음부터 생성하지 않고, 해당 Bean이 필요한 순간에 지연 로딩 방식으로 인스턴스화 한다. 그리고 인스턴스화 하는 시점에 의존 관계에 있는 Bean들을 찾아서 모두 인스턴스화 한 다음, 의존성이 만족되는 순간 주입한다.
DI(생성자 주입, 세터 주입, 필드 주입)와 지연 로딩은 객체를 생성을 관리한다는 공통점이 있지만, 의미와 목적은 분명히 다르다.
DI는 Bean들간의 의존성을 자동으로 주입하기 위해 사용되며, 지연 로딩은 Bean 자체를 생성하는 방식으로 사용된다. DI는 외부(컨테이너 또는 객체) 에서 주입을 받는 방식이고, 지연 로딩은 필요 시점까지 인스턴스를 초기화하지 않는 방식이다.
따라서 DI는 결합도를 낮추기 위해, 지연 로딩은 리소스를 효율적으로 사용하기 위해 사용된다.
올바르게 이해했다면
IoC 개념
전통적인 접근 방식에서는 객체가 필요한 의존성을 직접 생성(new
)하고 관리했다. 하지만 Spring에서는 객체가 필요한 시점에 외부에서 주입하는 등, 관리 주체가 외부(IoC Container)로 변경되었기 때문에, 의존 관계가 역전되었다고 표현한다.
이로써 얻는 장점은 의존 관계에 있는 객체를 직접 관리할 필요가 없기 때문에, 의존 관계만 맺고 사용하는 것에만 집중할 수 있다는 점이다.
ApplicationContext에서 Bean 주입 시점
Bean들은 하나의 ApplicationContext 내에서 서로 의존 관계를 가지고 있고, 이러한 정보를 Bean에 포함된 metadata로 인해 IoC Container가 알고 있다.
ApplicationContext가 초기화되는 시점에 Bean으로 등록된 모든 Bean과, 해당 Bean에 의존 관계에 있는 모든 Bean들 또한 인스턴스화 되고, 각 인스턴스들의 참조가 주입된다.
예시를 통해 살펴보자.
만약 A, B, C 세 개의 Bean이 존재하고 A가 B와 C에 의존 관계를 가지고 있다고 생각해보자. 기본적으로는 모두 즉시 로딩되어 인스턴스화 되어 메모리에 로드 됐을 것이다.
그런데 만약 A에 @Lazy
로 지연 로딩 방식을 선언한 경우라면, A는 ApplicationContext 초기화 시점에 인스턴스화 되지 않고, 대신 B와 C는 인스턴스화 된다. 그리고 A를 사용하는 시점에 이미 인스턴스화 된 B와 C의 참조가 주입된다.
또는 A가 아닌, B가 @Lazy
방식이라면 어떨까? 그런 경우, 최초 A와 C가 인스턴스화 되고, B는 인스턴스화 되지 않는다. 그리고 A에서 B를 사용하는 시점에 그제서야 B가 인스턴스화 되어 참조가 주입된다.
Singleton 객체와 상태
다음의 코드는 뭐가 문제일까?
@Service
class CatService(
private val name: String,
private val age: Int,
private val catRepository: CatRepository
) { ... }
완전히 잘못 작성된 코드이며, name
과 age
를 생성자에서 없 앤다면 문제가 없어보인다. 그런데 정확히 왜일까?
Spring의 @Service
annotation과 같은 stereotype의 클래스는 상태를 가지지 않는, 인터페이스로 제공되는 연산을 수행하는 독립된 서비스 계층이다. 그런데 내부적으로 상태를 가지는 name
과 age
가 전달된 것이다.
도메인 서비스는 순수하게 로직만을 처리해야하고 상태를 가지지 않아야 한다. name
과 age
가 런타임 시에 전달받게 되면, 최초 호출 시의 값이 애플리케이션 생명주기 동안 유지되게 된다. 그런데, Spring은 멀티 스레드를 지원하기 때문에, 동시에 여러 요청이 발생한 경우, 스레드 안전성에 문제가 발생할 수 있다.
따라서 특정 상태값이 필요한 경우, 아래와 같이 메서드의 인자로 주입받게 해야한다.
@Service
class CatService(private val catRepository: CatRepository) {
fun introduce(name: String, age: Int) {
...
}
}
이와 달리, 환경 변수 등 시스템의 설정 값을 전달받아야 하는 경우
@Value
annotation을 통해 값을 전달 받을 수 있다.@Service
class SomeService(
@Value("\${some.key}") private val someField: String
) { ... }