본문 바로가기

Tech/Java

객체 지향이 생소한 당신을 위해 (feat. 객체지향 생활 체조)

이런 분들을 위해 작성했습니다.

이 글에서는 객체 지향이 무엇인지에 대해 다루지 않습니다! 구글링하면 더 좋은 글들이 많답니다. 대신 이 글은 객체 지향이 뭔지 도대체 감은 안오지만 관심 정도는 있어 기웃거리시는 분들을 위해 작성했습니다. 클린 코드리팩토링에 대해 관심있는 분들에게도 좋을 것 같습니다!

 

객체 지향 생활 체조?

혹시 복싱을 배워보신 적이 있나요? 가보신 분들은 아시겠지만 복싱을 처음 배우러 가면 첫 한 달동안은 거의 줄넘기만 시킵니다. 저도 상대방의 허점을 노리는 스트레이트, 그걸 미리 읽어 카운터를 치는 화려한 모습에 반해 시작했지만 지루한 줄넘기에 무료함을 느끼고 금방 그만둔 경험이 있습니다.

 

사실 복싱에서 줄넘기는 매우 중요한 운동이라고 합니다. 복서에게 요구되는 박자감과 신체의 협응력을 키우고, 기초 체력을 단련시키기에 가장 적합한 운동이기 때문이죠!

 

복싱을 잘 하기위해 줄넘기를 하듯이, 마찬가지로 객체 지향적인 코드를 잘 작성하기에 앞서 훈련이 될만한 코딩 가이드가 있는데 그게 바로 <객체 지향 생활 체조 원칙>라는 것입니다!

 

이름부터 심상치 않은 이 원칙은 마틴 파울러의 책 <The ThoughtWorks Anthology(소트웍스 앤솔러지)>에 수록된 9가지 원칙을 의미합니다.

 

 

객체 지향 생활 체조 원칙은 좋은 품질의 소프트웨어를 만들기 위한

  • 응집도(conhension)
  • 느슨한 결합(loose coupling)
  • 무중복(zero duplication)
  • 캡슐화(encapsulation)
  • 테스트 가능성(testability)
  • 가독성(readability)

등을 실현하기 위한 훈련 지침입니다.

 

 

어려운 용어가 쏟아져 나와 당장이라도 창을 닫고 싶으시겠지만 진정하시고..! 복싱의 줄넘기를 다시 생각해봅시다.

 

줄넘기가 아무리 복싱에 좋은 운동이라고는 하지만 뉴비들은 복싱에 어디가 좋고, 리듬감이 어떻고… 이런 것들을 고려하지 않습니다. 그냥 줄넘기를 하다보니 리듬감이 생기고, 체력이 붙는 것처럼 이 객체 지향 생활 체조 원칙도 눈 앞에 원칙을 준수하는 훈련만 한다면 객체 지향을 이해하는데 자연스럽게 준비가 될 겁니다!

 

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

객체 지향 생활 체조의 9가지 원칙은 다음과 같습니다.

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

하나씩 살펴보도록 하죠!

 

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

코드에 너무 많은 들여쓰기 깊이가 깊어질수록 가독성이 하락합니다. 또한 한 메서드에 들여쓰기가 여러 개 존재한다면, 해당 메서드는 여러가지 일을 하고 있을 확률이 높습니다!

 

이는 클린 코드와 객체 지향 개발 5대 원칙(SOLID)에서도 중요하게 여기는 부분입니다. 한 메서드는 한 가지의 역할만 수행해야하며, 더 나아가 한 클래스는 하나의 역할만을 가져야 합니다.

 

아래는 들여쓰기 깊이가 2단계인 예시 코드입니다.

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

들여쓰기 1단계를 넘어가지 않도록 코드를 수정한 결과 코드가 더 읽기 쉬워졌다는 점과 재사용하기 쉬워졌다는 장점이 생겼습니다. 여기에 분리한 메서드들의 네이밍을 기능에 맞게 작성한다면 더 가독성있는 코드가 됩니다.

 

 

원칙 2. else 표현을 사용하지 않는다.

대부분의 프로그래밍 언어에는 if/else 구문을 지원합니다. 그렇기에 그동안 자연스럽게 else 구문을 작성했는데 이제와서 쓰지말라니 이상하지 않나요?

 

먼저 예시 코드를 살펴보겠습니다.

public String getAgeCategory(int age) {
    if (age < 5) {
        return "infant";
    } else {
        if (age < 12) {
            return "child";
        } else {
            if (age < 19) {
                return "teenager";
            } else {
                return "adult";
            }
        }
    }
}

위 메서드는 연령을 입력 받아 연령에 대한 분류를 반환하는 코드입니다. 해당 코드는 if문이 과도하게 중첩되면서 코드가 비선형적으로 흐르기 때문에 사람이 읽기에 직관적이지 않은 코드입니다.

 

이러한 코드는 향후 또다른 분류가 생기는 등 조건이 추가될 때에도 많은 수정 지점이 발생하게 됩니다.

 

