싱글톤을 이용하면 시스템 내에서 객체 인스턴스의 유일함을 보장할 수 있고 객체 생성 비용이 절약되며, 자원을 공유하기 좋다. 이러한 장점 때문에 싱글톤은 자주 언급되고 유명한 디자인 패턴이기도 하다.
하지만 싱글톤 패턴도 단점이 있다. 다음과 같이 싱글톤을 구현했다고 가정해보자.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
private SingletonService() {}
public static SingletonService getInstance() {
return instance;
}
}
이런 싱글톤 패턴은 생각보다 많은 단점이 있는데
- 싱글톤 패턴을 구현하는 코드 자체가 많이 필요하다.
- 클라이언트가 미리 생성된 instance에 의존하기 때문에 DIP를 위반하고 OCP 또한 위반할 가능성이 높다.
- 테스트하기 어렵다. 싱글톤 구조이기 때문에 인스턴스가 미리 생성되어 여러 가지 설정이 어렵기 때문에 테스트의 유연성이 떨어진다.
- 싱글톤 특성상 내부 속성을 변경하거나 초기화 하기 어렵다. (테스트하기 어렵다는 뜻이기도)
- 자식 클래스를 만들기 어렵다. (그럴꺼면 싱글톤을 안 쓰는 게 맞다)
- 결론적으로 유연성이 떨어진다.
- 따라서 안티패턴으로 불리기도 한다. (꼭 필요할 때만 사용해야 한다는 뜻)
스프링에서 싱글톤 패턴을 이용하는 대표적인 것이 싱글톤 컨테이너이다. 기존에 AppConfig로 IoC를 통해서 의존 관계를 설정하고 주입해줬었는데 이 AppConfig에는 큰 단점이 있다. 예를 들어 appConfig의 memberService( ) 메서드를 호출해서 memberSerivce 객체를 얻는다고 했을 때, memberSerivce 메서드가 호출될 때마다 memberSerivce 객체는 계속해서 새롭게 생성된다. 한두 번은 상관없겠지만 100번 1000번이 된다면?.. 꽤 부담되는 오버헤드가 발생할 것이다.
그래서 스프링에서는 memberService가 매번 새롭게 생성되지 않도록 싱글톤 컨테이너에서 스프링 빈을 관리한다. 즉 객체가 싱글톤 패턴으로 생성되어 단 한개만 존재하는 것이다. 그런데 여기에도 함정이 있다.
위에처럼 사용자가 모두 하나의 클래스 인스턴스를 공유하고 있다고 하자. 만약 OrderClass가 이전에 주문한 주문 정보를 저장하고 나중에 꺼내보는 클래스라면 어떤 일이 일어날까?
만약 A라는 사람이 주문을 하고 나중에 주문 정보를 꺼내봤을 때 그것이 내 주문 정보인지 보장되지 않을 것이다. 즉 굉장히 심각한 문제가 발생할 수 있는 것이다. 이러한 문제를 근본적으로 해결하는 방법은 애초에 싱글톤 객체가 "무상태성"을 지니도록 설계해야 한다.
그럼 기존의 AppConfig를 어떻게 스프링으로 전환할 수 있을까? 기존의 코드에 간단하게 @Configuration, @Bean 어노테이션을 붙임으로써 간단하게 전환할 수 있다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
흠.. 그런데도 여전히 풀리지 않는 의문이 있다. 바로 기존 코드의 new 구문이다. 대체 @Configuration, @Bean 어노테이션을 붙이면 어떻게 동작하길래 new 구문이 있음에도 불구하고 객체가 싱글톤 패턴으로 생성되는 것일까? (아무리 스프링이라도 코드를 무시할 수는 없을 텐데!)
그건 바로 @Configuration 어노테이션이 바이트코드 조작 라이브러리를 사용하면서 가능하게 된 부분이다. 실제로 테스트 코드를 돌려봐도 클래스 인스턴스는 단 한 개만 생성된다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
@Configuration 어노테이션을 사용하면 스프링이 바이트 조작 라이브러리(GCLIB)를 사용해서 AppConfig 클래스를 싱글톤이 보장되는 컨테이너로 변경 후 사용한다. 즉 스프링 빈이 유일하도록 보장한다.
라이브러리의 내부 동작은 자세히 알기 어렵지만 아마도 우리가 컨테이너에서 getBean으로 객체가 있는지 확인했던 것과 비슷한 과정을 거쳐서 객체를 단 한개만 생성하지 않을까 라는 생각이 든다.
reference
'Spring' 카테고리의 다른 글
생성자 주입과 스프링 (0) | 2022.04.06 |
---|---|
스프링 컴포넌트 스캔 (0) | 2022.04.03 |
POJO에서 스프링으로의 전환 (0) | 2022.03.26 |
IoC (0) | 2022.03.25 |
객체 지향 설계 원칙을 잘 지키는 법 (0) | 2022.03.24 |