본문 바로가기
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. 일급 컬렉션을 사용한다.

일급 컬렉션은 컬렉션을 Class로 Wrapping 하고 해당 Class는 컬렉션을 제외 한 반드시 다른 멤버변수가 없어야 한다.

반드시 컬렉션 하나만 멤버 변수로 가진 Class여야 한다는 것이다.

 

before

Map<String, String> ranks = new HashMap<>();
ranks.put("김", "A");
ranks.put("이", "B");
ranks.put("박", "C");

 

after

public class GameRanking {
	
    private Map<String, String> ranks = new HashMap<>();
    
    public GameRanking(Map<String, String> ranks) {
    	validateMemberName(ranks);
    	this.ranks = ranks;
    }
    
    public String getFirstMember() {
    	// 로직..
    }
    
    private void validateMemberName(Map<String, String> ranks) {
    	if(!검증 로직) {
        	throw new ...
        }
    } 
}

 

  • GameRanking 이라는 일급 컬렉션의 "명확한 이름"으로 검색이 쉬워지며 소통 불일치도 줄어든다.
  • Enum 처럼 상태와 행위를 한 곳에서 처리를 하기에 관계를 명확하게 할 수 있다
    • 관계가 명확해지면 메소드 누락을 줄일 수 있다 => 위 getFirstMember()는 일급 컬렉션을 사용하는 어디서든 존재한다.
    • 위에선 1등 멤버를 조회하는 메소드를 GameRanking 일급 컬렉션에 구현해 주었기에 똑같은 메소드가 중복으로 작성될 가능성도 낮아지고 해당 컬렉션에서 1등 멤버를 조회하는 로직을 수행한다는 관계를 명확하게 나타낼 수 있다
  • 컬렉션 불변성 보장 확률 증가 (java의 final 키워드 특성상 100%는 아님)
    • java의 final은 재할당을 막을 뿐이지 컬렉션 값 수정을 막지는 않는다
    • 그럼에도 불구하고 내부 컬렉션을 private 하게 막아두고 생성자에서 할당을 시켜준다. 그럼 재할당이 불가능
    • 또한, 위 GameRanking 일급 컬렉션은 "조회" 메소드만 있지 "추가" 및 "수정" 메소드는 구현되어 있지않다
      • 이 말은 GameRanking 컬렉션 내부 값을 추가, 수정하는 방법은 존재하지 않는다는 것이다
    • 컬렉션의 불변성을 보장할수록 사이드 이펙트가 터질 확률이 줄어들고 코드 유연성도 올라간다는 아주 좋은 점이 존재한다
  • 비즈니스에 종속적이게 된다
    • 보통 서비스 레이어에서 컬렉션에 대한 검증을 거치게 되어있다
    • 하지만 해당 컬렉션을 사용하는 모든 곳에서 검증이 필요한데 신규 입사자가 이걸 제대로 숙지할 수 있을까?
    • 또한, 해당 데이터 타입의 컬렉션은 모두 이러한 검증이 필요한걸까 의문이 들 수 있다.
    • 그렇기에 일급 컬렉션 생성자에서 아예 검증 로직을 호출하도록 하고 외부에선 이런 검증 로직을 신경쓰지 않아도 되게 할 수 있다
    • 서비스 레이어에서 진행하는 컬렉션에 대한 검증을 일급 컬렉션 내부에서 수행하면서 서비스 레이어 비즈니스 로직에 종속된 일급 컬렉션을 생성하게 된다.

 

 

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

 

 

 

[Java] 일급 컬렉션을 알아보자!

Java 일급 컬렉션이란? First Class Collection 정의 Collection을 Wrapping하면서, 그 외 다른 변수가 없는 클래스의 상태를 일급 컬렉션이라 함 장점 1. 비즈니스에 종속적인 자료구조 해당 컬렉션에서 필요

jamie95.tistory.com

 

 

일급 컬렉션 (First Class Collection)의 소개와 써야할 이유

최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코

jojoldu.tistory.com

 

작성 날짜 - 2023년 10월 11일 / 수정 날짜 - 2025년 9월 22일