티스토리 뷰

책 표지만 보면 그렇게 재밌어 보이진 않는다

 

오늘부터 우테코 안드로이드 코치 제이슨이 열어주신 <엘레강트 오브젝트> 북 스터디가 시작됐다.

스터디 이름은 제이슨의 가르침을 받아 무럭무럭 자라나자는 뜻을 담아 '제이슨의 방울토마토들 🍅'이라고 지어졌다.

오늘 방울토마토 크루들과 함께 논의하고 학습했던 내용을 정리하려 한다.

 

사실 나는 이 스터디에 들어가기에 앞서 고민을 좀 했다.

'프론트엔드 관련해서 공부할 것도 이미 차고 넘치는데, 객체지향을 공부하는 게 사치가 아닐까' 하는 생각 때문이었다.

 

그럼에도 스터디를 시작한 이유는 다음과 같다.

1. 객체 지향을 공부하는 일은 단순히 방법론을 익히는 게 아니라, 좋은 구조를 보는 시각을 기르는 것이라 생각했다.

2. 객체지향에 더 능숙한 안드로이드 크루들에게 혼나가면서 빠르게 배우고 싶었다.

3. 재밌어 보였다.

 

스터디 첫 날 소감을 한 줄 요약하면, 기대했던 것만큼 재밌었고 객체와 한결 가까워진 것 같은 즐거운 착각이 들었다!


들어가며

성공적인 절차지향 개발자에서 성공적인 객체지향
개발자로 탈바꿈하기 위한 첫 걸음은 뇌의 일부를
잘라내는 것이다 🧠

- 데이비드 웨스트 - 

 

 

책의 첫 장부터 임팩트 강한 문구가 보였다.

이 문구를 넣은 것에서 유추할 수 있듯, 저자는 급진적 객체주의파이다.

전해 듣기로는 절차지향을 못 견뎌해 for 문, if 문까지 객체로 만들어 사용할 정도라고 한다.

 

저자는 객체를 사람에 비유하며 자율적인 독립체로서 마땅히 존중받아야 할 권리가 있다고까지 이야기한다.

그래서 책의 목차도 인간의 라이프 사이클에 빗대어 birth, education, employee, retirement로 지어졌다.

 

오늘은 그 중 1장에 해당하는 birth의 내용을 정리하려 한다.

(명시적으로 나와있지 않지만 내용을 보니, 챕터의 이름이 birth인 이유는 '객체의 생성'에 관한 내용을 다루기 때문인 것 같다.)

 

중요한 내용은 책의 목차에 포함되지 않아도 별도 항목으로 추가했다.

또한 책 내용엔 포함되지 않지만, 스터디 도중 논의가 이뤄진 내용이나 스스로 정리한 내용은 Q&A 항목으로 추가했다.

 

목차는 다음과 같다.

1. 유지보수성을 위해 객체지향을!
2. class는 객체의 팩토리이다.
3. -er로 끝나는 이름을 사용하지 마세요.
4. 생성자 하나를 주 생성자로 만드세요.
5. 생성자에 코드를 넣지 마세요.

1. 유지보수성을 위해 객체지향을!

'객체지향 프로그래밍을 왜 해야 할까?'에서부터 내용이 시작된다.

 

절차지향적으로 작성된 코드도 기술적으로 아무런 문제도 없고, 심지어 성능 부분에서는 절차지향이 나을 때가 많다.

그럼에도 객체지향을 선택하는 이유가 무엇일까?

바로 유지보수성 때문이다.

 

먼 과거에는 유지보수성보다 성능이 중요했다.

왜냐하면 컴퓨터가 프로그래머보다 비쌌기 때문이다.

그래서 인간이 CPU에 가까운 저수준 언어를 꾸역꾸역 사용해야 했다.

하지만 컴퓨터의 성능이 발전하면서 프로그래머가 더 비싸지고, 소프트웨어가 다양한 요구사항에 유연하게 대응해야 할 필요성이 커졌다.

그래서 이제는 자연어에 가까운, 고수준 언어를 사용하게 된 것이다.

 

이런 맥락에서 코드를 짜는 방식도 인간에게 읽히기 쉬운 형태가 유리하게 되었다.

절차지향적으로 쓰인 코드는 개발자가 이해하는 데 시간이 오래 걸린다.

코드로 인해 어떤 일이 벌어지는지 알아내려면 모든 라인을 읽어 내려가야 하기 때문이다.

이 말은 프로젝트에 들어가는 비용이 높아진다는 뜻이기도 하다.

개발자들이 유지보수하기 좋은 형태로 만들어 개발 비용을 낮추기 위해 객체지향을 택하는 것이다.

 

