[Clean Code] Ⅵ. 객체와 자료구조

Clean Code 3판을 읽고 정리한 글입니다

Ⅵ. 객체와 자료구조

Intro

변수를 비공개private로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶어서다. 충동이든 변덕이든, 변수 타입이나 구현을 맘대로 바꾸고 싶어서다. 그렇다면 어째서 수많은 프로그래머가 조회get 함수와 설정set 함수를 당연하게 공개public해 비공개 변수를 외부에 노출할까?

  • 인트로가 괜춘하다
  • 한번 생각해보자.. 일단 캡슐화
  • 캡슐화 - 외부에 들어나지 않도록
  • 다수가 함께 작업하는 프로젝트의 경우 해당 자료를 보호할 수 있도록 자료 보호

자료 추상화

6-1 구체적 Point 클래스 : 구현 회부 노출
1
2
3
4
public class Point { 
public double x;
public double y;
}
6-2 추상적인 Point 클래스 : 구현을 완전히 숨긴다
1
2
3
4
5
6
7
8
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}

인터페이스 포인트(6-2)

  • 어떤 좌표계인지는 알 수가 없다
  • 자료구조를 명백히 표현하며 그 이상도 표현한다
    • 클래스 메서드가 접근 정책 강제
    • 좌표를 읽을 때는 각 값을 개별적으로 읽어야 한다
    • 좌표 설정시에는 두 값을 한꺼번에 설정해야 가능

클래스 포인트(6-1)

  • 확실하게 직교좌표계를 사용
  • 개별적으로 좌표값 읽고 설정하게 강제
  • 구현을 노출 : 변수는 private인데 getter,setter가 있어도 마찬가지로 외부 노출로 본다

6-2 > 6.1

진정한 클래스

  • 형식 논리에 치우쳐 조회 함수와 설정 함수로 변수를 다룬다고 클래스가 되지 않는다
  • 변수 사이에 함수 계층이 들어간다고 구현이 저절로 감춰지지 않는다
  • 진정한 의미의 클래스 : 추상 인터페이스를 제공해서 사용자가 구현을 모르는 채 자료의 핵심 조작 가능
  • 추상적인 개념으로 표현 >>> 자료 세부 공개하기
  • 인터페이스나 getter,setter 설정으로 추상화가 되지 않음
  • 생각없는 getter/setter를 추가하는 방법이 가장 나쁘다
  • 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다

자료/객체 비대칭

특징 : 사소한 차이지만 영향은 엄청남

  • 객체 : 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개
  • 자료 구조 : 자료를 그대로 공개하며 별다른 함수를 제공하지 않음
procedural Shape
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
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();
}
}

procedural Shape

  • GeoMetry 클래스는 3가지 도형 클래스 다룬다
  • 각 클래스는 간단한 자료 구조, 메서드 X
  • 도형이 동작하는 방식 GeoMetry클래스에서 구현
  • 객체 지향에서는 상상할 수 없는 클래스
  • 변화시?
    • Geometry에 둘레 길이 구하는 함수 추가? → 3가지 도형 클래스는 아무 영향이 없음
    • 반대로 새 도형을 추가하고 싶다면? → Geomtry의 함수를 모두 고쳐야 함
Polymorphic Shape
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
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;
}
}

Polymorphic Shape

  • 객체지향적
  • area() : polymorphic method
  • Geomtry클래스 필요없음
  • 변화시?
    • 새 도형을 추가하고 싶다면? → 기존 함수 영향 zero
    • Geometry에 둘레 길이 구하는 함수 추가? → 3가지 도형 클래스 모두 뜯어 고쳐야 함

해결법 ?VISIOR 패턴?

  • 주로 상속없이 클래스에 메서드를 효과적으로 추가하기 위해 사용
  • 합성 객체의 내부 구조가 VISITOR에 Open됨 → 캡슐화 위반 문제점

procedural Shape VS Polymorphic Shape
상호 보완적이며 사실상 반대 → 객체와 자료구조는 근본적으로 양분됨
서로의 장점

  • (자료구조를 사용하는) 절차적 코드 : 기존 자료 구조 변경 없이 새함수 추가
  • OOP 코드 : 기존 함수 변경 없이 새 클래스 추가 쉬움
    서로의 단점
  • (자료구조를 사용하는) 절차적 코드 : 새로운 자료 추가 어려움(모든 함수 수정 필요)
  • OOP 코드 : 새로운 함수를 추가하기 어려움(모든 클래스를 고쳐야 함)
    결론
  • 모든 것이 객체라는 생각은 미신
  • 때로는 단순한 자료 구조+절차적 코드가 가장 적합한 상황도 존재

디미터 법칙

상세 내용은 실용주의 프로그래머 227 p

  • 잘 알려진 휴리스틱
    • 휴리스틱 : 경험에 기반하여 문제를 해결하거나 학습하거나 발견해 내는 방법
  • 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다
    • 객체는 자료를 숨기고 함수를 공개
    • 즉 조회 함수로 내부 구조를 공개하면 안된다는 의미
    • ex) 모든 getter 사용? 내부 구조가 노출되므로 안된다는 뜻
  • 클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다
    • 클래스 C
    • f가 생성한 객체
    • f 인수로 넘어온 객체
    • C 인스턴스 변수에 저장된 객체
    • 주의: 위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출 X

train wreck = 기차 충돌

