본문 바로가기
DevelopmentTools/Java

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

by 수짱수짱 2024. 3. 9.

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

= 메서드의 깊이는 최대 1

즉, 맡은 일을 작게 가져가라.

깊이가 깊다면 그만큼 맡은 일이 많다(책임이 많다)는 의미이다. 이는 객체지향적이지 못하다는 의미와 같다.

 

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

-> 핵심은 `early return`

동시에 1번 규칙인 "깊이 제한은 1"까지 동시에 지키게 된다

 

문제코드를 한 번 보자

public class JamieObject {

    String JamieStatus(int hour, boolean isStudy) {
        String status = "";
        if (hour > 4 && hour <= 12) {
            status = "취침";
        } else {
            if (isStudy) {
                status = "공부";
            } else {
                status = "여가";
            }
        }
        return status;
    }
}

해당 코드의 문제점은 "return status"의 값이 무엇인지 다시 코드를 거슬러 올라가 읽어보아야 한다는 문제가 있다.

 

이것을 early return 규칙을 지킨 코드로 변경해 보자

public class JamieObject {

    String JamieStatus(int hour, boolean isStudy) {
        if (hour > 4 && hour <= 12) {
            return "취침";
        }
        return isStudy ? "공부" : "여가";
    }
}

return되는 status가 무엇인지 바로 한 눈에 알아보기가 쉬워졌다

 

삼항연산자가 가독성이 떨어진다면 이것또한 수정해 볼 수 있다

`isStudy`와 같이 boolean 타입은 `if/else`를 사용해도 depth가 1이므로 충분히 표현이 가능하다

삼항연산자는 때에 따라 가독성 문제가 발생할 수 있으므로 boolean 타입을 활용해보자

 

Q. 그렇다면 `switch문`을 사용해도 문제가 없을까??

A> 정답은 "ㄴㄴ 마찬가지로 문제있음"이다.

 

`early reeturn`의 목적은 "분기를 태우지 않는다"인 것이다

따라서 switch문을 사용해도 분기를 타는 것은 마찬가지이므로 해결책이 될 수는 없다

 

그렇다면 어떻게 해결할 수 있을까?

인터페이스를 활용한 전략 패턴, 팩토리 패턴 등등 여러 방법으로 switch문을 없앨 수 있다

가장 핵심은 자바의 기능인 "인터페이스"를 활용한다면 많은 역량을 올릴 수 있다는 것이 핵심이다

 

예시를 들어보자

아래 코드는 Service를 인자로 받았을 때 Service가 어떤 인자인지 분기 처리 후 메서드를 호출한다

void 함수 (Service service) {
	switch(service) {
		A -> AService.action();
		B -> BService.action();
	}
}

 

이를 해결하기 위해 "인터페이스"를 활용하는 것이다

Service가 인터페이스라면 `AService`, `BService`는 구현체이므로 분기를 나누지 않아도 각 구현체의 메서드를 호출 할 수 있다!

void 함수 (Service service) {
	service.action() // Service는 인터페이스
}

 

3. 규칙이 있는 원시 타입은 class로 매핑하자

`int`money 는 `Money`money로 래핑할 수 있다

왜냐? 현금은 "규칙"이 있기 때문이다.

그렇다면 "규칙"은 어떤 것일까?

  • 현금은 음수 일 수 없다. 무조건 0원 이상이다
  • 1원 단위의 현금은 없으므로 10원 단위부터 시작한다

=> 즉, `Money` 클래스에 현금의 특성을 넣어 원시 타입을 Wrapper하는 것이다!

public class JamieMoney {

    private final int money;

    public JamieMoney(int money) {
        validMoney(money <= 0, "현금은 0원 이상이여야 합니다.");
        validMoney(money % 10 != 0, "현금은 10원 단위 이상만 허용 합니다.");
        this.money = money;
    }

    /* 유효성 검사 */
    private void validMoney(boolean expression, String exceptionMessage) {
        if (expression) {
            throw new IllegalArgumentException(exceptionMessage);
        }
    }

    /* 행위 */
    public int getMoney() {
        return money;
    }
}

 

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

 

위 코드는 자바의 stream 예시이다. 이 것은 한 줄에 점을 하나만 찍는다는 규칙에 위배되는가 ? => 위배되지 않는다

