본문 바로가기

Spring

[인프런 강의 복습] 스프링 핵심 원리 - 싱글톤 컨테이너

■ 웹 애플리케이션과 싱글톤

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 어노테이션을 사용하자.

 

 

[출처 : 인프런 스프링 핵심 원리 기본편]