Intro
- 시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다.
- 때로는 패키지나 오픈소스를 사용하거나, 사내 다른팀이 제공하는 컴포넌트를 사용해야한다.
- 어떤식이든, 우리는 이 외부 코드를 우리 내부 코드에 "깨끗하게" 통합시켜야 한다.
- 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴보자.
외부 코드 사용하기
- 인터페이스를 "제공하는" 입장과 "사용하는" 입장 사이에는 특유의 긴장이 존재한다.
- "제공하는" 입장에서는 다양한 환경에서 좀 더 많은 사용자가 사용할 수 있도록 적용성을 넓히려고 애쓴다.
- "사용하는" 입장에서는 그들의 요구에 집중하는 인터페이스를 바란다.
- 이러한 긴장으로 시스템 경계에서 문제가 생길 소지가 많다.
- 한 예로 java.util.Map 의 경우 다양한 인터페이스로 수많은 기능을 제공한다.
- Map 의 인터페이스가 바뀌거나 할 경우 또한 우리 코드의 많은 부분들이 바뀌어야 한다.
- 실제로 Java 5버전에서 generic이 추가되었을 때 Map의 인터페이스가 바뀐 사례가 있다.
- 경계 인터페이스를 다루는 좋은 방법은 감싸기이다.
- 다만, Map 클래스를 사용할 때마다 캡슐화하라는 소리가 아니다.
- Map과 같은 "경계 인터페이스"를 여기저기 넘기지 말아야한다.
- 해당 객체를 사용하는 클래스 내부에 넣던지 가까운 계열의 클래스에 넣어 외부로 노출되지 않도록 주의한다.
- 공개된 api에서 인자로 받거나 리턴하지 마라.
public class Sensors {
private Map<String, Sensor> sensors = new HashMap<>();
public Sensor getById(String id) {
return sensors.get(id);
}
}
- 추가 : 외부 api가 변경될 경우 전반적으로 적용된 코드를 수정해야 하므로 wrapping 하여 변경점을 최소화 할수 있다.
경계 살피고 익히기
- 외부 코드를 사용할 때, 해당 코드의 테스트가 우리 책임은 아니다. (제공자의 책임)
- 하지만 우리 자신을 위해 우리가 사용할 부분의 코드는 테스트하는 편이 바람직하다.
- 외부 코드를 익히는것과 외부 코드를 통합하는건 어렵다, 둘을 동시에 하면 더욱 어렵다.
- 따라서 바로 외부 코드를 통합하는 대신 간단한 테스트 케이스를 작성해 외부 코드를 익히는것도 좋은 방법이다.
- 짐 뉴커크는 이를 "학습 테스트" 라고 부른다.
log4j 공부하기
@Test
public void testLogCreate(){
Logger logger=Logger.getLogger("MyLogger");
logger.info("hello");
}
@Test
public void testLogAddAppender(){
Logger logger=Logger.getLogger("MyLogger");
ConsoleAppender appender=new ConsoleAppender();
logger.addAppender(appender);
logger.info("hello");
}
@Test
public void testLogAddAppender(){
Logger logger=Logger.getLogger("MyLogger");
logger.removeAllAppenders();
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n"),
ConsoleAppender.SYSTEM_OUT));
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")));
logger.info("addAppenderWithoutStream");
}
}
학습 테스트는 공짜 이상이다
- 학습 테스트에 드는 비용은 없다. 어쨋든 API를 배워야 하므로...
- 오히려 필요한 지식만 확보하는 손쉬운 방법이다.
- 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인할 수 있다.
- 새버전이 기존과 호환 되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.
- 이러한 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.
- 경계 테스트가 없다면 낡은 버전을 필요 이상으로 오랫동안 사용하려는 유혹에 빠지기 쉽다.
아직 존재하지 않는 코드 사용하기
- 경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.
- 때로는 (적어도 지금은) 알려고해도 알수가 없다.
- 아직 개발되지 않은 모듈이 필요하지만, 기능은 커녕 인터페이스조차 구현되지 않은 경우가 있을 수 있다.
- 그럴 경우 구현은 뒤로 미루고 자체적으로 인터페이스를 정의하여 진행하는 방법도 있다.
- 예시
- 저자는 무선통신 시스템을 구축하는 프로젝트를 하고 있었다.
- 그 소프트웨어에는 "송신기"라는 하위 시스템이 있었는데 저자 팀은 송신기에 대한 지식이 거의 없었다.
- "송신기" 팀은 인터페이스도 정의하지 못한 상태였다.
- 프로젝트 지연을 원하지 않았기에 "송신기" 하위 시스템과 아주 먼 부분부터 작업하기 시작했다.
- Adaptor pattern 으로 API 사용을 캡슐화 하였다. 이와 같은 설계는 테스트도 아주 편하다.
public interface Transimitter {
void transmit(SomeType frequency, OtherType stream);
}
public class FakeTransmitter implements Transimitter {
public void transmit(SomeType frequency, OtherType stream) {
}
}
public class TransmitterAdapter implements Transimitter {
public void transmit(SomeType frequency, OtherType stream) {
}
}
public class CommunicationController {
public void someMethod() {
Transmitter transmitter = new FakeTransmitter();
transmitter.transmit(someFrequency, someStream);
}
public void someMethod() {
Transmitter transmitter = new TransmitterAdapter();
transmitter.transmit(someFrequency, someStream);
}
}
깨끗한 경계
- 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.
- 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리코드에 의존하는 편이 훨씬 좋다.
- 자칫하면 오히려 외부 코드에 휘둘리고 만다.
- 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
- 새로운 클래스로 경계를 감싸거나 adaptor 패턴을 사용해 우리의 인터페이스로 변환하자.
- 코드 가독성이 높아진다.
- 경계 인터페이스를 사용하는 일관성도 높아진다.
- 외부 패키지가 변했을 때 변경할 코드도 줄어든다.