(저자는 물론 절차지향도 아무런 문제가 없다고 한다. 마치 어셈블리어로 코드를 짜도 아무런 문제가 없는 것처럼 말이다.)

 

2. class는 객체의 팩토리이다.

클래스는 객체를 인스턴스화(instantiate)하기 위해 존재하는 것이다.

팩토리 패턴에서 이야기하는 팩토리처럼 객체를 만들고, 추적하고, 적절한 시점에 파괴하는 역할을 하는 것이다.

다만, 이런 기능이 클래스의 코드가 아니라 런타임 엔진에 의해 구현된다는 차이만 존재할 뿐이다.

이처럼 클래스는 하나의 팩토리, 내지는 객체를 보관하는 웨어하우스라는 관점에서 바라봐야 한다.

그리고 이 관점에서 보면, 어떤 객체도 인스턴스화하지 않는 유틸리티 클래스는 명예로운 객체 시민으로 존중받을 수 없다는 것을 알 수 있다.

 

3. -er로 끝나는 이름을 사용하지 마세요.

class의 이름으로 흔히 Validator, Encoder, Decoder 와 같은 이름을 짓곤 한다.

저자는 여기서 쓰이는 'er'을 악마의 접미사라고 칭한다.

'-er'을 이름에 포함하는 클래스들은 다른 객체의 외부 세계와 내부 세계를 이어주는 연결장치에 불과하기 때문이다.

그런 연결장치는 객체로서 존중받을 수 없다.

 

그래서 Validator, Encoder, Decoder와 같은 이름을 ValidPage, EncodedData, DecodedData 와 같은 식으로 표현하길 권장한다.

물론 단순히 이름만 바꾸라는 뜻은 아닐 것이다.

각 객체들은 자신이 캡슐화하고 있는 데이터를 대변해야 한다.

그리고 그 데이터를 기반으로 직접 결정을 내리고 행동할 수 있어야 한다.

단순히 특정 정보를 처리하기 위한 절차들의 집합이 되어서는 안 된다.

그러한 형태는 객체지향 문법으로 절차지향적인 코드를 구현하는 것에 불과하기 때문이다.

 

[Q&A]

* 유효성 검증만을 담당하는 ValidPage와 같은 객체를 만들면, 어차피 데이터를 객체에 넣었다가 다시 getter를 통해 검증된 데이터를 꺼내와야 할 게 뻔한데요? 그냥 validator를 쓰는 게 더 간단하지 않나요?

이 과정만 놓고 본다면 더 번거로워 보일 수 있다.

유효성 검증을 위해 별도의 객체를 만드는 게 당장은 더 복잡하게 만들 수 있는 것도 사실이다.

하지만 여기서 이야기하는 요지는, 다른 객체의 책임을 일부 대리 수행하는 객체는 객체다운 객체가 아니라는 것이다.

객체를 객체답게 사용하려면, 절차지향적인 코드를 지양하려면, 이름에 '-er'을 붙인 보조적인 역할의 클래스를 두어서는 안 된다는 것이다.

 

* 객체다운 객체란, 자립적으로 행동하는 객체란 무슨 객체일까요? 그렇게 설계하면 유지보수에 어떻게 도움이 된다는 것일까요?

객체다운 객체란, 단순히 명령문(절차)들을 모아놓은 집합체가 아닌 객체이다.

캡슐화하는 데이터를 대변하는 존재로서의 객체이다.

내부의 세부사항을 일일이 드러내지 않고 메시지를 전달받아 행동을 수행하는 객체이다.

 

이러한 객체들 간 협력 구조로 프로그램을 설계하면, 단순한 명령문의 나열이 일정한 의미 단위로 묶인다.

일련의 긴 절차에 의해 수행되었던 기능이 객체간의 상호작용으로 추상화된다.

그렇게 되면 모든 것을 하나하나 들여다 볼 필요없이 추상화된 형태를 보고 코드의 동작을 빠르게 파악할 수 있다.

그리고 각 객체가 독립적이라면 side effect에 대한 걱정을 덜어낸 채 수정을 해나갈 수 있다.

 

4. 생성자 하나를 주 생성자로 만드세요.

주 생성자는 인스턴스의 프로퍼티를 초기화하는데 기본적으로 사용되는 생성자이다.

그리고 부 생성자는 인자를 다양한 형태로 받기 위해 추가적으로 선언되는 보조적 생성자이다.

주 생성자에 초기화 로직을 두고, 부 생성자에서 주 생성자를 호출하는 방식으로 사용해야 한다.

 

코드로 설명하면 다음과 같다.

// java 

// good code
class Cash {
    private int dollars;
    
