[Clean Code] 8장 경계

ASk·2021년 12월 30일
0

Clean Code

목록 보기
8/17
post-thumbnail

Intro

  • 시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다.
  • 때로는 패키지나 오픈소스를 사용하거나, 사내 다른팀이 제공하는 컴포넌트를 사용해야한다.
  • 어떤식이든, 우리는 이 외부 코드를 우리 내부 코드에 "깨끗하게" 통합시켜야 한다.
  • 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴보자.

외부 코드 사용하기

  • 인터페이스를 "제공하는" 입장과 "사용하는" 입장 사이에는 특유의 긴장이 존재한다.
  • "제공하는" 입장에서는 다양한 환경에서 좀 더 많은 사용자가 사용할 수 있도록 적용성을 넓히려고 애쓴다.
  • "사용하는" 입장에서는 그들의 요구에 집중하는 인터페이스를 바란다.
  • 이러한 긴장으로 시스템 경계에서 문제가 생길 소지가 많다.
  • 한 예로 java.util.Map 의 경우 다양한 인터페이스로 수많은 기능을 제공한다.
  • Map 의 인터페이스가 바뀌거나 할 경우 또한 우리 코드의 많은 부분들이 바뀌어야 한다.
    • 실제로 Java 5버전에서 generic이 추가되었을 때 Map의 인터페이스가 바뀐 사례가 있다.
  • 경계 인터페이스를 다루는 좋은 방법은 감싸기이다.
  • 다만, Map 클래스를 사용할 때마다 캡슐화하라는 소리가 아니다.
  • Map과 같은 "경계 인터페이스"를 여기저기 넘기지 말아야한다.
    • 해당 객체를 사용하는 클래스 내부에 넣던지 가까운 계열의 클래스에 넣어 외부로 노출되지 않도록 주의한다.
    • 공개된 api에서 인자로 받거나 리턴하지 마라.
// 경계의 인터페이스(이 경우에는 Map)는 숨겨진다.
// Map의 인터페이스가 변경되더라도 여파를 최소화할 수 있다.
// 코드를 이해하기는 쉽지만 오용하기는 어렵다.
public class Sensors {

  private Map<String, Sensor> sensors = new HashMap<>();

  public Sensor getById(String id) {
    return sensors.get(id);
  }
  // 이하 생략
}
  • 추가 : 외부 api가 변경될 경우 전반적으로 적용된 코드를 수정해야 하므로 wrapping 하여 변경점을 최소화 할수 있다.

경계 살피고 익히기

  • 외부 코드를 사용할 때, 해당 코드의 테스트가 우리 책임은 아니다. (제공자의 책임)
  • 하지만 우리 자신을 위해 우리가 사용할 부분의 코드는 테스트하는 편이 바람직하다.
  • 외부 코드를 익히는것과 외부 코드를 통합하는건 어렵다, 둘을 동시에 하면 더욱 어렵다.
  • 따라서 바로 외부 코드를 통합하는 대신 간단한 테스트 케이스를 작성해 외부 코드를 익히는것도 좋은 방법이다.
  • 짐 뉴커크는 이를 "학습 테스트" 라고 부른다.

log4j 공부하기

// 1.
// 패키지를 내려받아 소개 페이지를 열고 문서롤 자세히 읽기 전에 첫번째 테스트 케이스를 작성한다.
// 화면에 "hello"를 출력하는 테스트 케이스이다.
@Test
public void testLogCreate(){
  Logger logger=Logger.getLogger("MyLogger");
  logger.info("hello");
}
// 2.
// 위 테스트는 "Appender라는게 필요하다"는 에러를 뱉는다.
// 조금 더 읽어보니 ConsoleAppender라는게 있는걸 알아냈다.
// 그래서 ConsoleAppender를 생성한 후 테스트 케이스를 다시 돌린다.
@Test
public void testLogAddAppender(){
  Logger logger=Logger.getLogger("MyLogger");
  ConsoleAppender appender=new ConsoleAppender();
  logger.addAppender(appender);
  logger.info("hello");
}
// 3.
// 이번에는 출력 스트림이 없다는 사실을 발견한다.
// 이상하다. 가지고 있는게 정상 아닌가?
// 구글을 검색한 후 다음과 같이 시도한다.
@Test
public void testLogAddAppender(){
  Logger logger=Logger.getLogger("MyLogger");
  logger.removeAllAppenders();
  logger.addAppender(new ConsoleAppender(
      new PatternLayout("%p %t %m%n"),
      ConsoleAppender.SYSTEM_OUT));
  logger.info("hello");
}
// 성공했다. 하지만 ConsoleAppender를 만들어놓고 콘솔에 쓰라고 알리는 것이 뭔가 수상하다.
// 그래서 ConsoleAppender.SYSTEM_OUT 인수를 제거했더니 잘 돌아간다.
// 하지만 PatternLayout을 제거하니 돌아가지 않는다.
// 그래서 문서를 살펴봤더니 "ConsoleAppender의 기본 생성자는 설정되지 않은상태" 란다.
// 당연하지도 유용하지도 않다. log4j 버그이거나 적어도 일관성 부족으로 여겨진다.
// 조금 더 구글링, 문서 읽기, 테스트를 거쳐 log4j의 동작법을 알아냈고 그것을 간단한 유닛테스트로 기록했다.
// 이제 이 지식을 기반으로 log4j를 독자적인 로거 클래스로 캡슐화 한다.
// 그러면 나머지 코드들은 log4j 경게 인터페이스를 몰라도 된다.
public class LogTest {
  private Logger logger;

