Study/CleanCode

[클린코드] 8장. 경계

Wonol 2023. 11. 4. 15:59
반응형

클린코드(CleanCode)를 읽고 간략하게 정리한 글입니다.


8장. 경계

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

- 때로는 패키지를 사고, 때로는 오픈 소스를 이용하고, 때로는 사내 다른 팀이 제공하는 컴포넌트를 사용한다.

- 어떤 식으로든 이 외부 코드를 우리 코드에 깔끔하게 통합해야만 한다.

1. 외부 코드 사용하기

- 인터페이스 제공자와 사용자 사이에는 특유의 긴장이 존재한다.

  • 패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓히려 한다.
  • 반대로, 사용자는 자신의 요구에 집중하는 인터페이스를 요구한다.

- 예를 들어, java.util.Map을 확인 할 수 있다.

  • Map 은 굉장히 다양한 인터페이스로 수많은 기능을 제공한다.
  • Map 이 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험도(누구나 clear() 메서드를 사용)도 크다.
Map sensors = new HashMap();
Sensor s = (Sensor) sensors.get(sensorId);

 

- 위와 같은 코드가 한 번이 아니라 여러 차례 사용 된다.

- Map 이 반환하는 Object 를 올바른 유형으로 변환할 책임은 Map을 사용하는 클라이언트에게 있다.

  • 깨끗한 코드라 보기 어렵다.
  • 의도도 분명하게 드러나지 않는다.
Map<String, Sensor> sensors = new HashMap<Sensor>();
...
Sensor s = sensors.get(sensorId);

- 제네릭을 사용하여 위의 문제를 해결하고 코드 가독성을 높였습니다.

- 그렇지만, 이 방법도 사용자에게 필요하지 않은 기능까지 제공하는 문제가 있습니다.

public class Sensors {
    private Map sensors = new HashMap();
    
    public Sensor getById(String id) {
        return (Sensor) sensor.get(id);
    }
}

 

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

  • Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.
  • Sensors 클래스 안에서 객체 유형을 관리하고 변환하기 때문에 제네릭을 사용하든 말든 문제가 안된다.

- Map 클래스를 사용할 때마다 위와 같이 캡슐화를 하라는 말은 아니다.

  • Map(혹은 유사한 경계 인터페이스) 을 여기저기 넘기지 말라는 것이다.
  • Map 과 같은 경계 인터페이스를 사용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다.
  • Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.

2. 경계 살피고 익히기


외부 코드를 익히기는 어렵다. 외부 코드를 통합하기도 어렵다.
두 가지를 동시에 하기는 두 배나 어렵다.

- 외부 패키지 테스트는 우리의 책임이 아니다. 그렇지만 우리 자신을 위해 우리가 사용할 코드를 테스트해야만 한다.

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

3. log4j 익히기

- 로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지를 사용한다고 가정한다.

1) 문서를 자세히 읽기 전에 첫 번째 테스트 케이스를 작성한다.

// 화면에 "hello"를 출력하는 테스트 케이스이다.
@Test
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}

2) 테스트 케이스를 확인한다. -> Appender 라는 뭔가가 필요하다는 오류가 발생한다.

3) 문서를 읽어보면 ConsoleAppender 라는 클래스를 확인할 수 있다. -> Appender에 출력 스트림이 없다는 사실을 발견한다.

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

4) 구글을 검색한 후 아래와 같이 시도한다. -> 정상 확인이 된다.

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

5) 테스트 케이스를 작성하는 과정에서 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");
    }
}

6) 모든 지식을 활용하여 Logger 클래스로 캡슐화한다.

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

- 학습 테스트는 이해도를 높여줄 뿐만 아니라 패키지가 예상대로 동작하는지 검증도 한다.

- 통합한 이후라고 하더라도 패키지가 우리 코드와 항상 호환되리라는 보장은 없다.

- 패키지 새 버전이 나올 때마다 새로운 위험이 생긴다. -> 학습 테스트를 통해 위험을 감지할 수 있다.

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

- 경계와 관련해 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계이다. 때로는 우리 지식이 경계를 넘어 미치지 못하는 코드 영역도 존재한다.

- 지금 알지 못하는 코드(다른 팀에서 아직 구현이 안된 코드)를 구현할 때 자체적으로 인터페이스를 정의한다.

  • 우리가 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제한다는 장점이 생긴다.
  • 코드 가독성도 높아지고 코드 의도도 분명해진다.

- 통제가 불가능한 외부 패키지에 의존하는 것보다는 통제가 가능한 우리 코드에 의존하는 편이 좋다.

6. 깨끗한 경계

- 경계에서는 흥미로운 일이 많이 벌어진다. 대표적으로 변경이다.

- 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않고, 엄청난 시간과 노력과 재작업을 요구하지 않는다.

- 통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 주의한다.

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

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

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

  • 새로운 클래스로 경계를 감싸거나 Adapter 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환한다.
반응형