태그 보관물: 빌더

메서드명은 주의 깊게 골라라

참고: 이 글은 Elegant Object 책의 샘플 PDF로 제공되는 내용을 저자의 허락하에 번역한 것입니다.


2.4 메서드명은 주의 깊게 골라라

이미 1.1 절에서 클래스 이름을 지정하는 방법을 살펴봤다. 이제 메서드명을 적절히 지어볼 시간이다. 가장 간단하고도 중요한 규칙은 다음과 같다: 빌더(builder)는 명사이고, 조종자(manipulator)는 동사다. 이것이 무슨 뜻인지 알아보자.

무언가를 만들어 새로운 객체를 반환하는 메서드를 빌더라고 한다. 이 이름은 그냥 내가 만들긴 했지만 나에게는 논리적인 이름으로 보인다. 빌더는 항상 뭔가를 반환한다. 빌더는 절대 void를 반환하지 않으며, 빌더의 이름은 항상 명사다.예를 들면 다음과 같다.

int pow(int base, int power);
float speed();
Employee employee(int id);
String parsedCell(int x, int y);

마지막 메서드인 parsedCell()을 눈여겨보자. 이 메서드는 명사는 아니지만 앞에 형용사가 붙어있다. 이렇게 해도 원칙을 위배하지는 않는다. 단지 이름을 좀 더 자세히 설명할 뿐이다. 이 이름은 여전히 명사지만 이 메서드에 관한 정보를 더 담고 있다. 단순히 셀이 아니라 파싱된 셀인 것이다. 우리는 이 메서드가 어떤 식으로든 내용을 변형한 셀을 반환하리라 예상할 것이다.

객체를 통해 추상화된 현실 세계의 개체에 변형을 가하는 메서드를 조종자라고 한다. 조종자는 항상 void를 반환하고 그것의 이름은 늘 동사다. 예를 들면 다음과 같다.

void save(String content);
void put(String key, Float value);
void remove(Employee emp);
void quicklyPrint(int id);

마지막 메서드인 quicklyPrint()를 눈여겨보자.이 메서드의 이름은 앞에 부사가 붙은 동사다. 여기서 핵심적인 부분은 “print”라는 동사이고, “quickly”는 단순히 이 동사를 설명해줌으로써 이 메서드의 문맥과 목적에 관한 추가 정보를 준다는 것이다.

다시 말하지만, 이러한 빌더와 조종자는 이번 장의 내용에 맞춰 내가 만들어낸 용어다. 빌더와 조종자를 다른 식으로 불러도 되지만 ‘빌더는 무언가를 만들고 조종자는 조작한다’라는 원칙은 온전히 지키려고 노력해야 한다. 그리고 빌더와 조종자 사이에는 아무것도 없다. 무언가를 조작하고 반환하는 메서드뿐 아니라 무언가를 만들고 동시에 조작하기도 하는 메서드가 있어서는 안 된다. 몇 가지 나쁜 예를 들어 보겠다.

// 저장된 전체 바이트 수를 반환
 int save(String content);

// 맵이 변경됐다면 TRUE를 반환
 boolean put(String key, Float value);

// 속도를 저장하고 이전 값을 반환 
float speed(float val);

나중에 3.5절에서 “설정자(setter)”와 “접근자(getter)”에 대해 살펴보겠지만 여기서는 이미 get으로 시작하는 이름이 잘못됐다는 점이 훤히 드러난다. 그 이유는 “get”은 동사지만 접근자 메서드는 기본적으로 뭔가를 반환하도록 만들어진 빌더이기 때문이다. 따라서 이것이 바로 “접근자” 메서드에 대한 나의 첫 주장이다.

이제 이 아이디어에 대해 설명해야 할 것 같다. 이 주장에 찬성하는 몇 가지 논거들이 있다.

2.4.1 빌더는 명사다

