<aside> ❗
// 순수한 DI 컨테이너
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
// 1. 조회: 호출할 때 마다 객체를 생성
MemberService memberService1 = appConfig.memberService();
// 2. 조회: 호출할 때 마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
// 참조 값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// 두 객체가 다른 경우 테스트 통과
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
/*
결과 - 서로 다른 객체 생성됨
memberService1 = hello.core.member.MemberServiceImpl@709ba3fb
memberService2 = hello.core.member.MemberServiceImpl@3d36e4cd
*/
<aside> ❗
package hello.core.singleton;
public class SingletonService {
// SingletonService 클래스가 로드될 때 자기 자신의 인스턴스를 딱 한 번 생성하여, 이후 변경 불가능한 클래스 변수로 참조하게 함
private static final SingletonService instance = new SingletonService();
// instance 참조 변수의 Getter
// 해당 인스턴스를 꺼낼 수 있는 방법은 이 Getter 밖에 없다.
public static SingletonService getInstance() {
return instance;
}
// private 생성자
// 접근 제어자를 private으로 줌으로써 클래스 밖에서 생성하지 못하도록 함
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// isSameAs - ==와 동일
// isEqualTo - equals() 메서드를 어떻게 정의하느냐에 따라 다름
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
/*
결과 - 동일한 객체 출력 됨
singletonService1 = hello.core.singleton.SingletonService@70b0b186
singletonService2 = hello.core.singleton.SingletonService@70b0b186
*/
[참고]
<aside> ❗
<aside> ❗
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1만개 생성)으로 관리한다.
지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.
[싱글톤 컨테이너]
스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리 이전에 설명한 컨테이너 생성 과정을 보면, 컨테이너는 객체를 하나만 생성해서 관리
스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.
스프링 컨테이너가 싱글톤 패턴을 보장하므로, 클래스를 싱글톤 패턴으로 구현할 필요가 없음 → 코드가 구현 객체에 의존하지 않기 때문에 (DIP) 다형성을 활용해 구현체를 변경하더라도 빈의 코드를 수정할 필요가 없다.(OCP) → 코드가 스프링 컨테이너에 의존적이지 않기 때문에 테스트 코드 작성이 쉬움
→ 코드가 싱글톤 패턴으로 구현되지 않기 때문에 자유롭게 생성자를 호출할 수 있다. 자식 클래스 만들 수 있음
@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);
assertThat(memberService1).isSameAs(memberService2);
}
/*
결과
memberService1 = hello.core.member.MemberServiceImpl@26ceffa8
memberService2 = hello.core.member.MemberServiceImpl@26ceffa8
*/
[참고]
스프링 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.
<aside> ❗
싱글톤 방식 주의점
// StatefulService
package hello.core.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;
}
}
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA: A 사용자 10000원 주문
statefulService1.order("userA", 10000);
// ThreadBL B 사용자 20000원 주문
statefulService1.order("userB", 20000);
// ThreadA : 사용자A가 주문 금액을 조회
System.out.println("statefulService1.getPrice() = " + statefulService1.getPrice());
// 싱글톤 패턴을 사용해 동일한 객체의 price 변수에 값을 저장하므로 마지막으로 주문한 사용자의 값이 저장됨
Assertions.assertThat(statefulService1.getPrice()).isNotEqualTo(10000);
// ThreadB : 사용자B가 주문 금액을 조회
System.out.println("statefulService2.getPrice() = " + statefulService2.getPrice());
Assertions.assertThat(statefulService2.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
// 무상태로 설계
package hello.core.singleton;
public class StatefulService {
public int order(String name, int price){
System.out.println("name = " + name + " price = " + price);
return price;
}
}
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA: A 사용자 10000원 주문
int userAPrice = statefulService1.order("userA", 10000);
// ThreadBL B 사용자 20000원 주문
int userBPrice = statefulService1.order("userB", 20000);
// ThreadA : 사용자A가 주문 금액을 조회
System.out.println("statefulService1.getPrice() = " + userAPrice);
// 싱글톤 패턴을 사용해 동일한 객체의 price 변수에 값을 저장하므로 마지막으로 주문한 사용자의 값이 저장됨
Assertions.assertThat(userAPrice).isEqualTo(10000);
// ThreadB : 사용자B가 주문 금액을 조회
System.out.println("statefulService2.getPrice() = " + userBPrice);
Assertions.assertThat(userBPrice).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
</aside>
<aside> ❗
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
@Bean DiscountPolicy discountPolicy2() {
return new RateDiscontPolicy();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy2());
}
}
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 구체 타입으로 꺼내면 안 좋지만 getMemberRepository() 메서드를 사용하기 위해 구체 타입으로 꺼낸다.
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("memberRepository = " + memberRepository);
System.out.println("memberRepository1 = " + memberRepository1);
System.out.println("memberRepository2 = " + memberRepository2);
Assertions.assertThat(memberRepository1).isSameAs(memberRepository);
Assertions.assertThat(memberRepository2).isSameAs(memberRepository);
}
}
/*
결과 - 동일한 MemoryMemberRepository 객체를 사용
memberRepository = hello.core.member.MemoryMemberRepository@63f259c3
memberRepository1 = hello.core.member.MemoryMemberRepository@63f259c3
memberRepository2 = hello.core.member.MemoryMemberRepository@63f259c3
*/
<aside> ❗
@Test
void configurationDeep() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig been = ac.getBean(AppConfig.class);
System.out.println("been.getClass() = " + been.getClass());
}
/*
been.getClass() = class hello.core.AppConfig$$SpringCGLIB$$0
*/
AppConfing를 상속 받은 다른 클래스를 만든다.(AppConfig@CGLIB) → 원본 AppConfig 코드의 동작을 감싸고, 싱글톤 보장을 위해 추가적인 로직을 삽입 AppConfig@CGLIB는 기존 AppConfig의 @Bean 애너테이션이 붙은 메서드를 오버라이딩(재정의)해서 싱글톤 보장을 위한 로직을 삽입함
AppConfig@CGLIB이 조작한 빈을 스프링 빈으로 등록 (AppConfig가 조작 X)
스프링 컨테이너에는 이름은 appConfig인데 인스턴스 객체가 내가 등록한 얘가 아닌 다른 클래스가 등록된다.
그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있다면 ?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { // 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
[참고]
** 프록시 클래스는 바이트 코드 수준에서 생성되고 런타임에만 존재하는 코드
</aside>
<aside> ❗
// 테스트 결과
// memberRepository()를 세 번 실행해 서로 다른 객체가 3개 생성됨
call AppConfig.memberRepository
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
memberRepository = hello.core.member.MemoryMemberRepository@2dc9b0f5
memberRepository1 = hello.core.member.MemoryMemberRepository@6531a794
memberRepository2 = hello.core.member.MemoryMemberRepository@3b5fad2d
</aside>
<aside> ❗