본문 바로가기
공부 기록

[리팩토링 1판] Chapter 08. 데이터 체계화

by 매트(Mat) 2024. 4. 3.

Chapter 08. 데이터 체계화

이번 장은 내용이 좀 많다. 다 아는 내용일 수도 있지만 "아, 이런 것들이 있었지 혹은 있었구나."라고 복습 차원에서 한번 훓어보는 것이 좋다.
다만, 외우려고 하지 말자.

이 장에서 다루는 리팩토링 기법은 데이터 연동을 더 간편하게 해준다.

  • 많은 이들이 필드 자체 캡슐화는 불필요하다고 생각한다.
  • 객체가 내부의 데이터에 직접 접근하게 해야할지, 아니면 getter/setter 메서드를 통해 접근하게 해야 할지에 대한 문제는 오랫동안 좋은 논란거리였다.

이 책이 2002년에 출판된 책인데.. 사실상 요즘은 거의 패턴이 정해져 있다. 위 고민들은 안하게 되고 당연하게 필드는 캡슐화한다.

필드 자체 캡슐화

필드에 직접 접근하던 중 그 필드로의 결합에 문제가 생길 땐

  • 그 필드용 getter/setter 메서드를 작성해서 두 메서드를 통해서만 필드에 접근하게 만들자.
public class Sample1 {
    private int low;
    private int high;

    public boolean includes(int arg) {
        return arg >= low && arg <= high;
    }
}

리팩토링 후

public class Sample1 {
    private int low;
    private int high;

    public int getLow() {
        return low;
    }

    public int getHigh() {
        return high;
    }

    public boolean includes(int arg) {
        return arg >= getLow() && arg <= getHigh();
    }
}
  • 필드 캡슐화는 필드를 private으로 선언하고 getter 메서드로 호출하는 것이다.
  • setter 메서드는 고민해볼 필요가 있다.
    • 의도치 않은 변경이 일어날 수 있다.
    • Immutable 하지 않다.
    • setter 대신 생성자나 별도의 초기화 메서드를 만드는 것이 좋다.

데이터 값을 객체로 전환

데이터 항목에 데이터나 기능을 더 추가해야 할 때는

  • 데이터 항목을 객체로 만들자.
  • 개발이 진행되다 보면 간단한 항목들이 점점 복잡해진다.
  • 이럴 때는 즉시 데이터 값을 객체로 전환하자.

이 역시 예전에는 VO 라는 객체로 관리했고, 현재는 Domain 객체가 있고, DTO 객체를 만들어서 관리하고 있다.

값을 참조로 전환

클래스에 같은 인스턴스가 많이 들어 있어서 이것들을 하나의 객체로 바꿔야 할 땐

  • 그 객체를 참조 객체로 전환하자.
  • 참조 객체로의 접근을 담당할 객체를 정한다.
    • 이 기능은 정적 딕셔너리나 레지스트리 객체가 담당할 수도 있다.
    • 참조 객체로의 접근을 둘 이상의 객체가 담당할 수도 있다.
private static Dictionary instances = new Hashtable();

private void store() {
    instances.put(this.getName(), this);
}

public static Customer create(String name) {
    return (Customer) instances.get(name);
}
  • 딕셔너리 객체를 통해 저장하는 것과 가져오는 메서드를 선언하여 사용한다.

참조를 값으로 전환

참조 객체가 작성 수정할 수 없고 관리하기 힘들 땐

  • 그 참조 객체를 값 객체로 만들자.
  • 참조 객체를 사용한 작업이 복잡해지는 순간 참조를 값으로 바꿔야 할 시점이다.
  • 전환할 객체가 변경 불가가 아니어야 이 기법을 사용할 수 있다.
  • equals 메서드와 hashCode 메서드를 재정의한다.

배열을 객체로 전환

배열을 구성하는 특정 원소가 별의별 의미를 지닐 땐

  • 그 배열을 각 원소마다 필드가 하나씩 든 객체로 전환하자.
public class Sample2 {
    public void methodA() {
        String[] row = new String[3];
        row[0] = "Liverpool";
        row[1] = "15";
    }
}

리팩토링 후

public class Sample2 {
    public void methodA() {
        Performance row = new Performance();
        row.setName("Liverpool");
        row.setWins("15");
    }

    static class Performance {
        private String name;
        private String wins;

        public void setName(String name) {
            this.name = name;
        }

        public void setWins(String wins) {
            this.wins = wins;
        }
    }
}

Observer 데이터 복제

