본문 바로가기
Language/Java

[Java] 객체지향 생활체조 (Object Calishenics)

by 1000zoo 2023. 10. 25.

Object Calisthenics

  • Jeff Bay 라는 사람이 ThoughtWorks Anthology 라는 공학 에세이 모음집에서 등장한 개념이라 한다.
  • 객체지향 프로그래밍을 잘하기 위한 9가지 체크리스트라 보면 될듯 하다.

1. One level of indentation per method

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

Why?

  • 메소드에 들여쓰기가 많아지면 가독성이 떨어진다.
  • 객체지향의 사상 중, 하나의 메소드는 한 가지 기능만 담당한다를 위반할 수 있다.

example

String board() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 10; j++) {
            sb.append(data[i][j]);
        }
        sb.append("\n");
    }
    return sb.toString();
}

위 코드는, 기본적인 이중포문 구조이다.
위와 같은 메소드 구조를, 아래와 같이 3개의 메소드로 나눈다.

String board() {
    StringBuilder sb = new StringBuilder();
    collectRows(sb);
    return sb.toString();
}

void collectRows(StringBuilder sb) {
    for (int i = 0; i < 10; i++) {
        collectRow(sb, i);
    }
}

void collectRow(StringBuilder sb, int row) {
    for (int i = 0; i < 10; i++) {
        sb.append(data[row][i]);
    }
    sb.append("\n");
}

기초적인 예시라 별 차이가 없어보이지만, 기능들이 잘 나뉜 것을 볼 수 있다.


2. Don't use the ELSE keyword

else 예약어를 쓰지 않는다.

Why?

  • 가독성이 떨어진다.
  • 중첩된 조건문이 발생할 수 있다.
  • 오류처리가 비효율적이다.
  • 코드의 명확성이 떨어진다.
  • 다형성과 같은 객체지향프로그래밍의 중요 특성을 무시하게 될 수 있다.

example

Early Return

  • 조건에 대해 미리 리턴시켜버리는 방법이다.
  • 코딩문제 풀 때 엣지케이스 처리에대해 자주 사용했던 방법

String getName(Person person) {
    if (person == null) {
        return "Unknown";
    }
    return person.name;
}
  • 매우 간단한 예시긴 하지만, 위와 같은 방법으로 미리 리턴시켜버리면 else예약어를 피할 수 있다.

Map 사용

  • 이것도 뭔가했는데, 당연한 것 일 수 있다.
  • 예시를 바로 보자

public String something(String action) {
    if (action.equals("0")) {
        return "USA"
    } else if (action.equals("1")) {
        return "KR"
    }
    ...
    else {
        return "CN"
    }
}

엄청 길 텐데, 이를 맵을 사용한다면 더 효과적일 것이다.

/**
    Map<String, String> map = new HashMap<>();
    map.put("0", "USA");
    map.put("1", "KR");
    ...
*/
public String something(String action) {
    if (!map.containsKey(action)) {
        throw new Exception();
    }
    return map.get(action);
}

다형성 이용

  • if/else/switch의 좋은 대체제이다.
  • 예를들어, 전략패턴이나 상속을 사용할 수 있다.

public class ProcessOrder {
    public int getOrderGrandTotal(Customer customer, int subTotal) {
        if (customer.type.equals("EMPLOYEE")) {
            return subTotal - 20;
        }
        else if (customer.type.equals("NON_EMPLOYEE")) {
            return subTotal - 10;
        }
        return subsTotal;
    }
}
  • 위 코드는 다형성을 전혀 활용하고 있지 않다.
  • 아래와 같이 상속을 이용한 다형성을 활용한다면, 코드가 간결해지고 가독성도 좋아진다.

public abstract class Customer {
    public abstract int getDiscount();
}

public class Employee extends Customer {
    @Override
    public int getDiscount() {
        return 20;
    }
}

public class NonEmployee extends Customer {
    @Override
    public int getDiscount() {
        return 10;
    }
}

public class ProcessOrder {
    public int getOrderGrandTotal(Customer customer, int amount) {
        return amount - customer.getDiscount();
    }
}

3. Wrap all primitives and Strings

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

Why?

  • Domain Driven Design (DDD)를 지키기 위해
  • 특히 자바에서 원시값은 실제 객체가 아니기 때문에 다른 객체들과 다른 규칙을 갖게된다.
  • 객체지향적이지 않은 syntax를 사용한다.
    • 특히 int의 경우, 그저 스칼라 값이기에 아무런 의미가 없다.
    • 변수명으로 모든 의미를 부여해야한다.

example

  • 의미를 부여하는 클래스를 만든다.
  • 포장을 해야하는 근본적인 의미는, int 변수명만 봐서는 어떤 역할을 하는 지 판단하기 어렵기 때문이다.
  • 속성을 하나만 가지더라도, 작은 객체들을 만들자

public class SMSSubscription {
    private String id;
    private int quantity;
    private int month;
    private int year;

    public SMSSubscription(String id, int quantity, int month, int year) {  
        this.id = id;  

        if (quantity < 1 || quantity > 250) {  
            throw new IllegalArgumentException();  
        }  
        this.quantity = quantity;  

        if (month < 1 || month > 12) {  
            throw new IllegalArgumentException();  
        }  
        this.month = month;  

        if (year < 1970) {  
            throw new IllegalArgumentException();  
        }  
        this.year = year;  
    }
}
  • 위 예시의 경우, SMSSubscription 생성자 내부가 예외로 가득차버린다.
  • quantity, month, year에 대해 Wrapper를 적용하게 되면, 아래와 같이 간결하게 된다.

