자바/클린 코드

클린코드 8장 - 경계

끄적끄적 2022. 5. 24. 18:43

외부 코드 사용하기
외부 코드(다른 팀의 컴포넌트 또는 오픈소스)는 기능성과 유연성을 제공하기 위해 필요이상의 기능을 제공할 수가 있다.

아래와 같이 Sensor를 담는 Map(외부 인터페이스)을 사용하면, get할때마다 캐스팅을 해야 한다.

Map sensors = new HashMap();
...
Sensor s = (Sensor)sensors.get(sensorId);

아래와 같이 Generics를 사용하면 조금 나아지지만, Map인터페이스가 변하면 소스를 다 수정해야 한다.

Map<String, Sensor> sensors = new HashMap<Sensor>();
...
Sensor s = sensors.get(sensorId);

아래와 같이 Map을 Sensors 안으로 숨기면, Map이 변해도 나머지 프로그램에 영향을 미치지 않는다.

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

모든 Map클래스를 래핑하라는 말은 아니다. Map과 같은 "경계 인터페이스"는 클래스 내부에 배치하고, 여기저기 넘기거나 공개 API의 인수나 반환값으로 사용하지 않는게 좋다는 말이다.

경계 살피고 익히기
외부 코드를 테스트 없이 코드로 작성하면, 우리 버그인지 라이브러리 버그인지 찾아내기 어렵다.
곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는게 좋다. 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 

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를 익혀야 하므로 학습테스트에 드는 비용은 없으며, 효과적인 방법이다.
패키지의 새 버전이 나온다면 학습테스트를 돌려 차이가 있는지 확인한다. 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하며, 이것이 있으면 새로운 버전으로 이전은 어렵지 않다.

아직 존재하지 않는 코드를 사용하기
아직 개발되지 않은 외부 모듈에 대해 인터페이스조차 구현되지 않은 경우가 있을 수 있다. 
아직 개발되지 않은 송신기(Transmitter)에 대비해 아래와 같이 먼저 진행할 수가 있다.
Transmitter Interface를 정의하고, Fake Transmitter로 테스트 한다. 상대방쪽에서 외부 API의 인터페이스가 정해지면 Adapter에서 해당 API를 지원하도록 수정한다. (Adapter Design Pattern 참고)

코드로 구현하면 아래 예시와 같다.

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

깨끗한 경계
통제하지 못하는 외부 코드를 사용할 때는 향후 변경으로 인한 영향을 각별히 주의해야 한다.
내부 코드가 외부 코드를 많이 알지 못하게 막아야 한다. 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다.
Map객체를 래핑한 것과 같이 감싸거나 Adapter패턴으로 우리에게 맞게 인터페이스를 변환하자.

반응형