싱글톤 방식의 주의점

 

무상태(stateless)로 설계해야 하는 이유

웹 어플리케이션 설계에서 유용한 싱글톤 방식을 사용할 때는 싱글톤 클래스를 반드시 무상태로 설계해야 한다. 다음 예제를 보자.

 

: StatefulService.java

: StatefulServiceTest.java

 

해당 코드에서는 ThreadA에서 userA가 주문을 하고, 가격을 확정하기 전에 ThreadB에서 userB가 다른 상품을 주문한다.

싱글톤 클래스라 클래스 안의 필드도 공유하고 있기 때문에, 이 경우 userA가 주문했던 금액과는 다른 금액이 나오게 된다.

무상태로 설계하기 위해서는 기존의 클래스 안의 멤버 변수(필드)를 지역 변수로 바꾸거나, ThreadLocal을 사용해야 한다.

 

지역 변수를 사용한 예시

StatelessService.java

더보기
package hello.core.singleton;

public class StatelessService {

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

}

StatelessServiceTest.java

더보기
public class StatelessServiceTest {

    @Test
    @DisplayName("싱글톤 클래스를 무상태로 설계해야 한다.")
    void statelessServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatelessService statelessService1 = ac.getBean("statelessService", StatelessService.class);
        StatelessService statelessService2 = ac.getBean("statelessService", StatelessService.class);

        int priceA = statelessService1.order("userA", 10000);
        int priceB = statelessService2.order("userB", 20000);

        assertThat(priceA).isEqualTo(10000);
        assertThat(priceB).isEqualTo(20000);

    }

    @Configuration
    static class TestConfig {

        @Bean
        public StatelessService statelessService() {
            return new StatelessService();
        }
    }

}

 

ThreadLocal이란?

스레드(Thread)의 개념

프로그램을 실행할 때, 스레드(thread) 프로세스(process)라는 말을 사용한다.

 

 프로세스: 운영체제로부터 자원을 할당받는 작업의 단위.

여러 개의 어플리케이션을 사용한다면, 여러 개의 프로세스(multi-process)가 동작 중이다.

 

 스레드: 프로그램(프로세스) 실행의 단위.

하나의 어플리케이션(프로세스)를 실행할 때도 여러 개의 스레드(multi-thread)를 사용할 수 있다.

 

 멀티 프로세스 -> 멀티 스레드

 멀티 스레드 -> 멀티 프로세스

 

변수의 생성 영역(메모리)

객체나 변수를 생성할 때 Heap영역, 또는 Stack영역에 위치시킬 수 있다.

 

 Heap: 모든 스레드에서 공유하는 영역.

ex) 정적(static) 변수

 Stack: 하나의 스레드에서 사용하는 영역.

ex) 지역(local) 변수

 

ThreadLocal의 구조

ThreadLocal스레드 정보를 key로 사용하여 값을 저장하는 Map 구조를 갖고 있다.

멀티 스레드 환경에서 Stateful 클래스를 싱글톤으로 선언하고 싶을 때, ThreadLocal을 사용하면 각 스레드 별로 다른 변수를 사용할 수 있다.

 

ThreadLocalService.java

더보기
package hello.core.threadlocal;

public class ThreadLocalService {

    private static ThreadLocal<String> product = new ThreadLocal<>();
    private static ThreadLocal<Integer> price = new ThreadLocal<>();

    public ThreadLocalService() {}

    public void setProduct(String item) {
        product.set(item); // set()으로 value 값 지정 가능
    }

    public void setPrice(Integer value) {
        price.set(value);
    }

    public void printOrder() {
        System.out.println("product = " + product.get() + ", price = " + price.get());
				// get()으로 value 값 반환 가능
    }
}

ThreadLocalServiceTest.java

더보기
package hello.core.threadlocal;

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;


public class ThreadLocalServiceTest {

    @Test
    void threadLocalOrder() {

        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        ThreadLocalService tls = ac.getBean("threadLocalService", ThreadLocalService.class);

        Integer priceA = 10000;
        Integer priceB = 20000;
        String productA = "strawberry";
        String productB = "watermelon";

        Thread threadA = new Thread(()->{
            tls.setPrice(priceA);
            tls.setProduct(productA);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tls.printOrder();

        });

        Thread threadB = new Thread(()->{
            tls.setPrice(priceB);
            tls.setProduct(productB);
            tls.printOrder();
        });

        threadA.start();
        threadB.start();

    }

    @Configuration
    static class TestConfig {

        @Bean
        ThreadLocalService threadLocalService() {
            return new ThreadLocalService();
        }
    }

}

 

