개요
시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다.
패키지, 오픈소스, 사내 다른 팀이 구현한 컴포넌트를 사용할 수 있다.
이 외부 코드를 우리 코드에 깔끔하게 통합해야 한다.
이 장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴본다.
외부 코드 사용하기
인터페이스 제공자(또는 프레임워크 제공자)는 적용성을 최대한 넓히려고 애쓴다.
반면 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.
이 두 의견의 입장 차이로 인해 시스템 경계에서 문제가 생길 소지가 많다.
java.utill.Map을 예시로 설명을 들어보자면
Map은 다양한 인터페이스로 수많은 기능을 제공한다.
Map이 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험도 크다.
왜냐하면 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을 Sensors 안으로 숨긴다.
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
}
따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.
그리고 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공하고 설계 규칙과 비즈니스 규칙을 따르도록 강제할 수 있다.
경계 인터페이스를 사용할 때 주의사항
- 경계 인터페이스를 여기저기 넘기지 않기
- 경계 인터페이스를 이용하는 클래스나 클래스 계열 밖으로는 노출되지 않도록 주의하기
경계 살피고 익히기
외부에서 가져온 패키지를 사용하고 싶다면 어디서 어떻게 시작해야 좋을까?
- 하루나 이틀(아니면 더 오랫동안) 문서를 읽으며 사용법 결정한다.
- 우리 쪽 코드를 작성해 라이브러리가 예상대로 동작하는지 확인한다.
- 버그 발생 시 우리 버그인지, 라이브러리 버그인지 디버깅한다.
외부 코드를 익히고 통합하기는 어려워서 학습 테스트를 작성하면 좋다.
- 간단한 테스트 케이스를 작성해 외부 코드를 익힐 수 있다.
- 프로그램에서 사용하려는 방식대로 외부 API를 호출하기 때문에 사용하려는 목적에 초점을 맞춘다.
log4j 익히기
로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지를 사용하려고 가정한다.
문서를 자세히 읽기 전에 첫 번째 케이스를 작성한다.
화면에 "hello"를 출력하는 테스트 케이스다.
@Test
public void testLogCreate() {
Logger logger = Logger.getLogger("MyLogger");
logger.info("hello");
}
아마 이렇게 작성하면 오류가 생길 거다.
Appender를 추가하고 여러 문서를 통해 수정한다면 log4j가 돌아가는 방식을 상당히 많이 이해할 것이다.
이렇게 얻은 것들을 독자적인 로거 클래스로 캡슐화한다.
그러면 나머지 프로그램은 log4j 경계 인터페이스를 몰라도 된다.
학습 테스트는 공짜 이상이다
학습 테스트는 많은 이점을 준다.
- 이해도를 높여주는 정확한 실험이다.
- 패키지가 예상대로 도는지 검증한다.
- 새로운 패키지가 나왔을 때 우리 코드와 호환되지 않으면 밝혀준다.
학습 테스트를 이용한 학습이 필요하든 아니든,
실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다.
이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다.
아직 존재하지 않는 코드를 사용하기
경계에는 아는 코드와 모르는 코드를 분리하는 경계의 유형이 있다.
때로는 우리 지식이 경계를 넘어 미치지 못하는 코드 영역도 있다.
예시를 들어,
무선통신 시스템에 들어가는 소프트웨어를 개발 중이다.
소프트웨어의 하위 시스템 중에 송신기가 있는데 여기에 대한 지식이 거의 없다.
아직 송신기 API가 설계되지 않은 상태이다.
우리가 송신기 모듈에게 원하는 기능은 다음과 같았다.
" 지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송 "
우리가 자체적으로 만들어지기 바라는 송신기 인터페이스를 정의한다.
나중에 송신기 API가 정의된 후에 TransmitterAdapter를 구현를 한다.
API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한곳으로 모은다.
이와 같은 설계는 테스트도 편하다.
적절한 FakeTransmitter 클래스를 사용하면 CommunciationsController 클래스를 테스트할 수 있다.
Transmitter API 인터페이스가 나온 다음 경계 테스트 케이스를 생성해 우리가 API를 올바로 사용하는지 테스트할 수 있다.
깨끗한 경계
소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.
통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.
경계에 위치하는 코드는 깔끔하게 분리한다.
또한 기대치를 정의하는 테스트 케이스도 작성한다.
통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다.
자칫하면 오히려 외부 코드에 휘둘리고 만다.
외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
새로운 클래스로 경계를 감싸거나 Adapter 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.
어느 방법이든 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.
글쓴이의 생각
대학교 졸업 작품을 파이썬으로 구현하였다.
파이썬을 이용하면서 외부 라이브러리를 굉장히 많이 사용하였는데
덕분에 내가 코드를 짤 필요가 없는 경우가 많아 편리하였다.
근데 파이썬이랑 외부 라이브러리가 업데이트하면서 저쪽에서 이게 지원 안된다던가 하는 문제 때문에 고생했던 적이 있었다.
이 책의 내용처럼 너무 외부 코드에 의존되도록 짠 것도 문제가 되었다.
앞으로는 외부 패키지, 라이브러리를 사용할 때 이러한 점을 조심해서 짜야겠다.
'클린코드(CleanCode) 독후감' 카테고리의 다른 글
[클린코드(CleanCode)] 10장 클래스 (4) | 2025.01.15 |
---|---|
[클린코드(CleanCode)] 9장 단위 테스트 (6) | 2025.01.15 |
[클린코드(CleanCode)] 7장 오류 처리 (6) | 2025.01.13 |
[클린코드(CleanCode)] 6장 객체와 자료 구조 (5) | 2025.01.10 |
[클린코드(CleanCode)] 5장 형식 맞추기 (2) | 2025.01.10 |