원문: https://testing.googleblog.com/2008/05/tott-using-dependancy-injection-to.html (Translated by Google Gemini)


싱글턴을 사용하는 코드를 테스트하기는 어렵습니다. 일반적으로 테스트하려는 코드는 싱글턴 인스턴스와 강력하게 결합되어 있습니다. 싱글턴 객체가 종종 정적 생성자나 정적 메서드에서 생성되기 때문에 싱글턴 객체의 생성을 제어할 수 없습니다. 결과적으로 싱글턴 인스턴스의 동작을 시뮬레이션할 수도 없습니다.

싱글턴 클래스의 구현 변경이 불가능하지만, 싱글턴 클라이언트는 변경이 가능하다면 간단한 리팩터링으로 테스트를 더 쉽게 만들 수 있습니다. 싱글턴 인스턴스로 Server를 사용하는 메서드가 있다고 가정해 봅시다.

public class Client {
  public int process(Params params) {
    Server server = Server.getInstance();
    Data data = server.retrieveData(params);
    ...
  }
}

Client를 리팩터링하여 의존성 주입을 사용하고 싱글턴 패턴의 사용을 완전히 피할 수 있습니다. 어떤 기능도 잃지 않았고, Server의 싱글턴 인스턴스만 존재해야 한다는 요구 사항도 잃지 않았습니다. 유일한 차이점은 Server 인스턴스를 정적 getInstance 메서드에서 가져오는 대신, Client가 생성자에서 받는다는 것입니다. 클래스를 테스트하기 더 쉽게 만들었습니다!

public class Client {
  private final Server server;
  
  public Client(Server server) {
    this.server = server;
  }
  
  public int process(Params params){
    Data data = this.server.retrieveData(params);
    ...
  }
}

테스트할 때, 필요한 예상 동작을 가진 목 (mock) Server를 생성하여 테스트 중인 Client에 전달할 수 있습니다.

public void testProcess() {
  Server mockServer = createMock(Server.class);
  Client c = new Client(mockServer);
  assertEquals(5, c.process(params));
}

(역자 주: 이례적으로 이 글에 댓글 논의가 많아 댓글들도 가져왔습니다.)

댓글 12개 :#

Calvin Spealman 2008년 5월 18일 오후 3:34:00 PDT

이 방법에는 몇 가지 단점이 있습니다. 클라이언트 사용자가 싱글턴을 어디서 가져와야 하는지 알아야 하고, 클라이언트 클래스를 생성할 때마다 접근해야 합니다. 또 다른 문제는 이 싱글턴이 싱글턴을 얻는 시점의 차이를 무시한다는 것인데, 이는 중요한 의미를 가질 수 있으며 해결해야 합니다. 이러한 제안에는 클라이언트 클래스가 싱글턴 주입을 선택적 매개변수로 취하고 기본적으로 싱글턴에 액세스해야 하며, 가능하면 싱글턴을 사용할 때마다 특별히 액세스해야 한다고 포함되어야 합니다.

Ran Biron 2008년 5월 19일 오전 11:07:00 PDT

몇 번의 실패 끝에 우리(저와 제가 일하는 비공개 회사의 팀)는 몇 가지 결론에 도달했습니다.

  1. DI 프레임워크(예: Spring)는 테스트를 매우 편하게 할 수 있게 해줍니다.
  2. DI 프레임워크를 사용할 수 없다면 (간단한) 프레임워크를 만드십시오.

이제 2번에 대한 설명이 필요합니다. 우리가 하는 일은 실제 싱글턴 하나를 갖는 것입니다. 싱글턴(애플리케이션 컨텍스트, 컨테이너, 신(god) – 원하는 것을 선택하십시오)은 여러 컨텍스트 “허브"에 대한 매우 제한적인 액세스 맵 역할을 합니다. 각 허브는 맵과 직접 통신하여 필요한 객체를 먼저 로드한 다음 필요에 따라 추출하고 형 변환합니다. 컨텍스트 객체는 생성 시 매우 작은 공간을 차지하며 이벤트(애플리케이션 초기화) / 요청(실제 싱글턴과 동일) 시에만 초기화됩니다.

테스트는 아래 두 방법 중 하나로 가능합니다:

  1. 적은 수의 목 컨텍스트만 필요한 경우, 수동으로 생성하여 싱글턴 맵에 로드합니다. 향후 요청은 이를 검색할 것입니다. 테스트 해체 시 전체 맵을 비웁니다.
  2. 많은 수의 목 컨텍스트(대규모 단위 테스트 / 통합 테스트)가 필요한 경우, 실제 허브 중 하나(또는 그 이상)를 실행하고 맵에서 필요한 것만(있는 경우) 목 구현으로 덮어씁니다.

