
외부 코드를 우리 코드에 깔끔하게 통합하기 위해 소프트웨어 경계를 깔끔하게 정리하는 기법과 기교를 살펴본다.
패키지/프레임워크 제공자는 적용성을 최대한 넓히려 애쓰고, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이러한 차이로 인해 시스템 경계에서 문제가 생길 소지가 많다.
java.util.Map은 다양한 인터페이스로 수많은 기능을 제공한다.
/*
* 기본 예제, Sensor라는 객체를 담는 Map 생성
*/
Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId); //Sensor 객체 호출
/*
* 예제2, 기본예제에서 제네릭스(Generics) 사용 -> 가독성 향상
* 기본 문제점인 Map<String, Sensor>가 사용자에게 필요하지 않은 기능까지 제공한다는 점은 해결 불가
*/
Map<String, Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId); //Sensor 객체 호출
/*
* 예제3, 기본예제 리팩토링(캡슐화)
* 제네릭스의 사용 여부는 Sensors 내부에서 결정 -> 사용자는 제네릭스가 사용 여부와 무관
* 프로그램에 필요한 인터페이스만 제공 -> 코드 이해 쉬움, 오용 어려움
*/
public class Sensors {
//경계 인터페이스인 Map을 Sensors안으로 숨겨 Map 인터페이스가 변하더라도 나머지 프로그램에 영향 X
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor)sensors.get(id);
}
}
외부 코드를 사용할 때 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 프로그램에서 사용하려는 방식대로 외부 API를 호출하는 간단한 테스트 케이스를 작성해 외부 코드를 익힌 후 사용하는 것을 학습 테스트라 부른다. 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.
로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지 사용 시 다음과 같이 테스트 케이스를 작성하고 실행하는 과정을 통해 독자적인 로거 클래스로 캡슐화 할 수 있다. 이 클래스외 나머지 프로그램은 log4j 경계 인터페이스를 몰라도 된다.
@Test
public void testLogCreate() {
Logger logger = Logger.getLogger("MyLogger");
logger.info("hello");
}
@Test
public void testLogAppender() {
Logger logger = Logger.getLogger("MyLogger");
ConsoleAppender appender = new ConsoleAppender();
logger.addAppender(appender);
logger.info("hello");
}
@Test
public void testLogAppender() {
Logger logger = Logger.getLogger("MyLogger");
logger.removeAllAppenders();
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n"),
ConsoleAppender.SYSTEM_OUT)); //ConsoleAppender.SYSTEM_OUT 제거 무관, PatternLayout 제거 시 오류 발생
logger.info("hello");
}
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"),
ConsoleAppender.SYSTEM_OUT));
logger.info("addAppenderWithoutStream");
}
}
학습 테스트는 필요한 지식만 확보하는 손쉬운 방법이자 이해도를 높여주는 정확한 실험이다.
학습 테스트는 패키지가 예상대로 도는지 검증한다. 패키지 새 버전이 출시되면 우리 코드와 호환되는지 아닌지를 학습 테스트가 곧바로 밝혀낸다. 학습 테스트를 이용한 학습이 필요하든 아니든 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다.
경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.
무선통신 시스템에 들어갈 소프트웨어를 개발한다고 할 때, 소프트웨어에 '송신기'라는 하위 시스템이 필요하다고 하자. '송신기' 시스템에서 제공하는 인터페이스도 없는 상태인 경우, 아래 순서에 따라 설계하고 프로젝트를 진행하면 개발에 수월하다.
<Interface>클래스 생성 -> transmit(frequency, stream) 메서드 추가, 주파수와 자료 스트림을 입력으로 받음