Pv_log

6. 싱글턴 패턴 본문

Develop Study/Design Pattern

6. 싱글턴 패턴

Priv 2022. 7. 3. 23:09


 

 

1. 프린터 관리자 만들기

회사의 영업 부서에서 10명의 직원들이 프린터 1개를 공유하여 사용한다고 가정하자.

이러한 상황에서 프린터를 코드로 구현하려면 어떻게 해야 할까?

먼저 아래와 같이 리소스를 받아서 출력하는 Printer 클래스를 작성했다.

public class Printer {
    public Printer() {
    }

    public void print(Resource r) {
        
    }
}

이제 여기서 Printer 클래스로 프린터를 사용하려면, 클라이언트 쪽에서 new Printer()를 1번만 호출되도록 주의를 기울여야 한다.

이 문제를 해결하기 위해서는 생성자를 외부에서 호출할 수 없도록 제한하는 것이다.

public class Printer {
    private Printer() {
    }

    public void print(Resource r) {

    }
}

이제 new Printer()를 사용할 수 없게 되었다.

그러나 프린터 인스턴스는 일단 최소 1개는 꼭 필요하므로, 인스턴스를 만들어서 외부에 전달해줄 메서드가 따로 필요해졌다.

public class Printer {
    private static Printer printer = null;


    private Printer() {
    }

    public static Printer getPrinter() {
        if (printer == null) {
            printer = new Printer();
        }

        return printer;
    }

    public void print(Resource r) {
        
    }

}

getPrinter() 메서드를 통해 프린터 인스턴스가 생성되어 있는 지를 검사한 뒤, 검사 결과가 null일 때만 new Printer()를 호출하도록 만들었다.

코드를 좀 더 살펴보면 getPrinter() 메서드와 printer 변수가 static(정적)으로 선언되었다.

static으로 선언된 정적 메서드와 정적 변수는 구체적인 인스턴스에 속하지 않고 클래스 자체에 속한다는 의미를 지닌다.

즉, 클래스 인스턴스 없이도 메서드 실행과 변수 참조가 가능하다는 것이다.

지금 목적은 단 1개의 객체를 생성해 어디서든지 참조할 수 있도록 만드는 것이기 때문에 getPrinter 메서드가 정적 메서드이어야 한다.

이제 사용자가 프린터를 사용하는 코드를 작성해보자.

package P6;

public class Printer {
    private static Printer printer = null;


    private Printer() {
    }

    public static Printer getPrinter() {
        if (printer == null) {
            printer = new Printer();
        }

        return printer;
    }

    public void print(String str) {
        System.out.println(str);
    }

}
package P6;

public class User {
    private String name;

    
    public User(String name) {
        this.name = name;
    }

    public void Print() {
        Printer printer = Printer.getPrinter();
        printer.print(this.name + " print using " + printer.toString() + ".");
    }
}

 


 

2. 문제점

다중 스레드에서 Printer 클래스를 이용할 때 인스턴스가 1개 이상 생성되는 경우가 발생한다면 어떻게 될까?

  • Printer 인스턴스가 아직 생성되지 않았을 때 스레드 1이 getPrinter 메서드의 if 문을 실행해 이미 인스턴스가 생성되었는지 확인한다. 현재 printer 변수는 null이다.
  • 만약 스레드 1이 생성자를 호출해 인스턴스를 만들기 전 스레드 2가 if 문을 실행해 printer 변수가 null인지 확인한다. 현재 null이기 때문에 인스턴스를 생성하는 코드, 즉, 생성자를 호출하는 코드를 실행하게 된다.
  • 스레드 1도 스레드 2와 마찬가지로 인스턴스를 생성하는 코드를 실행하게 되면 결과적으로 Printer 클래스의 인스턴스가 2개 생성된다.
  • 위와 같은 시나리오의 상황을 경합 조건이라고 부른다. (메모리와 같은 동일한 자원을 2개 이상의 스레드가 이용하려고 경합하는 현상)

다만 현재 작성된 Printer 클래스의 경우에는 인스턴스가 2개 이상 생긴다고 해서 특별한 문제가 발생하지는 않는다.

하지만 아래와 같이 Printer 클래스가 상태를 유지해야 하는 경우에는 문제가 발생한다.

package P6;

public class Printer {
    private static Printer printer = null;
    private int counter = 0;


    private Printer() {
    }

    public static Printer getPrinter() {
        if (printer == null) {
            try {
                Thread.sleep(1);
            }
            catch (InterruptedException e) {
                printer = new Printer();
            }
        }

        return printer;
    }

    public void print(String str) {
        this.counter++;
        System.out.println(str);
    }

}

위와 같은 코드를 실행하면 counter 변수가 각각의 Printer 클래스 인스턴스 내에서 생성되어 값이 유지되기 때문에 원하는 결과가 나오지 않는 문제가 발생한다.

 


 

