[책 리뷰] 엘레강트 오브젝트를 읽고



글또내의 ‘#스터디또’ 라는 채널에서 엘레강트 오브젝트 라는 책을 같이 스터디 할 팀원을 모집하는 것을 봤다! 책도 얇고 가볍게 읽을 수 있겠다 싶었다. 또한 스터디를 진행하는 방식이 맘에 들어 참여하게 되었다. 한달여간 진행했고, 막 마쳤는데 **해당 책을 읽으며 조금은 눈에 띄었던 챕터를 정리**해 본다.

소개할 챕터는 ‘정적 메서드를 사용하지 마세요’‘절대 getter와 setter를 사용하지 마세요’ 이다. 궁금 벌써부터 궁금하지 않은가! — # 3.2 정적 메서드를 사용하지 마세요 저자는 OOP에 NULL을 도입한 것 이상으로 정적 메서드는 커다란 실수였다고 한다. 정적 메서드 대신 객체를 사용하라. 정적 메서드는 객체를 새로 생성하지 않기때문에 더 빠르다고 생각한다. 또한 가비지컬렉션에 신경 쓸 필요도 없으며 ‘유틸리티’ 클래스에 정적 메서드를 모아놓을 수 있다. 하지만 잘못됐다. 정적 메서드는 객체 패러다임의 남용이다. 왜냐면 **유지보수를 어렵게 만들기 때문**이다.

## 3.2.1 객체 대 컴퓨터 사고 (object vs. computerthinking) ### 절차적 프로그래밍

  • 제공된 명령어를 하나씩 순차적으로 실행한다.
  • 우리가 결정하고 컴퓨터는 따르며, 흐름은 순차적이고 위에서 아래로 흐른다. 우리는 컴퓨터에게 할 일을 지시하는 것이 아니라 정의해야한다. (함수형 프로그래밍이 그 예다)
    // 올바른 예
    Number x = new Max(5, 9);
    //나쁜 예
    int x = Math.max(5, 9);
    

## 3.2.2 선언형 스타일 대 명령형 스타일

  • 명령형 프로그래밍
  • 프로그램의 상태를 변경하는 문장을 사용해서 계산 방식을 서술
  • 컴퓨터처럼 연산을 차례대로 실행
  • 선언형 프로그래밍
  • 제어 흐름을 서술하지 않고 계산 로직을 표현
  • ‘엔티티’와 엔티티 사이의 ‘관계’로 구성되는 자연스러운 사고 패러다임 최대 수를 구할 때 if(a > b) 라는 조건은 명령형이든 선언형이든 무조건 볼 수 밖에 없다. 하지만 **둘의 차이점은 다른 클래스, 객체, 메서드가 이 기능을 사용하는 방법**에 있다.

### 예시 두개의 정수로 구성된 간격이 있고 그 간격 사이에 존재해야하는 정수가 있는데, 해당 정수가 간격안에 포함되는지 여부를 확인하는 예제이다.

### 명령형 스타일

public static int between (int l, int r, int x) {
return Math.min(Math.max(l, x), r);
}
//실행시
int y = Math.between(5, 9, 13); //9를 반환

정적 메서드의 경우 Math.max() 를 사용하기 위해 또 다른 정적 메서드인 between() 을 만들어야 한다.

### 선언형 스타일

class Between implements Number {
private final Number num;
Between (Number left, Number right, Number x) {
this.num = new Min(new Max(left, x), right);
}
@Override
public int intValue() {
return this.num.intValue();
}
}
//실행시
Number y = new Between(5, 9, 13); //아직!

해당 방식은 **Between이 무엇인지만 정의하고 사용자가 값을 계산하는 시점을 결정*한다. 그래서 더 선언형이 더 좋은이유는? *1. 빠르다

  • 선언형 방식에서는 우리가 직접 성능 최적화를 제어할 수 있기때문에 빠르다. (하지만 현재의 언어에는 명령형 방식이 선언형 방식보다 빠르다)
  • 하나의 정적메서드의 경우 정적메서드가 빠르지만 다수의 정적메서드를 호출해야 하는 경우엔 다르다. 2. 다형성
  • Between, Max, Min은 모두 클래스이기 때문에 각 객체들을 쉽게 분리하거나 넘겨줄수 있다. (하지만 정적메서드는 불가능하다)
  • 객체 사이의 결합도를 낮출 수 있고 우아하게 처리할 수 있다. 낮은 결합도는 나은 유지보수성으로 이어진다. 3. 표현력
  • 선언형은 결과를 이야기하지만 명령형 방식은 수행가능한 한가지 방법을 이야기한다.
  • 알고리즘과 실행 대신 객체와 행동의 관점에서 사고하기 시작한다. 4. 코드 응집도
  • 관련된 코드들이 한곳에 뭉쳐있도록 한다.
  • 시간적인 결합 문제를 제거할 수 있으며, 유지보수성을 개선할 수 있다. 작은 비즈니스를 처리하려고 정적 메서드를 사용하다간 큰 비즈니스를 구현하기 어려워 질 수 있다. 정적 메서드를 재사용하면 깔끔한 객체지향 코드를 작성할 수 없다.