아파치 프레임워크의 train wreck처럼 보이는 함수
1
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
  • 위와 같은 코드는 디미터 법칙이 안지켜지는 것으로 보여짐

  • ctxt가 Options를 포함 → Options가 ScratchDir 포함 → ScratchDir이 AbsolutePath를 포함
    →함수가 하나가 아는 지식이 굉장히 많음 → 해당 함수가 많은 객체를 탐색 가능함

    이렇게 나누는 것이 좋다
    1
    2
    3
    Options opts = ctxt.getOptions();
    File scratchDir = opts.getScratchDir();
    final String outputDir = scratchDir.getAbsolutePath();

    첫번째 예제가 디미터 법칙을 위반했는가?

  • ctx, Options, ScratchDir이 객체인가 자료구조인지에 따라 달림

    • 객체라면? 내부구조를 숨겨야하므로 확실하게 디미터 법칙 위반
    • 자료구조라면? 당연히 내부 구조를 노출해야함 → 디미터 법칙 적용 안됨
  • 첫번째 예제의 혼란의 이유? Getter 사용 덕분

    이런식으로 구현되면 디미터 법칙을 거론할 필요가 없다
    1
    final String outputDir = ctxt.options.scratchDir.absolutePath;
  • 자료구조는 무조건 함수없이 공개 변수만 포함, 객체는 비공개 변수+공개함수를 포함하면 문제가 간단해진다 ‘

잡종구조

  • 단순한 자료 구조에도 getter,setter를 요구하는 프레임워크나 표준(bean등;)이 존재 → 원흉
  • 이런 혼란으로 때때로 절반은 객체, 절반은 자료구조인 잡종 구조가 등장
  • 잡종 구조는 중요기능 수행 함수 + 공개변수 + getter,setter도 존재
  • getter,setter덕에 미공개 변수를 사용하고픈 유혹에 빠지기 쉬움 → 리팩토링 책에서는 기능 욕심(Feature Envy)라고 함
  • 새로운 함수는 물론이고 새로운 자료구조도 추가하기 어려움 → 양쪽의 단점만 모아놓은 구조
  • 잡종 구조는 되도록 피하는 편이 좋다
  • 프로그그래머 함수/타입을 보호할지 공개할지 확신하지 못하거나(아니면 무지해서) 어중간하게 내놓은 설계에 불과

구조체 감추기

1
2
3
4
//1
ctxt.getAbsolutePathOfScratchDirectoryOption();
//2
ctxt.getScratchDirectoryOption().getAbsolutePath();

문제점 : 둘 다 좋은 방법이 아님

  • 1번은 ctxt객체에 공개해야하는 메서드가 너무 많아짐
  • 2번은 getScratchDirectoryOption()가 객체가 아닌 자료구조 반환임을 가정

ctxt가 객체라면 무언가를 해라 라고 해야지 속을 드러내라고 하면 안됨

  • 임시 디렉토리의 절대경로가 왜 필요한가? 절대경로를 얻어 어디에 쓸것인가?
    한참 아래의 같은 모듈에서 가져온 코드
    1
    2
    3
    4
    5
    6
    // 추상화 수준이 뒤섞여서 불편
    // 점과 슬래시와 파일 확장자등이 부주의하게 섞여있음
    String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
    FileOutputStream fout = new FileOutputStream(outFile);
    BufferedOutputStream bos = new BufferedOutputStream(fout);
    // 결론 : 임시파일을 생성하기 위해 절대 경로가 필요함
    ctxt 객체에 임시 파일을 생성하라고 시키면 어떨까?
    1
    BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
  • 객체에 맡기기 적당한 임무로 보임
  • ctxt는 내부 구조를 드러내지 않으며, 모듈은 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.
  • 따라서 디미터 법칙을 위반하지 않는다.

자료 전달 객체

자료 구조의 전형적인 형태 : DTO

  • 공개 변수만 존재 , 함수 없음
  • DTO라고도 한다
  • 굉장히 유용한 구조체 : DB 통신, 소켓 메시지 구문 통신
  • 흠히 DB의 가공되지 않은 정보를 어플리케이션 코드의 객체로 변환과정에서 가장 처음으로 사용하는 구조
  • getter/setter를 가진다

좀더 일반적인 형태 : bean 구조

  • 데이터 표현이 목적인 자바 객체
  • private 변수 + getter, setter
  • 일종의 사이비 캡슐화, 별다른 이득이 없음
  • zh

활성 레코드 : DTO의 특수 형태

  • 위의 dto나 bean구조의 자료구조 + 탐색 함수(save or find) 제공
  • DB 테이블이나 다른 소스에서 자료를 직접 변환한 결과 → DB row를 객체에 매핑하는 패턴
  • 엔티티와 다르게 생겼음~
  • 불행히도 활성 레코드에 비지니스 규칙 메서드를 추가한 자료구조를 객체 취급하는 개발자가 흔함
  • 잡종 구조가 나오므로 바람직하지 않음
  • 해결책
    • 활성 레코드는 자료 구조로 취급해야한다
    • 비지니스 규칙을 담으면서 내부 자료를 숨기는 객체(엔티티등)는 따로 생성
  • 객체가 많아지면 복잡해짐, 가까운 곳에 관련 로직이 있는 것이 좋음
    → 현업에서는 엔티티에 간단한 메서드를 추가해 사용
    → 현업에서는 엔티티에 간단한 메서드를 추가해 사용

결론

객체는 동작을 공개하고 자료를 숨긴다.
그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.

자료 구조는 별다른 동작 없이 자료를 노출한다.
그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기는 어렵다.

  • (어떤) 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다.
  • 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
  • 우수한 소프트웨어 개발자는 편견 없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.
  • 김과장이 햇갈려서 질의 응답에 많은 시간을 허비
    • 디미터의 법칙을 객체가 아닌 메서드의 관점으로 착각해서 체인메서드들을 쓰면 안되는 것으로 착각함
    • 객체의 관점으로 봐야 함을 설명하는데도 이해를 못해서 많은 시간 허비
  • 다음 번 알려줄 때에는 해당 부분을 햇갈리지 않도록 부연 설명을 미리 준비 해야 할 듯

Related POST

공유하기