개요
- 개발을 하면서 모든 기능을 직접 개발하는 경우는 거의 불가능이다.
- 때때로는 패키지를 사기도하고, 오픈 소스를 사용하는 경우도 많다.
- 이번 장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴보려고 한다.
외부 코드 사용하기
- 인터페이스 제공자와 인터페이스 사용자 사이의 경계를 정확히 파악하는 것은 중요하다.
- 패키지 제공자는 다양한 환경에서 고객의 요구사항을 충족시키기 위해서 적용성을 최대한 넓히려고 노력한다.
- 반면에 사용자는 자신의 요구에 집중할 수 있는 인터페이스를 원한다.
- 정리 하자면, 제공자는 폭넓게 제공하고 사용자는 자신의 요구사항에 맞는(폭 좁게) 기능을 제공받기를 원하기 때문에 이 둘간의 간극으로 인해서 문제가 생길 소지가 있다.
외부 코드의 대표적인 예시 -> java.util.Map
- Map 은 대표적인 외부 코드이다.
- 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
- 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의 clear() 메서드는 Map의 사용자라면 누구나 Map 내용을 지울 권한이 있다.
- 뿐만아니라, Map에는 객체 유형을 제한하지 않기 때문에 어떤 타입이든 다 넣을 수 있다.
Map sensors = new HashMap();
Sensor sensor = (Sensor) sensors.get(sensorId);
- 위의 코드에서 올바른 타입으로 객체를 반환하는 책임은 Map 을 사용하는 클라이언트에게 있다.
- 그렇기 때문에 제네릭을 사용해서 아래와 같이 수정하는 것이 좋다.
Map<String, Sensor> sensors = new HashMap<Sensor>();
Sensor sensor = sensors.get(sensorId);
- 하지만 더 좋은 방법은 아래와 같이 캡슐화를 하는 것이다.
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 도 정의 되어있지않고, 내가 잘 아는 분야도 아닌 영역을 개발해야한다면 어떻게 해야할까?
- 모든 API 가 정의되고 내가 그 분야를 완벽히 이해할 때까지 개발을 멈춰야할까? -> 당연히 아니다.
- 예를들어, 내가 잘알지 못하는 송신기 기능에 대해서 개발을 해야한다고 가정하자.
- 먼저, 만들어지기 바라는 송신기 인터페이스를 자체적으로 정의한다.
- 그 이후에 송신기 기능에 대한 API 가 정의된 후에 Adapter를 구현를 구현해서 내가 구현한 인터페이스와 정의된 API 사이의 간극을 매운다.
- 그리고 API 사용을 캡슐화 해 API가 바뀔 때 수정할 코드를 한곳으로 모은다.
- 이와 같은 설계는 테스트에도 아주 용이하다.
깨끗한 경계
- 경계에서는 흥미로운 일이 많이 벌어진다. -> 변경할 일이 많기 때문이다.
- 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.
- 그 이유는 이미 충분한 테스트를 거쳤고, 변경에 열려있는 설계를 했기때문이다. -> 변경이 무섭지 않을 것이다.
- 통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.
- 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 이용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.
- 두 방법 모두 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.
- 다시한번 정리하면, 통제 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 좋다는 것을 기억하는 것이 중요하다.
REFERENCES