Java/클린코드

[클린코드] 객체와 자료구조

chjs93 2022. 2. 23. 00:34

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 같은 메소드를 작성하는데 차이가 있다.

Active Record vs Data Mapper


책에서는 활성 레코드에 비즈니스 로직을 추가하여 활성레코드 자료구조를 객체로 취급하는 경우를 바람직하지 않다고 한다.

하지만 현업에서는 엔티티에 간단한 메소드를 추가해 사용하는 경우가 많다.