IoC(Inversion Of Control): 제어권의 역전, 프로그램의 제어 흐름이 뒤바뀌는 것.
들어가기에 앞서
UserDao 클래스는 사용자의 정보를 DB에 넣고 관리할 수 있는 클래스이다. (토비의 스프링 1장 초난감 DAO 참조)
예시를 들어 설명해보자.(다음 예시는 Spring과 별개의 프로그램으로 생각한다.) 다음과 같이 UserDao는 main내에서 생성되고 ConnectionMaker 생성자에 userDao를 넘겨준다. 통상적으로 다음과 같이 userDao 인스턴스를 직접 만들어서 또다른 생성자에 넘겨주는건 흔하고 일반적이다.
fun main() {
val userDao = UserDao()
val connectionMaker = ConnectionMaker(userDao)
connectionMaker.disconnect()
}
제어의 역전은 이런 흐름을 뒤집는다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 다음 코드 예시를 보자
fun main(connectionMaker: ConnectionMaker) {
connectionMaker.disconnect()
}
첫번째와 다르게 main에서 connectionMaker에 대해 직접 제어 하지 않는다. connectionMaker 에 대한 인스턴스 생성은 main 에서 이뤄지지 않고 main을 호출하는 또 다른 어딘가에서 이를 관리한다.
이것을 제어의 역전 이라고 한다.
프레임워크와 라이브러리의 차이
질문1
언뜻보면 비슷하지만, IoC의 관점에서 보면 둘은 다르다. 라이브러리는 직접 어플리케이션을 제어한다. 하지만 프레임워크는 어플리케이션을 직접 제어하지 않는다. 오히려 어플리케이션에 의해 프레임워크가 제어를 받는다. 이러한 관점에서 보면 라이브러리는 어플리케이션이 필요에 따라 직접 제어하지만, 프레임워크는 어플리케이션의 제어를 제한한다.
스프링에서의 IoC
@Configuration 을 이용한 어플리케이션 컨텍스트 설정 예시
스프링에서 Swagger 를 사용한다고 생각해보자. 이를 위해서 SwaggerConfiguration 이라는 클래스를 만들어서 다음과 같이 설정할 수 있다.
@Configuration
class SwaggerConfiguration {
@Bean
fun getOpenAPI(): OpenAPI {
return OpenAPI()
.servers(listOf(Server().apply { url = "/" }))
.info(
Info().title("api")
.description("API")
.version("v0.1.0")
)
.components(
Components()
.addSecuritySchemes(
securityRequirementName,
securityScheme
)
)
.externalDocs(
ExternalDocumentation()
.description("API")
)
}
}
용어 정리
질문2
빈: 스프링이 IoC 방식으로 관리하는 오브젝트, 스프링이 직접 생성과 제어를 담당한다.
빈 팩토리: 빈을 등록하고 조회하고 관리하는 핵심 컨테이너
애플리케이션 컨텍스트: 빈팩토리를 확장한 IoC 컨테이너, 빈 팩토리와 기본적으로 기능은 같으나, 스프링의 부가 서비스를 추가로 제공한다.
설정 정보/ 설정 메타 정보: 스프링의 애플리케이션 컨텍스트가 IoC를 적용하기 위해 사용하는 메타 정보
IoC 컨테이너: IoC 방식으로 빈을 관리하는 컨테이너, 앞의 어플리케이션 컨텍스트나 빈 팩토리를 그냥 IoC 컨테이너라 하기도 한다. ‘컨테이너’ 라는 단어가 어플리케이션 컨텍스트 보다는 추상적이다. 하나의 어플리케이션에는 여러개 어플리케이션 컨텍스트가 구현될 수 있는데 이걸 뭉뚱그려 그냥 스프링 컨테이너라고 하기도 한다.
@Configuration 어노테이션으로 애플리케이션 컨텍스트에 등록하는 경우, 가져오는 오브젝트(Bean)은 동일한 오브젝트를 가져올 것이다. 이는 스프링의 애플리케이션 컨텍스트가 오브젝트를 기본적으로 싱글톤(Singleton)으로 취급한다는 것을 알 수 있다.
주의: 참고로 스프링에서 취급하는 싱글톤 오브젝트는 일반적으로 디자인 패턴에서 구현되는 싱글톤 패턴의 구현방법과는 약간 다르다. (후술)
왜 스프링에서는 오브젝트(Bean)를 싱글톤으로 만들까
질문3
만약 웹서버에 들어오는 요청이 많아진다고 생각해보자, 스프링에서 제공하는 오브젝트가 싱글톤이 아닌 요청당 인스턴스를 생성하는 방식으로 처리할 수도 있다. 하지만 그렇게 되면 요청이 증가할 수록 인스턴스도 늘어나고 메모리 점유도 그만큼 높아진다. 초당 500개의 요청이 들어오고 한 번에 새로운 오브젝트가 5개씩 생성되면 2500개의 오브젝트가 생성되는 꼴이다. 자바의 GC 성능이 좋다 한들 이는 서버에 많은 부하를 준다.
스프링은 기본적으로 엔터 프라이즈 시스템 을 위해 고안된 기술이다. 우리가 구현할 시스템은 기본적으로 당연히 요청이 많을 수밖에 없고, 높은 성능을 요구받는다.
싱글톤의 한계
싱글톤 레지스트리
앞서 말했듯이 스프링의 싱글톤은 약간 구현방식이 다른데, 이를 위해 스프링에서 제공되는 것 중 하나가 ‘싱글톤 레지스트리’ 이다. 일반적으로 자바에서 구현하는 싱글톤 패턴과 다르게 싱글톤 레지스트리에서는 public 생성자를 가진다. 따라서 테스트 환경에서도 사용하기가 편하다.
싱글톤과 오브젝트 상태
싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근하는 것이 가능하다. 그러다 보니 싱글톤 인스턴스를 만들때는 가급적 stateless 하게 만들어야 한다. 즉, 읽고 쓰는 가변상태가 없어야 한다는 것이다.
가변 상태가 있는 예시(Stateful)
class Stateful {
private var orderNo = 0
fun order(orderNo: Int) {
this.orderNo = orderNo // 여기서 문제가 발생한다.
}
}
가변 상태가 없는 예시(Stateless)
class UserDao {
private val connectionMaker = ConnectionMaker()
constructor() {
// connectionMaker 를 초기화
}
fun getUserDao() {
// userDAO를 반환
}
}
일반적인 자바/코틀린에서의 싱글톤 패턴 구현
질문4
Kotlin의 경우 언어차원에서 싱글톤 구현을 지원한다.
object MySingleton {
}
Java의 경우 싱글톤을 구현하려면 다음과 같이 생성자를 private 로 만들고 싱글톤 인스턴스를 가지는 static 변수를 하나 선언한다.
class MySingleton {
private static MySingleton singleton;
public getInstance() {
if (singleton == null {
singleton = new MySingleton();
}
return singleton;
}
private MySingleton() {
}
}
스프링 부트에서 오브젝트 스코프 (추가)
앞서 설명했듯 기본은 싱글톤 스코프이지만 이외에도 여러 스코프가 존재한다.