스프링을 공부하다 보면 싱글톤이라는 말을 접하게 된다.
싱글톤이란 무엇인지, 스프링에서 싱글톤을 어떻게 적용하는지 알아보자
싱글톤이란?
싱글톤이란 디자인패턴 중 하나로,
객체의 인스턴스가 하나만 생성되는 패턴을 말한다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// 생성자는 외부에서 호출못하게 private 으로 지정해야 한다.
}
public static Singleton getInstance() {
return instance;
}
public void say() {
System.out.println("hi, there");
}
}
위의 코드와 같이 외부에서 객체 인스턴스를 생성할 수 없도록 하고, 내부에서 생성된 하나의 객체 인스턴스만을 생성하여 사용하는 패턴이다.
싱글톤의 장점
- 객체를 하나만 생성하여 재사용하기 때문에 메모리 낭비를 방지할 수 있다
- 이미 생성된 인스턴스를 사용하기 때문에 속도 측면에서도 좋다
- 클래스 간에 데이터 공유가 쉽다(설계의 목적에 맞게 주의해서 사용해야함)
싱글톤의 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 필요하다
- 의존관계상 클라이언트가 구체 클래스에 의존한다 -> DIP를 위반한다
- 클라이언트가 구체 클래스에 의존하므로 OCP원칙을 위반할 가능성이 높다
- 테스트 하기 어렵다
- 내부 속성을 변경하거나 초기화 하기 어렵다
- 결론적으로 유연성이 떨어지고 안티패턴으로 불리기도 한다
분명 장점이 있긴 하지만 문제점이 더 많아보인다.
그렇다면 스프링에서 왜 싱글톤을 사용하는 걸까?
스프링에서 싱글톤을 사용하는 이유
싱글톤으로 빈을 관리하는 가장 큰 이유는 대규모 트래픽을 처리할 수 있도록 하기 위함이다.
스프링은 최초의 설계부터 대규모의 엔터프라이즈 환경에서 요청을 처리할 수 있도록 고안되었고, 이에 따라 계층적으로 처리 구조(Controller, Service, Repository 등)가 나뉘어지게 되었다.
그런데 클라이언트의 요청이 들어올 때마다 빈을 생성하게 되면 어떻게 될까?
작은 서비스라면 모르겠지만, 대규모 트래픽이 발생하는 네이버나 배달의 민족 같은 서비스는 분명 부하가 걸릴 수 밖에 없을 것이다. 1초에 100,000개의 요청만 들어와도 당장 100,000개의 객체가 생성되는 데 이를 감당하긴 힘들어 보인다.
따라서 스프링은 빈을 싱글톤 스코프로 관리하여 1개의 요청이 왔을 때, 여러 쓰레드가 빈을 공유해 처리하도록 하여 이런 문제를 해결한다.
즉, 우리는 싱글톤의 문제점을 해결해주는 스프링을 사용하기 때문에 싱글톤의 장점을 가져다 사용하기만 하면 된다!
스프링에서 싱글톤 적용
스프링에서 싱글톤을 어떻게 적용시키는 지 알아보자.
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
...
}
위의 코드는 싱글톤을 적용시키지 않은 코드로 빈이 생성될 때마다 new를 통해 인스턴스를 계속 생성한다.
싱글톤을 적용시켜보자.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
...
}
싱글톤을 적용하였다.
자세히 보면 맨 윗줄에 @Configuration 어노테이션이 추가된 걸 볼 수 있다.
이렇게나 간단하게 싱글톤을 적용시킬 수 있다.
@Configuration에 대한 궁금증이 생기지 않을 수가 없지 않은가!?
아래의 설명을 통해 궁금증을 해결할 수 있을 것이다.
@Configuration
정말 마법처럼 싱글톤을 @Configuration 어노테이션 하나만으로 적용시킬 수 있다.
먼저 싱글톤이 적용된 빈을 한번 출력해보자.
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
//출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
순수한 클래스라면 class hello.core.AppConfig가 출력되어야 하지만,
출력 결과는 class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70이다.
EnhancerBySpringCGLIB$$... 처럼 복잡하게 출력되는데, 이는 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 싱글톤을 적용시키기 때문이다.
스프링은 CGLIB를 이용하여 임의의 클래스를 만들고, 그 클래스를 스프링 빈으로 등록한다.
AppConfig를 예시로 임의의 클래스를 예상해보면,
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
이런식으로 작성되어 있을 것이다. 따라서 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
이렇게 마법같은 CGLIB 라이브러리를 사용하여 스프링은 싱글톤을 적용시킨다.
CGLIB 라이브러리의 내부동작은 매우 복잡하므로 CGLIB를 사용하여 싱글톤을 적용시킨다는 것만 알아두면 충분할 것 같다!
또한 자동으로 빈을 등록해주는 컴포넌트 스캔을 사용하더라도 위의 원리와 같이 싱글톤이 적용되므로 참고하자.
출처
https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/
https://greatzzang21.tistory.com/38