[Refactoring] 20 ~ 24. 거대한 클래스 / 서로 다른 인터페이스의 대안 클래스들 / 데이터 클래스 / 상속 포기 / 주석

2025. 1. 5. 17:09·ETC
728x90
반응형

거대한 클래스(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;
    }
}
728x90
반응형

'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
'ETC' 카테고리의 다른 글
  • [Next.js] Next.js 14 Rendering 및 서버 실전 압축 정리
  • [Refactoring] 리팩토링 요약
  • [Refactoring] 18 ~ 19. 중재자 / 내부자 거래
  • [Refactoring] 15 ~ 17. 추측성 일반화 / 임시 필드 / 메시지 체인
mxruhxn
mxruhxn
소소하게 개발 공부 기록하기
    반응형
    250x250
  • mxruhxn
    maruhxn
    mxruhxn
  • 전체
    오늘
    어제
    • 분류 전체보기 (150)
      • Java (21)
      • Spring (4)
      • Database (13)
      • Operating Syste.. (1)
      • Computer Archit.. (0)
      • Network (24)
      • Data Structure (6)
      • Algorithm (11)
      • Data Infra (7)
      • DevOps (12)
      • ETC (27)
      • Project (21)
      • Book (1)
      • Look Back (1)
  • 블로그 메뉴

    • 링크

      • Github
    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    mxruhxn
    [Refactoring] 20 ~ 24. 거대한 클래스 / 서로 다른 인터페이스의 대안 클래스들 / 데이터 클래스 / 상속 포기 / 주석
    상단으로

    티스토리툴바