1) 대부분의 스프링 애플리케이션은 웹 애플리케이션 - 웹 애플리케이션은 보통 여러 유저가 동시에 요청을 한다. - 유저가 동시에 요청을 할 때 마다 서비스 객체를 생성하고 소멸 -> 메모리 낭비가 심하다 - 이를 해결할 수 있는 방법은, 웹 애플리케이션 서비스에 필요한 객체를 딱 1개만 생성하고, 공유하도록 설계 ->싱글톤 패턴 사용 2) 싱글톤 패턴 - 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴 - 객체 인스턴스가 2개 이상 생성되지 못하도록 막아야 한다 -private 생성자를 사용해서 외부에서 new 키워드를 통해 객체를 생성하는 것을 막는다. - 싱글톤 패턴을 적용하면 유저의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 생성된 객체를 공유해서 효율적으로 사용
3) 싱글톤 패턴의 문제점 - 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. (코드의 양이 많아진다.) - 의존관계상 클라이언트가 구체 클래스에 의존 -> DIP 위반 - 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성 높다. - 내부 속성을 변경하거나 초기화 하기 어렵다. - private 생성자로 자식 클래스를 만들기 어렵다. - 유연성이 떨어진다.
■ 싱글톤 컨테이너
- 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리 - 스프링 컨테이너는 싱글톤 컨테이너 역할 - 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트 - 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지 - 스프링 컨테이너 덕분에 유저의 요청이 들어올때마다 새로운 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용
■ 싱글톤 방식의 주의점
싱글톤 방식은 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계 X
[무상태(stateless)]로 설계 - 특정 클라이언트에 의존적인 필드가 있으면 안된다. - 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다. - 가급적이면 읽기만 가능해야한다. - 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용
스프링 빈의 필드에 공유 값을 설정하면 큰 장애가 발생할 가능성이 매우 높다.
■ 상태를 유지할 경우 발생하는 문제점 예시
-StatefulService의 price 필드는 공유 필드이다. 이 경우, 특정 클라이언트가 값을 변경하게 되면 예상치 못한 결과 값을 조회하게 된다.
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;
}
}
import hello.core.AppConfig;
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.junit.jupiter.api.Assertions.*;
class StatefulServiceTest {
@Test
@DisplayName("상태를 유지하는 경우 문제 발생")
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//단순하게 사용하기 위해 Thread는 사용하지 않음
//ThreadA : order1 사용자가 10000원 주문
statefulService1.order("order1", 10000);
//ThreadB : order2 사용자가 20000원 주문
statefulService2.order("order2", 20000);
//ThreadA : order1 사용자가 주문 금액을 조회, 10000원을 기대했지만
//기대와 다르게 20000원이 조회회
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(10000);
}
@Configuration
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
■ @Configuration과 싱글톤
1) @Configuration과 바이트코드 조작의 마법 - 스프링 컨테이너는 싱글톤 레지스트리이기때문에, 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. - 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용 - AnnotationConfigApplicationContext에 파라미터로 넘긴 AppConfig.class 값도 스프링 빈으로 등록된다. - 스프링 빈으로 등록된 AppConfig의 클래스 정보를 출력하면, 위의 출력 결과처럼 xxxCGLIB복잡한 값이 조회된 것을 확인할 수 있다. - 이 클래스는 내가 만든 것이 아니라 스프링이 CGLIB 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 이 다른 클래스를 스프링 빈으로 등록한 것이다. - 위의 AppConfig@CGLIB 클래스가 싱글톤을 보장해준다. 아마 바이트 코드를 조작해서 작성되었을 것이다. - AppConfig@CGLIB 클래스의 예상 코드를 확인해보면 다음과 같은 프로세스로 구현되어 있을 것이다.(실제로는 CGLIB의 내부 기술을 사용하며, 매우 복잡하다) - @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환, 등록된 스프링 빈이 없으면 생성해서 스프링 빈으로 반환하는 코드가 동적으로 만들어진다. - 이 덕분에 싱글톤이 보장되는 것이다. - [참고] AppConfig@CGLIB은 AppConfig의 자식 타입으로, AppConfig 타입으로 조회할 수 있다.
2) @Configuration를 적용하지 않은 경우 - @Configuration을 적용하지 않고 @Bean만 적용한 경우 싱글톤이 보장되지 않는다. - CGLIB 기술 없이 순수한 AppConfig가 스프링 빈에 등록
[정리] - @Bean만 사용해도 스프링 빈으로 등록되지만 싱글톤은 보장되지 않는다. - 스프링 설정 정보는 항상 @Configuration 어노테이션을 사용하자.