본문 바로가기
Backend

[스프링 핵심 원리 이해] 순수한 DI 컨테이너에서의 싱글톤 구현 및 문제점

by 햣둘 2025. 2. 3.

웹 애플리케이션과 싱글톤 

[ 배경 ]

- 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생함

- 대부분의 스프링 애플리케이션은 웹 애플리케이션임

- 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있음

- 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 함

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

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

        //memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }
}
// 결과
memberService1 = hello.core.member.MemberServiceImpl@780cb77
memberService2 = hello.core.member.MemberServiceImpl@691a7f8f

Process finished with exit code 0

 

[ 위 코드의 문제점 ]

- 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성함

- 고객 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성되고 소멸됨 -> 메모리 낭비가 심함!!!

 

[ 해결방안 ]

- 해당 객체가 딱 1개가 생성되고, 공유하도록 설계하면 됨 = 싱글톤 패턴

 

싱글톤 패턴

싱글톤 패턴은 "클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴"임

- 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 함

- ex) private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 함

package hello.core.singleton;

public class SingletonService {
    
    // 1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    
    // 2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance() {
        return instance;
    }
    
    // 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {
    }
    
    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

 

1. static 영역에 객체 instance를 미리 하나 생성해서 올려둠

2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있음

    이 메서드를 호출하면 항상 같은 인스턴스를 반환함

3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막음 

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

        // 같은 객체 인스턴스 반환!
        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        assertThat(singletonService1).isSameAs(singletonService2);
// 테스트 결과
singletonService1 = hello.core.singleton.SingletonService@5af97850
singletonService2 = hello.core.singleton.SingletonService@5af97850

Process finished with exit code 0

 

-> 자바가 뜰 때 객체 인스턴스 생성해놓은 것을 가져다 쓰는 거임!!! 

테스트 결과를 보면 동일한 싱글톤 객체를 참조하고 있다는 것을 알 수 있음

 

참고) 싱글톤 패턴을 구현하는 방법은 여러가지가 있음

여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했음

 

- 싱글톤 패턴을 적용하면 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있지만, 싱글톤 패턴은 다음과 같은 수많은 문제점들을 가지고 있음

 

[ 싱글톤 패턴의 문제점 ]

- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어감

- 의존관계 상 클라이언트가 구체 클래스에 의존함 -> DIP를 위반함

- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높음

- 유연하게 테스트 하기 어려움

- 내부 속성을 변경하거나 초기화하기 어려움

- private 생성자로 자식 클래스를 만들기 어려움

- 결론적으로 유연성이 떨어짐

- 안티패턴으로 불리기도 함