티스토리 뷰
객체 간의 기능 이동
객체 설계에서 원칙은 아니지만 가장 중요한 일 중 하나가 "기능을 어디에 넣을지 판단하는 것" 입니다. 적절한 위치를 판단하는 개인적인
기준은 우선 정보 전문가를 파악한 후 해당 정보 전문가가 위치한 곳에 넣는 것입니다.
이번 장에서는 기능을 적절한 위치에 넣기 위한 여러가지 방법을 소개하고 있습니다. 하나씩 천천히 살펴보겠습니다.
메서드 이동
메서드가 자신이 속한 클래스보다 다른 클래스의 필드 및 메서드를 더 많이 사용한다면 제일 많이 사용하는 클래스안으로 메서드를 옮기는 것입니다.
💡 동기 및 예제 코드
클래스에 기능이 너무 많거나 클래스가 다른 클래스와 과하게 의존되어 있는 경우 메서드를 옮기는 것이 좋습니다. 메서드를 옮기면 클래스가 간결해지고 명확하게 기능을 구현할 수 있습니다.
메서드를 옮길 때는 옮길 메서드를 호출하는 메서드, 옮길 메서드가 호출하는 메서드, 상속 계층이라면 옮길 메서드를 재정의하는 메서드를 살펴봐야 합니다.
메서드 이동을 하는 과정에서의 판단은 어렵습니다. 이때 메서드를 옮길지 확신이 서지 않는다면 다른 메서드를 살펴보고 다른 메서드를 옮길지 판단하는 것도 하나의 방법입니다.
public class Account {
private int daysOverdrawn;
private AccountType accountType;
public double bankCharge() {
double result = 4.5;
if (daysOverdrawn > 0) {
result += overdraftCharge();
}
return result;
}
private double overdraftCharge() {
if (accountType.isPremium()) {
double result = 10;
if (daysOverdrawn > 7) {
result += (daysOverdrawn - 7) * 0.85;
}
return result;
}
return daysOverdrawn * 1.75;
}
}
public class AccountType {
public boolean isPremium() {
return true;
}
}
💡 리팩토링된 코드
AccountType 클래스에서 원본 클래스(Account)의 기능을 사용하려면 아래 네 가지 작업 중 하나를 실시하면 됩니다.
- 그 기능을 대상 클래스로 옮깁니다.
- 대상 클래스에서 원본 클래스로의 참조를 생성하거나 사용해야 합니다.
- 원본 객체를 대상 클래스의 매개변수로 전달합니다.
public class Account {
private int daysOverdrawn;
private AccountType accountType;
public double bankCharge() {
double result = 4.5;
if (daysOverdrawn > 0) {
result += accountType.overdraftCharge(daysOverdrawn);
}
return result;
}
}
public class AccountType {
private boolean isPremium() {
return true;
}
public double overdraftCharge(int daysOverdrawn) {
if (isPremium()) {
double result = 10;
if (daysOverdrawn > 7) {
result += (daysOverdrawn - 7) * 0.85;
}
return result;
}
return daysOverdrawn * 1.75;
}
}
필드 이동
어떤 필드가 자신이 속한 클래스보다 다른 클래스에서 더 많이 사용되는 경우 대상 클래스안에 새로운 필드를 선언하고 기존 참조를 수정합니다.
💡 동기
어떤 필드가 자신이 속한 클래스보다 다른 클래스에 있는 메서드를 더 많이 참조하고 있는 경우라면 해당 필드를 옮길 것을 생각해봐야 합니다. 이때 메서드 이동같은 방법을 생각해볼 수 있지만 현재 그 메서드의 위치가 적적하다고 판단이 되는 경우 필드 이동을 해야합니다.
클래스 추출
하나의 클래스가 두가지의 기능을 하고 있다면 각각의 클래스를 만들어 역할과 책임을 분명히 나눠야합니다. (단일 책임 원칙)
💡 동기 및 예제 코드
클래스는 시간이 지날수록 방대해지기 마련입니다. 개발자는 점증적으로 어떤 기능이나 데이터를 추가하기 때문입니다. 별도의 클래스로 만들기엔 사소한 기능을 추가하지만 그런 사소한 기능도 계속 추가하다보면 클래스가 상당히 복잡해집니다. 결국 방대해진 클래스는 이해하기가 힘들뿐더러 어떻게 나눠야할지 많은 생각을 해야합니다. 이때 데이터의 일부분과 메서드의 일부분이 한 덩어리이거나, 주로 함께 변화하거나 의존적인 데이터의 일부분도 클래스로 떼어내기 좋습니다.
@Getter
@AllArgsConstructor
public class Book {
private String title;
private double price;
private String authorName;
private String authorEmail;
public String getAuthorInfo() {
return authorName + authorEmail;
}
}
💡 리팩토링된 코드
@Getter
@AllArgsConstructor
public class Book {
private String title;
private double price;
private Author author;
public String getAuthorInfo() {
return author.getAuthorInfo();
}
}
@Getter
@AllArgsConstructor
public class Author {
private String authorName;
private String authorEmail;
public String getAuthorInfo() {
return authorName + authorEmail;
}
}
클래스 내용 직접 삽입
클래스의 기능이 너무 적을 땐 해당 클래스의 모든 기능을 다른 클래스에 합치고 기존 클래스는 삭제합니다.
💡 동기
클래스 내용 직접 삽입은 클래스 추출과 반대입니다. 클래스 내용 직접 삽입은 클래스가 더 이상 제 역할을 수행하지 못해 존재할 이유가 없을 때 실시합니다. 이러한 상황은 주로 클래스의 대부분 기능을 다른 클래스로 옮기는 리팩토링을 실시한 후 남은 기능이 거의 없을 때 나타납니다.
대리 객체 은폐
클라이언트가 객체의 대기 클래스를 호출할 땐 대리 클래스를 감추는 메서드를 작성하자
💡 동기 및 예제 코드
객체에서 핵심 개념 중 하나가 캡슐화입니다. 캡슐화란 객체가 시스템의 다른 부분에 대한 정보의 일부만 알 수 있도록 하는 것을 말합니다. 객체를 캡슐화하면 무엇가를 변경할 때 변경으로 인한 여파를 줄일 수 있습니다.
클라이언트가 서버 객체의 필드 중 하나에 정의된 메서드를 호출할 때 그 클라이언트는 대리 객체에 관해 알아야합니다. 또한 대리 객체가 변경될 때 클라이언트도 함께 변경이 이루어져야합니다. 이런 의존성을 없애려면 대리 객체를 감추는 간단한 위임 메서드를 서버에 두면 됩니다. 이렇게 된다면 변경은 서버에서만 이루어지고 클라이언트는 영향을 받지 않게 됩니다.
@Getter
@AllArgsConstructor
public class Person {
private String name;
}
@Getter
@AllArgsConstructor
public class Department {
private String name;
private Person person;
}
public class Main {
public static void main(String[] args) {
Person person = new Person("홍길동");
Department department = new Department("개발팀", person);
department.getPerson().getName(); // 부서의 부서장명 추출
}
}
💡 리팩토링된 코드
@Getter
@AllArgsConstructor
public class Person {
private String name;
}
@Getter
@AllArgsConstructor
public class Department {
private String name;
private Person person;
public String getManager() {
return person.getName();
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("홍길동");
Department department = new Department("개발팀", person);
System.out.println(department.getManager()); // 부서의 부서장명 추출
}
}
과잉 중개 메서드 제거
클래스에 자잘한 위임이 많은 경우 개발자에게 혼동을 줄 수 있기 때문에 대리 객체를 클라이언트가 직접 호출하게 합니다.
💡 동기
대리 객체 은폐 기법을 사용하면 장점을 얻는 동시 단점도 생기게 됩니다. 클라이언트가 대리 객체의 새 기능을 사용하기 위해서는 서버에 간단한 위임 메서드를 추가해야 합니다. 이러한 위임 메서드는 중개자에 불과하므로 이때는 클라이언트가 직접 대리 객체를 호출하도록 해야합니다. 해당 예제는 대리 객체 은폐 기법에서 리팩토링 되기 전의 코드를 살펴보면 됩니다.
외래 클래스에 메서드 추가
사용중인 서버 클래스에 메서드를 추가해야 하는데 그 클래스에 접근할 수 없거나 수정할 수 없는 경우 클라이언트 클래스안에 추가해야 합니다.
💡 동기
이런 상황은 흔하게 발생할 수 있습니다. 현재 우리가 사용하고 있는 클래스에는 모든 기능이 있습니다. 하지만 추후 한가지 기능이 필요해졌을 경우 해당 클래스에 접근할 수 있다면 추구하면 됩니다. 하지만 원본 클래스를 접근할 수 없거나 수정할 수 없는 경우에는 추가하고자 하는 메서드를 클라이언트의 클래스안에 작성해야 합니다.
이때 추가하고자 하는 메서드를 클라이언트 클래스의 한 곳에서만 사용된다면 별 문제가 없지만 여기저기서 사용이 된다면 중복 코드가 발생할 우려가 있기 때문에 유틸 클래스를 만들어 이를 활용하는게 좋습니다. 또한 국소적 상속확장 클래스 기법을 사용할 수 있습니다.
public static void main(String[] args) {
LocalDate nextDate = nextDate(LocalDate.of(2022, 12, 25));
}
private static LocalDate nextDate(LocalDate localDate) {
return LocalDate.of(localDate.getYear(), localDate.getMonth(), localDate.getDayOfMonth() + 1);
}
국소적 상속확장 클래스 사용
사용중인 서버 클래스에 여러 개의 메서드를 추가해야하는데 원본 클래스를 수정할 수 없을 경우 새 클래스를 작성하고 그 안에 필요한 여러 개의 메서드를 작성합니다. 이 상속확장 클래스를 원본 클래스의 하위 클래스나 래퍼 클래스로 만들어야 합니다.
💡 동기
필요한 메서드가 한두 개일 때는 외래 클래스에 메서드 추가 기법을 사용할 수 있지만 필요한 메서드 수가 세 개 이상이라면 해당 방법으로는 무리가 있습니다. 국소적 상속확장 클래스를 만들 수 있습니다.
국소적 상속확장 클래스는 별도의 클래스지만 상속확장하는 클래스의 하위 타입입니다. 따라서 국소적 상속확장 클래스는 원본 클래스의 모든 기능을 사용가능 하면서 새로운 기능을 추가할 수 있습니다. 국소적 상속확장 클래스를 만드는 방법에는 두 가지 방법이 있습니다.
이펙티브 자바 - 상속보다는 컴포지션을 사용하라 라는 부분과 같이 읽으면 좋을거 같아서 첨부합니다.
💡 하위 클래스 사용
public class CustomDate extends Date {
public CustomDate (String dateString) {
super(dateString);
}
public CustomDate (Date arg) {
super(arg.getTime());
}
public Date nextDay() {
return new Date(getYear(), getMonth(), getDate() + 1);
}
}
💡 래퍼 클래스 사용
public class CustomDate {
private Date original;
public CustomDate (String dateString) {
original = new Date(dateString);
}
public CustomDate (Date arg) {
this.original = arg;
}
public int getYear() {
return original.getYear();
}
// 위임 메서드 추가
public Date nextDay() {
return new Date(getYear(), getMonth(), getDate() + 1);
}
public boolean equals(Object arg) {
if (this == arg) return true;
if (!(arg instanceof CustomDate)) return false;
CustomDate other = ((CustomDate) arg);
return original.equals(other.original);
}
}
- Total
- Today
- Yesterday
- redis 대기열 구현
- JDK Dynamic Proxy와 CGLIB의 차이
- redis sorted set으로 대기열 구현
- 공간 기반 아키텍처
- redis sorted set
- transactional outbox pattern spring boot
- transactional outbox pattern
- 트랜잭셔널 아웃박스 패턴 스프링부트
- pipeline architecture
- spring boot 엑셀 다운로드
- spring boot excel download oom
- polling publisher spring boot
- 서비스 기반 아키텍처
- spring boot redisson sorted set
- java userThread와 DaemonThread
- spring boot redis 대기열 구현
- service based architecture
- spring boot redisson destributed lock
- microkernel architecture
- 레이어드 아키텍처란
- pipe and filter architecture
- 람다 표현식
- 자바 백엔드 개발자 추천 도서
- spring boot poi excel download
- spring boot excel download paging
- @ControllerAdvice
- space based architecture
- java ThreadLocal
- spring boot redisson 분산락 구현
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |