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 메서드를 제공합니다.

  1. 새 도형을 추가해도 기존 함수에 영향을 미치지 않음
  2. 새 함수를 추가할 때에는 도형 클래스 전부를 고쳐야 함.
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는 아래 객체의 메서드만 호출해야 합니다.

  1. 클래스 C
  2. f가 생성한 객체
  3. f 인수로 넘어온 객체
  4. 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을 포함하며, OptionsScratchDir을 포함하며, ScratchDirAbsolutePath를 포함한다는 사실을 알기 때문에 함수 하나가 아는 지식이 상당히 많습니다. 위 코드를 사용하는 함수는 많은 객체를 탐색할 수 있습니다.

위의 예제가 디미터 법칙을 위반하는지 여부는 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() {
		...
	}
	...
}

결론

어떤 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합합니다.

중요한 것은 편견 없이 직면한 문제에 최적인 해결책을 선택해야 한다는 사실입니다.

reference

Clean Code 클린코드 : 애자일 소프트웨어 장인 정신