BackEnd/Refactoring

[Refactoring] 객체지향 생활 체조 원칙

샤아이인 2022. 2. 18.

The ThoughtWorks Anthology을 일부 읽고 정리한 내용 입니다.

 

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

  • 1. 한 메서드에 오직 한 단계의 들여 쓰기만 한다.
  • 2. else 예약어를 쓰지 않는다.
  • 3. 모든 원시 값과 문자열을 포장한다.
  • 4. 한 줄에 점을 하나만 찍는다.
  • 5. 줄여 쓰지 않는다(축약 금지).
  • 6. 모든 엔티티를 작게 유지한다.
  • 7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  • 8. 일급 컬렉션을 쓴다.
  • 9. getter/setter/프로퍼티를 쓰지 않는다.

 

1. 한 메서드에 오직 한 단계의 들여 쓰기만 한다.

코드에 너무 많은 들여쓰기가 있다면, 가독성과 유지 관리성에 좋지 않은 경우가 많습니다.

대부분의 경우 머릿속으로 컴파일 하지 않고는 쉽게 이해하지 못하도록 하는 경우가 대부분 입니다.

 

예를 들어 다음 코드를 살펴보실까요?

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();
    }
}

위와 같이 서로 다른 수준에서 다양한 조건을 갖거나, 2중 loop가 도는 경우 코드가 간단하면 모를까 복잡한 코드라면 이해하기 더 어려워 집니다.

 

위에서 언급한 "한 메서드에는 오직 indent 1만"을 지키도록 하기 위하여 위 board 메서드의 내부를 더욱 작게 나누어야 합니다.

Martin Fowler의 리펙토링 에서는 Extract Method에 대하여 소개하고 있는데, 이를 사용하면 해결 가능합니다!

비록 코드의 라인 수 를 줄이지는 않지만 가독성을 상당히 증가하게 됩니다! 코드는 가독성이 짱이죠!

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");
    }
}

 

2. else 예약어를 쓰지 않는다.

대부분의 프로그래밍 언어에서는 if/else를 지원합니다.

기존 코드에 새로운 else를 추가하는 것이 일단은 조건을 추가하기에 쉽기 때문에 else를 쓰고싶은 유혹이 생긴다.

public void login(String username, String password) {
    if (userRepository.isValid(username, password)) {
        redirect("homepage");
    } else {
        addFlash("error", "Bad credentials");

        redirect("login");
    }
}

위와 같은 코드의 else 문은 early return 문을 통해 다음과 같이 해결할수가 있다.

public void login(String username, String password) {
    if (userRepository.isValid(username, password)) {
        return redirect("homepage");
    }

    addFlash("error", "Bad credentials");

    return redirect("login");
}

조건은 optimistic(즉, 오류를 걸러내는 if 조건이 있으면, 나머지 로직은 if 이후의 기본 시나리오를 따르는) 하거나,

또는 defensive(즉, 기본 시나리오를 조건에 지정한 후 조건이 충족되지 않으면 오류 상태를 반환하는)하게 접근할 수 있다.

바로 위 코드는 isValid로 타당한지를 먼저 검증후, 타당하지 않으면 login 페이지로 redirect 하니, defensive라 할 수 있습니다.

 

아니면 항상 가능한것은 아니지만, 대안으로 반환 문을 매개 변수화할 수 있도록 변수를 도입할 수 있습니다. 다음과 같이 말이죠!

public void login(String username, String password) {
    String redirectRoute = "homepage";

    if (!userRepository.isValid(username, password)) {
        addFlash("error", "Bad credentials");
        redirectRoute = "login";
    }

    redirect(redirectRoute); // 최종적으로 두 로직 모두 여기까지 도달
}

유저가 아니라면 "login"으로 리다이렉션 될것이며, 유저라면 "homepage" 로 리다이렉션 되게 됩니다.

 

또한 객체 지향 프로그래밍이 다형성과 같은 강력한 기능을 제공한다는 것도 언급해야 겠죠?