    Cash(float dlr) {
    	this((int) dlr);
    }
    
    Cash(String dlr) {
    	this(Cash.parse(dlr));
    }
    
    Cash(int dlr) {
    	this.dollars = dlr;
    }
}

// bad code
class Cash {
    private int dollars;
    
    Cash(float dlr) {
    	this.dollars = (int) dlr;
    }
    
    Cash(String dlr) {
    	this.dollars = Cash.parse(dlr);
    }
    
    Cash(int dlr) {
    	this.dollars = dlr;
    }
}

 

내가 생각했을 때 저자의 요지는 두개였다.

 

1. 새로운 메서드를 추가하기보다는 오버로딩(부 생성자)을 적극 활용해라.

많은 수의 메서드는 응집도를 떨어뜨린다.

메서드의 수가 많아질수록 클래스의 초점이 흐려지고 단일 책임 원칙에서 멀어지게 된다.

그렇기에 메서드를 늘리기보다는 오버로딩을 활용하는 것이 현명하다.

오버로딩을 활용하면, 다양한 인자를 수용함으로써 유연하게 동작하면서도 응집도 높은 클래스를 만들 수 있다.

 

2. 주 생성자를 재사용하여 반복되는 코드를 줄여라.

모든 생성자에서 공통적으로 이뤄지는 할당 작업을 주 생성자에만 두고, 부 생성자에서 주 생성자를 호출한다면 반복되는 코드를 줄일 수 있다.

 

그런데 여기서 한가지! 프론트엔드에서 사용하는 JS는 오버로딩을 지원하지 않는다.

저자는 JS와 같이 오버로딩이 불가능한 언어에서 오버로딩을 구현하는 트릭을 소개했다.

그건 바로 Map 자료형과 if문을 활용하는 방식이다.

 

(책에서는 php를 사용하지만 js로 바꾸어 표현해봤다. 단, php에서는 생성자 내에서 생성자를 재귀호출하는 것이 가능하지만, js에서는 불가능해 부 생성자에서 주 생성자를 호출해야 한다는 원칙은 지키지 못했다.)

// javascript
class Cash {
  #dollars;

  constructor(argument) {
    if (Number.isInteger(argument)) {
      this.#dollars = argument;
    } else if (argument instanceof Map && argument.has("float")) {
      this.#dollars = parseInt(argument.get("float"), 10);
    } else if (argument instanceof Map && argument.has("string")) {
      this.#dollars = parseInt(argument.get("string"), 10);
    } else {
      throw new Error("잘못된 형식의 현금입니다.");
    }
  }
}5

new Cash(20);
new Cash(new Map([['float', 20.1]]));
new Cash(new Map([['string', "20.1"]]));

 

5. 생성자에 코드를 넣지 마세요.

생성자 내부를 가능한 가볍게 유지해야 한다.

생성자 내부에서 로직(ex. 파싱)을 넣어서는 안 되며 인자를 건드려서도 안 된다.

대신 필요하다면 다른 타입의 객체로 감싸서 캡슐화하는 것만 허용된다.

 

그 이유는 다음과 같다.

 

1. 이해하고 재사용하기 쉬워진다.(투명해진다)

생성자에서 별도의 동작 없이 객체 생성을 위한 초기화만 이뤄진다는 확신이 있다면 이 객체를 이해하고 재사용하는 일이 쉬워진다.

만약 생성자에서 다양한 작업들이 수행된다면, 이 코드를 보는 다른 개발자가 쉽게 예측할 수 있을까?

생성자는 인스턴스의 프로퍼티를 초기화하는 곳이지 동작을 수행하는 곳이 아니다.

생성자에 담겨 있는 복잡한 로직들은 코드를 이해하기 어렵게 만든다.

 

2. Lazy한 방식으로 행동한다.

생성자에 담긴 로직은 객체 생성 시 무조건 실행된다.

그런데 그 로직들이 과연 생성 즉시 필요했을까를 생각해보면 아닌 경우가 있을 수 있다.

반면, 객체가 Lazy하게 행동한다면 로직들이 실행되는 시점을 사용자가 정할 수 있다.

이는 사용자가 최적화를 할 수 있게 선택지를 열어주는 것이다.

더불어 객체의 빠른 생성을 보장해준다.

 

이렇게만 이야기하면 이해가 잘 되지 않는다.

코드로 살펴보자.

// javascript

// bad code
class Cash {
  #dollars;
  
  constructor(dlr) {
    this.#dollars = parseInt(dlr, 10); // 객체 생성 즉시 파싱 로직이 수행된다.
  }
}

 

