싱글턴 패턴

이주오·2022년 6월 26일
0

디자인 패턴

목록 보기
9/12
post-thumbnail

싱글턴 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.


고전적인 싱글턴 패턴 구현법

package singleton.before;

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

}

초콜릿 공장

초콜릿 공장에서는 초콜릿을 끓이는 장치인 초콜릿 보일러를 컴퓨터로 제어한다.

이 보일러에서는 초콜릿과 우유를 받아서 끓이고 초코바를 만드는 단계로 넘겨준다. 여기에 초콜릿 보일러를 제어하기 위한 클래스가 나와 있다.

public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;

    public ChocolateBoiler() {
        this.empty = true;
        this.boiled = false;
    }

    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
        }
    }

    public void drain() {
        if (!isEmpty() && !isBoiled()) {
            empty = true;
        }
    }

    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            boiled = true;
        }
    }

    public boolean isEmpty() {
        return this.empty;
    }

    public boolean isBoiled() {
        return this.boiled;
    }

}
  • 코드를 보면 실수를 하지 않도록 주의를 기울여져 있다.
  • 하지만 두 개의 ChocolateBoiler 인스턴스가 따로 돌아가게 되면 상당히 안 좋은 상황이 일어날 수 있다는 것을 알 수 있다.
  • 만약 애플리케이션에서 ChocolateBoiler 인스턴스가 두 개 이상 만들어지게 되면 어떤 문제가 생길까??
    • 자원을 불필요하게 잡아먹고, 애플리케이션의 동작이 이상하게 돌아가는 결과에 일관성이 없어지는 심각한 문제가 발생할 것이다.
    • ChocolateBoiler 클래스를 싱글턴으로 업그레드해보자
public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;

    private static ChocolateBoiler uniqueInstance;

    private ChocolateBoiler() {
        this.empty = true;
        this.boiled = false;
    }

    public static ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }

    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
        }
    }

    public void drain() {
        if (!isEmpty() && !isBoiled()) {
            empty = true;
        }
    }

    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            boiled = true;
        }
    }

    public boolean isEmpty() {
        return this.empty;
    }

    public boolean isBoiled() {
        return this.boiled;
    }

}

싱글턴 패턴의 정의

싱글턴의 고전적인 구현법을 배웠다. 그렇다면 싱글턴 패턴의 정의는 무엇이고, 실제로 어떤 식으로 싱글턴 패턴을 적용해야할까??

💡 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.
  • 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만들면 된다.
    • 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 한다.
    • 따라서 인스턴스가 필요하면 반드시 클래스 자신을 거치도록 해야 될 것이다.
  • 어디서든 그 인스턴스를 접근할 수 있도록 만들어야 한다.
    • 다른 객체에서 이 인스턴스가 필요하면 언제든지 클래스한테 요청을 할 수 있게 만들고, 요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 만들어야 한다.
    • 앞에서 보았듯이, 싱글턴이 Lazy하게 생성되도록 구현할 수도 있다. 싱글턴 클래스의 객체가 자원을 많이 잡아먹는 경우에는 유용하다.

클래스 다이어그램

https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823


골칫덩어리 스레드

고전적인 싱글턴을 이용해서 코드를 고쳤음에도 ChocolateBoiler에 있는 fill() 메소드에서 아직 초콜릿이 끓고 있는데 재료를 집어넣고 말았다. 무슨 일이 일어난 것일까??

  • 조금 전에 다중 스레드를 사용하도록 ChocolateBoiler 컨트롤러를 최적화시킨 것이 문제일까??
  • 스레드가 추가된 것 때문에 이런 문제가 생긴 것일까??

JVM의 입장

두 개의 스레드에서 여기에 있는 코드를 실행시킨다고 가정해보고 두 스레드가 다른 보일러 객체를 사용하게 될 가능성이 있는지 따져보자.

ChocolateBoiler boiler = ChocolateBoiler.getInstacne();
boiler.fill();
boiler.boil();
boiler.drain();
public static ChocolateBoiler getInstance() {
      if (uniqueInstance == null) {
          uniqueInstance = new ChocolateBoiler();
      }
      return uniqueInstance;
}
  • 바로 두 스레드가 동시에 getInstance() 메소드를 수행하게 되면 uniqueInstance null 상태라 각 스레드마다 ChocolateBoiler 인스턴스를 생성하여 리턴하여 결국 서로 다른 두 인스턴스가 만들어진다.

멀티스레딩 문제 해결 방법

문제를 해결하는 방법은 간단한데 바로 getInstance()를 동기화시키기만 하면 된다.

public static synchronized ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
}
  • synchronized 키워드를 추가하면 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려야 한다.
  • 즉, 두 스레드가 이 메소드를 동시에 실행시키는 일은 일어나지 않는다.
  • 하지만 이렇게 하면 동기화로 인한 속도 문제가 생긴다.
    • 사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때 뿐이다.
    • 즉, 일단 uniqueInstance 변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메소드를 동기화된 상태로 유지시킬 필요가 없다.
    • 불필요한 오버헤드만 증가시킬뿐인 것

