본문으로 바로가기

[Clean Code] 8장 경계

category Prodo 독서 리뷰 2021. 3. 26. 17:31

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 

때로는 패키지를 사고 오픈 소스를 이용한다.

어떤 식으로든 이 외부 코드와 우리 코드의 경계를 깔끔하게 처리하고 통합해야 한다.

 

1. 외부 코드 사용하기

패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓혀 더 많은 환경에서 돌아가길 원한다.

하지만 사용자는 자신의 요구에 집중하는 인터페이스를 더 원한다.

 

예로 java.util.Map을 살펴보자. Map이 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험도 크다.

  • 넘기는 쪽에서는 Map 내용을 삭제하지 않으리라 믿을 수 있지만, Map이 제공하는 메서드 중 Clear()가 존재한다. 즉, Map 사용자라면 누구나 Map 내용을 지울 수 있다.
  • 어떤 클래스를 저장하는 Map 객체를 사용한다면 제네릭스나 Warpper 클래스를 통해 관리해야 한다.
// 1. 기본 형태
// 캐스팅의 부담을 안게 된다.
Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);

// 2. generic
// Map 객체가 필요 이상의 기능을 제공하는 것은 막지 못한다.
Map<String, Sensor> sensors = new HashMap<String, Sensor>();
Sensor s = sensors.get(sensorId);

// 3. Wrapping
// 경계의 인터페이스(이 경우에는 Map의 메서드)는 숨겨진다.
// Map의 인터페이스가 변경되더라도 여파를 최소화할 수 있다. 예를 들어 Generic을 사용하던 직접 캐스팅하던 그건 구현 디테일이며 Sensor클래스를 사용하는 측에서는 신경쓸 필요가 없다.
// 이는 또한 사용자의 목적에 딱 맞게 디자인되어 있으므로 이해하기 쉽고 잘못 사용하기 어렵게 된다.
public class Sensors {
    private Map sensors = new HashMap();

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

 

2. 경계 살피고 익히기

외부에서 가져온 패키지를 사용하고 싶다면 어디서 어떻게 시작해야 좋을까?

  1. 타사 라이브러리를 가져와 사용법이 분명치 않으니 대개는 하루나 이틀 문서를 읽으며 사용법을 결정한다.
  2. 우리 쪽 코드를 작성해 라이브러리가 예상대로 동작하는지 확인한다.
  3. 때로는 우리 버그인지 라이브러리 버그인지 찾아내느라 오랜 디버깅으로 골치를 앓는다.

 

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

다르게 접근해보자.

 

곧바로 우리 쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 학습한다.

이를 학습 테스트라 부른다.

  • 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. (통제된 환경에서 API를 제대로 이해하는지를 확인하려는 셈이다.)
  • 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.
  • 학습 테스트는 이해도를 높여주는 정확한 실험이다.
// 1. log4j 라이브러리를 다운받자.
// "hello"가 출력되길 바라면서 아래의 테스트 코드를 작성해보자.
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}

// 2. "Appender라는 게 필요하다"라는 에러를 뱉는다.
// 조금 더 읽어보니 ConsoleAppender라는 게 있는 걸 알아냈다.
// 그래서 ConsoleAppender라는 객체를 만들어 넣어줘봤다.
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConsoleAppender();
    logger.addAppender(appender);
    logger.info("hello");
}

// 3. "Appender에 출력 스트림이 없다"라고 한다.
// 이상하다. 가지고 있는 게 이성적일 것 같은데...
// 구글의 도움을 빌려, 다음과 같이 해보았다.
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");
}

// 성공했다.
// 하지만 ConsoleAppender를 만들어놓고 ConsoleAppender.SYSTEM_OUT을 받는 건 이상하다.
// 그래서 빼봤더니 잘 돌아간다.
// 하지만 PatternLayout을 제거하니 돌아가지 않는다.
// 그래서 문서를 살펴봤더니 "ConsoleAppender의 기본 생성자는 unconfigured 상태"란다.
// 명백하지도 않고 실용적이지도 않다... 버그이거나, 적어도 "일관적이지 않다"라고 느껴진다.

// 조금 더 구글링, 문서 읽기, 테스트를 거쳐 log4j의 동작법을 알아냈고 그것을 간단한 유닛 테스트로 기록했다.
// 이제 이 지식을 기반으로 log4j를 래핑 하는 클래스를 만들 수 있다.

 

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

때로는 우리 지식이 경계를 너머 미치치 못하는 코드 영역도 있다.

알려고 해도 알 수가 없다. 때로는 더 이상 내다보지 않기로 결정한다.

 

  • 저자는 무선통신 시스템을 구축하는 프로젝트를 하고 있었다.
  • 그 팀 안의 하부 팀으로 송신기를 담당하는 팀이 있었는데 나머지 팀원들은 송신기에 대한 지식이 거의 없었다.
  • 송신기팀은 인터페이스를 제공하지 않았다. 하지만 저자는 송신기팀을 기다리는 대신 원하는 기능을 정의하고 인터페이스로 만들었다.
    • 원하는 기능: 지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라
  • 인터페이스를 정의함으로써 메인 로직을 더 깔끔하게 짤 수 있었고 목표를 명확하게 나타낼 수 있었다.
  • Adapter Pattern으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한 곳으로 모았다.
  • API 인터페이스가 나온 다음 경계 테스트 케이스를 생성해 우리가 API를 올바르게 사용하는지 테스트할 수도 있다.

 

4. 깨끗한 경계

소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.

  • 경계에 위치하는 코드는 깔끔히 분리하고 기대치를 정의하는 테스트 케이스도 작성한다.
  • 이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다.
  • 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 좋다.
  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
  • 새로운 클래스로 경계를 감싸거나 Adapter 패턴을 통해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

'Prodo 독서 리뷰' 카테고리의 다른 글

[Clean Code] 11장 시스템  (0) 2021.03.26
[Clean Code] 9장 단위 테스트  (0) 2021.03.26
[Clean Code] 7장 오류처리  (0) 2021.03.26
[Clean Code] 6장 객체와 자료 구조  (0) 2021.03.26
[Clean Code] 5장 형식 맞추기  (0) 2021.03.26