모르지 않다는 것은 아는것과 다르다.

Books

클린코드 6장 (객체와 자료구조)

채마스 2022. 4. 23. 21:26

개요

  • 이번 장에서는 객체와 자료구조의 차이를 설명한다.
  • 또한 변수를 비공개(private) 으로 정의하는 이유, 많은 프로그래머가 getter 와 setter 를 공개(public)해 비공개 변수를 외부로 노출하는 이유에 대해서 설명한다.

 

추상화

  • 추상화란 무엇일까?
    • 컴퓨터 과학에서 추상화는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
  • 즉, 추상화란 자료에 대한 핵심적인 부분을 간추려 내는 것을 말한다.
  • 추상화를 통해서 개발자(사용자)는 세세한 구현 부분을 몰라도 기능을 사용할 수 있게 하는 것이다.

 

자료 추상화

  • 먼저 아래와 같은 코드가 있다고 해보자
public class Point {
  public double x;
  public double y;
}
  • 위의 코드를 보면 변수들이 public으로 되어있다. -> 그렇기 때문에 사용자는 해당 클래스에 대한 변수들을 전부 알 수 있다.
  • 이것은 추상화에 위배되는 구현이다.
  • 반면에 아래의 코드를 보자
public interface Point {
  double getX();
  double getY();
  void setCartesian(double x, double y);
  double getR();
  double getTheta();
  void setPolar(double r, double theta);
}
  • 위의 코드는 Point 클래스를 추상화 했다고 볼 수 있다.
  • 클래스 메서드가 접근을 강제해서, 공개나 변경을 원하는 자료 구조와 기능만을 간추려서 공개했다.
  • 그렇다면 위의 코드는 좋은 추상화인가? -> 아니다 -> 각 값마다 getter 와 setter 를 제공한다면 이전과 마찬가지로 구현을 외부로 노출시키는 것과 같다.
  • 다시말해 변수 사이에 함수라는 계층을 넣는다고 해서 구현이 저절로 감춰지진 않는다.
  • 그렇기 때문에 그저 형식과 논리에 치우쳐 Getter 와 Setter 함수로 변수를 다룬다고 클래스가 되진 않는다.
  • 핵심은, 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스이다.
  • 따라서 아래와 같이 위의 코드를 개선할 수 있다.
public interface Point {
  void getCartesian(double x, double y);
  void setCartesian(double x, double y);
}
  • 위의 코드는 정보가 어디서 오는지 전혀 드러나지 않는다.
  • 정리하면, 자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다.
  • 또한, 인터페이스나 조회/설정 함수만으로는 추상화가 이뤄지지 않는다.

 

자료/객체 비대칭

  • 객체와 자료구조의 차이를 아는가?
  • 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만을 제공한다.
  • 자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
  • 비교를 위해서 먼저 아래의 코드를 보자.
// 절차지향적인 코드
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(); 
    }
}
  • 위의 코드는 3가지 도형을 절차 지향적인 코드이다.
  • 도형의 동작은 Geometry 클래스 에서 구현한다.
  • 객체 지향으로 개발하는 나와 같은 프로그래머는 이런 코드를 보고 비웃을 지도 모른다.
  • 하지만 위와 같은 코드가 무조건 비웃음의 대상이 된다고 할 순 없다.
  • 그 이유는 만약 도형의 동작이 추가된다고 했을때 도형클래스에는 수정할 필요가 없기 때문이다. -> 장점이다.
  • 하지만, 도형을 추가할 때는 모든 함수를 수정해야 된다는 단점이 있다.
  • 이번에는 위와 상반되는 객체 지향적인 코드를 보자.
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;
    } 
}
  • 위의 코드는 Geometry 클래스는 존재하지 않는다. -> 다형성(다형 메소드)이 적용되었기 때문이다.
  • 그렇기 때문에 새 도형을 추가해도 기존 함수에 영향을 미치지 않는다.
  • 하지만, 새 함수를 추가할 때에는 도형 클래스를 전부 고쳐야 한다.
  • 그렇다면, 절차적인 코드와 객체 지향적인 코드중 어떤것이 좋은가?
  • 정리하자면 아래와 같다.
    • 새로운 자료 타입은 잘 추가되지 않고, 새로운 함수가 자주 추가되는 경우엔 절차적인 코드가 유라하다.
    • 새로운 함수보다는 새로운 자료 타입이 필요한 경우에는 객체지향적인 코드가 유리하다.
  • 개발을 하다보면 객체지향적인 코드가 유리한 경우가 많으나, 모든 문제를 객체로 해결하려고 하는것은 바람직 하지 못하다.
  • 적어도 차이를 알고 객체지향적으로 코딩하는 것과 무작정 객체지향적으로 코딩하는 것은 큰 차이가 있다.

 

