모듈화된 코드 #좋은코드나쁜코드 #8장
* 본 아티클은, '좋은 코드 나쁜 코드' 8장을 기반으로 작성된 글입니다.
요구 사항이 어떻게 바뀔지는 예측할 수 없어도,
어떤 식으로든 바뀐다는 점은 확신할 수 있다. (대부분의 경우)
모듈화는 어떤 상황에서도 변경과 재구성이 용이한 코드를 작성하기 위해 중요하다.
요구사항이 변경되면, 관련 부분만 수정하면 되도록 말이다.
이는 간결한 추상화 계층을 기반으로 한다.
모듈화가 잘 이루어지면 적응성이 뛰어날 뿐만 아니라 시스템에 대한 추론도 쉬워진다.
재사용과 테스트에도 더 적합해진다.
의존성 주입
필드에 있는 인스턴스를 직접 생성하지 말고, 생성자 호출 인자 등을 통해 외부로부터 주입 받는다.
class RoutePlanner {
private final RoadMap roadMap;
RoutePlanner(RoadMap roadMap) {
this.roadMap = roadMap;
}
}
필요한 경우에는, 팩토리 함수를 통해 생성할 수도 있다.
static RoutePlanner createEuropeRoutePlanner() {
return new RoutePlanner(newEuropeRoadMap());
}
의존성 주입을 사용할 수 있다는 가능성을 의식적으로 고려하는 것이 좋다.
특히, static 함수에 과도하게 의존하는 정적 매달림은 좋지 않다.
주요 이유 중 하나는 테스트 더블을 사용할 수 없다는 것이다.
인터페이스에 의존하라
구체적인 구현 클래스에 의존하면 적응성이 제한된다.
interface RoadMap {
List<Road> getRoads
List<Junction> getJunctions();
}
class NorthAmericanRoadMap implements RoadMap {
...
}
class RoutePlanner {
private final RoadMap roadMap; // 인터페이스에 의존
// private final NorthAmericaRoadMap roadmap // 구체 클래스에 의존
RoutePlanner(RoadMap roadMap) {
this.roadMap = roadMap;
}
}
이를 통해 보다 더 간결한 추상화 계층, 더 나은 모듈화를 달성할 수 있다.
구체적인 것 말고 추상적인 것에 의존하라는 것은 SOLID 원칙 중 D에 해당하는 의존성 역전 원리(Dependency Inversion Principle)의 핵심이다.
클래스 상속을 주의하라
상속은 반드시 is-a 관계일 때만 사용해야 한다.
하지만 is-a 관계라고 무조건 안전한 것은 아니므로, 될 수 있다면 다른 방법을 사용해야 한다.
아래와 같은 문제가 있기 때문이다.
- 추상화 계층에 방해가 된다.
- Car, Aircraft가 있을 때, FlyingCar가 개발되었다면 무엇을 상속해야 할지 정하기 어렵다.
- 많은 언어가 다중 상속을 지원하지 않는다.
- 다중 상속을 지원하면, 다이아몬드 문제가 발생한다.
- Car, Aircraft가 있을 때, FlyingCar가 개발되었다면 무엇을 상속해야 할지 정하기 어렵다.
- 적응성 높은 코드 작성이 어려워진다.
- 슈퍼클래스가 수정된 후, 서브클래스의 동작이 안전한지 판단하기 어렵다.
따라서 구성(조합) 즉, Composition을 사용해야 한다.
클래스는 자신의 기능에만 집중해야 한다
다른 클래스의 지식을 필요로 하거나, 그것을 활용해 수정을 가하면 코드 모듈화가 깨진다.
클래스는 서로에 대한 어느 정도의 지식을 필요로 할 때도 있지만, 가능한 한 이것을 최소화해야 한다.
관련 있는 데이터를 함께 캡슐화하라
관련 있는 데이터가 캡슐화되지 않고 흩어져 있다면, 그것을 한 데 모아 사용하기까지 클래스끼리 서로의 구현을 지나치게 많이 알아야 한다.
함께 움직여야 하는 데이터라면, 하나의 객체로 캡슐화하여 다루는 것이 좋다.
반환 타입, 예외에 구현 세부 정보가 유출되지 않도록 하라
반환 타입 또는 그것의 필드가, 상위에서는 몰라도 되는 많은 정보를 담고 있을 수 있다.
class ProfilePictureService {
private final HttpFetcher httpFetcher;
...
ProfilePictureResult getProfilePicture(Int64 userId) { ... }
}
class ProfilePictureResult {
...
HttpResponse.Status getStatus() { ... }
HttpResponse.Payload? getImageData() { ...}
}
ProfilePictureService의 getProfilePicture() 메서드는 ProfilePictureResult를 반환하는데,
ProfilePictureResult는 게터 함수를 통해 HTTP 관련 객체를 반환한다.
이는 내부 구현이 HTTP와 관련되어 있음을 불필요하게 밝히는 것이다.
HTTP가 아닌 Web socket을 이용한 구현이 추가로 이루어져, 의존성 주입을 통해 로직이 변경될 수도 있다.
이런 경우에는 추상화 계층에 적합한 새로운 타입을 생성할 수 있다.
enum Status {
SUCCESS,
USER_DOES_NOT_EXIST,
OTHER_ERROR;
}
...
List<Byte>? getImageData() { ... }
예외의 경우도 마찬가지이다.
예외의 이름이 하위 객체와 관련되게 지어졌고, 비검사 예외라서 상위에서 그것이 해결되어야 한다면,
다른 구현을 사용할 때 예외 처리문이 문제가 될 수 있다.
추상화 계층에 적절한 예외를 만들어서 사용할 수도 있고,
표준 예외 타입을 사용할 수도 있다.