일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Spring
- TCP/IP
- redis
- OS
- n타일링2
- FIFO paging
- 윤성우 저자
- C++
- inflearn
- 스프링 입문
- 2475번
- 제프리리처
- 열혈 tcp/ip 프로그래밍
- Operating System.
- Window-Via-c/c++
- 김영한
- HTTP
- 우아한레디스
- BOJ
- 에러핸들링
- 이펙티브코틀린
- C#
- 스프링 핵심 원리
- 우아한 테크 세미나
- Operating System
- 토마토
- 열혈 TCP/IP 소켓 프로그래밍
- Four Squares
- 10026번
- 운영체제
- Today
- Total
나의 브을로오그으
#5-1. [스프링 핵심 원리-기본편] - 싱글턴 패턴 본문
웹 애플리케이션과 싱글톤
- 스프링은 태생이 기업용 온라인 서비스 기술 지원을 위해 탄생했다.
- 대부분의 스프링 앱은 웹 앱이다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 가능하다.
- 웹 앱은 보통 여러 고객이 동시 요청을 한다.

[BeanDefinitionTest]
package hello.core.beandefinition;
import hello.core.order.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class BeanDefinitionTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// GenericXmlApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
@Test
@DisplayName("빈 설정 메타정보 확인")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
System.out.println("beanDefinitionName = " + beanDefinitionName +
" beanDefinition = " + beanDefinition);
}
}
}
}
- 우리가 만들었던 스프링 없는 순수한 DI컨테이너인 AppConfig는 요청 시 객체를 새로 생성한다.
- 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성, 소멸된다 -> 메모리 낭비 심함
- 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. -> 싱글턴 패턴
싱글턴 패턴
- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
- 객체 인스턴스가 2개 생성되지 않도록 막기만 하면 됨.
package hello.core.singleton;
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() { return instance; }
private SingletonService() { }
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
1. static 영역에 객체 instance를 미리 하나 생성한다.
2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메소드를 통해서만 조회 가능하다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
[SingletonTest]
package hello.core.singleton;
import hello.core.member.MemberService;
import hello.core.order.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
// 1. 조회 : 호출할 때 마다 객체를 생성
MemberService memberService_1 = appConfig.memberService();
// 2. 조회 : 호출할 때 마다 객체를 생성
MemberService memberService_2 = appConfig.memberService();
// memberService1 != memberService2
Assertions.assertThat(memberService_1).isNotSameAs(memberService_2);
}
@Test
@DisplayName("싱글턴 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService_1 = SingletonService.getInstance();
SingletonService singletonService_2 = SingletonService.getInstance();
Assertions.assertThat(singletonService_1).isSameAs(singletonService_2);
// same == 실제 자바 객체의 할당된 값이 같은지 비교
// equal == equals 메소드를 오버라이드 가능, 그것을 호출
}
}
- private으로 new 키워드를 막아둔다.
- 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인 가능하다.
※참고 : 싱글턴 패턴 구현 방법은 다양하다. 그중에서 가장 단순하고 안전한 방법으로 구현하였다.
싱글턴 패턴을 작용하면 고객 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효울적으로 사용 가능하다. 하지만 싱글턴 패턴은 다음의 문제점들이 있다.
싱글턴 패턴 문제점
- 싱글턴 패턴을 구현하는 코드 자체가 많아진다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP 위반
- 클라이언트가 구현 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 결론적으로 유연성이 떨어진다.
- 안티패턴으로 불리기도 한다.
그렇다면, Service, Repository 등 모두 공통으로 쓰는 객체이므로 싱글톤으로 생성해서 사용하면 된다.
그러나!!! 스프링에서는 이렇게 할 필요가 없다. 스프링 컨테이너가 기본적으로 객체를 싱글턴으로 생성해서 관리해준다.
싱글턴 컨테이너
- 스프링 컨테이너는 싱글턴 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글턴(1개만 생성)으로 관리한다. 지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리하는 빈이다.
- 싱글턴 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글턴으로 관리한다.
- 이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컴테이너는 객체를 하나만 생성해서 관리한다.
싱글턴 방식의 주의점
- 싱글턴 패턴이든, 싱글턴 컨테이너를 사용하든, 싱글턴 객체는 여러 클라이언트에서 공유해서 사용하기 때문에 객체의 상태를 유지(stateful)하게 설계하면 안된다.
- 무상태(stateless)로 설계해야 한다.
* 특정 클라이언트에 의존적인 필드가 있으면 안된다.
* 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
* 가급적 읽기만 가능해야 한다.
* 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
[StatefulServiceTest]
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
class StatefulServiceTest {
@Test
@DisplayName("스테이트가 변경되는 필드를 가진 싱글톤 객체 테스트")
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService_1 = ac.getBean(StatefulService.class);
StatefulService statefulService_2 = ac.getBean(StatefulService.class);
/*
시나리오 : 사용자 A가 10000원에 주문을 하고 가격을 확인하는데
그 사이에 사용자 B가 20000원에 주문을 해버렸다. 이러면 price는 20000원이 되었으므로
잘못된 싱글턴 객체 설계이다.
*/
// ThreadA : A 사용자가 10000원 주문
statefulService_1.order("userA", 10000);
// ThreadB : B 사용자가 20000원 주문
statefulService_2.order("userB", 20000);
// ThreadA : 사용자A 주문 금액 조회
int price_1 = statefulService_1.getPrice();
System.out.println("price = " + price_1);
Assertions.assertThat(statefulService_1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
- 실제로 테스트해보려면 쓰레드를 이용해서 해보는게 가장 정확하다.
- ThreadA가 사용자A 코드를 호출하고, ThreadB가 사용자B 코드를 호출한다고 가정하자.
- StatefulService의 price필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
- 사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.
- 진짜 공유되는 필드는 조심히 사용해야 한다.
[해결책]
[StatefulService]
package hello.core.singleton;
public class StatefulService {
// private int price; // 상태를 유지하는 필드
public int order(String name, int price) {
System.out.println("name = " + name + "price = " + price);
// this.price = price; // 여기가 문제
return price;
}
}
[StatefulServiceTest]
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
class StatefulServiceTest {
@Test
@DisplayName("스테이트가 변경되는 필드를 가진 싱글톤 객체 테스트")
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService_1 = ac.getBean(StatefulService.class);
StatefulService statefulService_2 = ac.getBean(StatefulService.class);
/*
시나리오 : 사용자 A가 10000원에 주문을 하고 가격을 확인하는데
그 사이에 사용자 B가 20000원에 주문을 해버렸다. 이러면 price는 20000원이 되었으므로
잘못된 싱글턴 객체 설계이다.
*/
// ThreadA : A 사용자가 10000원 주문
int userAPrice = statefulService_1.order("userA", 10000);
// ThreadB : B 사용자가 20000원 주문
int userBPrice = statefulService_2.order("userB", 20000);
// ThreadA : 사용자A 주문 금액 조회
System.out.println("price = " + userAPrice);
Assertions.assertThat(userAPrice).isEqualTo(10000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
다시한번 스프링 빈(싱글턴 객체)은 무상태를 유지하자!
'Spring' 카테고리의 다른 글
#6. [스프링 핵심 원리-기본편] - 컴포넌트 스캔 (0) | 2022.07.26 |
---|---|
#5-2. [스프링 핵심 원리-기본편] - @Configuration과 싱글턴 (0) | 2022.07.21 |
#4-5. [스프링 핵심 원리-기본편] - 스프링 빈 설정 메타 정보 (0) | 2022.07.20 |
#4-4. [스프링 핵심 원리-기본편] - 다양한 설정 형식 지원 (0) | 2022.07.19 |
#4-3. [스프링 핵심 원리-기본편] - BeanFactory와 ApplicationContext (0) | 2022.07.19 |