위와 같은 코드를 아래와 같이 바꾸는 것이다.

// javascript

// good code
class Cash {
  #dollars;
  
  constructor(dlr) {
	this.#dollars = new StringAsInteger(dlr); // 객체 생성 시점에는 아무 로직도 실행하지 않는다.
  }
}

class StringAsInteger extends Number {
  #source;
  
  constructor(src) {
    this.source = src;
  }
  
  intValue() {
    return parseInt(this.source);
  }
}

 

이렇게 하면 Cash 객체를 생성하는 동시에 파싱을 꼭 수행할 필요가 없게 된다.

// Lazy한 동작으로 성능 상 이득을 보게 되는 예시
const five = new StringAsInteger("5");

if (어떤 문제가 발생한다) {
  throw new Error("어떤 오류가 발생했다.")
}

five.intValue();

 

(대신 이러한 구조에서는 메서드를 호출할 때마다 파싱이 이루어지게 되는데, 이는 캐싱으로 극복할 수 있다.)

 

[Q&A]

생성자에서 로직을 다 없애라구요? 그럼 생성자 내에서 유효성 검증도 하지 말아야 하는 건가요?

그렇지 않다. 유효성 검증은 수행해야 한다.

유효성 검증은 클래스의 사전 조건을 충족시키기 위한 것이므로 필수적으로 이루어져야 한다.

여기서 사전 조건이란 메서드가 정상 동작하기 위해 만족해야 하는 조건이다.

사전 조건을 만족하지 않으면 메서드의 정상 동작 자체가 불가능해질 것이므로 객체 생성 시 잘 색출해내야 한다.

예를 들면, 나이(name)와 관련된 동작을 수행하는데 그 값이 null이거나, -1이라면 어떨까?

나이를 다루는 메서드에서 예상치 못한 동작이나 오류가 발생할 것이다.

 

단, 생성자에서 즉시 검증해야 할 항목과 메서드 실행 시 lazy하게 검증해야 할 항목을 잘 분별하는 것이 중요하다.

생성자에서 검증해야 할 항목은 해당 클래스의 전제 조건에 한정된다.

 

스터디 도중, 제이슨이 이와 관련해 좋은 예시를 들어 설명해주셨다.

 

다음 중 URL이 아닌 것은?

1) www.naver.com  2) www.ILoveyouobject.com  3) www.몰라.com  4) hello, world!

 

1)은 URL임이 자명해 보인다. 2), 3)은 도메인 네임이 약간 수상하긴 하지만, 형식을 보니 URL이 맞는 것 같긴 하다.

반면, 4)는 죽었다 깨어나도 URL처럼 보이지 않는다.

그렇다면 URL 객체의 생성자 유효성 검증 시 걸러야 할 은 무엇일까?

4)뿐이다.

 

그렇다면 2)와 3)이 실존하는 URL인지 확인하는 절차는 필요없을까?

물론 필요할 것이다.

하지만 그 검증을 수행하는 것은 URL 객체가 생성되는 시점이 아니라, URL을 사용하는 시점이 될 것이다.

URL 객체로 성립할 수 있는지 검증하는 과정에서는 www나 .com과 같이 URL로서의 형식을 갖추었는지만을 확인해야 한다.

 

URL 객체를 생성하는 당시에는 www.ILoveyouobject.com이 없었지만, 그 객체의 값을 호출하는 시점에는 누군가 해당 도메인을 개설할 수도 있지 않겠는가?

이러한 경우를 생각해봐도 객체 생성 즉시 실존하는 URL인지 검증하는 건 비합리적으로 보인다.

 

요지는 생성자에서 유효성 검증으로 걸러내야 할 것은 해당 객체로서 절대 성립될 수 없는 케이스에 한정되어야 한다는 것이다.

그 외의 경우는 메서드에서 필요할 때 확인하는 것이 좋다.


짧은 소감

저자의 태도가 급진적이어서 오히려 좋았다.

객체찬양론자 같은 저자의 설명 덕분에, 객체지향이란 무엇인지 감을 잡는데 도움이 되었다.

물론 내용이 급진적인 만큼 잘 취사선택하여 받아들이는 것도 중요해 보인다.

 

저자의 설명에 영혼이 담겨 있어서 즐겁게 읽어 나갈 수 있었다.

저자는 객체를 머리로만 이해한 것이 아니라, 몸으로 느끼고 있는 것 같아 보였다.

'어떤 분야의 전문가라고 인정받기 위해선 이런 모습을 보여야 하지 않을까'하는 생각도 들었다.

 

객체지향의 길이 아직 아득하게 느껴지지만, 앞으로도 꾸준히 알아가보고 싶다 🙂