본문 바로가기
CS/디자인패턴

[CS] 싱글톤 패턴을 구현하는 7가지 방법

by Johnny's 2023. 8. 31.

싱글톤 패턴을 구현하는 7가지 방법

1. 단순한 메서드 호출

싱글톤 패턴 생성 여부를 확인하고 싱글톤이 없으면 새로 만들고 있다면 만들어진 인스턴스를 반환

 

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() { // 호출이 되었을 때 인스턴스 생성
        if (instance == null) {
        instance = new Singleton();
        }
        return instance;
    }
}


그러나 이 코드는 메서드의 원자성이 결여되어 있음. 멀티스레드 환경에서는 싱글톤 인스턴스를 2개 이상 만들 수 있음

 

thread - Java

public class YunhaSync {

    private static String yunha = "오르트구름";

    public static void main(String[] agrs) {
        YunhaSync a = new YunhaSync();
        // 쓰레드1
        new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            a.say("사건의 지평선");
        }
        }).start();

        //쓰레드2
        new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            a.say("오르트 구름");
        }
    }).start();

    }

    public void say(String song) {
        yunha = song;
        try {
            long sleep = (long) (Math.random() * 100);
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!yunha.equals(song)) {
            System.out.println(song + " | " + yunha);
        }
    } 
}

// 사건의 지평선 | 오르트 구름
// 오르트 구름 | 사건의 지평선
// 사건의 지평선 | 오르트 구름
// 오르트 구름 | 사건의 지평선
// 사건의 지평선 | 오르트 구름
// 오르트 구름 | 사건의 지평선
// 사건의 지평선 | 오르트 구름
// 오르트 구름 | 사건의 지평선
// 사건의 지평선 | 오르트 구름
// 오르트 구름 | 사건의 지평선

 

say 함수 분석

1. yunha = song 할당

2. sleep

3. yunha == song -> true

 

쓰레드2개 각각 다른 매개편수를 기반으로 say 함수를 호출

T1 → yunha = 사건의 지평선

sleep 일 때, T2 yunha = 오르트 구름

결국 yunha !== song 다를 수 있음

 

2. synchronized

인스턴스를 반환하기 전까지 격리 공간에 놓기 위해 synchronized 키워드로 잠금을 할 수 있음. 최초로 접근한 스레드가 해당 메서드 호출시다른 스레드가 접근하지 못하도록 잠금(lock)을 걸어줌
이 때 getInstance() 메서드를 호출할 때마다 lock이 걸려 성능 저하가 됨
인스턴스가 만들어졌는데도 getInstacne()는 호출이 가능하니 해당 부분도 좋지 않음

 

  public synchronized void say(String song) {
        yunha = song;
        try {
            long sleep = (long) (Math.random() * 100);
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!yunha.equals(song)) {
            System.out.println(song + " | " + yunha); //실행 안됨
        }
    }

 

synchronized 키워드를 추가하면 yunha == song이 true가 되므로 출력 되지 않음

 

3. 정적 멤버

정적(static) 멤버나 블록은 런타임이 아니라 최초에 JVM이 클래스 로딩 때 모든 클래스들을 로드할 때 미리 인스턴스를 생성하는데 이를 이용한 방법
클래스 로딩과 동시에 싱글톤 인스턴스를 만듬. 그렇기 때문에 모듈들이 싱글톤 인스턴스를 요청할 때 그냥 만들어진 인스턴스를 반환하면 됨

 

public class Singleton {
	
    //처음에 할당
    private static final Singleton instance = new Singleton(); //정적 멤버

    private Singleton() {}

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

 

이는 불필요한 자원낭비라는 문제점이 있음(호출하지 않아도 무조건 최초에 만들게 됨) 싱글톤 인스턴스가 필요없는 경우도 무조건 싱글톤 클래스를 호출해 인스턴스를 만들어야 하기 때문

 

4. 정적 블록

public class Singleton {

    private static Singleton instance = null;
    
    //정적 블록
    static {
        instance = new Singleton();
    }

    private Singleton() {}

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

 

5. 정적 멤버와 Lazy Holder(중첩 클래스)

singleInstanceHolder라는 내부 클래스를 하나 더 만듬으로써, Singleton클래스가 최초에 로딩되더라도 함께 초기화가 되지 않고, getInstance()가 호출될 때 singleInstanceHolder 클래스가 로딩되어 인스턴스를 생성하게 됨

 

class Singleton {

    private static class singleInstanceHolder {

        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }
}

 

 

6.이중 확인 잠금(DCL)

이중 확인 잠금(DCL, Double Checked Locking)

인스턴스 생성 여부를 싱글톤 패턴 잠금 전에 한번, 객체를 생성하기 전에 한 번 2번 체크하면 인스턴스가 존재하지 않을 때만 잠금을 걸 수 있기 때문에 앞서 생겼던 문제점을 해결할 수 있음

 

public class Singleton {

    private volatile Singleton instance;
    private Singleton() {}

    public Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
            if (instance == null) { // 체크 2번
              instance = new Singleton();
            }
        }
    }
        return instance;
    }
}

 

volatile

메모리구조

메인 메모리 위에 CPU 캐시메모리라고 불리는 L3, L2, L1 캐시가 있음(L4도 드물긴 하지만 L4까지 CPU 캐시 메모리라고 부름)

JAVA에서는 스레드 2개가 열리면 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 캐시 메모리에서 각각의 캐시메모리를 기반으로 가져옴

 

무한루프 발생

public class Test {

    boolean flag = true;

    public void test() {
        //쓰레드1
        new Thread(() -> {
            int cnt = 0;
            while (flag) {
            cnt++;
            }
            System.out.println("Thread1 finished\n");
        }).start();
        //쓰레드2
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ignored) {}
            System.out.println("flag to false");
            flag = false; //flag를 false로
        }).start();
    }

    public static void main(String[] args) {
        new Test().test();
    }
}

 

그렇기 때문에 앞의 코드와 같은 변수 값 불일치 문제가 발생
근데 여기서 volatile 키워드를 추가하게 되면 Main Memory를 기반으로 저장하고 읽어오기 때문에 이 문제를 해결

 

volatile boolean flag = true;

//flag to false
//Thread1 finished

 

7. enum

enum의 인스턴스는 기본적으로 스레드세이프(thread safe)한 점이 보장되기 때문에 이를 통해 생성할 수 있음

 

public enum SingletonEnum {

    INSTANCE;
    public void oortCloud() {}
}

 

정리

5번 정적 멤버와 Lazy Holder(중첩 클래스) 가장 많이 쓰이고, 7번 enum 이펙티브 자바를 쓴 조슈아 블로크가 추천

 

 

A single-element enum type is the best way to implement a singleton
- Joshua Bloch, Eective Java 2nd Edition p.18

 

* 참고

- CS 지식의 정석 | 디자인패턴 네트워크 운영체제 데이터베이스 자료구조 -인프런

 

댓글