본문으로 바로가기

[CleanCode]6장 객체와 자료구조

category BackEndCleanCode 3년 전
728x90
반응형

CleanCode - 로버트 C.마틴

 

▷ 6. 객체와 자료구조

자료 추상화

public class Point {
    public double x;
    public double y;
}
public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta)
}
  • 위 코드는 구현을 외부로 노출하고 아래는 구현을 완전히 숨긴다.
  • 아래 코드는 자료 구조의 이상을 표현한다.
  • 위 코드에서 구현을 감추려면 추상화가 필요하다. 그저 조회 함수와 설정 함수(게터, 세터)로 변수를 다룬다고 클래스가 되지는 않는다.
public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}
public interface Vehicle {
    double getPercentFuelRemaining();
}
  • 자료를 세세하게 공개하기 보다는 아래처럼 추상적인 개념으로 표현하는 편이 좋다.
  • 인터페이스나 조회/설정 함수만으로는 추상화가 이뤄지지 않는다. 아무 생각없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.

자료/객체 비대칭

  • 위의 두 가지 예제는 객체와 자료 구조 사이에 벌어진 차이를 보여준다.
  • 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 제공한다. 자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
public class Square {
    public Point topLeft;
    public double side;
}

public class Rectangle {
    public Point topLeft;
    public double height;
    public double width;
}

public class Circie {
    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.side * r.side;
        } else if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return c.side * c.side;
        }
    }
}
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;
    }
}
  • 위 코드는 절차지향적이다. 만약 Geometry 클래스에 둘레 길이를 구하는 함수를 추가한다면 도형 클래스는 아무런 영향도 받지 않는다.
  • 반대로 새 도형을 추가하고 싶다면? Geometry 클래스에 속한 함수를 모두 고쳐야한다.
  • 아래 코드는 객체지향적인 도형 클래스이다. 여기서 area()는 다형 메서드이다. Geometry 클래스는 필요없다. 따라서 새 도형을 추가해도 기존 함수에 아무런 영향을 미치지 않는다.
  • 반면 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.
  • 즉 자료 구조를 사용하는 절차적 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉬우며 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
  • 같은 말로 절차적인 코드는 새로운 자료 구조를 추가하기 어려우며 객체 지향 코드는 새로운 함수를 추가하기 어렵다.(모든 클래스를 고쳐야 한다.)
  • 복잡한 시스템을 짜다 보면 새로운 함수가 아니라 새로운 자료 타입이 필요한 경우가 생긴다. 이때는 절차적인 코드와 자료 구조가 좀더 적합하다.
  • 분별있는 프로그래머는 모든 것이 객체라는 생각이 미신임을 잘 안다. 때로는 단순한 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다.

디미터 법칙

  • 잘 알려진 휴리스틱으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
  • 클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다고 주장한다.
1. 클래스 C
2. f가 생성한 객체
3. f 인수로 넘어온 객체
4. C 인스턴스 변수에 저장된 객체

 

기차 충돌

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
  • 디미터 법칙을 어기는 다음 코드는 기차 충돌이라 부른다. 
  • 일반적으로 조잡하다고 여겨져 피하는 편이 좋으며 다음과 같이 나누는 편이 좋다.
Options opts = ctxt.getOptions();
File scrachDir = opts.getScratchDir();
final String outputDir = scrachDir.getAbsolutePath();
  • 위 예제가 디미터 법칙을 위반하는지 여부는 ctxt, Options, ScrachDir이 객체인지 자료 구조인지에 달렸다.
  • 객체라면 내부 구조를 숨겨야하므로 디미터 법칙을 위반하며 자료 구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다.
  • 위 예제는 조회 함수를 사용하는 바람에 혼란을 일으킨다. 코드를 다음과 같이 구현했다면 디미터 법칙을 거론할 필요가 없어진다.
final String outputDir = ctxt.options.scratchDir.absolutePath;
  • 이와 같이 자료 구조는 무조건 함수 없이 공개 변수만 포함하고 객체는 비공개 변수와 공개 함수를 포함한다면 문제는 훨씬 간단하다.

구조체 감추기

  • 만약 ctxt, options, scratchDir이 진짜 객체라면? ctxt가 뭔가를 하라고 말해야지 속을 드러내라고 말하면 안된다.
  • 다음은 같은 모듈에서 한참 아래로 내려가서 가져온 코드이다.
String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);
  • 점, 슬래시, 파일 확장자, File 객체를 마구 뒤섞으면 안된다. 어찌됐든, 위 코드를 통해 임시 디렉터리의 절대 경로를 얻으려는 이유가 임시 파일을 생성하기 위한 목적이라는 사실을 알게되었다.
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
  • 때문에 ctxt 객체에 임시 파일을 생성하라고 시킴으로써 ctxt는 내부 구조를 드러내지 않으며 여러 객체를 탐색할 필요가 없다. 따라서 디미터의 법칙을 위반하지 않는다.

자료전달 객체

  • DTO : 공개 변수만 있고 함수는 없는 클래스
  • Bean : 비공개 변수를 조화/설정 함수로 조작
  • 활성 레코드 : 공개, 비공개 변수와 조회/설정 함수 그리고 탐색 함수도 제공한다.

결론

  • 객체는 동작을 공개하고 자료를 숨긴다. 때문에 기존 동작을 변경하지 안흐면서 새 객체 타입을 추가하기는 쉬우며 기존 객체에 새 동작을 추가하기는 어렵다.
  • 자료 구조는 별다른 동작없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나 기존 함수에 새 자료 구조를 추가하기는 어렵다.
  • 시스템을 구현할 때 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
반응형