[Refactoring] 18 ~ 19. 중재자 / 내부자 거래

2025. 1. 5. 15:19·ETC
728x90
반응형

중재자(Meddle Man)

  • 어떤 클래스의 메서드가 대부분 다른 클래스로 메서드 호출을 위임하고 있다면, 중재자를 제거하고 클라이언트가 해당 클래스를 직접 사용하도록 코드 개선 가능 (= 불필요한 추상화/캡슐화 제거)
    • 이전에 본 "메시지 체인"의 반대에 해당하는 것
  • 관련 리팩토링
    • "중재자 제거하기(Remove Middel Man)" -> 클라이언트가 필요한 클래슬르 직접 사용하도록 개선 가능
    • "함수 인라인(Inline Function)" -> 메서드 호출한 쪽으로 코드를 보내서 중재자를 없앨 수 있음
    • "슈퍼클래스를 위임으로 바꾸기(Replace Superclass with Delegate)"
    • "서브클래스를 위임으로 바꾸기(Replace Subclass with Delegate)"

중재자 제거하기(Remove Middel Man)

  • "위임 숨기기"의 반대에 해당하는 리팩토링
  • 캡슐화의 정도를 "중재자 제거하기"와 "위임 숨기기" 리팩토링을 통해 조절할 수 있음
    • => 캡술화 정도에 정답은 없음
  • 위임하고 있는 객체를 클라이언트가 사용할 수 있도록 getter를 제공하고, 클라이언트는 메시지 체인을 사용하도록 코드를 고친 뒤, 캡슐화에 사용했던 메서드를 제거하는 순서로 진행

예제 코드

public class Department {

    private Person manager;

    public Department(Person manager) {
        this.manager = manager;
    }

    public Person getManager() {
        return manager;
    }
}

---

public class Person {

    private Department department;

    private String name;

    public Person(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    public Person getManager() {
        return this.department.getManager();
    }
}

 

위 예제는 이전 "위임 숨기기"에서 다뤘던 예제와 동일하다. 정확히 반대의 과정을 거쳐서 "중재자 제거하기" 리팩토링을 수행할 수 있다. 이를 위해 getManager()를 통해 캡슐화 했던 것을 제거해주자.

public class Person {

    private Department department;

    private String name;

    public Person(String name, Department department) {
        this.name = name;
        this.department = department;
    }

    public Person getDepartment() {
        return this.department;
    }
}

슈퍼클래스를 위임으로 바꾸기(Replace Superclass with Delegate)

  • 서브클래슨느 슈퍼클래스의 모든 기능을 지원해야 한다
  • 서브클래스는 슈퍼클래스 자리를 대체하더라도 잘 동작해야 한다
    • 리스코프 치환 원칙
  • 서브클래스는 슈퍼클래스의 변경에 취약하다.. (상속의 한계)
  • 그렇다면 상속을 사용하지 않는 것이 좋은가?
    • 상속은 적절한 경우 사용하면 매우 쉽고 효율적인 방법이다
    • => 우선 상속을 적용한 이후, 적절하지 않다고 판단되면 그때 이 리팩토링을 적용하자

예제 코드

public class CategoryItem {

    private Integer id;

    private String title;

    private List<String> tags;

    public CategoryItem(Integer id, String title, List<String> tags) {
        this.id = id;
        this.title = title;
        this.tags = tags;
    }

    public Integer getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public boolean hasTag(String tag) {
        return this.tags.contains(tag);
    }
}

---

public class Scroll extends CategoryItem {

    private LocalDate dateLastCleaned;

    public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
        super(id, title, tags);
        this.dateLastCleaned = dateLastCleaned;
    }

    public long daysSinceLastCleaning(LocalDate targetDate) {
        return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}

 

위 코드에서 Scroll은 별도의 Item이지 카테고리 아이템이라고 보기는 어려울 것 같다. 상속 구조를 제거하고 합성으로 변경하는 것이 좋아보인다.

public class Scroll {

