클린코드 - 6장
🔖 6장 : 객체와 자료 구조
읽은 날짜 : 2024.09.04
지은이 : 로버트 C. 마틴
출판사 : 인사이트
기억하고 싶은 내용
자료 추상화
- 변수를 private으로 선언하더라도 각 값마다 getter, setter를 제공한다면 구현을 외부로 노출하는 것이다.
- 클래스는 추상 인터페이스를 제공해서 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 한다.
자료 구조 vs 객체
1. 자료 구조
- 자료를 공개하고 아무 메서드도 제공하지 않는다.
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.14;
public double area(Object shape) throws NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
} else if (shape instanceof Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
throw new NoSuchException();
}
}
2. 객체
- 객체는 자료를 감추고 자료를 다루는 함수만 공개한다. (getter/setter가 아님!)
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side * side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.14;
public double area() {
return PI * radius * radius
}
}
3. 자료 구조와 객체는 사실상 반대이면서 상호 보완적이다.
- 자료 구조
- 절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
- 절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 기존 함수들이 새로운 자료 구조를 처리할 수 있도록 모두 수정해야 한다.
- 객체
- 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
- 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 기존 클래스에 모든 클래스에 새로운 함수를 추가해야 한다.
- 모든 것이 객체는 아니며, 때로는 단순한 자료 구조와 절차적인 코드가 적합한 상황도 있다.
디미터 법칙
- 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다.
- 객체 지향 프로그래밍에서의 디미터 법칙
- 클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.
- 클래스 C
- f가 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
- 또한, 위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안 된다.
- 클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.
1. 추이적 탐색을 피하자
- 한 모듈은 주변 모듈을 모를수록 좋다.
- A가 B를 사용하고 B가 C를 사용하더라도 A가 C를 알아야 할 필요는 없다.
2. 자료 구조와 객체의 특성을 혼합한 설계를 피하자
- 이를테면 공개 변수를 사용하면서 어떤 기능을 수행하는 함수나 getter/setter도 있는 구조이다.
- 이렇게 되면 새로운 자료 구조도, 새로운 함수도 추가하기 어렵다.
// Bad ❌
class Rectangle {
public int width;
public int height;
public int getArea() {
return width * height;
}
}
3. 구조체를 감추자
- 객체는 내부 구조를 드러내지 말고, 함수를 통해 객체에게 '무언가를 하라'고 말해야 한다.
- 이때 함수는 추상화 수준을 일관되게 유지하여 직접적으로 알고 있는 객체만 탐색한다.
- 추상화 수준은 한 단계만 내려가야 한다. [p.392 참고]
// Bad
String street = company.getManager().getAddress().getStreet();
// Good
String street = company.getManagerStreet();
자료 전달 객체(Data Transfer Object, DTO)
- 자료 구조의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스로, 자료 전달 객체라고도 한다.
- DTO는 데이터베이스에 저장된 데이터를 애플리케이션 코드에서 사용할 객체로 변환하는 단계에서 사용한다.
활성 레코드
- DTO의 특수한 형태
- 공개 변수가 있거나 비공개 변수에 getter/setter가 있는 자료 구조지만, 대게 save, find 같은 함수도 제공한다.
- 활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 구조를 객체로 취급하는 것은 바람직하지 않다. 자료 구조도 객체도 아닌 혼합된 구조다.
- 해결책은 활성 레코드는 자료 구조로 취급하는 것이다. 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다.
결론
- 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다.
- 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
오늘 읽은 소감
이번 장을 읽으면서 엔티티 클래스의 프로퍼티를 private으로 하면 TypeORM이 데이터베이스 결과를 객체로 매핑할 때 오류가 발생했던 일이 떠올랐습니다.
즉, 엔티티 클래스의 프로퍼티를 public으로 둘 수 밖에 없다는 뜻인데요, 이건 JPA와 확연히 다른 부분입니다.
왜 TypeORM에서 private 프로퍼티를 지원하지 않는지에 대해 찾아보았는데, TypeORM GitHub 이슈에서 개발자의 아래와 같은 코멘트가 있었습니다.
"If you want to send back only model with some method and no properties (or partially available) then maybe its good idea to have a separate model for that? And keep your entity as a only-schema-definition place? It's a perfect encapsulation."
"모델을 반환할 때 몇가지의 메서드가 있고 속성이 없거나 일부 속성만 포함된 모델을 반환하려는 경우, 별도의 모델을 만드는 것이 좋을 수 있습니다. 그리고 엔티티는 스키마 정의만을 위한 공간으로 유지하세요. 이것이 완벽한 캡슐화입니다."
DDD를 고려했을 때 엔티티는 자료 구조로 취급하고, 캡슐화된 모델 클래스를 따로 만들어서 비즈니스 규칙 메서드를 구현하는 게 좋을 것이라는 이야기 같습니다.
저는 이 코멘트를 보고 클린 코드에서 이야기하는 자료 구조와 객체를 구분하자는 관점에서 TypeORM이 설계되지 않았을까... 라는 추측을 해보았습니다.
궁금한 내용 & 잘 이해되지 않는 내용
구현을 왜 감춰야 하는가?
- 객체지향 프로그래밍에서는 '캡슐화'라고 한다.
- 데이터에 대한 직접적인 접근을 막아서, 데이터를 외부에서 잘못 변경하는 것을 막고 변경 로직을 한 곳으로 제한하여 요구사항 변경에도 유연하게 대응할 수 있다. [참고]
- 또한 복잡한 내부 인터페이스를 숨겨서 외부 인터페이스에 대한 설명과 문서화를 쉽게 만들어준다.
- JavaScript 캡슐화 방법 [참고]
- '#' prefix를 붙여 private 프로퍼티로 설정한다.
- protected 접근제어자를 지원하지 않는다. 대신 자바스크립트 개발자 사이에서 관습적으로 protected 프로퍼티를 표현할 때 '_' prefix를 붙인다.
- 읽기 전용 프로퍼티로 만들기 위해 getter만 만들고, setter는 만들지 않는다.
활성 레코드와 비즈니스 규칙이 혼합되는 것이 왜 문제가 되는가?
- 데이터 관리와 비즈니스 로직 처리의 두 가지 역할을 동시에 수행하여 단일 책임 원칙을 위반한다.
- 데이터베이스 관련 코드와 비즈니스 로직이 섞여 유지 보수와 데이터베이스 없이 단위 테스트가 어려워진다.
절차적 프로그래밍
- 프로그램을 수행해야 하는 일련의 단계로 나누고, 각 단계는 프로시저(procedure) 또는 함수로 표현되는 프로그래밍 패러다임
- 객체지향 프로그래밍의 반대말이 아니다!
휴리스틱(heuristic)
- 컴퓨터학 용어 : 문제 해결이나 결정을 위해 사용되는 규칙 또는 경험적인 방법
- 심리학 용어 : 복잡한 과제를 간단한 판단 작업으로 단순화시켜 의사 결정하는 경향
- 경제학 용어 : 어떤 사안 또는 상황에 대해 엄밀한 분석에 의하기보다 제한된 정보만으로 즉흥적, 직관적으로 판단, 선택하는 의사결정 방식
참고
Support for private entity attributes (optional) #3548