싱글톤 패턴의 7가지 구현 방법

이강용·2024년 2월 6일
0

CS

목록 보기
16/109

1. 단순한 메서드 호출

싱글톤 패턴 생성 여부를 확인하고 싱글톤이 없으면 사로 만들고 있다면 만들어진 인스턴스를 반환함
그러나 이 코드는 메서드의 원자성이 결여되어 있어 멀티스레드 환경에서는 싱글톤 인스턴스를 2개 이상 만들 수 있음


public class Singleton {
	
	private static Singleton instance;
	private Singleton() {
		
	}
	
	
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		
		return instance;
	}

}

private 키워드를 사용하는 이유는?

  1. 인스턴스 변수의 은닉 : private static Singleton instance 는 Singleton 클래스의 인스턴스를 저장하는 변수 instance가 클래스 외부에서 직접 접근되지 않도록 함. 이는 인스턴스가 오직 getInstance() 메서드를 통해서만 접근될 수 있도록 보장하여, 인스턴스의 생성과 관리를 클래스 내부에서만 제어할 수 있게 함

  2. 생성자의 은닉 : private Singleton(){} Singleton 클래스의 생성자를 private로 선언하여, 클래스 외부에서 new 키워드를 이용한 객체 생성을 방지함. 이는 모든 객체 생성 요청이 getInstance() 메서드를 통해 이루어지도록 강제하여, 이 메서드에서 인스턴스 생성의 유일성을 보장함

멀티 스레드에서 원자성이 결여된다는 의미는 무엇인가?


public class YunhaSync {
	
	private static String yunha = "오르트구름";
	
	public static void main(String[] args) {
		YunhaSync a = new YunhaSync();
		new Thread(() -> {
			for(int i = 0; i < 10; i++) {
				a.say("사건의 지평선");
			}
		}).start();
		
		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);
		}
	}

}

원자성은 연산이 전부 아니면 전혀 수행되지 않는 것을 의미하는데, 위 로직에서는 yunha라는 공유 자원을 두 개의 스레드가 동시에 접근하고 수정하고 있음
say 메서드 내에서 yunha의 값을 설정하고 나서, 스레드가 잠시 일시 중지(Thread.sleep됨. 이 일시 중지 동안 다른 스레드가 실행되어 yunha 변수의 값을 변경할 수 있음. 첫 번째 스레드가 일시 중지에서 깨어나 yunha의 값을 확인할 때, 다른 스레드에 의해 변경된 값이 있을 수 있으므로, 첫 번째 스레드가 예상했던 값과 실제 값이 일치하지 않는 경우가 발생할 수 있음

어떻게 결여된 원자성을 회복할 수 있나?

package programmers;

public class YunhaSync {
	
	private static String yunha = "오르트구름";
	
	public static void main(String[] args) {
		YunhaSync a = new YunhaSync();
		new Thread(() -> {
			for(int i = 0; i < 10; i++) {
				a.say("사건의 지평선");
			}
		}).start();
		
		new Thread(() -> {
			for(int i = 0; i < 10; i++) {
				a.say("오르트구름");
			}
		}).start();
	}
	
	
	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);
		}
	}

}

2. synchronized

synchronized 키워드는 멀티 스레드 프로그래밍 시 동기화를 달성하기 위해 사용함.
synchronized 키워드가 적용된 메서드나 블록에 대해서는 한 시점에 하나의 스레드만 접근할 수 있도록 보장함. 이는 공유 자원에 대한 동시 접근을 막고, 스레드 간의 안전한 상호작용을 가능하게 하여 데이터 무결성을 유지함

synchronized 키워드 사용

  1. 메서드 동기화 : 메서드 전체를 동기화하려면 메서드 선언에 synchronized 키워드를 추가함. 이렇게 하면 해당 메서드가 포함된 객체의 lock을 획득한 스레드만 그 메서드를 실행할 수 있음
public synchronized void synchronizedMethod(){
	//
}
  1. 블록 동기화 : 특정 코드 블록만 동기화하려면 해당 블록을 synchronized 블록으로 만듬. 이 떄, 동기화할 객체의 lock을 명시해야 함
public void method(){
	synchronized(this){
    	// 동기화 코드
    }
}

synchronized와 싱글톤

싱글톤 패턴의 getInstance()메서드에 synchronized를 사용하는 경우
1. 인스턴스 생성 전 동기화 : 인스턴스가 아직 생성되지 않았을 때, syncronized 키워드는 첫 번째로 getInstance()를 호출하는 스레드가 싱글톤 인스턴스를 안전하게 생성할 수 있도록 보장함

package programmers;

public class Singleton {
	
	private static Singleton instance;
	private Singleton() {
		
	}
	
	
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		
		return instance;
	}

}
  1. 성능 고려 : 한번 인스턴스가 생성된 후에도 synchronized 메서드를 계속 사용하면, 모든 getInstance() 호출이 동기화되므로 성능에 부정적인 영향을 미칠 수 있음. 이 문제를 완화하기 위해 double-checked locking 패턴을 사용함 (6번에서 자세히 설명)

3. 정적 멤버

클래스 로딩 시점에 싱글톤 인스턴스를 생성함으로써, 멀티스레딩 환경에서도 안전하게 인스턴스를 관리할 수 있도록함

정적 멤버를 이용한 싱글톤 구현

