이번 장에서는 외부의 API(라이브러리)를 우리 코드에 깔끔하게 통합하는 방법을 소개해요. 소프트웨어간의 경계를 깔끔하게 처리하는 기법과 기교를 알려준다고 생각하면 되요. 대표적인 예로 다른 팀의 코드를 사용하거나, 오픈 소스 패키지를 다운받아 사용하는 것이 있어요.
인터페이스 제공자와 사용자 사이에는 특유의 긴장이 존재해요. 인터페이스 제공자는 최대한 적용성을 넓혀 다양한 환경에서 사용가능하게 만들고 싶어해요. 하지만 사용자는 사용자의 요구사항에 맞는 인터페이스를 원해요. 그래서 이러한 긴장으로 시스템 경계에서 문제가 생길 소지가 많아요.
Java의 Map 패키지를 예시로 살펴봐요.
clear() void
containsKey(Object key) boolean
isEmpty() boolean
get(Object key) Object
Map은 위와 같이 다양한 메서드를 제공해요. 그런데 위 패키지를 활용해서 어떤 값들을 저장 및 조회할 수 있게 만든다고 해봐요.
Map<String, Sensor> sensors = new HashMap<Sensor>();...Sensor s = Sensors.get(sensorId);위와 같이 sensors에서 sensor를 가져오는 코드를 만들 수 있어요. 그런데 Sensors에서 clear()과 같이 필요하지 않은 기능을 함꼐 가지고 있어요. 그래서 코드 사용자가 실수할 가능성이 생기고 많은 api에 대한 정보를 받게되요. 또한 Map 패키지가 업데이트되어 메서드 명이 바껴도 직접적으로 사용자 코드에 영향을 줘요. 그래서 이런 영향을 최소화하기 위해 경계를 잘 분리해야해요.
public class Sensors { private Map sensors = new HashMap(); public Sensor getById(String id) { return (Sensor) sensors.get(id); } ...}위 코드처럼 Sensors Class로 경계를 처리해서 노출할 인터페이스를 제한하며 제공할 수 있어요. 그래서 사용하는 코드에서 설계 규칙과 비즈니스 규칙을 따르도록 강제하여 오용을 줄일 수 있어요. 또한 Map이 업데이트되도, Sensors 내부 구현에서 변경사항만 적용해주면 되요.
물론 Map을 만날때마다 위와같이 캡슐화하라는 것은 아니에요. Map과 같은 유사한 경계 인터페이스를 여기저기 넘기지 말라는 거에요. Map 인스턴스를 인수로 넘기거나 반환 값으로 사용하지 않도록 주의해주세요.
외부 코드를 사용하면 기능 출시에 필요한 시간이 더 적어져요. 그런데 외부 코드를 사용하기 위해선 사용법을 숙지해야해요.
클린 코드에서는 학습 테스트라는 방법으로 경계를 살피고 익히길 권장해요. 학습 테스트는 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 방법이에요. 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 테스트 코드에서 호출하는 거에요. 그래서 통제된 환경에서 API를 제대로 이해하는지 확인해요.
public class LogTest { private Logger logger; @Before public void initialize() { logger = Logger.getLogger("logger"); logger.removeAllAppenders(); Logger.getRootLogger().removeAllAppenders(); } @Test public void basicLogger() { BasicConfigurator.configure(); logger.info("basicLogger"); } @Test public void addAppenderWithStream() { logger.addAppender(new ConsoleAppender( new PatternLayout("%p %t %m%n"), ConsoleAppender.SYSTEM_OUT)); logger.info("addAppenderWithStream"); } @Test public void addAppenderWithoutStream() { logger.addAppender(new ConsoleAppender( new PatternLayout("%p %t %m%n"))); logger.info("addAppenderWithoutStream"); }}위 코드가 옛날의 log4j 익히기를 위한 테스트 코드 예시에요. 콘솔 로거 초기화하는 방법을 테스트 코드로 익혔으니 이제 위 경계를 독자적인 로거 클래스로 캡슐화하여 사용하면 되요. 이러면 사용자는 log4j 경계 인터페이스를 몰라도 로거를 사용할 수 있어요. 그리고 log4j가 업데이트되어 위 테스트가 에러를 일으키면, 그때 업데이트된 부분을 반영해주면되고요.
이렇게 테스트 케이스를 활용하면 패키지의 새버전으로 이전도 쉽고, 새로운 패키지로 바꾸기도 쉬워지고, 원하는 인터페이스로 노출할 수 있어요.
일을 하다보면, 어떤 이유로든 다른 팀의 API가 정해지지 않는 경우가 있어요. 그래서 아는 코드와 모르는 코드를 분리하는 경계가 존재하게 되고, 경계 너머는 뭐가 있는지 보이지가 않아요. 하지만 어떤 기능이 우리 코드에 사용될지는 감으로라도 알 수 있어요. 그래서 우리 코드와 다른 팀의 코드가 만나는 경계를 잘 처리할 수 있게 만들 수 있어요.
예를 들면 우리 LocationService 코드에 GPS 모듈의 기능이 필요하다고 해봐요. 그런데 GPS 모듈의 인터페이스가 없다고 가정해요. 이런 경우에는 GPS 기능을 분리하고 Adapter 패턴을 적용하여 API를 캡슐화하여 구현하도록 만들면 외부 GPS 모듈 api가 어떻게 바뀌든 쉽게 반영할 수 있어요.

위 Class Diagram UML이 그 예시에요. 위와 같이 구현하면 테스트코드를 만들기도 쉬워요. ExternalGpsClient는 없지만 FakeGpsGateway를 사용하여 임시로 테스트할 수 있기 때문이에요.
경계에서는 변경과 같은 일들이 많이 일어나요. 하지만 설계가 잘 되어 있다면 변경하는데 많은 투자와 작업이 필요하지 않아요.
그래서 경계에 위치하는 코드는 깔끔히 분리하는게 좋아요. 그리고 기대치를 정의하는 테스트 케이스를 작성하는 것도 좋고요. 우리 코드는 외부 패키지에 대해서 자세히 알 필요도 없어야 해요. 이 목표들을 달성하기 위해서는 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리해야해요. Map 패키지 사용을 Wrapper Class로 처리하거나 어떤 인터페이스로 서비스를 제공할 지 모르는 GPS 모듈을 Adapter 패턴으로 처리하는 것 처럼 잘 관리해야해요.
엔지니어링의 핵심은 분리라는 것을 잘 보여주는 것 같아요. 물론 그 분리의 목적은 확실해야하겠지만요. 이걸 보면서 어떻게 코드를 리뷰하거나 평가하는 것이 좋을지 스스로 다시 생각해보는 계기를 가졌어요. 결론은 전체적인 아키텍처를 볼때랑 코드를 볼때 시각을 달리해야한다고 생각하게되었어요.
예를 들면 전체적인 서비스 흐름은 아키텍처 처럼 흐름을 봐야하고, 코드는 경계를 기준으로 그 안의 코드만을 봐야한다고 생각했어요. 가끔씩 코드를 보다보면 경계 너머도 함께 보는 경우가 있는데, 경계 너머까지 보면 개념이나 흐름, 중요한 것들을 다 놓치고 뒤죽박죽되는 것 같아요. 그리고 왜 그렇게 뒤죽박죽 코드를 제대로 평가하지 못했는지 깨달은 것 같아요.
결론은 클린 코드와는 상관 없을 수 있지만, 코드를 리뷰할 땐 경계 단위로 평가해야 제대로 평가할 수 있고, 코드를 제대로 이해하는 것이라 생각한다는 것입니다.