싱글톤 패턴에 대해 알아보자

Posted on March 11, 2021  -  6 min read

싱글톤이란?

  • 클래스를 static으로 최초 한번만 메모리에 할당하고, 그 메모리에 객체를 만들어 사용하는 디자인 패턴이다.
  • 생성자를 이용하여 반복적으로 호출해도 최초에 할당했던 메모리에 있는 객체를 반환한다.

싱글톤을 사용하는 이유

  • 우선 고정된 메모리를 사용하고, 하나의 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있다.
  • 또한 싱글톤으로 만들어진 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다.
  • 다른 객체들이 공통된 객체에 접근해야하는 경우 유용하다.
  • 인스턴스가 절대적으로 하나만 존재하는 것을 보증하고 싶은 경우 사용하기도 한다.

싱글톤의 문제점

  • 싱글톤 인스턴스가 많은 일을 하거나 많은 데이터를 공유한다면 다른 클래스들의 인스턴스와의 결합도가 높아진다.
  • 이는 객체 지향 설계 원칙의 개방-폐쇄 원칙을 위배하고, 테스트하고 수정하기 어렵게 만든다.
  • 또한 멀티 쓰레드 환경에서 동기화 처리 문제(인스턴스가 두 개 생성되는 경우) 등이 발생할 수 있다.
  • 그렇기 때문에 싱글톤은 꼭! 필요한 경우에만 사용하는 것이 좋다.

싱글톤 구현하는 법

일반적인 초기화

public class GeneralInitialization {
    private static GeneralInitialization instance = new GeneralInitialization();

    private GeneralInitialization() {}

    public static GeneralInitialization getInstance() {
        System.out.println("General instance initialized");
        return instance;
    }
}
  • 우선 다른 클래스에서 접근하지 못하도록 인스턴스를 private 으로 만든다.
  • 이 때, 전역 변수인 static을 선언하여 프로그램이 로드될 때 최초 한 번만 메모리를 할당하도록 한다.
  • 생성자도 다른 클래스에서 새로운 생성자를 통해 인스턴스를 만들지 못하도록 private으로 만든다.
  • 다른 클래스에서 인스턴스를 호출할 수 있도록 getInstance() 메서드를 만든다.
  • 생성자를 만들지 않고 바로 메서드를 호출하기 위해서 static 으로 선언한다.

Static block initialization

public class StaticBlockInitialization {
    private static StaticBlockInitialization instance;

    private StaticBlockInitialization() {}

    static {
        try {
            instance = new StaticBlockInitialization();
        } catch (Exception e) {
            throw new Error("Create Instance failed: " + e.getMessage());
        }
    }

    public static StaticBlockInitialization getInstance() {
        return instance;
    }
}
  • Static block을 이용하면 클래스가 로딩 될 때 최초 한번 실행하게 된다.
  • 초기화 블럭을 이용하면 초기 변수 세팅, 에러 처리 등과 같은 로직을 담을 수 있다.
  • 일반적인 초기화와 마찬가지로 인스턴스가 생성되는 시점에 메모리에 올라가는 것이 아닌, 프로그램이 최초로 로드 될 때 메모리에 올라간다는 문제점이 있다.

Lazy initialization

public class LazyInitialization {
    private static LazyInitialization instance;

    private LazyInitialization() {}

    public static LazyInitialization getInstance() {
        if(instance == null) {
            instance = new LazyInitialization();
        }
        System.out.println("Lazy instance initialized");
        return instance;
    }
}
  • 게으른 초기화는 앞선 두 방법과 달리 인스턴스를 호출하는 시점에 메모리에 할당하는 방법이다.
  • 위 코드와 같이 getInstance()를 통한 인스턴스 호출이 발생했을 경우 생성자를 통하여 인스턴스를 만들고 반환한다.
  • 일반적인 방법보다 메모리를 효율적으로 관리할 수 있다.
  • 하지만 만약 멀티 쓰레드 환경에서 개발을 하게 된다면 문제가 발생할 수 있다.

Thread-safe Lazy initialization

public class ThreadSafeLazyInitialization {
    public static ThreadSafeLazyInitialization instance;

    private ThreadSafeLazyInitialization() {}

    public static synchronized ThreadSafeLazyInitialization getInstance() {
        if(instance == null) {
            instance = new ThreadSafeLazyInitialization();
        }
        return instance;
    }
}
  • Lazy initialization에 synchronized 키워드를 추가하여 멀티 쓰레드 환경에서도 안전하게 작동하도록 하는 방법이다.
  • 하지만 synchronized 키워드를 사용하면 성능저하가 발생할 수 있다.

Initialization on demand holder idiom

public class LazyHolderInitialization {
    private LazyHolderInitialization() {}

    private static class LazyHolder {
        public static final LazyHolderInitialization instance = new LazyHolderInitialization();
    }

    public static LazyHolderInitialization getInstance() {
        return LazyHolder.instance;
    }
}
  • 클래스 안에 또 다른 클래스(holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스의 로드 시점을 이용한 방법이다.
  • JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용하여 싱글톤의 초기화 문제에 대한 책임을 JVM에 넘긴다.
  • 현재 싱글톤을 이용한다고 하면 이 방법을 제일 많이 이용된다.


참고

Loading script...