싱글톤 패턴(Singleton Pattern) 이란 디자인 패턴중 하나로, 객체의 인스턴스가 하나만 생성되어 관리되는 것을 말한다.
해당 글은 싱글톤 자체와 관련된 내용이 아닌, 싱글톤 방식으로 동작하는 스프링 컨테이너에 관한 글이 되겠다.
따라서 싱글톤 패턴에 대한 자세한 설명은 아래의 글을 참고하면 되겠다.
https://jnsodevelop.tistory.com/38
[Design Pattern] Singleton Pattern (싱글톤 패턴)
Singleton Pattern Singleton Pattern (싱글톤 패턴) 은 하나의 클래스가 오직 하나의 인스턴스만 가지는 패턴이다. 싱글톤 인스턴스는 최초 생성시 하나만 만들어지며, 해당 인스턴스를 다른 모듈이 공유
jnsodevelop.tistory.com
1️⃣ 싱글톤 패턴의 문제점
싱글톤 패턴은 하나의 인스턴스만 생성한다는 특징 때문에 많은 객체를 생성하지 않아도 된다는 장점이 있다.
그러나 장점이 있는 만큼 단점 또한 존재하는데 크게 아래와 같은 문제점이 있다.
- 싱글톤 패턴 구현시 코드가 복잡해질 수 있다.
- 클라이언트가 구체 클래스에 의존하게 되어 DIP를 위반한다. (getInstance() 등의 사용)
- 클라이언트가 구체 클래스에 의존하게 되어 OCP를 위반할 수 있다.
- 테스트가 어렵다.
- 내부 속성의 변경이나 초기화가 어려워 전체적으로 유연성이 떨어질 수 있다.
그동안 공부했던 스프링 컨테이너는 객체 인스턴스들 (스프링 빈)을 싱글톤으로 관리한다.
그렇다면 어떻게 위와 같은 싱글톤 패턴의 문제점을 해결하면서 인스턴스들을 관리할 수 있을까?
바로 몇 가지 주의점을 지키며 코드를 작성하면 된다.
싱글톤은 하나의 객체 인스턴스가 생성되어 공유되기 때문에 상태를 유지하게 설계하면 안된다.
즉, 'stateful' 이 아닌 'stateless' 로 설계해야 한다는 것이다.
아래는 우리가 싱글톤 패턴을 사용할 때에 주의해야할 점이다.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안되며, 가급적 읽기만 가능해야 한다.
- 필드 대신 공유할 수 없는 방식(지역 변수, 파라미터, ThreadLocal 등) 을 사용해야 한다.
2️⃣ 싱글톤 컨테이너
결론부터 말하자면 스프링 컨테이너는 싱글톤 컨테이너라고도 할 수 있다.
스프링 컨테이너에서 관리되는 객체 인스턴스들은 싱글톤으로 관리되기 때문이다.
그렇다면 바로 확인해보도록 하자.
public class SingletonTest {
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(SingletonConfig.class);
@Test
@DisplayName("싱글톤 객체 확인")
void checkSingleton(){
MemberRepository bean1 = ac.getBean("memberRepository", MemberRepository.class);
MemberRepository bean2 = ac.getBean("memberRepository", MemberRepository.class);
System.out.println("bean1.toString() = " + bean1.toString());
System.out.println("bean2.toString() = " + bean2.toString());
Assertions.assertThat(bean1).isEqualTo(bean2);
}
@Configuration
static class SingletonConfig{
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
}
우선 'SingletonConfig' 라는 static class 를 만들어 'memberRepository' 를 스프링 빈으로 등록했다.
그렇다면 현재 스프링 컨테이너상에 해당 객체 인스턴스가 저장될 것이다.
다음으로 테스트 코드에서 ac.getBean() 메소드를 통해 MemberRepository 타입의 bean1, bean2 에 각각 주입해준다.
memberRepository() 메소드가 호출될 때 new 연산자를 통해 MemoryMemberRepository를 반환한다면 bean1 과 bean2 는 서로 다르지 않을까 라는 생각을 할 수도 있다.
이를 눈으로도 확인해보고 실제 테스트로도 검증하기 위해 System.out.print 와 assertj 를 사용하였다.
결과를 확인해보자.
둘 모두 MemoryMemberRepository@299321e2 로 같은 객체가 나왔다.
이는 print 문으로 출력한 결과이며, 테스트가 통과된 것을 보니 isEqualTo를 통해 둘은 동일한 객체라는 것을 알 수 있다.
그렇다면 어떠한 방식으로 싱글톤 패턴이 적용된 것일까?
그 비밀은 바로 '@Configuration' 어노테이션에 있다.
3️⃣ @Configuration
스프링 컨테이너는 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
@Configuration 은 스프링이 바이트 코드를 조작할 수 있도록 만들어주는 아주 중요한 어노테이션이다.
거두절미하고 아래 코드를 보자.
@Test
void configurationTest(){
ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
AnnotationConfigApplicationContext 의 파라미터인 AppConfig 는 스프링 빈이 된다.
따라서 스프링 컨테이너에 저장되고, 우리는 이를 조회할 수 있다.
AppConfig 에 대한 정보를 조회해보자.
@Test
@DisplayName("AppConfig 정보 조회")
void getAppConfigInfo(){
AppConfig appConfig = ac.getBean(AppConfig.class);
System.out.println("appConfig = " + appConfig);
}
클래스명을 보면 아래와 같이 나온다.
appConfig = inflearn.springcore.AppConfig$$EnhancerBySpringCGLIB$$5bdba4f5@293a5f75
그렇다면 우리가 만든 AppConfig 클래스가 아니라는 말이다.
결국 스프링에서는 AppConfig 클래스를 상속받은 임의의 CGLIB 클래스를 만들고, 이를 스프링 빈으로 등록한다.
이는 싱글톤으로 관리되며, @Bean 어노테이션이 붙은 메소드가 컨테이너에 있다면 해당 인스턴스를, 없다면 등록을 하는방식으로 동작한다. 따라서 @Configuration 을 적용하지 않는다면 CGLIB 이 아닌 순수 우리가 만든 클래스를 사용하게 되고, 이는 싱글톤을 잃게 된다.
꼭 @Configuration 을 붙이자!!
참고 : 김영한-스프링 핵심 원리 (기본편)