객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

 

목차

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

 

 

Getter / Setter / Property를 쓰지 않는다.


벌써 마지막 9번째 입니다. 여전히 마지막까지 추상적인 말만 늘어놓네요.

이 규칙은 정말로 getter setter를 쓰지말라는 의미가 아닙니다. 객체 내부에 어떤 속성이 있는지 외부에서 알지 못하게 하는 캡슐화에 초점을 맞춰야합니다.

 

TDA 원칙이라는것이 있습니다. Tell, Don't ask, 즉 정보를 묻지말고 하고싶은걸 하라는건데요.

 

 

 

예시로 값이 짝수인지 확인하는 로직을 만들어보겠습니다.

public class Number {

    private int number;

    public Number(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }
    
}

public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        int intNumber = number.getNumber();

        if (intNumber%2 == 0) {
            System.out.println("짝수임");
        } else {
            System.out.println("짝수아님");
        }
    }
}

 

number.getNumber()를 호출하면서 객체 내부의 값이 노출되었고, 그 값을 로직에 사용했습니다. 확실히 캡슐화가 깨졌습니다.

짝수인지 판단하는 로직을 Number 객체 안에 숨겨보겠습니다.

 

 

public class Number {

    private final int number;

    public Number(int number) {
        this.number = number;
    }

    public void isEven() {
        if (number%2 == 0) {
            System.out.println("짝수임");
        } else {
            System.out.println("짝수아님");
        }
    }
}

public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        number.isEven();

    }
}

 

Getter가 사라져 Number 객체안에 값을 알지 못하게되었습니다.

 

그렇다면 Gette는 절대 사용하면 안되는것일까요? 당연히 아닙니다. 경우에 따라 다릅니다.

객체의 값을 외부로 표현해주어야할 경우에는 Getter를 사용해야합니다.

 

public void printNumber() {
    System.out.println("현재 값은 " + number + " 입니다.");
}

 

 

 

 

 

Setter의 경우도 마찬가지입니다.

 

public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        number.setNumber(number.getNumber() + 2);

    }
}

현재값에 2를 더하는 로직입니다. setter를 사용하기 위해 getter + 2를 하는건 상당히 보기 안좋습니다.

 

public void addNumber(int number) {
    this.number += number;
}


public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        number.addNumber(2);

    }
}

 

핵심은 캡슐화라는것을 명심해주세요.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

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

 

인스턴스 변수가 많으면 많을 수록 응집도가 떨어진다. 라고 해석하면 좋을 것 같습니다. 여기서 말하는 인스턴스 변수는 기본형을 의미하는것 같습니다. 확실하지는 않아요. 사실 이번편은 3편 원시값과 문자열을 포장한다와 맥락이 상당히 유사합니다.

 

 

public class Student {

    private String name;
    private String age;
    private String score;
    
}

Student 클래스안에 3개의 인스턴스 변수가 존재합니다.

 

이를 

public class Student {

    private Name name;
    private Age age;
    private Score score;
    
}

이렇게 원시값과 문자열을 포장하거나

 

public class Student {

    private UserInfo info;
    private Score score;
    
}

public class UserInfo {

    private String name;
    private int age;

}

 

DB 제2 정규화 과정처럼 부분적 종속 변수를 묶거나 하라는 의미같습니다.

 

사실 조금 이해가 안되는 부분도 많아서 인스턴스변수가 많아지면 응집도가 낮아진다 라고 이해하고 넘어가도 좋을것같습니다.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

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

 

모든 엔티티를 작게 유지한다.

 

 

이 규칙은 클래스는 50줄 이하로 유지하고, 패키지는 10개 이하의 파일만 가져야한다. 라고 책에서 설명되어있습니다. 가능한가? 싶기도 하지만 잘 생각해보면 '코드가 길어진다는 것은 클래스가 한가지 이상의 일을 하고있을 확률이 높다' 라고도 해석할 수 있을 것같습니다.

그러니 50줄 이하의 클래스, 10개 이하의 패키지에 초점을 맞추기 보단 최대한 엔티티를 작게 유지할 수 있도록 노력하는것이 제가 해석하는 바입니다.

 

 

그냥 말로만 하고 넘어가긴 아쉬우니 지금까지 만들었던 StudentList 객체가 규칙을 잘 지키고 있는지 확인해보겠습니다.

public class StudentList {

