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

목차

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

 

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

 

 

단순히 한 줄에 점의 개수를 헤아려 줄이라는것이 아닙니다. 점을 찍는 행위는 인스턴스의 접근을 하는 행위로 이는 호출자와 피호출자와의 결합도가 강하게 형성된다는 의미입니다.

 

이전시간인 일급 컬렉션에서 사용되었던 List<Student> students를 예시로 사용해보겠습니다.

 

public class Student {

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

Score 객체를 추가로 넣었습니다. 내부에는 int score가 존재합니다.

 

List<Student> 내에서 가장 점수가 높은 학생을 선발한다고 가정한다면

public static void main(String[] args) {
    List<Student> students = new ArrayList<>();
    
    Student student = getTopScoreStudent(students);

}

private Student getTopScoreStudent(List<Student> students) {
    Student topStudent = null;
    for (Student student : students) {
        if (topStudent == null) {
            topStudent = student;
            continue;
        }
        if (topStudent.getScore().getScore() < student.getScore().getScore()) {
            topStudent = student;
        }
    }
    return topStudent;
}

이렇게 작성될것입니다. 하지만 처음에 언급했듯이 이 클래스와 Score와는 아무런 관계가 없습니다 즉, 불필요한 결합도가 높아졌다는 것을 의미합니다. 만약 Student 객체에 대한 변경이 생긴다면? Score 객체에 대한 변경이 생긴다면? Score 의 멤버변수 타입이 변경된다면? 당연하게도 위에 메서드를 사용하고있는 객체에 변경이 불가피합니다. 그리고 변경과정에서 오류의 가능성도 증가하겠죠.

 

그럼 어떻게 해야하는지 차근차근 알아봅시다.

 

 

먼저 저번시간에 만들었던 것처럼 일급클래스를 적용시켜줍니다.

public class StudentList {

    private final List<Student> students;

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

            if (topStudent.getScore().getScore() < student.getScore().getScore()) {
                topStudent = student;
            }
        }
        return topStudent;
    }
    
    // 나머지 메서드 ...
}

 

일급 클래스만 적용시켜도 Main 클래스와의 결합도가 낮아졌습니다.

 

 

이제 Student 객체로 일을 넘겨보겠습니다.

public class StudentList {

    private final List<Student> students;

    public Student getTopScoreStudent() {
        Student topStudent = null;
        for (Student student : students) {
            // 생략 ...

            topStudent = topStudent.compareScore(student);
        }
        return topStudent;
    }
    
    // 나머지 메서드 ...
}

public class Student {

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

    public Student compareScore(Student student) {
        if (score.getScore() < student.getScore().getScore()) {
            return student;
        }
        return this;
    }
}

 

이제 마지막으로 Score 객체에 일을 넘겨보겠습니다.

 

public class Student {

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

    public Student compareScore(Student student) {
        Score compareScore = score.compareScore(student.getScore());
        if (this.score == compareScore) { // 같은 주소값을 반환하기 때문에 == 를 사용했습니다.
            return this;
        }
        return student;
    }
    
    // 이하 생략 ...
}

public class Score {

    private int score;

    public Score(int score) {
        this.score = score;
    }
    public Score compareScore(Score arguScore) {
        if (score < arguScore.getScore()) {
            return arguScore;
        }
        return this;
    }
    
    // 이하 생략 ...
}

 

기존 코드와 달리 하위 객체의 일을 알 필요가 없어졌고 그저 자신의 일만 하게 되었습니다.

이로써 Main - StudentList - Student - Score 간에 결합도를 완전히 낮췄습니다.

 

결국 이 한 줄에 한 점을 찍는다는 의미는 자신이 알고있는 객체하고만 상호작용하면 되고 제 3자인 객체하고는 상호작용하지 않음으로써 결합도를 약하게 만들어 종속성을 최소화 시키는것입니다.

 

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

목차

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

 

 

일급 컬렉션이란?

Collection을 Wrapping하면서 그 외의 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 합니다.

일급컬렉션의 장점은 아래와 같습니다.

  1. 비즈니스의 종속적인 자료구조
  2. 불변성 보장
  3. 상태와 행위를 한 곳에서 관리
  4. 이름이 있는 컬렉션

 

@Getter
public class Student {

    private Name name;
    private Age age;

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

 

이전 원시값 포장에 대해서 설명할때 사용되었던 Student 객체입니다.

 

이번에는 학생명단을 만들어보겠습니다.

  • 학생명단은 최대 10명
  • 이름은 중복되지 않아야함

예시를 위해서 두가지의 검증로직이 필요하다고 가정하겠습니다. 그리고 이 검증로직을 Service 계층에서 진행했습니다.

 

public class StudentService {