왜냐하면, stream은 stream을 반환하고 filter는 stream을 반환하고 map도 stream을 반환한다

이 말은 즉슨 "자기 자신(stream)을 계속해서 리턴하는 것"이므로 안에 구조를 모르니 상관이 없다는 것이다

참고로 자바 stream은 '메소드 체이닝'으로 4번 규칙이 적용되진 않는다

 

그렇다면 오른쪽 코드는 문제가 되는걸까요?

넵 엄청난 문제가 됩니다

 

자신 소유의 객체, 자신이 생성한 객체, 그리고 누군가 준(파라미터로) 객체에만 메시지를 보낼 것

→ 근데 지금은? User, Name의 내부 구조까지 다 침범함

→ 캡슐화 어김 (문제) = 다른 객체에 너무 깊이 관여하게 되어 캡슐화를 어기는 것

 

이것은 디미터(Demeter)의 법칙 : "친구하고만 대화하라" 이 지켜지지 않은 예시이다.

 

`post.writer.name.name`은 Main이 Post 안의 구조, User 안의 구조를 다 알게되는 문제가 발생한다

`post.writer`는 User이므로 Name 내부의 규칙을 알아서도 안되고 몰라야만 한다

또한, `User`는 `Name` 내부가 어떻게 생긴지 모른다

 

현재 `Main`이 알 수 있는 것은 오로지 `Post`밖에 없다.

따라서 `Post`로만 응용해야 한다

val writer = post.getWriterName(); // 디미터의 법칙 (친구와만 얘기해라)

 

이렇게 되면 `Post`에선 `getWriterName()`이라는 메서드가 구현되어서 `User`의 `ggetUserName()`을 호출 할 것이다

또한, `User`에선 `getUserName()`이라는 메서드가 작동할  것이고 동시에 `Name`에선 `getName()`이라는 메서드로 return name을 적절히 넘겨줄 것이다

 

따라서, 메시지를 받는 객체는 자신의 속을 오픈하기보단 별도의 메서드를 호출하는 방식으로 작업을 해주어야 한다

 

5. 줄여쓰지 않는다

msg -> mesaage

req -> request

// X
public class Jamie {

    void printJamieName() {
        String EName = "Jamie";
        String KName = "제이미";
    }
}

// O
public class Jamie {

    void printName() {
        String englishName = "Jamie";
        String koreanName = "제이미";
    }
}

 

단 여기서 주의해야 할 점이있다.

메서드의 이름이 긴 이유 중 하나는 책임을 너무 많이 갖고 있거나, 적절한 클래스의 아래에 위치하지 않아서 일 수 있다

참고로 JPA의 repository findIdAndAge…등등의 이름은 문제되지 않는다 어쩔 수 없는 것이니까.

 

메소드이름이 `validateNameLengthAndAge` 으로 길다.

근데 잘 보면 책임을 둘로 나눌 수 있지 않는가??

 

이런식으로 "이름 길이", "나이"를 검증하는 메서드 2개로 나누어 버리면 긴 메서드 이름이 분리될 수 있다!

 

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

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

 

클래스

  • 50줄 이상인 경우 보통 클래스가 한 가지 일만 하지 않는다. (한 가지 일만 한다면 놔둬도 되는 듯...?)
  • 50줄 정도면 스크롤을 내리지 않아도 된다. => 한 눈에 들어오는 효과!

패키지

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

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

보통 `prefix`가 같은 변수들을 묶어서 class로 표현하여 인스턴스 변수의 수를 줄인다

아래 코드에서 예시를 알아보자

 

적용전

public class Jamie {
    
    private final String userName;
    private final String userJob;
    private final int userAge;

    public Jamie(String userName, String userJob, int userAge) {
        this.userName = userName;
        this.userJob = userJob;
        this.userAge = userAge;
    }
}

→ Jamie 클래스의 인스턴수 변수 갯수가 3개

 

 

적용후

→ prefix가 `user`로 같은 변수들을 모아 `UserInfo`라는 Class로 묶어 표현했다.

→ 따라서, Jamie 클래스의 인스턴스 변수 갯수가 3개에서 1개로 파격적이게 줄어든 모습을 확인할 수 있다.

 

더보기

7번 설명

