[클린코드] 객체와 자료구조
01 자료구조 vs 객체
- 자료구조
- 데이터 그 자체
- 자료를 공개한다.
- 변수 사이에 조회 함수와 설정 함수로 변수를 다룬다고 객체가 되지 않는다. (getter, setter)
- 객체
- 비즈니스 로직과 관련
- 자료를 숨기고, 추상화한다.
- 자료를 다루는 함수만 공개한다.
- 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있다.
자료구조 예시
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
public class Car implements Vehicle {
double fuelTankCapacityInGallons;
double gallonsOfGasoline;
public double getFuelTankCapacityInGallons() {
return this.fuelTankCapacityInGallons;
}
public double getGallonsOfGasoline() {
return this.gallonsOfGasoline;
}
}
getter 를 가지고 단순히 데이터를 가져오는 형태를 볼 수 있다.
객체 예시
public interface Vehicle {
double getPercentFuelRemain();
}
public class Car implements Vehicle {
double fuelTankCapacityInGallons;
double gallonsOfGasoline;
public Car(double fuelTankCapacityInGallons, double gallonsOfGasoline) {
if (fuelTankCapacityInGallons <= 0) {
throw new IllegalArgumentException("fuelTankCapacityInGallons must be greater than zero");
}
this.fuelTankCapacityInGallons = fuelTankCapacityInGallons;
this.gallonsOfGasoline = gallonsOfGasoline;
}
public double getPercentFuelRemain() {
return this.gallonsOfGasoline / this.fuelTankCapacityInGallons * 100;
}
}
데이터를 그대로 반환하지 않고 계산을 하고 반환한다.
생성을 할때 데이터의 validation 을 하도록 제어가 되어 있다.
휴대폰 배터리 처럼 실제 수치보단 퍼센트가 중요할 때 사용한다.
자료구조 예시 2
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();
}
}
위 코드는 Geometry 에서 Object 를 받아 각 Square, Rectangle, Circle 의 타입에 맞는 인스턴스일시 계산값을 반환하는 절차적인 코드 형태이며
새로운 자료구조를 추가할 시 조건문 else if 문을 계속 추가해야하는 구조인 절차적인 코드는 새로운 자료구조를 추가하기 어렵다. 함수를 고쳐야한다.
객체 예시 2
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 Cicle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
Shape 가 erea 메소드를 가지고 있기에 상속받은 각 도형 클래스들은 구현해 자료구조 형태보다는 깔끔하게 작성을 할수 있지만
단점은 새로운 함수를 추가시 상속받은 모든 클래스를 고쳐야하는 단점은 있다.
상황에 맞는 선택을 하자
- 자료구조 장점 : 자료구조를 사용하는 절차적인 코드는 기본 자료구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
- 자료구조 단점 : 절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. -> 모든 함수를 고쳐야한다.
- 객체 장점 : 기존 함수를 변경하지 않으면서 새 클래스를 추가 하기 쉽다.
- 객체 단점 : 객체 지향 코드는 새로운 함수를 추가하기 어렵다. -> 모든 클래스를 고쳐야 한다.
자료구조는 단순하게 자료를 담고 있는 건지, 객체처럼 자료는 담고 있지만 비즈니스 로직처럼 풀어내야 할지 판단해서 사용해야 한다.
객체 - 디미터 법칙
클래스 C 의 메소드 f 는 다음과 같은 객체의 메소드만 호출해야한다.
- 클래스 C
- f 가 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
간단하게 이야기 하면 A -> B -> C 상속 구조를 가진 클래스에서
A 는 B 를 호출 할 수 있지만 A 가 C 를 호출 할 수는 없다 라는 것이다.
휴리스틱
경험에 기반하여 문제를 해결하기 위해 발견한 벙법 의사결정을 단순화 하기 위한 법칙들 -> 경험적으로 만들어낸 법칙
많은 사람들이 경험한 것들이 모여 만들어 진 개념이 클린코드이다.
기차 충돌
디미터의 법칙에 어긋나는 상황을 기차 충돌이라고 한다.
자료구조 형태를 객체 지향적으로 변환하기 위해 다음 코드로 생각해볼 필요가 있다.
// 자료구조 - 디미터 법칙 OK
final String outputDir = ctxt.options.scatchDir.absolutePath;
// 객체 - 기차 충돌. 디미터의 법칙 위배
final String outputDir = ctxt.getOptions().getScratchDir().gitAbsolutePath();
// 객체에 대한 해결책이 아니다. getter를 통했을 뿐, 값을 가져오는 것은 자료구조이다.
ctxt.getAbsolutePathOfScratchDirectoryOption();
ctxt.getScratchDirectoryOption().getAbsolutePath();
// 왜 절대 경로를 가져올까.. 근본 원인을 생각해보자!
// 객체는 자료를 숨기고 자료를 다루는 함수만 공개한다.
BufferedOutputStream = ctxt.createScratchFileStream(classFileName);
ctxt 를 통해서 절대경로를 가져오기 위한 목적은 파일을 생성하기 위함이기에 classFileName 인자만 받고 목적에 맞는 함수를 만드는 것이 객체 지향적 코드화 하는 것의 방법이 된다.
DTO (Data Transfer Object) = 자료구조
public class AddressDto {
private String street;
private String zip;
}
public AddressDto(String street, String zip) {
this.street = street;
this.zip = zip;
}
public String getStreet() {
return street;
}
public String setStreet(String street) {
this.street = street;
}
public String getZip() {
return zip;
}
public String setZip(String zip) {
this.zip = zip;
}
DTO 는 대표적인 자료구조 이다.
- 다른 계층 간 데이터를 교환할 때 사용한다.
- 로직없이 필드만 갖는다.
- 일반적으로 클래스명이 Dto(or DTO) 로 끝난다.
- getter/setter 를 갖기도 한다.
- *Beans (Java Beans : 데이터 표현이 목적인 자바 객체)
- 맴버 변수는 private 속성이다.
- getter와 setter 를 가진다.
요즘은 인스턴스 변수를 public 하게 접근 가능하게 많이 사용한다고 한다 특히 kotlin 에서 그렇게 사용한다고 한다.
활성 레코드 Active Record
DTO 의 특수한 형태, 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만, 대개 save 나 find 와 같은 탐색 함수도 제공한다.
Database row 를 객체에 맵핑하는 패턴
보통 많이 알고 있는 Data Mapper 패턴은 Database 에서 조회한 row 에 대해 Mapper 라는 인터페이스로 save 나 find 같은 메소드를 작성하는데 차이가 있다.
책에서는 활성 레코드에 비즈니스 로직을 추가하여 활성레코드 자료구조를 객체로 취급하는 경우를 바람직하지 않다고 한다.
하지만 현업에서는 엔티티에 간단한 메소드를 추가해 사용하는 경우가 많다.