먼저 싱글톤이란 무엇일까?
클래스의 인스턴스가 단 하나만 생성되도록 하는 디자인 패턴이다.
[디자인패턴] 생성 패턴 - 싱글톤 패턴(Singleton Pattern)
[디자인 패턴] 디자인 패턴(Design Pattern)이란 1. 디자인 패턴(Design Pattern)이란 설계에 자주 사용하는 패턴들을 정형화 하여 유형별로 정해두고 상황에 맞게 사용될 수 있는 문제들을 해결하는데에
hyunbenny.tistory.com
갑자기 싱글톤 패턴은 왜..?
웹 어플리케이션은 클라이언트의 요청이 엄청나게 많은데 순수 DI컨테이너(AppConfig)는 요청이 올 때마다 객체를 새로 생성한다.
따라서 트래픽에 따라 메모리 낭비가 심하기 때문에 객체를 1개만 생성하고 공유하도록 설계하여 메모리 낭비를 방지하고자 한다.
싱글톤 패턴의 단점
- 싱글톤 패턴을 위한 구현 코드가 많다.
- DIP를 위반한다.(→ 클라이언트가 구현 클래스에 의존한다.)
- OCP위반 가능성이 높아진다.
- 테스트 어려워진다.
- 내부속성의 변경 및 초기화가 어려워진다.
- private생성자로 자식 클래스를 만들기 어려워진다(→ 유연성이 떨어지며 이는 곧 안티패턴이다.)
메모리 낭비 방지를 위한다는 점 외에 단점이 너무 많은데....?
위에서 언급한 이러한 단점들을 `싱글톤 컨테이너`를 통해 스프링 프레임워크가 모두 해결해준다.
스프링 컨테이너와 싱글톤
스프링 컨테이너는 싱글톤 컨테이너 역할을 한다.
- 싱글톤 객체들을 생성하고 관리하는 기능을 한다.
스프링 프레임워크에서 빈을 등록하는 방식 역시 싱글톤이다.
@Test
@DisplayName("순수 DI컨테이너")
void pureDIContainer() {
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
@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);
Assertions.assertThat(memberService1).isSameAs(memberService2);
/**
* isEqualTo : .equals() 비교
* isSameAs : == 비교
*/
}
- 순수 DI컨테이너 테스트를 보면 우리가 이전에 만들었던 AppConfig 객체를 직접 생성해서 사용하고 있다.
- 이 경우, appconfig에서 memberService객체를 얻어오면 요청할 때마다 new로 새로운 객체를 반환하기 때문에 memberService1과 memberService2는 다른 객체일 수 밖에 없다.
- 하지만 스프링 컨테이너는 싱글톤으로 객체(빈)들을 관리하기 떄문에 ac.getBean("memberService", MemberService.class); 할 때마다 새로운 객체가 아닌 이미 생성해 놓은 같은 객체를 반환해 준다.
DI컨테이너의 AppConfig 코드
public class AppConfig {
// 역할
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// 역할
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
싱글톤 패턴을 사용할 때의 주의점
싱글톤은 클래스의 인스턴스를 하나만 생성하여 공유하여 사용하는 것이라고 했다.
그렇기 때문에 객체를 stateless하게 설계해야 한다.
무상태란?
서버와 클라이언트 간 통신 시, 해당 요청과 응답이 독립적인 트랜잭션으로 취급하는 네트워크 프로토콜이다.
https://ko.wikipedia.org/wiki/%EB%AC%B4%EC%83%81%ED%83%9C_%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C
그럼 객체를 stateless하게 설계한다는 게 무슨 말이야?
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다(→ 읽기만 가능해야 한다.)
- 필드 대신 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
⇒ 스프링 빈으로 등록하는 객체의 필드에 값을 공유하도록 설계하면 절대 안된다!!
스프링 빈에 값을 공유하게 설계하면 일이 벌어지길래..??
public class StatefulService {
private int price; // 상태를 유지하는 필드
public void order(String name, int price) {
this.price = price;
System.out.println("name : " + name + ", price : " + price);
}
public int getPrice() {
return this.price;
}
}
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// Thread1
statefulService1.order("userA", 10000);
// Thread2
statefulService2.order("userB", 30000);
// Thread1 : userA가 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("userA price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(30000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
위의 코드에서 statefulService1과 statefulService2가 각각의 쓰레드라고 가정하고 userA의 요청이 완료되기 전에 userB의 요청이 들어왔다고 하자.
➡️ statefulService를 빈으로 등록하여 공유하여 사용하기 때문에 userA의 값이 10,000원에서 30,000원으로 바뀌는 일이 발생한다.
따라서 아래와 같이 상태가 공유되지 않도록 설계해야 한다.
public class StatefulService {
private int price; // 상태를 유지하는 필드
public int order(String name, int price) {
System.out.println("name : " + name + ", price : " + price);
return price;
}
}
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// Thread1
int userAPrice = statefulService1.order("userA", 10000);
// Thread2
int userBPrice = statefulService2.order("userB", 30000);
System.out.println("userA Price = " + userAPrice);
System.out.println("userB Price = " + userBPrice);
Assertions.assertThat(userAPrice).isEqualTo(10000);
Assertions.assertThat(userBPrice).isEqualTo(30000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
order()가 호출되면 그 값을 객체의 필드에 가지고 있지 않고 바로 리턴해 줌으로써 두 요청 간 연결이 되지 않도록 설계행야 한다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
'Spring > Spring Framework' 카테고리의 다른 글
[Spring Framework] 의존관계 주입(Dependency Injection) (0) | 2023.04.24 |
---|---|
[Spring Framework] @ComponentScan (0) | 2023.04.10 |
[Spring Framework] 스프링 컨테이너와 스프링 빈 (0) | 2023.04.06 |
[Spring Framework] IOC와 DI (0) | 2023.04.05 |
[Spring Framework] 스프링 프레임워크란 (0) | 2023.04.05 |