OOP & CleanCode

CleanCode - 6. 객체와 자료 구조

Dlise 2024. 11. 30. 19:00

6장. 객체와 자료 구조

6장은 객체와 자료 구조에 대한 개념과 비교, 추상화에 대한 내용을 다룬다.

 

클래스를 구현할 때 변수의 접근 제어자를 private로 설정해 외부에서 직접 접근하지 못하도록 막는다. 그리고 이를 getter/setter로 접근하도록 구현하는 경우가 있는데, 이러한 부분에 대한 의문과 해결 방안을 이야기한다.

 

 

자료 추상화

클래스 혹은 인터페이스의 자료를 세세히 공개하는 것보다 추상적으로 정의함으로써 내부를 숨겨야 한다는 내용이다.

 

예를 들어, 구체적인 아래 코드보다

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();
    double setPolar(double r, double theta);
}

구체적인 코드의 경우, 클래스가 직교좌표계를 사용하고 있으며, 각 변수에 직접 접근해 값을 입력, 조회할 수 있다. 만약 getter/setter를 사용했다고 하더라도, 내부 내용이 감춰지지 않는다.

 

추상적인 코드의 경우, 직교좌표계를 사용하는지, 극좌표계를 사용하는지 알 수 없다. 그럼에도 불구하고 우리는 Point를 사용할 수 있다. 또한 좌표 설정을 할 때엔 두 값을 한 번에 입력하고 조회할 때엔 개별로 읽도록 접근을 강제하기도 한다. 

 

이처럼 구현 내용을 모른 채 자료의 핵심을 조작할 수 있어야 한다.

// case 1
public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getCallonsOfGasoline();
}

// case 2
public interface Vehicle {
    double getPercentFuelRemaining();
}

같은 경우로 case 1보다 case 2가 더 좋다.

 

case 1 두 함수가 변수의 값을 가져오고 있다는 것이 확실하며,

case 2는 연료 상태를 백분율이라는 추상적인 개념으로 알려줄 뿐, 정보가 어디서 오는지 드러나지 않기 때문이다.

 

단순한 인터페이스 활용이나 getter/setter 함수로는 추상화가 이루어지지 않으며, 개발자는 이가 올바르게 이루어지도록 지속적으로 고민해야 한다.

 


자료/객체 비대칭

객체: 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한 것

자료구조: 자료를 그대로 공개하며 별다른 함수는 제공하지 않는 것

두 개념은 상반된다.

 

객체 지향 관점에선 객체가 올바른 구현 방법으로 보이지만, 자료구조가 무조건 좋지 않은 것은 아니다.

예를 들어 아래의 코드는 자료구조 중심 코드이다.

class Square {
    public Point topLeft;
    public double side;
}

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

class Circle {
    public Point center;
    public double radius;
}

class Geometry {
    public final double PI = 3.141592;
    
    public double area(Object shape) throws NoSuchShapeException {
        if (shape instanceof Square) {
            Square s = (Square)shape;
            return s.side * s.side;
        } else if (shape instanceof Retangle) {
            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();
    }    
}

절차적이기에 올바르지 않은 구현 방법으로 보이지만, 만약 둘레를 구하는 perimeter() 함수를 추가하고자 한다면 Geometry class만 건드리면 되므로 도형 클래스는 영향을 받지 않는다.

 

만약 아래와 같이 객체 지향적으로 구현했다면, 모든 클래스에 접근해 값을 수정해야 한다.

class Square implements Shape {
    private Point topLeft;
    private double side;
    
    public double area() {
        return side * side;
    }
}

class Rectangle implements Shape {
    private Point topLeft;
    private double height; 
    private double width;
    
    public double area() {
        return height * width;
    }
}

class Circle implements Shape {
    private Point center;
    private double radius;
    
