Search
📗

Elegant Objects - Yegor Bugayenko

0. Findings

가. 유지보수성, 유지보수성, 유지보수성

객체지향 패러다임에서 제공하는 높은 유지보수성을 누리자
복잡성과 중복을 줄여서 유지보수성을 높이는 방향으로 패러다임은 교체되었다
→ 유지보수성을 높이면 전체적인 소프트웨어 비용이 절감된다
객체지향 기술 안에 남겨진 절차지향 패러다임의 레거시로 인해 객체지향의 유지보수성을 저해하는 요소가 있다. 객체지향 기술 내 절차지향 레거시를 의식하고 제거하여 객체지향의 이점을 추가하는 방향으로 개발해야 한다

1. Birth

가. 클래스 이름

기능이 아니라 역량에 따라 객체를 정의하고 이름을 짓자
객체지향에서 클래스는 능동적인 성격의 객체 관리자로 인식해야 함
→ 절차지향의 자료구조와의 가장 큰 자이는 클래스는 데이터를 능동적으로 관리한다는 것
→ 외부에 의해 수동적으로 관리되는 절차지향의 자료구조와 혼동해서는 안됨
능동성이 핵심이기 때문에 클래스의 이름은 기능 중심이 아닌 역량 중심으로 지어야 한다
→ Not What He Does But What He Is
예시
class CashFormatter { private int dollars; CashFormatter(int dlr){ this.dollars = dls; } public String format(){ return String.format(~~) } } // to class Cash { private int dollars; Cash(int dlr){ this.dollars = dls; } public String usd(){ return String.format(~~) } }
Java
복사

나. 주 생성자와 보조 생성자

주 생성자는 하나만 만들어서 복잡성과 중복을 해결할 것
하나의 주 생성자와 복수의 보조 생성자를 구현하면 유효성 검사는 오직 주 생성자에서만 추가하면 된다
→ 만약 필요한 만큼의 주 생성자를 만들면 각각의 생성자에 대해 유효성 검사를 추가해야 한다

다. 생성자는 인스턴스 생성만

생성자에는 객체 생성만 수행함으로써 생성자와 메소드 역할을 구분할 것
생성자에서 객체의 다른 책임을 수행하면 추후 변경사항이 발생하면 큰 비용을 지불해야 함
→ 만약 생성자 내부의 로직의 일부에 수정사항이 생긴다면 해당 객체의 모든 인스턴스에 부작용이 발생할 수 있다
생성자를 가볍게 만들면 성능면에서 이점이 있다
→ 생성자가 가볍다는 것은 인스턴스 생성에 따라 반복적으로 수행해야 할 일이 적다는 것

2. Education

가. 4개 이하로 캡슐화

직관적인 객체로 유지하기 위해 객체 당 캡슐화는 4개 이하로 할 것
하나의 객체에 4개를 초과한 캡슐화는 직관성을 흔든다. 우리 사회에서도 하나의 정체성을 정의할 때 4개를 넘는 변수를 두지 않는다.
→ 주민등록번호의 경우, 생년월일, 성별, 지역을 기반으로 정체적을 둔다.
→ 개인을 타인에게 소개할 때, 나이 직업 성별 등 4개 이하의 특성만을 토대로 소개한다

나. 인터페이스로 느슨한 결합을!

클래스 간 결합은 필수불가결이나, 인터페이스를 활용하여 느슨한 결합으로 만들 것
인터페이스 기반 느슨한 결합의 장점
→ 시스템 전체에 대한 쉽게 이해 가능
→ 객체 간 협력이 필요한 부분에 대한 수정은 인터페이스 수정을 전제로 하기 때문에 시스템 일부에 대해 실수로 수정해도 시스템 전체는 유지될 수 있는 장치가 됨
→ 하나의 인터페이스를 구현하는 여러 클래스를 만들어서 기존 기능을 쉽게 대체 가능
인터페이스 활용 전제
→ 인터페이스에 정의된 모든 public method는 하나도 빠짐없이 모두 구현되어야 한다

다. 메소드 정의