더 효율적인 방법은 없을까?

대부분의 자바 애플리케이션에서 싱글턴이 다중 스레드 환경에서 돌아갈 수도 있도록 만들어야 한다. 하지만 getInstance() 메소드를 동기화시키려면 대가를 치뤄야 한다. 다른 방법은 없을까??

1. getInstance()의 속도가 중요하지 않다면 그냥 둔다.

만약 getInstance() 메소드가 애플리케이션에 큰 부담을 주지 않는다면 그냥 놔둬도 된다. getInstance()를 동기화시키는게 굉장히 쉽고, 효율 면에서도 나쁘지 않을 수있다.

하지만 메소드를 동기화하면 성능이 100배 정도 저하된다는 것을 기억하자. 만약 getInstance()가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해야 한다.

2. 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버린다.

애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면, 또는 인스턴스를 실행중에 수시로 만들고 관리하기가 성가시다면 다음과 같은 식으로 처음부터 Singleton 인스턴스를 만들어버리는 것도 괜찮은 방법이다.

public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }

}
  • 이런 접근법을 사용하면 클래스가 로딩될 때, JVM에서 Singleton의 유일한 인스턴스를 생성해 주고, JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없다.

3. DCL(Double Checking Locking)을 써서 getInstance()에서 동기화되는 부분일 줄인다.

DCL을 사용하면 , 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있다. 이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않도록 동작하여, 바로 원하던 동작이 수행된다.

public class Singleton {

		// 자바 5 이전 버전은 동기화 x
    private volatile static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) { // 인스턴스가 있는지 확인하고, 없으면 동기화된 블럭으러 진입
            synchronized (Singleton.class) {
                if(uniqueInstance == null) { // 블록으로 들어온 후레도 다시 한번 널체크한 후, 인스턴스를 생성한다.
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }

}

싱글턴 관련 Q&A

Q) 모든 메서드와 변수가 static으로 선언된 클래스를 만들어도 결과적으로는 같지 않을까?

A) 맞다. 하지만 필요한 내용이 클래스에 다 들어있고, 복잡한 초기화가 필요없는 경우에만 해당 방법을 사용할 수 있다. 그리고 자바에서 정적 초기화를 처리하는 방법 때문에 복잡해질 수 있다. 특히 여러 클래스가 얽혀 있는 경우에는 지저분하고, 초기화 순서와 관련된 버그는 찾기 어렵기 때문에 해당 방식으로 싱글턴 비슷한 걸 만들어야 한다면 좋지 않을 수 있다.

Q) 클래스 로더와 관련된 문제는 없을까?

A) 클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문에 클래스 로더가 두 개 이상이라면 같은 클래스를 여러 번 로딩할 수도 있다. 만약 싱글턴을 그런 식으로 로딩하면 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있다. 따라서 클래스 로더를 여러 개 사용하면서 싱글턴을 사용한다면 조심해야 하고, 클래스 로더를 직접 지정해서 문제를 회피할 수도 있다.

Q) 전역 변수가 싱글턴보다 나쁜 이유는 무엇일까??

A) 자바의 전역 변수는 기본적으로 객체에 대한 정적 레퍼런스다. 전역 변수를 이런 식으로 사용한다면 게으른 인스턴스를 사용할 수 없는 단점과 싱글턴 패턴을 쓰는 두 가지 이유 중, 클래스의 인스턴스가 하나만 있을 수 있도록 할 수 없다. 전역 변수를 사용한다면 간단한 객체에 대한 전역 레퍼런스를 자꾸 만들게 도면서 네임스페이스를 지저분한게 만드는 경향이 생긴다.


핵심 정리

  • 어떤 클래스를 싱글턴 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가 최대 한 개 까지만 있도록 할 수 있다.
  • 싱글턴 패턴을 이용하면 유일한 인스턴스를 어디서든지 접근할 수 있도록 할 수 있다.
  • 자바에서 싱글턴 패턴을 구현할 때는 private 생성자와 정적 메소드, 정적 변수를 사용한다.
  • 다중 스레드를 사용하는 애플리케이션에서는 속도와 자원 문제를 파악해보고 적절한 구현법을 사용해야 한다.
    • 사실상 멀티스레딩을 기본으로 가정해야한다.
  • DCL을 사용하는 방법은 자바 2 버전5보다 전에 나온 버전에서는 쓸 수 없다.
  • 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고, 여러 개의 인스턴스가 생길 수 있다.
  • 1.2 버전보다 전에 나온 JVM을 사용하는 경우에는 가바지 컬렉터 관련 버그 때문에 싱글턴 레지스트리를 사용해야 할 수도 있다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
    • 싱글턴 패턴 : 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글