06. 싱글턴 패턴

AlmondGood·2022년 7월 19일
0

디자인패턴

목록 보기
7/16
post-thumbnail

싱글턴 패턴(Singleton Pattern)

싱글턴 패턴이란, 특정 클래스의 인스턴스가 하나만 만들어지도록 하는 패턴입니다.
객체가 하나밖에 필요없는 상황은 무엇이 있을까요?
그 객체 자체가 고유해야 하거나, 둘 이상이 필요없는 경우 등이 있겠죠.
실제로 사용하는 곳은 캐시, 사용자 설정, 로그 기록 객체 등이 있습니다.

싱글턴 패턴으로 만들어진 인스턴스는 전역 인스턴스로 생성되기 때문에 다른 인스턴스에서 쉽게 참조가 가능합니다.



싱글턴 패턴 구현

게임에서 게임 캐릭터를 생성한다고 생각해봅시다. 캐릭터를 생성할 때 똑같은 것을 2개를 생성하거나 이미 있는 캐릭터를 생성할 수는 없겠죠.

class MyCharacter {
	
    // 클래스에 아무도 접근하지 못 하도록 접근제어자 설정
    // 클래스 자신을 정적으로 선언하여 null일 때는 생성하고, null이 아니면 생성 X
    private static MyCharacter myCharacter = null;
    private MyCharacter() {}
    
    // 객체 생성 메소드를 정적으로 선언
    public static MyCharacter createMe() {
        // 한번 생성하고 나면 null
        if (myCharacter == null) {
            myCharacter = new MyCharacter();
        }
        
        return myCharacter;
    }
}

public static void main(String[] args) {
	MyCharacter myCharacter = Mycharacter.createCharacter(); // 캐릭터 생성!
    MyCharacter myCharacter2 = Mycharacter.createCharacter(); // 캐릭터 생성 실패
}

싱글턴 패턴 완성입니다! 이제 여러분도 싱글턴 패턴을 구현하실 수 있겠죠?
라고 한다면 아주 무책임한 결론이겠죠?

위의 구현에는 심각한 문제점이 있습니다.
그 전에, 먼저 스레드에 대해 알아보고 가겠습니다.



프로세스(Process) / 스레드(Thread)

간단하게 설명하면

프로세스 : 현재 실행 중인 프로그램, 여러분들이 켜놓은 브라우저, 음악 어플리케이션 등도 전부 프로세스입니다.
스레드 : 프로세스에서도 함수 단위로 더 잘게 쪼개어 실행하는 것.


프로그램이 실행되면 스택, 힙, 데이터, 코드 4가지 영역으로 나누어져 메모리에 올라갑니다.
각 프로세스는 독립적인 개체로서, 서로에게 간섭할 수 없습니다.

스택 : 임시적인 데이터들 (함수 호출, 지역 변수 등)
: 동적으로 만들어지는 데이터 (객체 등)
데이터 : 변수 또는 초기화된 데이터
코드 : 프로그램을 이루는 코드

프로그램을 효율적으로 실행하려면 코드를 분할해서 실행시켜야 겠죠.
마치 물건을 만들 때 분업하는 것과 같다고 생각하시면 됩니다.
그래서 나온 개념이 멀티 프로세스입니다.

하지만 프로그램 하나를 실행시키기 위해서 독립적인 여러 개의 프로세스를 실행시킨다면 오히려 비효율적이게 됩니다.

마치 장난감 하나 조립하는 데 공장 한 곳에서 하면 될 것을 머리 조립 공장, 팔 조립 공장, 다리 조립 공장을 만드는 격입니다.

그리고 여기서 더 발전하여 스레드가 등장합니다.

스레드는 프로세스의 스택 영역에서 실행되고, 그 중에서도 스택 안에서 여러 개의 스레드를 실행시키면 멀티 스레드가 됩니다.
스레드들은 힙, 데이터, 코드 영역을 공유하여 사용합니다.

전역 변수처럼 한 가지 변수를 계속 다뤄야 한다면 프로세스처럼 독립적인 것보다 공유를 할 수 있는 게 더 효율적이겠죠?

