거대한 클래스(Large Class)
- 어떤 클래스가 너무 많은 책임을 가지면 필드도 많아지고 중복 코드가 보이기 시작함
- 클라이언트가 해당 클래스가 제공하는 기능 중 일부만 사용한다면, 각각의 세부 기능을 별도의 클래스로 분리할 수 있음
- "클래스 추출하기(Extract Class)" -> 관련있는 필드를 한 곳으로 모을 수 있음
- "슈퍼클래스 추출하기(Extract Superclass)" 또는 "타입 코드를 서브클래스로 교체하기" -> 상속 구조 적용
- 클래스 내부에 산재하는 중복 코드는 메서드를 추출하여 제거 가능
슈퍼클래스 추출하기(Extract Superclass)
- 두 개의 클래스에서 비슷한 것들이 보인다면 상속을 적용하고, 슈퍼클래스로 "필드 올리기(Pull Up Field)"와 "메서드 올리기(Pull Up Method)"를 사용
- 대안으로는 "클래스 추출하기(Extract Class)"를 적용해 위임 사용 가능
- 우선은 간단히 상속 적용 이후, 나중에 필요하다면 "슈퍼클래스를 위임으로 교체하기" 적용
예제 코드
public class Department {
private String name;
private List<Employee> staff;
public String getName() {
return name;
}
public List<Employee> getStaff() {
return staff;
}
public double totalMonthlyCost() {
return this.staff.stream().mapToDouble(e -> e.getMonthlyCost()).sum();
}
public double totalAnnualCost() {
return this.totalMonthlyCost() * 12;
}
public int headCount() {
return this.staff.size();
}
}
---
public class Employee {
private Integer id;
private String name;
private double monthlyCost;
public double annualCost() {
return this.monthlyCost * 12;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public double getMonthlyCost() {
return monthlyCost;
}
}
두 클래스 안에는 연간 비용, 월간 비용을 계산하는 동작은 다르지만 같은 의미를 나타내는 메서드들이 보인다. 이를 상위 클래스를 만들어 상속 구조로 만들 수 있을 것 같다.
상위 클래스를 만든 후, 필드와 메서드를 올려주자.
// 상위 추상 클래스
public abstract class Party {
protected String name;
public Party(String name) {
this.name = name;
}
public String getName() {
return name;
}
public double annualCost() {
return this.monthlyCost() * 12;
}
abstract double monthlyCost();
}
---
public class Department extends Party {
private List<Employee> staff;
public Department(String name) {
super(name);
}
public List<Employee> getStaff() {
return staff;
}
public double monthlyCost() {
return this.staff.stream().mapToDouble(Employee::monthlyCost).sum();
}
public int headCount() {
return this.staff.size();
}
}
---
public class Employee extends Party {
private Integer id;
private double monthlyCost;
public Employee(String name) {
super(name);
}
public Integer getId() {
return id;
}
@Override
double monthlyCost() {
return monthlyCost;
}
}
서로 다른 인터페이스의 대안 클래스들(Alternative Classes with Different Interfaces
- 비슷한 일을 여러 곳에서 서로 다른 규약(서로 다른 인터페이스)을 사용해 지원하고 있는 코드 냄새
- 대안 클래스로 사용하려면 동일한 인터페이스를 구현해야 함
- "함수 선언 변경하기(Change Function Declaration)"와 "함수 옮기기(Move Function)"을 사용해서 서로 동일한 인터페이스를 구현하게끔 코드를 수정 가능
- 두 클래스에서 일부 코드가 중복되는 경우 -> "슈퍼클래스 추출하기(Extract Superclass)"를 사용해 중복된 코드를 슈퍼클래스로 옮기고 두 클래스를 새로운 슈퍼클래스의 서브클래스로 만들 수 있음
예제 코드
public class OrderProcessor {
private EmailService emailService;
public void notifyShipping(Shipping shipping) {
EmailMessage emailMessage = new EmailMessage();
emailMessage.setTitle(shipping.getOrder() + " is shipped");
emailMessage.setTo(shipping.getEmail());
emailMessage.setFrom("no-reply@whiteship.com");
emailService.sendEmail(emailMessage);
}
}
---
public class OrderAlerts {
private AlertService alertService;
public void alertShipped(Order order) {
AlertMessage alertMessage = new AlertMessage();
alertMessage.setMessage(order.toString() + " is shipped");
alertMessage.setFor(order.getEmail());
alertService.add(alertMessage);
}
}
위 두 클래스는 알림을 보낸다는 점에섯 비슷한 기능을 제공하는데, 사용하는 인터페이스가 서로 다르다. (emailService.sendEmail
<-> alertService.add
)
이 인터페이스를 우리가 고칠 수 있으면 좋겠지만, 이를 고칠 수 없다고 가정하고 리팩토링을 적용해보자. 방법은 똑같다. 역시 추상 계층을 하나 더 쌓으면 되는 것이다. NotificationService
라는 추상 계층을 만들자.
public class Notification {
private String title;
private String receiver;
private String sender;
private Notification(String title) {
this.title = title;
}
public static Notification newNotification(String title) {
return new Notification(title);
}
public Notification receiver(String receiver) {
this.receiver = receiver;
return this;
}
public Notification sender(String sender) {
this.sender = sender;
return this;
}
public String getTitle() {
return title;
}
public String getReceiver() {
return receiver;
}
public String getSender() {
return sender;
}
}
public interface NotificationService {
void sendNotification(Notification notification);
}
---
public class EmailNotificationService implements NotificationService {
private EmailService emailService;
public EmailNotificationService(EmailService emailService) {
this.emailService = emailService;
}
@Override
public void sendNotification(Notification notification) {
EmailMessage emailMessage = new EmailMessage();
emailMessage.setTitle(notification.getTitle());
emailMessage.setTo(notification.getReceiver());
emailMessage.setFrom(notification.getSender());
emailService.sendEmail(emailMessage);
}
}
public class OrderProcessor {
private NotificationService notificationService;
public void notifyShipping(Shipping shipping) {
Notification notification = Notification.newNotification(shipping.getOrder() + " is shipped")
.receiver(shipping.getEmail())
.sender("no-reply@whiteship.com");
notificationService.sendNotification(notification);
}
}
(OrderAlert는 생략..)
데이터 클래스(Data Class)
- 데이터 클래스: public 필드 또는 필드에 대한 게터와 세터만 있는 클래스
- 데이터 클래스의 필드를 어딘가에서 사용하고 있다면, 이를 데이터 클래스 내부로 책임을 이동시키는 것이 적절할 것..
- => 코드가 적절한 위치에 있지 않기 때문에 이러한 냄새가 생길 수 있음
- 예외적으로, "단계 쪼개기"에서 중간 데이터를 표현하는 데에 사용할 레코드는 불변 객체로 데이터를 전달하는 용도로 사용 가능
- public 필드를 가지고 있다면, "레코드 캡슐화하기(Encapsulate Record)"를 사용해 게터나 세터를 통해서 접근하도록 고칠 수 있음
- 변경되지 않아야 할 필드 -> "세터 제거하기(Remove Setting Method)" 적용
- 게터와 세터가 사용되는 메서드를 찾아본 후, "함수 옮기기(Move Function)" 적용해서 데이터 클래스로 옮길 수 있음
- 메서드 전체가 아니라 일부 코드만 옮겨야 한다면 -> "함수 추출하기(Extract Function)"을 선행한 후에 옮길 수 있음
레코드 캡슐화하기(Encapsulate Record)
- 변하는 데이터를 다룰 때는 레코드 보다는 객체를 선호
- 여기서의 "레코드" = public 필드로 구성된 데이터 클래스를 말함
- 데이터를 메서드 뒤로 감추면 객체의 클라이언트는 어떤 데이터가 저장되어 있는지 신경쓸 필요가 없음
- 필드 이름을 변경할 때 점진적으로 변경 가능
- 하지만, 자바의 Record는 불변 객체라서 이런 리팩토링이 필요 없음
- public 필드를 사용하는 코드를 private 필드와 게터, 세터를 사용하도록 변경
예제 코드
public class Organization {
public String name;
public String country;
}
위와 같은 클래스가 책에서 설명하는 "레코드"이다. 모든 필드가 public이다. 이는 다음과 같이 수정하여 필드를 캡슐화하는 것이 적절할 것이다.
public class Organization {
private String name;
private String country;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
하지만, 만약 불변 객체를 사용하고 싶은 경우라면? 자바의 record를 사용하면 더욱 쉽게 사용 가능하다.
public record Organization(String name, String country) {
}
상속 포기(Refused Bequest)
- 서브클래스가 슈퍼클래스에서 제공하는 메서드나 데이터를 잘 활용하지 않는다는 것은 해당 상속 구조에 문제가 있다는 것
- 이는 서브클래스마다 구체적으로 가지고 있어야 할 메서드나 필드를 슈퍼클래스에서 가지고 있기 때문에 발생하는 냄새임
- 기존의 서브클래스 또는 새로운 서브클래스를 만들고 슈퍼클래스에서 "메서드와 필드를 내려주면(Push Down Method / Field)" 슈퍼클래스에 공동으로 사용하는 기능만 남길 수 있음
- 서브클래스가 슈퍼클래스의 기능을 재사용하고 싶지만 인터페이스를 따르고 싶지 않은 경우
- 상속 구조를 제거할 필요가 있음
- => "슈퍼클래스 또는 서브크래스를 위임으로 교체하기" 리팩토링을 적용할 수 있음
주석(Comments)
- 주석을 남겨야할 것 같다면 먼저 코드를 리팩토링하자 => 불필요한 주석을 줄일 수 있음
- 모든 주석이 나쁘다는 것도 아니고, 주석을 쓰지 말자는 것도 아님
- 주석은 좋은 냄새에 해당하기도 함
- 관련 리팩토링
- "함수 추출하기" -> 설명이 필요한 부분을 별도의 메서드로 빼내어 의도를 드러내자
- "함수 선언부 변경하기" -> 함수 이름을 재정의
- "어서션 추가하기(Introduce Assertion)" -> 시스템적으로 어떤 필요한 규칙이 있다면 assertion으로 적용 가능
어서션 추가하기(Introduce Assertion)
- 종종 코드로 표현하지 않았지만 기본적으로 가정하고 있는 조건들이 있음
- 그런 조건을 알고리즘을 파악하거나 주석을 읽으면서 확인해야 함..
- 이를 Assertion을 사용하면 보다 명시적으로 나타낼 수 있음
- Assertion은 if나 switch 문과 달리 "항상" true이길 기대하는 조건을 표현할 때 사용 (if/switch와 assertion의 차이)
- Assertion이 실패한다면 프로그래머의 실수.
- Assertion이 없어도 프로그램은 동작해야 함
- 특정 부분에서 특정한 상태를 가정하고 있다는 것을 명시적으로 나타냄으로써, 의사표현적인 가치를 지니고 있음
자바에서는 컴파일 옵션으로 assert 문을 사용하지 않도록 설정할 수 있음. 빌드 시 VM Option에 '-ea' (enable assertion)를 전달하면 assert문을 사용하고, 전달하지 않으면 assert를 사용하지 않는다는 의미임.
예제 코드
public class Customer {
private Double discountRate;
public double applyDiscount(double amount) {
return (this.discountRate != null) ? amount - (this.discountRate * amount) : amount;
}
public Double getDiscountRate() {
return discountRate;
}
public void setDiscountRate(Double discountRate) {
this.discountRate = discountRate;
}
}
위 코드에서 applyDiscount
메서드는 discountRate가 null이 아니고 양수임을 가정하고 있다.
public class Customer {
private Double discountRate;
public double applyDiscount(double amount) {
return amount - (this.discountRate * amount);
}
public Double getDiscountRate() {
return discountRate;
}
public void setDiscountRate(Double discountRate) {
assert discountRate != null && discountRate > 0;
this.discountRate = discountRate;
}
}
하지만, assertion은 끄고 킬 수 있는 옵션이라 반드시 검증해야 하는 부분에 대해서는 if문을 사용하는 것이 더 적절하긴 하다.
public class Customer {
...
public void setDiscountRate(Double discountRate) {
if (discountRate == null || discountRate < 0) {
throw new IllegalArgumentException("Discount rate cannot be negative");
}
this.discountRate = discountRate;
}
}
'ETC' 카테고리의 다른 글
[Next.js] Next.js 14 Rendering 및 서버 실전 압축 정리 (0) | 2025.01.18 |
---|---|
[Refactoring] 리팩토링 요약 (0) | 2025.01.06 |
[Refactoring] 18 ~ 19. 중재자 / 내부자 거래 (1) | 2025.01.05 |
[Refactoring] 15 ~ 17. 추측성 일반화 / 임시 필드 / 메시지 체인 (2) | 2025.01.04 |
[Refactoring] 12 ~ 14. 반복되는 switch 문 / 반복문 / 성의없는 요소 (0) | 2025.01.04 |