메소드는 빌더와 조정자로 구분하여 정의하고 혼합하여 사용하지 말 것
빌더(builder): 무언가를 만들어서 새로운 객체를 반환하는 메서드
→ 반환타입은 void가 될 수 없다
→ 이름은 항상 명사로 짓는다. 단, 형용사를 더해서 행동을 구체적으로 꾸밀 수 있다
조정자(manipulator): 엔티티를 수정하는 메서드
→ 엔티티: 실세계를 모방하여, 추상화된 객체
→ 반환타입은 항상 void가 된다
→ 이름은 항상 동사로 짓는다. 단, 부사를 더해서 행동을 구체적으로 꾸밀 수 있다
메소드 정의 시 빌더와 조장자의 책임을 섞지 말고, 명확하게 구분할 것
int save(String content) // content를 저장하고 content의 bytes를 반환 // to int bytesSaved(String content) // or void save(String content)
Java
복사
빌더 패턴은 사용하지 말것
→ 빌더 패턴은 생성자에 너무 많은 매개변수를 전달하여 인스턴스 생성 시 가독성이 떨어지는 것을 방지하기 위해 등장했음
→ 매개변수가 너무 많다는 것 자체가 문제이므로 객체를 분리하는 것이 원천적인 문제해결 방법임
빌더와 조장자의 혼합 메소드에 대한 리팩토링
→ 파일 내용을 저장하는 것은 빌더, 저장된 파일을 bytes로 반환하는 것은 조장자의 책임으로 분리
→ OutputPipe 객체를 생성하여 빌더와 조장자의 역할을 각각 수행하는 메소드를 정의
class Document { // 파일 내용을 저장하고, 저장된 파일의 bytes를 반환함 int write(InputStream content); } // to class Document { OutputPipe output(); } class OutputPipe { void write(InputStream content); int bytes(); long time(); }
Java
복사
boolean 반환 메소드 정의
→ 빌더에 속하지만 가독성면에서 형용사로 이름을 지을 것. is는 중복이므로 간결한 이름을 유지하기 위해 제거하는 것 권장
boolean empty(); boolean readable(); boolean negative();
Java
복사
→ boolean 반환하는 논리 구성자는 주로 제어문에서 사용되는데 빌더의 명사로 이름을 지으면 어색하게 사용됨
if (name.emptiness() == true) {} // to if (name.empty() == true) {}
Java
복사
→ 동사로 이름 짓는 것을 방지하기 위해 앞에 is를 붙였을 때, 문법적으로 말이 되는지 확인할 수 있음
boolean exists(); // is exists X boolean equals(); // is equals X // to boolean present(); // is present O boolean equalTo(); // is equal to O
Java
복사

라. 퍼블릭 상수나 열거형을 객체로!

인터페이스 제약 속에서 변경 가능성을 높이기 위해 퍼블릭 상수나 열거형을 객체로 대체할 것
퍼블릭 상수를 사용하는 것은 절차지향 패러다임의 레거시
→ 객체 간의 폐쇄성을 무너뜨리기 때문에 캡슐화를 비롯한 객체지향 패러다임을 파괴하는 꼴
퍼블릭 상수를 객체로 변경하여 인터페이스라는 제약 속에서 수정되도록 강제해야 함
→ 장점 1: 하드 코딩된 부분을 대체함으로써 결합도 저하
→ 장점 2: 데이터가 아닌 기능을 공유함으로써 응집도 저하
열거형도 상수와 마찬가지로 대체 대상
→ 인터페이스 제약에 따른 변경 가능성이 없다는 점에서 클래스로 변경하는 것이 낫다

마. 오직 불변 객체만 남길 것

불변객체의 장점은 무수히 많다. 불변객체만 써라
불변객체란 인스턴스 생성 후 상태를 변경할 수 없는 객체
불변객체에 대한 수정 작업은 불가하므로 새로운 객체를 생성하여 반환하는 방식으로 변경해야 함
Cash five = new Cash(); Cash ten = five.multi(2);
Java
복사
불변객체 활용에 따른 이점
→ 쓰레드 안정성
→ NULL 참조 강제 제거, 불변 객체는 NULL로 초기화하면 의미있는 값을 넣을 수 없기 때문임
→ Map에서 특정 객체를 키로 사용할 때, 키에 대한 해쉬값이 달라지지 않으므로 추후 키 이슈가 생기는 것에서 안전한다

3. Employment

가. public 메소드는 5개 이하로

작은 객체의 기준은 public 메소드의 개수로 판단 가능, public 메소드가 많다면 리팩토링 필요

나. 정적 메소드는 절차지향의 레거시

정적 메소드의 폐해
→ 성능 상 불리: 객체지향적 객체를 활용하면 필요한 시점에 객체 내 메소드를 선택적으로 호출하면 되지만, 정적 메소드는 정적 메소드가 담긴 객체를 호출함에 따라 객체 내 작업에 필요한 모든 정적 메소드를 호출해야 함.
→ 다형성의 이점 존재하지 않음: 객체지향적 객체의 경우 하나의 객체를 이루는 하위 객체의 구현부를 교체할 수 있지만 정적 메소드는 불가
class Between implements Number{ private final Number num; Between(int left, int right, int x) { this(new Min(new Max(left, x), right)); } Between(Number number) { this.num = number; } }
Java
복사
→ 표현력 면에서 명령형의 정적 메소드는 절차지향적이기 때문에 패러다임 불일치
외부 라이브러리로 제공되는 정적 메소드를 리팩토링 하는 방법
→ 객체로 감싸는 방향으로 재정의
Java
복사
객체 간에는 완전 분리되어야 하므로 어떤 메서드나 주 생성자 안에서 new 연산자를 사용하면 안됨
→ 메서드나 생성자에 new 연산자를 통해 다른 객체가 생성되면 객체 간 결합도가 높아져서 유지보수성이 떨어짐

다. 유틸리티, 헬퍼, 싱글톤도 정적메소드와 다를게 없다

유틸리티 클래스 또는 헬퍼는 정적 메소드를 모아둔 이름 뿐인 객체
싱글톤도 결과적으로 전역변수와 같은 역할을 함
→ 캡슐화를 완벽하게 위반하므로 객체지향적 구현에 해가 됨
소프트웨어의 모든 클래스들이 사용할 기능에서 싱글톤을 대체할 방법
→ 해당 기능을 제공하는 객체를 모든 클래스에서 캡슐화할 것
→ 캡슐화할 것이 너무 많다면 관련 클래스를 리팩토링해서 더욱 작게 만들 것
if와 for문도 객체지향적으로 대체 가능함
float rate; if (client.age() > 65){ rate = 2.5; } else { rate = 3.0; } // to float rate = new If{ new GreaterThan(new AgeOf(client), 65), 2.5, 3.0 };
Java
복사

라. 매개변수로 NULL을 허용하지 말 것

NULL은 C 언어의 pointer에서 주소를 가리키지 않는다는 것이 객체지향으로 넘어온 것
→ 객체지향 언어에서 쓸 필요가 없는 타입
객체로 전달할 내용이 없다면 빈 것처럼 행동하는 객체를 전달할 것을 권장
→ 빈 것처럼 행동하는 객체 ‘AnyFile’을 정의하고, 빈 Mask 객체를 전달할 필요가 있을 때, find의 AnyFile을 전달함으로써 NULL을 대신할 수 있다
interface Mask { boolean matches(File file); } class AnyFile implements Mask { @override boolean matches(File file) { return true; } }
Java
복사
public Iterable<File> find(Mask mask) { Collection<File> files = new LinkedList<>(); for (File file : "모든 파일") { if(mask.matches(file)) { files.add(file); } } return files }
Java
복사
매개변수로 전달된 변수의 null 처리 로직으로 코드를 오염시키지 말고, NullPointerException이 발생하도록 방치할 것
→ NullPointerException으로 NULL 참조에 대한 디버깅을 충분히 할 수 있으므로 추가적인 예외처리는 불필요함

마. No Getter, No Setter

Getter와 Setter는 객체를 자료구조로 격하하여 객체지향 패러다임의 캡슐화 이점을 상쇄한다
프로그래밍 패러다임의 핵심 목표는 가시성의 범위를 축소해서 사물을 단순화시키는 것.
→ 한 시점에 사람이 인지할 수 있는 범위는 제한적. 제한된 범위 안에 쉽게 작업할 수 있다는 점에서 객체지향이 절차지향 보다 우월함. 이 우월성의 한 축이 캡슐화.
getter와 setter는 객체의 폐쇄성을 제거하고 자료구조의 개방성을 주입하는 레거시로 볼 수 있다.
→ getter, setter는 객체의 메소드로써 책임 또는 행동을 나타내지 않고 객체 내부의 데이터를 노출시킬 뿐임
→ 자료구조의 개방성을 객체에 부여하면 객체를 활용하는 방식이 절차지향적으로 흘러감
예시
→ Cash::getDollars의 경우 Cash 객체를 자료구조로 바라보면서 저장된 데이터를 외부에서 요구하는 것으로 해석할 수 있다
→ Cash::dollars는 Cash를 자료구조로 바라보지 않고 Cash 객체에게 얼마의 달러가 있는지를 물어보는 것으로 해석할 수 있다. 특히 get이 사라짐으로써 Cash 객체 내부에서 dollars에 대한 데이터 타입과 접근제어자 성격 등이 모두 가려진다.
class Cash { private final int value; public int getDollars(){ return this.value; } } // to class Cash { private final int value; public int dollars(){ return this.value; } }
Java
복사

4. Retirement