본문 바로가기

강의/TDD, Clean Code with Java 12기

엘레강트 오브젝트

좋은 교육이란

교육 과정에서 그치지 않고

교육을 받는 교육생이 행동하도록 함으로써 실질적인 변화를 만드는 교육. 

개인에서 한발 더 나아가 팀, 회사에 변화를 만드는 교육 - 교육에 대한 포비의 철학

 

나의 영향력 범위 내에서 변화를 만들 수 있는 최소한의 변화를 만든다.

 - 코드 컨벤션, 커밋 로그 컨벤션을 적용

 - 유틸성 클래스에 학습 테스트 적용하고 리팩토링하기

 - else문 쓰지 않기, indent depth를 줄이는 리팩토링 (메서드 추출 리팩토링)

 

자신감과 용기가 쌓이면 더 큰 변화를 만든다.

 - 문자열과 원시값 포장, 일급 콜렉션 적용 리팩토링 (클래스 분리 리팩토링)을 TDDfh wlsgod

 - 정적 분석 도구 (예를 들어 sonarqube)를 적용하고 리팩토링

 - 지속적 통합 도구 (CI)를 도입한다.

 - 코드 리뷰 문화를 만든다.

 

 

엘레강트 오브젝트

클래스 이름 : -er로 끝나는 이름을 사용하지 마세요.

클래스는 객체의 팩토리(factory)이다.

클래스는 객체를 만들고, 추적하고, 적절한 시점에 파괴한다.

즉, 객체의 라이프 사이클을 관리한다.

 

종종 클래스를 개체의 템플릿으로 설명하지만, 이 설명은 완전히 잘못된 설명이다.

이런 식의 설명은 수동적이고, 멍청한 코드 덩어리로 클래스를 격하 시킨다.

 

클래스는 객체의 능동적인 관리자.

즉, 객체를 꺼내거나 반환할 수 있는 역할이기 떄문에 저장소(storage) 또는 웨어하우스 라고 불러야한다.

 

사실 객체를 살아있는 생명체로 생가한다면 클래스는 객체의 어머니라고 할 수 있다.

이 은유가 가장 정확한 표현이다.

 

클래스 이름을 짓는 방식

 - 클래스의 객체들이 무엇을 하는지(what he does)를 살펴본 후 기능에 기반해서 이름을 짓는 방법.

 - 클래스의 객체들이 무엇인지(what he is)에 기반해 이름을 짓는 방법.

 

클래스의 객체들이 무엇인지(what he is)에 기반해 이름을 짓는것이 맞다.

 

나쁜 예 : 클래스의 객체들이 무엇을 하는지(what he does)를 살펴본 후 기능에 기반해서 이름을 짓는 방법.

public class CashFormatter {
    private int dollars;

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

좋은 예 : 클래스의 객체들이 무엇인지(what he is)에 기반해 이름을 짓는 방법.

package bowling.domain;

public class Cash {
    private int dollars;

    public Cash(int dollars) {
        this.dollars = dollars;
    }