마지막으로, Null Object, State 패턴, Strategy 패턴이 도움이 될 수 있습니다!

 

3.  모든 원시값과 문자열을 포장한다.

간단하게 객체 내의 모든 원시 요소를 캡슐화하기만 하면 됩니다.

안티 패턴중 하나인 Primitive Obsession을 피하기 위해서 입니다.

 

http://wiki.c2.com/?PrimitiveObsession

This site uses features not available in older browsers.

wiki.c2.com

만약 당신이 사용하는 변수에 행동(behavior)이 포함된다면, 캡슐화 하기를 적극 추천한다.

이런 것들을 DDD(도메인 주도 개발) 에서는 특별히 VO(Value Object) 라고 부른다.

예를 들어, Money, Time, NaturalNumber(자연수) 등이 가능할 것 이다.

 

4.  한 줄에 점 하나만 찍는다.

일반적으로 자바, C# 등을 사용하면서 메서드를 호출할때 .(dot) 연산을 사용하게 됩니다.

 

이 규직은 보통 "메서드 호출을 연쇄적으로 연결해서는 안 된다" 라고 합니다.

그러나 위 문장은 Fluent Interface를 만들때는 적용되지 않으며, 더 일반적으로 메소드 체인 패턴(Builder 패턴 같은 경우)을 구현하는 경우 적용되지 않습니다.

 

위의 몇가지 경우를 제외하고는 보통 이 규칙을 준수하는 것이 좋아요!

"한 줄에 점 하나만 찍는다" 규칙은 보통 Law of Demeter 로 불리는데, 인접한 친구에게만 말을 걸어라 로 유명합니다!

 

5. 줄여 쓰지 않는다.

한가지 질문을 먼저 해봅시다. 평소 명칭을 줄여쓰는 분은 왜 줄여서 사용할려 하는 것 이죠?

 

당신은 아마 "같은 이름을 반복해서 쓰기 싫어서" 라고 대답할 것 같군요?

아니면, 이 함수가 여러 번 재사용되고 있으며, 코드 복붙처럼 보인다고 대답할수도 있구요?

(아니라면 죄송합니다 ㅎㅎ)

 

결론적으로 당신은 메서드 이름이 너무 길기 때문이라고 말할 것 입니다.

아니면, 당신의 class가 SRP(단일 책임 원칙)을 지키지 않고 있어서 여러 책임을 동시에 수행하고 있기 때문 아닐까요?

뭔가 메서드 명이 길면 안될것 같고, 막 줄여야 할것 같고?, 그럼 좀 간지도 나는것 같고 그렇죠??

(아니라면 죄송합니다 ㅎㅎ)

 

저는 이렇게 말씀 드리고 싶군요

이름은 의미를 전달할수 있을정도로 충분하게 길어도 된다. 의미전달이 명확한것이 더 중요하다.

 

혹시 이름을 지어주기가 어려워서 그런것 이라면 다음 글을 읽어보길 권장해요!

https://williamdurand.fr/2012/01/24/designing-a-software-by-naming-things/

 

Designing A Software By Naming Things

There are only two hard things in Computer Science: cache invalidation and naming things. Phil Karlton

williamdurand.fr

이름은 축약하지 마시길!

 

6. 모든 Entity를 작게 유지한다.

하나의 Class는 50줄을 넘기지 않기를 권장하며, 하나의 Package에는 10개의 파일 이상 담지 않기를 권장합니다.

물론 당싱의 구현에 따라 달라지긴 하겠지만, 하나의 클래스가 100~150줄을 넘어가고 있다면, 분명 따로 분리해낼수 있을거에요

 

이러한 원칙의 숨은 뒷면에는 class가 길면 읽기가 어렵다 라는 생각도 담겨있습니다!

 

7.  3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

이번 규칙을 말하면 솔직하게 사람들이 말도 안된다고 소리지를 줄 알았는데, 그건 아니라 다행입니다(?)