    private LocalDate dateLastCleaned;
    private CategoryItem categoryItem; // 합성으로 변경

    public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
        this.dateLastCleaned = dateLastCleaned;
        this.categoryItem = new CategoryItem(id, title, tags);
    }

    public long daysSinceLastCleaning(LocalDate targetDate) {
        return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}

서브클래스를 위임으로 바꾸기(Replace Subclass with Delegate)

  • 어떤 객체의 행동이 카테고리에 따라 바뀐다면, 보통 상속을 사용해서 일반적인 로직은 슈퍼클래스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현한다
  • 하지만, 대부분의 프로그래밍 언어에서 상속은 오직 한번만 사용할 수 있다..
    • => 만약 어떤 객체를 2가지 이상의 카테고리로 구분해야 한다면?
    • => 위임을 사용하면 얼마든지 여러가지 이유롤 여러 다른 객체로 위임을 할 수 있다
  • 슈퍼클래스가 바뀌면 모든 서브 클래스에 영향을 줄 수 있다. 때문에, 슈퍼클래스를 변경할 때 서브클래스까지 신경써야 한다.
    • => 만약 서브클래스가 전혀 다른 모듈에 있다면?
    • => 위임을 사용한다면 중간에 '인터페이스'를 만들어 의존성을 줄일 수 있다
  • "상속 대신 위임을 선호하라"는 결고 "상속은 나쁘다"라는 말이 아니다
    • 처음엔 상속을 적용하고 언제든지 이런 리팩토링을 사용해 위임으로 전환할 수 있다

예제 코드

public class Booking {

    protected Show show;

    protected LocalDateTime time;

    public Booking(Show show, LocalDateTime time) {
        this.show = show;
        this.time = time;
    }

    public boolean hasTalkback() {
        return this.show.hasOwnProperty("talkback") && !this.isPeakDay();
    }

    protected boolean isPeakDay() {
        DayOfWeek dayOfWeek = this.time.getDayOfWeek();
        return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
    }

    public double basePrice() {
        double result = this.show.getPrice();
        if (this.isPeakDay()) result += Math.round(result * 0.15);
        return result;
    }

}

---

public class PremiumBooking extends Booking {

    private PremiumExtra extra;

    public PremiumBooking(Show show, LocalDateTime time, PremiumExtra extra) {
        super(show, time);
        this.extra = extra;
    }

    @Override
    public boolean hasTalkback() {
        return this.show.hasOwnProperty("talkback");
    }

    @Override
    public double basePrice() {
        return Math.round(super.basePrice() + this.extra.getPremiumFee());
    }

    public boolean hasDinner() {
        return this.extra.hasOwnProperty("dinner") && !this.isPeakDay();
    }
}

 

일반적인 Booking과 PremiumBooking이 있고, 둘은 peakDay일 때 talkback 가능 여부와 가격도 다르다. 이는 상속을 적절히 사용한 구조라고 볼 수 있다. 하지만, 요구사항 추가 등으로 인해 나중에 상속 구조가 변경될 수도 있으며, Booking에서 PremiumBooking으로 업그레이드 하는 요구사항이 생기면 인스턴스를 갈아끼우는 과정이 번거로워진다.

 

여기에 위임을 적용해서 설계를 변경해보자.

public class PremiumDelegate {

    private Booking host;
    private PremiumExtra extra;

    public PremiumDelegate(Booking host, PremiumExtra extra) {
        this.host = host;
        this.extra = extra;
    }

    public boolean hasTalkback() {
        return this.host.show.hasOwnProperty("talkback");
    }

    public double extendBasePrice(double result) {
        return Math.round(result + this.extra.getPremiumFee());
    }

    public boolean hasDinner() {
        return this.extra.hasOwnProperty("dinner") && !this.host.isPeakDay();
    }
}
public class Booking {

