본문 바로가기
Book

[독서 기록] 엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 3장 - 2

by Renechoi 2022. 11. 19.
 
엘레강트 오브젝트
『엘레강트 오브젝트』 는 〈-er로 끝나는 이름을 사용하지 마세요〉, 〈생성자 하나를 주 생성자로 만드세요〉, 〈생성자에 코드를 넣지 마세요〉, 〈가능하면 적게 캡슐화하세요〉, 〈최소한 뭔가는 캡슐화하세요〉, 〈항상 인터페이스를 사용하세요〉등을 수록하고 있는 책이다.
저자
Yegor Bugayenko
출판
지앤선
출판일
2020.12.30

 

 

엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 3장 -2 (조영호 옮김, 지앤선) 

 


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

- 120p 

 

 

findAll, find 

=> 각각을 사용하지 않고 하나의 메서드만 사용하면 더 편할 것이다

 

=> 

 

이 방식이 논리적이라고 생각할 수는 있겠지만, 각각의 객체가 자신의 행동을 온전히 책임진다는 객체 패러다임과는 상반되는 아이디어입니다.

- 121p 

 

 

인자의 값으로 NULL을 허용하면 mask==null과 같은 비교문을 사용할 수 밖에 없습니다. 객체와 협력할 때마다 객체의 '실체'를 확인하는 것 말고는 NULL인지 여부를 판단할 수 있는 방법이 없습니다. 그리고 NULL 여부를 체크함으로써 객체가 맡아야 하는 상당량의 책임을 빼앗게 됩니다. 우리는 외부에서 자신의 데이터를 다뤄주기만을 기대하고 스스로를 책임질 수 없는 멍청한 자료구조로 객체를 퇴화시키고 있는 것입니다.

- 122p

 

 

충성스러우면서 불변이거나, 아니면 상수이거나 

 

비록 content() 메서드의 결과를 예측할 수는 없더라도 WebPage는 불변 객체에 속합니다.

- 128p 

 

 

객체란 디스크에 있는 파일, 웹 페이지, 바이트 배열, 해시맵, 달력의 월과 같은 실제 엔티티의 대표자입니다. 여기서 실제라는 말은 객체의 가시성 범위 밖에 존재하는 모든 것을 의미합니다. 예를 들어 다음 코드에서 객체 f는 디스크에 저장되어 있는 파일을 대표합니다. 

 