public class Quantity {
    private int quantity;

    public Quantity(int quantity) {
        if (quantity < 1 || quantity > 250) {
            throw new IllegalArgumentationException();
        }
        this.quantity = quantity;
    }
}

public class Month {
    private int month;

    public Month(int month) {
        if (month < 1 || month > 12) {
            throw new IllegalArgumentationException();
        }
        this.month = month;
    }
}

public class Year {
    private int year;

    public Year(int year) {
        if (year < 1970) {
            throw new IllegalArgumentationException();
        }
        this.year = year;
    }
}

public class SMSSubscription {
    ...

    public SMSSubscription(String id, Quantity q, Month m, Year y) {
        this.id = id;
        this.quantity = q;
        this.month = m;
        this.year = y;
    }
}
  • 원시타입을 클래스화 하면서, 변수에 의미가 생기고 의미에 따라서 예외처리를 직접 해줄 수 있게 되었다.

4. First class Collections

일급 콜렉션을 쓴다.
  • Collection을 속성으로 가지고 있는 클래스는 다른 속성을 가지면 안된다.Why?
  • Collection의 불변성을 보장하기 위해
  • 객체의 의미를 부여하기 위해?
  • 비즈니스에 종속적인 자료구조로 만들기 위해
  • 상태와 행위를 한 곳에서 관리하기 위해

example

Map<String, String> map = new HashMap<>();
map.put("1", "A");
...

public class Ranking {

    private Map<String, String> ranks;

    public Ranking(Map<String, String> ranks) {
        this.ranks = ranks;
    }

}

5. One dot per line

한 줄에 점 하나만 찍는다.

Why?

  • 점이 여러개다 보면, 행동에 책임을 갖는 객체를 찾기 힘들어진다.
  • 점이 많다는 의미는, 객체가 다른 객체에 더 깊게 의존하게 된다는 의미가 된다.
  • 캡슐화를 위반하게 된다.
  • Null Pointer Exception가 발생할 우려가 있다.

example

  • Demeter의 법칙을 따른다.

class Driver {
    private final Car car;

    void goLeft() {
        car.getHandle().left();
    }
}

class Car {
    private final Handle handle;

    Handle getHandle() {
        return handle;
    }
}

class Handle {
    void left() {}
}

class Driver {
    private final Car car;

    void goLeft() {
        car.goLeft();
    }
}

class Car {
    private final Handle handle;

    void goLeft() {
        handle.left();
    }
}

class Handle {
    void left() {}
}

괜찮은 경우

a.foo(b.foo());

list.stream() 
    .filter(item -> item != null) 
    .map(item -> item.toString()) 
    .collect(Collectors.toList());

6. Don't abbreviate

줄여쓰지 않는다.

Why?

변수명이 길어진다는 것은, 단일책임원칙([[SRP]])을 위반하고 있을 가능성이 있다.

example

  • 클래스를 세분화한다.
  • 클래스나 메소드 이름을 1-2 단어로 만드려 노력해라.
  • 메소드명이 길어진다면, 책임이 잘못된 곳에 있을 가능성이 있다.

7. Keep all entities small

모든 엔티티를 작게 유지한다.
  • 클래스의 길이는 50~ 150, 패키지의 파일 수는 10개로 제한해라.Why?
  • 클래스 길이가 길어지면 가독성을 해치고, 유지보수하기 어려워진다.

8. No classes with more than two instance variables

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

Why?

  • 2개 이하로 속성을 줄이면서, 높은 결합력과 나은 캡슐화가 된다.

example

  • 위 그림과 같이 묶어서 구현한다.

9. No getters/setters/properties

게터,세터,프로퍼티를 쓰지 않는다.
  • DTO 의 경우 허용

Why?

  • 기본적으로 캡슐화를 위반한다.
    • 구현 세부 정보를 노출하게 된다.
  • 객체의 상태를 변할 우려가 생기게 된다.
    • getter의 경우에도 불러오는 객체가 컬렉션과 같은 참조값이라면, 상태가 변할 수 있다.
  • 리팩토링에 어려움이 생긴다.
  • 사이드 이팩트가 발생할 수 있다.

example

Tell, Don't Ask

  • 객체에게 묻지 말고, 무엇을 할 지 명령해라

public class MusicPlayer {
    private List<Music> playList;

    public List<Music> getPlayList() {
        return playList;
    }
}

public class MusicPlayer {
    private List<Music> playList;

    public void addMusic(Music music) {
        playList.add(music);
    }
}

Reference

https://developerhandbook.stakater.com/content/architecture/object-calisthenics.html

'Language > Java' 카테고리의 다른 글

[Java] Enum 활용하기  (0) 2023.10.24
[Java] Functional Interface  (0) 2023.10.24
[Java] Google Java Style Guide 번역  (0) 2023.10.17
[Java] JDK Java version 변경하기 (M1)  (0) 2023.09.25
[Java] 정규식 정리 (Regular Expression)  (0) 2023.09.21