해당 코드에서 else 구문을 사용하지 않도록 코드를 다시 작성하면 다음과 같습니다. (이런 방식을 early return 문이라고 하는 것 같네요)

public String getAgeCategory(int age) {
    if (age < 5) {
        return "infant";
    }

    if (age < 12) {
        return "child";
    }

    if (age < 19) {
        return "teenager";
    }

    return "adult";
}

가독성 측면에서도 훨씬 좋아졌고, 추가적인 분기 조건이 추가되어야할 때도 수정이 용이해졌음을 알 수 있습니다.

 

 

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

이 원칙은 모든 원시값(primitive)을 객체로 포장하라는 원칙입니다. 여기서 말하는 원시값은 원시형 데이터로 int, float, double 등 타입만 알 뿐 아무런 의미를 담지 않는 데이터를 의미합니다. 이러한 원시형 데이터는 변수명으로서만 그 의미를 추론해야 합니다.

 

예시 코드로 살펴보겠습니다.

public class Person {

    private final int id;
    private final int age;
    private final int money;

    public Person(final int id, final int age, final int money) {
        this.id = id;
        this.age = age;
        this.money = money;
    }
}

Person 객체는 id, age, money 필드를 가지며 모두 int 타입을 가집니다. 만약 개발자가 아래와 같은 실수를 저지르게 된다면 어떻게 될까요?

int age = 15;
int money = 5000;
int id = 150;

Person person = new Person(age, money, id);

id, age, money 값이 바뀌어서 들어갔음에도 컴파일 에러가 발생하지 않는다는 문제가 있습니다. 아래의 코드와 같이 각각의 원시값을 객체로 포장한다면 이러한 문제를 컴파일 시점에서 발견할 수 있습니다.

public class Person {

    private final Id id;
    private final Age age;
    private final Money money;

    public Person(final Id id, final Age age, final Money money) {
        this.id = id;
        this.age = age;
        this.money = money;
    }
}

원시값을 클래스로 감싼다면 그 데이터가 무엇인지, 그리고 왜 사용해야 하는지를 명확하게 전달할 수 있게 됩니다.

 

 

원칙 4. 한 줄에 점을 하나만 사용한다.

여기서 점이란 객체 멤버에 접근하기 위한 점(.)을 의미합니다. 한 줄에 점이 여러 개 찍힌 코드는 다음과 같은 사례가 있을 것입니다.

if (person.getMoney().getValue() > 10000) {
    System.out.println("당신은 부자 💰");
}

위 코드처럼 점이 여러 개 찍혔다는 것은 결합도가 높아졌음을 의미합니다. 해당 코드를 사용하는 클래스는 Person 뿐만 아니라 Money에 대한 의존성을 추가로 갖게 되기 때문입니다. (다른 코드에 의존적인 코드는 향후 코드 복잡성 문제를 야기함)

if (person.hasMoneyMoreThan(10000)) {
    System.out.println("당신은 부자 💰");
}

위와 같이 점을 하나만 사용하도록 코드를 개선한다면 Person의 Money 메소드를 호출하는 것이 아닌 Person 내부적으로 Money를 가져오도록함으로써 의존을 끊어낼 수가 있습니다.

 

이 원칙은 디미터의 법칙과 연관성이 있는데 궁금하시다면 함께 참고해서 읽어보면 좋을 것 같습니다.

 

 

원칙 5. 이름을 줄여 쓰지 않는다(축약 금지)

변수명, 메서드명의 과도한 축약은 코드 가독성을 저해하는 요소입니다. 이 원칙에서 생각해봐야할 부분은 “애초에 왜 우리는 축약을 하려고 하나?”라는 것입니다.

 

메서드나 변수 이름을 계속 입력해야하기 때문이라고 이유를 들 수 있지만, IDE들에 자동 완성 기능이 너무 잘 되어 있기 때문에 변수명 길이가 코드 작성하는데 문제가 되지 않습니다.

 

또는 메서드의 이름이 너무 길어지기 때문이라는 이유도 있을 것입니다. 하지만 메서드 이름이 너무 길어지는 것이 고민이라는 것은 해당 메서드가 많은 책임을 가지고 있어 분리해야할 필요성이 있다는 신호일지도 모릅니다!

 

이렇게 객체 지향 생활 체조를 준수한다는 것은 좋지 못한 코드의 냄새를 맡을 수 있는 신호로써 활용될 수 있다는 점에서도 이점이 있습니다.

 

 

원칙 6. 모든 엔티티를 작게 유지한다.

50줄이 넘는 클래스와, 파일이 10개 이상인 패키지를 지양하자는 원칙입니다. 보통 50줄이 넘는 클래스는 한가지 일을 하고 있지 않으며, 코드의 이해와 재사용을 어렵게 만드는 요소입니다. 또한 50줄이 넘지 않은 클래스는 한눈에 보기 쉽다는 부가적인 효과도 존재합니다.

 

 

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

인스턴스 변수가 많아진다는 것은 클래스의 응집도가 낮음을 의미합니다. 마틴 파울러는 대부분의 클래스가 인스턴스 변수 하나만으로 일을 하는 것이 마땅하다고 합니다.

 

