외부 코드 사용하기

  • 인터페이스 제공자와 인터페이스 사용자 사이에는 특유의 긴장이 존재한다.
  • 제공자는 적용성을 최대한 넓히려 애쓴다.
  • 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.
  • 이런 긴장으로 인해 시스템 경계에서 문제가 생길 소지가 많다.
Map을 깔끔하게 사용한 코드
public class Sensors {
    private Map sensors = new HashMap();

    public Sensor getById(String id) {
        return (Sensor)sensors.get(id);
    }
    // 이하 생략
}
  • 경계 인터페이스인 Map을 Sensors 안으로 숨긴다.
  • 제네릭스를 사용하든 하지 않든 문제가 안 된다.
  • Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다.
  • Map 클래스를 사용할 때마다 캡슐화하라는 소리가 아니다. Map을 여기저기 넘기지 말라는 말이다.
  • Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.

경계를 살피고 익히기

  • 외부 코드를 익히기는 어렵고 통합하기도 어렵다.
  • 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히자. (학습 테스트)

log4j 익히기

@Test
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}
  • 패키지를 내려 받아 소개 페이지를 연다.
  • 문서를 자세히 읽기 전에 간단한 테스트 케이스를 작성한다.
  • 테스트를 돌렸더니 Appender라는 뭔가가 필요하다는 오류가 발생한다.
  • 문서를 좀 더 읽어보니 ConsoleAppender라는 클래스가 있다.
  • 그래서 ConsoleAppender를 생성한 후 테스트 케이스를 다시 돌린다.
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConsoleAppender();
    logger.addAppender(appender);
    logger.info("hello");
}
  • 이번에는 Appender에 출력 스트림이 없다는 사실을 발견한다.
  • 이상하다. 출력 스트림이 있어야 정상이 아닌가? 구글링을 한 후 다음과 같이 시도한다.
@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 버그이거나 적어도 일관성 부족으로 여겨진다.
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");
    }
}
  • 좀 더 구글링을 하고, 문서를 읽어보고, 테스트를 돌린 끝에 얻어낸 테스트 코드이다.
  • 지금까지 간단한 콘솔 로거를 초기화하는 방법을 익혔으니, 이제 모든 지식을 독자적인 로거 클래스로 캡슐화한다.
  • 그러면 나머지 프로그램은 log4j 경계 인터페이스를 몰라도 된다.

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

  • 패키지 새 버전이 나올 때마다 새로운 위험이 생긴다.
  • 새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.
  • 학습 테스트를 이용한 학습이 필요하든 그렇지 않든, 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다.
  • 이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.

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

  • 경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.
  • 저자 예시
    • 무선통신 시스템에 들어갈 소프트웨어 개발에 참여했다.
    • 소프트웨어에는 '송신기'라는 하위 시스템이 있었는데, 우리는 여기에 대한 지식이 거의 없었다.
    • '송신기' 시스템을 책임진 사람들은 인터페이스도 정의하지 못한 상태였다. 저자는 '송신기' 팀을 기다리는 대신에 저자 팀이 '송신기' 모듈에게 원하는 기능을 자체적으로 인터페이스로 정의했다.
      • 지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라.
    • 저자 팀이 바라는 인터페이스를 구현함으로써 인터페이스를 전적으로 통제한다는 장점이 생긴다. 또한 코드 가독성도 높아지고 코드 의도도 분명해진다.
    • ADAPTER 패턴으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한곳으로 모았다.

깨끗한 경계

  • 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.
  • 경계에 위치하는 코드는 깔끔히 분리하고 기대치를 정의하는 테스트 케이스도 작성하자.
  • 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하자.
  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
    • Map에서 봤듯이, 새로운 클래스로 경계를 감싸자.
    • 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

'Clean Code' 카테고리의 다른 글

[Clean Code] 클래스  (0) 2020.09.03
[Clean Code] 단위 테스트  (0) 2020.09.03
[Clean Code] 오류 처리  (0) 2020.09.03
[Clean Code] 객체와 자료구조  (0) 2020.09.03
[Clean Code] 형식 맞추기  (0) 2020.09.03

+ 따끈한 최근 게시물