본문 바로가기

우아한테크코스

📝 엘레강트 오브젝트 뽀개기 1 강의 정리

2021-03-20글

강의를 들으며 정리하기 ✍️

엘레강트 오브젝트

image

  • 문자열과 원시값을 포장해라
  • 반환 값은 모두 일급 컬렉션image

인자의 값으로 null을 절대 허용하지 마세요

public Iterable<File> find(String mask) {
    if (mask == null) {
// .
} else {
// .
} }

null을 체크하는 로직을 구현하는 것은 객체를 무시하는 것이다.
mask 객체를 존중했다면 조건의 존재 여부를 객체 스스로 결정하게 했을 것이다.
이를 위해 원시 값과 문자열을 포장하면 null을 허용하지 않을 수 있다.

image

이 부분이 randomNumber를 무시하는 것이 아닌가.
randomNumber를 객체로 만든다. (원시값 포장).

image

객체에게 메시지를 보내도록 리팩토링한다.

image

테스트 가능하게 오버라이딩.

RandomNumber의 null 체크 안해줘도 되나 ?
이 메서드가 외부에 쓰인다면 고민해봐야하지만, 내부에서만 move가 호출되면
null 체크를 굳이 안해줘도 좋다.
내가 컨트롤할 수 있는 범위라면 안해도 될 것 같다.
move() 를 사용하는 범위가 어디까지인지에 따라 null 체크 여부가 달린 것 같다.

final이거나 abstract 이거나

상속은 객체들의 관계를 복잡하게 만드니 최대한 자제하고,
final이나 abstract로 만들어라

변수에 final 을 붙이면 재할당이 불가능하지만, 메서드에 붙이면 오버라이딩이 불가하다.
클래스에 final 은 상속을 불가능하게 한다.

final 클래스가 테스트 가능하도록 하는 법.
인터페이스로 만든다.

RandomNumber 클래스를 final로 만들고 Number 인터페이스를 만든다.

imageimageimage

그러면 이렇게 Number 인터페이스와 의존 관계를 맺는다.
인터페이스와 의존 관계를 맺는게 final 클래스와 의존 관계를 맺는 것 보다 테스트가 더 용이하다.

클래스에 final을 붙이는 것은 좋은 습관이다.
final을 테스트하고 싶으면 인터페이스를 추출하면 된다.

인터페이스 구현체가 여러개가 되면 중복들이 많아지는데,
이 중복을 어떻게 없애나 ?
인터페이스 구현체 중간에 추상 클래스를 둔다.
Car - 인터페이스
AbstractCar - 추상 클래스
Sonatar - 클래스

이러면 추상 클래스의 필요성을 느낄 수 있을 것이다.

근데 default 메소드를 사용하면 안되나 ?

포비는 default 를 남용하지 않는다. 추상 클래스를 쓰는게 더 좋다.

중복을 제거할 때 추상 클래스의 인스턴스 변수, 즉 인스턴스에 종속된 메서드면 default 메서드를 만들기 어려울 수 있다. default 메서드를 만들 수 있는 경우, 없는 경우가 있으니 잘 구별.
추상 클래스는 상태를 가지니 상태를 활용할 수 있다.

상속 때 부모 클래스에 있는 인스턴스 변수를 private로 막아라 !
그래야 캡슐화가 잘 되었다고 한다.
접근할 때는 메서드를 통해 접근해라

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

클래스 이름을 지을 때 좋은 가이드.

클래스는 객체를 만들어 내는 역할일 뿐이다 (객체의 Factory).
클래스는 객체를 만들고, 추적하고, 적절한 시점에 파괴한다 (라이프 사이클 관리).

클래스를 객체의 템플릿으로 바라보는 것은 클래스를 수동적인 존재로 만드는 것이다.
클래스는 객체의 능동적인 관리자이다.
객체를 꺼내거나 반환하는 저장소이다.

클래스 이름을 짓는 방식

클래스의 객체들이 무엇을 하는지 가 아닌, 무엇인지 에 기반해 지어라.

  • 무엇을 하는지로 이름을 지은 잘못된 예
public class CashFormatter {
    private int dollars;
    public CashFormatter(int dollars) {
        this.dollars = dollars;
    }
    public String format() {
        return String.format("$ %d", this.dollars);
    }
}

무엇인지를 기반으로 객체의 역량을 나타내도록 이름을 지어야 한다.

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

객체는 객체의 외부 세계와 내부 세계를 이어주는 연결장치가 아니고,
내부에 캡슐화된 데이터를 다루기 위해 요청할 수 있는 절차의 집합도 아니다.
객체는 캡슐화된 데이터의 대표자이다.

무엇인지로 객체를 추출하면 무엇을 하는지까지 포괄하는 것이 아닌가.
무엇을 하는지는 수동적인 존재가 된다.

메서드에서는 protected를 사용할 수 있는데 변수에는 사용하지 않는 것이 좋다.
DTO는 모르겠는데 도메인 객체에서는 무조건 인스턴스 변수는 private여야 한다.

“무엇인지” 생각하지 않고 “무엇을 하는지”를 먼저 생각하고 설계하지 말라

처음에 메세지를 정하고 객체의 협력을 구상해야하는데 이를 하기 위해는 “무엇을 하는지”가 아니라 객체가 “무엇인지” 부터 생각을 해야하는 것 같아요. 객체가 무엇인지 생각하고 책임을 부여하면서 “무엇을 하는지” 가 된다.

이미 er과 or을 가지고 있는 것들은 써도 되지만, 나머지는 자제하라