    private final List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }

    public void addStudent(Student student) {
        validateStudentsSize();
        validateStudentName(student);

        students.add(student);
    }

    public Student getTopScoreStudent() {
        Student topStudent = null;
        for (Student student : students) {
            if (topStudent == null) {
                topStudent = student;
                continue;
            }

            topStudent = topStudent.compareScore(student);
        }
        return topStudent;
    }

    public List<Student> getStudents() {
        return Collections.unmodifiableList(students);
    }

    public double getAverageAge() {
        return students.stream()
            .mapToInt(student -> student.getAge().getAge()) // 쉿!
            .average()
            .orElse(0);
    }

    private void validateStudentName(Student student) {
        String name = student.getName().getName();

        for (Student studentFor : students) {
            String nameFor = studentFor.getName().getName();

            if (name.equals(nameFor)) {
                throw new IllegalStudentsException("중복된 이름이 존재합니다.");
            }
        }
    }

    private void validateStudentsSize() {
        if (students.size() > 10) {
            throw new IllegalStudentsException("최대 학생 수는 10명입니다.");
        }
    }

}

 

라인을 확인해보니 58줄.. 8줄 오버되고 있네요. 저번편을 보신분들이라면 눈에 거슬리는 메서드가 있을겁니다.

바로 validateStudentName() 메서드입니다. 아.. '코드가 길어진다는 것은 클래스가 한가지 이상의 일을 하고있을 확률이 높다' 이 말이 틀린게 하나 없었네요. 대체 어디까지 보신겁니까....

 

 

// 변경 전
private void validateStudentName(Student student) {
    String name = student.getName().getName();

    for (Student studentFor : students) {
        String nameFor = studentFor.getName().getName();

        if (name.equals(nameFor)) {
            throw new IllegalStudentsException("중복된 이름이 존재합니다.");
        }
    }
}

// 변경 후
private void validateStudentName(Student student) {
    for (Student studentFor : students) {
        student.checkDistinctName(studentFor);
    }
}

 

 자세한 내용은 5편에서 다뤘기 때문에 생략하도록 하겠습니다.

 

StudentList의 길이가 58줄에서 52줄로 6줄 줄어들었습니다. 강박적으로 줄이라면 더욱 줄일 수 있겠지만 이 정도만해도 최대한 엔티티를 작게 유지하도록 노력한것이라고 봐도 무방하겠죠?

 

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

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

 

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

 

쉽게말해 int, String 과 같은 타입의 값을 객체로 포장하는겁니다.

 

public class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

 

Student 객체에는 이름과 나이를 가지고 있습니다.

 

만약 이름은 최소 2자 이상, 나이는 20세 이상 이라는 조건이 추가된다면?

 

public class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        validName(name);
        validAge(age);
        this.name = name;
        this.age = age;
    }

    private void validName(String name) {
        if (name.length() < 2) {
            throw new IllegalStudentNameException("이름은 최소 2자 이상이어야 합니다.");
        }
    }
    private void validAge(int age) {
        if (age < 20) {
            throw new IllegalStudentAgeException("나이는 20살 이상이어야 합니다.");
        }
    }

}

 

잘못된 값의 검증을 제외한 일반적인 예외처리를 했음에도 Student가 할 일이 엄청 늘어났습니다.

Student는 이제 이름과 나이에 대한 상태관리를 모두 해야합니다.

그럼 만약 Student에 이메일, 성별 등 변수가 추가된다면 코드는 복잡해질것입니다.

 

이제 원시 타입 변수로 포장해보겠습니다.

public class Student {

    private Name name;
    private Age age;

    public Student(Name name, Age age) {
        this.name = name;
        this.age = age;
    }
}

public class Name {

    private String name;

    public Name(String name) {
        validName(name);
        this.name = name;
    }

    private void validName(String name) {
        if (name.length() < 2) {
            throw new IllegalStudentNameException("이름은 최소 2자 이상이어야 합니다.");
        }
    }

}

public class Age {

    private int age;

    public Age(int age) {
        validAge(age);
        this.age = age;
    }

    private void validAge(int age) {
        if (age < 20) {
            throw new IllegalStudentAgeException("나이는 20살 이상이어야 합니다.");
        }
    }

}

 

이제 Student에서 이름과 나이에 대한 책임이 분리 되면서 단일 책임 원칙(SRP)도 지키게 되었습니다.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

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

 

 

 

else 예약어를 사용하지 않는다.

 

