[Refactoring] 중재자 (Middle Man)

서준교·2023년 12월 30일
0

Refactoring

목록 보기
7/8
post-thumbnail

👉 이 게시글은 inflearn의 코딩으로 학습하는 리팩토링 강의 내용을 바탕으로 작성되었습니다.

이전에 살펴보았던 메시지 체인의 리팩토링을 해결하기 위한 위임 숨기기(Hide Delegate)가 남용되는 경우 중재자라는 code smell이 발생할 수 있습니다.

코드에 캡슐화가 지나치게 적용되면, 클라이언트가 직접적으로 접근해도 되는 정보를 다른 클래스를 통해 접근하는 구조가 만들어지게 되어 비효율적인 코드가 됩니다.

따라서 어떤 클래스의 메소드가 대부분 다른 클래스로 메소드 호출을 위임하고 있다면, 이러한 중재자를 제거하고 클라이언트가 해당 클래스를 직접 사용할 수 있도록 코드를 개선할 수 있습니다.

이 글에서는 중재자를 제거하는 여러 리팩토링 기술에 대해 알아보도록 하겠습니다.

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

위에서 언급한 바와 같이 중재자 제거하기는 위임 숨기기의 반대에 해당하는 리팩토링 기술입니다.

위임하고 있는 객체를 클라이언트가 사용할 수 있도록 getter를 제공하고, 클라이언트는 메세지 체인을 사용하도록 코드를 수정한 뒤에 캡슐화에 사용했던 메소드를 제거하는 방식으로 이루어집니다.

필요한 캡슐화의 정도는 상황에 따라 바뀔 수 있기 때문에, 적절하게 조절하는 것이 중요합니다.

Before Refactoring

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();
    }

    public Department getDepartment() {
        return department;
    }
}
public class Department {

    private Person manager;

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

    public Person getManager() {
        return manager;
    }
}
class PersonTest {

    @Test
    void getManager() {
        Person nick = new Person("nick", null);
        Person joonkyo = new Person("joonkyo", new Department(nick));
        assertEquals(nick, joonkyo.getManager());
    }

}

기존에는 Person 클래스 내부의 getManager()가 중재자 역할을 하여 해당 메소드를 통해서 department 객체의 getManager() 메소드에 접근이 가능했습니다. 따라서 클라이언트에서는 getManager() 메소드만 호출하면 바로 manager를 얻어올 수 있었습니다.

After Refactoring

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

    public Department getDepartment() {
        return department;
}
class PersonTest {

    @Test
    void getManager() {
        Person nick = new Person("nick", null);
        Person joonkyo = new Person("joonkyo", new Department(nick));
        assertEquals(nick, joonkyo.getDepartment().getManager());
    }

}

리팩토링 이후에는 Person 클래스 내부의 중재자 역할을 하는 메소드였던 getManager()를 제거하고 getDepartment() 메소드를 통해 department 객체를 받아올 수 있도록 하였기 때문에, 클라이언트에서도 getDepartment() 메소드를 호출하고 getManager() 메소드를 호출하는 방식으로 변경되었습니다.

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

객체지향에서 상속은 기존의 기능을 재사용하는 효율적인 방법이지만 적절하지 않은 경우도 있습니다.
서브클래스는 슈퍼클래스 자리를 대체하더라도 잘 동작해야 한다는 리스코프 치환 원칙, 서브클래스는 슈퍼클래스의 변경에 취약하다는 점을 잘 고려하여 상속을 적용한 이후 적절치 않다고 판단되는 경우에 해당 리팩토링 기술을 적용할 수 있습니다.

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 클래스는 CategoryItem 클래스를 상속받는 구조로 되어 있습니다. 상속 관계를 끊고 Scroll 클래스에서 CategoryItem 클래스 객체의 생성을 위임하도록 하겠습니다.

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)

특정 개체의 행동이 카테고리에 따라 바뀐다면, 보통 상속을 사용해서 일반적인 로직은 슈퍼클레스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현합니다. 하지만 대부분의 프로그래밍 언어에서는 상속은 한 번만 허용하기 때문에 슈퍼클래스가 바뀌면 모든 서브클래스에 영향을 준다는 사실을 인지하면서 클래스 간의 관계를 정의해야 합니다.

만약 서브클래스가 전혀 다른 모듈에 존재한다면 위임을 사용하여 중간에 인터페이스를 생성하여 의존성을 줄일 수 있습니다.

즉, 상속을 처음에 적용하되, 언제든지 이러한 리팩토링 기법을 통하여 위임으로 전환할 수 있다는 것입니다.

Before Refactoring

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();
    }
}

기존의 PremiumBooking 클래스는 Booking 클래스를 상속받고 있는 구조를 갖고 있습니다. 본 리팩토링 기술을 통해서 기존의 상속 관계를 끊은 뒤 PremiumBooking 클래스를 삭제하고, 새로운 위임을 생성해 특정 행위를 위임 관계에 있는 객체에게 넘겨 처리하도록 하겠습니다.

After Refactoring


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);
        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();
    }
}
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();
    }
}

리팩토링이 적용된 코드의 가장 큰 특징은 정적 팩토리 메소드를 통해 생성자를 호출하여 특정 카테고리에 따른 인스턴스를 생성한다는 점입니다. 생성자와 비교했을 때 정적 팩토리 메소드의 장점은 생성 목적에 대한 보다 구체적인 이름 표현이 가능하고, 객체의 리턴 타입이 변경되더라도 유연하게 대처할 수 있다는 점입니다. PremiumBooking인 경우에만 별도의 인스턴스(PremiumDelegete)를 생성하여 조건에 행위 처리를 위임을 통해 진행하고 있는 것을 확인하실 수 있습니다.

Reference

profile
매일 성장하는 개발자가 되고 싶습니다. 😊

0개의 댓글