쾌락코딩

클린코드_경계

|

개인프로젝트를 제외하면 우리는 거의 대부분 오픈소스를 이용하거나 패키지를 사서 사용한다. 때로는 사내 다른 팀이 제공하는 컴포넌트를 사용한다. 프로젝트를 위해 내가 작성하는 코드와 타인이 작성한 코드사이의 경계, 이 경계를 깔끔하게 처리하는 방법에 대한 내용이다.

외부 코드 사용하기

패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓히려 애쓴다. 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 경계의 온도차를 잘 극복해야 한다.

한 예로, java.util.Map을 살펴보자. Map은 아래와 같이 굉장히 다양한 인터페이스로 수많은 기능을 제공한다.

  • clear() void - map
  • containsKey(Object key) boolean - Map
  • containsValue(Object value) boolean - Map
  • entrySet() set - Map
  • equals(Object o) boolean - Map
  • get(Object key)Object - Map
  • getClass() Class<? extends Object> - Object
  • hashCode() int - Map
  • isEmpty() boolean - Map
  • keySet() Set - Map
  • notify() void - Object
  • notifyAll() void - Object
  • put(Object key, Object value) Object - Map
  • putAll(Map t) void - Map
  • remove(Object key) Object - Map
  • size() int - Map
  • toString() String - Object
  • values() Collection - Map
  • wait() void - Object
  • wait(long timeout) void - Object
  • wait(long timeout, int nanos) void - Object

기능이 많다고 무조건 좋은 것은 아니다. 예를 들어 우리 팀에서는 Map의 모든 데이터를 지울수 있는 claer메서드를 불필요하고 위험하다고 여긴다. 그러나 Map은 누구에나 clear메서드를 제공한다.

조금 더 살펴보자.

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

위 코드는 제네릭을 사용함으로써 깔끔해보이지만 사용자에게 필요하지 않은 기능까지 제공한다. 또한 프로그램에서 Map<String,Sensor>인스턴스를 여기저기로 넘긴다면, Map인터페이스가 변할 경우, 수정할 코드가 상당히 많아진다. 인터페이스가 변할 가능성이 거의 없다고 여길지도 모르지만, 자바 5가 제네릭스를 지원하면서 Map 인터페이스가 변했다는 사실을 명심해야한다.

아래는 Map을 좀 더 깔끔하게 사용한 코드다.

public class Sensors {
    private Map sensors = new Hashmap();

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

경계 인터페이스인 MapSensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. Sensors 클래스만 적절히 대응해주면 된다.

Sensors를 사용하는 입장에서는 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없다. 제네릭스를 사용할지 말지는 Sensors에서 결정하면 될 일이다.

또한 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다. 위에서 고민했던 clear메서드는 Sensors에서는 찾아볼 수 없다.

**Map 클래스를 사용할 때마다 위와 같이 캡슐화 하라는 소리가 아니다. Map을 여기저기 넘기지 말라는 말이다. 즉, 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 하자.

경계 살피고 익히기

외부 코드를 처음 사용하려고 할때 어떻게 시작해야하는지, 어떻게 익히는게 좋은지 알아보자.

외부 패키지 테스트가 우리 책임은 아니다. 하지만 우리 자신을 위해 우리가 사용할 코드를 테스트하는 편이 바람직하다.

학습 테스트 : 외부 코드에 대한 문서를 읽으며 사용법을 결정한 후, 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것.

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

적용 예시

로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지를 사용하려 한다고 가정하자. 먼저 문서를 열어 자세히 읽기 전에 첫 번째 테스트 케이스를 작성한다.

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

테스트 케이스를 돌렸더니 Appender라는 것이 필요하다는 오류가 발생한다. 문서를 더 읽어본다. 더 읽어보니 ConsoleAppender라는 클래스가 있다. 그래서 Console Appender를 생성한 후 테스트 케이스를 다시 돌린다.

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

이제 잘 돌아간다(책에서는 이 코드에서 수상한 점을 해결해 나가는 방법도 소개하지만, 그 부분은 위의 단계를 하며 익힐 수 있는 단계라 넘어가겠다).

이 과정을 거치며 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가 제공하는 인터페이스를 몰라도 된다(log4j 인터페이스가 변한다면 독자적으로 만든 클래스만 수정하자).

결론

  • 외부 API를 사용할 때 해당 부분 TEST CODE를 작성하며 익히고 사용하자.
  • 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하게 하자(외부 패키지를 캡슐화해서 우리 패키지인 것 처럼 사용하게 하자).

Comments