이 파트를 읽고 머리를 한 대 맞은 듯 싶었다.
모든 클래스에 getter, setter를 습관적으로 사용하는 개발자들이 읽었으면 좋겠다.
객체 vs 자료 구조
객체는 동작을 공개하고 자료를 숨긴다.
자료 구조는 별다른 동작 없이 자료를 노출한다.
getter, setter
객체에서 getter, setter함수를 제공한다면 구현을 외부에 노출하는 걸까? 노출하지 않는 걸까?
정답은 '노출한다'이다.
객체의 멤버 변수 자체에 접근 하는 것은 아니지만, 객체의 변수엔 무엇이 있는지를 외부로 노출한다.
따라서, 구현을 외부로 노출하는 셈이다.
무지성 getter, setter는 나쁘다.
💡 구현을 숨기려면, 추상 인터페이스를 제공해 사용자가 구현을 모른 채 변수의 핵심을 조작할 수 있어야 한다. ➡️ 추상적인 개념으로 표현하는 연습을 하자.
항상 객체 지향 프로그래밍이 옳을까?
객체 지향적 코드와 절차 지향적 코드를 봐보자.
객체 지향적 코드
// 객체지향적 접근 (새로운 도형 추가가 쉬움)
interface Shape {
double getArea();
double getPerimeter();
}
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
@Override
public double getPerimeter() {
return 2 * (width + height);
}
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public double getPerimeter() {
return 2 * Math.PI * radius;
}
}
// 새로운 도형 추가가 쉬움
class Triangle implements Shape {
private double a, b, c; // 세 변의 길이
public Triangle(double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public double getArea() {
// 헤론의 공식 사용
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
@Override
public double getPerimeter() {
return a + b + c;
}
}
절차 지향적 코드
// 절차지향적 접근 (새로운 연산 추가가 쉬움)
class ShapeCalculator {
// 도형 타입을 나타내는 enum
enum ShapeType {
RECTANGLE,
CIRCLE
}
// 넓이 계산
public static double calculateArea(ShapeType type, double... params) {
switch (type) {
case RECTANGLE:
return params[0] * params[1]; // width * height
case CIRCLE:
return Math.PI * params[0] * params[0]; // PI * radius^2
default:
throw new IllegalArgumentException("Unknown shape type");
}
}
// 둘레 계산
public static double calculatePerimeter(ShapeType type, double... params) {
switch (type) {
case RECTANGLE:
return 2 * (params[0] + params[1]); // 2 * (width + height)
case CIRCLE:
return 2 * Math.PI * params[0]; // 2 * PI * radius
default:
throw new IllegalArgumentException("Unknown shape type");
}
}
// 새로운 연산 추가가 쉬움 (예: 대각선 길이 계산)
public static double calculateDiagonal(ShapeType type, double... params) {
switch (type) {
case RECTANGLE:
return Math.sqrt(params[0] * params[0] + params[1] * params[1]);
case CIRCLE:
return 2 * params[0]; // diameter
default:
throw new IllegalArgumentException("Unknown shape type");
}
}
}
객체 지향적 코드에 다른 도형을 추가하거나, 절차 지향적 코드에 다른 함수를 추가하기는 쉽다.
하지만 객체 지향적 코드에 다른 함수를 추가하려면 모든 도형을 수정해야 하고,
절차 지향적 코드에 다른 도형을 추가하려면 모든 함수를 수정해야 한다.
Java는 객체 지향 프로그래밍 언어이기 때문에 거의 대부분 객체 지향적으로 코드를 작성하겠지만, 상황에 따라 절차 지향적 코드 작성도 고려해보자.
| 객체 지향 | 절차 지향 | |
| 장점 | 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉬움 | 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉬움 |
| 단점 | 새로운 함수 추가 어려움 | 새로운 자료 구조 추가 어려움 |
디미터 법칙
디미터 법칙은 '모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙'이다.
다시 말하면, '클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.'이다.
- 클래스 C
- f가 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
이해가 안가 예시를 작성해 보았다. 참고하자.
class UserService { // 클래스 C
private void validateUser() { ... } // C의 메서드
public void processUser() { // 메서드 f
validateUser(); // 자신의 메서드 호출 OK
}
}
------------------------------------------------------------------------------------
class OrderService {
public void processOrder() { // 메서드 f
Order newOrder = new Order(); // f가 생성한 객체
newOrder.validate(); // 직접 생성한 객체의 메서드 호출 OK
}
}
------------------------------------------------------------------------------------
class PaymentService {
public void processPayment(Payment payment) { // 메서드 f, payment는 인수
payment.execute(); // 인수로 받은 객체의 메서드 호출 OK
}
}
------------------------------------------------------------------------------------
class CustomerService {
private Database database; // C의 인스턴스 변수
public void updateCustomer() { // 메서드 f
database.save(); // 인스턴스 변수에 저장된 객체의 메서드 호출 OK
}
}
자료 전달 객체
DTO는 공개,비공개 변수만 있고 함수가 없는 클래스다. Spring boot로 개발하며 DB와 통신할 때, 많이 사용한 기억이 있다.
활성 레코드라는 DTO가 있다. 이 녀석 변수의 조회/설정 함수와 함께 save, find와 같은 탐색 함수도 제공하는 경우가 있다.
주의 할 것은 활성 레코드에 비즈니스 로직을 추가해 자료 구조를 객체로 취급하는 것이다. 이는 자료 구조도 아니고 객체도 아닌 잡종 구조가 나오기 때문이다.
활성 레코드는 자료 구조로 취급하고 비즈니스 로직을 담으면서 활성 레코드의 인스턴스를 숨기는 객체를 따로 생성하자.
'Clean Code' 카테고리의 다른 글
| [Day 6] 경계 (1) | 2024.10.29 |
|---|---|
| [Day 5] 오류 처리 (0) | 2024.10.29 |
| [Day 3] 형식 맞추기 (0) | 2024.10.24 |
| [Day 2] 함수 (1) | 2024.10.23 |
| [Day 1] 의미 있는 이름 (3) | 2024.10.22 |