이 접근 방식은 프로덕션 코드에 매우 용이하면서도 실제 DI만큼 테스트 가능합니다.

Unknown 2008년 5월 19일 오후 11:03:00 PDT

솔직히, 장점을 모르겠습니다…

Client 클래스의 첫 번째 구현에서는 다음과 같이 할 수 있었습니다.

Client c = new Client();
client.process();

그게 다입니다… 여기서 단점은 무엇입니까?

Leo Gutierrez R 2016년 12월 25일 오후 4:02:00 PST

흠, 장점은 두 번째 접근 방식에서 Server 생성을 추출하여 이를 목킹할 수 있다는 것입니다. 첫 번째 접근 방식에서는 불가능했습니다.

Unknown 2008년 5월 20일 오전 9:43:00 PDT

싱글턴을 사용하는 코드를 테스트하기는 어렵습니다. << 올해의 과소평가 :P 우리는 테스트를 위해 DI를 사용하여 많은 운을 얻었으며, 싱글턴이 필요한 경우 테스트를 위해 객체를 구성할 매개변수를 받는 대체 getInstance 메서드를 제공합니다.

David 2008년 5월 20일 오후 3:31:00 PDT

이런 일을 위해 Guice를 사용합니다.

Gauthier 2014년 11월 19일 오전 2:29:00 PST

물론입니다. 하지만 여전히 API는 좀 별로입니다. Client가 Server에 대해 강한 의존성을 갖는지 여부(public process() 내부인지 protected obtainServer() 메서드 내부인지)는 제대로 문서화되지 않으면 명확하지 않습니다. 의존성 주입은 이러한 의존성을 API에 바로 보여줍니다. 그리고 Client를 재사용할 때마다 다른 서브클래스를 만들어야 할 것입니다.

roni 2014년 10월 3일 오전 2:26:00 PDT

싱글턴을 사용하는 코드를 테스트하는 것은 어렵지 않습니다.

쉽습니다.

싱글턴을 사용하는 클라이언트를 테스트하는 방법은 다음과 같습니다. 먼저, 싱글턴 인스턴스를 얻는 코드를 다음과 같이 새 protected 메서드로 리팩토링합니다.

 public class Client {
    public int process(Params params) {
       Server server = obtainServer();
       Data data = server.retrieveData(params);
       ...
    }

    //new method
    protected Server obtainServer() {
       return Server.getInstance();
    }
 }

그런 다음 다음과 같이 목 Server를 사용하여 테스트합니다.

 public class ClientTest extends Client {
    public void testProcess() {
       Client c = new Client() {
          @Override protected Server obtainServer() {
             return new MyMockServer();
          }
       };
       assertEquals(5, c.process(params));
    }

Unknown 2016년 9월 25일 오전 7:56:00 PDT

이렇게 오랜 시간이 지난 후에 질문해서 죄송하지만, 자신만의 목 클라이언트를 만드는 방법에 대해 설명해주시거나 자료를 알려주실 수 있나요?

감사합니다!

Anonymous 2018년 1월 15일 오전 10:55:00 PST

우리는 1990년대 후반 이리듐 위성 페이로드 프로젝트에서 이런 아이디어를 광범위하게 사용했습니다. 모든 인터페이스에는 의존성 인라인 함수가 있는 include 파일이 있었는데, include를 교체함으로써 모든 것을 목킹할 수 있었습니다. 그 결과, 각 서비스의 언어가 완전히 다르고 거의 무작위적이었기 때문에 아무도 다른 사람의 코드를 읽을 수 없었습니다.

JB 2014년 11월 19일 오전 2:29:00 PST

확실히 그렇지만, 여전히 당신의 API는 좀 엉망입니다. Client가 Server에 강력한 의존성을 가지고 있는지(public process() 안에 있는지 protected obtainServer() 메서드 안에 있는지) 적절하게 문서화되지 않으면 명확하지 않습니다. 의존성 주입은 이러한 의존성을 API에 직접 보여줍니다. 그리고 obtainServer()에 다른 로직이 필요한 Client를 재사용할 때마다 다른 서브클래스를 만들어야 할 것입니다. 클라이언트가 서버를 얻는다는 것은 - 적어도 저에게는 - 범위를 훨씬 벗어나는 것처럼 보인다는 것은 말할 것도 없습니다. 제 짧은 의견일 뿐입니다 (just my 2 cents).

Alexander Todorov 2015년 11월 4일 오후 3:03:00 PST

그건 그렇고, 이 페이지 제목에 오타가 있습니다. Dependancy와 Dependency를 보세요.

이 페이지로 연결되는 기사를 맞춤법 검사하고 있었는데 이것이 나타났습니다.