    public double area() {
        return PI * radius * radius;
    }
}

즉, 자료구조를 사용하는 절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽지만, 새로운 자료 구조를 추가하기는 어렵다.

반면, 객체를 사용하는 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽지만, 새 함수를 추가하기는 어렵다.

 

상황에 맞게 방법을 선택할 수 있어야 한다.

 

 

디미터 법칙

디미터 법칙은 각 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙으로,

객체는 자료를 숨기고 함수를 공개해야 한다는 것이다.

 

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

위 코드가 디미터 법칙을 위반하는 경우로, 반환받는 객체의 자료들을 알게 되기 때문이다.

 

 

기차 충돌

위와 같은 코드를 train wreck이라고 부른다.

dot으로 연결된 것이 마치 기차처럼 보이기 때문이다. 보았을 때 조잡해 보이므로 피하는 것이 좋다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();

그렇다면 위와 같이 수정하면 해결될까?

train wrack은 해결했지만, 해당 함수가 알게되는 정보가 너무 많다.

객체라면 내부 자료를 숨겨야 하기 때문에 디미터 법칙을 위반한 코드이다.

 

 

반면, 아래와 같이 자료구조라면 디미터 법칙이 적용되지 않는다. 

final String outputDir = ctxt.options.scratchDir.absolutePath;

자료구조는 함수없이 공개 변수만 포함하기 때문에 디미터 법칙을 거론할 필요가 없다.

 

 

잡종 구조

이따금 절반은 객체, 절반은 자료구조 형태인 잡종 구조가 등장한다.

이는 새로운 함수를 추가하는 경우, 새로운 자료 구조를 추가하는 경우 모두 어려워지는 단점만 모아놓은 상태이다.

따라서, 되도록 피하는 것이 좋다.

 

 

구조체 감추기

다시 위의 코드로 돌아와서, ctxt, opts, scratchDir이 모두 객체라면 이를 어떻게 해결해야 할까?

가급적이면 함수 이름만으로 동작을 바로 알 수 있도록 추상화하는 것이 중요하다. 

 

이를 위해 위 코드의 존재 이유를 살펴본 결과, 임시 파일을 생성하기 위함이라는 것을 알 수 있었다.

String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);

 

그렇다면, 아래와 같이 코드를 수정할 수 있다.

BufferedOutputStream bos = ctxt.createScrtchFileStream(classFileName);

ctxt 객체에 임시 파일을 생성하라고 시켰다. 객체에게 동작을 시켰고, 내부 동작도 알지 못하므로 좋은 변경이다.

 

 

자료 전달 객체

자료 구조를 때로는 DTO(Data Transfer Object)라고도 하며 이는 매우 유용한 구조체이다.

 

 

결론

객체는 동작을 공개하고 자료를 숨긴다. 자료구조는 별다른 동작 없이 자료를 노출한다.

시스템을 구현할 때 새로운 자료 타입을 추가하고자 한다면 객체가,

새로운 동작을 추가하고자 한다면 자료구조와 절차적 코드가 적합하다.

 

상황에 맞게 최적의 해결책을 선택할 수 있어야 한다.

 

 

견해

이번 장을 읽으면서 객체와 자료구조의 차이점과 그 내용들을 알 수 있었다. 특히, 객체 지향 구현이라고 무조건 좋은 것이 아니며 절차적 구현이라고 무조건 나쁜 것이 아니라는 것을 예제와 함께 보니 크게 와닿았다.

 

다만, 자료구조와 객체를 섞어 사용하는 것이 단점만 모아놓은 방법이라는 점에 대해선 설득되지 않았다. 코드를 작성할 때 동작을 수행하는 class와 DTO의 역할을 이해한 상태에서 이를 적절히 사용해 왔다고 생각하기 때문이다. 되려 장점을 부각했다고 본다.

 

자료를 숨기고 동작을 추상화시키라는 점에 대해선 크게 배웠다. 코드를 구현하다보면 항상 변수명, 메서드명을 고민하는 자신을 발견하게 되는데, 확실히 좋은 이름이 좋은 코드 구현에 있어 큰 비중을 차지한다고 생각한다.