김영한님의 Spring 핵심원리 강의를 듣고 정리하는 글입니다.
1. 싱글톤(Singleton)이란?
- 싱글톤 패턴은 객체의 인스턴스가 오직 1개만 생성되는 패턴을 의미한다.
- 클라이언트의 요청에 따라 객체의 인스턴스를 생성할 경우 메모리 측면에서 매우 비효율적이다. 이를 해결하기 위해 싱글톤 패턴을 사용한다.
싱글톤 코드
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
- 생성자는 외부에서 호출하지 못하도록 private으로 설정해야 한다.
- 위 방식으로 클래스를 설계하게 되면 외부에서 생성자를 직접 호출 하지 못하기 때문에 하나의 인스턴스만을 객체를 공유하게 된다.
2. 싱글톤의 문제점
- 싱글톤을 구현하는 코드 자체가 필요하다.(멀티스레딩 환경에서 발생할 수 있는 동시성 문제를 해결하기 위해서도 별도의 로직이 필요하다.)
- 테스트가 어렵다. (자원을 공유하고 있기 때문에 매번 자원을 초기화 해줘야 한다.)
- 의존 관계상 클라이언트 구체 클래스에 의존하기 때문에 DIP, OCP 원칙을 위반한다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
- private 생성자로 인해 자식 클래스를 만들기 어렵다.
- 하지만 스프링 컨테이너는 싱글톤의 이러한 문제를 한번에 해결하면서 객체를 인스턴스를 싱글톤으로 관리한다.
3. 싱글톤의 주의점
- 클라이언트들이 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다. ⇒ 무상태(stateless)로 설계해야 한다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 최대한 읽기만 지원해야 한다.
싱글톤 공유 필드로 설계할 경우 발생할 수 있는 문제 코드를 보자.
public class StatefulService {
private int price; //상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; //여기가 문제!
}
public int getPrice() {
return price;
}
}
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
실행결과
price = 20000
- StatefulService는 스프링 컨테이너에서 관리하기 때문에 객체의 인스턴스를 싱글톤으로 관리한다.
- 위 예시처럼 필드를 공유하게 되면 userA의 주문 금액 10000원이 20000원으로 결과가 나오게 된다.
4. 스프링 컨테이너와 싱글톤
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//1. 조회
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 참조값 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}
실행 결과
memberService1 = hello.core.member.MemberServiceImpl@59a008ba
memberService2 = hello.core.member.MemberServiceImpl@59a008ba
- ApplicationContext를 통해 memberService 빈을 스프링 컨테이너가 관리하도록 설정했다.
- 실행 결과를 보게 되면 memberServiceImpl1, memberService2 는 같은 참조값을 가지고 있다.
- 위 결과를 보았을 때 스프링 컨테이너는 객체의 인스턴스를 싱글톤으로 관리하고 있다.
- 하지만 스프링 컨테이너는 항상 싱글톤으로 관리하는 것은 아니다. 필요에 따라 클라이언트의 요청시 새로운 객체를 생성하는 기능도 제공한다. (prototype 빈)
5. @Configuration과 싱글톤
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
- AppConfig를 보게 되면 MemberRepository는 memberService, memberRepository, orderService에서 각각 호출하기 때문에 총 3번을 호출하게 되어 new 키워드로 인스턴스를 3번 생성하게 될 것이다.
- 하지만 스프링은 이 문제 또한 인스턴스를 싱글톤으로 관리한다.
간단한 테스트를 해보자.
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
System.out.println("memberRepository1 = " + memberRepository1);
System.out.println("memberRepository2 = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
실행 결과
memberRepository1 = hello.core.member.MemoryMemberRepository@30c93896
memberRepository2 = hello.core.member.MemoryMemberRepository@30c93896
memberRepository = hello.core.member.MemoryMemberRepository@30c93896
- 각자 서로 다른 인스턴스를 가질 거라 예상했지만 인스턴스를 싱글톤으로 관리하는 것을 확인할 수 있다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.orderService");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.memberRepository");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
실행 결과
call AppConfig.memberService
call AppConfig.orderService
call AppConfig.memberRepository
bean = class hello.core.AppConfig$$SpringCGLIB$$0
- 실행 결과를 보면 예상과 달리 “call AppConfig.memberRepository” 는 한번만 호출이 됐다.
- 스프링은 이를 바이트코드 조작으로 해결했다.
6. @Configuration과 바이트코드 조작
- CGLIB 바이트 코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받는 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록해서 해결을 한 것이다.
대충 이러한 로직을 가지고 구현되어 있을 것이다.
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
- @Bean이 붙은 메서드마다 이미 스프링 빈에 존재할 경우 해당 빈을 찾아 반환하고, 빈으로 등록하지 않았을 경우 생성해서 스프링 빈으로 등록한다.
- @Configuration을 통해 싱글톤을 보장하게 된다. (@Bean만 사용할 경우 스프링 빈으로 등록만 되며, 싱글톤을 보장할 수 없다.)
'Programming > Spring' 카테고리의 다른 글
[Spring] 웹 스코프란? (1) | 2024.01.11 |
---|---|
[Spring] 싱글톤과 프로토타입 스코프 (1) | 2024.01.10 |
[Spring] @Autowired에서 조회 빈이 2개 이상인 경우 (1) | 2024.01.10 |
[Spring] 순환참조 문제 해결 (1) | 2023.10.04 |
[Spring Boot] Spring Boot + ES 예제 (0) | 2023.09.28 |