클린코드 8장 - 경계
CleanCode 8장 경계
에 대해 정리한 포스트입니다.
Overview
시스템에 들어가는 소프트웨어를 직접 다 개발하는 경우는 드뭅니다.
- 구매한 패키지, 오픈소스 등을 사용
- 사내 다른 팀이 제공하는 컴퍼넌트를 사용
이 장에서는 소프트웨어 경계 를 깔끔하게 처리하는 기법과 기교를 설명합니다.
외부 코드 사용하기
인터페이스 제공자 와 인터페이스 사용자 사이에는 입장차가 있습니다.
- 제공자: 적용성을 최대한 넓히려 함
- 사용자: 자신의 요구에 집중하는 인터페이스를 바람
이러한 차이로 인해 시스템 경계에서 문제가 생길 소지가 많습니다.
예시 - java.util.Map
- clear() void - map
- containsKey(Object key) boolean - Map
- containsValue(Object value) boolean - Map
- entrySet() set - Map
- equals(Object o) boolean - Map
- get(Object key)Object - Map
- getClass() Class<? extends Object> - Object
- hashCode() int - Map
- isEmpty() boolean - Map
- keySet() Set - Map
- notify() void - Object
- notifyAll() void - Object
- put(Object key, Object value) Object - Map
- putAll(Map t) void - Map
- remove(Object key) Object - Map
- size() int - Map
- toString() String - Object
- values() Collection - Map
- wait() void - Object
- wait(long timeout) void - Object
- wait(long timeout, int nanos) void - Object
Map은 굉장히 다양한 인터페이스로 수많은 기능을 제공합니다. 이는 굉장히 유용하지만 그만큼 위험도 클 수 있습니다.
예를 들어:
- Map 을 만들어 인수나 반환으로 이리 저리 넘길 경우 clear() 메서드 사용으로 어디선가 예상치 않게 지울 수도 있다.
- Map 에 특정 객체 유형만 저장하기로 결정했을 때, Map(Generics 지원 이전의)은 유형을 제한하지 않기 때문에 사용자가 예상치 않은 객체를 추가할 위험도 있다.
-
물론 반환 받아서 형번환을 하지 않을 가능성도 있다.
Map sensors = new HashMap(); Sensor s = (Sensor)sensors.get(sensorId);
-
이런 경우는 Generic이 지원된 이후에 해결되었다.
Map<String, Sensor> sensors = new HashMap<String, Sensor>(); Sensor s = sensors.get(sensorId);
-
-
Map의 interface가 변경될 경우 수정할 코드가 너무 많아 질 수 있다.
Generics 지원으로 인해 Map의 인터페이스가 변경되었다는 사실을 명심하자.
위의 문제들은 객체를 감싸는 방법으로 해결이 가능합니다.
- Sensors 사용자는 외부 라이브러리를 어떤 식으로 사용하는지 신경쓰지 않아도 됨
- 필요하지 않는 인터페이스(clear()와 같은)를 제공하지 않을 수 있음
public class Sensors {
private Map sensors = new Hashmap();
public Sensor getById(string id) {
return (sensor) sensors.get(id);
}
}
역시나 Map을(혹은 유사한 경계 인터페이스를) 여기저기 넘기지 않는 것이 가장 중요합니다.
- 공개 API의 인수로 넘기거나 반환값으로 사용하지 않도록 주의, 즉 이러한 인터페이스를 이용하는 클래스 밖으로 노출되지 않도록 주의
하지만, 피치 못하게 노출될 경우에 캡슐화를 통해 설계 규칙을 지키도록 강제하는 것이 좋습니다.
경계 살피고 익히기
타사 라이브러리를 처음 가져왔을 때 사용법이 분명치 않다고 가정하면:
- 하루나 이틀(혹은 더더더) 문서를 읽으며 사용법을 결정
- 코드를 작성해 라이브러리가 예상대로 동작하는지 확인
- 디버깅(의 반복)
위와 다르게 학습 테스트 를 통해 외부 코드를 익히고 통합하는 어려운 일을 쉽게 할 수 있다.
- 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익힘
- 학습 테스트 는 프로그램에서 사용하려는 방식대로 외부 API를 호출(API를 사용하려는 목적에 초점을 맞춘다.)
예시 - log4j 익히기
로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지를 사용하려 한다고 가정하자.
- 패키지를 내려 받아 소개 페이지를 연다.
-
문서를 자세히 읽기 전에 첫 번째 테스트 케이스를 작성한다.
// 화면에 "hello"를 출력하는 테스트 케이스이다. @Test public void testLogCreate() { Logger logger = Logger.getLogger("MyLogger"); logger.info("hello"); }
- 테스트 케이스를 돌려본다.
- Appender라는 뭔가가 필요하다는 오류가 발생한다.
-
문서를 읽어보니 ConsoleAppender 라는 클래스가 있다. 그래서 ConsoleAppender 를 생선한 후 테스트 케이스를 다시 돌린다.
@Test public void testLogAddAppender() { Logger logger = Logger.getLogger("MyLogger"); ConsoleAppender appender = new ConsoleAppender(); 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"); }
잘 돌아간다.
-
테스트 케이스를 짜는 과정에서 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"); } }
- 모든 지식을 통해 Logger 클래스로 캡슐화한다.
학습 테스트는 공짜 이상이다
학습 테스트 는 드는 비용은 없지만 필요한 지식만 확보할 수 있는 손쉬운 방법입니다.
추가적으로 아래의 이점도 있습니다.
- 패키지가 예상대로 도는지 검증하고, 통합 이후에도 주기적으로 검증이 가능
- 패키지의 새 버전이 나오면 학습 테스트만 돌려 차이가 있는지만 확인하면 동작을 보장할 수도 있음
- 호환되지 않을 경우 코드를 수정하든 패키지를 수정하든 조치를 미리 취할 수 있음
- 경계 테스트와 함께라면 버전 변경도 두렵지 않다.
- 필요 이상으로 낡은 버전을 사용하려는 유혹에서 빠져나올 수 있음
아직 존재하지 않는 코드를 사용하기
지금 알지 못하는 코드 영역을 개발할 때도 경계는 유용하게 쓰일 수 있습니다.
- 필요한 인터페이스를 정의/구현하면 전적으로 통제가 가능해짐
- 테스트도 간편하게 진행할 수 있음
깨끗한 경계
변경이 이루어질 때 경계를 통해 통제하고 있었다면 향후 변경으로 인해 발생하는 비용이 줄어들 수 있습니다.
- 경계에 위치하는 코드는 깔끔히 분리
- 기대치를 정의하는 테스트 케이스 작성
통제가 불가능한 외부 패키지 에 의존하는 것보다는 통제가 가능한 우리 코드에 의존 하는 편이 좋습니다.