public void echo() {
	File f = new File("/tmp/test.txt");
    System.out.println("File. ...

 

- 128p 

 

 

동일한 URI를 가진 두 개의 Webpage 인스턴스를 생성할 경우 이들은 서로 다른 객체일까요? 서로 다른 행동을 노출할까요? 그렇지 않습니다. 두 객체는 동일한 웹 페이지를 대표합니다. 따라서 두 객체 중 무엇을 사용하더라도 아무런 차이가 없습니다. 

- 129p 

 

 

 

절대 getter와 setter를 사용하지 마세요 

 

class Cash {
	private int dollars;
    public int getDollars(){
    	return this.dollars;
    }

	public void setDollars(int value) {
    	this.dollars = value;
    }
}

 

Cash는 getter인 getDollars()를 이용해 노출되고 setter인 setDollars()를 이용해 변경 가능한 하나의 private 프로퍼티 dollars를 포함하는 가변(mutable) 클래스입니다. 섹션 2.6에서 이미 모든 클래스는 불변이어야 한다고 설명했습니다. 이 클래스는 가변입니다. 섹션 2.4에서 메서드의 이름을 짓는 방법을 설명했는데, 이 기준에 따르면 이 클래스에 포함된 두 메서드는 이름이 잘못 지어졌습니다. 이 클래스에너느 생성자가 없기 때문에 섹션 2.1에서 설명한 원칙에도 위배됩니다. 

- 136p 

 

 

그것은 바로 Cash가 진짜 클래스가 아니라 단순한 자료구조라는 사실입니다.

- 137p 

 

 

클래스는 다릅니다. 클래스는 어떤 식으로든 멤버에게 접근하는 것을 허용하지 않습니다. 게다가 자신의 멤버를 노출하지도 않습니다. 심지어 클래스  안에 dollars라는 멤버가 있는지조차 알 수 없습니다. 우리가 할 수 있는 일이라고는 객체에게 자기 자신을 print()하라고 요청하는 것뿐입니다. print()가 실제로 어떤 방식으로 동작하는 지도 알 수 없고, 캡슐화된 어떤 멤버가 이 작업에 개입하는 지도 알 수 없습니다. 이것이 바로 캡슐화이며, OOP가 지향하는 가장 중요한 설계 원칙 중 하나입니다. 

- 137p

 

 

모든 프로그래밍 스타일의 핵심 목표는 가시성의 범위를 축소해서 사물을 단순화시키는 것입니다. 특정한 시점에 이해해야 하는 범위가 작을수록, 소프트웨어의 유지보수성이 향상되고 이해하고 수정하기도 쉬워집니다.

-138p

 

 

OOP에서는 코드가 데이터를 지배하지 않습니다. 대신 필요한 시점에 객체가 자신의 코드를 실행시킵니다. 너무 추상적으로 들릴 수도 있겠지만, 이게 제가 할 수 있는 최선의 설명입니다. 추상적이든 아디느 상관없이, 절차적인 스타일과 객체지향적인 스타일 사이의 근본적인 차이점을 이해하는 것은 중요합니다. 코드는 더 이상 핵심이 아닙니다. 코드는 OOP에서 부차적인 요소입니다. 객체가 일급 시민이며, 생성자를 통한 객체 초기화가 곧 소프트웨어입니다. 소프트웨어는 연산자나 구문이 아닌 생성자를 통해 구성됩니다.

- 138p 

 

 

public 프로퍼티를 추가해야하는 어색한 상황을 피하기 위해, 프로퍼티를 private으로 변경하고 모든 프로퍼티에 getter와 setter를 추가하기로 결정했습니다.

- 140p 

 

 

제 이야기의 요점은 getter와 setter를 사용하면 OOP의 캡슐화 원칙을 손쉽게 위반할 수 있다는 점입니다. 겉으로는 메서드처럼 보이지만, 실제로는 우리가 데이터에 직접 접근하고 있다는 불쾌한 현실을 가리고 있을 뿐입니다. 이 경우에 데이터는 무방비로 노출되어 있는 것과 같습니다.

- 140p 

 

 

 

어떤 데이터를 반환하는 메서드를 포함하는 것은 괜찮습니다. 예를 들어 보겠습니다. 

 

class Cash {
	private final int value;
    public int dollars() {
    	return this.value;
    }
}

 

하지만 이 메서드의 이름을 다음과 같이 짓는 것은 적절하지 않습니다. 

 

class Cash {
	private final int value;
    public int getDollars() {
    	return this.value;
    }
}

 

이 차이는 근본적이며 매우 중요합니다. getDollars()는 "데이터 중에 dollars를 찾은 후 반환하세요"라고 이야기하는 것이고, dollars()는 "얼마나 많은 달러가 필요한가요?"라고 묻는 것입니다. dollars()는 객체를 데이터의 장소로 취급하지 않고, 객체를 존중합니다. 

 

- 141 ~ 142p

 

 

부 ctor 밖에서는 new를 사용하지 마세요

 

class Cash {
	private final int dollars;
    
    public int euro() {
    	return new Exchange().rate("USD", "EUR") * this.dollars; 140
    }
}

 

이 예제는 의존성에 문제가 있는 코드의 전형적인 모습을 잘 보여주고 있습니다. 예제의 euro() 메서드 안에서는 new 연산자를 이용해 Exchange 인스턴스를 생성하고 있습니다. ... 문제를 일으킨 범인의 이름은 '하드코딩된 의존성'입니다. Cash 클래스는 Exchange 클래스에 직접 연결되어 있기 때문에, 의존성을 끊기 위해서는 Cash 클래스의 내부 코드를 변경할 수 밖에 없습니다. 

- 143p

 

 

다음은 수정한 후의 Cash 클래스입니다.

 

class Cash {
	private final int dollars;
    private final Exchange exchange;
    
    Cash(int value, Exchange exch) {
    	this.dollars = values;
        this.exchange = exch;
    }
    
    public int euro() {
    	return this.exchange.rate("USD", "EUR') * this.dollars;
    }
}

문제가 해결됐습니다. 다음은 print를 테스트하는 올바른 코드입니다.

 

Cash five = new Cash(5, new FakeExchange()));
print("$5 equals to %d", five.euro()));

 

수정한 코드에서는 ctor의 두 번째 인자에 Exchange 인스턴스를 전달해야 합니다. Cash 클래스는 더 이상 Exchange 인스턴스를 직접 생성할 수 없습니다. 오직 ctor을 통해 제공된 Exchange와만 협력할 수 있습니다. Cash 클래스는 더 이상 Exchange 클래스에 의존하지 않습니다. 정확히 말해서 의존은 하지만, 의존성을 제어하는 주체가 Cash가 아니라 우리 자신이라는 점에서 차이가 있습니다. Cash는 USD를 EUR로 변환하는데 필요한 환율을 얻을 위치를 직접 결정하지 않습니다. 대신 우리의 결정에 의지하고, 우리가 제공하는 객체와 협력합니다. 

 

다시 말해서, 객체가 필요한 의존성을 직접 생성하는 대신, 우리가 ctor을 통해 의존성을 주입(inject)합니다.

 

- 144 ~ 145p 

 

 

인트로스펙션과 캐스팅을 피하세요

- 148p

 

 

 


 

관점 지향 프로그래밍을 사용하세요

 

AOP를 사용한다면 재시도 매커니즘을 다음과 같이 구현할 수 있습니다. 

 

@RetryOnfailure(attempts = 3)
public String content() throws IOException {
	return http();
}

- 172p

 

 

 

반응형