⌗ 엘레강트 오브젝트 책을 리뷰해보도록 하겠습니다.
책의 내용은 다양한 언어로 되어있지만, 저는 예시 코드로 Typescript 를 사용해 제 사족을 조금씩 붙일 예정입니다.
생성자 하나를 주 생성자로 만드세요
class Cash {
private dollars: number;
public constructor(dollars: number) {
this.dollars = dollars;
}
}
constructor(생성자) 는 새로운 객체에 대한 진입점입니다.
위 예시처럼 생성자는 몇 개의 인자들을 전달받아 어떤 일을 수행한 후 객체를 준비시킵니다.
이 책의 저자는 올바른 클래스의 설계 결과물로 많은 수의 생성자와 적은 수의 메서드를 얘기하고 있습니다.
이 책에선 예시에 Java 코드를 활용해 여러 개의 생성자를 작성하고 있지만, typescript(javascript 도 마찬가지) 에선 조금 다른 형태를 취해야 합니다.
어찌됐건, 이 책에서 권장하는 올바른 설계를 따르면 생성자의 수가 메서드의 수보다 더 많아집니다.
별 근거는 없지만 2 ~ 3개의 메서드와 5 ~ 10개의 생성자를 포함하는 것이 적당하다고 얘기하고 있습니다.
근거는 없다고 하지만, 경우에 따라 그 경험을 기반으로 한 주장이 더 신빙성이 있을 수 있겠죠. 이론으로만 OOP 에 대해 얘기하는 것보단 실무에서 훨씬 실용적일 수 있다는 얘기이고, 이 책을 읽는동안은 이 의견을 존중해보도록 하겠습니다.
new Cash(30);
new Cash('$29.95');
new Cash(29.95, 'USD');
위 문장들은 모두 동일하게 행동하는 객체를 생성합니다. 생성자가 많아질수록 클라이언트가 클래스를 더 유연하게 사용할 수 있습니다.
메서드가 많아질수록 클래스를 사용하기는 더 어려워지고, 단일 책임 원칙을 위반합니다.
생성자의 주된 작업은 제공된 인자를 사용해 캡슐화하고 있는 프로퍼티를 초기화하는 일입니다.
초기화 로직을 단 하나의 생성자에만 위치시키고 주 생성자 라고 부르며, 부 생성자라고 부르는 다른 생성자들이 주 생성자를 호출하도록 만들기 바랍니다.
말하고자하는 바는 하나의 주 생성자와, 여러개의 부 생성자를 설계함으로써 여러 곳에서 유효성 검사 로직을 작성할 필요 없이 한 곳에서만 유효성 검사 로직을 해주면 되는 장점을 가져 중복 코드를 방지하고 유지보수성을 향상시킵니다.
내부 프로퍼티는 오직 한 곳에서만 초기화해야 한다는 핵심 원칙을 지키는 것입니다.
typescript 에선 아래와 같이 코드를 작성해야 할 겁니다.
class Cash {
private dollars: number;
public constructor(dollars: number | string) {
if (typeof dollars === 'number') {
this.dollars = dollars;
} else {
this.dollars = Number(dollars);
}
}
}
typescript 를 사용하다보니 이 책에서 정말 얘기하고자 하는 바를 코드에서 온전히 나타내기는 조금 어려워졌습니다.
위 예시는 심플하게 제가 작성한 것 일뿐 다른 형태로도 constructor overloading 과 비슷한 코드를 만들어낼 수 있습니다.
약간 다른 접근법을 알게 된 것이라면 저는 그동안 생성자의 dollars 가 number 타입만 받을 수 있게 만들어 두고 이 클래스를 통해 객체를 생성하고자 하는 클라이언트가 직접 형변환을 해서 객체를 생성해 사용하도록 설계를 해왔었습니다.
이 책에서 말하는 바는 이것부터가 클래스를 사용하기 어려워지고 유연함이 떨어진다고 말하는 포인트인 것 같습니다.
생성자에 코드를 넣지 마세요
생성자를 통해 들어오는 인자에 대해선, 인자에 손대지 말라는 한가지 규칙을 얘기하고 있습니다.
class Cash {
private dollars: number;
public constructor(dollars: string) {
this.dollars = Number(dollars);
}
}
위 예시에서 클래스가 내부에 캡슐화하고 있는 것은 number type 이지만, 생성자에 선언된 인자 타입은 string type 입니다.
그래서 Number 로 형변환을 하고 있지만, 이것은 아주 잘못된 방법입니다. 객체 초기화에는 코드가 없어야하고 인자를 건드려서는 안됩니다. 하지만 어쩔 수 없는 것 아닐까요? 좀 더 보도록 하겠습니다.
class StringAsInteger implements Number {
private source: string;
public constructor(source: string) {
this.source = source;
}
valueOf(): number {
return Number(this.source);
}
...
}
class Cash {
private dollars: Number;
public constructor(dollars: string) {
this.dollars = new StringAsInteger(dollars);
}
}
Number interface 를 구현하기 위한 다른 메서드들은 생략했습니다.
여기서 이전 예시와의 차이점은, 객체를 초기화 하는 시점에 곧장 string 을 number 로 변환한 것과 달리 실제로 사용하는 시점까지 객체의 변환 작업을 연기한 것입니다. 원래는 Cash 생성자가 받는 dollars 인자가 number 타입도 받을 수 있도록 해야하지만, 임의로 생략하겠습니다.
두 예시 모두 Cash 클래스로부터 인스턴스를 생성하는 과정은 동일해보입니다.
하지만 첫번째 예시는 생성자로 들어온 인자를 캡슐화하지만, 두번째 예시는 숫자처럼 보이는 StringAsInteger 인스턴스를 캡슐화합니다.
여기서도 StringAsInteger 클래스에는 생성자에 코드는 없습니다. 저자가 주장하는 바는 생성자에 코드가 없을 경우 성능 최적화가 더 쉽기 때문에 코드 실행속도가 더 빨라진다고 합니다. 예시를 보죠.
const test1 = new StringAsInteger('123');
console.info(test1.valueOf()); // 123
console.info(test1.valueOf()); // 123
이 예시에서 실제로 valueOf 를 두번 호출했으니, Number 에 의한 형변환도 두번 실행됩니다.
class StringAsInteger implements Number {
private num: number;
public constructor(source: string) {
this.num = Number(source);
}
valueOf(): number {
return this.num;
}
...
}
const test1 = new StringAsInteger('123');
console.info(test1.valueOf());
console.info(test1.valueOf());
이렇게만 보면 이 코드가 왜 이전 코드보다 더 느린지는 좀 의아하긴 합니다.
말하고자 하는 요점은 생성자에서 직접 파싱을 수행하는 경우 최적화가 불가능합니다.
객체를 만들 때마다 매번 형변환이 일어나므로 실행 여부를 제어할 수 없습니다(물론 형변환이 뭐 대단한 작업은 아니니 최적화까지 해야해? 라는 생각이 당연히 들 수 있지만 하나의 예시일 뿐 충분히 다른 코드가 들어갈 수 있으니까요)
만약 위 예시에서 valueOf 메서드를 호출할 일이 없다면, 형변환을 할 필요가 없었음에도 항상 실행하게 되는 불필요함이 있습니다.
생성자에 없어야 하는 코드가 형변환이 아니라 좀 더 무거운 작업이었다면, 그만큼 동작은 더 느렸을 것입니다.
반대로 인자를 전달된 상태 그대로 캡슐화하고 요청이 있을 때 파싱하도록 하면, 클래스의 사용자들이 파싱 시점을 자유롭게 결정할 수 있게 됩니다.
생성자에는 코드가 없게 함으로써 제어하기 쉽고 투명해집니다. 객체를 인스턴스화하는 동안에는 객체를 만드는 일 외에는 어떤 일도 수행하지 않으므로, 사용자가 쉽게 제어할 수 있는 투명한 객체를 만들 수 있으며 객체를 이해하고 재사용하기 쉬워집니다.
객체는 요청을 받을 때만 행동하고 어떤 일도 하지 않으므로, 좋은 느낌으로 게으릅니다.
생성자에서 어떤 일을 하고있다면, 이는 이후 리팩토링을 하기 어렵게 만듭니다. 결국 유지보수성과 아주 밀접한 부분이네요.
이번 챕터에서 얘기하고 있는 부분은 어느정도 이해는 되었지만, 생성자에 this.dollars = Number(dollars) 정도의 코드도 넣지 말아야 할까? 이런 부분은 좀 더 생각을 해보고 직접 클래스를 설계해보면서 겪어봐야 할 듯 합니다.
때로는 규칙을 무조건 지키다가 팀적인 차원에서 가독성이 안좋은 코드가 나오기도 했으니 말이죠..
'책 리뷰 > 엘레강트 오브젝트' 카테고리의 다른 글
엘레강트 오브젝트 [2장 - (4)] (0) | 2022.05.21 |
---|---|
엘레강트 오브젝트 [2장 - (3)] (0) | 2022.05.14 |
엘레강트 오브젝트 [2장 - (2)] (0) | 2022.05.07 |
엘레강트 오브젝트 [2장 - (1)] (0) | 2022.04.30 |
엘레강트 오브젝트 [1장 - (1)] (0) | 2022.04.16 |