피드백, 오타 지적 환영합니다
시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 때로는 패키지를 사고, 떄로는 오픈 소스를 이용한다. 떄로는 사내 다른 팀이 제공하는 컴포넌트를 사용한다. 어떤 식으로든 이 외부코드를 우리 코드에 깔끔하게 통합해야만 한다.
이 장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴본다.
인터페이스 제공자와 인터페이스 사용자 사이에는 특유의 긴장이 존재한다. 패키지 제공자는 적용성을 최대한 넓히려 애쓴다. 더 많은 환경에서 돌아가야 더 많은 고객이 구매하니까.
반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 긴장으로 인해 시스템 경계에서 문제가 생길 소지가 많다.
한 예로 java.util.Map을 살펴보자.
clear() void – Map
containsKey(Object key) boolean – Map
containsValue(Object value) boolean – Map
ntrySet() Set – Map
equals(Object o) boolean – Map
get(Object key) Object – Map
getClass() Class – 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 내용을 지울 권한이 있다는 말이다.
또한 객체 유형을 제한하지 않으므로 마음만 먹으면 사용자는 어떤 객체 유형도 추가할 수 있다. 대신 제네릭스를 사용하면 코드 가독성이 크게 높아진다.
그러나 위 방법도 사용자에게 필요하지 않은 기능까지 제공한다는 문제는 해결하지 못한다.
프로그램에서 Map<String, Sensor> 인스턴스를 여기저기로 넘긴다면 Map 인터페이스가 변할 경우, 수정할 코드가 상당히 많아진다. 실제로 제네릭스 사용을 금지하는 시스템도 보았다. 기존 시스템에서 Map을 너무 많이 사용한 탓에 변경할 코드 역시 너무 많은 탓이었다.
다음은 Map을 좀 더 깔끔하게 사용한 코드다. Sensors 사용자는 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없다. 제네릭스의 사용 여부는 Sensors 안에서 결정한다.
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
// 이하 생략
}
경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map의 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 제네릭스를 사용하든 하지 않든 더 이상 문제가 안 된다.
또한 Sensors는 프로그램에 필요한 인터페이스만 제공한다. 그래서 코드를 이해하기는 쉽지만 오용하기는 어렵다.
Map을 사용할 때마다 위와 같이 캡슐화하라는 소리가 아니다. Map을 여기저기 넘기지 말라는 말이다.
외부코드를 익히기는 어렵다. 외부 코드를 통합하기도 어렵다. 두 가지를 동시에 하기는 두 배나 어렵다. 다르게 접근하면 어떨까? 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히면 어떨까? 이를 짐 뉴커크는 학습 테스트라 부른다.
학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이다. 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.
학습 테스트에 드는 비용은 없다. 어쩄든 API를 배워야 하므로,,,, 오히려 필요한 지식만 확보하는 손쉬운 방법이다. 학습 테스트는 이해도를 높여주는 정확한 실험이다.
학습 테스트를 이용한 학습이 필요하든 그렇지 않든, 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다. 이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.
소프트웨어 설계가 우수한다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 엄청난 시간과 노력과 재작업을 요구하지 않는다. 통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.
경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다. 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.