8장. 경계

오픈 소스나 사내 라이브러리 등 외부 모듈을 우리 코드에 깔끔하게 통합하려면 어떻게 해야할까?

외부 코드 사용하기

java.util.Map의 인터페이스는 clear() 메서드를 지원한다.

즉, Map을 만들어서 여기저기서 사용하면 clear() 또한 누구나 사용할 수 있게 된다.

Map.get()가 리턴하는 객체가 Sensor라는 것을 명시해주기 위해 (Sensor)를 앞에 붙였다. 하지만 이런 방식보다 더 좋은 방식이 있다.

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

이처럼 제네릭을 사용하면 Map이 어떤 타입을 리턴하는지 확실히 알 수 있다.
하지만 사용자가 사용하지 않는 기능까지 전부 지원하고 있는 문제는 해결하지 못한다.

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

이런 점을 방지하기 위해서 Map을 캡슐화해주는 방법이 있다.

publc class Sensors {
    private Map sensors = new HashMap();

    public Sensor GetById(String id) {
        return (Sensor) sensors.get(id);
    }

    // ...
}

물론 매번 캡슐화를 할 필요는 없다.

만약 Map 인스턴스를 여기저기 넘기지 않는다면 위와 같이 굳이 캡슐화를 하지 않고 사용해도 크게 문제될 것은 없을 것이다.

경계 살피고 익히기

외부 모듈을 사용하기 위해서는 문서를 읽으며 사용법을 익혀야 한다.

또한 우리의 의도대로 해당 모듈이 동작하는지 테스트도 해봐야 한다.

이것들을 전부 다 하기엔 오래걸린다. 그 대신 학습 테스트를 하면 효율이 좋다.

학습 테스트

프로그램에서 사용하려는 방식대로 외부 API를 호출해보는 테스트


log4j 익히기

학습 테스트 방식으로 log4j를 익히면 다음과 같이 진행된다.

화면에 "hello" 출력하기

  1. 출력을 담당하는 메서드인 info를 사용해본다. 실행해보면 ConsoleAppender라는 클래스가 필요하다는 오류가 발생한다.
@Test
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}

  1. ConsoleAppender를 추가한다. 이번엔 Appender에 출력스트림이 없다고 뜬다.
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConsoleAppender();
    logger.addAppender(appender);
    logger.info("hello");
}

  1. ConsoleAppenderPatternLayoutConsoleAppender.SYSTEM_OUT을 전달해준다. 이제는 콘솔에 'hello'가 찍힌다.
@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");
}

  1. PatternLayout을 지우면 스트림이 없다는 오류가 뜨지만 ConsoleAppender.SYSTEM_OUT는 제거해도 문제없이 '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");
    }
}

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

사용하는 모듈이 업데이트 되면서 동작이 달라지더라도 테스트를 실행하여 해당 모듈이 예상대로 동작하는지 검증할 수 있다.

이러한 점은 현재 사용하는 모듈의 버전업을 하기 쉬워지는 장점이 있다.


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

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.

모르는 코드를 이해하려고 노력하지 않아도 된다. 우리가 필요한건 해당 기능의 입출력만 알면 된다.

송신기를 예로 들어보자. 송신기를 만드는 팀이 아직 API를 설계하지 않았다면 우리가 자체적으로 인터페이스를 정의해보자.

인터페이스는 주파수와 자료 스트림을 입력으로 받는다. 즉, 우리가 바라는 인터페이스다.

우리가 바라는 인터페이스는 전적으로 우리가 통제한다는 장점이 생긴다.

그러므로 코드의 가독성도 높아지고 코드 의도도 분명해진다.

따라서 우리가 통제하지 못하는 송신기 API에서 CommunicationsController를 분리했다.

또한 저쪽 팀에서 API를 정의한 후에는 TransmitterAdapter를 구현해 간극을 메꾼다.

또한 어댑터 패턴으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한 곳으로 모았다.


이젠 FakeTransmitter 클래스를 사용해서 CommunicationController 클래스를 테스트 할 수 있다.
Transmitter 인터페이스가 구현되면 경계 테스트 케이스를 생성해 우리가 API를 제대로 사용하는지 테스트할 수도 있다.

깨끗한 경계

외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.

새로운 클래스로 경계를 감싸거나, 어댑터 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

어느 방법이든 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.

Last Updated: