웹 애플리케이션은 보통 여러 고객이 요청을 진행한다
우리가 만들었던 AppConfig의 경우 요청을 할때마다 객체를 새로 생성하는데,
초당 100건이 들어오면 초당 100개의 객체가 생성되고 소멸되기 때문에 메모리 낭비가 심하다는 단점이 있다.
따라서 이 문제를 해결하기 위해 객체를 딱 하나만 생성하고, 공유하도록 설계하면 된다!
클래스의 인스턴스가 딱 한 개만 생성되는 것을 보장하는 디자인 패턴을 싱글톤 패턴이라 부른다
-> 객체 인스턴스가 2개 이상 생성되는 것을 막아야하기 때문에 생성자를 private으로 설정해 new 키워드 사용을 막는다.
package hello.springintroduction.Singleton;
public class SingletonService {
private static final SingletonService instance = new SingletonService();
// static - static 영역에 단 1개만 존재 (스스로 선언)
public static SingletonService getInstance(){
return instance;
} // 스스로 생성해서 인스턴스의 참조를 넣어놓음.
// 조회할 때는 getInstance() 사용
// Consturctor 막아놓음으로써 생성을 마강버림
private SingletonService(){
// Prevention for creating new SingletonService()
}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
private으로 생성자의 접근제어자를 설정해서 new 키워드를 통한 객체 생성을 막았다.
1. static영역에 객체 인스턴스를 미리 하나 생성해서 올려둔다
2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있게 한다.
(이 메서드를 호출하면 항상 같은 객체 반환을 보장)
3. 딱 1개의 인스턴스만 존재해야하기 때문에 생성자를 private으로 막는다.
-> 제일 좋은 오류는 컴파일 과정상에서 발생하는 오류이다.
package hello.springintroduction.Singleton;
import hello.AppConfig;
import hello.springintroduction.Member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SingletonTest {
@Test
@DisplayName("Pure Container without Spring")
void pureContainer(){
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Assertions.assertThat(memberService1).isNotEqualTo(memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
/*
isSameAs: 주소 비교. 메모리 상에서 같은 객체를 참조하는지 확인
isEqualTo: 값 비교. 객체가 서로 같은 값을 가지고 있는지 확인
isInstanceOf: 타입 비교
*/
// Keep Generating Each Classes per calling -> Not Efficient.
// Memory Leak -> 100 request per sec creates 100 objects per sec
// Sharing the same object instance ensures more efficient use of memory.
}
@Test
@DisplayName("싱글톤 패턴을 적용한 새 객체 사용")
void singletonServiceTest(){
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
@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);
}
}
싱글턴 패턴 문제점 - Trade off에 가까움
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존 관계상 클라이언트가 구체 클래스에 의존한다 -> DIP 위반
다음과 같은 코드 형식으로 호출해야하기 때문
ClassType instanceName = 구체클래스.getInstance();
클라이언트가 구체 클래스에 의존하기 때문에 OCP 원칙을 위반할 가능성이 높다.
테스트하기 어렵다
내부 속성을 변경하거나 초기화하기 어렵다(생성자를 막아버림)
private 생성자로 자식 클래스를 만들기 어렵다 -> 사실 상속을 하는 경우가 거의 없는데
상속 받는 자식이 생성되는 경우 부모의 생성자를 호출하는데, 이게 안됨.
결론적으로 유연성이 떨어진다(DI가 어려움)
안티패턴으로 불리기도 하는데 상황에 따라 적절하게 선택해야한다.
싱글톤 컨테이너
스프링 컨테이너는 싱글톤의 문제점을 해결하면서 객체 인스턴스를 싱글톤으로 관리한다.
1) 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다
2) 스프링 컨테이너는 싱글턴 컨테이너의 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
3) 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다
- 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
- DIP, OCP, 테스트, Private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
스프링 빈은 기본적으로 싱글톤으로 빈 등록을 제공하나, 싱글톤 방식 이외에도 prototype 방식을 지원한다.
이 내용은 이 장 마지막에서 설명한다. (Bean Scope)
싱글톤 방식의 주의점
객체 인스턴스를 하나만 공유해서 사용하기 때문에, 싱글톤 객체는 stateful하게 설계해서는 안된다
stateless로 설계해야한다.
- 특정 클라이언트에 의존적인 필드를 만들지 말것
- 특정 클라이언트에 의해 값이 변경될 수 있는 필드를 만들지 말것
- 가급적 Read-Only로 설계할 것
- 필드 대신에 자바에서 공유되지 않는 local 변수, 파라미터, ThreadLocal등을 사용할 것.
- 스프링 빈 필드에 공유값을 설정하면 로그인 했는데 남의 정보가 보이는 등 큰 장애가 발생할 수 있다.
예를 들면 다음과 같은 코드에서 문제가 발생한다.
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;
}
}
교재의 형광펜 칠한 부분대로
Stateful하게 설계되는 경우 다음과 같은 문제점이 발생한다.
ThreadA가 사용자 A 코드를 호출하고 ThreadB가 사용자 B 코드를 호출한다고 가정하자.
StatefulService의 price 필드는 공유되는 필드이나, 특정 클라이언트에 의해 값이 변경된다.
사용자 A의 주문금액이 10000원이 되어야하는데, 20000원이라는 결과로 Overwrite되는 문제가 발생한다.
공유필드는 항상 조심하자, 그냥 쓰지 말자 문제생긴다.
필드 생성대신 parameter를 통해 넘기는 방식을 고려하는 것이 더 좋다.
Configuration과 싱글톤
그런데 우리가 설계한 AppConfig 코드를 보면
package hello;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import hello.springintroduction.order.OrderService;
import hello.springintroduction.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 사용영역과 구성 영역으로 구역이 이분할 됨.
// 사용영억(클라이언트 코드), 구성영역 (DIP 코드)
@Configuration
public class AppConfig { // 생성자를 통한 주입
// 각각의 Impl에 생성자를 구현함으로써 Dependency injection이 가능함.
// XML로 사용하면 AppConfig.java 조차 필요가 없다!
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
/*public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}*/
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
memberRepostiory가 여러번 parameter로 넘어가는 것이 보인다, 이것 또한 새로 객체를 생성하는것중에 하나인데
그렇다면 각 new에서 parameter로 호출할때마다 객체가 새로 생성되어야하는것이 아닌가?
스프링 컨테이너는 어떻게 이 문제를 해결할까?
스프링 컨테이너는 싱글톤 레지스트리이기 때문에 스프링 빈이 싱글톤이 되도록 보장해주어야 하나,
스프링이 자바 코드까지 직접 조작하기는 어려운게 사실이다.
그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다
모든 비밀은 @Configuration을 적용한 AppConfig에 있다.
예상과는 다르게 클래스 명에 xxxCGLIB가 붙는다.
CGLIB는 바이트코드 조작 라이브러리를 이용하여 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고
그 다른 클래스를 스프링 빈으로 등록한 것이다
이 때 상속하여 만든 임의의 클래스에서 싱글톤을 보장하도록 해준다
내부 상세 로직은 매우 복잡하다.
결론 ---
@Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지
않는다.
크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.
'스프링 공부 (인프런 김영한 선생님) > 스프링 핵심원리' 카테고리의 다른 글
[스프링 핵심원리] 8. 의존관계 자동 주입 (0) | 2023.05.15 |
---|---|
[스프링 핵심원리] 7. Component Scan (0) | 2023.05.14 |
[스프링 핵심원리] 5. 스프링 빈 조회 상속관계 BeanFactory와 ApplicationContext (0) | 2023.05.10 |
[스프링 핵심원리] 4. 스프링 전환 / 스프링 컨테이너 기본 (1) | 2023.05.10 |
[스프링 핵심원리] 3. 객체지향 설계와 스프링, 정리 (0) | 2023.05.07 |