[CleanCode] -8. 경계

Young Min Sim ·2021년 4월 19일
1

CleanCode

목록 보기
8/16

시스템에 들어가는 모든 SW 를 직접 개발하는 경우는 드물다.
어떤 식으로든 이 외부 코드를 우리 코드에 깔끔하게 통합해야만 한다.
이 장에서는 SW 경계를 깔끔하게 처리하는 기법과 기교를 살펴본다.


1. 외부 코드 사용하기

패키지 제공자/프레임워크 제공자는 (더 많은 환경에서 돌아가야 많은 고객이 구매하니)
적용성을 최대한 넓히려 애쓴다.
반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.

예를 들어 java.util.Map 을 살펴보자.
다양한 인터페이스로 수많은 기능(clear, containsKey, equals, isEmpty ...)
을 제공하는 만큼, 기능성과 유연성은 유용하다.

하지만, 그만큼 위험도 크다.

예를 들어 설계 시 Map에 특정 객체 타입만 저장하기로 결정했다고 가정했다.
하지만 Map 은 객체 유형을 제한하지 않는다. 마음만 먹으면 사용자는 어떤 객체 유형도 추가할 수 있다.

개선 전

아래 코드는 Map 이 반환하는 Object 를 올바른 타입으로 변환할 책임을 Map을 사용하는 클라이언트에게 부여한다.
이렇게 되면 동작은 하지만, 사용할 때마다 변환을 해주어야 하므로 깨끗한 코드라 보기 어렵다.

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

개선 후

경계 인터페이스인 Map 을 Sensor 클래스 안으로 숨긴다.

  • 객체 유형을 관리하고 변환하는 책임을 Sensor 클래스가 갖게 된다.
  • (Map 에 직접 의존하는 것이 아니기 때문에) Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.
  • Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다.
    나머지 프로그램이 설계 규칙과 비즈니스 규칙을 따르도록 강제할 수 있다.
public class Sensors {
    private Map sensors = new HashMap();

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

2. 경계 살피고 익히기

외부 코드를 익히기는 어렵다
외부 코드를 (우리 코드와) 통합하기도 어렵다
-> 두 가지를 동시에 하는 것은 두 배나 어렵다.

다르게 접근하면 어떨까?

우리쪽 코드를 작성해 외부 코드를 호출하는 대신,
먼저 간단한 테스트 케이스를 작성해 외부 코드를 익혀보면 어떨까?

이를 '학습 테스트' 라고 부른다.
통제된 환경에서 API 를 제대로 이해하는지를 확인하는 것으로,
API를 사용하려는 목적에 초점을 맞춘다.

API의 많은 기능 중 사용하려는 기능에 맞춰 테스트케이스를 작성해가며
사용할 기능들을 이해하가는 과정이라고 이해했습니다.


3. log4j 익히기

(로깅과 관련된 라이브러리)

1단계

화면에 "hello"를 출력하는 테스트 케이스이다.

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

2단계

돌렸더니 Appender 라는 뭔가가 필요하다는 오류가 발생한다.
문서를 읽어보고 ConsoleAppender 를 생성한 후 테스트케이스를 다시 돌린다.

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

3단계

이번에는 출력 스트림이 없다는 사실을 발견한다.
구글을 검색한 후 다음과 같이 시도하고 그제서야 제대로 돌아감을 확인한다.

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

4단계

이렇게 얻은 지식들을 아래와 같이 간단한 단위 테스트 케이스 몇 개로 표현한다.

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


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

학습 테스트는 패키지가 예상대로 도는지 검증한다.
새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.

이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.
그렇지 않다면 낡은 버전을 필요 이상으로 오랫동안 사용하려는 유혹에 빠지기 쉽다.


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

필요로 하는 모듈이 완성되지 않아도 개발을 진행할 수 있어야 한다.

저자의 사례)

무선 통신 시스템에 들어갈 SW개발에 참여할 당시,
송신기(Transmitter) 라는 하위 시스템이 있었는데 인터페이스도 정의하지 못한 상태
프로젝트 지연을 원치 않았기에 송신기 하위 시스템과 아주 먼 부분부터 작업하기 시작

나중에 그 경계에 부딪히기 시작하자
(저쪽 팀이 아직 API를 설계하지 않았으므로) 구현을 나중으로 미루고 인터페이스부터 정의

현재 원하는 기능이 '지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송'이므로
Transmitter 인터페이스는 주파수와 자료 스트림을 입력으로 받게 함

(통제하지 못하고 정의되지도 않은) 송신기 API 에서 CommunicationsController 를 분리하여
CommunicationsController 는 깔끔하고 깨끗했다.

또한 저쪽 팀이 송신기 API 를 정의한 후에는
ADAPTER 패턴으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한 곳으로 모았다.

이와 같은 설계는

  • FakeTransmitter 클래스를 사용하면 CommunicationsController 클래스를 테스트 할 수 있고,
  • Transmitter API 인터페이스가 나온 다음 경계 테스트 케이스를 생성해 우리가 API를 올바르게 사용하는지 테스트할 수 있다.

변경으로 인한 영향을 최소화할 수 있는 장점도 있다고 생각합니다.


6. 깨끗한 경계

  • 경계에 위치하는 코드는 깔끔히 분리한다.

    • 이 쪽 코드에서 외부 패키지를 세세하게 알 필요가 없다.
    • 즉, 통제가 가능한 외부 패키지보다는 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다.
  • 외부 패키지를 직접 호출하는 코드를 가능한 줄여 경계를 관리하자

    • (Map 에서 봤듯이) 새로운 클래스로 경계를 감싸거나
    • ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자

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

0개의 댓글