싱글톤 패턴(Singleton Pattern)

전홍영·2024년 3월 2일
0

디자인 패턴

목록 보기
10/12

싱글톤?

싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴을 말한다. 메모리 절약을 위해, 인스턴스가 필요할 때 똑같은 인스턴스를 새로 만들지 않고 기존의 인스턴스를 가져와 활용해야 할 때 싱글톤 패턴을 사용한다.

디자인 패턴들 중 가장 개념적으로 간단하지만 어디에 쓰이는지 어떤 문제가 있는지 알아야 한다.

구조


싱글톤의 구조는 딱히 특별한 것이 없다. 주의 깊게 보아야 할 것은 생성자의 함수의 접근 제어자가 private이다. 외부에서 new 키워드를 이용하여 새로운 객체 생성을 막기 위해서이다.
또한 자신의 인스턴스를 저장하는 클래스 변수를(static 필드) 갖는다. 자신의 인스턴스를 저장하고 있는 클래스 변수를 가지고 있어 이 값이 null일 경우 클래스 변수에 인스턴스를 할당하고 null이 아니면 Heap 메모리에 저장되어 있는 인스턴스 주소를 리턴한다.

싱글톤 구현 해보기

public class Singleton {
    //싱글톤 객체를 담을 인스턴스 변수
    private static Singleton instance;

    //생성자를 private으로 선언하여 외부에서 생성자에 접근하지 못하도록 함
    private Singleton() {
    }

    //외부에서 정적 메서드 호출하면 인스턴스가 있으면 그대로 반환하고 없으면 생성하여 반환(지연 초기화)
    public static Singleton getInstance() {
        //싱글톤 객체가 없을 경우에만 객체를 생성
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

@Test
void 싱글톤_테스트(){
    Singleton singleton = Singleton.getInstance();
    Singleton singleton1 = Singleton.getInstance();
    //같은 객체인지 검사 - true
    Assertions.assertThat(singleton).isEqualTo(singleton1);
}

가장 흔히 사용되는 싱글톤 패턴이다. 객체 생성에 대한 관리를 내부적으로 처리한다. 하지만 스레드 세이프하지 않는 단점을 가진다. 따라서 멀티 쓰레드 환경에서 각 스레드는 자신의 실행단위를 기억하면서 코드를 위에서 아래로 읽기 때문에 오류가 발생한다.

문제 코드

@Test
    void 싱글톤_멀티_쓰레드_문제(){
        // 1. 싱글톤 객체를 담을 배열
        Singleton[] singleton = new Singleton[10];

        // 2. 스레드 풀 생성
        ExecutorService service = Executors.newCachedThreadPool();

        // 3. 반복문을 통해 10개의 스레드가 동시에 인스턴스 생성
        for (int i = 0; i < 10; i++) {
            final int num = i;
            service.submit(() -> {
                singleton[num] = Singleton.getInstance();
            });
        }

        // 4. 종료
        service.shutdown();

        // 5. 싱글톤 객체 주소 출력
        for(Singleton s : singleton) {
            System.out.println(s.toString());
        }
    }

이렇게 되면 쓰레드 A가 싱글톤 객체의 if문을 통해 객체를 생성 중에 쓰레드 B가 먼저 객체를 생성해버려서 서로 다른 객체를 생성하게 되어 싱글톤이 깨져버리는 결과를 낳을 수 있다. 따라서 이러한 문제를 해결하기 위한 방법이 등장하였다.

Bill Pugh Solution(LazyHolder)

멀티쓰레드 환경에서 안전하고 Lazy Loading도 가능한 싱글톤 기법이다. 클래스 안에 내부 클래스를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법이다.

public class BillPughSolutionSingleton {
    //싱글톤 객체를 담을 인스턴스 변수
    private BillPughSolutionSingleton() {
    }

    //외부에서 정적 메서드 호출하면 인스턴스가 있으면 그대로 반환하고 없으면 생성하여 반환(지연 초기화)
    //static 내부 클래스를 이용하여 싱글톤 객체를 생성, Holder 로 만들어, 클래스가 메모리에 로드되지 않고 getInstance() 메서드가 호출될 때 로드됨
    private static class SingletonHelper {
        private static final BillPughSolutionSingleton INSTANCE = new BillPughSolutionSingleton();
    }

    public static BillPughSolutionSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

static 메서드에서는 static 필드만 호출하기 때문에 내부 클래스를 static으로 설정하고 내부 클래스의 문제의 메모리 누수를 해결하기 위해 내부 클래스를 static으로 설정한다.

다만 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점을 지닌다.

Enum 사용

enum은 멤버를 만들때 private로 만들고 한번만 초기화하기 때문에 thread safe하다. enum 내에서 상수뿐만 아니라, 변수나 메서드를 선언해 사용이 가능하기 때문에 이를 싱글톤 클래스처럼 응용이 가능하다.
하지만 만일 싱글톤 클래스를 일반적인 클래스로 마이그레이션해야할때 처음부터 코드를 다시 짜야하는 단점이 존재한다. 또한 클래스 상속이 필요할 때 enum외의 클래스 상속은 불가능하다.

public enum EnumSingleton {
    INSTANCE;
    
    EnumSingleton() {
    }
    
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }

    public void print() {
        System.out.println("Hello, EnumSingleton!");
    }
}

따라서 싱글톤의 문제점을 해결하는데 권장하는 방법은 이 두가지이다. LazyHolder를 이용하는 경우는 성능이 중요시 되는 환경에서 사용하면되고 Enum을 이용할 때는 직렬화, 안정성이 중요시되는 환경에서 사용하면 된다.

싱글톤 단점

싱글톤은 고정된 메모리 영역을 가지고 있어 하나의 인스턴스만 사용하기 때문에 메모리 낭비를 방지하며 객체 생성이 여러번 필요한 상황에서 많이 사용된다.
하지만 싱글톤은 여러 문제점을 수반한다.

모듈간 의존성

싱글톤을 이용하는 경우 대부분 인스턴스가 아닌 클래스의 객체를 생성하고 활용하기 때문에 클래스 간의 의존성과 높은 결합이 생기게도니다. 따라서 유지보수성이 떨어지고 너무 많은 곳에서 싱글톤 객체를 사용하면 문제가 발생할 수 있다.

SOLID 위배

인스턴스 자체가 하나만 생성되기 때문에 여러가지 책임을 지니게 되는 경우가 많아 SRP에 위배되는 경우가 있다. 또한 결합도가 높아져 OCP에 위배되는 경우도 있고, 의존 관계상 클라이언트가 인터페이스가 아닌 클래스에 의존하기 때문에 DIP도 위반하게 된다. 따라서 싱글톤 객체를 너무 많은 곳에서 사용한다면 잘못된 디자인 형태라고 할 수 있다.

TDD 단위 테스트 애로 사항

단위 테스트 시에 단위 테스트는 서로 독립적으로 실행되어야 하는데 싱글톤 객체를 사용하게 된다면 자원을 공유하기 때문에 테스트가 온전히 실행된다는 보장을 할 수 없다.

결론

싱글톤 기법은 오직 한개의 객체 생성으로 성능을 올릴 수 있으나 부작용이 존재한다. 따라서 많은 이들이 싱글톤 패턴은 유연성이 떨어지는 안티 패턴이라 주장한다. 그래서 직접 개발자가 사용하는 것보다는 스프링 컨테이너와 같은 프레임워크에 도움을 받으면 싱글톤 문제점을 보완하면서 장점의 혜택을 누릴수 있다.
스프링 컨테이너는 내부적으로 클래스의 제어를 IoC 방식으로 컨테이너에게 넘겨 컨테이너가 관리하기 때문에, 이를 통해 평법한 객체도 하나의 인스턴스 뿐인 싱글톤으로 존재가 가능하게 된다.

예제 코드
참고: https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4-%EA%BC%BC%EA%BC%BC%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90#%E2%9A%A0%EF%B8%8F_%EB%A9%80%ED%8B%B0_%EC%93%B0%EB%A0%88%EB%93%9C_%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98_%EC%B9%98%EB%AA%85%EC%A0%81%EC%9D%B8_%EB%AC%B8%EC%A0%9C%EC%A0%90

profile
Don't watch the clock; do what it does. Keep going.

0개의 댓글