클린코드 6장 - 객체와 자료 구조
CleanCode 6장 객체와 자료 구조
에 대해 정리한 포스트입니다.
Overview
변수를 private로 정의하는 것은 남들이 변수에 의존하지 않게 만들고 싶어서입니다.
- 충동이든 변덕이든 변수 타입이나 구현을 맘대로 바꿀 수 있음
그런데 대부분의 프로그래머들은 조회(get), 설정(set) 함수를 당연하게 public으로 공개합니다.
자료 추상화
구현을 감추기 위해서는 추상화가 필요합니다.
- 변수 사이에 함수라는 계층을 넣는다고 구현이 감춰지지 않음
- 조회/설정 함수로 변수를 다룬다고 감춰지는 것이 아님
- 생각없이 추가하는 조회/설정 함수는 아주 나쁘다.
사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 Class
- 자료를 세세하게 공개 «« 추상적인 개념으로 표현
// 구체적인 Point 클래스
// 명확하게 직교 좌표계를 쓴다는 것을 알 수 있다.
public class Point {
public double x;
public double y;
}
// 추상적인 Point 클래스
// 클래스 메서드가 접근 정책을 강제한다.
public interface Point {
double getX();
double getY(); // 조회는 각각 가능하지만
void setCartesian(double x, double y); // 설정을 2개의 값을 동시에 넣어주어야 한다.
double getR();
double getTheta();
void setPolar(double r, double theta);
// 구체적인 Vehicle 클래스
// 변수를 그대로 리턴하는 함수일 것이 틀림없다.
public interface Vehicle {
public getFuelThankCapacityInGallons();
public getGallonsOfGasoline();
}
// 추상적인 Vehicle 클래스
// 백분율이라는 추상적인 개념으로 반환하기에 어디서 오는지 사용자에게 드러나지 않는다.
public interface Vehicle {
double getPercentFuelRemaining();
}
자료/객체 비대칭
자료와 자료 구조는 본질적으로 상반됩니다.
- 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개
- 자료 구조는 자료를 그대로 공개
1. 절차적인 도형
각 도형 클래스는 간단한 자료 구조, 아무 메서드도 제공하지 않습니다.
- 절차 지향적
-
도형의 동작은 Geometry 클래스에서 구현
- 함수를 추가할 때는 도형 클래스는 수정할 필요가 없다.
- 도형을 추가할 때는 모든 함수를 수정해야 한다.
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.141592653589793;
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 NoSuchShapeException();
}
}
2. 객체 지향적인 도형
각 도형 객체는 area()는 다형^polymorphic 메서드를 제공합니다.
- 새 도형을 추가해도 기존 함수에 영향을 미치지 않음
- 새 함수를 추가할 때에는 도형 클래스 전부를 고쳐야 함.
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.141592653589793;
public double area() {
return PI * radius * radius;
}
}
3. 절차적인 코드 vs 객체 지향 코드
(자료 구조를 사용하는)절차적인 코드
- 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
- 새로운 자료 구조를 추가기 어렵다. 그러기 위해선 모든 함수를 고쳐야 한다.
- 새로운 자료 타입이 아니라 새로운 함수가 필요한 경우에 더 유리
객체 지향 코드
- 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
- 새로운 클래스를 추가하기 어렵다. 그러기 위해선 모든 클래스를 고쳐야 한다.
- 새로운 함수보다는 새로운 자료 타입이 필요한 경우 더 유리
모든 문제를 객체로 해결하려는 생각은 좋지 않다.
디미터 법칙
디비터 법칙 은 잘 알려진 휴리스틱으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙입니다.
휴리스틱이란 경험에 기반하여 문제를 해결하거나 학습하거나 발견해 내는 방법을 말한다. 최적의 해 대신 현식적으로 만족할 만한 수준의 해를 구하는 방법을 일컫는다.
자세히 표현하자면 클래스 C의 메서드 f는 아래 객체의 메서드만 호출해야 합니다.
- 클래스 C
- f가 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
class Demeter {
private A a;
private int func() { return 0; }
public void example(B b) {
C c = new C();
int f = func(); // 1번의 경우
b.invert(); // 2번의 경우
a = new A();
a.setActive(); // 3번의 경우
c.print(); // 4번의 경우
}
}
1. 기차 충돌
아래 코드는 디미터 법칙을 어기는 것으로 보입니다. 이와 같은 코드를 기차 충돌 이라 부릅니다.
- 객차가 한줄로 이어진 기차처럼 보임
- 일반적으로 조잡하다 여겨지는 방식이기 때문에 피하는 것이 좋음
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
나누는 편이 좋습니다.
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
ctxt 객체가 Options
을 포함하며, Options
가 ScratchDir
을 포함하며, ScratchDir
이 AbsolutePath
를 포함한다는 사실을 알기 때문에 함수 하나가 아는 지식이 상당히 많습니다. 위 코드를 사용하는 함수는 많은 객체를 탐색할 수 있습니다.
위의 예제가 디미터 법칙을 위반하는지 여부는 ctxt
, Options
, ScratchDir
이 객체인지 자료 구조인지에 달려 있습니다.
- 객체라면 디미터 법칙을 위반
- 자료구조라면 내부 구조를 당연히 노출하므로 디미터 법칙의 대상이 되지 않습니다.
자료구조 였을 경우에 아래와 같이 구현하였다면 고민의 여지가 없었을 것입니다.
final String outputDir = ctxt.options.scratchDir.absolutePath;
2. 잡종 구조
단순한 자료 구조에도 조회/설정 함수를 정의하라 요구하는 프레임워크와 표준^bean 이 존재합니다. 이로 인해, 절반은 객체, 절반은 자료 구조인 잡종 구조가 나오게 됩니다.
- 중요한 기능을 수행하는 함수와 public 변수가 공존
- public 조회/설정 함수는 private 변수를 그대로 노출
이런 잡종 구조는 새로운 함수, 자료 구조 어떠너 것도 추가하기 어려워 집니다.
- 양쪽 구조의 단점만 모아놓은 구조이므로 되도록 피하는 편이 좋음
- 프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해 어중간하게 내놓은 설계에 불과
3. 구조체 감추기
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
ctxt
, options
, scratchDir
이 객체라면 함수 내에서 줄줄이 사탕으로 호출해서는 안됩니다.
- 객체라면 내부 구조를 감춰야 하기 때문
// ctxt 객체에 공개해야 하는 메서드가 너무 많음
ctxt.getAbsolutePathOfScratchDirectoryOption();
// ctxt가 객체라면 뭔가를 하라고 해야 하는데
// getScratchDirectoryOption()에서 속을 드러내라고 말하는 느낌이다.
ctxt.getScratchDirectoryOption().getAbsolutePath()
// ctxt 객체에게 임시 파일을 생성하라고 시킴
// ctxt는 내부 구조를 드러내지 않으며, 모듈에서 해당 함수는 여러 객체를 탐색할 필요가 없다.
// 디미터 법칙 충족
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
자료 전달 객체
자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스입니다.
이런 자료 구조체를 때로는 자료 전달 객체^Data ^Transfer ^Object, ^DTO 라고 합니다.
- 데이터베이스와 통신하거나 소켓에서 받은 메시지의 구문을 분석할 때 유용
- 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 처음 사용되는 구조체
class Address {
private final String postalCode;
private final String city;
private final String street;
private final String streetNumber;
private final String apartmentNumber;
public Address(String postalCode, String city, String street, String streetNumber, String apartmentNumber) {
this.postalCode = postalCode;
this.city = city;
this.street = street;
this.streetNumber = streetNumber;
this.apartmentNumber = apartmentNumber;
}
public String getPostalCode() {
return postalCode;
}
public String getCity() {
return city;
}
public String street() {
return street;
}
public String streetNumber() {
return streetNumber;
}
public String apartmentNumber() {
return apartmentNumber;
}
}
활성 레코드
활성 레코드는 DTO의 특수한 형태입니다.
- 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료구조
- save나 find와 같은 탐색 함수도 제공
활성 레코드에 비즈니스 규칙 메서드를 추가해 객체로 취급하는 경우가 있는데 바람직하지 않습니다.
- 잡종 구조가 나오기 때문
비즈니스 규칙을 담는 객체는 따로 생성하는 것이 옳습니다. 내부 자료는 활성 레코드의 인스턴스일 가능성이 높습니다.
class Person {
private String name;
private String email;
public Person(String name, String email) {
this.name = name;
this.email = email;
}
...
public void sendEmail(){
...
}
}
class EmailSender{
private Person receiver;
...
public void sendEmail() {
...
}
...
}
결론
어떤 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합합니다.
중요한 것은 편견 없이 직면한 문제에 최적인 해결책을 선택해야 한다는 사실입니다.