ThreadLocal 사용할 때 주의점

⚠️ 메모리 문제

ThreadLocal은 사용 시 메모리를 관리해야 한다.

ThreadPool을 사용하면 기존에 생성했던 ThreadLocal을 재사용할 수 있는데, 이때 이전에 삭제되지 않은 메모리가 남아있을 수 있다.

ThreadPool을 사용할 경우 반드시 ThreadLocal.remove()를 이용하여 남은 데이터를 제거해 주어야 한다.

 

*참고한 포스트들*

 

@Configuration과 싱글톤

스프링 컨테이너는 @Configuration 으로 등록한 클래스에서 빈을 생성 및 조회하는데, 이 빈들을 모두 싱글톤 타입으로 만든다고 했다.

그런데 @Configuration으로 등록된 AppConfig 클래스의 각 빈 메소드에서는 new() 생성자로 객체를 생성한다.

어떻게 각 빈을 호출하면서 각 객체는 한 번만 생성하는 것이 가능할까?

 

: AppConfig.java

 

테스트를 위해서 각 구현 클래스(MemberServiceImpl.java, OrderServiceImpl.java)에 MemberRepository 객체를 조회할 수 있는 클래스를 임시로 작성하고, 결과를 보자.

 

: ConfigurationSingletonTest.java

 

모든 MemberRepository 객체의 주소값이 같다.

즉 MemberRepository 객체는 딱 한 번만 생성되고, 다른 클래스는 오직 하나의 객체를 공유하고 있다.

⇒ 싱글톤 패턴을 잘 준수한다.

 

AppConfig.java 파일을 변경하여, 각 빈이 호출될 때마다 로그를 남겨서 확인해 보자.

결과는 다음과 같다.

빈이 한 번씩만 호출되었다. 스프링 컨테이너는 인스턴스를 한 번 생성하고 나면 해당 빈 메소드를 더 이상 호출하지 않은 것이다.

 

@Configuration과 바이트코드 조작

빈이 한 번씩만 호출된 이유는 스프링 컨테이너가 바이트 코드를 조작해서, 사용자가 입력한 @Bean과는 다른 클래스를 만들고 그 클래스를 통해 싱글톤 패턴을 사용하고 있기 때문이다.

 

: ConfigurationTest.java

 

스프링 컨테이너는 싱글톤 패턴을 유지하기 위해서 사용자가 @Configuration에 등록한 코드를 조작해서 새로운 클래스를 만든다. 그 클래스가 스프링 빈으로 등록된 것이다.

 

바이트 코드 조작

바이트 코드란?

 바이트 코드

고급 언어(프로그래밍 언어. 자바 언어)로 작성된 코드를 가상머신(여기서는 JVM)이 이해할 수 있는 중간 단계의 코드로 컴파일한 언어.

기계어보다는 추상적(high-level)이고 고급 언어보다는 low-level이다.

 

코드 조작이 가능한 이유

🔑 바이트 코드를 조작하면 실행 결과가 달라지는 이유

자바 코드는 1차로 JVM이 처리할 수 있는 바이트 코드로 변환된다. 그리고 그 바이트 코드를 읽어서 비로소 프로그램이 실행된다.

바이트 코드 내용대로 실행이 되기 때문에, 중간 단계에서 코드가 조작된다면 결과값도 바뀌게 된다.

참고한 포스트: [Java] 바이트코드 조작 (tistory.com)

 

코드를 조작하는 방법(예시)

💡 byteBuddy 라이브러리

바이트 코드를 조작하는 코드를 작성하고, 결과를 다른 파일에 저장한다고 치자.

저장한 파일은 원본과는 다른 코드를 갖고 있다.

참고한 포스트: 자바 바이트 코드 조작하는 방법 (tistory.com)

 

바이트 코드 조작의 결과

아마도 바이트 코드를 조작해서, 이런 싱글톤 클래스를 만들었을 것이다.

  1. 사용자가 빈으로 등록한 클래스가 이미 스프링 컨테이너에 있다 → 해당 인스턴스 반환.
  2. 스프링 컨테이너에 사용자가 빈으로 등록한 클래스가 없다 → 새로운 인스턴스 생성.

 

*정리*

@Configuration은 싱글톤 패턴으로 빈을 생성하는 어노테이션이다. @Configuration이 없어도 빈은 생성된다! 다만 싱글톤 타입이 아니다.

 

@Configuration을 지우고 AppConfig.java 를 참고하여 코드를 실행한 결과는 다음과 같다:

 

+ Recent posts