Home

Published

- 12 min read

[ OOP ] 객체지향 생활체조 원칙 9가지

img of [ OOP ] 객체지향 생활체조 원칙 9가지

코드를 짜던 도중, 객체지향 생활체조라는 말을 봐서 이번 기회에 이게 뭔지, 그리고 이걸 왜 하는지 정리를 해두려고 한다.

객체지향 생활체조가 뭐야

이것은 객체지향 소프트웨어 개발에서 좋은 코드를 작성하기 위한 지침과 원칙을 제공하는 개발 방법론이 아니라, 소프트웨어 개발자들 사이에서 공유되는 개발 원칙과 가이드라인이다.

“객체지향 생활체조”는 주로 객체지향 프로그래밍에서의 코드 품질과 가독성을 향상시키기 위한 규칙을 정리한 목록으로, 코드를 개선하고 개발 팀 간에 일관성 있는 코드 스타일을 촉진하기 위해 사용된다. 이러한 규칙은 Clean Code 커뮤니티와 같은 소프트웨어 개발 커뮤니티에서 공유되며, 보다 가독성 높고 유지보수 가능한 코드를 작성하기 위한 지침으로 활용된다.

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

메서드는 맡은 일이 적을수록(잘게 쪼갤수록), 재사용성이 높고 디버깅에 용이하다. depth(깊이)를 너무 깊게 두지 말자. 이해하기 힘들어 죽겠다.

나쁜 예시

   public void processOrder(Order order) {
    if (orderIsValid(order)) {
        if (order.getTotalPrice() > 100) {
            applyDiscount(order);
            saveOrder(order);
        } else {
            saveOrder(order);
        }
    }
}

좋은 예시

   public void processOrder(Order order) {
    validateOrder(order);
    calculateTotalPrice(order);
    applyDiscount(order);
    saveOrder(order);
}

이렇게 쓰지 말자!!

2. else 키워드를 쓰지 않는다.

조건문은 복제의 원인이 되기도 한다. 또한 가독성도 좋지 않다.

나쁜 예시

   public String getGrade(int score) {
    if (score >= 90) {
        return "A";
    } else {
        if (score >= 80) {
            return "B";
        } else {
            if (score >= 70) {
                return "C";
            } else {
                return "D";
            }
        }
    }
}

보기만 해도 어지럽다. 이렇게 쓰지 말자

좋은 예시

   public String getGrade(int score) {
    if (score >= 90) {
        return "A";
    }
    if (score >= 80) {
        return "B";
    }
    if (score >= 70) {
        return "C";
    }
    return "D";
}

왜?

  1. 복잡성 감소: 복잡한 ‘if-else’ 블록은 코드를 이해하기 어렵게 만들 수 있다. ‘else’ 블록은 이전 ‘if’ 조건에 해당하지 않는 모든 경우를 처리하므로 코드 경로를 따라가는 것이 어려워질 수 있다.
  2. 가독성 향상: ‘if’와 ‘else’를 사용하지 않고 명시적으로 조건을 검사하고 처리하는 방법을 사용하면 코드의 의도가 명확해질 수 있다.
  3. 추상화: ‘else’ 블록은 종종 다양한 경우를 하나의 블록 안에 포함하는 방식으로 동작한다. 이것은 코드를 추상화하고 비즈니스 로직을 이해하기 어렵게 만들 수 있다.

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

왜?

  1. 유지보수성 및 가독성 향상: 객체를 사용하면 데이터와 관련된 동작(메서드)을 한 곳에 모을 수 있다. 이로써 코드의 가독성이 향상되고 유지보수가 더 쉬워진다. 원시값의 처리 로직이 여러 곳에 분산되지 않으며, 객체 자체가 그 값의 의미와 동작을 나타내게 된다.
  2. 유연성 및 확장성: 객체를 사용하면 데이터와 관련된 동작을 더 쉽게 확장할 수 있다. 새로운 동작을 추가하거나 기존 동작을 변경하기가 간편하며, 데이터와 동작이 관련 있는 경우 적합한 객체 지향 설계를 수행할 수 있다.
  3. 타입 안정성과 오류 감소: 원시값은 타입이 없는 값이거나 강제 형변환이 필요한 경우가 있다. 그러나 객체를 사용하면 타입 안정성이 더 높아지며, 컴파일 타임에 오류를 발견하기 쉬워진다.
  4. Null 값 다루기: 객체를 사용하면 null 값을 대체할 수 있는 방법을 제공할 수 있으며, Null 포인터 예외를 방지할 수 있다.
  5. 코드 응집성 강화: 원시값은 종종 여러 곳에서 사용되고 여러 목적으로 변경될 수 있다. 객체를 사용하면 데이터와 해당 데이터에 대한 동작이 관련성이 있는 방식으로 그룹화되며, 코드의 응집성이 강화된다.

