008. (Clean Code) 8. 경계 - Boundaries

8. 경계 - Boundaries

거대한 시스템을 개발함에 있어, 모든 부분을 직접 개발하는 경우는 드물다.

외부 솔루션을 구매해서 도입하거나, 오픈 소스를 이용하기도 한다.

본 포스팅에서는 이러한 외부 의존성을 개발중인 시스템에 깔끔하게 통합하는 방법에 대해서 알아보자.

8.1. 외부코드 사용하기

패키지 및 프레임워크를 통해 인터페이스를 제공하는 경우, 적용성을 최대한 넓히려 애쓰며,

인터페이스 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.

이 견해차이로 인해 시스템의 경게에선 문제가 생길 소지가 많다.

하나의 예시로 java.util.Map 인터페이스를 살펴보자.

참고 java.util.Map Docs

Map은 굉장히 다양한 인터페이스를 통해 수많은 기능을 제공한다.

수많은 기능을 통해 주어지는 유용함도 좋지만, 그만큼 위험도도 증가한다.

만약 프로그램내에서 Map을 만들어 여기저기 넘긴다고 가정하자.

이때 넘겨받는 쪽이 Map 내부의 데이터를 지우지않는다고 보장할 수 있는가?

Map Method

제일 위에 clear() 메서드가 보인다.

즉 어떤 위치에서건 해당 Map 객체의 내용을 소거할 수 있는 상황이라는 것이다.

또 다른 예졔도 살펴 보자.

1
2
3
Map sensors = new HashMap();

Sensor s = (Sensor)sensors.get(sensorId);

Map 객체는 특정 타입을 제한하지않으므로 어떠한 객체 타입도 추가할 수 있다.

즉 위와 같은 코드가 반복적으로 요구되며, 올바른 객체 타입을 변환할 책임을 사용자에게 강요하게 된다.

결과적으로 깨끗한 코드가 아니게 된다.

이를 해결하기위해 Map은 아래와 같이 제네릭을 지원한다.

1
2
3
Map<String, Sensor> sensors = new HashMap<Sensor>();

Sensor s = sensors.get(sensorId);

하지만 위 방법도 Map<String, Sensor>가 필요하지않은 기능까지 전부 제공한다는 문제가 남아있게 된다.

또한 Map의 인터페이스가 변할 경우 사용하는 모든 부분에 대해서 변경이 발생하게 될 것이다.

참고 컬렉션에 해당하는 Map의 변경은 거의 없을 것이다. 다만 Java 5에서 제네릭을 지원하기위해 인터페이스가 변한 역사가 있기에 장담할 수 없는 문제이다.

아래는 좀 더 깔끔하게 Map을 사용한 코드이다.

1
2
3
4
5
6
7
8
public class Sensors {
private Map sensors = new HashMap();

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

경계에 해당하는 인터페이스인 MapSensors 클래스 안으로 숨겨서 Map의 변경사항을 Sensors 클래스 내부에서만 처리할 수 있게 되었다.

또한 Sensors는 프로그램에 꼭 필요한 getById() 메서드만을 제공하여서 읽기 쉬운 코드와 함께 오용을 방지하기도 한다.

그렇다면 Map과 같은 경계 인터페이스를 사용할 때마다 항상 캡슐화르 해야할까?

아니다. 경게 인터페이스를 시스템 내 여기저기에 넘기는 상황을 방지하는 것이 더 중요하다.

결론적으로 Map 인스턴스를 공개된 메서드의 파라미터나 반환 타입으로 사용하지않는 것이 좋다.

8.2. 경계 살피고 익히기

외부에서 가져온 패키지를 사용하고 싶다면 어디서 어떻게 시작해야할까?

일단 외부 패키지의 테스트는 우리의 책임은 아니다.

하지만 우리 스스로를 위해 사용할 부분의 코드를 테스트하는 것이 바람직하다.

외부 코드는 익히기도 어렵고, 통합하기도 어려우면 두 가지를 동시에 하는 것은 더더욱 어렵다.

따라서 접근법을 바꾸어야 한다.

우리쪽 코드를 작성해 외부 코드를 호출하는 대신, 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 학습 테스트를 적용해볼 수 있다.

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다.

테스트 환경이라는 통제된 환경에서 외부의 API를 제대로 이해하고 있는지를 검증하는 것이다.

이처럼 학습 테스트는 API를 사용하려는 목적 자체에 초점을 맞춘다.

8.3. 학습 테스트는 공짜 이상이다

학습 테스트 자체에 드는 비용은 없다. 오히려 필요한 지식만 확보하는 손쉬운 방법일 수도 있다.

사실 학습 테스트는 투자하는 노력보다 얻는 성과가 더 큰 공짜 그 이상의 값어치를 한다.

만약 새로운 패키지가 출시한다면 학습 테스트만 돌려도 현재 시스템에 영향을 주는 변경이 있는지 단 번에 알 수 있다.

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

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계이다.

때로는 우리의 지식이 경계 너머를 알 수 있는 상태일수도,

알려고 해도 알 수 없는 경우도 종종 존재한다.

이 경우 예상되는 기능들에 대해 자체적인 인터페이스를 정의한 후 마찬가지로 예상되는 데이터들을 주입해서 일종의 Mock 객체를 만드는 것도 방법이다.

자세한 내용은 아래 포스트를 참고하도록 하자.

참고
(Working Effectively with Legacy Code) 014. Dependencies on Libraries Are Killing Me
(Working Effectively with Legacy Code) 015. My Application Is All API Calls

8.5. 깨끗한 경계

경게에서는 흥미로운 일이 많이 벌어진다.

대표적인 예시가 바로 경계에서 벌어지는 변경이다.

우수한 설계가 적용된 소프트웨어라면 변경에 많은 리소스를 투입할 필요는 없다.

다만 통제하지 못하는 코드를 사용할 때는 의도치않게 많은 리소스가 소모될 수도 있다.

따라서 경게에 위치하는 코드는 깔끔히 분리하고, 이에 대한 기대치를 정의하는 테스트 케이스도 작성해두어야 한다.

통제가 불가능한 외부 패키지에 의존하는 대신 이를 통제가 가능한 우리 코드에 의존하는 것이 훨씬 낫기 때문이다.