본문 바로가기
WEB/Spring

[Spring] 싱글톤 패턴과 싱글톤 컨테이너 개념

by 정권이 내 2023. 12. 13.

[Spring] 싱글톤 패턴과 싱글톤 컨테이너 개념

 

싱글톤 컨테이너의 개념

싱글톤 컨테이너란 클래스의 인스턴스가 Java JVM 내의 단 하나만 존재하는 것을 뜻합니다.

웹 애플리케이션은 수많은 클라이언트로부터 서비스 요청을 받는데 만약 서버에서 클라이언트의 요청을 받아 처리할때마다 내부에서 클래스 인스턴스를 생성하게 되면 JVM 메모리의 사용량이 증가하게 되고 서버는 부하를 감당할 수 없게 될 것입니다.

 

테스트 코드

간단한 테스트 코드를 통해 위의 상황을 확인 해보겠습니다.

 

Appconfig.java

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository getMemberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy getDiscountPolicy() {
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(getMemberRepository(), getDiscountPolicy());
    }
}

 

SingletonTest.java

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        //1. 조회 : 호출할때마다 인스턴스를 생성
        MemberService memberService1 = appConfig.memberService();

        //2. 조회 : 호출할때마다 인스턴스를 생성
        MemberService memberService2 = appConfig.memberService();

        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }
}

 

결과

memberService1 = com.example.springdemostudy.member.MemberServiceImpl@29526c05
memberService2 = com.example.springdemostudy.member.MemberServiceImpl@42b02722

 

Appconfig는 MemberService의 인스턴스 반환해주는 클래스이고 SingletonTest 클래스의 pureContainer 메서드에서는 Appconfig를 통해 만들어진 2개의 Memberservice 인스턴스의 참조값이 다른 것을 확인할 수 있습니다.

즉, 스프링이 아닌 순수 Java 코드를 이용한 AppConfig는 사용자의 요청이 들어올 때마다 새로운 MemberService 클래스의 인스턴스를 생성하게 되고 생성 할때마다 JVM 메모리 사용량이 증가하게 되는 것입니다.

해결방법은 AppConfig에서 생성한 MemberService 객체를 JVM에서 공유하는 것입니다.

싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴이란 JVM에서 클래스의 객체가 단 하나만 존재하게 하는 디자인 패턴입니다. 가장 기초적인 방법은 클래스 내부에서 private static final 키워드로 객체를 만들면 외부에서는 해당 클래스의 객체를 새로 생성할 수 없으므로 싱글톤 패턴 조건을 만족하게 됩니다.

 

SingletonService.java

SingletonService의 객체 instance를 private static final 키워드로 선언하였습니다.

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() {
        
    }
}

 

Test.java

public static void main(String[] args) {
    SingletonService singletonService = new SingletonService();
}

다른 클래스인 Test에서 SingletonService 클래스의 객체를 생성하려고 한다면 'SingletonService()' has private access in...이라는 에러를 확인할 수 있습니다. SingletonService 클래스의 생성자를 호출할때 private 타입의 SingletonService 인스턴스인 instance를 호출할수 없기때문에 SingletonService 클래스 객체는 getInstance 메서드를 통해서만 접근할 수 있게 됩니다.

 

테스트 코드

SingletonTest.java

@Test
void singletonServiceTest(){
    SingletonService singletonService1 = SingletonService.getInstance();
    SingletonService singletonService2 = SingletonService.getInstance();

    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);
    
    Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}

 

결과

singletonService1 = com.example.springdemostudy.singleton.SingletonService@6e4566f1
singletonService2 = com.example.springdemostudy.singleton.SingletonService@6e4566f1

동일한 인스턴스가 반환되는 것을 확인할 수 있습니다.

 

여기까지는 순수 Java 코드로 싱글톤 패턴을 적용하는 방법인데 실제로 프로젝트에서 사용하기에는 문제점들이 존재합니다.

  • 싱글톤 패턴을 구현하기 위한 코드가 늘어남

  • 인스턴스를 반환해주는 구현 클래스를 직접 참조해야 하므로 DIP를 위반한다. (OCP 원칙)

  • 내부 속성을 변경, 초기화하기가 어렵다.

  • private 생성자로 자식 클래스를 생성하기 어렵다.

  • 유연성이 떨어진다.

     

싱글톤 컨테이너 (Singleton Conatiner)

스프링에서는 사용자가 일일이 구현하지 않고 싱글톤 패턴의 문제점들도 보완해주면서 싱글톤 패턴으로 클래스의 인스턴스를 사용할수 있도록 해주는 기능이 있는데 이를 싱글톤 컨테이너라고 합니다.

스프링 컨테이너는 싱글톤 컨테이너의 역할을 하며 싱글톤 객체를 생성, 관리하는 주체를 싱글톤 레지스트리라고 부릅니다. 스프링 컨테이너의 기능으로 인해 순수 Java에서 싱글톤 패턴을 구현하기 위한 코드를 사용하지 않아도 되고 OCP원칙을 위배하지 않게 되었습니다.

 

테스트 코드

SingletonTest.java

@Test
void springContainer() {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);

    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);

    Assertions.assertThat(memberService1).isSameAs(memberService2);

}

 

테스트 결과로 MemberService 클래스의 인스턴스가 동일한 값임을 확인할 수 있습니다.

memberService1 = com.example.springdemostudy.member.MemberServiceImpl@78e16155
memberService2 = com.example.springdemostudy.member.MemberServiceImpl@78e16155

 

스프링 컨테이너 개념도

스프링 컨테이너에서 미리 만들어진 memberService 객체를 공유하게 됩니다.

img

싱글톤 방식의 주의점

싱글톤 패턴은 여러 클라이언트가 객체를 공유하므로 공유되는 객체는 어떠한 상태를 유지(stateful) 되게 설계하면 안 되고 다음과 같은 주의사항을 반드시 지켜야 합니다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안 된다.

  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.

  • 읽기만 가능해야 한다.

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

     

테스트 코드를 통해 위 주의사항을 위배할 경우 어떤 문제가 발생하는지 알아보겠습니다.

 

StatefulService.java

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;
    }
}

 

StatefulServiceTest.java

class StatefulServiceTest {

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

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

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

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

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

}

 

결과

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

userA는 10,000원을 결제 하였지만 주문금액을 조회해보니 20,000원이 나오는 문제가 발생합니다.

원인은 사용자의 주문정보를 관리하는 StatefulService 클래스의 인스턴스를 사용하는데 스프링 컨테이너로 공유되기 때문에 userB StatefulService 인스턴스에서 order 메서드를 호출하면서 price 변수값을 변경했기 때문입니다.

 

해결방법은 공유되는 필드인 price를 제거하고 order 메서드에서 값을 반환하면 됩니다.

 

StatefulService.java

public class StatefulService {

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

 

StatefulServiceTest.java

class StatefulServiceTest {

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

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

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

        Assertions.assertThat(userAPrice).isEqualTo(10000);
    }

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

 

결과

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

 

연관 포스팅

반응형

댓글