반응형
# 싱글톤 컨테이너
## 싱글톤 컨테이너
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
- 스프링 컨테이너가 싱글톤 역할을 한다.
- 싱글톤 레지스트리 : 싱글톤 객체를 생성, 관리하는 기능.
SingletonTest
public class SingletonTest {
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
AnnotationConfigApplicationContext 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);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
}
- 클라이언트의 요청(memberService 요청) 시 동일한 memberService를 반환(요청시마다 새로 생성하는게 아닌 이미 만들어진 객체(동일한 객체)를 반환).
## 싱글톤 방식의 주의점 (공유필드 조심! 항상 무상태 (stateless) 로 설계하기!)
- 객체 인스턴스를 하나만 생성해서 공유하는 경우, 여러 클라이언트가 하나의 객체를 공유해서 사용하기 때문에 싱글톤 객체는 상태를 유지 (stateful) 하게 설계하면 안된다. -> 무상태 (stateless) 로 설계해야 한다.
- 특정 클라이언트에 의존적인 필드가 없어야 한다.
- 특정 클라이언트가 값을 변경할수 있는 필드가 없어야 한다.
- 읽기만 가능해야 하고 값을 수정하면 안된다.
- 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
상태 유지로 설계된 경우 예시.
StatefulService (상태 유지)
package singleton;
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;
}
}
statefulServiceTest
package singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자 20,000 주문.
statefulService1.order("userA", 20000);
// ThreadB : B 사용자 150,000 주문.
statefulService2.order("userB", 150000);
// ThreadA : 사용자A 가 주문 금액을 조회.
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(150000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
- 위 테스트 코드 실행 시 A사용자가 주문한 금액을 조회하면 아래와 같은 결과가 나온다.
- 특정 클라이언트가 공유되는 필드의 값을 변경하여 문제 발생. (공유 필드는 조심해야 함)
무상태로 설계된 경우 예시. (위 상태 유지 코드를 아래와 같이 변경 가능)
package singleton;
public class StatefulService {
public int order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
return price;
}
}
package singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자 20,000 주문.
int userAPrice = statefulService1.order("userA", 20000);
// ThreadB : B 사용자 150,000 주문.
int userBPrice = statefulService2.order("userB", 150000);
// ThreadA : 사용자A 가 주문 금액을 조회.
System.out.println("price = " + userAPrice);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
## @Configuration 과 싱글톤
AppConfig
@Configuration
public class AppConfig {
// memberService 역할
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// memberRepository 역할
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
// orderService 역할
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
// discountPolicy 역할
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
- AppConfig 에서 로직을 살펴보면 아래와 같은 문제점이 존재한다는걸 확인할 수 있다.
@Bean memberService() 시 new MemoryMemberRepository() 호출
@Bean orderService() 시 new MemoryMemberRepository() 중복 호출
위 내용 관련하여 테스트해서 확인 진행
MemberServiceImpl 에 테스트용 메서드 생성.
package hello.core.member;
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
// 테스트용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
OrderServiceImpl 에 테스트용 메서드 생성.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// 테스트용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
ConfigurationSingletonTest
package singleton;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ConfigurationSingletonTest {
@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("memberService > memberRepository = " + memberRepository1);
System.out.println("orderService > memberRepository = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
- 테스트 코드를 실행해 보면 아래와 같은 결과를 확인할 수 있다. (동일함)
- 동일한 인스턴스를 공유해서 사용중임을 확인할 수 있다.
- AppConfig 동작 시 아래와 같이 특정 메서드가 중복 호출될 것 같은데 실제로는 중복 없이 호출된다. (@Configuration)
AppConfig.memberService
AppConfig.memberRepository
AppConfig.memberRepository
AppConfig.orderService
AppConfig.memberRepository
## @Configuration 과 바이트코드 조작의 마법
- 스프링 컨테이너는 싱글톤 레지스트리임. (스프링 빈이 싱글톤이 되도록 보장)
- AppConfig 에서 특정 메서드가 중복으로 호출될 것 같은데 실제로는 중복 없이 호출되는 비밀은 @Configuration 에 있다.
- 아래와 같이 AnnotationConfigApplicationContext 에 파라미터로 넘긴 값(AppConfig) 은 스프링 빈으로 등록된다. 그렇기 때문에 AppConfig 도 스프링 빈으로 등록됨.
new AnnotationConfigApplicationContext(AppConfig.class);
ConfigurationSingletonTest 에서 등록된 AppConfig 조회 관련 테스트 코드 추가. (빈을 조회하여 클래스 정보 출력)
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
- 위 테스트 코드 실행 시 아래와 같은 결과를 확인 할 수 있다. (순수한 클래스라면 정상적인 path가 출력)
- 위와 같은 결과가 나온 이유는 스프링 빈이 CGLIB라는 바이트코드 조작 라이브러리를 사용, AppConfig 클래스를 상속받은 임의의 다른 클래스를 생성하고 그걸 스프링 빈으로 등록한 것.
- 그래서 이름은 등록한 AppConfig 이지만, 바이트코드를 조작하여 작성된 다른 클래스 정보가 표시되는 것.
- AppConfig@CGLIB 내부 로직을 예상해 보면 아마도, 등록된 빈이 없으면 신규로 빈을 등록하고, 존재하면 등록된 빈을 반환하도록 되어있을 것임 -> 그래서 AppConfig에서 중복없이 메서드 호출.
@Configuration 부착여부에 따라
- @Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 를 사용해서 싱글톤을 보장해준다.
- @Configuration 붙이지 않고 @Bean 만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장하지 않게됨. > AppConfig 실행 시, 중복된 메서드를 호출하게 된다. (중복 호출된 빈은 모두 동일하지 않음)
반응형
'인프런 강의 학습 > 스프링 핵심 원리(기본편)' 카테고리의 다른 글
재학습_싱글톤 컨테이너_1 (0) | 2022.09.19 |
---|---|
재학습_스프링 컨테이너와 스프링 빈_2 (0) | 2022.09.12 |
재학습_스프링 컨테이너와 스프링 빈_1 (0) | 2022.09.07 |
재학습_객체 지향 원리 적용_2 (0) | 2022.09.04 |
재학습_객체 지향 원리 적용_1 (0) | 2022.09.02 |