이끌든지 따르든지 비키든지

Software Development/Java

[Java] 객체지향 생활 체조 원칙 - 1

SeongHo5 2023. 10. 18. 01:10

SOLID 원칙, 추상화, 다형성···, 객체 지향 프로그래밍에 대해 공부할 때 정말 지겹도록 보게 되는 OOP의 특징과 원칙들이 있습니다. 코드를 객체 지향적으로 작성하려면 이러한 특징들을 지켜야 하지만, 초보 개발자가 저 원칙들만 보고 코드에 적용하기엔, 개념이 추상적이기에 쉬운 일이 아닙니다..

 

객체지향 생활 체조 원칙은 이에 대한 조금 더 구체적인 가이드를 제공한다고 보면 될 듯합니다.

각 원칙과 설명을 읽어보면서 이 원칙이 추구하고자 하는 목표를 생각해 보고  내 코드에 적용한다면 큰 도움이 될 것 같습니다.

 


객체지향 생활 체조 원칙

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

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

 

들여 쓰기(indent)는 주로 코드 블록에 다른 코드 블록을 중첩할 때, 중첩되어 있음을 표시해 가독성을 높일 때 주로 사용하는 표기법을 말합니다.

 

왜? 한 단계의 들여 쓰기만을 해야 할까?

들여 쓰기는 보통조건문(if · while)이나 반복문을 작성할 때 사용하게 되는데, 조건(분기)이 많아지고, 조건문과 반복문이 중첩될수록 들여 쓰기 레벨이 증가하고, 이 메서드가 무슨 일을 하는지 파악하기 어려워집니다. (가독성 ▼)

 

들여 쓰기 레벨이 높아질수록, 그 메서드는 하는 일(책임)이 많아질 확률이 높아지고, 코드의 복잡성이 증가하고 유지보수가 어려워질 수 있습니다. 또한, 단일 책임 원칙(Single Responsibility Principle)에 위배될 가능성도 높아집니다.


public class LoginService {
    public boolean loginUser(String username, String password) {
        if (!username.isEmpty() && password != null) {
            if (user != null) {
                if (user.getPassword().equals(password)) {
                    if (user.isActive()) {
                        // 로그인 성공
                        return true;
                    } else {
                        // 계정 비활성화된 상태
                    }
                } else {
                    // 잘못된 비밀번호
                }
            } else {
                // 사용자를 찾을 수 없음
            }
        } else {
            // 잘못된 입력
        }
        // 로그인 실패
        return false;
    }
    
}

 

 

로그인 요청을 처리하는 이 로직은 if문 중첩으로 가독성이 매우 떨어지고 확실히 좋은 코드는 아닌 것 같습니다.

 

이 로직에서 입력을 검증하는 부분을 메서드로 추출해서 따로 작성하는 방식으로 리팩터링 할 수 있습니다.

 

public class LoginService {

    public void loginUser(String username, String password) {
        isUserValidate();
        // 로그인 처리 로직
}

// 검증 클래스
public static class LoginValidation {

	public static void isUserValidate(String username, String password) {
    	isInputInRange(username, password);
        isUserActive(username);
        isPasswordCorrect(username, password);
    }
    
    private static void isInputInRange(String username, String password) {
    	// 입력값이 잘못되었는지 확인
        throw new InvalidUserInputException();
    }
    
    private static void isUserActive(String username) {
    	// 사용자 상태가 "활성화"인지 확인
        throw new DeactivatedUserException();
    }
    
