[Chapter 16] 싱글톤과 모노스테이트 패턴

Seungjae·2022년 2월 18일
0

이 챕터에서는 단일성을 강제하는 두 패턴을 다룬다. 때때로 이런 단일성이 적용된 객체는 프로그램의 루트가 되기도하고, 다른 객체를 만들기 위해 사용하는 factory가 되기도 하며, 다른 객체를 추적하여 그 객체의 pace에 맞게 동작시키는 관리자가 되기도 한다.

싱글톤 패턴


책 내의 2가지 테스트 케이스를 보며 싱글톤이 어떻게 동작하는지 알 수 있다.

  1. public static 메서드(메서드 명이 Instance라고 가정)를 통해 Singleton 인스턴스에 접근한다.
  2. Instance 메서드는 항상 동일한 참조값을 가진 인스턴스를 반환한다.
  3. Singleton 클래스는 public 생성자가 없기에 Instance메서드를 사용하지 않고서는 인스턴스를 생성할 방법이 없다.

구현

public class Singleton {
	private static Singleton theInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton Instance() {
    	return theInstance;
    }

}

이점

  1. 플랫폼 호환 (RMI를 이용)
  2. 어떤 클래스에도 적용 가능
  3. 파생을 통해 생성 가능 -> 주어진 클래스에서 싱글톤인 서브 클래스를 만들 수 있다.
  4. 게으른 처리(꼭 필요하기 전까지 처리를 미룸)

비용

  1. 소멸이 정의되어 있지 않음 : 싱글톤을 없애거나 사용을 중지하는 좋은 방법이 없다. null처리를 통해 필드에 할당된 인스턴스를 없애려고해도 다른 모듈에서 그 기존 인스턴스에 대한 참조값을 유지하고 있을 수 있고, 이어서 인스턴스가 2개가 존재하게 될 수도 있다.
  2. 상속되지 않음 : 싱글톤에서 파생된 클래스는 싱글톤이 아니다.
  3. 효율성 : 싱글톤의 인스턴스를 가져오는 메서드에서 현재 싱글톤 인스턴스가 존재하는지 null체크를 할 경우, 대부분의 경우 필요없는 if문이 계속하여 호출되게 된다.
  4. 비투명성 : 싱글톤의 사용자가 public static 메서드를 통해 인스턴스에 접근하므로 자신이 싱글톤을 사용한다는 사실을 안다.

동작에 있어서의 싱글톤 🔑Key point

  1. 데이터베이스를 사용하는 웹 서비스 Example -> 어떤 사용자를 읽고 쓰기 위해 필요한 모든 모듈에서 데이터베이스에 직접 접근할 수 있는 경우
  2. 이 경우 코드 전체에 데이터베이스에 접근하는 서드파티 API사용이 확산되게 됨. 접근이나 구조에 대한 규정을 강제할 수 없게 된다.
  3. 좋은 해결책은 퍼사드 패턴을 사용하는 것 -> UserDatabase 클래스
  4. UserDatabase는 User객체와 데이터베이스의 테이블과 행 사이에 변환 작업을 한다. UserDatabase 내에서 구조와 접근의 규정을 강제할 수 있다.
  5. 여러 모듈에서 이것을 동시에 읽고 쓰지 않도록 보장하기 위해 싱글톤을 사용할 수 있다. -> UserDatabaseSource
  6. UserDatabaseSource의 단일 인스턴스로 모든 DB접근이 이루어진다는 것을 보장한다면 검사, lock 등을 넣어 접근과 구조에 대한 규정을 강제하기 쉬워진다.
public class UserDatabaseSource implements UserDatabase {
	private static UserDatabase theInstance = new UserdatabaseSource();
    
    public static UserDatabase instance() {
    	return theInstance;
    }
    
    private UserDatabaseSource() {}
    
    public User readUser(String userName) {
    	// 구현 부분
    }
    
    public void writeUser(User user) {
    	// 구현 부분
    }
}

모노스테이트 패턴


모노스테이트 패턴은 싱글톤 패턴과는 완전히 다른 매커니즘이다. 모노스테이트 패턴도 책 내의 2가지 테스트 케이스를 보며 어떻게 동작하는지 알 수 있다.

  1. 자신의 필드가 설정되거나 검색될 수 있는 객체이다.
  2. 한 인스턴스에서 필드 값을 A로 설정하면, 다른 인스턴스에서도 필드 값이 A가 된다.
  3. 단일 인스턴스라는 제약을 강제하지 않은 Singleton의 행위를 나타낸다.
  4. 모든 변수를 정적으로 만듦으로써 쉽게 구현할 수 있다.
  5. 단, 어떤 메서드도 정적이 아니라는 점에 주목하자.
  6. 인스턴스가 여러개여도 단일 객체처럼 동작
  7. 데이터를 잃지 않고도 현재 있는 모든 인스턴스를 없애거나 사용 중지 가능 (method area에 필드가 static으로 자리하고 있으니까!)

구현

public class Monostate {
	private static int x = 0;
    
    public Monostate() {}
    
    public void setX(int x) {
    	this.x = x;
    }
    
    public int getX() {
    	return x;
    }
}

