경계

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 패키지를 사고, 오픈소스를 이용한다. 때로는 사내 다른 팀이 제공하는 컴포넌트를 사용한다. 외부 코드를 우리 코드에 깔끔하게 통합하기 위해서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 익혀야한다.

외부 코드 사용하기

패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓히려 애쓴다. 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 긴장으로 인해 시스템 경계에서 문제가 생길 소지가 많다.

java.util.Map

Map은 다양한 인터페이스로 수많은 기능을 제공한다. Map이 제공하는 기능성과 유연성은 유용하지만 그만큼 위험도 크다.

Map 캡슐화
public class Sensors {
    private Map<Sensor> sensors = new HashMap<>();

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

경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.
Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다. 그래서 코드를 이해하기 쉽지만 오용하기는 어렵다. Sensors 클래스는 설계 규칙과 비즈니스 규칙을 따르도록 강제할 수 있다.
아래 사항을 지키면 Map 클래스를 사용할 때마다 위와같이 캡슐화할 필요는 없다.

  • Map과 같은 경계 인터페이스를 이용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 말아야 한다.
  • Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 말아야 한다.

학습 테스트

우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 방식

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이다. 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.

log4j 익히기

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

테스트 케이스를 돌렸더니 Appender가 필요하다는 오류가 발생한다. 그래서 ConsoleAppender를 생성한 후 테스트 케이스를 다시 돌린다.

@Test
public void testLogAddAppender() {
	Logger logger = Logger.getLogger("LogTest");
	ConsoleAppender appender = new ConsoleAppender();
	logger.addAppender(appender);
	logger.info("hello");
}

이번에는 Appender에 출력 스트림이 없다는 사실을 발견한다.

@Test
public void testLogAddAppender() {
	Logger logger = Logger.getLogger("LogTest");
    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 버그이거나 일관성 부족으로 여겨진다.

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 경계 인터페이스를 몰라도 된다.

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

투자하는 노력보다 얻는 성과가 더 크다. 필요한 지식만 확보하는 손쉬운 방법이다. 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인한다.
학습 테스트는 패키지가 예상대로 도는지 검증한다. 일단 통합한 이후라고 하더라도 패키지가 우리 코드와 호환되리라는 보장은 없다. 패키지 작성자에게 코드를 변경할 필요가 생길지도 모른다. 새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다. 이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.

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

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다. 때로는 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다. 예를 들어 다른 팀에서 제공해줘야하는 API가 아직 구현되지 않았다면 알려고 해도 알 수가 없다.
일단 우리가 바라는 인터페이스를 정의해보자. 우리가 인터페이스를 전적으로 통제한다는 장점이 생긴다. 또한 코드 가독성도 높아지고 코드 의도도 분명해진다. 추후에 다른 팀이 API를 정의하여 제공한다면 ADAPTER 패턴으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 모으면 된다.

이와 같은 설계는 테스트도 아주 편하다. API 인터페이스가 나온 다음에도 경계 테스트 케이스를 생성해 우리가 API를 올바로 사용하는지 테스트할 수도 있다.

깨끗한 경계

경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다. 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 주의해야 한다.

경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다. 이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다. 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다.

외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. 새로운 클래스로 경계를 감싸거나, ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. 코드가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.

Categories:

Updated:

Comments