CashFormatter가 아닌 FomattedCash는 어때 ?
CashFormatter는format() 이외의 기능을 가지면 부자연스럽기 때문이다.

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

메서드 명은 무조건 동사가 아니다.

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

반환타입이 void가 아닌 메서드로, 무언가를 만들어 새로운 객체를 반환한다.
이 메서드의 명은 항상 명사여야 한다.
형용사를 덧붙여 메서드의 의미를 좀 더 풍부하게 설명하는 명사로 나타낼 수도 있다.

Ex) float speed(), Employee employee(), String parsedCell()

조정자(manipulator)의 이름은 동사로

반환타입이 void인 메서드로 엔티티를 수정하는 메서드이다.
이 메서드 명은 항상 동사여야 한다.
부사를 덧붙여 메서드의 문맥과 목적에 관한 풍부한 정보를 제송하는 동사로 나타낼 수 있다.

ex) void save(String content), void quicklyPrint(int id);

잘못된 예시

boolean put(String key, Float value);

이 메서드는 PutOperation 과 같은 클래스를 추가해 save() , success() 메서드로 분리한다.

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

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

cookBrownie() , brewCupOfCoffee() 는 실제로는 객체의 메서드가 아니고, 프로시저이다.
객체는 자신의 의무를 수행하는 방법을 알고 있고, 존중해줘야하는 살아있는 유기체이다.
단순한 명령에 따르지 않고, 계약에 기반해 일하고 싶어한다.

하지만 관념적인 부분은 포기해도 괜찮다 !

getXxx()는 내부적으로 xxx를 가지고 있을 것이라는 것을 드러내지만,
명사를 사용하면 그 메서드 내부에 로직이 있는지를 숨길 수 있다.

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

ex) DJ에게 음악을 틀어달라고 요청할 때

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

이 중 방법 1가 더 객체를 존중하고 있다.
그 객체가 알아서 하겠지 ! 를 생각해라. 객체를 믿어라.

print도 매개변수가 다르니 print() 로 통일할 수 있다.

빌더와 조정자로 분리 3 - 빌더와 조정자 혼합하기

class Document {
    int write(InputStream content);
}

값을 반환하고 있는 write를 빌더와 조정자로 분리한다.

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

빌더와 조정자로 분리 - Boolean 값을 반환하는 경우 형용사로 지어라

빌더이지만, 가독성을 위해 형용사로 지어라.

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

클래스는 23개의 메서드와 510개의 생성자를 포함해야한다.

생성자가 많고 메서드가 적을 수록 응집도가 높고 견고한 클래스가 된다.
생성자가 많을 수록 클라이언트가 클래스를 더 편하게 사용할 수 있다.

  • 주 생성자 : 프로퍼티를 초기화한다. 오직 주 생성자만 담당한다.
  • 부 생성자 : 주 생성자를 호출한다.

인자수가 적은 수부터 많은 순으로, 주 생성자는 마지막에 둔다.

테스트를 위한 생성자는 좋다 ! 하지만 메서드 추가는 옳지 않다 !
생성자 여러개를 테스트 할 때 다른 타입 매개변수의 생성자 동등성을 테스트 하면 된다.

뷰에서 List 정도로 가공은 할 수 있을 것 같다.

좋은 객체는 모든 메서드가 각각 모든 인스턴스 변수를 사용하고 있다 (100%는 어렵다).

문서를 작성하는 대신 테스트를 만들어라

주석이 없어도 클래스명과 메서드만을 봐도 무슨일을 하는지 알 수 있게,
테스트를 통해 메서드의 의도를 알도록 하라.

깔끔하게 만든다라는 것은 단위 테스트도 만든다는 의미이다.
단위 테스트는 클래스의 일부이고 독립적인 개체가 아니다.

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

인자에 손을 대지 말라는 의미이다.

  • 잘못된 예시
public class Cash {
    private int dollars;
    public Cash(String dlr) {
        this.dollars = Integer.parseInt(dlr);
    }
}

생성자에 코드가 있을 경우 객체 변환과 관련한 예외를 제어할 수 없다.

  • 좋은 예시
public class Cash {
    private Number dollars;
    public Cash(String dlr) {
        this.dollars = new StringAsInteger(dlr);
    }
}
public class StringAsInteger extends Number {
    private String source;
    public StringAsInteger(String txt) {
        this.source = txt;
    }
@Override
    public int intValue() {
        return Integer.parseInt(this.source);
    }
... }

이렇게 리팩토링을 하면 실제로 객체를 사용하는 시점까지의 객체 변환 작업은 연기된다.
생성자에 코드가 없을 경우 성능 최적화가 더 쉬워 실행 속도가 더 빨라진다.

진정한 객체지향에서 인스턴스화란 더 작은 객체들을 조합해서 더 큰 객체를 만드는 것이다.

객체의 변환을 뒤로 미뤄 파싱이 여러번 실행되지 않도록 데코레이터(decorator)를 추가해 파싱 결과를 캐싱할 수 있다.

public class CachedNumber extends Number {
    private Number origin;
    private List<Integer> cached = new ArrayList<>(1);
    public CachedNumber(Number num) {
        this.origin = num;
    }
@Override
    public int intValue() {
        if (this.cached.isEmpty()) {
            this.cached.add(this.origin.intValue());
        }
        return this.cached.get(0);
    }
... }
public class Cash {
    private Number dollars;
    public Cash(String dlr) {
        this.dollars = new CachedNumber(new StringAsInteger(dlr));
    }
}