[Spring] 스프링 싱글톤 방식의 주의점

반응형

스프링 컨테이너는 객체를 Bean으로 등록해 싱글톤으로 관리한다.

동일한 객체가 매번 생성되지 않고 하나만 생성해 공유해 사용하는 이 싱글톤 방식은 메모리를 효율적으로 관리할 수 있다

하지만 주의할 점이 있다.

이번 포스팅에서는 스프링 싱글톤 방식에서 주의해야할 점에 대해 다뤄보려한다.

 

싱글톤 방식에서 주의할 점

싱글톤 방식은 하나의 객체를 여러 스레드가 공유한다.

이 공유한다는 점에서 주의할 점이 있다.

하나의 객체를 여러 스레드가 공유하기 때문에 객체의 필드 (상태)까지 공유 될 수 있다.

즉, 스프링 컨테이너에 Bean으로 등록되는 객체(Bean)은 무상태(stateless)로 설계해야한다.

 

만약 상태를 갖게 설계하면 어떤일이 벌어질까?

글만 보는 것보다 예시를 보는 것이 더 쉽게 이해될 것이다.

테스트 코드로 예시를 돌려볼 예정이라 수동 빈 등록 방식을 사용했다.

위 코드에서 눈 여겨볼 점스프링 컨테이너에 Bean으로 등록되는 PaymentService가 상태를 가지고 있고

payment()를 호출할 때마다 이 상태가 변하는 점이다.

(PaymentRepository의 코드는 설명과 무관해 첨부하지 않았다.)

 

class PaymentServiceTest {

    @Test
    void payment() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(PaymentConfig.class);

        // Thread1 : kaki 회원의 주문
        PaymentService paymentService1 = ac.getBean(PaymentService.class);
        paymentService1.payment(new User("kaki"), 15_000);

        // Thread2 : hyun 회원의 주문
        PaymentService paymentService2 = ac.getBean(PaymentService.class);
        paymentService2.payment(new User("hyun"), 300_000);

        assertThat(paymentService1.getPrice()).isEqualTo(15_000);
        assertThat(paymentService2.getPrice()).isEqualTo(300_000);
    }
}

kaki 회원은 15,000원을 결제했고, hyun은 300,000원을 결제했다.

이 코드를 실행하면 어떻게 될까 테스트 통과할까?

 

결과는 실패이다.

실패하는 이유는 싱글톤인 PaymentService가 갖고 있는 상태(필드) price가 다른 스레드와 공유되기 때문에

hyun 회원의 요청으로 kaki 회원의 결제 금액이 변경되어 버린 것이다.

15,000을 결제 했는데 300,000원이 결제된다면 상상만으로 끔찍하다. 🤯

 

무상태로 설계하려면 어떻게?

주의할점은 바로 "스프링 컨테이너가 관리하는 Bean(객체)가 상태를 가져선 안된다"는 것이다.

즉, 테이블과 매핑되는 @Entity 객체나 예시 코드에서 사용된 User같은 VO나 DTO는 스프링 컨테이너에 등록되지 않는 객체이기 때문에 상태를 가져도 상관없다.

그런데 위 처럼 스프링의 Bean(객체)가 상태를 가지게 될 경우에는 어떻게 해결해야할까?

이 경우에는 간단하다.

데이터베이스, 세션, 캐시 등을 사용하여 상태를 저장해 관리하도록 하고

비즈니스 로직을 수행하는 객체(Bean)들은 상태를 가지지 않도록(stateless)으로 구현할 수 있다.

또한 위 예시 코드 PaymentService의 경우에는 DB를 사용하지 않더라도 아래처럼 결제 금액을 반환하는 방식으로 해결할 수도 있다.

public class PaymentService {
    private final PaymentRepository paymentRepository;

    public PaymentService(final PaymentRepository paymentRepository) {
        this.paymentRepository = paymentRepository;
    }

    public int payment(User user, int price) {
        paymentRepository.save(user, price);
        return price;
    }
}