ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 엘레강트 오브젝트 - 접미사 er 사용을 자제하자
    책책책 책을 읽읍시다/프로그래밍 2023. 7. 30. 22:57

    저자 : Yegor Bugayenko

    엮자 : 조영호

     

    들어가며


     저자는 순수 OOP를 지향한다. 이러한 방식은 무조건 옳다라고 말하는 점에서 명쾌하기도 하고 너무 극단적이라 난해하기도 했던게 흥미로웠다. 이보다도 엮자 서문이 더 흥미로웠다. 보통은 책 내용에 동조하며 좋은 면을 위주로 소개하는데 반해 이 책을 읽을 때 주의할 점 먼저 설명한다. 엮자인 조영호 님의 저서인 '객체지향의 사실과 오해', '오브젝트'을 읽으며 객체지향에 대한 생각이 일관되게 잘 정리된 흔들리지 않을 느낌을 받았는데, 저자도 생각이 다르지만 결은 같은 사람 같았다. 좌파 정치인이 우파 정치인의 책을 출간한 느낌이랄까?

     소개된 내용 중 실무에서 했던 내 과오와 연결되는 것이 많아서 반성문 쓴다라는 느낌으로 정리해보았다.

     

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


     종종 클래스를 객체의 템플릿으로 설명하지만, 이 설명은 완전히 잘못됐다. 이런 류의 설명은 단순히 필요한 시점에 어딘가에서 복사되는 수동적이고 멍청한 코드 덩어리로 클래스를 격하시킨다. 비록 클래스가 기술적인 관점에서는 템플릿과 동일하더라도 이런 식으로 생각해서는 안된다. 클래스는 객체의 팩토리이다. 클래스는 객체의 능동적인 관리자(active manager)로 생각해야 한다. 클래스는 객체를 꺼내거나 반환할 수 있는 위치이기 때문에, 클래스를 저장소(storage unit) 또는 웨어하우스라고 불러야 타당하다.

     객체를 살아있는 생명체로 생각한다면 클래스는 객체의 어머니라고 할 수 있다. 이 은유가 가장 정확한 표현이다.

     클래스에 대해 가장 먼저 할 일은 이름 짓기이다. 기본적으로 두 가지 방법이 있는데 하나는 올바른 방법이고 하나는 잘못된 방법이다. 잘못된 방법은 클래스의 객체들이 무엇을 하고 있는(doing)지를 살펴본 후 기능(functionality)에 기반해서 이름을 짓는 방법이다. 아래는 이 방식으로 이름 지어진 클래스이다.

    public class CashFormatter {
        private int dollars;
    
        public CashFormatter(int dollars) {
            this.dollars = dollars;
        }
        
        public String format() {
            return String.format("$ %d", this.dollars);
        }
    }

     CashFormatter 클래스의 객체는 dollar에 저장된 금액을 문자로 포맷팅하는 일을 수행한다. 따라서 이 객체의 이름을 포매터(formatter)로 짓기로 한 결정은 적절해 보인다.

     이런 명명 원칙은 아주 잘못된 방식이지만 매우 인기가 많다. 그러나 이 방식을 따르지 않기를 권한다. 클래스의 이름은 객체가 노출하고 있는 기능에 기반해서는 안된다. 클래스의 이름은 무엇을 하는지(what he does)가 아니라 무엇인지(what he is)에 기반해야 한다. CashFormatter라는 이름은 Cash, USDCash, CashInUSD와 같은 이름으로 바꿔야 하고 메서드 format()은 usd()로 수정해야 한다.

    public class Cash {
        private int dollars;
    
        public Cash(int dollars) {
            this.dollars = dollars;
        }
        
        public String usd() {
            return String.format("$ %d", this.dollars);
        }
    }

     다시 말해서, 객체는 그의 역량(capability)으로 특정지어야 한다. 내가 어떤 사람인지는 키, 몸무게, 피부색과 같은 속성(attribute)가 아니라, 내가 할 수 있는 일(what I can do)로 설명해야 한다.

     여기에 숨어있는 악마는 바로 접미사 '-er'이다.

     '-er'로 끝나는 이름을 가진 수많은 클래스들이 존재한다. Manager, Controller, Helper, Handler, Writer, Reader, Converter, Validator, Router, Dispatcher, Observer, Listener, Sorter, Encoder, Decoder가 여기에 속한다. 모두 잘못 지어진 이름이다. 상반된 방식으로 지어진 이름으로는 Target, EncodedText, DecodedData, Content, SortedLines, ValidPage, Source가 있다.

     물론 이 규칙에도 예외는 있다. 영어 명사에는 어떤 활동을 수행하는 대상을 가리키기 위해 접미사 '-er'이 붙지만 오랜 시간이 흐르면서 의미가 정착된 경우도 있다. 대표적인 예가 바로 computer와 user이다. 우리는 더 이상 user가 말 그대로 어떤 물건을 '사용(use)'하는 사람을 지칭한다고 생각하지 않는. 소프트웨어 시스템과 상호 작용하는 사람이라는 의미로 사용한다. computer는 '계산(compute)'하는 무엇이 아니라 전자 기기인 컴퓨터 자체를 가리킨다. 하지만 이런 예외는 그렇게 많지 않다.

     

     객체는 객체의 외부 세계와 내부 세계를 이어주는 연결장치(connector)가 아니다. 객체는 내부에 캡슐화된 데이터를 다루기 위해 요청할 수 있는 절차의 집합이 아니다. 대신 객체는 캡슐화된 데이터의 대표자(representative)이다. 연결장치는 존중받지 못한다. 정보를 수정하거나 스스로 어떤 일을 수행할 만큼 충분히 강력하지도 똑똑하지도 못하기 때문에 단순히 정보를 전달하기만 한다. 반면에 대표자는 스스로 결정을 내리고 행동할 수 있는 자립적인 엔티티이다. 

     

     클래스의 이름이 '-er'로 끝난다면, 이 클래스의 인스턴스는 실제로는 객체가 아니라 어떤 데이터를 다루는 절차들의 집합일 뿐이다. 이것은 과거에 C, COBOL, Basic 등의 언어를 사용하다 전향한 많은 객체지향 개발자들로부터 물려받은 절차적인 사고 방식이다. Java와 Ruby를 사용하고는 있지만, 여전히 데이터와 절차를 이요해서 생각하고 있는 것이다.

     그렇다면 올바른 클래스 이름은 어떻게 지어야 할까?

     클래스의 객체들이 무엇을 캡슐화할 것인지를 관찰하고 이 요소들에 붙일 적합한 이름을 찾아야 한다. 임의의 숫자 리스트가 존재할 때, 이 리스트의 요소 중에서 소수를 찾는 알고리즘을 만든다고 가정해 보자. 오직 소수만으로 구성된 리스트를 얻는 것이 목적이라면, 클래스의 이름을 Primer, PrimeFinder, PrimeChooser, PrimeHelper 등으로 지으면 안된다. 대신 PrimeNumber라고 지어야 한다. 아래는 Ruby로 구현한 예이다.

    class PrimeNumbers
        def initialize(origin)
            @origin = origin
        end
        def each
            @origin
                .select { |i| prime ? i }
                .each { |i| yield i }
        end
        def prime?(x)
            # ...
        end
    end

     클래스 PrimeNumbers는 숫자들의 리스트처럼 행동하지만 오직 소수만 반환한다. 아래는 C언어를 이용해서 유사한 기능을 순수하게 절차지향적인 스타일로 설계한 예이다.

    void find_prime_numbers(int* origin, int* primes, int size) {
        for (int i = 0; i< size; ++i) {
            primes[i] = (int) is_prime(origin[i]);
        }
    }

     find_prime_numbers는 두 개의 정수 배열을 인자로 취하는 프로시저이다. 이 프로시저는 첫 번째 배열인 origin에서 소수를 찾은 후 두 번째 배열인 primes에 발견된 소수의 위치를 표시한다. 이 과정에는 어떤 객체도 관여하고 있지 않는다. 이것은 순수하게 절차적인 방식이며 올바르지 못한 접근방법이다.

     이 프로시저는 원래의 숫자 목록(origin)과 소수 목록(primes)이라는 두 개의 데이터 조각을 이어주는 연결장치이다. 객체는 다르다. 객체는 연결장치가 아니다. 객체는 여러 객체들이 모여 구성된 연합의 대표자이다. 앞의 예제에서는 숫자 컬렉션처럼 행동하는 PrimeNumbers 객체를 생성했지만, 외부에서는 오직 컬렉션 안에 포함된 소수만 볼 수 있다.

     우리들의 객체가 find_prime_numbers 프로시저와 비슷하다면 문제이다. 기술 관점에서는 유사해 보인다고 하더라도 객체는 프로시저의 집합처럼 행동해서는 안된다. PrimeNumbers 클래스가 숫자 리스트를 캡슐화하고 있는 동안에는 외부에서 직접 객체 내부에 포함된 숫자 리스트를 처리하거나 조회하도록 허용해서는 안된다. 대신 PrimeNumbers는 "지금은 내가 바로 그 목록이야!"라고 이야기해야 한다. 외부에서 목록을 이용해서 어떤 일을 해야 한다면 객체에게 그 일을 하도록 요청하고, 수신한 요청을 처리하기 위해 객체 스스로 무엇을 할지 결정해야 한다. 객체가 요청에 응할 마음이 있다면, 캡슐화하고 있는 리스트로부터 원하는 데이터를 가져온다. 하지만 객체가 원하지 않는다면, 어떤 일이 일어날 지는 객체에게 달려있다.

     PrimeNumbers는 숫자의 리스트이다(is a). 리스트를 처리하기 위한 메서드의 집합이 아니라 리스트 그 자체이다!

     

     새로운 클래스에 이름을 붙일 때는 무엇을 하는지(what he does)가 아니라 무엇인지(what he is)를 생각해야 한다. 그는 리스트이고, 인덱스 번호를 통해 특정 위치의 요소를 선택할 수 있다. 그는 SQL 레코드이고, 임의의 셀에 저장된 정수값을 조회할 수 있다. 그는 픽셀이고, 스스로 색을 바꿀 수 있다. 그는 하나의 파일이고, 디스크에서 내용을 읽어올 수 있다. 그는 인코딩 알고리즘이고, 인코드 작업을 수행할 수 있다. 그는 HTML 문서이고, 브라우저에서 HTML을 렌더링할 수 있다.

     내가 무엇을 하는 지와 내가 누구인지는 다르다.

     

    회고


     이번 챕터를 보면서 그간 했었던 악행?들이 주마등처럼 스쳐 지나갔다. 주로 특정 출력용 객체를 만들때 '-Maker'란 접미어를 붙여 도메인 클래스를 만들곤 했다. 예를들면 앱에 표시할 SampleComponent라는 앱 컴포넌트가 있다고 하자. 로그인한 사용자와 비로그인 사용자별로 SampleComponent 출력 내용이 달라야 한다. 

    SampleComponent 구조 - before

     이 사례가 바로 생각난 이유는 클래스를 만들때도 메서드명을 붙일때도 찝찝했기 때문이다. 그 찝찝함이 아직도 기억에서 지워지지 않았는데 마침 엘레강트 오브젝트에서 이런 안티패턴을 짚어줬다. Maker란 이름이 프로그래밍 생태계에서 생소하기도 하고, 클래스명이 무언가를 만든다는 Maker인데 메서드명도 무언갈 만들어야하는 이름으로 지어야한다! ViewMaker.make()라고 지을뻔 했으나 make가 중복이다. 안될건 없지만 뭔가 불편하여 create라는 명목상 중복만 피할뿐 의미상의 중복은 피하지 못했다. 클래스명과 메서드명이 의미상으로 중복되었다는 건 어딘가 추상화가 덜 되었다는 신호이다. 메서드명은 이미 충분히 무얼 하는지 잘 나타내는 것 같다. 그렇다 클래스명이 문제이다. 클래스명을 지을때 무엇인지(what he is)가 아니라 무엇을 하는지(what he does) 관점에서 접근해서 이런 부끄러운 코드가 나왔다. 그럼 이 클래스는 무엇이어야 하는가? 컴포넌트 뷰 모델 그 자체여야 한다. 그럼 그 모델이 ComponentView를 만들고 수정하고 지우고 보여주고 다 할 수 있다. 즉, SampleComponentViewMaker라는 클래스는 SampleComponentView로 명명되어야 한다. 그러면 메서드명도 create로 간단하게 뺄 수 있다.

    SampleComponent 구조 - after

     클래스명/메서드명도 줄어들어 읽는 부담이 덜해졌다. 무엇보다 클래스/메서드가 무엇인지 무엇을 하는지가 명확히 구분되어 코드를 이해하기 더 좋아진 것 같다.

     앞으로는 클래스명을 지을 때 다른건 몰라도 '-er' 접미어는 자제하는게 좋을 것 같다. Helper나 Handler같은 역할 자체가 일종의 컨벤션이 된건 그렇다 쳐도, Maker, Creator, Calculator와 같은 행위 관점에서의 클래스명은 지양하자.

     

     

     

    댓글

Designed by Tistory.