if-else에서 else 예약어를 사용하지 않는다는 의미입니다. switch문도 허용하지 않습니다.

 

public class ex2 {

    public static void main(String[] args) {

        String select = "1";

        String payment = getPayment(select);
        System.out.println(payment); // "신용카드"
    }

    private static String getPayment(String select) {
        String payment = null;

        if ("1".equals(select)) {
            payment = "신용카드";
        } else if ("2".equals(select)) {
            payment = "무통장입금";
        } else if ("3".equals(select)) {
            payment = "카카오페이";
        } else {
            payment = "네이버페이";
        }
        return payment;
    }
}

 

if-else 문을 사용하여 코드를 작성했습니다. 겉보기에는 가독성도 괜찮고 전혀 문제될것이 없는 코드 같습니다. 하지만 실제 로직을 작성한다면 이보다 훨씬 복잡한 코드를 작성하게 될것입니다.

 

이 코드의 비즈니스적으로 문제가 있습니다. 사용자가 1,2,3,4가 아닌 다른 값을 넣었다면? 개발자는 예외가 발생하길 원하지만 실제로는 "네이버페이"가 정상적으로 반환될것입니다. 이는 설계의도를 완전히 벗어난 결과입니다. 이제 early return 구조를 적용해보겠습니다.

 

public class ex2 {

    public static void main(String[] args) {

        String select = "5";

        String payment = getPayment(select); // NotFoundPaymentException 예외발생!!
        System.out.println(payment);
    }

    private static String getPayment(String select) {

        if ("1".equals(select)) {
            return "카드";
        }
        if ("2".equals(select)) {
            return "무통장입금";
        }
        if ("3".equals(select)) {
            return "카카오페이";
        }
        if ("4".equals(select)){
            return "네이버 페이";
        }
        throw new NotFoundPaymentException("잘못된 결제방식입니다.");
    }
}

 

구조를 살짝 바꾸면서 메서드의 가독성이 높아졌습니다. 개발자가 의도한대로 조건에 만족한다면 결제방식을 바로 반환하며 종료됩니다. 반대로 조건에 만족하는 결과가 없다면 예외를 발생시키면서 설계의도대로 동작할것입니다. 

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

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

 

 

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

 

들여쓰기의 깊이를 2 이상 두지 말라는 지침입니다. for 문안에 if문이 있으면 깊이가 2인 코드가 됩니다. 따라서 한 메서드 안에 깊이가 2 이상인 코드가 존재하면 안된다는 말과 같습니다.

 

이 원칙의 의미는 코드를 작성할때 무조건 들여쓰기를 지양하라는 것보다는 메서드를 분리함으로써 하나의 메서드가 하나의 일을 하도록 설계하자는데 의의가 있습니다. 일을 잘개 쪼갤수록 결합도는 낮아지고, 응집도는 높아집니다. 이러한 코드는 재사용성이 높고, 디버깅에 용이해집니다. 따라서 한 메서드에는 한가지의 주요 작업을 수행하도록 구성하는 것이 좋습니다.

 

public class ex1 {

    static List<Integer> boardNumbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

    public static void main(String[] args) {
        int sum = sumNumbers();
        System.out.println(sum);
    }

    private static int sumNumbers() {
        int sum = 0;
        for (int number : boardNumbers) {
            if (number > 5) {
                sum += number;
            }
        }
        return sum;
    }
}

 

boardNumbers에서 5 이상인 수만 더해서 반환하는 메서드 입니다. for 문 안에 if문이 있다는건 깊이가 2라는 의미입니다.

메서드를 분리해보겠습니다.

 

public class ex1 {

    static List<Integer> boardNumbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

    public static void main(String[] args) {
        int sum = sumNumbers();
        System.out.println(sum);
    }

    private static int sumNumbers() {
        int sum = 0;
        for (int number : boardNumbers) {
            sum += check(number);
        }
        return sum;
    }

    private static int check(int number) {
        if (number > 5) {
            return number;
        }
        return 0;
    }
}

 

메서드를 분리함으로써 sumNumbers()는 수를 더하는 일만 하게되었고, check(int number)는 수의 조건을 확인하는 일만 하게되었습니다. 예시코드가 짧아서 들여쓰기에 대한 장점이 잘 부각되지않은것 같지만 하나의 메서드는 하나의 일을 한다는 개념을 이해하셨다면 충분합니다.

 

 

 

 

 

 

+ Recent posts