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
'공부 기록' 카테고리의 다른 글
[리팩토링 1판] Chapter 10. 메서드 호출 단순화 (1) | 2024.04.06 |
---|---|
[리팩토링 1판] Chapter 09. 조건문 간결화 (1) | 2024.04.04 |
[리팩토링 1판] Chapter 07. 객체 간의 기능 이동 (0) | 2024.04.01 |
[리팩토링 1판] Chapter 06. 메서드 정리 (0) | 2024.03.31 |
[리팩토링 1판] Chapter 05. 리팩토링 기법 카탈로그에 대해 (0) | 2024.03.30 |
댓글