도메인 데이터는 GUI 컨트롤 안에서만 사용 가능한데, 도메인 메서드가 그 데이터에 접근해야 할 땐

  • 그 데이터를 도메인 객체로 복사하고, 양측의 데이터를 동기화하는 observer 인터페이스 observer를 작성하자.
  • 수정대상은 GUI 관련 뷰이고, GUI와 관련없는 모델은 수정이 필요없다.
  • 모델과 뷰를 분리하는 Observer 데이터 복제 리팩토링
    • 뷰는 모델의 데이터를 복사한 형태로 가지고, 관찰자 패턴이나 이벤트 리스너로 두 데이터를 동기화한다.
  • 이러면 클래스 역할이 명확해진다.
  • 주의할 점은 모델 하나가 여러 뷰를 가지면 안된다.

클래스의 단방향 연결을 양방향으로 전환

두 클래스가 서로의 기능을 사용해야 하는데 한 방향으로만 연결되어 있을 땐

  • 역 포인터를 추가하고 두 클래스를 모두 업데이트할 수 있게 접근 한정자를 수정하자.
  • 두 클래스를 설정할 때 한 클래스가 다른 클래스를 참조하게 해놓은 경우가 있다.
  • 나중에 참조되는 클래스를 사용하는 부분에서 그 클래스를 참조하는 객체들을 가져와야 할 수도 있다.
  • 즉, 포인터를 역방향으로 참조해야 한다.
public class Customer {
    private Set<Order> orders = new HashSet<>();
}
  • 먼저 역 포인터 참조용 필드를 추가한다.
  • 한 고객은 여러 주문을 할 수 있으므로 Set 컬렉션을 사용했다.

다음으로 연결 제어 기능을 어느 클래스에 넣을지 정해야 하는데 판단 기준은 아래와 같다.

  • 두 객체가 모두 참조 객체이고, 연결이 일대다이면 참조가 하나 들어 있는 객체를 제어 객체로 정한다.
  • 즉, Customer:Order = 1:N 관계에서 N쪽에 연결 제어 객체로 택한다.
  • 이는 마치 JPA의 연관관계 매핑과 비슷하다.
    • 연관관계에서 1:N의 관계에서 외래키가 있는 곳이 연관관계의 주인이 되는 것과 비슷한 것 같다. (아님 말고..)

클래스의 양방향 연결을 단방향으로 전환

두 클래스가 양방향으로 연결되어 있는데 한 클래스가 다른 클래스의 기능을 더 이상 사용하지 않게 됐을 땐

  • 불필요한 방향의 연결을 끊자.
  • 항상 양방향 연결은 쓸모가 많지만 대가가 따른다. (JPA 연관관계도?..)
  • 양방향 연결이 익숙하지 않은 대부분의 프로그래머는 에러를 발생시킨다.
  • 즉, 종속성이 많아지고 결합력이 강해져서 사소한 수정에도 예기치 못한 각종 문제가 발생한다.
  • 따라서 양방향 연결은 꼭 필요할 때만 사용해야 한다. (아니 어쩌면 사용하지 말아야 한다..)

마법 숫자를 기호 상수로 전환

특수 의미를 지닌 리터럴 숫자가 있을 땐

  • 의미를 살린 이름의 상수를 작성한 후 리터럴 숫자를 그 상수로 교체하자.
public class Sample3 {
    public double potentialEnergy(double mass, double height) {
        return mass * 9.81 * height;
    }
}

리팩토링 후

public class Sample3 {

    public static final double GRAVITATIONAL_CONSTANT = 9.81;

    public double potentialEnergy(double mass, double height) {
        return mass * GRAVITATIONAL_CONSTANT * height;
    }
}
  • 숫자만 보았을 때는 무슨 의미인지 파악하기 힘들다.
  • 따라서 상수로 만들어서 그 의미를 명확하게 하는 것이 유지보수에도 중요하다.

필드 캡슐화

public class Sample4 {
    public String name;
}

리팩토링 후

public class Sample4 {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  • 필드를 public으로 선언하여 어느 곳에서나 사용할 수 있게 두면 안된다.
  • 좋은 프로그램은 제약을 두는 것이다.
    • private 으로 다른 곳에서 사용할 수 없도록 은폐시킨다.
    • getter/setter를 만들어서 메서드를 통해서만 접근할 수 있도록 한다.

컬렉션 캡슐화

메서드가 컬렉션을 반환할 땐

