외부 코드 사용하기
인터페이스 제공자는 더 많은 환경에서 돌아가야 더 많은 고객이 구매하니까 적용성을 최대한 넓히려 애쓴다. 반면, 인터페이스 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 긴장으로 인해 시스템 경계에서 문제가 생길 소지가 많다.
한 예로, java.util.Map을 살펴보자. Map이 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험도 크다. 예를 들어, 프로그램에서 Map을 만들어 여기저기 넘긴다고 가정하자. 넘기는 쪽에서는 아무도 Map 내용을 삭제하지 않으리라 믿겠지만, Map에는 clear() 메서드가 있고, 누구나 Map 내용을 지울 권한이 있다는 말이다.
또 다른 예로, 설계 시 Map에 특정 객체 유형만 저장하기로 결정했다고 하자. 그렇지만 Map은 객체 유형을 제한하지 않는다. Sensor라는 객체를 담는 Map을 만들려면 다음과 같이 Map을 생성한다.
Map sensors = new HashMap();
Sensor 객체가 필요한 코드는 다음과 같이 Sensor 객체를 가져온다.
Sensor s = (Sensor)sensors.get(sensorId);
Map이 반환하는 Object를 올바른 유형으로 변환할 책임은 Map을 사용하는 클라이어트에 있다. 대신 다음과 같이 제네릭스를 사용하면 코드 가독성이 크게 높아진다.
Map<String, Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId);
그렇지만 위 방법도 "Map<String, Sensor>가 사용자에게 필요하지 않은 기능까지 제공한다"는 문제는 해결하지 못한다. 프로그램에서 Map<String, Sensro> 인스턴스를 여기저기로 넘긴다면, Map 인터페이스가 변할 경우, 수정할 코드가 상당히 많아진다.
다음은 Map을 좀 더 깔끔하게 사용한 코드다. Sensors 사용자는 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없다.
class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
}
경계 인터페이스인 Map을 Sensors 안으로 숨긴다. Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 또한 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공하기 때문에 이해하기 쉽고 오용하기는 어려워 진다.
Map 클래스를 사용할 때마다 위와 같이 캡슐화하라는 소리가 아니다. Map을 여기저기 넘기지 말라는 말이다. Map과 같은 경계 인터페이스를 아용할 때는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다. Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.
경계 살피고 익히기
외부에서 가져온 패키지를 사용하기 싶다면 어디서 어떻게 시작해야 좋을까? 외부 패키지 테스트가 우리 책임은 아니지만 우리 자신을 위해 우리가 사용할 코드를 테스트하는 편이 바람직하다. 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것을 학습 테스트라 부른다. 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.
log4j 익히기
로깅 기능을 직접 구현하는 대신 아파차의 log4j 패키지를 사용하려 한다고 가정하자. 이에 대한 학습 테스트를 간단한 단위 테스트 몇 개로 표현했다.
public class LogTest {
private Logger logger;
@Before
public void intialize() {
logger = Logger.getLogger("logger");
logger.removeAllAppenders();
Logger.getRootLogger().removeAllApenders();
}
@Test
public void basicLogger() {
BasicConfigurator.configure();
logger.info("basicLogger");
}
@Test
public void addAppenderWithStream() {
logger.addApender(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");
}
}
학습 테스트는 공짜 이상이다
학습 테스트는 투자하는 노력보다 얻는 성과가 더 크다. 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인한다. 새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.
아직 존재하지 않는 코드를 사용하기
경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다. 만약 무선통신 시스템을 개발하는데 '송신기'라는 하위 시스템이 필요한 상황이고, '송신기' 시스템을 책임진 사람들은 아직 API를 개발하지 못한 상황이라고 하자. 그러나 프로젝트 지연이 되면 않되기 대문에 우리는 자체적으로 인터페이스를 정의한다. Transmitter라는 간단한 클래스를 만들어 개발할 수 있다.
만약 저쪽 팀이 송신기 API를 정희한 후에는 TransmitterAdapter를 구현해 간극을 매울 수 있다. ADAPTER 패턴으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한 곳으로 모은다.
또한, 따로 테스트를 위한 클래스를 생성해 API가를 올바로 사용하는지 테스트할 수도 있다.
깨끗한 경계
경계에서는 흥미로운 일이 많이 벌어진다. 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다. Map에서 봤듯이, 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변한하자. 어느 방법이든 코드 가독성은 높아지며, 외부 패키지가 변했을 때, 변경할 코드도 줄어든다.
'book > clean code' 카테고리의 다른 글
[Clean Code] 9. 단위 테스트 (1) | 2021.09.30 |
---|---|
[Clean Code] 7. 오류 처리 (0) | 2021.09.16 |
[Clean Code] 6. 객체와 자료 구조 (0) | 2021.09.11 |
[Clean Code] 5. 형식 맞추기 (0) | 2021.09.02 |
[Clean Code] 4. 주석 (0) | 2021.08.30 |