백앤드/SpringBoot + Java 좋은 객체 지향 설계

[SpringBoot + Java] 좋은 객체 지향 설계(싱글톤)

맏리믓 2023. 8. 1. 19:29

들어가며

- 인프런 김영한 님의 싱글톤 강의 정리입니다.

- 웹 어플리케이션은 보통 수십, 수백명의 고객의 요청을 동시에 수행 합니다.

- 이 때 각 요청이 들어 올 때 마다 Service class 에서 객체를 새로 만들어 요청을 수행 한다면 너무 많은 객체가 생성 되어 메모리가 낭비 됩니다.


싱글톤(순수 자바)

- 싱글톤을 간단히 정의 하면 클래스의 인스턴스가 딱 하나만 생성 되는것을 보장 하는 패턴 입니다.

- 즉 위와 같은 상황에서 단 하나의 인스턴스 만을 만들어 이를 공유 하며 사용 하게 하는 것입니다.

 

- 이렇게 하기 위한 방법중 하나가 아에 2개가 생성되지 못하게 하는 것 입니다.

 . 아래와 같이 Service 를 만들게 되면 생성자가 private 이기 때문에 외부에서는 생성 할 수 없게 됩니다.

 . 따라서 해당 instance 가 필요 할 때는 getInstance 를 통해 Service 내에서 생성된 instance 를 가져와야 합니다.

 . 이렇게 되면 무조건 단 하나의 인스턴스 만을 생성 할 수 있게 됩니다.

public class SingletonService{
	private static SingletonService instance = new SingletonService();
    
    public static SingletonService getInstance(){
    	return instance;
    }
    
    private SingletonService() {}
}

테스트(사용법)

- 실제로 하나의 인스턴스 만이 생성 되는지 확인 해 보기 위해 테스트를 진행 해 보겠습니다.

 . 위 조건에 맞추어 getInstance 로 인스턴스를 가져 오고 동일 한지 확인 해 보았습니다.

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest(){
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
    }

문제점

- 싱글톤에 장점만 존재 할 수는 없기 때문에 여러 단점도 존재 합니다.

 . 우선 싱글톤을 구현 하는데 코드가 많이 들어 갑니다.

 . 또한 의존관계상 클라이언트가 여러 구현 클래스를 의존 해야 하기 때문에 DIP 를 위반 합니다.

 . 이에 따라 OCP 를 위반할 가능성이 높습니다.

 . 결국 코드가 유연하지 못하게 됩니다.


스프링 컨테이너(싱글톤 컨테이너)

- 스프링 컨테이너는 위의 문제를 해결하면서 싱클톤으로 관리 할 수 있는 방법 입니다.

 . 컨테이너는 빈으로 등록된 객체들을 생성해 모아 두고 관리 합니다.

 . 즉 별도의 작업을 하지 않고 싱글톤을 유지 할 수 있습니다.

- 이렇게 되면 코드가 지저분 해 지지도 않고 DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있습니다.

 

- 테스트로 확인 할 수 있습니다.

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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

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

주의해야 할 점

- 다만 인스턴스가 하나만 생성 되기에 생기는 문제점도 존재 합니다.

 . 다음과 같이 본인이 구매한 금액이 아닌 B 가 구매한 물품의 금액이 출력 되는게 그 예시 입니다.

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

        //A 사용자가 10000원을 주문 함
        statefulService1.order("userA", 10000);
        //B 사용자가 20000원을 주문 함
        statefulService2.order("userB", 20000);

        //A 사용자가 자신의 주문 금액을 조회 -> 20000원이 나와 버림 하나의 해결 책은 위에 넣을때 바로 반환 받아 버리면 됨
        int price = statefulService1.getPrice();
    }

- 이렇듯 인스턴스를 하나만 생성해서 공유 할 때는(싱글톤 객체 일때는) 객체의 상태를 유지하게 설계해서는 안됩니다.

 . 위 상황에서도 A 가 10000원을 주문 한 후 10000원이 라는 상태가 유지 되었기 때문에 문제가 발생 하였습니다.

 . 이 문제를 어떻게 해결 할 수 있을까요?

 

- 바로 무상태(stateless) 로 설계 하면 됩니다.

 . 위 예시에서 하나의 해결책을 제시 하자면 주문을 함과 동시에 바로 orderPrice 를 return 해 버리면 됩니다.

 . 그렇게 되면 A 의 주문 상태가 유지 되지 않기 때문에 문제가 발생 하지 않습니다.

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

        //A 사용자가 10000원을 주문 함
        int orderPriceA = statefulService1.order("userA", 10000);
        //B 사용자가 20000원을 주문 함
        int orderPriceB = statefulService2.order("userB", 20000);

        System.out.println(orderPriceA);
    }