### 모든 코드가 정적 메서드로 가득차 있다면? → 사용하지 말아라. 가장 좋은 방법은 우리의 코드가 객체를 직접 처리할 수 있도록 정적 메서드를 감싸는 클래스를 만들어 고립시키는 것이다.

## 3.2.3 유틸리티 클래스 유틸리티 클래스는 정적 메서드들을 모아 놓은 정적 메서드들의 컬렉션(다른말로 Helper라고도 부른다.)이다. **유틸리티 클래스를 클래스라고 부르기 어려운 이유는 인스턴스를 생성하지 않기 때문*이다. 유틸리티 클래스는 정적 메서드 처럼 단순히 나쁜 요소가 아닌 나쁜 요소들을 모아놓은 집합체(!!!) 이다. *끔찍한 안티패턴이니 가까이 하지 말아라.

## 3.2.4 싱글톤 패턴 정적 메서드 대신 사용할 수 있는 매우 유명한 개념이다.

class Math {
private static Math INSTANCE = new Math();
private Math() {}
public static Math getInstant() {
return Math.INSTANCE;
}
public int max(int a, int b) {
if (a < b) {
return b;
}
return a;
}
}
//실제사용
Math.getInstance().max(5, 9);

유명한 디자인 패턴이지만 끔찍한 안티패턴이다. 결국 아래의 정적메서드(유틸리티 클래스)와 다를바없다.

class Math {
private Math() {}
public static int max(int a, int b) {
if (a < b) {
return b;
}
return a;
}
}
//실제사용
Math.max(5, 9);

대부분 싱글톤과 유틸리티 클래스 사이의 차이점을 묻는 질문에 ‘싱글톤은 상태를 유지’ 한다고 대답하지만 틀렸다. 즉 **싱글톤의 장점은 getInstance() 와 함꼐 setInstance()를 추가할 수 있다는 점*이다. *싱글톤이 유틸리티 클래스보단 낫지만 여전히 안티패턴이며, 전역변수 그 이상도 이하도 아니다.

### 소프트웨어의 전체 클래스들이 사용해야 하는 기능은 어떻게 구현할 것인가? → 캡슐화를 통해 구현해라!

## 3.2.5 함수형 프로그래밍 함수형 프로그래밍을 하면 코드가 훨씬 짧지만 객체의 표현력이 더 뛰어나고 강력하다. 이상적인 OOP 언어에는 클래스와 함께 함수가 포함되어야 한다. 작은 프로시저로 동작하는 java의 메서드가 아니라 하나의 출구(exit point)만 포함하는 순수한 FP 패러다임에 기반하는 진정한 함수를 포함해야 한다. (어쨌든 FP보단 OOP가 더 낫다는 소리)

## 3.2.6 조합 가능한 데코레이터 그저 다른 객체를 감싸는 객체이다. (데코레이터 패턴과 동일)

names = new Sorted(
new Unique(
new Capitalized(
new Replaced(
new FileNames(
new Directory(
"/var/users/*.xml"
)
),
"([^.]+)\.xml",
"$1"
)
)
)
);

매우 깔끔하면서도 객체지향적이고, 순수하게 선언형이다. 어떻게 만들었는지를 전혀 설명하지 않고도 이 객체가 무엇인지를 설명했다. 단지 선언형인데 말이다. 각각의 클래스는 데코레이터이다. 객체들의 전체적인 행동은 내부에 캡슐화하고있는 객체들에 의해 유도된다. 각 데코레이터는 내부에 캡슐화하고 있는 객체에 별도의 행동을 추가한다. (.. 예제에선 if 명령어마저 객체로 만들어서 처리하는 예시를 보여준다)

## 정리 정적 메서드는 조합이 불가능하다. 작은 객체들을 조합하여 더 큰 객체를 만들 수 없다. 이것이 OOP에서 정적 메서드를 사용해선 안되는 이유다. 절대 static 키워드를 사용하지 말아라.## 느낀점 primitive 타입보다는 class로 객체를 생성하여 하나의 객체로 만들어 내부에 로직을 넣는다. 그리고 각 객체들은 서로간의 대화(메세지를 보냄)를 통해 비즈니스 로직을 구성해 나간다. 이번 챕터는 다른 관점에서 느낄 수 있어서 좋았다. ‘정적 메서드를 왜 쓰지말라는걸까?’ 라고 생각했는데 사실상 유틸성 정적메서드를 말한 것이었다는 점. 레거시에서 어쩔수 없이 사용하고 있다는 변명을 하는것이 부끄러워졌다ㅋㅋㅋㅋ 이번에 아예 새로 갈아엎는 프로젝트에서는 최대한 정적 유틸 메서드를 지양하리라.. 그리고 팀 내에 헬퍼 메서드를 만드는 것을 99% 권장하는 팀원분이 계신데 한번 이 책을 추천드리고 싶다는 생각이 들었다. 또 그분은 어떤 관점으로 생각하실지 궁금하기때문이다. 또한 데코레이터 패턴.. 사실 난 별로 좋아하지 않았다. 새로 객체를 만든다는게 거부감이 있으며(최대한 자원을 아껴야 한다는 강박관념인듯) 하나의 객체안에 하나를 넣고 하나를 넣고 하며 중첩으로 만든다는것을 좋아하지 않았기 때문이었지만 이번 챕터를 통해 다르게 생각하게 되었다.