하지만 바로 여기서 문제가 발생합니다.


싱글턴 패턴과 멀티 스레드

일반적으로 우리가 공부하는 환경에서 멀티 스레드 문제가 발생할 상황은 많지 않습니다.
하지만 현업에서 멀티 스레드는 최적화를 위해 자주 사용되기 때문에 꼭 주의해서 알아두어야 합니다.

코드를 다시 한번 살펴봅시다.

class MyCharacter {

    // 클래스에 아무도 접근하지 못 하도록 접근제어자 설정
    // 클래스 자신을 정적으로 선언하여 null일 때는 생성하고, null이 아니면 생성 X
    private static MyCharacter myCharacter = null;
    private MyCharacter() {}

    // 객체 생성 메소드를 정적으로 선언
    public static MyCharacter createCharacter() {
        // 한번 생성하고 나면 null
        if (myCharacter == null) {
            myCharacter = new MyCharacter();
        }
  
        return myCharacter;
    }
}

public static void main(String[] args) {
	MyCharacter myCharacter = Mycharacter.createCharacter(); // 캐릭터 생성!
}

두 개의 스레드가 같이 일하고 있다고 생각해봅시다. 그렇다면 어떻게 문제가 일어날 수 있을까요?

// 시간에 따른 작동 순서

// 스레드 A
createCharacter() 메소드 진입

					// 스레드 B
					createCharacter() 메소드 진입

// 스레드 A
if (myCharacter == null) (true)

					// 스레드 B
					if (myCharacter == null) (true)
                  
// 스레드 A
myCharacter = new MyCharacter(); (객체 A 생성)

					// 스레드 B
					myCharacter = new MyCharacter(); (객체 B 생성)

두 개의 스레드가 거의 동시에 진입하게 되면 if(myCharacter == null) 을 그냥 통과해버립니다.
그 결과, 하나 뿐이었어야 할 객체가 2개가 생성이 되어버렸죠.



문제 해결

그렇다면 어떻게 이를 해결할 수 있을까요?


1. synchronized 키워드 사용

class MyCharacter {

    private static MyCharacter myCharacter = null;
    private MyCharacter() {}

    public static synchronized MyCharacter createCharacter() {
        if (myCharacter == null) {
            myCharacter = new MyCharacter();
        }

        return myCharacter;
    }
}
>
public static void main(String[] args) {
	MyCharacter myCharacter = Mycharacter.createCharacter(); // 캐릭터 생성!
}

synchronized를 사용하면 스레드가 동기화되어 캐릭터 생성 메소드가 실행, 끝날 때까지 다른 스레드는 기다려야 합니다.
반대로 말하면, 나중에 이 메소드를 실행시켜야 하는 때가 온다면, 불필요하게 동기화되어 실행 시간만 100배 늦추는 셈이죠. 이런걸 thread-unsafe 하다고 합니다.


2. 처음부터 인스턴스화

class MyCharacter {

    private static MyCharacter myCharacter = new MyCharacter();
    private MyCharacter() {}

    public static MyCharacter createCharacter() {
    	return myCharacter;
    }
}
>
public static void main(String[] args) {
	MyCharacter myCharacter = Mycharacter.createCharacter(); // 캐릭터 생성!
}

객체가 전역 변수로 선언되어 있기 때문에 시작과 동시에 생성되어 하나만 존재하게 되고, 생성 메소드는 그 객체만 반환해주면 됩니다.
하지만 전역 변수로 선언했다는 말은 캐릭터를 생성하기 전까지 의미없는 메모리만 차지한다는 것을 의미합니다.


3. Double-Checked Locking(DCL)

class MyCharacter {

    private volatile static MyCharacter myCharacter = new MyCharacter();
    private MyCharacter() {}

