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
복사