  @Before
  public void initialize() {
    logger = Logger.getLogger("logger");
    logger.removeAllAppenders();
    Logger.getRootLogger().removeAllAppenders();
  }

  @Test
  public void basicLogger() {
    BasicConfigurator.configure();
    logger.info("basicLogger");
  }

  @Test
  public void addAppenderWithStream() {
    logger.addAppender(new ConsoleAppender(
        new PatternLayout("%p %t %m%n"),
        ConsoleAppender.SYSTEM_OUT));
    logger.info("addAppenderWithStream");
  }

  @Test
  public void addAppenderWithoutStream() {
    logger.addAppender(new ConsoleAppender(
        new PatternLayout("%p %t %m%n")));
    logger.info("addAppenderWithoutStream");
  }
}

학습 테스트는 공짜 이상이다

  • 학습 테스트에 드는 비용은 없다. 어쨋든 API를 배워야 하므로...
  • 오히려 필요한 지식만 확보하는 손쉬운 방법이다.
  • 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인할 수 있다.
  • 새버전이 기존과 호환 되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.
  • 이러한 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.
  • 경계 테스트가 없다면 낡은 버전을 필요 이상으로 오랫동안 사용하려는 유혹에 빠지기 쉽다.

아직 존재하지 않는 코드 사용하기

  • 경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.
  • 때로는 (적어도 지금은) 알려고해도 알수가 없다.
  • 아직 개발되지 않은 모듈이 필요하지만, 기능은 커녕 인터페이스조차 구현되지 않은 경우가 있을 수 있다.
  • 그럴 경우 구현은 뒤로 미루고 자체적으로 인터페이스를 정의하여 진행하는 방법도 있다.
  • 예시
    • 저자는 무선통신 시스템을 구축하는 프로젝트를 하고 있었다.
    • 그 소프트웨어에는 "송신기"라는 하위 시스템이 있었는데 저자 팀은 송신기에 대한 지식이 거의 없었다.
    • "송신기" 팀은 인터페이스도 정의하지 못한 상태였다.
    • 프로젝트 지연을 원하지 않았기에 "송신기" 하위 시스템과 아주 먼 부분부터 작업하기 시작했다.
    • Adaptor pattern 으로 API 사용을 캡슐화 하였다. 이와 같은 설계는 테스트도 아주 편하다.
public interface Transimitter { 
  void transmit(SomeType frequency, OtherType stream);
}

public class FakeTransmitter implements Transimitter {
  public void transmit(SomeType frequency, OtherType stream) {
    // 실제 구현이 되기 전까지 더미 로직으로 대체
  }
}

public class TransmitterAdapter implements Transimitter {
  public void transmit(SomeType frequency, OtherType stream) {
    // RealTransimitter(외부 API)를 사용해 실제 로직을 여기에 구현.
    // Transmitter의 변경이 미치는 영향은 이 부분에 한정된다.
  }
}

public class CommunicationController {
  // Transmitter팀의 API가 제공되기 전에는 아래와 같이 사용한다.
  public void someMethod() {
    Transmitter transmitter = new FakeTransmitter();
    transmitter.transmit(someFrequency, someStream);
  }
    
  // Transmitter팀의 API가 제공되면 아래와 같이 사용한다.
  public void someMethod() {
    Transmitter transmitter = new TransmitterAdapter();
    transmitter.transmit(someFrequency, someStream);
  }
}

깨끗한 경계

  • 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.
  • 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리코드에 의존하는 편이 훨씬 좋다.
  • 자칫하면 오히려 외부 코드에 휘둘리고 만다.
  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
  • 새로운 클래스로 경계를 감싸거나 adaptor 패턴을 사용해 우리의 인터페이스로 변환하자.
    • 코드 가독성이 높아진다.
    • 경계 인터페이스를 사용하는 일관성도 높아진다.
    • 외부 패키지가 변했을 때 변경할 코드도 줄어든다.
profile
Backend Developer

0개의 댓글