두 패턴의 차이

  1. 행위 vs 구조
  2. 싱글톤 패턴은 단일성 구조를 강제 -> 인스턴스가 둘 이상 생성되는 것을 막음!(단일 인스턴스 제약)
  3. 모노스테이트 패턴은 구조적인 제약을 부여하지 않고도 단일성이 있는 행위를 강제!
  4. 모노스테이트 테스트 케이스가 싱글톤 클래스에 대해서도 유효! 하지만 그 반대인 싱글톤 테스트 케이스는 모노스테이트 클래스에 대해 유효하지 않다!

이점

  1. 투명성 : 사용자는 이 객체가 모노스테이트임을 알 필요가 없다.(일반 객체와 동일하게 사용)
  2. 파생 가능성 : 모노스테이트의 파생 클래스는 모노스테이트. 이들은 모두 같은 정적 변수를 공유
  3. 다형성 : 모노스테이트의 메서드는 정적이 아님 -> 파생 클래스에서 오버라이드 가능! -> 같은 정적 변수에 대해 다른 행위 제공 가능!
  4. 잘 정의된 생성과 소멸 : 정적인 변수를 같기에 생성과 소멸 시기가 잘 정의되어 있다. (시작에 클래스가 로드되며 생성, 프로그램이 종료되며 소멸 이야기인듯?)

Java의 정적 메서드는 왜 override가 안될까?

Java에서 정적 메서드는 재정의가 불가하다. 이유는 Static 메서드는 클래스가 컴파일 되는 시점에 결정되지만, override의 경우는 런타임 시점에 사용될 메서드가 결정되기 때문이다. static의 경우 클래스 단위로 만들어지게 되고, override의 경우 객체 단위로 만들어지게 된다.

비용

  1. 변환 불가 : 보통 클래스는 파생을 통해 모노스테이트로 변환될 수 없다.
  2. 효율성 : 실제 객체이기에 많은 생성과 소멸을 겪는다. 종종 비용이 꽤 들 수 있다.
  3. 실재함 : 모노스테이트 변수는 정적 변수이기에 사용되지 않더라도 공간을 차지한다.
  4. 플랫폼 한정 : 한 모노스테이트는 여러 JVM 인스턴스나 여러 플랫폼에서 동작하게 할 수 없다.

동작에 있어서의 모노스테이트 🔑Key point

  1. 지하철 개찰구를 위한 간단한 유한 상태 기계를 구현하는 Example
  2. 모노스테이트인 Turnstile을 만들고 2개의 이벤트 함수(coin, pass)는 유한 상태 기계의 상태를 표현하는 Turnstile의 두 파생 클래스에게 위임(Locked, Unlocked)
  3. 모노스테이트는 파생 클래스가 다형적이 된다는 것과 파생 클래스들도 모노스테이트가 된다는 것을 주목
  4. 단 모노스테이트를 일반 클래스로 바꾸는 것은 매우 어렵다. 아래 예시도 보면 Turnstile은 모노스테이트적 본질에 강하게 의존
  5. 예를 들어, 이 코드로 2개 이상의 개찰구를 제어하려 한다면, 많은 리팩토링이 필요할 것
  6. 아래 예시에서 Locked와 Unlocked는 별도의 객체로 보기보다는 Turnstile 추상화의 일부라고 볼 수 있다.
public class Turnstile {
	private static boolean isLocked = true;
    private static boolean isAlarming = false;
    private static int coins = 0;
    private static int refunds = 0;
    protected final static Turnstile LOCKED = new Locked();
    protected final static Turnstile UNLOCKED = new Unlocked();
    protected static Turnstile state = LOCKED;
    
    // 기타 구현...
}
class Locked extends Turnstile {
	public void coin() {
    	state = UNLOCKED;
        lock(false);
        alarm(false);
        deposit();
    }
    
    public void pass() {
    	alaram(true);
    }
}
class Unlocked extends Turnstile {
	public void coin() {
    	refund();
    }
    
    public void pass() {
    	lock(true);
        state = LOCKED;
    }
}

결론


싱글톤인스턴스 생성을 제어하고 제한하기 위해 private 생성자, 1개 정적 변수, 1개 정적 함수를 사용한다. 모노스테이트는 그저 객체의 모든 변수를 static으로 만든다.

싱글톤은 파생을 통해 제어하고 싶은 이미 존재하는 클래스가 있는 경우, 그리고 접근하기 위해 모두가 instance()를 호출해도 상관이 없을 때 좋은 선택이다. 모노스테이트는 단일 객체의 파생 객체가 다형적이 되게 하고 싶을 때 좋은 선택이다.

책 외적으로...


그렇다. 싱글톤은 안티패턴이다.

싱글톤은 안티패턴이다. 그 이유는 다음과 같다.

  1. private 생성자 -> 상속이 불가하다.
  2. Mocking 이용한 테스트가 힘들다. -> Mock은 서브클래싱 기반으로 작동하기 때문이다.
  3. 분산된 서버 환경에서는 싱글톤 보장이 안될수도 있다.
  4. 전역 상태를 만들 수 있다. -> 즉 전역 상태 관리가 추가적으로 필요하게 된다. (멀티쓰레드 환경에서의 문제)
  5. 전체에서 하나의 객체만을 공통으로 사용하고 있기 때문에 각 객체간의 결합도가 높아지고 변경에 유연하게 대처하기 힘들 수 있다. 또한 변경으로 인한 사이드 이팩트가 클 수 있다.

싱글톤이 안티패턴인 이유와 문제점에 대해 알고, trade-off를 고려하며 패턴을 사용해야할 것 같다.

profile
코드 품질의 중요성을 아는 개발자 👋🏻

0개의 댓글

관련 채용 정보