  • 그 메서드가 읽기전용 뷰를 반환하게 수정하고 추가 메서드와 삭제 메서드를 작성하자.
  • 읽기 메서드는 컬렉션 객체 자체를 반환해선 안된다.
  • 컬렉션 쓰기 메서드는 절대 있으면 안되므로, 원소를 추가하는 메서드와 삭제하는 메서드를 대신 사용해야 한다.

레코드를 데이터 클래스로 전환

전통적인 프로그래밍 환경에서 레코드 구조를 이용한 인터페이스를 제공해야 할 땐

  • 레코드 구조를 저장할 덤 데이터 객체를 작성하자.

객체지향 프로그램에 레코드 구조를 사용하는 이유는 다양하다.

  • 구버전 프로그램을 복사할 수 있다.
  • 구조화된 레코드를 기존의 프로그래밍 API나 데이터베이스 레코드와 소통하게 할 수도 있다.

사용 방법

  • 레코드를 표현할 클래스 생성
  • 그 클래스에 각 데이터 항목에 대한 getter/setter 메서드를 만들고 private 필드를 선언
  • 이렇게 만든 클래스가 덤 데이터 객체이다.

분류 부호를 클래스로 전환

기능에 영향을 미치는 숫자형 분류 부호가 든 클래스가 있을 땐

  • 그 숫자를 새 클래스로 바꾸자.

여기서 숫자형 분류 부호는 열거(Enum) 타입을 말한다.

  • 컴파일러는 Enum 클래스 안에서 종류 판단을 수행할 수 있다.
  • 팩토리 메서드를 작성하면 유효한 인스턴스만 생성되는지와 그런 인스턴스가 적절한 객체로 전달되는지를 정적으로 검사할 수 있다.

분류 부호를 하위클래스로 전환

클래스 기능에 영향을 주는 변경불가 분류 부호가 있을 땐

  • 분류 부호를 하위클래스로 만들자.

분류 부호가 클래스 기능에 영향을 미치는 현상은 조건문 switch문 또는 if-else문이다.

  • 분류 부호를 다형화된 기능이 든 상속 구조로 고쳐야 한다.
  • 상속 구조는 하나의 클래스와 각 분류 부호에 대한 하위클래스로 구성된다.

분류 부호를 상태/전략 패턴으로 전환

분류 부호가 클래스의 기능에 영향을 주지만 하위클래스로 전환할 수 없을 땐

  • 그 분류 부호를 상태 객체로 만들자.

분류 부호가 객체 수명주기 동안 변할 때나 다른 이유로 하위클래스로 만들 수 없을 때 사용한다.

  • 상태패턴이나 전략패턴 중 하나를 사용한다.
  • 전략패턴은 조건문을 재정의로 전환으로 하나의 알고리즘을 단순화해야 할 때 적절하다.
  • 상태패턴은 상태별 데이터를 이동하고 객체를 변화하는 상태로 생각할 때 적절하다.
  • 상태패턴의 상태클래스는 추상 클래스로 만들어서 하위클래스들이 상속받은 상태클래스의 메서드를 각각 재정의하여 반환한다.
  • switch 문이 생기면 조건문을 재정의로 전환 기법을 적용하면 다른 case문을 전부 삭제해도 된다.

하위클래스를 필드로 전환

여러 하위클래스가 상수 데이터를 반환하는 메서드만 다를 땐

  • 각 하위클래스의 메서드를 상위클래스 필드로 전환하고 하위클래스는 전부 삭제하자.

상수 메서드는 하드코딩된 값을 반환하는 메서드다.

  • 상수 메서드는 읽기 메서드에 각기 다른 값을 반환하는 하위클래스에 넣으면 유용하다.
  • 다만 하위클래스를 상수 메서드로만 구성한다고 해서 그만큼 효용성이 커지는 것은 아니다.
  • 상위클래스 안에 필드를 넣고 그런 하위클래스는 완전히 삭제하면 된다. 그러면 하위클래스의 불필요한 복잡함도 사라진다.

느낀 점

  • 데이터 캡슐화가 왜 필요한지와 어떻게 캡슐화하는지 알아보았다.
  • 비슷한 데이터 값은 나열하지 말고 객체로 전환하는 것이 좋다.
  • 기타 등등..

말고도 상수 선언이나 배열을 객체로, Enum 클래스 사용 등을 공부하였다. 솔직히 위 내용 모두 완벽하게 안 것은 아니다. "이런 기법들이 있구나." 하고 넘어갔고, 직접 실무에서 경험해보고 문제 있는 코드들을 개선해보면 더욱 기억에 남을 것이다.

그리고 꼭 모든 것을 완벽하게 알 필요도 없다. (알 수도 없고..) 그저 하루하루 꾸준히 공부하고 경험하다보면 깨닫게 될 것이라 믿는다.

References

댓글