우선 메서드에서 뭔가를 반환할 경우 메서드명을 동사로 짓는 것은 잘못됐다고 생각한다. 이러한 이름은 객체 중심의 사고와 충돌한다. 내가 제과점에 들렀다면 “브라우니 하나 만들어 주세요”라거나 “커피 한 잔 끓여주세요”라고 말하지 않는다. 그 대신 “브라우니 하나 주세요”나 “커피 한 잔 주세요.”라고 말할 것이다. 뭔가를 “만들어 주세요”라거나 “끓여주세요”라고 말한다면 말투가 다소 공격적으로 느껴질 것이다. 브라우니가 정확히 어떻게 만들어지거나 커피가 정확히 어떻게 끓여지는지 신경 써서는 안 된다. 그것들을 어떻게 만들지는 제과점에서 신경 쓸 일이다. 나는 객체(브라우니나 커피)를 요구하고 있다. 제과점은 내 요구를 충족시킬 수 있다. 제과점 내부에서 이 같은 일이 정확히 어떻게 일어나는지는 내가 상관할 바가 아니다. 다음은 제과점을 코드로 표현한 예다.

class Bakery {
  Food cookBrownie();
  Drink brewCupOfCoffee(String flavor);
}

위의 두 메서드는 사실 객체의 메서드가 아니다. 이 두 메서드는 절차(procedure)에 해당한다. 제과점에서 명명한 이름을 통해 우리는 제과점을 하나의 자족적이고 자주적인 객체로서 존중해야 하고 제과점이 어떤 역할을 하는지 이해할 수 있다. 이것은 절차적인 접근법이지 객체 지향적인 접근법이 아니다. 다음은 이 두 가지 절차가 C에서는 어떻게 설계될지 보여주는 예다.

Food* cook_brownie() {
  // 브라우니를 만듬
  // 만든 브라우니를 반환
}

Drink* brew_cup_of_coffee(char* flavor) {
  // 커피를 끓임
  // 끓인 커피를 반환
}

제과점이 아무데도 관여하지 않는다. 단순히 C 문법으로 된 두 개의 기계어 명령이 있고 그것들을 호출할 뿐이다. 이를 C 언어에서는 함수라고 하지만 함수는 함수형 프로그래밍과는 거의 무관하기 때문에 실제로는 절차에 해당한다. 우리는 컴퓨터에게 이러한 명령어를 실행해서 그 결과를 반환하도록 요청한다. 이것은 컴퓨터처럼 생각하는 것이지 객체처럼 생각하는 것이 아니다. 우리는 제과점을 신뢰하지 않기 때문에 마시고 싶은 커피를 요청한 다음 결과물이 만들어지도록 맡기는 대신 “커피를 끓여달라고”라고 주문하는 것이다.

너무 철학적인 이야기처럼 들리고 싶진 않지만 이러한 이름 짓기라는 주제는 사실 매우 추상적이고 개념적이다. 적절히 명명된 메서드는 사용자가 객체의 설계 목적과 사명, 존재 목적을 비롯해 객체에게 삶의 의미란 무엇인가를 더 잘 이해하도록 도와준다. 반면 부적절한 메서드명은 객체의 전체적인 인상을 망칠 수도 있고 사용자로 하여금 단순히 객체를 데이터 보관함이나 절차의 모음으로 여기게 만든다. 이것은 OOP 라이브러리, SDK, API 등에서 반복적으로 일어나는 아주 전형적인 실수다. 객체는 계약에 따라 동작하고 싶어하지 단순히 명령을 따르기만을 원하지는 않는다. 이 둘 사이에는 큰 차이점이 있다.

이 같은 이유로 메서드의 이름이 동사라면 그것은 기본적으로 객체가 “어떤 일을 할 것인가”를 알려준다. 그리고 객체에게 무언가를 “만들라”고 요청하는 것은 객체를 활용하는 공손하고 존중하는 방법이 아니다. 단지 만들고자 하는 것을 요청하기만 하고 그것을 어떻게 만들지는 객체가 결정하도록 내버려두자. 다음과 같은 이름은 모두 잘못된 것이다.

InputStream load(URL url);
String read(File file);
int add(int x, int y);

모두 다음과 같은 이름으로 바꿔야 한다.

InputStream stream(URL url);
String content(File file);
int sum(int x, int y);