    public static MyCharacter createCharacter() {

    	if (myCharacter == null) {
        	synchronized (MyCharacter.class){
            	if (myCharacter == null) {
                	myCharacter = new MyCharacter();
                }

    	return myCharacter;
    }
}
>
public static void main(String[] args) {
	MyCharacter myCharacter = Mycharacter.createCharacter(); // 캐릭터 생성!
}

volatile 키워드를 사용하면 첫 생성 시에만 동기화하고, 나중에는 동기화하지 않아도 됩니다.
하지만 JDK1.4 이상 부터만 사용 가능하고, 이도 만에 하나 오동작할 확률이 있습니다.


4. enum 클래스

public enum MyCharacter {
	MY_CHARACTER;
}

public static void main(String[] args) {
	MyCharacter myCharacter = Mycharacter.MY_CHARACTER; // 캐릭터 생성!
}

아주 간단하네요. enum 클래스가 뭔지 짧게 설명하면, 상수들의 집합을 클래스로 선언한 것입니다.
enum 클래스는 특별하기 때문에 생성자가 private으로 선언되어 있어, 위처럼 enum 상수 자체가 생성자처럼 사용되어 인스턴스화 되고, 상수이기 때문에 하나밖에 존재할 수 없습니다.


5. LazyHolder

class MyCharacter {
    private MyCharacter() {}

	// LazyHolder의 MY_CHARACTER를 참조함과 동시에 초기화
    public static MyCharacter createCharacter() {
        return LazyHolder.MY_CHARACTER;
    }

	// LazyHolder 기법
    private static class LazyHolder {
        private static final MyCharacter MY_CHARACTER = new MyCharacter();
    }
}

public static void main(String[] args) {
	MyCharacter myCharacter = Mycharacter.createCharacter(); // 캐릭터 생성!
}

LazyHolder 는 최근에 많이 사용되는 기법으로, JVM을 활용한 방법입니다.
필요할 때 클래스를 초기화하기 때문에 Lazy Initialization(게으른 초기화) 라고도 합니다.
Lazy Initialization 은 1, 3, 4번 방법도 해당합니다.

  • MyCharacter 클래스에는 LazyHolder 클래스의 변수가 없기 때문에 MyCharacter 클래스 로딩 시 LazyHolder 클래스를 초기화하지 않음
  • 클래스를 로딩하고 초기화하는 시점은 thread-safe를 보장
  • LazyHolder 안에 선언된 인스턴스가 static이기 때문에 클래스 로딩 시점에 한번만 호출



의존성 주입

의존성 주입이란, 한 객체의 의존 관계외부 객체에서 결정해주는 것을 의미합니다.
만약 이 말이 헷갈리신다면, 그동안 진행했던 패턴들에서 예제를 찾아볼 수 있습니다.

class Person{
	boolean leg = true;
    boolean foot = true;

    WalkStrategy walkStrategy;
    RunStrategy runStrategy;

    // 동작을 자유롭게 바꿀 수 있음
    void setWalk(WalkStrategy walkStrategy) {
    	this.walkStrategy = walkStrategy;
    }
    void setWalk(RunStrategy runStrategy) {
    	this.runStrategy = runStrategy;
    }


    void walk() {
        walkStrategy.walk();
    };
    void run(){
        runStrategy.run();
    };
}

setter 메소드를 사용하거나,

abstract class SiriDecorator implements Siri {

    // 명령을 받으려면 시리가 있어야겠죠. 
    // 시리를 선언해 줍니다.
    private Siri siri;

    // 시리가 어떠한 명령을 받습니다. 
    SiriDecorator(Siri siri) {  
    	this.siri = siri;
    }

    // 그 명령을 실행합니다.  
    @Override 
    public void command() {
    	siri.command();
    }
}

생성자를 이용하면 됩니다.

스프링의 bean 컨테이너가 싱글톤으로 이루어져 있고, bean을 이용해 의존성 주입을 하기도 합니다.



참고 자료

https://blog.javarouka.me/2018/11/20/no-instance/

https://www.nextree.co.kr/p11686/

https://medium.com/@joongwon/multi-thread-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%AC%EB%B0%94%EB%A5%B8-singleton-578d9511fd42

https://1-7171771.tistory.com/121

<헤드퍼스트 : 디자인 패턴>

profile
Zero-Base to Solid-Base

0개의 댓글