개요1. 의존성 주입의 사용을 고려하라하드 코드화된 의존성은 문제가 될 수 있다.해결책 : 의존성 주입을 사용하라의존성 주입 프레임워크의존성 주입을 염두에 두고 코드를 설계하라2. 인터페이스에 의존하라구체적인 구현에 의존하면 적응성이 제한된다해결책 : 가능한 경우 인터페이스에 의존하라3. 클래스 상속을 주의하라클래스 상속은 문제가 될 수 있다상속은 추상화 계층에 방해가 될 수 있다상속은 적응성 높은 코드의 작성을 어렵게 만들 수 있다해결책 : 구성을 사용하라구성의 이점 1 : 더 간결한 추상화 계층구성의 이점 2: 적응성이 높은 코드진정한 is-a 관계는 어떤가?4. 클래스는 자신의 기능에만 집중해야 한다다른 클래스와 지나치게 연관되어 있으면 문제가 될 수 있다해결책 : 자신의 기능에만 충실한 클래스를 만들라5. 관련 있는 데이터는 함께 캡슐화하라캡슐화되지 않은 데이터는 취급하기 어려울 수 있다해결책: 관련된 데이터는 객체 또는 클래스로 그룹화하라6. 반환 유형에 구현 세부정보가 유출되지 않도록 주의하라반환 형식에 구현 세부사항이 유출될 경우 문제가 될 수 있다해결책: 추상화 계층에 적합한 유형을 반환하라7. 예외 처리 시 구현 세부사항이 유출되지 않도록 주의하라예외 처리 시 구현 세부사항이 유출되면 문제가 될 수 있다해결책: 추상화 계층에 적절한 예외를 만들라요약
개요
- 1장에서는 소프트웨어 수명 주기 동안 요구사항이 어떻게 변하는지 논의 했다. 배포하기도 전에 요구사항이 바뀌는 경우가 많아서 코드를 작성하고 나서 몇 주 혹은 몇 개월 후에 이를 수정해야 하는 상황이 드물지 않게 볼 수 있다.
- 요구사항이 어떻게 바뀔지 예측하는 것은 대개 시간 낭비이고 , 요구사항이 어떤식으로든 바뀐다는 점은 어느 정도 확신할 수 있다
- 모듈화의 주된 목적 중 하나는 코드가 향후에 어떻게 변경되거나 재구성 될지 정확히 알지 못한 상태에서 변경과 재구성이 용이한 코드를 작성하는 것 ⇒ 각각의 기능(또는 요구사항)이 코드베이스의 서로 다른 부분에서 구현되어야 함
- 이번 장의 내용은 주로 2장에서 논의한 간결한 추상화 계층이라는 개념을 기초로 함
- 코드를 모듈화하는 것은 종종 하위 문제에 대한 해결책의 자세한 세부사항들이 독립적이고 서로 밀접하게 연관되지 않도록 하는 것으로 귀결됨
- 모듈화된 코드는 재사용과 테스트에 더 적합하기 때문에 코드 모듈화는 많은 이점을 가지고 있다.
1. 의존성 주입의 사용을 고려하라
하위 문제에 대해 해결책이 항상 하나만 존재하는 것은 아니므로 하위 문제를 재구성할 수 있는 방식으로 코드를 작성하는 것이 유용할 수 있다.
하드 코드화된 의존성은 문제가 될 수 있다.
- RoadMap은 여러 개의 다른 구현체를 갖는 인터페이스.
- 그러나 이 예에서 RoutePlanner 클래스는 생성자에서 NorthAmericaRoadMap 을 생성 ⇒ 의존성이 하드코딩 되어 있음
- 또 다른 문제는 NorthAmericaRoadMap의 생성자에 파라미터가 추가되었다고 가정하면, RoutePlanner에서 NorthAmericaRoadMap 을 생성하기 위해 필요한, NorthAmericaRoadMap 클래스에만 적용되는 개념을 처리해야 함
단점
- RoutePlanner 클래스를 다용도로 사용할 수가 없다.
- 북미 외의 지역에 대해 사용하려는 경우가 있을 수 있고 오프라인 상태에서도 애플리케이션이 작동하기를 원할 수 있는데 위와 같이 하드코딩 되어있으면 그렇게 사용할 수가 없음
해결책 : 의존성 주입을 사용하라
- 이렇게 로드맵을 주입하면 RoutePlanner 클래스의 생성자가 좀 더 복잡해진다는 단점이 있다. 몇 가지 팩토리 함수를 제공하면 이 과정이 훨씬 쉽게 될 수 있다.
- 팩토리 함수를 직접 작성하는 것의 대안으로 의존성 주입 프레임워크를 사용할 수도 있다
의존성 주입 프레임워크
- 중요한 점은 의존성 주입 프레임워크를 사용하면 팩토리 함수의 반복적인 코드를 작성하느라 허우적대지 않고 대신 매우 모듈화되고 다용도로 사용할 수 있는 코드를 만들 수 있다는 점임
- 주의할 점은 의존성 주입을 좋아하는 개발자라도 의존성 주입 프레임워크를 항상 사용하는 것은 아니라는 점. 주의해서 사용하지 않으면 파악하기 어려운 코드가 만들어질 수 있다.
의존성 주입을 염두에 두고 코드를 설계하라
- 위와 같이 구성하면 RoutePlanner 클래스는 RoadMap 인스턴스에 의존하지 않기 때문에 의존성 주입을 할 수 없다.
- 하나의 해결책만 있는 아주 근본적인 하위 문제라면 이렇게 해도 일반적으로 문제가 없지만, 상위 코드 계층에서 하위 문제에 대해 설정을 달리하고자 한다면 문제가 될 수있다.
하위 문제에 대해 해결책이 두 가지 이상 가능한 경우 인터페이스를 정의하는 편이 낫다.
2. 인터페이스에 의존하라
인터페이스에 의존하게 되면 어떤 구현 클래스라도 사용할 수 있으므로 코드가 훨씬 더 모듈화되고 적응성이 높아진다.
구체적인 구현에 의존하면 적응성이 제한된다
북미 외의 다른 어떤 지역에서도 작동하지 않는 RoutePlanner는 이상적인 클래스가 아니다. 이 클래스가 어떤 로드맵과도 동작하는 것이 더 바람직함
해결책 : 가능한 경우 인터페이스에 의존하라
구체적인 구현 클래스에 의존하면 인터페이스를 의존할 때보다 적응성이 제한되는 경우가 많다.
보다 구체적인 구현보다는 추상화에 의존하는 것이 낫다는 생각은 의존성 역전원리의 핵심이다.
3. 클래스 상속을 주의하라
- 두 가지 사물이 진정한 is-a 관계를 갖는다면 상속이 적절할 수 있다.
- 상속은 강력한 도구지만, 몇가지 단점이 있고 상속이 야기하는 문제가 치명적일 수 있기 때문에 한 클래스가 다른 클래스를 상속하는 코드를 작성하는 것에 대해서는 신중하게 생각해봐야 한다.
- 상속을 사용할 수 있는 상황에서 많은 경우 구성(composition)을 상속 대신 사용할 수 있다.
클래스 상속은 문제가 될 수 있다
- IntFileHandler 클래스는 슈퍼클래스인 CsvFileHandler의 함수를 마치 자신의 함수인 것처럼 액세스할 수 있으므로 IntFileReader 클래스 내에서 getNextValue()를 호출하면 슈퍼클래스의 함수가 호출된다
상속은 추상화 계층에 방해가 될 수 있다
한 클래스가 다른 클래스를 확장하면 슈퍼클래스의 모든 기능을 상속한다. 이 기능은 close() 함수의 경우처럼 유용할 때도 있지만, 원하는 것보다 더 많은 기능을 노출할 수도 있다.
위와 같이 상속시 IntFileHandler의 퍼블릭 API 는 아래와 같다
- 클래스의 일부 기능을 외부로 개방하는 경우 적어도 그 기능을 사용하는 개발자가 있을 것이라고 예상할 수 있고, 코드 베이스에서 해당 함수들을 사용하기 시작하면 IntFileHandler 클래스를 변경하기가 매우 어려워진다.
- IntFileHandler가 CsvFileHandler를 사용한다는 사실은 구현 세부사항이어야 하지만 상속을 통해 이 클래스의 함수들이 의도치 않게 외부에 공개된다.
상속은 적응성 높은 코드의 작성을 어렵게 만들 수 있다
IntFileReader 클래스를 통해 해결하려는 문제는 쉼표로 구분된 값을 가진 파일로부터 정수를 읽어들이는 것. 하지만 요구사항이 변경되어 쉼표뿐 아니라 세미콜론으로 구분된 값도 읽을 수 있어야 한다면.
- 쉼표로 구분된 파일 내용을 처리하는 것에 더해서 세미콜론으로 구분된 내용도 처리해야 하기 때문에 단순히 IntFileReader가 CsvFileHnadler(쉼표로 구분된 파일 내용처리) 대신 SemicolonFileHandler(세미콜론 구분 내용 처리) 를 상속하도록 바꿀 수 없다.
- 유일한 방법은 IntFileReader 클래스의 새 버전을 작성하고 이 클래스가 SemicolonFileHandler를 상속하도록 하는 것
- 새 클리스는 IntFileReader 클래스의 대부분을 그대로 가지고 있다. ⇒ 코드중복으로 인한 유지보수 비용 & 버그 발생 가능성 높임
- FileValueReader 인터페이스는 파일 형식을 모르더라도 값을 읽을 수 있는 추상화 계층을 제공함. 하지만 상속을 사용했기 때문에 이러한 추상화 계층을 활용할 수 없게 되었다.
해결책 : 구성을 사용하라
상속을 사용한 원래 동기는 IntFileReader 클래스를 구현하는 데 도움이 되고자 CsvFileHandler 클래스의 일부 기능을 재사용하는 것
CsvFileHandler의 기능을 재사용하는 다른 방법으로는 구성을 사용하는 것
- CsvFileHandler를 직접 사용하는 대신 FileValueReader 인터페이스 이용
- IntFileReader 클래스는 CsvFileHandler를 확장하는 대신 FileValueReader의 인스턴스를 참조할 멤버 변수를 가짐
- IntFileReader.close() 함수는 파일을 닫는 명령을 FileValueReader.close() 함수로 전달(
forwarding
)
구성의 이점 1 : 더 간결한 추상화 계층
- 상속 사용 시, 서브 클래스는 슈퍼클래스의 모든 기능을 상속하고 외부로 제공
- 상속 대신 구성을 사용하면 IntFileReader 클래스가 전달이나 위임을 사용하여 명시적으로 노출하지 않는 한 CsvFileHandler 클래스의 기능이 노출되지 않음
구성의 이점 2: 적응성이 높은 코드
앞의 변경된 요구사항은 쉼표만이 아니라 세미콜론으로 구분된 값을 사용하는 파일도 지원해야 한다는 것
이제 IntFileReader 클래스는 FileValueReader 인터페이스에 의존하며 의존성 주입을 통해 이 요구사항을 쉽게 지원 가능
진정한 is-a 관계는 어떤가?
두 클래스가 진정으로 is-a 관계일 때조차 상속하는 것이 좋은 접근법인지에 대해서는 명확하지 않을 수 있다.
주의할 점
- 취약한 베이스 클래스 문제 : 서브클래스가 슈퍼클래스에서 상속되고 슈퍼클래스가 나중에 수정되면 서브클래스가 작동하지 않을 수 있다.
- 다이아몬드 문제 : 일부 언어에서 두개 이상의 슈퍼클래스를 확장할 수 있을 때, 여러 슈퍼클래스가 동일한 함수의 각각 다른 버전을 제공하는 경우 문제가 발생할 수 있음
- 문제가 있는 계층 구조 : 많은 언어가 다중 상속을 지원하지 않으므로 클래스는 오직 하나의 클래스만 직접 확장할 수 있다. 이를 단일상속이라 하며 다른 유형의 문제가 발생할 수 있다.
- 두 타입(Car, Aircraft)을 모두 갖는 타입(FlyingCar)가 발생 시, 계층 구조에 포함시킬 수 있는 합리적 방법이 없음
클래스 상속에 숨어있는 많은 함정을 피하면서 계층구조를 달성하기 위해 다음과 같은 것들을 할 수 있다.
- 인터페이스를 사용하여 계층 구조를 정의
- 구성을 사용하여 코드를 재사용
4. 클래스는 자신의 기능에만 집중해야 한다
- 모듈화의 핵심 목표 중 하나는 요구 사항이 변경되면 그 변경과 직접 관련된 코드만 수정한다는 것
- 단일 개념이 단일 클래스 내에 완전히 포함된 경우라면 이 목표는 달성할 수 있다.
- 어떤 개념과 관련된 요구사항이 변경되면 그 개념에 해당하는 단 하나의 클래스만 수정하면 된다.
- 이것과 반대되는 상황은 하나의 개념이 여러 클래스에 분산되는 경우다. 해당 개념과 관련된 요구사항을 변경하려면 관련된 클래스를 모두 수정해야 한다.
다른 클래스와 지나치게 연관되어 있으면 문제가 될 수 있다
Book 클래스에서 Chapter클래스에 대해 지나치게 많이 알고 있다.
해결책 : 자신의 기능에만 충실한 클래스를 만들라
코드 모듈화를 유지하고 한 가지 사항에 대한 변경 사항이 코드의 한 부분만 영향을 미치도록 하기 위해, Book과 Chapter 클래스는 가능한 한 자신의 기능에만 충실하도록 해야 한다.
디미터의 법칙
한 객체가 다른 객체의 내용이나 구조에 대해 가능한 한 최대한으로 가정하지 않아야 한다는 소프트웨어 공학의 원칙. 이 원칙은 특히 한 객체는 직접 관련된 객체와만 상호작용 해야한다고 주장
5. 관련 있는 데이터는 함께 캡슐화하라
서로 다른 데이터가 서로 밀접하게 연관되어 있어 그것들이 항상 함께 움직여야 할 때가 있다. 이 경우에는 클래스(또는 유사한 구조)로 그룹화하는 것이 합리적이다.
캡슐화되지 않은 데이터는 취급하기 어려울 수 있다
- 위 예제코드에서 displayMessage() 함수는 uiSettings 클래스의 일부 정보를 renderText() 함수로 전달하는 택배기사와 비슷하다. 실제 생활에서 택배기사는 종종 소포 안에 무엇이 들어있는지 정확히 신경쓰지 않을 것
- 앞에서 살펴봤듯이 모듈화의 목적 중 하나는 요구사항의 변경이 있을 때 해당 요구사항과 직접 관련 있는 코드만 수정하고자 하는 것임. rednerText() 함수에 글꼴 스타일을 정의해야 하는 경우 이 새로운 정보를 전달하기 위해 displayMessage도 함께 수정해야 함
- 예제에서 UiSettings와 TextBox 클래스만 실제로 텍스트 스타일을 처리하기 때문에 displayMessage() 함수까지 수정해야 하는 것은 바람직하지 않음
해결책: 관련된 데이터는 객체 또는 클래스로 그룹화하라
TextOptions 클래스에 텍스트 스타일 정보를 캡슐화해서 해당 인스턴스를 전달 ⇒ 박스 안에 뭐가 들어 있는지 신경쓰지 않고 부지런히 소포를 배달만 하는 택배 기사와 같음
6. 반환 유형에 구현 세부정보가 유출되지 않도록 주의하라
- 구현 세부 정보가 유출되면 코드의 하위 계층에 대한 정보가 노출될 수 있으며, 향후 수정이나 재설정이 매우 어려워질 수 있다.
- 코드에서 구현 세부정보를 유출하는 일반적인 형태 중 하나는 해당 세부 정보와 밀접하게 연결된 유형을 반환하는 것이다.
반환 형식에 구현 세부사항이 유출될 경우 문제가 될 수 있다
- Http 통신을 한다는 구현 세부사항이 유출됨
- 다른 개발자가 위 클래스를 사용하려면 HttpResponse와 관련된 여러 개념을 처리해야 함. 프로피 사진 요청의 성공 여부와 실패한 이유를 이해하려면 HttpResponse.Status 열거 값을 확인해야 함
- 또한 getProfilePicture 메서드를 호출하는 모든 코드는 이 함수의 반환값을 처리하기 위해 HttpResponse.Status와 HttpResponse.Payload를 다뤄야 함 → HttpResponse에 특정된 유형을 반환한다는 사실에 의존
- 만약 웹소켓을 이용해 가져오는식으로 구현이 변경되면?? 너무 많은 코드가 변경되어야 함
해결책: 추상화 계층에 적합한 유형을 반환하라
ProfilePictureService 클래스가 해결하는 문제는 사용자의 프로필 사진을 가져오는 것임. 따라서 이 클래스를 통해 제공하고자 하는 이상적인 추상화 계층은 모든 반환 형식은 이 점을 반영해야 함
노출해야 할 최소한의 개념
- 요청이 성공하거나 다음 이유중 하나로 실패할 수 있따
- 사용자가 존재하지 않음
- 서버가 연결할 수 없는 등의 일시적인 오류발생
- 프로필 사진을 나타내는 데이터의 바이트 값
- HttpResponse.Status 와 HttpResponse.Payload 는 우리가 제공하는 추상화 계층에 적합하지 않다.
- 외부로 노출할 개념을 최소화하는 유형을 새로 정의해 사용하면 좀 더 모듈화된 코드와 간결한 추상화 계층을 얻을 수 있다.
7. 예외 처리 시 구현 세부사항이 유출되지 않도록 주의하라
호출하고자 하는 쪽에서 복구하고자 하는 오류에 대해 비검사 예외를 사용하는 경우 예외 처리 시 구현 세부 정보를 유출하는 것은 특히 문제가 될 수 있다
예외 처리 시 구현 세부사항이 유출되면 문제가 될 수 있다
- 위와 같은 상황에서
TextSummerizer
는PredictionModelException
이라는 모델 기반 예측을 사용한다는 세부 구현사항을 알리는 예외를 처리해야 하게 됨
- ModelBasedScorer는 구현 클래스 중 하나이기에 TextImportanceScorer를 구현하는 다른 클래스에서 완전히 다른 유형의 예외를 발생시킬 수도 있다.
해결책: 추상화 계층에 적절한 예외를 만들라
- TextSummerizer 클래스를 사용하는 개발자는 이제 TextSummerizerException만 처리하면 됨.
- TextImportanceScorer 클래스를 사용하는 곳에서도 TextImportanceScoreException 만 처리하면 됨
요약
- 코드가 모듈화되어 있으면 변경된 요구사항을 적용하기 위한 코드를 작성하기 쉽다
- 모듈화의 주요 목표 중 하나는 요구 사항의 변경이 해당 요구사항과 직접 관련된 코드에만 영향을 미치도록 하는 것
- 코드를 모듈식으로 만드는 것은 간결한 추상화 계층을 만드는 것과 깊은 관련이 있다.
- 다음의 기술을 사용하여 코드를 모듈화 할 수 있다
- 의존성 주입
- 구체적인 클래스가 아닌 인터페이스에 의존
- 클래스 상속 대신 인터페이스 및 구성의 활용
- 클래스는 자신의 기능만 처리
- 관련된 데이터의 캡슐화
- 반환 유형 및 예외 처리 시 구현 세부 정보 유출 방지