그런데 왜 굳이 2개까지 허용하는 것일까요? 클래스는 하나의 상태(인스턴스 변수)를 유지 및 관리하는 것과 두 개의 독립된 변수를 조율하는 두 가지 종류로 나뉜다고 보기 때문에 후자의 경우를 고려하여 두 개의 변수가 필요한 때가 있음을 인정한다고 합니다.

 

이 원칙은 클래스의 인스턴스 변수를 최대 2개로 제한하는 것은 굉장히 엄격하지만, 최대한 클래스를 많이 분리하게 강제함으로써 높은 응집도를 유지할 수 있게 하는 원칙입니다.

 

 

원칙 8. 일급 컬렉션을 사용한다.

컬렉션(Collection)을 가진 클래스는 컬렉션 외에는 다른 멤버 변수를 가지면 안된다는 원칙입니다. 여기서 말하는 컬렉션이란 Set, List, Map 등 데이터 집합을 의미합니다. 원칙3과 비슷하게 컬렉션을 클래스로 포장하는 것을 권장합니다. 이를 통해 컬렉션과 관련된 코드 중복을 막을 수 있고, 강제로 데이터를 캡슐화한다는 점에서 더 객체지향적인 코드를 작성할 수 있습니다.

public class RacingGame {
  private final List<Car> cars;
  //...

  public RacingGame(String carInputs) {
    cars = initCars(carInputs.trim());
  }
  // ...
}

위의 코드를 해당 원칙을 준수해서 수정한다면 다음과 같습니다.

public class RacingGame {
  private final Cars cars;
  // ...
    public RacingGame(String carInputs) {
      cars = initCars(carInputs);
    }
  // ...
}

Public class Cars {
  private final List<Car> cars;

  public Cars(List<Car> cars) {
    this.cars = cars;
  }
  // ...
}

이렇게 함으로써 데이터를 감추는 것과 동시에 중요한 데이터를 전담으로 관리하는 클래스를 따로 만들어 인터페이스를 단순화시킬 수 있습니다.

 

 

원칙 9. getter/setter를 구현하지 않는다.

이 원칙은 “객체에게 묻지말고 직접 말하게 해”라는 의미의 Tell, don’t ask 규칙으로 설명이 가능합니다. 객체의 상태를 가져오는 접근자를 사용하는 것은 괜찮지만, 객체 바깥에서 그 결과값을 사용해 객체에 대한 결정을 내리는 것은 지양하는 원칙입니다.

// 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의 스코어를 업데이트하기 위해 game의 getter와 setter를 이용하고 있습니다. 이는 객체의 상태에 대한 결정을 객체 바깥에서 내리게 하는 사례입니다.

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

// Usage
game.addScore(ENEMY_DESTROYED_SCORE);

다음과 같이 addScore()라는 분명한 목적을 가진 메서드를 만들어서 객체에게 “묻는 것”이 아닌 “말하는 방식”을 취하는 것이 좋습니다.

 

 

코딩 테스트에서도 이 원칙을 준수해야할까?

코딩 테스트에서 이 원칙을 고집할 필요는 없다고 생각합니다. 아무래도 제한된 시간 안에 빠르게 답을 맞추는 것이 코딩 테스트의 목적이기 때문에 그 목적을 먼저 달성하는 게 우선이라고 생각합니다. 하지만 과제 전형이라면 말이 다르겠죠? 과제 전형에서는 코드 작성를 작성할 때 충분히 고민했음을 녹여낼수록 좋으니까요!

 

 

마무리

지금까지 객체 지향 생활 체조에 대해서 알아보았습니다. 객체 지향 생활 체조가 소개된 책 <소트웍스 앤솔러지>에는 이런 말이 나온다고 합니다.

좋은 객체지향 설계는 배우기 어려울 수 있다. 그러나 간결함에 있어 무한한 결실을 맺을 수도 있다. 절차적 개발에서 객체지향 설계로의 이전은 겉보기보다 더 어려운 사고의 중대한 전환이 필요하다.

 

지금껏 객체 지향에 대해 잘 몰랐어도 무난하게 코드를 작성해왔고, 개발도 해왔기 때문에 이 방법이 조금은 불편하고 과하다고 느껴질 수 있다고 생각합니다. 하지만 간단하게 반복 가능하면서도 건강을 유지하는 생활 체조라는 말 그대로 객체 지향 생활 체조 원칙은 따라하다보면 자연스럽게 객체 지향과 친해질 수 있다고 생각합니다.

 

단, 원칙을 적용하는 것도 중요하지만 각각의 원칙이 어떤 문제 상황을 피하기 위해 만들어졌는지 그 숨은 의도를 알아내는 것이 더 중요합니다. 원칙 자체에 목숨 걸지 말고, 원칙이 어떤 문제를 해결하고자 하는지에 집중하는 것이 객체 지향적인 코드를 작성하는데 좋은 훈련이 될 것입니다.

 

[참고]

제가 작성한 글보다 훨씬 좋은 글들입니다! 꼭 참고하시면 좋을 듯 합니다.

반응형