on
클린코드 (Clean Code) 8장. 경계
클린코드 (Clean Code) 8장. 경계
시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드뭅니다.
때로는 패키지를 사고, 때로는 오픈 소스를 이용합니다. 때로는 사내 다른 팀이 제공하는 컴포넌트를 사용하기도 합니다.
어떤 식으로든 이 외부 코드를 우리 코드에 깔끔하게 통합해야만 합니다.
이번 장에서는 소프트웨어 경계를 깔끔하게 처리하는 방법에 대해 알아봅니다.
외부 코드 사용하기
패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓히려 애쓴다.
더 많은 환경에서 돌아가야 더 많이 사용할 테니까.
반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 간극으로 인해 시스템 경계에서 문제가 생길 소지가 많다.
예로 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 - Object
hasCode() 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을 만들어 여기저기 넘긴다고 가정하자.
넘기는 쪽에서는 아무도 Map 내용을 삭제하지 않으리라 믿을지도 모르겠다.
하지만 Map 사용자라면 누구나 Map 내용을 지울 권한이 있다.
왜냐하면 Map은 clear() 메서드를 제공하기 때문이다.
또는 Map에 특정 객체 유형만 저장하기로 결정했다고 가정하자.
하지만 Map은 객체 유형을 제한하지 않는다. 마음만 먹으면 사용자는 어떤 객체 유형도 추가할 수 있다.
이런 경우를 대비해서 캡슐화해서 사용하기도 한다.
public class Sensors { private Map sensors = new HashMap(); public Sensor getById(String id){ return (Sensor) sensors.get(id); } // 생략... }
경계 인터페이스인 Map을 Sensors안으로 숨긴다. (캡슐화)
따라서 Map인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.
제네릭스를 사용하든 하지 않든 더 이상 문제가 되지 않는다.
Sensors 클래스 안에서 객체 유형을 관리하고 변환하기 때문이다.
또한 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다.
그래서 코드는 이해하기 쉽지만 오용하기는 어렵다.
Map 클래스를 사용할 때마다 위와 같이 캡슐화하라는 소리가 아니다.
Map( 혹은 유사한 인터페이스를 )을 여기저기 넘기지 말라는 말이다. Map과 같은 경계 인터페이스를 이용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다.
경계 살피고 익히기
외부 코드를 사용하면 적은 시간에 더 많은 기능을 출시하기 쉬워진다.
만약 외부에서 가져온 패키지를 사용하고 싶다면 어디서 어떻게 시작해야 좋을까? 외부 패키지 테스트가 우리 책임은 아니다.
하지만 우리 자신을 위해 우리가 사용할 코드를 테스트하는 편이 바람직하다.
타사 라이브러리를 가져왔으나 사용법이 분명치 않다고 가정하자.
대개는 하루나 이틀 (아니면 더 오랫동안) 문서를 읽으며 사용법을 결정한다.
그런 다음 우리 쪽 코드를 작성해 라이브러리가 예상대로 동작하는지 확인한다. 때로는 우리 버그인지 라이브러리 버그인지 찾아내느라 오랜 디버깅으로 골치를 앓는다.
외부 코드를 익히기는 어렵다. 외부 코드를 통합하기도 어렵다. 두 가지를 동시에 하기는 두 배나 어렵다.
다른 방식으로 접근해보자. 곧바로 우리 쪽 코드를 작성하는 대신, 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것이다.
짐 뉴커크는 이를 학습 테스트라고 부른다.
어쨌든 API를 배워야 하므로 학습 테스트에 드는 비용은 없다. 오히려 필요한 지식만 확보하는 손쉬운 방법이다.
학습 테스트는 패키지가 예상대로 도는지 검증한다. 일단 통합한 이후라고 하더라도 패키지가 우리 코드와 호환되리라는 보장은 없다.
패키지 작성자에게 코드를 변경할 필요가 생길지도 모른다. 패키지 작성자는 버그를 수정하고 기능을 추가한다.
패키지 새 버전이 나올 때마다 위험이 생긴다.
새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.
깨끗한 경계
경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예이다.
소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 엄청난 시간과 노력과 재작업을 요구하지 않는다.
통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.
경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.
이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다.
통제 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 좋다.
자칫하면 오히려 외부 코드에 휘둘리고 만다.
from http://bang-jh.tistory.com/16 by ccl(A) rewrite - 2021-09-22 00:27:24