안녕하세요.
오늘은 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. 마틴 저
'객제지향, TDD, 클린코드 > 클린코드' 카테고리의 다른 글
경계 & 일급 컬렉션이란? (0) | 2023.03.24 |
---|---|
클래스는 작아야 한다! (0) | 2023.03.22 |
깨끗한 테스트 코드 유지하기 (0) | 2023.03.17 |
함수는 한 가지만 해라 (0) | 2023.03.16 |
오류 코드보다 예외를 사용하라 (0) | 2023.03.08 |