Spring, Apache, Java

스프링 빈은 무상태로 설계해야 한다 ❗️

cwchoiit 2024. 5. 24. 10:44
728x90
반응형
SMALL

정답은 아닌데 개인적으로 맞다고 본다. 스프링은 기본적으로 스프링 빈을 싱글톤으로 등록한다.

싱글톤이 안티패턴이라고까지 불릴 정도로 싱글톤은 여러 문제점이 있는데 스프링은 그 문제들을 전부 해결해준 상태로 싱글톤으로 등록해준다.

 

싱글톤의 문제점

  • 싱글톤을 만들어내야 하는 코드 자체가 번거롭다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. (싱글톤클래스.getInstance())
  • 구체 클래스에 의존한다는것은 DIP(의존관계 역전 법칙)를 위반한다는 뜻이고 그 뜻은 OCP원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다
  • 자식 클래스를 만들기 어렵거나 불가능하다

 

싱글톤은 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든 인스턴스가 하나이고 그걸 공유하는 방식이라면 상태를 유지하게 설계하면 안된다. 

 

  • 무상태로 설계하거나, 읽기 전용 필드만을 취급하는게 문제가 발생하지 않을 가장 좋은 방법이다.
  • 필드 대신 자바에서 공유되지 않는 지역변수나 ThreadLocal을 사용해야 한다.

 

스프링 빈 필드에 공유값을 설정하면 장애가 발생할 수 있고 이 장애가 이 원인인걸 파악하기가 정말 어렵다.

아래 예시 코드를 들어서 상태를 가지는 경우에 발생하는 문제를 보자.

 

StatefulService

package org.example.springcore.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;
    }
}

위 클래스를 스프링 빈으로 등록할 것이다(싱글톤). 그리고 price 필드를 공유하고 있다. 거기다가 필드는 읽기 전용도 아니고 쓰기가 행해지고 있다.

 

StatefulServiceTest

package org.example.springcore.singleton;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import static org.assertj.core.api.Assertions.*;

class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

        // ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);
        // ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);

        // ThreadA: 사용자A는 주문 금액을 조회한다. 사용자 A 입장에선 당연히 10000원을 기대한다.
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        // 하지만 2만원이 된다.
        assertThat(price).isEqualTo(20000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

실행결과

name = userA, price = 10000
name = userB, price = 20000
price = 20000

 

멀티 쓰레드 환경을 만드는 건 좀 더 복잡하니까 동시에 사용자가 스프링 빈 서비스에 요청을 했다고 가정해보자.

사용자A(Thread A)는 만원짜리 상품을 주문한다.

사용자B(Thread B)는 2만원짜리 상품을 주문한다.

 

당연히 사용자A는 본인의 주문 금액은 만원이라고 생각하고 결제를 한다. 결과는 2만원으로 결제가 된다. 

 

이런 문제가 발생한다. 싱글톤이 값을 공유하는 순간부터 그 코드는 문제가 생길 여지가 너무 크다.

그럼 어떻게 해결할까?

 

여러 해결방법이 있지만 지역변수를 사용하는 아주 간단한 해결방법이 있다.

 

StatefulService

package org.example.springcore.singleton;

public class StatefulService {
    
    public int order(String name, int price) {
        System.out.println("name = " + name + ", price = " + price);
        return price;
    }
}

 

그냥 이 코드처럼 결제 금액을 알려주기 위해 반환하면 그만이다. 왜 굳이 필드에 값을 저장해서 getter를 사용해야 하는가?

 

결론

결론은 스프링 빈 또는 싱글톤에서 (합리적인 이유가 있지 않은 이상) 적어도 나는 상태를 무상태로 설계하기로 마음먹었다.

 

728x90
반응형
LIST