초기화 시점의 명확성 : 정적 멤버를 이용한 싱글톤 패턴에서는, 클래스가 JVM에 로드되는 시점에 싱글톤 인스턴스가 생성됨. 이는 클래스 로딩이 일어나는 순간 단 한번만 수행되기 때문에, 초기화 시점이 명확하고, 추가적인 동기화가 필요하지 않음
스레드 안전성 : 클래스 로더에 의해 클래스가 초기화될 때, 정적 멤버가 한 번만 생성되므로, 이 방법은 스레드 안전성을 자연스럽게 보장함. 여러 스레드가 동시에 접근하더라도 인스턴스는 단 한번만 생성되기 때문에, 동기화 문제가 발생하지 않음
지연로딩(Lazy Loading) 미지원 : 정적 초기화 블록을 사용하는 싱글톤 패턴은 지연 로딩을 지원하지 않음. 즉, 클래스가 로드되는 시점에 인스턴스가 바로 생성되므로, 실제로 인스턴스가 필요하지 않더라도 메모리를 차지하게 됨. 이는 불필요한 리소스 사용을 초래할 수 있음

구현 예

public class Singleton {
    // 클래스 로딩 시점에 싱글톤 인스턴스 생성
    private static final Singleton instance = new Singleton();
    
    // private 생성자로 외부에서의 인스턴스 생성 방지
    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 클래스가 로딩되어 인스턴스를 생성하게 됨
package programmers;

public 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번 체크하여 인스턴스가 존재하지 않을 때만 잠금을 걸 수 있기 때문에 앞서 생겼던 문제점을 해결할 수 있음

double-checked locking 적용 로직

public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

instance 클래스 변수 앞에 붙은 volatile은 무엇인가?

  • java에서는 Thread 2개가 열리면 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 캐시 메모리에서 각각의 캐시 메모리 기반으로 가져오게 됨

public class Test {
	
	
	boolean flag = true;
	
	public void test() {
		new Thread(()-> {
				int cnt = 0;
				while(flag) {
					cnt++;
				}
				System.out.println("Thread1 finished\n");
			}
		).start();
		new Thread(() ->{
			try {
				Thread.sleep(100);
			
			}catch (InterruptedException ignored) {
			
			}
			System.out.println("flag to false");
			flag = false;
		  }
		).start();
		
		
	}
	
	
	public static void main(String[] args) {
		new Test().test();
	}

}

무한 루프 발생

위 로직이 무한 루프를 도는 이유는 , 첫 번째 스레드가 flag 변수의 값을 갱신하는 것을 두 번째 스레드가 볼 수 없기 때문이다. 이는 첫 번째 스레드가 자신의 로컬 캐시에 flag값을 가지고 있으며, 두 번째 스레드가 flag값을 false로 변경해도 첫 번째 스레드의 로컬 캐시에 반영되지 않기 때문에 발생한다.

boolean flag 앞에 volatile static 선언하기

public class Test {
	
	
	volatile static boolean flag = true;
	
	public void test() {
		new Thread(()-> {
				int cnt = 0;
				while(flag) {
					cnt++;
				}
				System.out.println("Thread1 finished\n");
			}
		).start();
		new Thread(() ->{
			try {
				Thread.sleep(100);
			
			}catch (InterruptedException ignored) {
			
			}
			System.out.println("flag to false");
			flag = false;
		  }
		).start();
		
		
	}
	
	
	public static void main(String[] args) {
		new Test().test();
	}

}

volatile 키워드는 자바의 변수를 다룰 때 사용되며, 주요한 세 가지 특성이 있음
1. 가시성 보장 : volatile로 선언된 변수는 값이 변경될 때 다른 스레드에 즉시 반영되어, 모든 스레드가 항상 최신 값을 볼 수 있음
2. 동기화 비용 절감 : syncronized에 비해 가볍지만, 변수의 읽기와 쓰기를 메인 메모리에서 직접 수행함으로써 동기화의 오버헤드를 줄임
3. 명령어 순서화 : 메모리 배리어(Memory Barrier) 기능을 통해 volatile 변수의 읽기과 쓰기 명령이 순서대로 실행되도록 하여, 명령어 재배치에 의한 부작용을 방지함

volatile static boolean flagvolatile boolean flag의 차이는?

키워드설명공유 수준
volatile static boolean flagflag는 클래스 변수로, 클래스의 모든 인스턴스 간에 공유되며 프로그램 전체에서 하나만 존재한다. volatile은 모든 스레드에 변경사항이 즉시 보이도록 보장한다. 이는 프로그램 전역 상태를 관리할 때 사용된다.클래스 수준(전역)
volatile boolean flagflag는 인스턴스 변수로, Test 클래스의 각 인스턴스마다 별도의 flag를 가진다. volatile은 해당 인스턴스를 사용하는 스레드에 변경사항이 즉시 보이도록 보장한다. 이는 각 객체 인스턴스의 상태를 스레드에 가시적으로 유지할 때 사용된다.인스턴스 수준

7. enum

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

public class Singleton {
	
	public enum SingletonEnum {
		INSTANCE;
		public void doSomething() {
			System.out.println("Doing something...");
		}
	}
	
	
	public static void main(String[] args) {
		SingletonEnum.INSTANCE.doSomething();
	}
	
	
}
profile
HW + SW = 1

0개의 댓글