add(x,y) 대신 sum(x,y)을 사용하도록 제안한다는 점을 눈여겨보자. 사소하고 별로 중요하지 않은 변화로 보일 수도 있지만 실제로 사고하는 데 큰 차이를 빚어낸다. 우리는 객체가 xy에 더하도록 요청하지 않는다. 대신 객체가 두 값의 합을 만들어서 새로운 객체를 반환하도록 요청한다. 그 객체가 실제로 합계를 알아낼까? 알 수 없다. 아마도 그럴 테지만. 내가 유일하게 아는 것은 결과가 xy의 합계처럼 보일 것이라는 점이다. 다시 말하지만, 나는 객체가 무엇을 하라고 말하는 게 아니라 결과가 특정 계약, 즉 정수형 숫자라는 것을 준수해야 함을 요청하는 것이다.

이것이 바로 나의 첫 번째 주장이자 첫 번째 사용 예다. 우리는 객체로부터 뭔가를 받고 있는데, 다시 말해 객체로 하여금 무언가를 만들도록 요구하고 있다. 이제 우리가 객체로 하여금 무언가를 조작하도록 요청할 때 일어나는 두 번째 논쟁거리이자 사용 사례로 넘어가자.

2.4.2 조종자는 동사다

기억하고 있다시피 객체는 현실 세계의 개체를 표현한 것이다. File 클래스의 객체는 디스크상의 파일을 표현하고, Pixel 클래스의 객체는 화면상의 픽셀을 표현하며, Integer 클래스의 인스턴스는 램의 4바이트를 나타낸다(놀랐는가? 이 주제에 대해서는 3.4절에서 좀 더 자세히 살펴보겠다).

현실 세계의 개체를 조작해야 하는 경우, 우리는 객체가 그렇게 하도록 요청한다. 다음 예제를 보자.

class Pixel {
  void paint(Color color);
}
Pixel center = new Pixel(50, 50);
center.paint(new Color("red"));

예제에서는 center 객체가 50×50 좌표에 위치한 픽셀을 칠하도록 요청하고 있다. 이 경우 아무것도 생성되지 않을 것으로 예상한다. 단지 우리는 무언가에 변화를 가하고 싶고 객체는 그것을 표현한다. 이제 이것이 어떻게 절차가 아닌지 의문이 들지도 모른다. 이것은 이름이 동사로 돼 있고, 기본적으로 객체가 우리 대신 무언가를 하도록 지시하고 있다. 그렇다, 좋은 질문이지만 핵심적인 차이점은 반환된 결과다.

paint() 메서드는 결과를 반환하지 않는다. 제과점 은유를 사용하자면 이것은 바텐더에게 음악 볼륨을 높여달라고 부탁하는 것과 비슷하다. 바텐더가 음악을 더 크게 틀 것인가? 아마 그렇게 할 것이다. 어쩌면 그렇지 않을지도 모른다. 우리의 요청은 그냥 무시될 수도 있다. 이 경우 우리에게 무언가가 되돌아오리라 예상하지 않으므로 이것은 공격적이거나 무례한 것이 아니다. 앞서 부탁한 내용을 다른 식으로 표현하면 어떻게 들릴지 상상해 보자. “음악을 키워주시고 볼륨을 키우고 나면 볼륨이 얼마나 되는지 저한테 말해주십시오.” 값을 반환하는 조종자는 정확히 이런 모습이다. 굉장히 무례한 것이다.

따라서 차이점은 바로 반환값에 있다. 오직 빌더만이 값을 반환할 수 있고, 빌더의 이름은 반드시 명사여야 한다. 객체가 우리로 하여금 무언가를 조작하도록 허용할 경우 그 객체의 이름은 동사여야 하고 반환값이 없어야만 한다.

이 같은 주요 원칙을 염두에 둔다면 조금 덜 엄격한 명명 관례는 준수하는 것이 가능하다고 생각한다. 예를 들어, 빌더 패턴을 사용할 경우 메서드명 앞에 with 접두사를 둘 수 있다.

class Book {
  Book withAuthor(String author);
  Book withTitle(String title);
  Book withPage(Page page);
}

예제에서 withTitle이라는 이름은 bookWithTitle을 짧게 줄인 형태다. 모든 메서드에서 이러한 book 접두사를 생략하려면 with 접두사를 쓰기만 하면 된다. 그렇지만 원칙은 여전히 지켜지고 있다. 즉, 이러한 메서드는 빌더이고, 빌더의 이름들은 명사로 분류되고 있는 것이다.