이번 규칙은 지키기 매우 어렵지만, 응집도를 높히고, 캡슐화 정도를 높혀줍니다.

 

예를 들면 다음과 같이 말이죠!

출처 -&nbsp;https://github.com/TheLadders/object-calisthenics#rule-8-no-classes-with-more-than-two-instance-variables

위 그림을 보면 Customer는 name, customerid 2가지 인스턴스를 갖게 됩니다.

다시 name은 성과 이름 2가지 인스턴스를 갖게 되는군요! 

(참고로 FirstName 과 LastName이 위 3번 조항에서 말한  ValueObject입니다.)

 

아마 이런 질문이 나올것 같군요?

 

왜 꼭 2개인거죠??

 

저의 의견으로는 다음 2종류의 Class를 구분하기 위해서 입니다.

1) 단일 인스턴스 변수만을 사용해 상태를 유지하는 Class

2) 2개의 개별 변수를 조화롭게 협력시키는 Class

2개는 Class를 많이 분리하도록 강요하는 임의적인 선택입니다.

 

8. 일급 컬렉션을 쓴다.

일급 컬렉션이란, 컬렉션을 포함하는 해당 Class는 컬렉션을 제외한 다른 멤버 변수를 포함하지 말아야 합니다.

 

예를 들어 Unit이라는 하나의 class가 있고, 이를 여러게 묶어(List<Unit>) 단위로 사용하는 Units는 1급 컬렉션이 될수 있습니다.

각 컬렉션은 자체 클래스로 감싸지기 때문에, 이제 컬렉션과 관련된 행동은 1급 컬렉션을 통해 사용할수 있습니다.

 

컬렉션의 모든 API 또한 open하지 않게되는 점도 장점입니다.

만약 컬렉션이 add, delete, peek, sort 라는 API를 지원하는데, 1급 컬렉션의 public API가 add, delete뿐 이라면 외부의 사용자는 오직 이 2가지 메서드만 사용하게 됩니다.

 

9. getter/setter/프로퍼티를 쓰지 않는다.

일반적으로 묻지말고 시켜라 라고 알려진 원칙입니다.

보통 한 객체의 상태에 기반한 모든 행동은 객체가 스스로 결정하도록 해야합니다. 

객체의 외부에서 결정하는데 사용하는 것 이 아니라면, 객체의 상태를 가져오기 위해 꼭 필요하다면 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);

위 코드에서의 getScore()는 외부에서 결정하는데 사용하고 있습니다.

game.setScore(game.getScore() + ENEMY_DESTROYED_SCORE);

위 코드 부분에서 getter로 가져온 값을 이용하여 추가적으로 값을 더한 후, setter를 통해 객체의 상태값을 변경하고 있습니다.

Game class가 스스로를 책임지지 못하는 상황인 것 이죠...

 

getter/settter 가 아닌, 객체에게 메시지를 전달하도록 변경해 봅시다.

명심할점이 있는데, 사용자는 그저 시킬 뿐 입니다! 물어보지 않죠!(getter를 안쓴다는 의미)

 

따라서 위의 코드를 다음과 같이 메시지를 전달하는 방식으로 변경할 수 있습니다.

// Game
public void addScore(int delta) {
    score += delta;
}

// Usage
game.addScore(ENEMY_DESTROYED_SCORE);

위 코드를 보면 game 객체에게 addScore 라는 메시지를 전달하고 있습니다.

물어보거나 하는것은 전혀 없는것을 알 수 있죠!

game 객체는 이제 자신 스스로 전달받은 메시지에 대하여 결정을 내릴수 있게 된것 입니다!

 

UI를 사용할때 정보를 표시해주기 위해 getScore()를 사용하는것은 좋지만, 절대 setter를 사용하지 않아야 합니다.

 

참고

 

Object Calisthenics

Object Calisthenics are 9 steps to better software design today.

williamdurand.fr

 

댓글