티스토리 뷰

CS/Clean Code

CS Clean Code - 경계

강태종 2022. 3. 10. 02:58

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 패키지를 사고, 오픈 소스를 이용하고, 사내 다른 팀이 제공하는 컴포넌트를 사용한다. 어떤 식으로든 이 외부 코드를 우리 코드에 깔끔하게 통합해야만 한다.

 

외부 코드 사용하기

패키지 제공자는 적용성을 최대한 넓히려 애쓴다. 더 많은 환경에서 돌아가야 더 많은 고객이 구매하니까. 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 긴장으로 인해 시스템 경계에서 문제가 생긴다.

 

대표적인 예로 java.util.Map을 살펴보자. Map은 다양한 인터페이스로 많은 기능을 제공한다. Map이 제공하는 기능과 유연성은 확실히 유용하지만 그만큼 위험도 크다.

  • Map에는 clear, set, remove 함수를 제공하기 때문에 Map을 매개 변수로 넘길 때 해당 함수가 Map 내용을 수정할 권한이 있다는 말이다.
  • Map에서 제공하는 인터페이스가 변경된 경우 Map을 사용하는 모든 코드에서 변경이 발생한다. 실제로 Java 5가 제네릭을 지원하면서 Map 인터페이스가 변했다.

 

다음은 Map을 깔끔하게 사용한 코드다.

  • Map을 Sensors 안으로 숨기기 때문에 Map 인터페이스가 변하더라도 나머지 프로그램에 미치는 영향이 적다.
  • Sensors는 필요한 인터페이스만 제공하기 때문에 오용하기 어렵다.
public class Sensors {
    private Map sensors = new HashMap();

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

 

 

경계 살피고 익히기

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

 

외부 코드를 익히기는 어렵다. 외부 코드를 통합하기도 어렵다. 두 가지를 동시에 하기는 두 배나 어렵다. 곧바로 우리 코드를 작성해 외부 코드를 호출하는 대신 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것은 어떨까? 이를 학습 테스트라 부른다.

 

첫 번째 테스트 케이스를 작성한다. 화면에 "hello"를 출력하는 테스트 케이스다. 테스트 케이스를 돌렷더니 Appender라는 뭔가가 필요한다는 오류가 발생한다.

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

 

공식 문서를 읽어보니 ConsoleAppender라는 클래스가 있다. 그래서 ConsoleAppender를 생성한 후 테스트 케이스를 다시 돌린다. 이번에는 Appender에 출력 스트림이 없다는 사실을 발견한다.

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

 

이상하다. 출력 스트림이 있어야 정상이 아닌가? 구글을 검색한 후 다음과 같이 시도한다. 이제서야 제대로 돌아간다.

@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.removeAppAppenders();
    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를 배워야 하므로 오히려 필요한 지식만 확보하는 손쉬운 방법이다. 학습 테스트는 이해도를 높여주는 정확한 실험이다.

 

패키지 작성자는 버그 수정이나 기능 추가로 코드를 변경할 필요가 생길지도 모른다. 코드가 변경되면 일단 통합한 이후라도 우리 코드와 호환되리라는 보장은 없다. 새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다. 이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.

 

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

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다. 때로는 우리 지식이 경계 너머 미치지 못하는 코드 영역도 있다.

몇 년 전 무선통신 시스템에 들어갈 소프트웨어 개발에 참여했다. 여기에 대한 지식이 거의 없었고, '송신기' 시스템을 책임진 사람들은 인터페이스도 정의하지 못한 상태였다. 우리는 '송신기' 하위 시스템과 아주 먼 부분부터 작업하기 시작했다.

점차 우리에게 필요한 경계 인터페이스가 뭔지 알게 되었다. `지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라.`

저쪽 팀이 아직 API를 설계하지 않았으므로 우리는 구현을 나중으로 미뤘다. 이쪽 코드를 진행하고자 자체적으로 인터페이스를 정의했다.

우리가 통제하지 못하며 정의되지도 않은 CommunicationController를 분리하고 Transmitter라는 인터페이스를 정의했다.

 

우리가 바라는 인터페이스를 구현하면 여러 장점이 있다.

  • 우리가 인터페이스를 전적으로 통제한다.
  • 코드 가독성도 높아지고 코드 의도도 분명해진다.
  • Adapter 패턴으로 API 사용을 캡슐화하여 API가 바뀔 때 수정할 코드를 한곳으로 모았다.
  • Fake Transmitter를 사용하면 테스트도 아주 편하다.

 

깨끗한 경계

경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다. 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않는다.

  • 경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.
  • 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다.

 

느낀점

외부 라이브러리를 사용하며 프로젝트를 진행할 때 처음에는 정말 사용하기 쉽다 생각하며 경계없이 사용했습니다. Paging, ViewPager 같이 숫자가 바뀌면서 급격하게 변하는 라이브러리를 볼 때 인터페이스를 감싸야 하나 생각도 했지만, 많은 시간이 들기 때문에 바로 사용했습니다. 그 결과 오랜 시간이 지나고 build.gradle을 볼 때 새로운 버전이 나왔다는 메시지가 많지만 버전을 올렸을 때 생기는 이슈가 무서워 오래된 버전을 오랫동안 사용하게 되었습니다.

외부 라이브러리를 인터페이스로 감싸는 작업은 많은 비용이 들지만, 그렇지 않았을 때 생기는 유지 보수 비용은 훨씬 클 것입니다.

 

 

'CS > Clean Code' 카테고리의 다른 글

CS Clean Code - 클래스  (0) 2022.03.17
CS CleanCode - 단위 테스트  (0) 2022.03.15
CS Clean Code - 오류처리  (0) 2022.03.08
CS Clean Code - 객체와 자료 구조  (0) 2022.03.03
CS Clean Code - 형식 맞추기  (0) 2022.02.23
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함