3. 해결책

이와 같이 스레드 애플리케이션에서 발생할 수 있는 문제를 해결하기 위한 방법으로는 2가지가 있다.

  • 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법
  • 인스턴스를 만드는 메서드에 동기화하는 방법

 

3.1) 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법

아래 코드는 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법으로 문제를 해결한 코드이다.

package P6;

public class Printer {
    private static Printer printer = new Printer();
    private int counter = 0;


    private Printer() {
    }

    public static Printer getPrinter() {
        return printer;
    }

    public void print(String str) {
        this.counter++;
        System.out.println(str);
    }

}

정적 변수는 객체가 생성되기 전, 클래스가 메모리에 로딩될 때 만들어져 초기화가 1번만 실행된다.

또한 정적 변수는 프로그램이 종료될 때까지 계속 메모리에 상주하며 클래스에서 생성된 모든 객체에서 참조할 수 있다.

정적 변수의 이러한 특징 덕분에 프린터 객체를 생성하는 코드가 실행되면 정적 변수 printer에 Printer 클래스가 바인딩되며, getPrinter라는 정적 메서드로 참조되는 인스턴스를 얻어올 수 있다.

이 방법은 다중 스레드 환경에서 문제를 일으켰던 if 문 자체를 제거한 방법이다.

 

3.2) 인스턴스를 만드는 메서드에 동기화하는 방법

다음은 Printer 클래스의 객체를 얻는 getPrinter 메서드를 동기화하는 코드이다.

package P6;

public class Printer {
    private static Printer printer = null;
    private int counter = 0;


    private Printer() {
    }

    public synchronized static Printer getPrinter() {
        if (printer == null) {
            printer = new Printer();
        }

        return printer;
    }

    public void print(String str) {
        synchronized(this) {
            this.counter++;
            System.out.println(str);
        }
    }

}

Printer 객체는 1개만 생성되었지만, 여러 스레드가 1개뿐인 counter 변수에 동시 접근하여 값이 틀어지는 문제를 해결하기 위해 print 메서드의 counter 변수를 변경하는 부분도 동기화를 적용해주었다.

 


 

4. 싱글턴 패턴

싱글턴 패턴은 인스턴스가 단 1개만 생성되는 것을 보장하고, 어디에서든 1개뿐인 인스턴스에 접근할 수 있도록 하는 디자인 패턴이다.

싱글턴이라는 단어는 '단 하나의 원소만 가진 집합'이라는 수학 이론에서 유래되었다.

싱글턴 패턴은 매우 단순하기 때문에 Singleton 요소 1개뿐이다.

Singleton이란, 1개의 인스턴스만 생성하는 책임이 있으며, getInstance 메서드로 모든 클라이언트에 동일한 인스턴스를 반환하는 것을 말한다.

그림 6-4는 싱글턴 패턴을 Printer 클래스에 적용한 경우를 나타낸 것이다.

 


 

5. 싱글턴 패턴과 정적 클래스

싱글턴 패턴을 굳이 사용하지 않고 정적 메서드로만 이루어진 정적 클래스를 사용해도 동일한 효과를 누릴 수 있다.

아래 코드는 Printer 클래스를 싱글턴 패턴 대신 정적 클래스로 구현한 코드이다.

package P6;

public class Printer {
    private static int counter = 0;


    public synchronized static void print(String str) {
        counter++;
        System.out.println(str);
    }

}
package P6;

public class UserThread extends Thread {
    public UserThread(String name) {
        super(name);
    }

    public void run() {
        Printer.print(Thread.currentThread().getName() + " print using " + ".");
    }
}

정적 클래스를 사용하는 방법과 싱글턴 패턴 사용 방법의 차이점은 객체를 전혀 생성하지 않고 메서드를 사용한다는 점이다.

또한 정적 메서드를 사용하기 때문에 일반적으로 실행할 때 바인딩되는(컴파일 타임에 바인딩되는) 인스턴스 메서드를 사용하는 것보다 성능 면에서 우수하다.

하지만 정적 클래스를 사용할 수 없는 경우도 존재한다.

가장 대표적인 경우가 인터페이스 구현이 필요한 경우이다.

정적 메서드에서는 인터페이스 구현이 불가능하기 때문에 문제가 발생한다.

인스턴스를 사용하는 주된 이유는 대체 구현이 필요한 경우이다.

이는 특히 모의 객체를 사용해 단위 테스트를 수행할 때 매우 중요하다.

 


 


수고하셨습니다!


'Develop Study > Design Pattern' 카테고리의 다른 글

8. 커맨드 패턴  (0) 2022.07.03
7. 스테이트 패턴  (0) 2022.07.03
5. 스트래티지 패턴  (0) 2022.07.03
4. 디자인 패턴  (0) 2022.07.03
3. SOLID 원칙  (0) 2022.07.03
0 Comments