싱글톤 패턴 : 테스트의 어려움과 해결책

이수찬·2023년 7월 22일
0

Effective Java

목록 보기
1/2

싱글턴 패턴이란?

  • 인스턴스가 하나임을 보장하는 디자인 패턴
  • 처음 1개의 인스턴스만 생성한 후 이후 같은 인스턴스를 사용한다.
  • 주로 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트를 싱글턴으로 만든다.

장점

  • 1개의 인스턴스만 사용하기에 메모리 관점에서 효율적이다.
  • 이미 생성된 인스턴스를 활용하니 속도 측면에서도 효율적이다.
  • 전역적으로 사용되는 인스턴스이기에 데이터 공유가 쉽다.

단점

  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트가 테스트하기 어려움
  • 클래스에 싱글턴 구현을 위해 많은 코드가 들어간다.
  • 의존 관계상 클라이언트가 구체 클래스에 의존하게 된다.
    (DIP원칙을 위배함)
  • 멀티쓰레드 환경에서 동시성 이슈가 발생할 수 있다.

이번에는 단점들 중 테스트 하기 어려운 이유 자세히 알아보자.

  1. 싱글턴 인스턴스를 mock 구현으로 대체할 수 없다.

  2. 싱글톤 인스턴스는 자원을 공유하고 있기 때문에 테스트가 결정적으로 격리된 환경에서 수행되려면 매번 인스턴스의 상태를 초기화 시켜주어야 한다. 그렇지 않으면 애플리케이션 전역에서 상태를 공유하기 때문에 테스트가 온전하게 수행되지 못한다.

    • 예를 들어 해당 인스턴스가 어떤 행동을 하는데 비용이 많이 든다고 해보자.
    • 이런 인스턴스는 비용이 많이 들기 때문에 대역이 필요하다.
    • 만약 해당 인스턴스가 인터페이스를 구현한 인스턴스가 아니라면, 대역을 사용할 수 없어 계속 해당 객체로 그 행동을 테스트 해야 한다.
      → 비용이 많이 발생하는 문제 발생
    • 연산이 굉장히 오래 걸리는 즉, 많은 자원을 잡아먹는 행동을 할 때, 계속 해당 객체로 행동을 실제로 수행하면 비용이 많이 발생한다.
      (또한 테스트는 한번만 실행하는 것이 아닌 여러 번 수행된다.)

Elvis Class

public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }
    public void sing() {
        System.out.println("sing~~");

    }
}

Concert Class

class ConcertTest {

    @Test
    void perform() {
        Concert concert = new Concert(Elvis.getInstance());
        concert.perform();

        assertTrue(concert.isLightsOn());
        assertTrue(concert.isMainStateOpen());
    }

}
  • Elvis가 비용이 많이 발생하는 자원이라 가정하자
  • Elvis를 계속해서 콘서트에 불러 테스트를 진행하면 많은 비용이 발생한다.

이번에는 테스트 비용을 줄이기 위해 인터페이스를 구현해보자.
Elvis Class

public class Elvis implements IElvis{
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    @Override
    public void sing() {
        System.out.println("sing~~");

    }
}
  • Elvis라는 가수가 있다고 가정해보자.
  • Elvis라는 가수를 한번 불러 공연을 하려면 많은 비용이 발생한다.

Concert Class

public class Concert {

    private boolean lightsOn;

    private boolean mainStateOpen;

    private IElvis elvis;

    public Concert(IElvis elvis) {
        this.elvis = elvis;
    }

    public void perform() {
        mainStateOpen = true;
        lightsOn = true;
        elvis.sing();
    }

    public boolean isLightsOn() {
        return lightsOn;
    }

    public boolean isMainStateOpen() {
        return mainStateOpen;
    }
}
  • Elvis는 콘서트장에서 공연을 하기로 했다.
  • 공연을 하기전에는 사전 리허설이 필요한데, 리허설을 위해 계속 Elvis를 부르면 많은 비용이 발생할 것이다.
  • 이를 위해 비용이 싼 가수를 초청해서 리허설을 진행하기로 했다.
  • 그러기 위해서는 그 가수도 Elvis와 같은 행동을 진행해야 한다.
  • 이와 같이 같은 행동을 하기 위해서는 인터페이스가 필요하다.
    (Effective Java에서 "인스턴스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다"라고 말한다.)

MockElvis Class

public class MockElvis implements IElvis {
    @Override
    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    @Override
    public void sing() {
        System.out.println("sing~~");
    }
}
  • Elvis 대신 리허설을 진행할 가수인 MockElvis를 만들었다.

ConcertTest

class ConcertTest {

    @Test
    void perform() {
        Concert concert = new Concert(new MockElvis());
        concert.perform();

        assertTrue(concert.isLightsOn());
        assertTrue(concert.isMainStateOpen());
    }

}
  • 위 코드를 보면 콘서트 리허설을 MockElvis가 대신 진행하는 것을 볼 수 있다.
  • 이렇게 인터페이스를 구현하면, 비용이 많이 발생하는 싱글턴 객체를 대신한 객체를 생성해 테스트할 수 있다.

1개의 댓글

comment-user-thumbnail
2023년 7월 22일

이런 유용한 정보를 나눠주셔서 감사합니다.

답글 달기