새로운 인스턴스 변수를 가진 클래스는 응집도가 떨어진다. 많은 인스턴스 변수를 가진 클래스로 응집력있는 단일 작업을 설명할 수 있는 경우는 거의 없다. (추측) 여기서 말하는 인스턴스 변수는 기본형 또는 자료구조형 객체들인 것으로, 일급 컬렉션이나 wrapper객체는 해당되지 않는 것 같다.

인스턴스 변수의 분해는 여러 개의 관련 인스턴스 변수의 공통성을 이해하게 하여 자료구조형으로 묶어 일급 컬렉션으로 생성할 수 있게 해준다.

인스턴스 변수들의 집합을 갖고 있는 것에서, 협력 객체(일급 컬렉션/Wrapper 객체)의 계층 구조로 분해하면 더 효율적인 객체 모델이 될 수 있다. 복잡하고 덩치 큰 객체를 이해하는 것은 어렵지만, 분해하면 간단해진다.

분해하는 것이 어렵거나 막막하다면, 객체를 상관 관계가 있는 반(half)씩 나누거나, 인스턴스 변수를 둘 골라서 그로부터 하나의 객체를 만드는 등을 하는 것을 추천한다.

 

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

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

 참고 : 일급 컬렉션을 알아보자!

 

일급컬렉션: list를 1개만 들고 있는 것.

만약 list 구조인 필드가 2개라면? 일급 컬렉션이 아닌 것!

 

 

9. getter / setter / property를 사용하지 않는다.

더보기

9번 설명

추측) 해당 내역은 도메인 객체에만 해당되므로, DTO / Controller 등엔 해당되지 않는다.

또한 DTO 등에서 사용하기 위해 도메인 객체에 getter를 놓는 것은 상관이 없는 것 같다.

무조건 사용을 하지 않는 것이 아닌, 도메인 객체끼리의 사용을 하지 않는 것이 중요하다!

 

만약 객체가 지금 인스턴스 변수의 적당한 집합을 캡슐화하고 있지만 그 설계가 여전히 어색하다면, 좀 더 직접적인 캡슐화 위반을 조사해봐야 한다.

그냥 단순히 현재 위치에서의 값을 물을 수 있는 동작이라면 해당 인스턴스 변수를 제대로 따라가지 못할 것이다.(= 값을 물어와서 인스턴스 변수에 담아두고 그 변수를 컨트롤한다면, 그 변수를 잘못 컨트롤 할 수도 있음)

강한 캡슐화 경계의 바탕에 깔린 사상은 동작의 검색과 배치를 위해 남겨둔 코드를 만질 다른 프로그래머를 위해 객체 모델의 단일한 지점으로 유도하려는 것이다.(= 객체에 메시지를 던져서 작업을 해야지, 값을 가져와서 다른 곳에서 작업을 하지 말자)

 

이는 많은 긍정적인 하부효과를 가져다 주는데, 중복 오류의 극적 축소(=메서드(기능들)가 객체 내부 한 곳에서 관리되므로)와 새 기능의 구현을 위한 변경의 지역화 개선(= 메서드가 한 곳에서 관리되므로, 변경을 한 곳에서 하면 됨) 등이 있다.

 

적용전

public class Jamie {

    private final Name name;
    private final Money money;

    public Jamie(Name name, Money money) {
        this.name = name;
        this.money = money;
    }

    boolean canBuySomething(int somthing) {
        return somthing <= money.getMoney(); // money.getMoney()를 수정해야 한다 (값을 가져오는 행위)
    }
}

 

적용후

public class Jamie {

    private final Name name;
    private final Money money;

    public Jamie(Name name, Money money) {
        this.name = name;
        this.money = money;
    }

    boolean canBuySomething(int somthing) {
        return money.moreThanOrEqualsPrice(somthing); // money 객체에 메시지를 던져 문제를 해결
    }
}

Reference

 

[Java] 객체지향 생활 체조 원칙 9가지 (from 소트웍스 앤솔러지)

1. 한 메서드에 오직 한 단계의 들여쓰기만 한다. 한 메서드에 들여쓰기가 여러 개 존재한다면, 해당 메서드는 여러가지 일을 하고 있다고 봐도 무관하다. 메서드는 맡은 일이 적을수록(잘게 쪼

jamie95.tistory.com

작성 날짜 - 2023년 10월 11일