Clean Code 8. 경계


외부 코드 사용하기

인터페이스 제공자는 적용성을 최대한 넓히려 애쓴다. 그래야 더 많은 환경에서 돌아가고, 더 많은 고객이 구매한다. 반면 사용자는 자신에 요구 사항에만 집중하고 필요없는 기능은 제공하지 않는 것을 좋아한다.

java.util.Map을 사용할 경우 굉장히 많은 기능을 제공한다. 그런데 사용하는 입장에서 내용을 지우는 행위를 원하지 않고, 특정 type으로만 사용을 원할 경우 어떻게 해야할까 ?

Map sensors = new HashMap();

Sensor s = (Sensor) sensors.get(sensorId);

캐스팅하는 코드를 계속해서 사용해야 한다. Generics를 사용하면 코드 가독성은 크게 높아진다.

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

Sensor s = sensors.get(sensorId);

이 역시 사용자가 윈치않는 기능을 제공하는 문제는 해결을 못했다. 더 큰 문제는 Map의 인터페이스가 바뀔 경우 수정할 코드가 상당히 많아진다.

가장 좋은 방법은 캡슐화다.

public class Sensors {
    private Map sensors = new HashMap();
    
    public Sensor getById(int id) {
        return (Sensor)sensors.get(id);
    }
}

경계 살피고 익히기

외부 패키지(third-party code)의 테스트는 우리 책임이 아니지만, 우리가 사용할 코드는 테스트하는게 바람직하다.

외부 코드를 익히기는 어렵다. 외부 코드를 통합하기도 어렵다.

짐 뉴커크(Jim Newkirk)의 학습 테스트 : 테스트 케이스를 작성해 외부 코드를 익힌다.

ex. log4j 익히기

먼저 화면에 hello를 출력해보자.

@Test
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}

Appender가 필요하다는 오류가 발생했다. 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");
}

이제 문제없이 동작한다. 이렇게 지식을 얻어가면서 테스트 케이스를 통해서 익히는 방법이다.

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");
    }
}

이제 자체적인 Logger 클래스로 캡슐화한다. 그러면 나머지 프로그램에서 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) {
        // Adapter Pattern으로 API 캡슐화
        // 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);
    }
}

깨끗한 경계

소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 경계에 위치하는 코드는 깔끔하게 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다. 외부 패티지를 호출하는 코드를 가능한 줄여 경계를 관리하자.

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)