Clean Code & Refactoring

[Clean Code] 6장. 객체와 자료구조 : Objects and Data Structures

유자맛바나나 2022. 7. 11. 03:57

[Clean Code 시리즈 포스팅]

[Clean Code] 1장. 깨끗한 코드: Clean Code

[Clean Code] 2장. 의미 있는 이름 : Meaningful Names

[Clean Code] 3장. 함수 : Functions

[Clean Code] 4장. 주석 : Comments

[Clean Code] 5장. 형식 맞추기 : Formatting

[Clean Code] 6장. 객체와 자료구조 : Objects and Data Structures (Now)

[Clean Code] 9장. 단위 테스트 : Unit Tests

 

 

❑ 들어가기에 앞서

  • 본 장의 제목은 '객체'와 '자료구조'다. 책의 내용을 보기에 앞서 필자가 말하는 객체와 자료구조에 대해 먼저 이해하면 도움이 될 것이다.
  • 필자가 본 장에서 말하고자 하는 것은 객체는 객체 답게, 자료구조는 자료구조 답게 만들라는 것이다. 객체 답게, 자료구조 답게 만들려면 객체와 자료구조로서 갖춰야 할 것들을 잘 알아야 한다. 본 장에서는 각각이 갖춰야 할 것이 무엇인지를 설명한다.

 

1. 객체

  • 저자가 말하는 객체는 역할과 책임을 갖고 외부에 공개하지 않는 데이터를 이용해 사용자(클라이언트 객체)에게 기능을 제공하는 클래스를 의미한다.
  • 객체는 동작을 공개하고 데이터를 숨긴다(캡슐화). 여기서 데이터는 보통 클래스 내부에 private으로 정의된 인스턴스 변수를 의미한다.

2. 자료구조

  • 자료구조는 보통 데이터와 몇 가지 동작만 갖고 있고 별 다른 기능을 제공하지 않는다.
  • DTO가 자료구조의 대표적인 예시다.

 

 

❑ 자료 추상화

1. Question: 변수를 private으로 정의하는데 왜 수많은 프로그래머는 getter/setter를 당연하게 public으로 공개할까?

  • private으로 변수를 정의하는 이유는 "남들(=클라이언트 객체)이 변수에 의존하지 않게 만들고 싶어서"다. 해당 이유는 객체와 자료구조를 구분하는데 있어 중요한 이유이니 기억해야 한다.
  • 저자는 변수를 private으로 정의해 캡슐화하고, 역할을 나눴다고 말하면서 정작 getter/setter를 무지성적으로 공개하면 변수를 public으로 선언해 제공하는 것과 다를 바 없고, 결과적으로 객체의 변수를 직접 조작하는 것에 의존하도록 만든다고 비판하는 것이다.

 

2. Question: 변수에 직접 접근(의존)하는 것이 왜 안좋을까?

  • 객체지향에서 '객체'는 포스팅 초반에 설명했듯 역할과 책임을 갖고, 외부에 공개하지 않는 데이터를 이용한 기능을 제공한다.
  • 클라이언트가 변수(=데이터)에 직접 접근할 수 있다면 객체에서 제공하는 기능을 통하지 않고 변수를 직접 조작해 해결하려는 것에 의존할 수 있다. 따라서 해당 객체의 역할과 책임이 무너질 수 있다.

 

3. 추상화가 답이다!

  • 따라서 저자는 변수를 공개하는 것을 최대한 지양하고, 변수들을 활용한 기능들을 잘 정의한 인터페이스를 제공해야 한다고 주장한다. 
  • 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다.
  • 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.

Example Code1

#Bad Case

  • 메서드 명을 잘 살펴보면 클래스 내 변수를 그대로 제공할 것으로 판단할 수 있다. 객체로서 역할과 책임을 수행하려면 데이터를 활용한 기능을 제공해야 한다.
public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}

#Good Case

  • Vehicle 클래스 내의 변수들을 활용해 연료를 백분율로 환산하는 기능을 제공하고 있다.
  • 뿐만 아니라 추상화된 interface로 제공하기 때문에 클라이언트 객체는 구현을 파악할 수 없다.
public interface Vehicle {
    double getPercentFuelRemaining();
}

 

Example Code2

  • 아래 Bad/Good Case는 모두 위에서 설명한 객체가 아닌 자료구조로 볼 수 있다.
  • 저자는 본 예제에서 자료구조 역시 동작을 제공하므로 추상화를 통해 내부 동작을 감춰 클라이언트가 자료를 조작하도록 만들어야 한다고 설명한다. 

#Bad Case

  • 변수에 직접 접근해 변경할 수 있도록 public으로 설정되어 있어 클라이언트 객체가 변수에 의존할 수 있다
