객제지향, TDD, 클린코드/클린코드

객체와 자료 구조

ummchicken 2023. 4. 19. 14:40

안녕하세요.

오늘은 Clean Code의 '객체와 자료 구조'에 대해 학습할 겁니다.

 

 

객체를 추상화하고... 

이러한 과정들이 어렵게 느껴집니다.

(객체지향은 파면 팔 수록 신세계지만, 참 어렵네요)

 

 


 

✔️ 들어가기 전

자료 / 객체

  • 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
  • 자료 구조는 자료를 그대로 공개하며 별다른 함수를 제공하지 않는다.

 

 

 

 

✔️ 자료 추상화

💡 변수를 private으로 하는 이유가 있다.

→ 변수에 의존하지 않게 만들고 싶고, 

변수 타입이나 구현을 맘대로 바꾸고 싶기 때문이다.

 

 

근데 왜, get(조회) 함수와 set(설정)함수를 당연하게 public해서

private 변수를 외부에 노출하는 것일까?

 

 

 

다음 두 클래스의 차이를 비교해보자.

두 클래스 모두 2차원 점을 표현한다.

하지만 한 클래스는 구현을 외부로 노출하고, 다른 클래스는 구현을 완전히 숨긴다.

 

1. 구체적인 Point 클래스

public class Point {
    public double x;
    public double y;
}
  • 확실히 직교좌표계를 사용한다.
  • 또한 개별적으로 좌표값을 읽고 설정하게 강제한다.
  • 이 클래스는 구현을 노출한다.
  • 변수를 prviate으로 설정하더라도, 각 값마다 get함수와 set 함수를 제공한다면, 구현을 외부로 노출하는 셈이다.

 

2. 추상적인 Point 클래스

public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}
  • 이 코드는 점이 직교좌표계를 사용하는지, 극좌표계를 사용하는지 알 길이 없다.
    • 어쩌면 둘 다 아닐수도?
  • 그럼에도 불구하고, interface는 자료구조를 명백하게 표현한다.

 

 

 

변수들 사이에 '함수'라는 계층을 넣는다고 구현이 저절로 감춰지진 않는다.

💡 구현을 감추려면 추상화가 필요하다!

 

🚨 그저 get(조회)함수와 set(설정)함수를 다루지 않는다고 클래스가 되지는 않는다.

→ 그보다는 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야, 진정한 의미의 클래스이다.

 

 

 

예를 들어, 다음 두 클래스를 비교해보자.

1. 구체적인 Vehicle 클래스

public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}
  • 자동차 연료 상태를 구체적인 숫자 값으로 알려준다.
  • 두 함수가 변수값을 읽어 return한다는 사실이 거의 확실하다!

 

2. 추상적인 Vehicle 클래스

public interface Vehicle {
    double getPercentFuelRemaining();
}
  • 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려준다.
  • 정보가 어디서 오는지 전혀 드러나지 않는다.

 

 

위 두 클래스 중, 아래의 추상적인 클래스가 더 좋다.

💡 자료를 세세하게 공개하기 보다는, 추상적인 개념으로 표현하는 편이 좋다.

인터페이스나 get/set 함수마능로는 추상화가 이뤄지지 않는다.
개발자는 객체가 포함하는 자료를 포함하는 자료를 표현할 수 있는 가장 좋은 방법을 고민해야 한다.
아무생각 없이 get/set 함수를 추가하는 방법이 가장 나쁘다.

 

 

 

 

✔️ 자료 / 객체 비대칭

  • 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
  • 자료 구조는 자료를 그대로 공개하며 별다른 함수를 제공하지 않는다.

 

 

다음 클래스는 절차적인 도형 클래스이다.

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();
    }
}
  • Geom-etry 클래스는 세 가지 도형 클래스를 다룬다.
  • 각 도형 클래스는 간단한 자료 구조다.
  • 즉, 아무 메서드도 제공하지 않는다.
  • 도형이 동작하는 방식은 Geometry 클래스에서 구현한다.
  • 새 함수를 추가하고 싶다면, 도형 클래스는 아무 영향도 받지 않는다.
  • 반대로 새 도형을 추가하고 싶다면? Geometry 클래스에 속한 함수를 모두 고쳐야 한다.

 

 

다음은 객체 지향적인 도형 클래스다.

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;
    }
}
  • area()는 다형(polymorphic) 메서드다.
  • Geometry 클래스는 필요 없다!
  • 그러므로 새 도형을 추가해도 기존 함수에 아무런 영향을 미치지 않는다.
  • 반면, 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.

 

 

위 두 코드는 상호 보완적인 특질이 있다. (사실상 반대다.)

그래서 객체와 자료 구조는 근본적으로 양분된다.

 

 

(자료 구조를 사용하는) 절차적인 코드는, 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
반면, 객체 지향 코드는 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.

즉, 

절차적인 코드는, 새로운 자료구조를 추가하기 어렵다.
그러려면 모든 함수를 고쳐야 한다.
ex) 새로운 자료 타입이 아니라, 새로운 함수가 필요한 경우에 절차지향&자료구조 적합


객체지향 코드는 새로운 함수를 추가하기 어렵다.
그러려면 모든 클래스를 고쳐야 한다.
ex) 새로운 함수가 아니라, 새로운 자료 타입이 필요한 경우에 객체지향&클래스 적합

 

 

💡 다시 말해, 객체 지향 코드에서 어려운 변경은 절차적인 코드에 쉬우며, 

절자적인 코드에서 어려운 변경은 객체지향 코드에서 쉽다.

 

 

 

 

✔️ 디미터 법칙

모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙

 

 

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

즉, 객체는 조회 함수로 내부 구조를 공개하면 안 된다는 것이다.

 

 

예를 들어, 다음 코드는 디미터 법칙을 어기는 것으로 보인다.

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
  • getOptions() 함수가 반환하는 객체의 getScratchDir() 함수를 호출한 후 getScratchDir() 함수가 반환하는 객체의 getAbsolutePath() 함수를 호출하기 때문이다.

 

 

 

 

✔️ 자료 전달 객체

(내가 좋아하는 DTO다.)

 

자료 구조체의 전형적인 형태는, 공개 변수만 있고 함수가 없는 클래스다.

💡 이런 자료 구조체를 Data Transfer Object(DTO)라고 한다.

 

 

자바에서 DTO의 일반적인 현태는 '자바 빈(java bean)' 구조다.

 

 

DTO는 굉장히 유용한 구조체다!

  • 데이터베이스와 통신하거나, 소켓에서 받은 메시지의 구문을 분석할 때 유용
  • 흔히 DTO는 데이터베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체다.

 

 

 


 

✔️ 결론

  • 객체는 동작을 공개하고, 자료를 숨긴다.
    • 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 
    • 기존 객체에 새 동작을 추가하기는 어렵다.
    • 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하다면 객체가 더 적합.

 

  • 자료구조는 별다른 동작 없이 자료를 노출한다.
    • 그래서 기존 자료구조에 새 동작을 추가하기는 쉬우나, 
    • 기존 함수에 새 자료 구조를 추가하기는 어렵다.
    • 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면, 자료구조와 절차지향 코드가 더 적합.

 

 

 

 

하지만 우리 모두 편견 없이, 직면한 문제에 최적인 해결책을 선택하자!

 

 

 

 

 

 

 

 

 

 

 

 

참고: Clean Code 클린 코드 애자일 소프트웨어 장인 정신 / 로버트 C. 마틴 저