디미터 법칙

  • 디미터 법칙이란 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
  • 최소 지식의 원칙이라고도 불린다.
  • 결합도가 낮은 설계를 위한 법칙이다.
  • 클래스 C의 메서드의 f라는 메서드는 다음과 같은 객체의 메서드만 호출할 수 있다.
    • 클래스 C
    • f가 생성한 객체
    • f 인수로 넘어온 객체
    • C 인스턴스 변수에 저장된 객체
  • 다시 말해서, 낯선 사람은 경계하고 친구랑만 놀라는 의미이다.
  • 즉, A가 B를 사용하고, B가 C를 사용한면, A는 C의 메소드는 호출할 수 있지만 C를 알 필요는 없다.
  • 따라서 아래와 같은 코드는 좋지 않다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
  • 오히려 아래와 같이 나누는 것이 좋다.
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
  • 하지만, 위의 코드도 객체의 입장에서는 디미터의 법칙을 지켰다고 할 수 없다. -> 내부의 정보를 너무 많이 노출하기 때문이다.
  • 그렇지만, 자료구조의 측면에서는 디미터의 법칙을 지켰다고 할 수 있다.
  • 그렇다면 위의 코드를 객체지향적인 측면에서 디미터의 법칙을 지키려면 어떻게 해야할까? -> 구조체를 감추면 된다.
  • 객체라면 내부 구조를 감춰야 하니 때문에 아래와 같이 구현할 수 있다.
ctxt.getAbsolutePathOfScratchDirectoryOption();
  • 하지만 ctxt 객체에 공개해야 하는 메서드가 너무 많아지기 때문에 좋은 방법은 아니다.
  • 절대 경로를 얻으려는 이유는 임시 파일을 생성하기 위함이다.
  • 그렇기 때문에 적절하게 classFileName 만들고, 아래와 같이 ctxt 객체에 임시 파일을 생성하라고 시키는 것이 낫다.
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
  • 이렇게 되면, ctxt는 내부 구조를 드러내지 않는다.
  • 또한, 모듈에서 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.
  • 그렇기 때문에 디미터 법칙을 위반하지 않는다.

 

활성 레코드

  • DTO의 특수한 형태이다.
  • 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료구조이다.
  • save나 find와 같은 탐색 함수도 제공한다.
  • 하지만 save 나 find 와 같은 메소드가 포함되어 있다고 해서 다른 비즈니스 메서드를 추가해서 사용하는 것은 바람직하지 않다.
  • 이런실수로 인해서 아래와 같이 잡종코드가 생겨난다.
class PersonDto {
    private String name;
    private String email;

    public PersonDto(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // save, find 메서드 생략

    // 비즈니스 메서드
    public void sendEmail(){
        ...
    }    
}
  • 해결책은 활성 레코드를 자료 구조로 취급하는 것이다.
  • 그 다음 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성하면 된다.
class EmailSender{
    private PersonDto personInfo;

    public void sendEmail() {
        //...
    }
}




REFERENCES

  • 클린코드 6장

'Books' 카테고리의 다른 글

클린코드 9장 (단위 테스트)  (0) 2022.04.30
클린코드 8장  (0) 2022.04.23
클린코드 4장 (주석)  (0) 2022.04.23
클린코드 3장 (함수)  (0) 2022.04.17
클린코드 1장, 2장  (0) 2022.04.17