1. 웹 애플리케이션과 싱글톤 패턴
스프링이 없는 순수한 DI 컨테이너는 호출(요청)할 때마다 새로운 객체를 생성함
- 이런 방식을 사용하게 되면 요청 수만큼 객체가 생성되고 소멸되므로 메모리 낭비가 심해짐
- 해결 방안 = 싱글톤 패턴: 해당 객체가 딱 1개만 생성되고, 새로운 요청이 들어오면 해당 객체를 공유하도록 설계하기
싱글톤 패턴
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
- 객체 인스턴스가 2개 이상 생성되지 못하도록 private 생성자를 사용해, 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 함
- 아래는 객체를 미리 생성해 두는 가장 단순하고 안전한 싱글톤 패턴 구현 방식
- 객체가 필요하다면 getInstance() 메서드를 통해 조회 가능 (항상 같은 인스턴스를 반환함)
private static final SingleTonService instance = new singletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {}
싱글톤 패턴의 문제점 (유연성이 떨어짐; 안티패턴으로 불리기도 함)
- 구현 코드 자체가 너무 긺
- 의존관계상 클라이언트가 구체 class에 의존함
- DIP 위반, OCP도 위반할 가능성이 높음
- 테스트하기 어려움
- 내부 속성을 변경하거나 초기화하기 어려움
- private 생성자를 사용하기 때문에 자식 class를 만들기 어려움
2. 싱글톤 컨테이너
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 동시에 객체 인스턴스를 싱글톤으로 관리함
- 스프링 빈 = 싱글톤으로 관리되는 빈
싱글톤 컨테이너
스프링 컨테이너
- 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리하며, 싱글톤 컨테이너 역할을 함
- 싱글톤 레지스트리 = 싱글톤 객체를 생성하고 관리하는 기능
- 스프링 컨테이너의 이런 기능 덕분에 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지 가능
- 싱글톤 패턴을 위한 지저분한 코드 필요 X
- DIP, OCP 만족 및, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용 가능
스프링 컨테이너 덕분에 이미 만들어진 객체를 공유해 효율적으로 재사용 가능!
- 참고: 스프링의 기본 빈 등록 방식 = 싱글톤
- 이외에도 여러 방식이 존재함 (요청할 때마다 새로운 객체를 생성해 반환하는 기능 등)
💫싱글톤 방식의 주의점
객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 무상태(stateless)로 설계해야 함
- 특정 클라이언트에 의존적인 필드나 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 됨
- 가급적 읽기만 가능해야 함
- 필드 대신 자바에서 공유되지 않는, 지역 변수, 파라미터, ThreadLocal 등을 사용해야 함
public class StatefulService {
private LocalDate now = LocalDate.now();
public int getLocalDateNow() {
return now;
}
}
3. @Configuration과 싱글톤
아래 AppConfig class의 코드를 보면 memberRepository()를 여러 번 호출해 싱글톤이 깨지는 것처럼 보임
- 그러나 로그를 찍어보면 각 메서드가 1번만 호출되는 걸 확인할 수 있음
@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과 바이트코드 조작의 마법
스프링 컨테이너 = 싱글톤 레지스트리
- 스프링 빈이 싱글톤이 되도록 보장해줘야 함
- 이를 위해 스프링은 class의 바이트코드를 조작하는 라이브러리를 사용함
@Test
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
}
AnnotationConfigApplicationContext에 파라미터로 넘긴 값(AppConfig.class)도 스프링 빈으로 등록됨
- AppConfig 스프링 빈을 조회해서 class 정보를 출력하면 순수한 class 정보인 class hello.core.AppConfig 대신 class 명에 xxxCGLIB...이 붙은 복잡한 정보가 출력됨
- 스프링이 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해 AppConfig class를 상속받은 임의의 다른 class를 만들고 그 class를 스프링 빈으로 등록한 것!
이 임의의 다른 class가 싱글톤을 보장해 줌 (바이트코드 조작)
- 호출한 메서드가 이미 스프링 컨테이너에 등록되어 있으면 스프링 컨테이너에서 찾아 반환하고, 그렇지 않다면 기존 로직을 호출해 생성 및 스프링 컨테이너에 등록하고 반환하는 코드로 동작함
- 참고: AppConfig@CGLIB은 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 가능
@Configuration을 적용하지 않고, @Bean만 적용한다면?
CGLIB 없이 순수한 AppConfig가 스프링 빈에 등록됨
- 싱글톤 보장 X, 이미 스프링 빈에 등록된 메서드를 여러 번 호출해 각각 다른 객체 인스턴스를 생성
- 따라서 스프링 설정 정보(Config)는 항상 @Configuration을 사용해야 함!