⌗ 엘레강트 오브젝트 책을 리뷰해보도록 하겠습니다.
책의 내용은 다양한 언어로 되어있지만, 저는 예시 코드로 Typescript 를 사용해 제 사족을 조금씩 붙일 예정입니다.
부 생성자 밖에서는 new 를 사용하지 마세요
의존성에 대한 얘기를 잠깐 해야할 것 같습니다.
class Cash {
private readonly dollars: number;
public euro(): number {
return new Exchange().rate('USD', 'EUR') * this.dollars;
}
}
위 예시는 의존성에 문제가 있는 코드의 모습을 보여주고 있습니다. 바로 euro 메서드 안에서 new 연산자를 이용해 Exchange 인스턴스를 생성하고 있기 때문인데요. 클래스가 작고 단순하며 값비싼 자원을 사용하지 않는다면 문제가 되지 않겠지만, 이렇게 직접 연결되어 있는
하드코딩된 의존성 문제는 의존성을 끊기 위해서는 Cash 클래스의 내부 코드를 변경할 수 밖에 없습니다.
우리는 euro 메서드가 내부에서 어떻게 동작하는지는 아무런 관심이 없고, 결과만 알고 싶을 뿐입니다.
하지만 예를들어 euro 메서드를 테스트한다고 할 때, Exchange 객체에 의해 매번 네트워크 통신이 발생할 수도 있습니다.
현재의 설계에서 방법은 억지로 Exchange 클래스를 끄집어내 모킹을 한 후 테스트를 하는 것입니다.
이 문제의 근본 원인은 new 연산자이고, Cash 가 Exchange 의 인스턴스를 직접 생성한 점이 문제입니다.
메서드 내부에서 new 연산자를 사용할 수 없도록 금지했다고 가정하겠습니다. 이제 객체가 새로운 객체를 직접 생성할 수 없기에
새로운 객체를 생성자의 인자로 전달받아 private 프로퍼티 안에 캡슐화할 수 밖에 없습니다.
class Cash {
private readonly dollars: number;
private readonly exchange: Exchange;
constructor(dollars: number, exchange: Exchange) {
this.dollars = dollars;
this.exchange = exchange;
}
public euro(): number {
return this.exchange.rate('USD', 'EUR') * this.dollars;
}
}
이렇게 수정하면, 이제 테스트시에도 Cash 객체를 생성할 때 두번째 인자에 Fake Exchange 객체를 전달해 쓸데없는 통신이 발생하지 않도록 막을 수 있습니다. Cash 클래스는 더이상 Exchange 인스턴스를 직접 생성할 수 없고, 오직 생성자를 통해 제공된 Exchange 와만 협력할 수 있습니다.
객체가 필요한 의존성을 직접 생성하는 대신 필요한 의존성을 생성자를 통해 주입하는 것은 아주 좋은 프랙티스입니다.
모든 객체를 이런 방법으로 설계해야 합니다.
이 책은 예시 코드들이 java 로 작성되어 있기 때문에, 이런 상황에서 편의를 위해 부 생성자를 여러개 추가할 수도 있지만 부 생성자를 제외한 어떤 곳에서라도 new 를 사용하지 말라고 얘기합니다. typescript 에선 여러개의 생성자를 둘 수 없기 때문에 해당되지 않는 내용이긴 합니다만, 이렇게 new 를 금지한다면 객체들은 충분히 분리되어 테스트 용이성과 유지보수성을 향상시킬 수 있습니다.
이 규칙이 DI(의존성 주입)와 IOC(제어의 역전)에 관해 알아야 하는 전부입니다.
타입 인트로스펙션과 캐스팅을 피하세요
타입 인트로스펙션과 타입 캐스팅을 사용하고 싶은 유혹에 빠지더라도 절대 사용해서는 안됩니다.
java 에선 instanceof 연산자와 Class.cast 메서드 등이 있지만, typescript 에도 instanceof 연산자가 있고 typeof, type assertion 등이 기술적인 관점에서 이 범주에 들어갈 것 같습니다.
const size = (items: Array<number> | Set<number>) => {
if (items instanceof Set) {
return items.size;
} else {
return items.length;
}
};
위 예시는 items 의 타입을 조사해 타입 별로 다른 처리를 하고 있습니다. 타입 인트로스펙션은 말 그대로 타입을 조사하는 것입니다. 리플렉션 이라는 더 포괄적인 용어로 불리는 기법들 중 하나인데, 이를 사용하면 메서드, 명령어, 구문, 클래스, 객체, 타입 등을 변경할 수 있습니다. 이는 코드를 유지보수하기 참 어렵게 만드는 지저분한 기법인데, 코드가 런타임에 다른 코드에 의해 수정된다는 사실을 항상 기억해야 한다면 코드를 읽기가 매우 어려울 것입니다.
타입에 따라 객체를 차별하기 때문에 OOP 의 기본 사상을 심각하게 훼손시키고, 런타임에 객체의 타입을 조사하는 것은 클래스 사이의 결합도가 높아지기 때문에 기술적인 관점에서도 좋지 않습니다.
하지만 이를 해결하려면 메서드 혹은 함수 오버로딩을 사용해야 하는데, javascript 나 typescript 는 이를 제대로 지원하지 않습니다.
따라서 우린 어쩔 수 없이 typeof, instanceof 등의 연산자를 사용해야 합니다.
아니면 차라리 서로 다른 두개의 메서드를 만들어놓고 사용자가 결정하도록 하는 방법도 있겠네요.
type Cash = {
dollars: number;
}
const dollars = (items: Array<any>) => {
return (items as Array<Cash>).map(item => item.dollars);
};
좀 억지스러운 type assertion 예시이지만, 이런 형태도 사전에 약속하지 않았던 새로운 계약을 따르도록 강제하는 것이므로 지금까지 얘기한 내용과 동일하게 적용됩니다. 생각보다 이런짓거리를 자주 하게되는데, 이런 부분때문에 typescript 에선 any 를 최대한 멀리하려 합니다. java 에서 통용되는 연산자나 메서드를 그대로 예시로 설명할 순 없지만, 이런 개념들이 안티패턴임을 다시한번 기억해야겠습니다.
'책 리뷰 > 엘레강트 오브젝트' 카테고리의 다른 글
엘레강트 오브젝트 [4장 - (2)] (0) | 2022.06.25 |
---|---|
엘레강트 오브젝트 [4장 - (1)] (0) | 2022.06.18 |
엘레강트 오브젝트 [3장 - (2)] (0) | 2022.06.05 |
엘레강트 오브젝트 [3장 - (1)] (0) | 2022.05.28 |
엘레강트 오브젝트 [2장 - (4)] (0) | 2022.05.21 |