CS/클린코드

[클린코드] chapter-8 경계

yhsim98 2022. 9. 5. 21:21

서드파티 코드 사용하기

  • 인터페이스를 "제공하는" 입장과 "사용하는" 입장 사이에는 필연적인 긴장감이 존재한다.
  • "제공하는" 입장에서는 좀 더 다양한 환경에서 좀 더 많은 사용자가 사용할 수 있도록 다양한 사용성을 지향한다.
  • "사용하는" 입장에서는 그들의 사용성에 맞는 specific한 인터페이스를 원한다.
  • 이것을 "경계에서의 긴장"이라 부른다.
  • 만약 우리가 Sensor클래스를 저장하는 Map객체를 사용한다면 다음과 같은 형태일 것이다.
  • Map sensors = new HashMap();
  • Sensor s = (Sensor) sensors.get(sensorId);
  • 이와 같은 방식은 Sensor클래스를 사용하는 코드 전반에 걸쳐 빈번히 사용된다.
  • 하지만 이는 사용되는 곳에서 캐스팅의 부담을 안게 된다. 그리고 적절한 문맥조차 줄 수 없다.
  • 이는 아래와 같이 generic을 사용함으로써 해결할 수 있다.
  • Map<Sensor> sensors = new HashMap<Sensor>();
  • Sensor s = sensors.get(sensorId);
  • 하지만 이 방법 또한 Map객체가 필요 이상의 기능을 제공하는 것은 막지 못한다.
  • Map의 인터페이스가 바뀌거나 할 경우 또한 우리 코드의 많은 부분들이 바뀌어야 한다.
  • 인터페이스가 바뀔 일이 별로 없을 것이라 생각할 지도 모르지만, 실제로 Java 5버전에서 generic이 추가되었을 때 Map의 인터페이스가 바뀐 사례가 있다.
  • 결국 제일 좋은 방법은 래핑이다.
  • 모든 Map을 이런 식으로 래핑하라는 말은 아니다.
  • 다만 Map과 같은 "경계에 있는 인터페이스"를 시스템 전반에 걸쳐 돌려가며 사용하지 말고,
  • 해당 객체를 사용하는 클래스 내부에 넣던지 가까운 계열의 클래스에 넣어라.
  • 공개된 api에서 인자로 받거나 리턴하지 마라.
public class Sensors {
    // 경계의 인터페이스(이 경우에는 Map의 메서드)는 숨겨진다.
    // Map의 인터페이스가 변경되더라도 여파를 최소화할 수 있다. 예를 들어 Generic을 사용하던 직접 캐스팅하던 그건 구현 디테일이며 Sensor클래스를 사용하는 측에서는 신경쓸 필요가 없다.
    // 이는 또한 사용자의 목적에 딱 맞게 디자인되어 있으므로 이해하기 쉽고 잘못 사용하기 어렵게 된다.

    private Map sensors = new HashMap();

    public Sensor getById(String id) {
        return (Sensor)sensors.get(id);
    }
    //snip
}

경계 살피고 익히기

  • 외부 코드를 사용하면 개발 시간이 감소되지만,
  • 우리가 사용할 코드를 테스트하는 편이 바람직하다
  • 간닪나 테스트 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히자
  • 짐 뉴커스는 이를 학습 테스트라 부른다(TDD 에서)

Log4j 익히기

  • 로깅을 직접 구현하는 대신 아파치의 log4j를 사용할 때,
  • 문서를 자세히 읽기 전 첫 번째 테스트 케이스를 작성한다
@Test
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}
  • 이 이후에도 출력 스트림이 없거나 Appender 이 필요한 등
    여러 문제를 해결하며 익혀본다
  • 이렇게 구글을 뒤지고,
  • 문서를 읽어보고, 테스트를 돌린 끝에
  • 돌아가는 방식을 이해하고, 여기서 얻은 지식을 이용하여
  • 간단한 단위 테스트 몇 개로 표현한다
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 경계 인터페이스를 몰라도 된다

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

  • 학습 테스트에 드는 비용은 없다
  • 투자하는 노력보다 얻는 성과가 더 크다
  • 패키지 새 버전이 나온다면 테스트만 돌려보면 된다

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

  • 아직 개발되지 않은 모듈이 필요한데, 기능은 커녕 인터페이스조차 구현되지 않은 경우가 있을 수 있다.
    하지만 우리는 이러한 제약때문에 우리의 구현이 늦어지는걸 탐탁치 않게 여긴다.
  • 예시
    • 저자는 무선통신 시스템을 구축하는 프로젝트를 하고 있었다.
    • 그 팀 안의 하부 팀으로 "송신기"를 담당하는 팀이 있었는데 나머지 팀원들은 송신기에 대한 지식이 거의 없었다.
    • "송신기"팀은 인터페이스를 제공하지 않았다. 하지만 저자는 "송신기"팀을 기다리는 대신 "원하는" 기능을 정의하고 인터페이스로 만들었다.
      • [지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라]
    • 이렇게 인터페이스를 정의함으로써 메인 로직을 더 깔끔하게 짤 수 있었고 목표를 명확하게 나타낼 수 있었다.
public interface Transimitter {
    public void transmit(SomeType frequency, OtherType stream);
}

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

// 경계 밖의 API
public class RealTransimitter {
    // 캡슐화된 구현
    ...
}

public class TransmitterAdapter extends RealTransimitter 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);
    }
}

출처

https://github.com/Yooii-Studios/Clean-Code/blob/master/Chapter%2008%20-%20%EA%B2%BD%EA%B3%84.md