    public void addStudent(List<Student> students, Student student) {
        validateStudentsSize(students);
        validateStudentName(students, student);

        students.add(student);
    }

    private void validateStudentName(List<Student> students, 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(List<Student> students) {
        if (students.size() > 10) {
            throw new IllegalStudentsException("최대 학생 수는 10명입니다.");
        }
    }
}

 

비즈니스로직을 모두 처리했지만 약간의 문제가 있습니다.

  1. List 컬렉션에 의존적입니다.
  2. List<Student>와 StudentService간에 결합도가 높습니다.
  3. 다른 사람이 보았을 때 List<Student>의 일을 파악하기 어렵습니다.
  4. 컬력션이 변경된다면 OCP원칙에 위배됩니다.

이러한 직접적인 의존성은 프로그램이 변화하거나 성장할 때 유연성을 제한하며, List<Student>의 변경은 StudentService 클래스의 범위를 넓혀 유지보수를 어렵게 만듭니다. 이런 구조는 코드 변경에 취약하며, 새로운 데이터 구조나 변경 사항이 발생할 때마다 Service 로직을 수정해야 하는 번거로움이 발생할 수 있습니다. 또한 이 문제는 개방-폐쇄원칙(OCP 원칙)을 위배하게 됩니다.

 

이제 이 문제들을 해결해 보겠습니다.

 

 

 

1. 비즈니스의 종속적인 자료구조

public class StudentList {

    private final List<Student> students;

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

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

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

    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명입니다.");
        }
    }
}

 

StudentList 클래스를 도입함으로써 List<Student> 객체를 캡슐화 했고, Service에서 관리하던 비즈니스로직을 StudentList 클래스로 종속시켰습니다이 변경으로 인해 StudentService(외부에서)는 더 이상 직접적으로 List<Student>를 다루지 않고, StudentList의 메소드를 통해 학생 목록을 조작할 수 있게 되었고, 

 

따라서 컬렉션 변경에 대한 OCP원칙을 지킬 수 있게 되었고, 학생명단과 StudentService간에 결합도가 낮아졌습니다.

 

 

 

2. 불변성 보장

일급 컬렉션은 컬렉션의 불변을 보장하는데, 단순히 final 을 사용하는 것이 아니라 캡슐화를 통해 이뤄집니다.

* final은 재할당만 금지하는 것이므로 add, remove 등이 가능합니다.

 

List<Student> 객체는 StudentList 클래스 내부에 있기때문에 Student 하위에 setter가 존재하지 않는 한 값의 변경이 일어날 수 없습니다. 불변성에 대한 자세한 내용은 차후 다룰 예정입니다.

 

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

 

// getStudents : Collections.unmodifiableList(students);
List<Student> students = studentList.getStudents();
Student newStudent = new Student(new Name("이OO"), new Age(21));
students.add(newStudent); // UnsupportedOperationException 런타임 예외발생!

 

 

3. 상태와 행위를 한 곳에서 관리

일급컬렉션은 값과 로직이 한 객체안에 있기때문에 응집도가 높아집니다. 따라서 변경이 용이하고 유지보수가 수월해집니다.

 

만약 비즈니스 로직이 추가된다면?

Service에서 로직을 추가할 경우 결합도만 높아지게 되고 수정사항이 증가하게 될겁니다.

하지만 일급컬렉션을 사용한다면 클래스 내부에서 수정 및 추가만 하면 됩니다.

 

public class StudentList {

    private final List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }
	
    // 만약 나이의 평균을 구하는 로직이 필요하다면? 
    public double averageAge() {
        return students.stream()
            .mapToInt(student -> student.getAge().getAge())
            .average()
            .orElse(0);
    }
	
    // 나머지 메서드 ...

}

 

 

4. 이름이 있는 컬렉션

List<Student> 만으로 그 의미를 추론하기는 어렵습니다. 하지만 StudentList 또는 그 상황에 맞는 이름을 붙혀준다면 그 의미는 명확해지고, 다른 사람이 코드를 분석할 때 객체 이름으로 어떤일을 할지 파악하기 쉬워진다는 겁니다.

 

public static void main(String[] args) {

    // 같은 List<Payment> 이기 때문에 ,
    // List<Payment>를 매개변수로 받는 메서드에는 모두 들어갈 수 있습니다.
    List<Payment> kakaoPay = createPayments();
    List<Payment> naverPay = createPayments();

    // 객체가 다르다면 사용용도가 명확해집니다.
    KakaoPay kakaoPay = createKakaoPay();
    NaverPay naverPay = createNaverPay();

}

 

 

+ Recent posts