Java에서 싱글톤(Singleton) 패턴을 사용하는 이유와 주의할 점

주의

이 문건은 과거 Hexo 블로그 (2019-09-23) 에서 이동된 문서입니다.

시간이 지남에 따라 최신 기술과 다를 수 있으니 주의 바랍니다.



Java에서 Singleton 패턴이란?

Singleton(이하 싱글톤) 패턴은 자바에서 많이 사용한다.
먼저 싱글톤이란 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 객체를 만들어 사용하는 디자인 패턴 을 의미한다.
즉 생성자의 호출이 반복적으로 이뤄져도 실제로 생성되는 객체는 최초 생성된 객체를 반환 해주는 것이다.
보통 아래와 같이 사용하게 된다.

public class ExampleClass {
    //Instance
    private static ExampleClass instance = new ExampleClass();

    //private construct
    private ExampleClass() {}

    public static ExampleClass getInstance() {
        return instance;
    }
}

위 코드에서는 instance라는 전역 변수를 선언하는데 static을 줌으로써 인스턴스화 하지 않고 사용할 수 있게 하였지만 접근 제한자가 private 로 되어 있어 직접적인 접근은 불가능하다.
또한 생성자도 private으로 되어 있어 new 를 통한 객체 생성도 불가능하다.
결국 getInstance 메서드를 통해서 해당 인스턴스를 얻을 수 있게 된다.
위의 예제는 아주 작은 규모에서 사용할 수 있는 싱글톤 패턴이며, 다음에서 좀 더 알아보기로 한다.


그렇다면 싱글톤 패턴을 사용하는 이유는?

위에서도 언급된 바와 같이 한번의 객체 생성으로 재 사용이 가능하기 때문에 메모리 낭비를 방지할 수 있다.
또한 싱글톤으로 생성된 객체는 무조건 한번 생성으로 전역성을 띄기에 다른 객체와 공유가 용이하다.
이렇게만 보면 싱글턴이 좋아보일 수 있지만 문제점도 존재한다.


싱글톤의 문제점

싱글톤도 위에서 언급된 것 처럼 전역성을 띄면서 다른 객체와 공통으로 사용하는 경우와 같은 몇 가지 케이스에서만 사용할 때 효율적이며 그 외에는 문제점이 생길 수 있다.

일단 싱글톤으로 만든 객체의 역할이 간단한 것이 아닌 역할이 복잡한 경우라면 해당 싱글톤 객체를 사용하는 다른 객체간의 결함도가 높아져서 객체 지향 설계 원칙에 어긋나게 된다. (개방-폐쇄)

또한 해당 싱글톤 객체를 수정할 경우 싱글톤 객체를 사용하는 곳에서 사이드 이팩트 발생 확률이 생기게 되며, 멀티 쓰래드환경에서 동기화 처리 문제등이 생기게 된다.


다양한 싱글톤의 구현

싱글톤을 구현하는 방법은 몇 가지가 있는데 아래와 같이 구현할 수 있다.

static block

public class ExampleClass {
    //Instance
    private static ExampleClass instance;

    //private construct
    private ExampleClass() {}

    static {
        try { instance = new ExampleClass();}
        catch(Exception e) { throw new RuntimeException("Create instace fail. error msg = " + e.getMessage() ); }
    }

    public static ExampleClass getInstance() {
        return instance;
    }
}

위와 같이 static 블럭을 사용힐 경우 클래스가 로딩될 때 한번만 실행을 하게 되는 특성을 사용한다.
하지만 인스턴스가 사용되는 시점이 아닌 클래스 로딩 시점에 실행이 된다.

lazy init

위 static 방법에서 개선하여 클래스 로딩 시점이 아닌 인스턴스가 필요하여 요청할 때 생성되는 형태로 작성하였다.

public class ExampleClass {
    //Instance
    private static ExampleClass instance;

    //private construct
    private ExampleClass() {}

    public static ExampleClass getInstance() {
        if (instance == null) { instance = new ExampleClass();}
        return instance;
    }
}

하지만 위 형태로 구성할 경우 멀티 쓰레드 환경에서 취약하다. 특정 쓰레드가 동시에 getInstance() 메서드를 호출하게 되면 인스턴스가 두 번 생성되는 문제가 발생한다.

Thread safe + lazy

public class ExampleClass {
    //Instance
    private static ExampleClass instance;

    //private construct
    private ExampleClass() {}

    public static synchronized ExampleClass getInstance() {
        if (instance == null) { instance = new ExampleClass();}
        return instance;
    }
}

Lazy에서 보였던 getInstance() 메서드에 synchronized 키워드를 붙임으로써 쓰레드에서 동시 접근에 대한 문제를 해결하였다.
하지만 synchronized 키워드는 성능 저하를 발생시킨다.

Holder

public class ExampleClass {

    //private construct
    private ExampleClass() {}

    private static class InnerInstanceClazz() {
        private static final ExampleClass instance = new ExampleClass();
    }

    public static ExampleClass getInstance() {
        return InnerInstanceClazz.instance;
    }
}

JVM의 클래스 로더 메커니즘과 클래스의 로드 시점을 이용하여 내부 클래스를 통해 생성 시킴으로써 쓰레드 간의 동기화 문제를 해결한다.
위 방법은 현재 java에서 싱글톤 생성에서 사용하는 대표적인 방법이다.


정리

싱글톤 패턴은 Spring framework에서도 많이 사용되며, 어떤식으로 구현하는지 알아두면 도움이 된다.

자바와 Spring에서의 싱글톤 차이점이라면, 싱글톤 객체의 생명주기가 다르다.

또한 자바에서 공유 범위는 Class loader 기준이지만, Spring에서는 ApplicationContext가 기준이 된다.


참고


Written by@MHLab
로또는 흑우집합소 🎲
와인관리, 시음노트, 셀러관리는 마와셀 🥂

🫥 My Service|  📜 Contact|  💻 GitHub