본 게시글은 <Clean Code>를 학습한 내용을 정리한 글입니다. (문제시 삭제하겠습니다.)
시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 패키지를 사거나 오픈소스, 다른 팀이 제공하는 컴포넌트를 사용한다. 어떤식으로든 이 외부 코드를 우리 코드에 깔끔하겍 통합해야만 한다.
외부 코드 사용하기
패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓히려 애쓴다. 더 많은 환경에서 돌아가야 더 많은 고객이 구매하니까. 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.
이런 긴장으로 인해 시스템 경계에서 문제가 생길 소지가 많다. 한 예로, java.util.Map이 있다. Map이 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험도 크다.
프로그램에서 Map을 만들어 여기저기 넘긴다고 가정할 때, 넘기는 쪽에서는 Map 내용을 삭제하지 않을 것이라는 기대를 하는 반면, Map에는 clear()라는 메서드가 존재하기 때문에 사용자라면 누구나 지울 권한이 있다.
또한, Map은 객체 유형을 제한하지 않는다. Sensor라는 객체를 담는 Map을 만들고, 가져오는 코드는 다음과 같다.
Map sensors = new HashMap();
...
Sensor s = (Sensor) sensors.get(id);
Map이 반환하는 Object를 올바른 유형으로 변환할 책임은 Map을 사용하는 클라이언트에 있다. 깨끗한 코드라 보기 어려우며, 의도도 분명하게 드러나지 않는다.
다음 코드와 같이 제네릭스(Generics)를 사용하면 코드 가독성이 크게 높아진다.
Map<String, Sensor> sensors = new HashMap<>();
...
Sensor sensor = sensors.get(id);
하지만 위 방법도 사용자에게 필요하지 않는 기능까지 제공한다는 문제는 해결하지 못한다. 자바 5가 제네릭스를 지원하면서 Map 인터페이스가 변경됨에 따라 변경해야할 코드가 너무 많은 탓에 제네릭스 사용을 금지하는 경우도 있다.
다음은 Map을 조금 더 깔끔하게 사용한 코드다. Sensors 사용자는 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없다.
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을(혹은 유사한 경계 인터페이스를) 여기저기 넘기지 않아야 한다는 것이다. 경계 인터페이스를 사용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다.
경계 살피고 익히기
외부에서 가져온 패키지를 사용하고 싶다면 어디서 어떻게 시작해야 좋을까? 외부 패키지 테스트가 우리의 책임은 아니다. 하지만 우리 자신을 위해 우리가 사용할 코드를 테스트하는 편이 바람직하다.
외부 코드를 익히기는 어렵다. 외부 코드를 통합하기도 어렵다. 두 가지를 동시에 하는 것은 더 어렵다. 그렇다면 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히면 어떨까? → 이를 학습 테스트라고 부른다.
학습 테스트는 프로그램에서 사용하는 방식대로 외부 API를 호출한다. 통제된 환경에서 API를 제대로 이해하는지를 확인하는 과정이고, API를 사용하려는 목적에 초첨을 맞춘다.
학습 테스트는 공짜 이상이다
학습 테스트에 드는 비용은 없다. 어쨌든 API를 배워야 하기 때문이다. 오히려 필요한 지식만 확보하는 손쉬운 방법이다. 학습 테스트는 이해도를 높여주는 정확한 실험이다.
학습 테스트는 공짜 이상이다. 투자하는 노력보다 얻는 성과가 더 크다. 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인한다.
학습 테스트는 패키지가 예상대로 도는지 확인한다. 새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.
이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다. 그렇지 않다면 낡은 버전을 필요 이상으로 오랫동안 사용하려는 유혹에 빠지기 쉽다.
아직 존재하지 않는 코드를 사용하기
때로는 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다.
무선통신 시스템에 들어갈 소프트웨어 개발을 하는데 송신기(Transmitter)라는 하위 시스템이 필요한 경우, 이에 대한 지식도 없고, 송신기 시스템을 책임진 사람들이 인터페이스도 정의하지 못한 상태라면?
송신기 모듈에게 원하는 기능 → 지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송
Transmitter라는 간단한 인터페이스를 만든후 transmit이라는 메서드를 추가한다. Transmit 인터페이스는 주파수와 자료 스트림을 입력으로 받는다.
(통제하지 못하고, 정의도되지 않은) 송신기 API에서 CommunicationsController를 분리했다. 우리에게 필요한 인터페이스를 정의했으므로 깔끔하고 깨끗한 코드를 가진 CommunicationsController를 구현할 수 있다.
송신기 API가 정의된 후에는 Adapter 패턴으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한곳으로 모았다.
테스트도 간단하다.
깨끗한 경계
소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.
경계에 위치하는 코드는 깔끔하게 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.
통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다. 자칫하면 오히려 외부 코드에 휘둘리고 만다.
외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. 새로운 클래스로 경계를 감싸거나 Adapter 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. 어느 방법이든 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.
'개발 서적 > 클린 코드(Clean Code)' 카테고리의 다른 글
[Clean Code(클린 코드)] 7장 오류 처리 (0) | 2022.01.30 |
---|---|
[Clean Code(클린 코드)] 6장 형식 맞추기 (0) | 2022.01.23 |
[Clean Code(클린 코드)] 5장 형식 맞추기 (0) | 2022.01.16 |
[Clean Code(클린 코드)] 4장 주석 (0) | 2021.12.29 |
[Clean Code(클린 코드)] 3장 함수 (0) | 2021.12.14 |