우리는 때로는 오픈 소스를 이용하거나, 사내에서 다른 팀이 제공하는 컴포넌트를 이용하기도 한다.
우리는 어떤 식으로든 이 외부 코드를 깔끔하게 통합을 해야한다. 이 장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 설명하고 있다.
위 두 가지의 간극으로 인해 시스템 경계에서 문제가 생길 소지가 많다.
자바에서의 Map은 다양한 인터페이스로 수많은 기능을 제공한다. 기능성, 유연성 모두 뛰어나지만 그 위험도 크다.
예를 들어 Map 인스턴스를 생성해 이곳 저곳에 넘기는 상황이라고 가정을하면. 어느 곳에서도 Map인스턴스가 clear()함수를 사용하는 것을 막을 수 없다. Map 사용자라면 누구나 Map 내용을 지울 권한이 있다는 말이다.
또 다른 예로, 설계시 Map에 특정 객체 유형만 저장하기로 결정했다고 쳤을 때, Map은 기본적으로 Object를 받기 때문에 유형을 제한하지 않는다.
그저 실수없이 올바르게 코드를 짰다고 기도할 수 밖에 없는 것이다.
아래는 Sensor라는 객체를 담는 Map의 사용 예다.
Map sensors = new HashMap();
sensor s = (Sesnsor)sensors.get(sensorId);
그렇다면 제네릭을 사용하면 다음과 같다.
Map<String, Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId);
Map이 변경될리가 없다고 생각하지만 실제로 자바5가 제네릭을 도입하면서 인터페이스가 바뀌었다고 한다.
다음은 Map을 조금 더 깔끔하게 사용한 예다.
경계 인터페이스인 Map을 Sensor안으로 숨긴다.
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
}
Map을 사용할 때마다 매번 위와 같이 캡슐화 하라는 소리가 아니다. 본질은 Map을 여기저기 넘기지 말라는 말이다.
외부 코드를 사용하면 적은 시간에 더 많은 기능을 출시하기 쉬워진다.
외부 패키지 테스트는 우리 책임이 아니지만 우리 자신을 위해 우리가 사용할 코드를 테스트를 하는 편이 바람직하다.
타사 라이브러리를 가져왔으나 사용법이 분명하지 않을 때, 대개는 시간을 투자해서 문서를 따로 읽으며 사용법을 알아본다. 그런 다음 우리쪽 코드를 작성해 라이브러리가 예상대로 동작하는지 확인한다.
때로는 원인 모를 버그로 골치를 앓는다.
외부 코드를 익히기는 어렵다. 통합하기도 어렵다. 두 가지 동시에 하기에는 더더욱 어렵다.
저자는 조금 다른 방식을 제시한다. 그 방법은 아래와 같다.
곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익힌다.
로깅 기능을 직접 구현하는 대신 아파치 log4j를 사용하려는 상황 가정이다. 아래와 같은 흐름으로 학습 테스트를 진행한다.
문서를 자세히 읽기 전에 첫번째 테스트 케이스를 작성한다.
@Test
public void testLogCreate() {
Logger logger = Logger.getLogger("MyLogger");
logger.info("hello");
}
테스트를 실행했더니 Appender라는 뭔가가 필요하다는 오류가 발생한다.
문서를 조금 더 읽어보니 ConsoleAppender가 있어서 생성한 후 테스트 케이스를 다시 돌린다.
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger("MyLogger");
ConsoleAppender appender = new ConsoleAppender();
logger.addAppender(appender);
logger.info("hello");
}
Appender에 출력 스트림이 없다는 사실을 발견한다. 구글 검색을 하고 다음과 같이 수정한다.
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger("MyLogger");
logger.removeAppAppenders();
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n"), ConsoleAppender.SYSTEM_OUT));
logger.info("hello");
}
"hello" 라는 로그가 잘 찍히지만 ConsoleAppender가 출력을 콘솔로 출력을 한다고 지정을 해주다니 (SYSTEM_OUT) 이상하다. 지워보지만 여전히 잘 출력된다.
하지만 PatternLayout을 제거했더니 또 다시 출력 스트림이 없다는 오류가 뜬다.
문서를 자세히 읽어보니 기본 ConsoleAppender 생성자는 '설정되지 않은' 상태라고 한다.
-> 버그, 일관성 부족 등...
구글을 뒤지고 문서를 읽어보고 테스트를 돌린 끝에 다음과 같은 코드를 얻었다.
이 과정에서 log4j가 돌아가는 방식을 상당히 많이 이해했다.
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 경계 인터페이스를 몰라도 된다.
학습 테스트를 이용한 학습이 필요하든 그렇지 않든, 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스는 필요하다.
저자는 무선통신 시스템 개발을 참여했을 때 얘기를 해준다.
'송신기' 하위 시스템 담당자가 있고, 저자는 송신기 시스템과 협력하는 또다른 어떤 하위 시스템을 만드는 상황인듯하다.
송신기 하위 시스템은 아직 인터페이스도 정의하지 못한 상태에 프로젝트 지연을 원하지 않았기 때문에 저자는 송신기 시스템과 아주 먼 부분부터 작업을 하기 시작했다.
대략 "지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라." 라는 추상적인 방향성만 갖고 작업을 하게 됐다.
송신시 시스템은 아직 API설계를 하지 않았고 저자쪽 코드를 진행하고자 자체적으로 인터페이스를 정의했다.
Transmitter라는 클래스를 만든 후 transmit이라는 메서드를 추가했다.
송신시 API에서 CommuicationController를 분리 했다.
송신기 팀이 API를 정의한 후에는 TransmitterAdapter를 구현해 간극을 메운다.
테스트도 편하다. FakeTransmitter 클래스를 사용하면 CommunicationsController 클래스를 테스트 할 수 있다.
경계에선는 많은 흥미로운 일이 벌어진다. 변경이 대표적인 예.