본문 바로가기

항해 99/Java

객체지향 생활체조 9가지 원칙

객체지향 생활 체조 원칙

소트웍스 앤솔러지(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