8장 경계
1. 📌 핵심 개념 정리
✅ 요약하기
1. 외부 코드 사용하기
- 패키지 제공자와 인터페이스 사용자 사이에는 적용성 극대화 vs. 사용자 요구 집중이라는 긴장이 존재하며, 이는 시스템 경계에서 문제를 일으킬 수 있습니다.
- 외부 코드를 사용하면 적은 시간에 더 많은 기능을 출시하기 쉽습니다.
2. 경계 살피고 익히기
- 외부 패키지 테스트가 우리의 직접적인 책임은 아니지만, 사용할 코드를 스스로 테스트하는 것이 바람직합니다.
- 타사 라이브러리 사용법이 불분명할 때, 문서를 읽고 우리 코드를 작성하여 예상대로 동작하는지 확인하는 과정에서 버그를 찾느라 오랜 시간이 걸릴 수 있습니다.
- 학습 테스트는 곧바로 우리 쪽 코드를 작성해 외부 코드를 호출하는 대신, 먼저 간단한 테스트 케이스를 작성하여 외부 코드를 익히는 방법입니다. 이는 프로그램에서 사용하려는 방식대로 외부 API를 사용하는 데 초점을 맞춥니다.
3. log4j 익히기
- log4j 패키지 사용 예시를 통해 학습 테스트의 과정을 보여줍니다.
- 개선 전:
testLogCreate
,testLogAddAppender
등의 테스트 코드를 작성하며 Appender, 출력 스트림,PatternLayout
등의 개념을 오류를 통해 점진적으로 파악하는 과정을 보여줍니다. 기본ConsoleAppender
생성자는 설정되지 않은 상태임이 확인됩니다. - 개선 후: 학습 테스트를 통해 log4j의 동작 방식을 파악하고, 이를 바탕으로 log4j를 래핑하는
LogTest
클래스를 만들어 나머지 코드에서는 log4j의 내부 동작 원리를 알 필요 없게 만듭니다.
4. 학습 테스트는 공짜 이상이다
- 학습 테스트는 투자하는 노력보다 더 큰 성과를 가져다줍니다.
- 패키지의 새 버전이 나올 때 학습 테스트를 돌려 변경 사항이나 호환성 문제를 확인할 수 있습니다.
- 학습 테스트는 패키지가 예상대로 작동하는지 검증하며, 새 버전과의 비호환성 문제를 즉시 발견할 수 있도록 돕습니다.
- 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스는 학습 테스트 유무와 관계없이 필요하며, 학습 테스트는 새 버전으로의 이전을 용이하게 합니다.
5. 아직 존재하지 않는 코드를 사용하기
- 상대 팀이 아직 API를 설계하지 않았을 때, 우리가 바라는 인터페이스를 미리 구현하면 인터페이스를 전적으로 통제하고 코드 가독성과 의도를 명확히 할 수 있다는 장점이 있습니다.
- API 인터페이스가 나온 후에는 경계 테스트 케이스를 생성하여 우리가 API를 올바르게 사용하는지 테스트할 수 있습니다.
- 아는 코드와 모르는 코드를 분리하는 경계를 설정하고, 필요한 기능에 대해 인터페이스를 먼저 정의하는 것이 중요합니다.
6. 깨끗한 경계
- 소프트웨어 설계가 우수하다면 변경에 많은 투자와 재작업이 필요하지 않습니다.
- 통제할 수 없는 외부 코드를 사용할 때는 과도한 투자나 향후 변경 비용 증가를 방지하기 위해 주의해야 합니다.
- 경계에 위치하는 코드는 깔끔하게 분리하고, 기대치를 정의하는 테스트 케이스를 작성해야 합니다.
- 통제 불가능한 외부 패키지에 의존하는 대신 통제 가능한 우리 코드에 의존하는 것이 좋습니다.
- 외부 패키지를 호출하는 코드를 가능한 줄여서 경계를 관리해야 합니다.
- ADAPTER 패턴을 사용하여 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하거나, 새로운 클래스로 경계를 감싸 외부 라이브러리를 직접 사용하는 대신 적절한 인터페이스를 정의하여 내부 코드와 분리하는 것이 좋습니다.
- 이는 코드 가독성을 높이고, 경계 인터페이스 사용의 일관성을 유지하며, 외부 패키지 변경에 유연하게 대응할 수 있도록 돕습니다.
- 경계를 명확히 하면 외부 코드 변경에 쉽게 대처하고 코드 유지보수성을 높일 수 있습니다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
1. 경계 테스트
- 어려웠던 부분: 입력 값의 경계에서 오류가 발생할 가능성이 높다는 점을 이용하여, 최소값, 최대값, 경계 바로 안쪽과 바깥쪽 값을 테스트하는 기법.
- 이해한 점: (이미 이해함) 입력 값의 경계에서 오류 발생 가능성을 활용한 테스트 기법.
2. ADAPTER 패턴
- 어려웠던 부분: 어댑터 패턴(Adapter pattern)은 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해줍니다. 이름 그대로 클래스를 어댑터로서 사용되는 구조 패턴입니다. 이를 객체 지향 프로그래밍에 접목해보면, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들을 함께 작동해주도록 변환 역할을 해주는 행동 패턴이라고 볼 수 있습니다. 이론만 알지 다뤄본 경험이 없어 이해가 안 갔습니다. 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하는 방법이 궁금합니다.
- 이해한 점:
- 서로 다른 두 서비스를 중간에 위치해 연결해 주는 역할을 하는 패턴을 말합니다.
- 기존 코드를 변경하지 않고 연결해 주는 역할인 어댑터 클래스를 만들어서 변환합니다.
- 기존 클래스를 변경하지 못하거나 새로운 시스템을 기존 시스템과 호환되게 할 때 사용합니다.
- 객체 어댑터 (Object Adaptor): 합성(Composition)된 멤버에게 위임을 이용하여 런타임 중
Adaptee
결정이 가능하며 유연하지만 공간 차지 비용이 발생할 수 있습니다. - 클래스 어댑터 (Class Adaptor): 클래스 상속을 이용하여 코드 재사용이 용이하지만 자바의 다중 상속 불가 문제로 일반적으로 권장되지 않습니다.
- 사용 시기: 레거시 코드 사용 시 인터페이스 비호환, 수정 불가능한 라이브러리 재사용, 기존 클래스를 새로운 API에 맞게 개조, 소프트웨어 구/신 버전 공존 등.
- 장점: 단일 책임 원칙(SRP) 및 개방 폐쇄 원칙(OCP) 만족, 빠른 메소드 추가, 쉬운 버그 검사.
- 단점: 코드 복잡성 증가, 때로는 직접 서비스 클래스 변경이 더 간단할 수 있음.
- 변환 방법:
- 우리가 원하는 타깃 인터페이스를 정의합니다.
public interface MapService { Location getCoordinates(String address); }
- 패키지가 제공하는 인터페이스(외부 라이브러리)를 확인합니다.
public class GoogleMapsAPI { public LatLng getLatLng(String address) { // 실제 Google Maps API 호출 (가정) return new LatLng(37.7749, -122.4194); } }
- Adapter 클래스를 생성하여 변환 역할을 수행합니다. 외부 라이브러리의 메소드를 호출하고, 그 결과를 우리가 원하는 형태로 변환하여 타깃 인터페이스를 통해 제공합니다.
public class GoogleMapsAdapter implements MapService { private final GoogleMapsAPI googleMapsAPI = new GoogleMapsAPI(); @Override public Location getCoordinates(String address) { LatLng latLng = googleMapsAPI.getLatLng(address); return new Location(latLng.getLatitude(), latLng.getLongitude()); } }
- 사용 시에는 Adapter 클래스를 통해 원하는 인터페이스를 호출합니다.
public class Main { public static void main(String[] args) { MapService mapService = new GoogleMapsAdapter(); Location location = mapService.getCoordinates("San Francisco"); System.out.println("위도: " + location.getLatitude() + ", 경도: " + location.getLongitude()); } }
- 우리가 원하는 타깃 인터페이스를 정의합니다.
- 예시:
Micro USB
를 사용하는 핸드폰에USB-C
충전기를 사용하기 위한USBAdapter
클래스 구현.