    protected Show show;

    protected LocalDateTime time;

    protected PremiumDelegate premiumDelegate;

    public Booking(Show show, LocalDateTime time) {
        this.show = show;
        this.time = time;
    }

    // 팩토리 메서드
    public static Booking createBooking(Show show, LocalDateTime time) {
        return new Booking(show, time);
    }

    public static Booking createPremiumBooking(Show show, LocalDateTime time, PremiumExtra extra) {
        Booking booking = createBooking(show, time);
        // premiumDelegate는 PremiumBooking을 만들 때에만 생성
        booking.premiumDelegate = new PremiumDelegate(booking, extra);
        return booking;
    }

    public boolean hasTalkback() {
        return (this.premiumDelegate != null) ?
                this.premiumDelegate.hasTalkback() :
                this.show.hasOwnProperty("talkback") && !this.isPeakDay();
    }

    protected boolean isPeakDay() {
        DayOfWeek dayOfWeek = this.time.getDayOfWeek();
        return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
    }

    public double basePrice() {
        double result = this.show.getPrice();

        if (this.isPeakDay()) result += Math.round(result * 0.15);

        return (this.premiumDelegate != null) ? this.premiumDelegate.extendBasePrice(result) : result;
    }

    public boolean hasDinner() {
        return this.premiumDelegate != null && this.premiumDelegate.hasDinner();
    }
}

 

PremiumDelegate라는 위임을 추가하고, Booking에서는 팩토리 메서드를 통해 Booking을 생성하도록 변경하였다. 그 결과, PremiumBooking이라는 서브클래스를 제거하고 상속 구조를 위임으로 변경할 수 있었다.


내부자 거래(Insider Trading)

  • 어떤 모듈이 다른 모듈의 내부 정보를 지나치게 많이 알고 있는 코드 냄새를 의미
    • => 그로 인해 지나치게 강한 결합도(coupling)가 생길 수 있음
  • 강한 결합도를 줄이기 위해 책임을 분배하거나 이동시켜줄 필요가 있음
  • 관련 리팩토링
    • "함수 옮기기(Move Function)" & "필드 옮기기(Move Field)"
    • "위임 숨기기(Hide Delegate)" -> 특정 모듈의 중재자를 두어 결합도를 줄일 수 있음
      • 혹은, 굳이 중재자를 두지 않더라도 여러 모듈이 자주 사용하는 공통적인 기능은 새로운 모듈을 만들어 관리할 수 있음
    • "슈퍼클래스 또는 서브클래슬르 위임으로 바꾸기" -> 상속으로 인한 결합도를 줄일 때 사용 가능
728x90
반응형

'ETC' 카테고리의 다른 글

[Refactoring] 리팩토링 요약  (0) 2025.01.06
[Refactoring] 20 ~ 24. 거대한 클래스 / 서로 다른 인터페이스의 대안 클래스들 / 데이터 클래스 / 상속 포기 / 주석  (0) 2025.01.05
[Refactoring] 15 ~ 17. 추측성 일반화 / 임시 필드 / 메시지 체인  (2) 2025.01.04
[Refactoring] 12 ~ 14. 반복되는 switch 문 / 반복문 / 성의없는 요소  (0) 2025.01.04
[Refactoring] 11. 기본형 집착  (0) 2025.01.04
'ETC' 카테고리의 다른 글
  • [Refactoring] 리팩토링 요약
  • [Refactoring] 20 ~ 24. 거대한 클래스 / 서로 다른 인터페이스의 대안 클래스들 / 데이터 클래스 / 상속 포기 / 주석
  • [Refactoring] 15 ~ 17. 추측성 일반화 / 임시 필드 / 메시지 체인
  • [Refactoring] 12 ~ 14. 반복되는 switch 문 / 반복문 / 성의없는 요소
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] 18 ~ 19. 중재자 / 내부자 거래
    상단으로

    티스토리툴바