객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.
목차
- 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
- else 예약어를 사용하지 않는다.
- 모든 원시 값과 문자열을 포장한다.
- 일급 컬렉션을 쓴다.
- 한 줄에 점을 하나만 찍는다.
- 줄여 쓰지 않는다 ( 축약 금지 )
- 모든 엔티티를 작게 유지한다.
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- Getter / Setter / Property를 쓰지 않는다.
일급 컬렉션
일급 컬렉션이란?
Collection을 Wrapping하면서 그 외의 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 합니다.
일급컬렉션의 장점은 아래와 같습니다.
- 비즈니스의 종속적인 자료구조
- 불변성 보장
- 상태와 행위를 한 곳에서 관리
- 이름이 있는 컬렉션
@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명입니다.");
}
}
}
비즈니스로직을 모두 처리했지만 약간의 문제가 있습니다.
- List 컬렉션에 의존적입니다.
- List<Student>와 StudentService간에 결합도가 높습니다.
- 다른 사람이 보았을 때 List<Student>의 일을 파악하기 어렵습니다.
- 컬력션이 변경된다면 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();
}
'Language > 객체지향' 카테고리의 다른 글
객체지향 생활체조 원칙 6. 줄여 쓰지 않는다 (축약 금지) (0) | 2024.01.21 |
---|---|
객체지향 생활체조 원칙 5. 한 줄에 점을 하나만 찍는다. (0) | 2024.01.21 |
객체지향 생활체조 원칙 3. 모든 원시 값과 문자열을 포장한다. (0) | 2024.01.10 |
객체지향 생활체조 원칙 2. else 예약어를 사용하지 않는다. (0) | 2024.01.02 |
객체지향 생활체조 원칙 1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다. (0) | 2024.01.02 |