클린 코드 #8 경계
SW 개발을 할 때 모든 SW를 직접 개발하지 않는다. Reuse도 많이한다. 오픈소스를 사용하거나, 패키지를 사거나, 사내 팀이 제공하는 컴포넌트를 쓰거나….
이러한 외부 코드를 우리 코드에 깔끔하게 통합하는 것이 중요하다. 이 경계를 깔끔하게 처리하는 기법을 살펴보자.
외부 코드 사용하기
인터페이스 제공자와 사용자는 원하는 바가 다르다.
- 인터페이스 제공자(ex. 패키지 제공자, 프레임워크 제공자)
- 적용성을 최대한 넓히려고 한다. (더 많은 고객 구매를 위해)
- 인터페이스 사용자
- 자신의 요구에 집중하는 인터페이스를 바란다.
이렇기에 시스템 경계에서 문제가 생길 소지가 많아진다.
ex) 대표적인 예시: java.util.Map
Map은 다양한 인터페이스로 다양한 기능을 제공한다. 하지만 그만큼 위험도 크다.
- Map에는
clear()
메서드가 존재한다 누군가가 지워버릴 수 있다. - Map은 객체 유형을 제한하지 않는다. A 타입으로 제한하려고 해도, B 타입이 추가될 수 있는 것이다.
- 제네릭 없는 Map을 사용한 예시이다.
- Map에서 객체를 반환받을 때 타입을 변환하는 책임도 클라이언트에게 있다.
만약 제네릭을 적용한다고 해도, 문제는 여전히 많이 남아있다.
- Map 인터페이스는 필요없는 기능까지 제공한다.
- Map 인터페이스를 여기저기 넘긴다면 인터페이스가 변경되는 겨우 수정할 코드가 굉장히 많아진다.
- 실제로 Java 1.5에서 Map 제네릭이 도입되면서 수 많은 코드가 변경되었다.
Map을 조금 더 깔끔하게 사용한다면 어떻게 사용할 수 있을까?
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
// 이하 생략
}
경계 인터페이스를 Sensors 안으로 숨겼다.
- 이렇게 되면, Sensors를 사용하는 입장에서 Map이 제네릭을 사용했는지 안했는지 알 필요가 없다.
- Java5에 제네릭이 도입되었어도, 외부에는 영향을 미치지 않는 것이다.
또한 Sensors 클래스는 필요한 인터페이스만 제공한다. 코드를 쉽게 이해하게 만들고, 오용은 막는다. 비즈니스 규칙과 설계를 강제할 수 있다.
물론 이 말이 Map을 사용할 때 마다 캡슐화하라는 소리는 아니다. Map을 여기저기 넘기지 말라는 이야기다.
Map과 같은 경계 인터페이스를 이용할 때는 이용하는 클래스 밖으로 노출되지 않도록 주의하자.
경계 살피고 익히기
외부 코드를 사용하면 생산성이 좋아진다. 외부에서 가져온 패키지를 사용한다면 어디서 시작해야 할까?
외부 코드를 익히기는 어렵다. 사용법 공부를 해도 예상치 않게 동작하기도 한다.
외부 패키지는 우리 책임은 아니지만 우리 자신을 위해 우리가 사용할 코드(외부 코드)를 테스트해야 한다.
곧바로 우리 코드에서 외부 코드를 호출하는 대신, 간단한 테스트 케이스를 만들어서 외부 코드를 익히자. (학습 테스트라 부른다)
통제된 환경에서 API 사용법을 익히는 것이다.
학습 테스트 예시: log4j 익히기
예를 들어, 직접 로거를 만드는 대신 아파치 log4j를 사용해본다고 하자.
- 메뉴얼을 읽기 전에 먼저 테스트 케이스를 작성하고 돌린다.
- Appender가 필요하다는 오류가 난다 ConsoleAppender라는걸 메뉴얼에서 찾는다.
- ConsoleAppender를 logger에 추가하고 테케를 돌린다.
- Appender에 출력스트림이 없다는 오류가 난다. ConsoleAppender를 추가했는데? 구글링
- 문제를 발견하고 다시 시도한다. 통과한다. 그런데 ConsoleAppender에 SYSTEM_OUT을 추가해야 한다? 이상하다
- SYSTEM_OUT 제거해도 통과한다. 그런데 PatternLayout 제거했더니 출력스트림이 없다는 오류가 난다
- 메뉴얼을 읽어보니 ConsoleAppender는 생성되지 않는 상태이다. 버그이거나 일관성이 없는거다.
이렇게 여러 테스트 케이스를 만들어보면서 사용법을 익힌다.
콘솔 로거를 초기화하는 방법을 익히고 독자적인 로거 클래스로 캡슐화한다.
그러면 나머지 프로그램은 log4j 경계 인터페이스를 몰라도 된다.
학습 테스트는 공짜 이상이다.
예? 외부 코드 중에서 필요한 지식만 확보하는 방법이므로 공짜라는 거다.
테스트에 투자하는 노력보다 얻는 성과가 더 크다. 외부 코드 버전이 바뀌면? 테스트를 돌려서 차이가 있는지 확인만 하면 된다.
학습 테스트는 외부 패키지가 예상대로 도는지 검증하는 것이다. 우리 코드에 통합해도 호환된다는 보장은 없다.
외부 코드에 오류가 있을 수도 있는 것이다. 오류가 있으면 또 변경되고, 새로운 버전이 나오고, 새로운 위험이 생긴다.
이럴 때 마다 학습 테스트를 통해서 위험을 밝혀내는 것이다.
이를 위해서는 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다. 새 버전으로 이전하기 쉽기 때문이다.
아직 존재하지 않는 코드 사용하기
경계의 또 다른 유형은, 아는 코드와 모르는 코드를 분리하는 경계이다. 알려고 해도 모를 수도 있다. 이는 내다보지 말자.
예를 들어서 송신기라는 하위 모듈을 다른 팀에서 만들고 있다고 해보자. 이 모듈은 인터페이스도 제대로 정의되어 있지 않다.
개발을 하다 보면 우리가 하위 모듈에 원하는 것이 무엇인지 파악하면서 경계 인터페이스가 무엇인지 알게된다.
따라서 자체적으로 우리 팀에서 인터페이스를 정의했다.
우리가 바라는 인터페이스를 만들었기 때문에 인터페이스를 우리가 전적으로 통제할 수 있고, 코드 가독성이 좋아진다.
그리고 나서, 다른 팀에서도 송신기 인터페이스를 만들었다.
서로 다른 인터페이스를 연결하기 위해서는? 어댑터 패턴을 사용하여 간극을 매꾼다.
이런 설계는 테스트도 간편하다. Fake 클래스를 만들어서 테스트할 수 있고, 실제로 하위 모듈 인터페이스가 나왔을 때 연결하여 테스트할 수도 있다.
깨끗한 경계
경계에서는 수 많은 일이 벌어지는데 대표적인 예가 변경이다. 좋은 SW는 변경하는데 비용이 적게 든다.
통제하지 못하는 코드를 사용할때는 너무 많은 투자를 하거나 변경 비용이 커지지 않도록 주의해야 한다.
- 경계 위치 코드는 깔끔히 분리
- 기대치를 정의하는 테스트 작성
- 통제 불가능한 외부 패키지 의존 대신, 우리 코드에 의존하도록 한다.
- 외부 패키지를 호출하는 코드를 가능한 줄여서 경계를 관리하자.
- ex) 클래스로 경계 감싸기
- ex) 어댑터 패턴 사용하여 인터페이스 변환
'Software Engineering > Clean Code' 카테고리의 다른 글
[클린 코드] #10 클래스 (1) | 2025.09.03 |
---|---|
[클린 코드] #9 단위 테스트 (0) | 2025.09.03 |
[클린 코드] #7 오류 처리 (0) | 2025.09.03 |
[클린 코드] #6 객체와 자료구조 (0) | 2025.09.03 |
[클린 코드] #5 형식 맞추기 (0) | 2025.09.03 |