    // 다른 검증 메서드
}

2. else 예약어를 쓰지 않는다.

처음 이 규칙을 보고, 저도 "else를 안 쓰면 뭘 쓰라고?"라고 놀랐지만, 위의 로그인 로직 예시에서도 보았듯이 indent와 밀접하게 연관된 규칙입니다. 

public String calculateGrade(int score) {
        if (score < 0 || score > 100) {
            return "Invalid Score";
        } else {
            if (score >= 90) {
                return "A+";
            } else {
                if (score >= 80) {
                    return "A";
                } else {
                    if (score >= 70) {
                        return "B+";
                    } else {
                        if (score >= 60) {
                            return "B";
                        } else {
                            return "C";
                        }
                    }
                }
            }
        }
    }

 

성적을 등급으로 환산해 반환하는 간단한 로직의 메서드이지만, else가 반복되어 코드 중간이 움푹 파인듯한 모습입니다.

 

이런 화살 모양 패턴의 코드를 Arrow Anti Pattern라 하는데, 들여 쓰기가 반복되어 코드를 읽기도 매우 힘들고, 중첩된 조건문 사이에 긴 로직을 추가한다면 나중에는 이 else문이 어떤 조건문의 else였는지조차 파악하기 힘들 수도 있습니다.

 

Arrow Anti Pattern의 코드, 장풍으로 코드를 밀어내는 듯 하다.

 

else 대신 early-return 방식을 사용하는 것이 권장되는데, 이 방식으로 위 코드를 수정하면 이렇게 리팩터링 할 수 있다.

 

public String calculateGrade(int score) {
        if (score < 0 || score > 100) {
            return "Invalid Score";
        }

        if (score >= 90) {
            return "A+";
        }

        if (score >= 80) {
            return "A";
        }

        if (score >= 70) {
            return "B+";
        }

        if (score >= 60) {
            return "B";
        }

        return "C";
    }

 

또는 디자인 패턴 중 하나인 전략 패턴(Stradegy Pattern)을 사용할 수도 있습니다.

 


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

원시 타입에 대한 집착(Primitive Obsession)을 없애기 위해 원시 요소를 객체로 포장(캡슐)하라는 규칙입니다.

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}

 

 

이렇게 작성된 코드는 메서드를 만들 때도 각각의 데이터를 파라미터로 넘겨주어야 하기에 파라미터의 개수가 늘어나게 되고, 관련된 데이터를 묶지 못하고 흩어놓게 되어, 정보 은닉이 어려울 수 있습니다.

 

public class PersonName {
    private final String firstName;
    private final String lastName;

    public PersonName(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    @Override
    public String toString() {
        return firstName + " " + lastName;
    }
    
    // Getter / Setter 생략
    
}

public class Person {
    private final PersonName name;
    private final int age;

    public Person(PersonName name, int age, Gender gender) {
        this.name = name;
        this.age = age;
    }

    public PersonName getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

}

 

이렇게 원시 요소를 객체로 포장하면 Primitive Obsession을 피하고, 데이터를 더 의미 있게 표현할 수 있습니다.

 


4. 일급 컬렉션을 쓴다.

"3. 모든 원시 값과 문자열을 포장한다." 원칙의 이유와 비슷하게, 컬렉션 또한 포장하지 않으면 의미 없는 객체의 모음에 불과하기 때문입니다. 

 

클래스에 다른 멤버 변수 없이 컬렉션만이 존재할 때, 이 컬렉션을 일급 컬렉션이라 하는데, 일급 컬렉션을 만들고 여기에 다른 객체가 가지던 책임을 위임하면 중복 코드를 줄이고, 응집도를 높일 수 있습니다.

 


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

이 원칙에서 점(.)은 단순한 점이 아니라, 객체 멤버에 접근하는 점(Getter 등)을 의미합니.

 

if (object.getValue1().getValue2().getValue3() != var1) {
	// Codes
}

 

위와 같이 여러 개의 Getter를 필요로 하는 코드를 사용하는 클래스는 object 등 해당 클래스에 의존적이게 되어 결합도가 올라가게 됩니다. 많은 점이 찍혀있다는 것은, 객체가 다른 객체에 깊숙이 관여하고 있음을 의미합니다.

그러므로 다른 객체의 메서드를 호출하기보단, 물어보는 방식으로 코드를 작성해 의존관계를 끊어내도록 해야 합니다.

 

※ Stream API는 메서드 체이닝을 사용하는 것이므로 이 원칙을 적용해야 하는 경우에 해당하지 않음!!


(다음 글에 이어서)

https://seongho-jo-5.tistory.com/36

 

[Java] 객체지향 생활 체조 원칙 - 2

(전 글에 이어서 작성) [Java] 객체지향 생활 체조 원칙 - 1 SOLID 원칙, 추상화, 다형성···, 객체 지향 프로그래밍(OOP)에 대해 공부할 때 정말 지겹도록 보게 되는 OOP의 특징과 원칙들이 있다. 코드

seongho-jo-5.tistory.com

 

참고 :

https://hudi.blog/thoughtworks-anthology-object-calisthenics/

https://catsbi.oopy.io/bf003ff6-2912-4714-8ac2-44eeb7becc93