# 3.5 절대 getter와 setter를 사용하지 마세요. 모든 클래스는 불변이어야 한다. 하지만 get과 set을 쓰는 순간 가변이 되어버린다.

## 3.5.1 객체 대 자료구조

### 예시 ### 자료구조(C)

struct Cash {
int dollars;
}

객체(C++)

#include
class Cash {
public:
Cash(int v): dollars(v) {};
std::string print() const;
private:
int dollars;
};

위 자료구조와 객체를 사용하는 방식에서 차이점이 보일것이다.

//자료구조(c)
print("Cash value is %d", cash.dollars);
//자료구조(c++)
print("Cash value is %d", cash.print());

- 자료구조

  • 멤버인 dollars에 직접 접근 후 값을 정수로 취급한다.
  • struct와는 아무런 의사소통도 없이 직접적으로 멤버에 접근하며, 단순한 데이터 가방이 된다.
  • 투명하다. (Glass Box)
  • 수동적이다. (죽어있다.)
  • 클래스
  • 멤버에게 접근하는 것을 허용하지 않는다.
  • 멤버를 노출하지 않는다. (캡슐화)
  • print()가 어떤 방식으로 동작하는지도 알수 없고 어떤 멤버가 이 작업에 개입하는지 알 수 없다.
  • 불투명하다. (Black box)
  • 능동적이다. (살아있다.) 모든 프로그래밍 스타일의 핵심 목표는 가시성의 범위를 축소해서 사물을 단순화 시키는 것이다. 특정 시점에 이해해야 하는 범위가 작을수록 유지보수성이 향상되고 이해와 수정이 쉬워진다. OOP는 코드가 데이터를 지배하지 않는다. 필요한 시점에 객체가 자신의 코드를 실행시킨다. 객체가 일급 시민이고 생성자를 통한 객체 초기화가 곧 소프트웨어이며, 소프트웨어는 생성자를 통해 구성된다.

객체지향을 위해선

  • 데이터를 객체안에 감추고 외부로 노출시키면 안된다.

3.5.2 좋은 의도, 나쁜 결과

getter와 setter는 캡슐화 원칙을 위반하기 위해 설계되었다. 자바에선 프로퍼티를 public으로 바꾸면 기본 규칙을 위반하게 되기 때문에 프로퍼티를 private로 만들고 getter와 setter를 추가시켰다. 하지만 이 getter와 setter를 사용하면 OOP의 캡슐화 원칙을 손쉽게 위반할 수 있다. 결국 행동이 아닌 데이터를 표현할 뿐인 메서드이기 때문이다.

3.5.3 접두사에 관한 모든 것

getter/setter 안티패턴에서 유해한 부분은 두 접두사인 ‘get’과 ‘set’이다. dollar() 라는 메서드를 통해 value 를 반환하는 것은 괜찮지만 getDollar() 라는 메서드 이름을 짓는건 적절하지 않다. 두 메서드의 뉘앙스는 다르다.

  • getDollars() → 데이터 중에 dollars를 찾은 후 반환하세요. (데이터의 저장소로 취급, 데이터 노출)
  • dollars() → 얼마나 많은 달러가 필요한가요? (객체 존중, 데이터 미노출) 절대로 getter와 setter방식으로 메서드 이름을 짓지 말아라. —

    느낀점

    동감하는 얘기뿐. 대신 자료구조와 클래스에 대한 차이점을 확실히 보여주며 해당 부분이 getter와 setter를 피해야 한다는 주장에 뒷받침이 되는 부분이 너무 좋았다. 쓰지 말란건 알고있었지만 이러한 정확한 근거 (자료구조로의 회귀) 가 좋은 예시였다고 생각한다. 또한 setter는 무조건적으로 지양했지만 getter는 사실.. 많이 쓰고있었다. 반성…


책에 대한 느낌

솔직히 처음에 어투가 너무 강하고 명령조라 왜 말투가 이럴까.. 생각했다. 하지말라고 하면 더 하고싶은법.. 너무 강하게 말하니 반발심이 들었다. 그런데 읽다보니 이 어투로 인해서 머리속에 더 각인되는 느낌이 들었다. 기분은 나쁘지만 효과는 좋은듯 하다.. 한번쯤 읽어보기 좋은책이다!




© 2019. by mintheon

Powered by mintheon