해당 포스트는 Clean Code의 내용을 참고하여 작성되었습니다.
외부 코드 사용하기
사용자는 자신의 요구에 집중하는 인터페이스를 만들어야한다. 또한 코드는 결합도는 낮추고 응집도는 높혀야한다. 예를 들어 java.util.Map을 보자. 프로그램에서 Map을 만들어 여기저기 넘긴다고 가정하자. 넘기는 쪽에서는 아무도 Map 내용을 삭제하지 않으리라 믿을지도 모르겠다. 그러나 Map의 내용은 clear() 메소드를 통해 누구나 지울 수 있다. 또 다른 예로 설계 시 Map에 특정 객체 유형만 저장하기로 결정했다고 하자. 하지만 Map은 객체 유형을 제한하지 않는다.
1 | Map sensors = new HashMap(); |
Object를 변환하는 책임은 Map을 사용하는 클라이언트에게 있다. 그나마 제네릭스를 사용하면 가독성은 높아진다.
1 | Map<String, Sensor> sensors = new HashMap<Sensor>(); |
하지만 위 방법도 Map이 사용자에게 필요하지 않은 기능까지 제공하는 문제는 해결하지 못한다. 또 Map의 인터페이스가 변할경우 변경할 코드가 많아진다. 다음을 보자.
1 | public class Sensors{ |
경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map의 인터페이스가 변하더라도 나머지 프로그램에는 문제가 안된다. 또한 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다. 그래서 코드는 이해하기 쉽지만 오용하기는 어렵다.
Map 클래스를 사용할 때 마다 캡슐화하라는 말이 아니다. Map과 같은 경계 인터페이스를 여기저기 넘기지 말라는 말이다.
아직 존재하지 않는 코드를 사용하기
경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다. 때로는 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다. 무선통신 시스템 개발을 하면서 송신기 시스템을 책임진 사람들은 인터페이스도 정의하지 못한 상태였다. 그렇기 때문에 우리는 송신기 시스템과 먼 부분부터 작업을 시작했다. 하지만 일을 하며 저쪽 세상과 만나는 경계에 부딪혔다. 경계 너머는 무지라는 안개와 구름으로 한치 앞도 내다보기 어려웠지만 점차 우리에게 필요한 경계 인터페이스가 무엇인지 알게되었다. 우리가 송신기 모듈에게 원하는 기능은 다음과 같았다. 지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라
저쪽 팀이 API를 설계하지 않았으므로 구체적인 방법은 몰랐다. 그래서 우리는 구현을 나중으로 미뤘다. 이쪽 코드를 진행하고자 우리는 자체적으로 인터페이스를 정의했다. Transmitter라는 간단한 클래스를 만든 후 transmitter라는 메소드를 추가했다. 인터페이스는 주파수와 자료 스트림을 입력으로 받았다. 즉 우리가 원하는 인터페이스였다. 우리는 송신기 API에서 CommunicationsController를 분리했다. 우리에게 필요한 인터페이스를 정의했으므로 CommunicationsController 코드는 깔끔하고 깨끗했다.
우리가 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제한다는 장점이 생긴다. 또한 코드 가독성도 높아지고 코드 의도도 분명해진다.
깨끗한 경계
경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다. 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다. 이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다. 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다. 자칫하면 오히려 외부 코드에 휘둘리고 만다.
외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.