백엔드/spring boot

싱글톤 패턴 방식의 주의사항

STUFIT 2023. 4. 18. 17:56
반응형

싱글톤 패턴?

싱글톤 패턴은 객체 인스턴스를 하나만 생성해서 공유하는 방식으로서 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하는 것을 말한다.

주의사항

- 싱글톤 패턴은 위의 설명처럼 하나의 객체 인스턴스를 공유하는 방식이기 때문에 상태유지(stateful)하게 설계하면 안되고 무상태(stateless) 로 설계를 해야된다!

무상태?

- 특정 클라이언트에 의존적인 필드가 존재하면 안된다.

- 특정 클라이언트가 값을 변경할 수 있는 필드가 존재하면 안된다.

- only write

- 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, threadlocal 등을 사용해야 한다.

예시



package hello.core.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; 
	}
}



--------------------------------------------------------------------------------
package hello.core.singleton;
import org.assertj.core.api.Assertions; 
import org.junit.jupiter.api.Test; 
import org.springframework.context.ApplicationContext; 
import org.springframework.context.annotation.AnnotationConfigApplicationContext; 
import org.springframework.context.annotation.Bean;

public 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 주문 금액 조회 
    int price = statefulService1.getPrice(); 
    //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력 
    System.out.println("price = " + price);

    Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);

}

static class TestConfig {

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

}

}

위의 코드에서는 userA와 userB 가 있는데, A가 10000원을 등록하고 조회하는 사이에 B가 20000원을 등록한다면, A의 값을 조회하는 순간 A는 20000원이 나오게 된다.

이는 StatefulService의 price 필드가 공유필드이기 때문에 값이 변경되는 것이다.

그러므로 항상 공유필드는 조심해야 하므로 무상태(stateless)로 설계해야 한다!

아래는 무상태로 변경한 코드값이다.

package hello.core.singleton;

public class StatefulService {

    private int price; //상태를 유지하는 필드
    public int order(String name, int price) { // void 를 int로 변경
        System.out.println("name = " + name + " price = " + price);
//        this.price = price; // 이부분은 문제가 되므로 주석
        return price;

    }
}


----------------------------------------------------------------------------------
package hello.core.singleton;

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

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

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // 변수 선언하여 변경
        int userA = statefulService1.order("userA", 10000);
        // 변수 선언하여 변경
        int userB = statefulService2.order("userB", 20000);

        //ThreadA: 사용자 A의 주문 금액 조회
//        int price = statefulService1.getPrice();
        System.out.println("price = " + userA);
        System.out.println("price = " + userB);


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

        }
    }
}

 

반응형