본문 바로가기
IT 공부/Clean Code

Clean Code 요약해보기 (7)

by 수박한암살자 2022. 5. 26.

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

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

이 장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴본다.


외부 코드 사용하기

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

제공자는 더 넓은 환경에 적용시키고 싶어하고, 사용자는 자신의 요구에 집중하길 원하니까.

 

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

이 Map을 코드 여기저기서 쓴다고 생각하자. 별 일 없는 경우가 많겠지만

Map의 첫 번째 메소드로 clear 메소드가 있다. 즉, 누구나 Map 사용자라면 Map 내용을 지울 권한이 있다.

 

// 이런 코드를 많이 쓴다.
Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);

// generics로 바꿔보자
Map<String, Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId);

아래 코드의 경우에도 Map<String, Sensor>가

사용자에게 필요하지 않은 기능까지 제공한다 라는 문제는 해결하지 못한다.

 

public class Sensors {
    private Map sensors = new HashMap();
    
    public Sensor getById(Stirng id) {
        return (Sensor) sensors.get(id);
    }
    
    // 이하 생략
}

이렇게 하면 경계 인터페이스인 Map을 Sensors 안으로 숨긴다.

따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.

 

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

Map을(혹은 유사한 경계 인터페이스를) 여기저기 넘기지 말라는 말이다.

 

경계 살피고 익히기

외부 패키지 테스트가 우리 책임은 아니지만, 우리를 위해 테스트하는 편이 바람직하다.

 

타사 라이브러리를 가져왔으나 사용법이 분명치 않다고 가정하자.

대개는 하루나 이틀 문서를 읽으며 사용법을 결정하고 우리 쪽 코드를 작성해 라이브러리가

예상대로 동작하는지 확인한다. 때로는 우리 버그인지 라이브러리 버그인지 찾아내느라 오랜 디버깅으로

골치를 앓는다. 이런 상황은 그리 놀랍지도 않다.

 

외부 코드를 익히기도 어렵고 통합하기도 어렵다. 그렇다면 간단한 테스트 케이스를 작성해 익혀보자.

이를 짐 뉴커크는 학습 테스트라 부른다.

 

log4j 익히기

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


// 테스트 케이스를 돌렸더니 Appender라는 뭔가가 필요하다는 오류가 발생한다.
// ConsoleAppender를 생성한 후 다시 TC
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConosoleAppender();
    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");
}


// 정상적이지만 ConsoleAppender에게 콘솔에 쓰라고 알려야 한다니 이상함
// ConsoleAppender.SYSTEM_OUT 인수를 제거하니 문제가 없다.
// PatternLayout을 제거하니 다시 출력 스트림이 없다는 오류가 뜬다.
// ConsoleAppender 생성자는 '설정되지 않은' 상태라고 한다.

// 최종적인 단위 테스트
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("addAppenderWithStream");
    }
}

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

학습 테스트에 드는 비용은 없다. 투자하는 노력보다 얻는 성과가 크다.

새 버전이 나와도 테스트 케이스로 바로 호환 여부도 알 수 있다.

학습 테스트가 없다면 낡은 버전을 필요 이상으로 오랫동안 사용하려는 유혹에 빠지기 쉽다.

 

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

경계와 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.

글쓴이는 '송신기' 모듈을 사용하는 과정에서 아래와 같은 요구 사항이 있었다.

 

'적절한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라'

 

아직 상대팀이 API를 설계하지 않았으므로 구현은 미뤘다.

하지만 인터페이스를 미리 정의하고 간단한 클래스와 메서드 등을 추가했다.

나중에 상대팀이 해당 인터페이스에 맞게 구현해줬고

이로인해 코드 가독성과 의도가 높아지는 효과를 얻었다고 한다.

 

깨끗한 경계

경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대리츨 정의하는 테스트 케이스도 작성한다.

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

Map에서 봤듯이, 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해

우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

반응형

'IT 공부 > Clean Code' 카테고리의 다른 글

Clean Code 요약해보기 (9)  (0) 2022.06.06
Clean Code 요약해보기 (8)  (0) 2022.06.01
Clean Code 요약해보기 (6)  (0) 2022.05.18
Clean Code 요약해보기 (5)  (0) 2022.05.11
Clean Code 요약해보기 (4)  (0) 2022.05.01

댓글