객체지향 생활 체조 원칙
소트웍스 앤솔러지(ThoughWorks Anthology) 책에서 나오는 원칙으로 9가지 원칙을 준수하면서 객체지향을 추구할 수 있다고 한다.
1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
2. else 예약어를 쓰지 않는다.
3. 모든 원시 값과 문자열을 포장한다.
4. 한 줄에 점을 하나만 찍는다
5. 줄여 쓰지 않는다(축약 금지).
6. 모든 엔티티(Entity)를 작게 유지한다.
7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
8. 일급 컬렉션을 쓴다.
9. Getter / Setter / 프로퍼티를 쓰지 않는다.
1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
- 코드에 너무 많은 들여쓰기가 있다면, 가독성과 유지 관리 측면에서 좋지 않은 경우가 많음
- 1개 메서드 안에서 if / for / while 등을 2depth 이상 사용하지 않는다
- 한 메서드에서 들여쓰기가 여러 개 존재하면, 해당 메서드는 여러가지 일을 하고 있다고 봐도 무관함
- 메서드는 맡은 일이 적을수록 재사용성이 높고 디버깅도 용이함
기존 코드
class Board {
public String board() {
StringBuilder buf = new StringBuilder();
// 0
for (int i = 0; i < 10; i++) {
// 1
for (int j = 0; j < 10; j++) {
// 2
buf.append(data[i][j]);
}
buf.append("\n");
}
return buf.toString();
}
}
수정된 코드
class Board {
public String board() {
StringBuilder buf = new StringBuilder();
collectRows(buf);
return buf.toString();
}
private void collectRows(StringBuilder buf) {
for (int i = 0; i < 10; i++) {
collectRow(buf, i);
}
}
private void collectRow(StringBuilder buf, int row) {
for (int i = 0; i < 10; i++) {
buf.append(data[row][i]);
}
buf.append("\n");
}
}
- Extract Method를 사용해 코드 리펙토링을 실시한다, 코드의 라인 수는 줄지 않지만 가독성 측면에서 상당히 좋아진 것을 알 수 있음
Extract Method : 그룹으로 함께 묶을 수 있는 코드 조각이 있으면 코드의 목적이 잘 드러나도록 메서드의 이름을 지어 별도의 메서드로 뽑아내는 것
2. else 예약어를 쓰지 않는다.
- else가 있는 코드는 의도를 파악하기 어려움, early exit pattern을 적용해서 의도를 분명히 나타낼 수 있다.
- 조건문은 복제의 원인이 되기도 하며 가독성 측면에서도 좋지 않음
- 조건은 optimistic, defensive 하게 접근 가능
- optimistic : 오류 조건을 걸러내는 if 조건이 있으면, 나머지 로직은 if 이후의 기본 시나리오를 따름
- defensive : 기본 시나리오를 조건에 저장한 후 조건이 충족되지 않으면 오류 상태를 반환하게 함
- 객체 지향 프로그래밍의 다형성 기능이나 Null Object, State 패턴, Strategy 패턴이 유용함
Null Object 패턴 : 인터페이스는 구현하지만 아무 일도 하지 않는 객체 또는 특정 객체가 존재하지 않는다는 것을 null이 아닌 Null Object를 반환하여 NullPointerException을 방지하는 기법
State 패턴 : 객체가 특정 상태에 따라 행위를 달리하는 상황에서, 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화하여 상태가 행동을 할 수 있도록 위임하는 패턴
Strategy 패턴 : 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴
기존 코드
public void login(String username, String password) {
if (userRepository.isValid(username, password)) {
redirect("homepage");
} else {
addFlash("error", "Bad credentials");
redirect("login");
}
}
수정된 코드
// 1
public void login(String username, String password) {
if (userRepository.isValid(username, password)) {
return redirect("homepage");
}
addFlash("error", "Bad credentials");
return redirect("login");
}
// 2
public void login(String username, String password) {
String redirectRoute = "homepage";
if (!userRepository.isValid(username, password)) {
addFlash("error", "Bad credentials");
redirectRoute = "login";
}
redirect(redirectRoute); // 최종적으로 두 로직 모두 여기까지 도달
}
3. 모든 원시값과 문자열을 포장(Wrap)한다.
- 간단하게 객체 내의 모든 원시 요소를 캡슐화 한다
- 안티 패턴 중 하나인 Primitive Obsession을 피하기 위함
- 원시형 변수로는 컴파일러가 의미적으로 맞는 프로그램 작성을 안내할 수 없음, 포장한 객체로라면 아주 사소하더라도 컴파일러와 개발자에게 해당 값이 어떤 값이며 왜 쓰는지에 대해 정보를 전달할 수 있음
기존 코드
private static int sum(int[] numArray) {
int result = 0;
for(int i = 0; i < numArray.length; i++) {
int num = numArray[i];
if(num < 0) {
throw new RuntimeException();
}
result += num;
}
return result;
}
수정된 코드
public class Positive {
private int number;
public Positive(int number) {
if (number < 0) {
throw new RuntimeException();
}
this.number = number;
}
public Poisitive add(Positive other) {
return new Positive(this.number + other.number);
}
public int getNumber() {
return number;
}
}
private static Positive[] toPositives(int[] values) {
Positive[] numbers = new Positive[values.length];
for (int i = 0; i < values.length; i++) {
numbers[i] = new Positive(values[i]);
}
return numbers;
}
private static int sum(Positive[] numbers) {
Positive result = new Positive(0);
for (Positive number : numbers) {
result = result.add(number);
}
return result.getNumber();
}
- 클래스 분리를 통해 객체 지향적인 코드를 유도하고 SOLID의 단일 책임 원칙도 만족할 수 있게 도움
- Positive 객체는 도메인을 충분히 반영하고 스스로를 검증하는 자율적인 객체이다. 결과적으로 Calculator 같은 상위 클래스에 비대한 책임을 주는 것을 막고 추가적인 요구사항에 대응하기가 매우 편리해짐
4. 한 줄에 점을 하나만 찍는다.
- 메서드 호출을 연쇄적으로 연결해서는 안 된다
- 메서드 체인 패턴(스트림, 빌더 등)을 구현하거나 Fluent Interface를 만들 때는 적용되지 않음
- Law of Demeter : 인접한 친구에게만 말을 걸어라
- 자신 소유의 객체, 자신이 생성한 객체, 그리고 누군가 준(파라미터로) 객체에만 메시지를 보낼 것, 그렇지 않으면 다른 객체에 너무 깊숙히 관여하게 됨 - 캡슐화를 어기는 것
- 메시지를 받는 객체는 자신의 속을 오픈하기보다는, 작업을 해주도록 해야 함
기존 코드
public class JamieObject {
void getMoney() {
jamieWallet.getTotalMoney().getMoney();
}
}
class JamieWallet {
private final JamieMoney totalMoney;
JamieMoney getTotalMoney() {
return totalMoney;
}
}
class JamieMoney {
private final int money;
int getMoney() {
return getMoney();
}
}
수정된 코드
public class JamieObject {
void getMoney() {
jamieWallet.getTotalMoney();
}
}
class JamieWallet {
private final JamieMoney totalMoney;
int getTotalMoney() {
return totalMoney.getMoney();
}
}
class JamieMoney {
private final int money;
int getMoney() {
return getMoney();
}
}
5. 줄여쓰지 않는다.
- 과도한 축약은 코드 가독성을 저해한다, 무조건 짧다고 좋은 것은 아님
- 메서드의 이름이 긴 이유 중 하나는, 책임을 너무 많이 갖고 있거나, 적절한 클래스의 아래에 위치하지 않아서 일 수 있음
- 한 두 단어 정도로 되어있는 경우에는 축약을 하지 말 것 (englishName을 eName으로 바꾸지 말 것)
- 문맥상 중복되는 단어는 자제할 것(클래스 Jamie의 printJamieName 메서드의 경우 printName으로)
- 이름은 의미를 전달할 수 있을 정도로 충분하게 길어도 됨(의미 전달이 명확한 것이 더 중요함).
기존 코드
public class Jamie {
void printJamieName() {
String EName = "Jamie";
String KName = "제이미";
}
}
수정된 코드
public class Jamie {
void printName() {
String englishName = "Jamie";
String koreanName = "제이미";
}
}
6. 모든 Entity(엔티티)를 작게 유지한다.
- 하나의 클래스는 50줄을 넘지 않고, 하나의 패키지에는 파일이 10개 이상 담지 않기를 권장한다.
- 50줄 정도의 클래스는 가독성이 좋음, 클래스가 100줄 이상이 넘어가는 경우 한 가지 일만 하는 것이 아니므로 분리할 수 있음
- 패키지는 하나의 목적을 달성하기 위한 연관된 클래스들의 모임, 작게 유지하면 진정한 정체성을 가지게 된다.
- 헥사고날 아키텍처의 어댑터에서는 package-private(defualt)을 사용하므로 JPA 엔터티나 Mapper, Adapter 등이 한 패키지에 있어야 한다. 그래서 한 패키지 안에서 많은 클래스가 존재할 수 있다.
- 바운디드 컨텍스트를 충분히 분리했는 지를 고민
- Mapper, JPA Entity 정도만 public으로 사용하고 Adapter는 패키지에 맞게 분리하는 방법도 가능
7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 인스턴스 변수가 많아질수록 클래스의 응집도는 낮아진다.
- 마틴 파울러는 대부분의 클래스가 인스턴스 변수 하나만으로 일을 하는 것이 적합하다고 말함
- 인스턴스 변수 : 기본형 또는 자료 구조형 객체들
- 일급 컬렉션이나 Wrapper 객체는 해당되지 않음
- 인스턴스 변수의 분해는 여러 개의 관련 인스턴스 변수의 공통성을 이해하게 하여 자료구조형으로 묶어 일급 컬렉션으로 생성할 수 있게 해줌
- 인스턴스 변수들의 집합을 갖고 있는 것에서, 협력 객체(일급 컬렉션/Wrapper 객체)의 계층 구조로 분해하면 더 효율적인 객체 모델이 될 수 있음
- 어려운 경우 객체를 상관 관계가 있는 반(half)씩 나누거나, 인스턴스 변수를 둘 골라서 그로부터 하나의 객체를 만드는 등을 하는 것을 추천
- CleanCode에서는 인스턴스 변수 뿐 아니라 메소드의 가장 이상적인 파라미터 개수는 0개라고 말한다. 인스턴스 필드나 메서드 파라미터를 최대한 적게 유지해서 응집도를 높여야 한다.
기존 코드
public class Jamie {
private final String name;
private final String job;
private final int age;
public Jamie(String name, String job, int age) {
this.name = name;
this.job = job;
this.age = age;
}
}
수정된 코드
public class Jamie {
private final Name name;
private final Job job;
private final Age age;
public Jamie(Name name, Job job, Age age) {
this.name = name;
this.job = job;
this.age = age;
}
}
8. 일급 컬렉션을 쓴다.
- 일급 컬렉션 : 컬렉션을 포함하는 해당 클래스는 컬렉션을 제외한 다른 멤버 변수를 포함하지 말아야 한다.
- Unit이라는 하나의 클래스가 있고 이를 여려 개 묶어(List<Unit>) 단위로 사용하는 Units는 1급 컬렉션이 될 수 있음
- 일급 컬렉션의 장점
- 비즈니스에 종속적인 자료구조 : 해당 컬렉션에서 필요한 모든 로직은 일급 컬렉션에서 구현 → 비즈니스에 종속적인 자료 구조가 만들어짐
- Collection의 불변성 보장 : private final을 선언하여 Collection을 생성해주고, 생성자를 통해 생성해주면 재할당이 불가능하므로 불변 컬렉션이 됨
- 상태와 행위를 한 곳에서 관리 : 값과 로직이 함께 존재함(해당 일급 컬렉션을 사용하는 클래스에서는 상태와 로직을 한 곳에서 관리할 수 잇음)
- Collection에 명명 가능 : 일급 컬렉션 생성 시 클래스명을 명명하게 됨(검색도 편하고 명확한 표현도 가능하게 됨)
- 컬렉션의 모든 API 또한 Open하지 않게 되는 점도 장점
- 컬렉션이 add, delete, peek, sort 라는 API를 지원하는데, 1급 컬렉션의 public API가 add, delete뿐 이라면 외부의 사용자는 오직 이 2가지 메서드만 사용하게 됨
예시 코드
public class Store {
private Set<Brand> brands;
public Store(List<Brand> brands) {
validSize(brands);
this.brands = brands;
}
private void validSize(List<Brand> brands) {
if(brands.size() >= 10) {
throw new IllegalArgumentException("브랜드는 10개 초과로 입점할 수 없습니다.");
}
}
}
- 일급 컬렉션은 필요한 도메인 로직을 담을 수 있다. 이로써 컬렉션을 사용하는 클래스에서 검증하는 것이 아니라, 일급 컬렉션에서 자율적으로 검증할 수 있음
- 일급 컬렉션을 사용하면 컬렉션의 불필요한 메서드에 대한 가시성 문제도 해결할 수 있음
9. Getter / Setter / property 를 쓰지 않는다.
- 객체의 상태에 기반한 모든 행동은 객체가 스스로 결정하도록 해야 한다.
- 객체의 상태를 가져오기 위해 꼭 필요하다면 Getter는 사용 가능
- 도메인 객체에만 해당되고, DTO / Controller 등에는 해당 되지 않는다.
- DTO 등에서 사요아기 위해 도메인 객체에 Getter를 놓는 것은 상관 없음
기존 코드
// Game
private int score;
public void setScore(int score) {
this.score = score;
}
public int getScore() {
return score;
}
// Usage
game.setScore(game.getScore() + ENEMY_DESTROYED_SCORE);
수정된 코드
// Game
public void addScore(int delta) {
score += delta;
}
// Usage
game.addScore(ENEMY_DESTROYED_SCORE);
- Getter / Setter 가 아닌 객체에게 메시지를 전달하도록 변경하면 메서드의 의도를 노출하기 쉬워짐
- game 객체에게 addScore 라는 메시지를 전달
- game 객체는 이제 자신 스스로 전달받은 메시지에 대하여 결정을 내릴수 있게 됨
'항해 99 > Java' 카테고리의 다른 글
Java - SOLID (0) | 2024.03.12 |
---|---|
WIL-4 (0) | 2024.03.03 |
빌터 패턴 (1) | 2024.02.26 |
WIL - 3 (0) | 2024.02.25 |
프로그래밍 테스트 및 IDE 프로젝트 빌드 오류 해결 (0) | 2024.02.19 |