    public String usd() {
        return String.format("$ %d", this.dollars);
    }
}

 

객체는 객체의 외부 세계와 내부 세계를 이어주는 연결장치가 아니다.

객체는 내부에 캡슐화된 데이터를 다루기 위해 요청할 수 있는 절차의 집합이 아니다.

 

객체는 캡슐화된 데이터의 대표자(representative)이다.

 

연결장치는 정보를 수정하거나 스스로 어떤 일을 수행할 만큼 충분히 강력하지도 똑똑하지도 못해 단순한 정보를 전달하기만 한다.

반면에 대표자는 스스로 결정을 내리고 행동할 수 있는 자립적인 엔티티이다. 

 

현실 세계의 존재가 소프트웨어의 존재로 들어올 때 수동적인 존재더라도 능동적이게 변함. 이를 두고 우리는 의인화라고 함.

 

소프트웨어의 세계에서는 객체가 더 많은 일들을 스스로 할 수 있어야 함

실세계에서는 존재하지도 않는 추상적인 것들 또한 스스로 결정할 수 있는 존재로 탄생

 

즉, 객체지향은 현실 세계를 모방한 것이 아니라 현실 세계를 참조하여 새로운 세상을 탄생시키는 것이다.

 

마트에서 우유를 결제하는 의인화 예시

나쁜 예

우유의 가격을 가져오고

원익이람 사람의 지갑을 가져와서 가격설정을 다시하고 있다.

package bowling.domain;

public class Main {
    public static void main(String[] args) {
        // 데이터베이스 연결
        DBConnection connection = new DBConnection();
        // 상품 금액 확인
        Milk milk = connection.findItem(" "); 
        int price = milk.getPrice();
        // 결제
        Person wonik = connection.findPerson(" ");
        Wallet wallet = wonik.getWallet();
        wallet.setAmount(wallet.getAmount() - price);
        // 상품 지급
        wonik.setItem(milk);
        // 데이터베이스 저장
        connection.save(wonik);
    }
}

리팩토링 1단계

package bowling.domain;

public class Main {
    public static void main(String[] args) {
        // 데이터 베이스 연결
        DBConnection connection = new DBConnection();
        // 상품 금액 확인
        Milk milk = connection.findItem(" "); 
        int price = milk.getPrice();
        // 결제
        Person wonik = connection.findPerson(" "); 
        wonik.paymentFor(price);
        // 상품 지급
        wonik.setItem(milk);
        // 데이터 베이스 저장
        connection.save(wonik);
    }
}

리팩토링 2단계 - 우유를 의인화

package bowling.domain;

public class Main {
    public static void main(String[] args) {
        // 데이터 베이스 연결
        DBConnection connection = new DBConnection();
        // 상품 및 구매자 조회
        Milk milk = connection.findItem(" "); 
        Person wonik = connection.findPerson(" ");
        // 결제 및 상품 지급
        milk.sellTo(wonik);
        // 데이터 베이스 저장
        connection.save(wonik);
    }
}

객체를 능동적으로 활용을 해라

무엇인지 기준으로 클래스를 지어야 능동적으로 활용할 수 있다.

새로운 클래스의 이름을 붙일 때 무엇을 하는지(what he does)가 아니라

무엇인지(what he is)를 생각해야 한다.

 

메서드 이름을 신중하게 선택하세요

간단한 경험 법칙은

빌더 (builder)의 이름은 명사로,

조정자 (manipulator)의 이름은 동사로 짓는 것이다.

 

빌더 (builder)란?

뭔가를 만들고 새로운 객체를 반환하는 메서드.

빌더는 항상 뭔가를 반환

빌더의 반환 타입은 절대 void가 될 수 없으며, 이름은 항상 명사여야 한다.

 

빌더 (builder) 메서드 예시

int pow(int base, int power);

float speed();

Employee employee(int id);

String parsedCell(int x, int y);

마지막 예인 parsedCell은 형용사를 덧붙여 메서드의 의미를 좀 더 풍부하게 설명하고 있는 명사로 원칙을 위반하지 않는다.

(getEmployee가 아닌 employee로 받는것이 인상깊다.)

 

조정자(manipulator)란?객체로 추상화한 실세계 인티티를 수정하는 메서드반환 타입은 항상 void이고, 이름은 항상 동사여야 한다.

 

조정자(manipulator) 메서드 예시void save(String content);void put(String key, Float value);void remove(Employee emp);void quicklyPrint(int id);

 

마지막 예인 quicklyPrint는 부사를 덧붙여 메서드의 문맥과 목적에 관한 풍부한 정보를 제공하는 동사로 원칙을 위반하지 않는다.

 

리팩토링

앞의 put aptjemsms PutOperation과 같은 클래스를 추가해 save() 메서드와 success() 메서드로 분리

앞의 speed 메서드는 SaveSpeed와 같은 클래스를 추가해 speed를 저장하는 메서드와 이전 값을 반환하는 메서드로 분리

 

빌더와 조정자로 분리 1- 빌더는 명사다.
class Bakery {
    Food cookBrownie(); // 브라우니를 요리해서 반환한다..
    Drink brewCupOfCoffee(String flavor); // 커피를 끓여서 반환한다.
}

위 두 메서드는 실제로는 객체의 메서드가 아니다.

이들은 프로시저(procedure)이다.

 

두 메서드는 Bakery를 자율적인 객체로 존중해서는 안되기 때문에 객체에게 할 일을 일일이 명령해야 한다고 속삭인다.

 

객체는 자신의 의무를 수행하는 방법을 알고 있고 존중 받기를 원하는 살아 있는 유기체이다.

객체는 단순히 지시에 따르는 것이 아니라 계약에 기반해 일하고 싶어한다.

 

나쁜 예

동사의 형태는 void값으로 return을 받아야 하는데 그렇지 못하고 있다.

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);

 

빌더와 조정자로 분리 2 - 조정자는 동사다

예를 들어 반텐더에게 음악을 틀어달라고 요청할 때

  • 방법1 - 음악을 틀어주세요.
  • 방법2 - 음악을 틀고, 현재의 볼륨 상태를 말해주세요.

바텐더 입장에서 각각의 느낌이 어떤가?

의존하고 있는 객체를 자율적인 존재로 존중하는 방법은 어느 방법인가?

 

빌더와 조정자로 분리 3- 빌더와 조정자 혼합하기
class Document {
    int write(InputStream content);
}

새로운 클래스를 만들어서 output으로 나가는 부분을 정리한다.

class Document {
    OutputPipe output();
}
class OutputPipe {
    void write(InputStream content);
    int bytes();
    long time();
}

 

빌더와 조정자로 분리 4 - Boolean 값을 결과로 반환하는 경우

Boolean 값을 반환하는 메서드는 규칙에 있어서 예외적이라 생각한다.

값을 반환하기 때문에 빌더에 속하지만, 가독성 측면에서 이름은 형용사로 지어야 한다.

 

boolean emtpy();
boolean readable();
boolean negative();

 

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

클래스를 잘 설계한다면, 클래스는 많은 수의 생성자와 적은 수의 메서드를 포함할 것이다.

2 ~ 3개의 메서드와 5 ~ 10개의 생성자를 포함하는 것이 적당하다.

 

이런 기준을 두는 핵심은 응집도가 높고, 견고한 클래스에는 적은 수의 메서드와 상대적으로 더 많은 수의 생성자가 존재한다는 점이다.

생성자의 개수가 더 많을 수록 클래스는 더 개선되고, 사용자 입장에서 클래스를 더 편하게 사용할 수 있다.

 

new Cash(30);
new Cash("$29.95");
new Cash(29.95d);
new Cash(29.95f);
new Cash(29.95, "USD");

 

메서드가 많아지면 클래스의 초점이 흐려지고, 단일 책임 원칙(SRP)을 위반하기 떄문이다.

이에 비해 생성자가 많아지면 유연성이 향상된다.

 

생성자 하나를 주 생성자로 만드세요.
  • 주(primary) 생성자는 프로퍼티를 초기화하는 일. 이런 초기화를 주 생성자만 담당하도록 한다.
  • 부(secondary) 생성자는 주 생성자를 호출하도록 한다.

 

저자는 항상 주 생성자를 모든 부 생성자 뒤에 위치시키는데, 주된 이유는 유지보수 하기 편하도록 하기 위함이다.

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

 

하나의 주 생성자와 다수의 부 생성자(one primary, many secondary) 원칙의 핵심은

중복 코드를 방지하고 설계를 더 간결하게 만들어 유지보수성이 향상된다는 것이다.

 

불변 객체로 만드세요.

모든 클래스를 상태 변경이 불가능한 불변 클래스(immutable class)로 구현하면 유지보수성을 크게 향상시킬 수 있다.

불변 객체를 기반으로 사고하면 더 깔끔하고, 더 작고, 더 쉽게 이해할 수 있는 코드를 구현할 수 있다.

 

불변성(immutability)이란?

인스턴스를 생성한 후에 상태를 변경할 수 없는 객체를 불변 객체라 부른다.

 

불변 객체로 구현하면 좋은 점(69 ~ 80페이지)

  • 식별자 가변성 문제가 없다.
  • 실패 원자성이 있다. - 완전하고 견고한 상태의 객체를 가지거나 아니면 실패하거나 둘 중 하나만 가능
  • 시간적 결합을 제거할 수 있다. - 가별 객체들이 많을 경우 연산들의 순서를 일일이 기억해야 한다.
  • 부수 효과를 제거할 수 있다.
  • null 참조를 없앨 수 있다.
  • 스레드 안전한 코드를 구현할 수 있다.
  • 더 작고 더 단순한 객체를 구현할 수 있다.

문서를 작성하는 대신 테스트를 만드세요

더 읽기 쉬운 코드를 만들기 위해서는

코드를 읽게 될 사람이 비즈니스 도메인, 프로그래밍 언어, 디자인 패턴, 알고리즘을 거의 이해하지 못하는 주니어 프로그래머라고 가정해야 한다.

이상적인 코드는 스스로를 설명하기 때문에 어떤 추가 문서도 필요하지 않다.

좋은 클래스는 목적이 명확하고, 작고 설계가 우아하다.

 

class WebPage {
    String content() { ... }
    void update(String content) { ... }
}

 

코드를 문서화하는 대신 코드를 깔끔하게 (clean) 구현하기 위해 노력한다.

깔끔하게 만든다 라는 말에는 단위 테스트도 함께 만든다는 의미가 포함되어있다.

 

깔끔하고 유지보수 가능한 단위 테스트를 추가하면,

클래스를 더 깔끔하게 만들 수 있고 유지보수성을 향상 시킬 수 있다.

 

단위 테스트가 바로 문서화이다.

하나의 단위 테스트는 한 페이지 분량의 문서 만큼이나 가치가 있다.

 

단위 테스트도 프로덕션 코드만큼 꾸준히 리팩토링을 진행해줘야 한다.

 

훌륭하고 깔끔한 단위 테스트를 만들기 위한 최고의 조언은 메인(프로덕션) 코드만큼 단위 테스트에도 관심을 기울여라.

 

 

'강의 > TDD, Clean Code with Java 12기' 카테고리의 다른 글

사다리타기 교육  (0) 2021.09.10
로또 게임 - 강의 정리  (0) 2021.09.07
자동차 게임 프로젝트 - 강의 정리  (0) 2021.09.03
옵셔널  (0) 2021.08.25
스트림 실습  (0) 2021.08.25