CS/클린코드
[클린코드] chapter-8 경계
yhsim98
2022. 9. 5. 21:21
서드파티 코드 사용하기
- 인터페이스를 "제공하는" 입장과 "사용하는" 입장 사이에는 필연적인 긴장감이 존재한다.
- "제공하는" 입장에서는 좀 더 다양한 환경에서 좀 더 많은 사용자가 사용할 수 있도록 다양한 사용성을 지향한다.
- "사용하는" 입장에서는 그들의 사용성에 맞는 specific한 인터페이스를 원한다.
- 이것을 "경계에서의 긴장"이라 부른다.
- 만약 우리가 Sensor클래스를 저장하는 Map객체를 사용한다면 다음과 같은 형태일 것이다.
- Map sensors = new HashMap();
- Sensor s = (Sensor) sensors.get(sensorId);
- 이와 같은 방식은 Sensor클래스를 사용하는 코드 전반에 걸쳐 빈번히 사용된다.
- 하지만 이는 사용되는 곳에서 캐스팅의 부담을 안게 된다. 그리고 적절한 문맥조차 줄 수 없다.
- 이는 아래와 같이 generic을 사용함으로써 해결할 수 있다.
Map<Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId);
- 하지만 이 방법 또한 Map객체가 필요 이상의 기능을 제공하는 것은 막지 못한다.
- Map의 인터페이스가 바뀌거나 할 경우 또한 우리 코드의 많은 부분들이 바뀌어야 한다.
- 인터페이스가 바뀔 일이 별로 없을 것이라 생각할 지도 모르지만, 실제로 Java 5버전에서 generic이 추가되었을 때 Map의 인터페이스가 바뀐 사례가 있다.
- 결국 제일 좋은 방법은 래핑이다.
- 모든 Map을 이런 식으로 래핑하라는 말은 아니다.
- 다만 Map과 같은 "경계에 있는 인터페이스"를 시스템 전반에 걸쳐 돌려가며 사용하지 말고,
- 해당 객체를 사용하는 클래스 내부에 넣던지 가까운 계열의 클래스에 넣어라.
- 공개된 api에서 인자로 받거나 리턴하지 마라.
public class Sensors {
// 경계의 인터페이스(이 경우에는 Map의 메서드)는 숨겨진다.
// Map의 인터페이스가 변경되더라도 여파를 최소화할 수 있다. 예를 들어 Generic을 사용하던 직접 캐스팅하던 그건 구현 디테일이며 Sensor클래스를 사용하는 측에서는 신경쓸 필요가 없다.
// 이는 또한 사용자의 목적에 딱 맞게 디자인되어 있으므로 이해하기 쉽고 잘못 사용하기 어렵게 된다.
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor)sensors.get(id);
}
//snip
}
경계 살피고 익히기
- 외부 코드를 사용하면 개발 시간이 감소되지만,
- 우리가 사용할 코드를 테스트하는 편이 바람직하다
- 간닪나 테스트 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히자
- 짐 뉴커스는 이를 학습 테스트라 부른다(TDD 에서)
Log4j 익히기
- 로깅을 직접 구현하는 대신 아파치의 log4j를 사용할 때,
- 문서를 자세히 읽기 전 첫 번째 테스트 케이스를 작성한다
@Test
public void testLogCreate() {
Logger logger = Logger.getLogger("MyLogger");
logger.info("hello");
}
- 이 이후에도 출력 스트림이 없거나 Appender 이 필요한 등
여러 문제를 해결하며 익혀본다
- 이렇게 구글을 뒤지고,
- 문서를 읽어보고, 테스트를 돌린 끝에
- 돌아가는 방식을 이해하고, 여기서 얻은 지식을 이용하여
- 간단한 단위 테스트 몇 개로 표현한다
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 경계 인터페이스를 몰라도 된다
학습 테스트는 공짜 이상이다
- 학습 테스트에 드는 비용은 없다
- 투자하는 노력보다 얻는 성과가 더 크다
- 패키지 새 버전이 나온다면 테스트만 돌려보면 된다
아직 존재하지 않는 코드를 사용하기
- 아직 개발되지 않은 모듈이 필요한데, 기능은 커녕 인터페이스조차 구현되지 않은 경우가 있을 수 있다.
하지만 우리는 이러한 제약때문에 우리의 구현이 늦어지는걸 탐탁치 않게 여긴다. - 예시
- 저자는 무선통신 시스템을 구축하는 프로젝트를 하고 있었다.
- 그 팀 안의 하부 팀으로 "송신기"를 담당하는 팀이 있었는데 나머지 팀원들은 송신기에 대한 지식이 거의 없었다.
- "송신기"팀은 인터페이스를 제공하지 않았다. 하지만 저자는 "송신기"팀을 기다리는 대신 "원하는" 기능을 정의하고 인터페이스로 만들었다.
- [지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라]
- 이렇게 인터페이스를 정의함으로써 메인 로직을 더 깔끔하게 짤 수 있었고 목표를 명확하게 나타낼 수 있었다.
public interface Transimitter {
public void transmit(SomeType frequency, OtherType stream);
}
public class FakeTransmitter implements Transimitter {
public void transmit(SomeType frequency, OtherType stream) {
// 실제 구현이 되기 전까지 더미 로직으로 대체
}
}
// 경계 밖의 API
public class RealTransimitter {
// 캡슐화된 구현
...
}
public class TransmitterAdapter extends RealTransimitter implements Transimitter {
public void transmit(SomeType frequency, OtherType stream) {
// RealTransimitter(외부 API)를 사용해 실제 로직을 여기에 구현.
// Transmitter의 변경이 미치는 영향은 이 부분에 한정된다.
}
}
public class CommunicationController {
// Transmitter팀의 API가 제공되기 전에는 아래와 같이 사용한다.
public void someMethod() {
Transmitter transmitter = new FakeTransmitter();
transmitter.transmit(someFrequency, someStream);
}
// Transmitter팀의 API가 제공되면 아래와 같이 사용한다.
public void someMethod() {
Transmitter transmitter = new TransmitterAdapter();
transmitter.transmit(someFrequency, someStream);
}
}
출처
https://github.com/Yooii-Studios/Clean-Code/blob/master/Chapter%2008%20-%20%EA%B2%BD%EA%B3%84.md