6. 객체와 자료 구조

Posted by yunki kim on January 17, 2022

  객체지향 패러다임에서 변수를 private으로 하는 이유는 변수에 의존하는 것을 막기 위해서이다. 그렇다면 왜 수많은 개발자들이 접근자와 수정가를 public으로 사용할까?

자료 추상화

  변수를 private으로 선언하고 각 값마다 접근자와 수정자를 만든다면 이것은 진정한 캡슐화가 아니다. 변수 사이에 함수라는 계층을 넣는것 만으로는 구현이 감추어지지 않는다. 진정한 캡슐화를 위해서는 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 한다. 즉, 자료를 세세하게 표기하기 보다는 추상적인 개념으로 표기해야 한다.

  이런 논리에 의거해 다음 두 인터페이스를 보면 두번째 인터페이스가 더 객체지향 스럽다는 것을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
// 자동차 연료 상태를 구체적인 숫자 값으로 알려준다
public interface Veichle {
    double getFuleTankCapacityInGallons();
    double getGallonsOfGasoline();
}
 
// 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려준다.
public interface Vehicle {
    double getPercentFuelRemaining();
}
cs

 

자료/객체 비대칭

  객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.

  자료 구조는 자료를 그대로 공개하고 별다른 함수를 제공하지 않는다.

  이 둘의 차이는 다음과 같은 영향을 미친다. 도형 관련 코드를 보자, 하나는 절차지향이고 하나는 객체지향이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 절차지향
 
public class Square {
    public Point topLeft;
    public double side;
}
 
public class Rectabgle {
    public Point topLeft;
    public double height;
    public double width;
}
 
public class Cirlce {
    public Point center;
    public double radius;
}
 
public class Geometry {
    public final double PI = 3.14;
 
    public double area(Object shape) throws NoSuchShapeException {
        if (shape instanceof Square) {
            Square s = (Square)shape;
            return s.side * s.side;
        } else if (shape instanceof Rectabgle) {
            Rectabgle r (Rectabgle)shape;
            return r.height * r.width;
        } else if (shape instanceof Cirlce) {
            Cirlce c = (Cirlce)shape;
            return PI * c.radius * c.radius;
        }
        throw new NoSuchShapeException();
    }
}
cs

  만약 위 코드에서 Geometry에 메서드를 추가할려 해도 도형 클래스는 아무 영향도 받지 않는다. 반대로 새로운 도형을 추가한다면 Geometry에 속한 메서드를 모두 수정해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 객체지향
 
public interface Shape {
    double area();
}
 
public class Square implements Shape {
    private Point topLeft;
    private double side;
 
    @Override
    public double area() {
        return side * side;
    }
}
 
public class Rectangle implements Shape {
    private Point topLeft;
    private double height;
    private double width;
 
    @Override
    public double area() {
        return height * width;
    }
}
 
public class Circle implements Shape {
    private Point center;
    private double radius;
    private final double PI = 3.14;
 
    @Override
    public double area() {
        return PI * radius * radius;
    }
}
cs

  반면 객체지향은 새로운 도형 추가에 영향을 받지 않지만 메서드 추가에 영향을 받는다.

  따라서 절차적인 코드는 새로운 자료 구조를 추가하기 위해 모든 함수를 고쳐야 하기 때문에 추가하기 어렵다. 반면 객체지향은 새로운 함수를 추가하기 위해 모든 클래스를 고쳐야 하기 때문에 새로운 함수를 추가하기 어렵다.

  복잡한 시스템에서 새로운 자료 타입이 필요하다면 이때는 객체지향이 적절하다. 반면, 새로운 함수가 필요하다면 이때는 절차적인 코드가 적절하다. 모든 것이 객체라는 것은 미신이다. 때로는 단순한 자료 구조와 절차적인 코드가 가장 접합한 상황도 있다.

 

디미터 법칙

  디미터 법칙은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다. 즉, 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안된다. 예를 들어 다음 코드에서 ctxt과 반환값이 객체라면 디미터 법칙을 위반하고 있다. 

1
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
cs

  기차 충돌

    위와 같은 코드는 기차 충돌(train wreck)이라는 문제를 가지고 있다. 여러 객체가 한 줄로 이어진 기차처럼 보이기 때문이다. 따라서 위 코드를 다음과 같이 나누는 편이 좋다.

1
2
3
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
cs

  위의 코드에서 ctxt와 반환값이 객체이면 당연히 디미터 법칙을 위반한다. 하지만 이들이 자료구조라면 내부 구조를 노출하는 것이 당연하므로 디미터 법칙을 위반하지 않는다. 또 한 자료구조라면 접근자를 사용하는 것이 혼란을 야기하므로 다음과 같이 수정하는 편이 좋다.

1
final String outputDir = ctxt.options.scratchDir.absolutePath;
cs

  잡종 구조

    위의 코드 같은 혼란으로 인해 결국에는 절반은 자료구조, 절반은 객체인 잡종 구조가 나올 수 있다. 이런 잡종 구조는 새로운 함수, 새로운 자료구조를 추가하기 어렵게 만든다. 따라서 잡종 구조는 되도록 피하는 것이 좋다.

 

자료 전달 객체

  자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 전달 구조체는 자료 전달 객체(Data Transfer Object - DTO)라고 부른다. DTO는 DB와 통신하거나 소켓에서 받은 메시지의 구문을 분석할 때 유용하다. DTO는 DB에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체다.

  좀 더 일반적인 형태는 빈(bean)구조다. 빈은 private 변수를 접근자, 수정자로 조작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// DTO 예시
public class Address {
    private String street;
    private String streetExtra;
    private String city;
    private String state;
    private String zip;
 
    public address(String street, String streetExtra, 
            String city, String state, String zip) {
 
        this.street = street;
        this.streetExtra = streetExtra;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }
 
    public String getStreet() {
        return street;
    }
 
    public String getStreetExtra() {
        return streetExtra;
    }
 
    public getCity() {
        return city;
    }
 
    public String getState() {
        return state;
    }
 
    public String getZip() {
        return zip;
    }
}
cs

  활성 레코드

    활성 레코드는 DTO의 특수한 형태이다. 활성 레코드는 공개 변수가 있거나 비공개 변수에 접근자, 수정자가 있는 자    료구조지만, 대개 save나 find 같은 탐색 함수도 제공한다. 활성 레코드는 DB 테이블이나 다른 소스에서 자료를 직접    변환한 결과다.

    활성 레코드는 자료 구조다. 따라서 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성해야 한다.

 

출처 - 클린 코드