원문: https://testing.googleblog.com/2020/12/testing-on-toilet-separation-of.html (Translated by Google Gemini)


다음 함수는 SpeedyImg라는 API를 사용하여 바이트 배열을 이미지로 디코딩합니다. 다른 팀이 소유한 API를 참조할 때 어떤 유지보수 문제가 발생할 수 있을까요?

SpeedyImgImage decodeImage(List<SpeedyImgDecoder> decoders, byte[] data) {
  SpeedyImgOptions options = getDefaultConvertOptions();
  for (SpeedyImgDecoder decoder : decoders) {
    SpeedyImgResult decodeResult = decoder.decode(decoder.formatBytes(data));
    SpeedyImgImage image = decodeResult.getImage(options);
    if (validateGoodImage(image)) { return image; }
  }
  throw new RuntimeException();
}

API 호출에 대한 세부 정보가 도메인 로직과 혼합되어 있어 코드를 이해하기 어렵게 만들 수 있습니다. 예를 들어, decoder.formatBytes() 호출은 API에서 필요하지만 바이트가 어떻게 포맷되는지는 도메인 로직과 관련이 없습니다.

또한 이 API가 코드베이스의 여러 곳에서 사용되는 경우, API 사용 방식이 변경되면 모든 사용처를 변경해야 할 수 있습니다. 예를 들어, 이 함수의 반환 유형이 더 일반적인 SpeedyImgResult 유형으로 변경되면 SpeedyImgImage의 사용처를 업데이트해야 합니다.

이러한 유지보수 문제를 피하려면, API 세부 정보를 추상화 뒤에 숨기기 위해 래퍼 (wrapper) 타입을 만드세요:

Image decodeImage(List<ImageDecoder> decoders, byte[] data) {
  for (ImageDecoder decoder : decoders) {
    Image decodedImage = decoder.decode(data);
    if (validateGoodImage(decodedImage)) { return decodedImage; }
  }
  throw new RuntimeException();
}

외부 API를 래핑하는 것은 관심사 분리 원칙을 따릅니다. API 호출 로직이 도메인 로직과 분리되기 때문입니다. 이는 다음과 같은 많은 이점을 제공합니다.

  • API 사용 방식이 변경되더라도 래퍼에 API를 캡슐화하면 변경 사항이 코드베이스 전체에 전파되는 것을 막을 수 있습니다.
  • 소유한 타입의 인터페이스나 구현은 수정할 수 있지만, API 타입은 수정할 수 없습니다.
  • 도입된 타입(예: ImageDecoder/Image)으로 여전히 표현될 수 있으므로 다른 API로 전환하거나 추가하기가 더 쉽습니다.
  • 핵심 로직을 이해하기 위해 API 코드를 자세히 살펴볼 필요가 없으므로 가독성이 향상될 수 있습니다.

모든 외부 API를 래핑할 필요는 없습니다. 예를 들어, API를 분리하는 데 엄청난 노력이 필요하거나 코드베이스를 오염시키지 않을 만큼 간단하다면 래퍼 타입을 도입하지 않는 것이 더 나을 수 있습니다(예: Java의 List 또는 C++의 std::vector와 같은 라이브러리 타입). 확실하지 않을 때는 래퍼가 코드를 명확하게 개선할 경우에만 추가해야 한다는 점을 기억하세요(YAGNI 원칙 참조).