또한 안티 패턴중 하나인 Primitive Obsession을 피할 수 있다.

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

(스트림 등 체이닝하는 일부를 제외)

5. 줄여쓰지 않는다.

가독성이 향상된다. 이전에 협업을 할 기회가 있었는데, 한 개발자분이 함수 이름을 축약해서 사용하셨었다. 그런데 이름이 너무 추상화되어서 이해를 하는데 10분이 넘게 걸린 기억이 있다. 너무 긴것도 오히려 가독성을 해치지만, 제발 축약해서 쓰지 말자.

   public void updUsrPrf(String un, String em, String pn, String ha) {
    // 코드 내용 생략
}

와, 보기만 해도 어지럽다. 제발.. 코드 내용이 간단해도 이해하는데 시간이 너무 오래 걸린다.

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

50줄 이상 되는 클래스 또는 10개 파일 이상의 패키지는 없어야 한다.

클래스

  • 50줄 이상인 경우 보통 클래스가 한 가지 일만 하지 않는다.
  • 50줄 정도면 스크롤을 내리지 않아도 된다.

패키지

  • 하나의 목적을 달생하기 위한 연관된 클래스들의 모임
  • 작게 유지하면 패키지가 진정한 정체성을 가지게 된다.

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

새로운 인스턴스 변수를 가진 클래스는 응집도가 떨어진다. 많은 인스턴스 변수를 가진 클래스로 응집력있는 단일 작업을 설명할 수 있는 경우는 거의 없다.

그리고 경험상 변수가 3개 이상이면 한번에 이해하기 너무 어려워진다.

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

컬렉션을 포함한 클래스는 반드시 다른 멤버변수가 없어야 한다.

  • 일급 컬렉션: Collection을 Wrapping하면서, 그 외 다른 변수가 없는 클래스의 상태를 일급 컬렉션이라 함

일급 컬렉션을 사용하지 않은 경우

   public class BookList {
    private List<String> titles;
    private List<String> authors;
    private List<Double> prices;

    public BookList() {
        titles = new ArrayList<>();
        authors = new ArrayList<>();
        prices = new ArrayList<>();
    }

    public void addBook(String title, String author, double price) {
        titles.add(title);
        authors.add(author);
        prices.add(price);
    }

    public String getBookTitle(int index) {
        return titles.get(index);
    }

    public String getBookAuthor(int index) {
        return authors.get(index);
    }

    public double getBookPrice(int index) {
        return prices.get(index);
    }
}

일급 컬렉션을 사용한 경우

   public class Book {
    private String title;
    private String author;
    private double price;

    public Book(String title, String author, double price) {
        this.title = title;
        this.author = author;
        this.price = price;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public double getPrice() {
        return price;
    }
}

public class BookList {
    private List<Book> books;

    public BookList() {
        books = new ArrayList<>();
    }

    public void addBook(Book book) {
        books.add(book);
    }

    public Book getBook(int index) {
        return books.get(index);
    }
}

9. getter / setter /property를 쓰지 않는다.

이부분은 Object, DDD 등에서 모두 강하게 강조하는 부분이다. (Tell, don’t ask) 원칙에 따르면 묻지 말고 객체에게 행위를 시켜라고 한다.

왜? why?

  • 캡슐화 위반: Getter와 setter는 주로 객체의 내부 상태에 대한 직접적인 접근을 허용하므로 캡슐화 원칙을 위반할 수 있다. 객체지향 프로그래밍에서는 객체의 내부 상태를 직접 노출하는 것을 피하고, 객체 내부 상태에 대한 접근을 객체 자체에게 제한하는 것이 중요하다.
  • 데이터 무결성 문제: Setter 메서드가 있을 때, 속성 값에 대한 검증 및 제한을 적용할 수 있지만, 이러한 검증 로직은 반드시 정확하게 구현되어야 한다. 또한, 속성에 대한 유효성 검사 로직이 여러 번 중복될 수 있다. 이로 인해 버그가 발생하거나 유지보수가 어려워질 수 있다.
  • 의존성 증가: 다른 클래스가 객체의 getter와 setter를 사용하면 그 객체에 대한 의존성이 높아질 수 있다. 이는 객체 간의 결합도를 증가시키고 유연성을 감소시킬 수 있다.
  • 불필요한 노출: 모든 속성을 외부에 노출할 필요는 없다. 일부 속성은 객체의 내부 구현 세부사항이며, 외부에서 직접 접근할 필요가 없는 경우가 있다.

끝!