public class Point {
    public double x;
    public double y;
}

#Good Case

  • x, y, r 등 변수에 직접 set 할 수 없어 마음대로 값을 조작하지 못해 변수에 의존할 가능성을 낮춘다.
  • 대신 setCartesian과 같은 메서드를 통해 x, y를 설정할 수 있는데 이는 정책적으로 Cartesian 좌표계로 설정을 강제하는 것이기 때문에 객체로서 역할을 수행하고 있다고 볼 수 있다.
public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}

 

❑ 자료/객체 비대칭

  • 자료/객체 비대칭에서는 [절차적인 코드 + 자료구조]와 [객체지향 코드 + 객체]의 차이점에 대해 설명한다.
  • 저자는 책에서 다음과 같이 말한다. "분별 있는 프로그래머는 모든 것이 객체라는 생각이 미신임을 잘 안다. 때로는 단순한 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다". 즉, 객체지향적 개발이 모든 상황에서 최고의 해법이 아니기 때문에 특징을 잘 활용해 때로는 절차적인 코드를 작성할줄도 알아야한다는 뜻이다. 

 

1. [절차적인 코드 + 자료구조]

Example Code

  • Square와 Circle는 간단한 자료구조다. 즉, 아무 메서드도 제공하지 않는다. 도형 클래스들은 절차적으로 나열되어 있다.
  • 도형이 동작하는 방식은 Geometry 클래스에서 구현한다. 현재는 넓이를 구하는 area 메서드가 정의되어 있다.
public class Square {
    public Point topLeft;
    public double side;
}

public class Circle {
    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) {
            Shape s = (Square)shape;
            return s.side * s.side;
        } else if (shape instanceof Circle) {
            Shape s = (Circle)shape;
            return PI * c.radius * c.radius;
        }
        
        throw new NoSuchShapeException();
    }
}

 

[절차적인 코드 + 자료구조]의 장점

  • 각 클래스(도형)의 동작하는 메서드를 추가하고 싶을 때 유리하다.
  • 만약 Example Code에서 도형의 넓이가 아닌 둘레 길이를 구하는 메서드 outlineLength()를 추가한다면, Geometry만 수정하면 된다. 즉, 도형 클래스(Square, Circle)은 아무런 영향을 받지 않는다.

[절차적인 코드 + 자료구조]의 단점

  • 새로운 클래스(도형)를 추가하고 싶을 때 불리하다.
  • 만약 Example Code에서 새로운 도형인 Triangle을 추가한다면, Geometry에 속한 메서드를 전부 수정해야 한다.

 

2. [객체지향 코드 + 객체]

Example Code

  • Square와 Circle는 객체 지향적인 도형 클래스다. area()는 다형(polymorphic) 메서드다. 따라서 Geometry 클래스가 필요 없다.
public class Square implements Shape {
    public Point topLeft;
    public double side;
    
    public double area() {
        return side * side;
    }
}

public class Circle implements Shape {
    public Point center;
    public double radius;
    public final double PI = 3.14;
    
    public double area() {
        return PI * radius * radius;
    }    
}

 

[객체지향 코드 + 객체]의 장점

  • 새로운 클래스(도형)을 추가하고 싶을 때 유리하다. → [절차적인 코드 + 자료구조]의 단점이었다
  • Example Code에서 새로운 도형인 Triangle을 추가한다면, 기존의 다른 도형 클래스는 전혀 영향을 받지 않는다.

[객체지향 코드 + 객체]의 단점

  • 각 클래스(도형)의 동작하는 메서드를 추가하고 싶을 때 불리하다. → [절차적인 코드 + 자료구조]의 장점이었다
  • Example Code에서 Shape 인터페이스에 둘레 길이를 구하는 메서드 outlineLength()를 추가한다면, 모든 구현 클래스(도형 클래스)를 수정해야만 한다.
  • 참고로, 메서드 추가가 빈번하게 일어날 경우 Visitor 패턴을 활용해 단점을 극복할 수 있다.

 

3. 정리

  • [절차적인 코드 + 자료구조]와 [객체지향 코드 + 객체]는 서로 상호 보완적인 특질이 있다. 즉, 객체지향 코드에서 어려운 변경은 절차적인 코드에서 쉽고, 그 반대도 마찬가지다.
  • 복잡한 시스템을 짜다 보면 새로운 함수가 아니라 새로운 자료 타입이 필요한 경우가 생긴다. 이때는 [객체지향 코드 + 객체]가 가장 적합하다.
  • 반면, 새로운 자료 타입이 아니라 새로운 함수가 필요한 경우도 생긴다. 이때는 절차적인 코드와 자료 구조가 좀 더 적합하다.