⌗ 엘레강트 오브젝트 책을 리뷰해보도록 하겠습니다.
책의 내용은 다양한 언어로 되어있지만, 저는 예시 코드로 Typescript 를 사용해 제 사족을 조금씩 붙일 예정입니다.
Chapter3. Employment
5개 이하의 public 메서드만 노출하세요
우아하고, 유지 보수가 가능하며 응집력이 높아 테스트하기에도 용이한 객체는 작은 객체입니다.
클래스의 크기를 정하는 기준으로 public 메서드의 개수를 사용하기를 권하며, 적절하다고 생각하는 public 메서드의 개수는 5개입니다.
당연히 저자의 개인 기준이죠.
4번째 메서드를 추가하고 5번째 메서드를 추가하기 전에 클래스의 크기에 대해 고민하기 바랍니다.
클래스가 작으면 메서드와 프로퍼티가 더 가까이 있을 수 있기 때문에 응집도가 높아집니다.
클래스에 두개의 프로퍼티가 있을 때 한 프로퍼티는 두개의 메서드에서만 사용된다면, 거의 연관성이 없는 독립적인 두 부분이 하나의 클래스에 뭉쳐있다고 볼 수도 있습니다. 이런 클래스는 대체로 응집도가 낮으며, 클래스가 작을 때 모든 메서드가 모든 프로퍼티와 상호작용할 가능성이 높아집니다.
정적 메서드(static method)를 사용하지 마세요
저도 은근히 많이 쓰는 정적 메서드이긴 합니다만, 정적 메서드만 놓고 보자면 쉽고 간편하게 사용할 수 있으며 사용성 측면에서도 꽤 직관적입니다. 하지만 문맥과 상관없이 정적 메서드를 사용하고 있는지 여부는 OOP 를 이해하지 못한 프로그래머를 구별하기 위한 최적의 지표입니다. 유지보수를 어렵게 만드는 정적 메서드의 현실적인 단점을 보도록 하겠습니다.
- 객체 대 컴퓨터 사고
const max = (a: number, b: number): number => {
if (a > b) return a;
return b;
};
위 코드는 별 문제도 없고, 실행도 잘 되며 이해하기 어렵지도 않습니다.
모든 컴퓨터는 실제로 이런 방식으로 동작하며, 프로그래머가 CPU 와 유사한 방식으로 수행될 작업을 CPU 에게 직접 지시하는 형태입니다. 우리가 결정하고 흐름은 항상 순차적이며 위에서 아래로 흐릅니다.
이런 순차적인 사고 방식을 가리켜 컴퓨터 입장에서 생각하기 라고 부릅니다.
우리는 CPU 에게 할 일을 지시하는게 아니라 정의해야 합니다. 컴퓨터처럼 생각하기에선 명령의 실행 흐름을 제어할 책임이 우리에게 있습니다. 객체지향적으로 생각하기에선 누구인지만 정의하고 객체들이 스스로 상호작용하도록 제어를 위임합니다.
class Max implements Number {
private readonly a: number;
private readonly b: number;
constructor(left: number, right: number) {
this.a = left;
this.b = right;
}
...
}
const x = new Max(5, 9);
OOP 의 관점에서 최대값을 계산하는 코드는 위와 같아야 합니다.
위 코드는 최대값을 계산하지 않습니다. x 가 5 와 9의 최대값이라는 사실만 정의할 뿐입니다.
const x = Math.max(5, 9);
일반적으로 Math 의 정적 메서드를 사용해 최대값을 구하는 코드입니다. 이는 절차적인 코드를 작성하도록 부추길 뿐입니다.
저자가 말하는 바가 대략적으로 이해는 가지만 Math.max 와 같은 편리함을 끊어낼 수 있을까.. 하는 생각은 다소 드는 부분이네요.
- 선언형 스타일 대 명령형 스타일
명령형 프로그래밍은 컴퓨터처럼 연산을 차례대로 실행합니다. 선언형 프로그래밍은 엔티티와 엔티티 사이의 관계로 구성되는 자연스러운 사고 패러다임에 더 가깝습니다.
const between = (l: number, r: number, x: number): number => {
return Math.min(Math.max(l, x), r);
};
const y = between(5, 9, 13);
console.info(y); // 9
between 이 어떤 객체의 정적 메서드라고 가정하고 보면, between 메서드를 호출하자마자 9 라는 값을 반환받습니다.
메서드를 호출한 시점에 CPU 가 즉시 결과를 계산하는데, 이것이 바로 명령형 스타일입니다.
class Between implements Number {
private readonly num: number;
constructor(l: number, r: number, x: number) {
this.num = new Min(new Max(l, x), r);
}
valueOf(): number {
return this.num.valueOf();
}
...
}
동일한 작업을 선언형 스타일로 작성한 코드입니다. Between 이 무엇인지 정의만하고, 사용자가 값을 계산하는 시점을 결정합니다.
CPU 에게 어떤 일을 하라고 지시하지 않고, 제어 서술 없이 로직만 표현했습니다.
그럼 이게 왜 좋을까요?
먼저 선언형 방식에서는 우리가 직접 성능 최적화를 제어할 수 있기 때문에 더 빠릅니다.
물론 인스턴스를 생성하는데 걸리는 시간이 있기 때문에 오직 하나의 정적 메서드만 호출하는 경우라면 명령형 스타일이 더 빠르다고 할 수 있습니다. 하지만 다수의 정적 메서드를 호출해야 하는 경우 명령형 방식에선 모든 정적 메서드를 순차적으로 호출해야만 합니다.
선언형 방식에선 CPU 에게 모든 것을 계산하라 말하지 않고, 결과가 필요한 시점과 위치를 결정하도록 위임합니다.
요점은 실행 관점에서 선언형 방식이 더 최적화되기 때문에 더 빠르다는 것입니다.
두번째는 다형성입니다. 다형성이란 코드 블록 사이의 의존성을 끊을 수 있는 능력을 말하는데, 작성한 선언형 방식에서는 무언가 로직을 수정해야할 경우 Between, Min, Max 가 전부 클래스이기 때문에 쉽게 분리가 가능합니다.
하지만 정적 메서드의 경우엔 그렇지 않죠. 따라서 선언형 프로그래밍을 이용하면 객체 사이의 결합도를 낮출 수 있어 더 나은 유지보수성으로 이어집니다.
그 외에 선언형 프로그래밍 방식을 따를 경우 표현력과 응집도가 좋아진다고 얘기하고 있습니다. 너무도 당연한 얘기라서 특별한 예시없이 다음으로 넘어가도록 하겠습니다.
- 유틸리티 클래스
유틸리티 클래스는 사실.. 정적 메서드들의 컬렉션이죠.
유틸리티 클래스는 절차적인 프로그래머들이 OOP 라는 영역에서 거둔 승리의 상징입니다. 끔찍한 안티패턴이라고 얘기하고 있습니다.
하지만 가끔 유틸리티 클래스가 정말 잘못되었을까.. 생각이 들긴 합니다. 안티패턴이라는 것은 이해하지만, 간혹 프로그래밍을 하다보면 정말 애매한 것들이 있습니다. 그냥 utils 에 넣으면 훨씬 깔끔할 것 같은데.. 하는 생각이 드는 일부 기능들이죠.
이 부분은 스스로 좀 더 고찰해봐야 할 것 같습니다.
- 싱글톤 패턴
싱글톤 패턴은 정적 메서드 대신 사용할 수 있는 유명한 개념입니다.
이 싱글톤으로 생성한 인스턴스는 오직 하나만 존재할 수 있고, 누구도 이 인스턴스에 접근할 수 있습니다.
유명한 디자인 패턴이지만, 끔찍한 안티패턴이라고 책에선 또다시 얘기하고 있습니다. 사실 유틸리티 클래스와 별반 다를게 없긴합니다.
// 싱글톤
class User {
private static INSTANCE: User = new User();
private name: string;
private constructor() {}
public static getInstance(): User {
return User.INSTANCE;
}
public getName(): string {
return this.name;
}
public setName(txt: string): void {
this.name = txt;
}
}
// 유틸리티 클래스
class User {
private static name: string;
private constructor() {}
public static getName(): string {
return User.name;
}
public static setName(txt: string): void {
User.name = txt;
}
}
이렇게만 보면 유틸리티 클래스와 싱글톤은 별 차이가 없습니다.
그나마 차이를 얘기한다면, 싱글톤은 분리 가능한 의존성으로 연결되어 있지만 유틸리티 클래스는 분리가 불가능한 결합도를 가진다는 것입니다. 싱글톤 패턴에선 캡슐화된 객체를 변경할 수 있기 때문에 유틸리티 클래스보다 더 좋다는 것이죠.
하지만 싱글톤은 여러 관점에서 봐도 전역 변수 그자체입니다. 전역 변수를 위한 자리는 없는 OOP 에선 끔찍한 안티패턴이죠.
대안은, 캡슐화입니다. 이런 전역 변수같은 무언가의 정보가 여러 객체에서 필요하다면, 그러한 객체 안에 이를 캡슐화하면 됩니다.
- 함수형 프로그래밍
객체의 크기가 작고 상태가 변하지 않으며 정적 메서드도 포함하지 않는다면 함수형 프로그래밍이 더 낫지 않느냐는 얘기도 있습니다.
이 책의 저자는 그럼에도 불구하고 OOP 가 함수형 프로그래밍보다 더 나은 부분은, 표현력이 더 뛰어나고 강력하다고 합니다.
FP 에서는 오직 함수만 사용할 수 있으나 OOP 에서는 객체와 메서드를 조합할 수 있습니다.
객체를 제공하는 FP 언어들이 있지만, 이런 언어들을 FP 요소를 포함하는 OOP 언어로 간주한다고 하네요.
사실 이 부분은 그냥 궤변을 통한 정신승리(?) 가 아닐까 싶긴 합니다만, 저는 FP 도 충분히 훌륭한 패러다임이라고 봅니다.
이 책에서 말하는 일급객체나 불변성, no side-effect 등은 FP 에서 먼저 나온 내용인 것 같기도 하구요.
이게 더 나으니 이것만 써라 보다는 OOP 와 FP 적절히 개발자가 사용해 생산성을 높이면서도 유지보수성을 끌어올리면 될 것 같습니다.
객체지향 프로그래밍이란 더 작은 객체들을 기반으로 더 큰 객체들을 조합하는 작업입니다.
함수형 프로그래밍도 더 작은 함수들을 기반으로 함수들을 조립해 큰 함수를 만드는 작업이죠. OOP 와 추구하는 바가 크게 다르진 않아 보입니다.
이번 포스팅에서 중요한 부분은, 정적 메서드는 조합이 불가능하고 합성이라는 아이디어와 대치됩니다. 이것이 OOP 에서 정적 메서드를
사용해선 안되는 이유입니다.
'책 리뷰 > 엘레강트 오브젝트' 카테고리의 다른 글
엘레강트 오브젝트 [3장 - (3)] (0) | 2022.06.11 |
---|---|
엘레강트 오브젝트 [3장 - (2)] (0) | 2022.06.05 |
엘레강트 오브젝트 [2장 - (4)] (0) | 2022.05.21 |
엘레강트 오브젝트 [2장 - (3)] (0) | 2022.05.14 |
엘레강트 오브젝트 [2장 - (2)] (0) | 2022.05.07 |