BackEnd/Refactoring

[Refactoring] 중재자 (Middle Man), 내부자 거래(Insider Trading), 거대한 클래스 (Large Class)

샤아이인 2023. 1. 8.

백기선 님의 리팩터링 강의를 들으며 요약한 내용입니다.

 

15. 중재자

캡슐화는 내부의 구체적인 정보를 최대한 감출 수 있기 때문에 자주 사용된다.

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

 

15 - 1) 중재자 제거하기

필요한 캡슐화의 정도는 상황에 따라서 달라질수가 있다.

흔히 말하는 Law of Demeter를 지나치게 따르기 보다는상황에 맞게 활용하도록 하자!

 

예를 들어 원래는 Person에 getManager()를 통해 한번에 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();
    }
}

 

하지만, getDepartment()를 만들어서 Person으로부터 직접 department를 불러온 후, 메서드 체인을 통해 한번더 manager를 로딩해올수도 있다. (물론 나라면 그냥 getManager()를 사용하는 위 코드가 더 책임과 역할에 합당하다 생각한다)

 

사용하는 client입장의 코드는 다음과 같이 변경될 것 이다.

// 중재자 제거 전
person.getManager();

// 중재자 제거 후
person.getDepartment().getManager();

 

위의 코드는 메시지 체인을 통하여 과도한 구현을 공개하지 않는다는점이 좋지만, 너무 과한 캡슐화 또한 지양해야 한다는점을 상기하고 넘어가자!

 

15 - 2) 슈퍼클래스를 위임으로 바꾸기

객체지향에서의 "상속"은 기존의 기능을 재사용하는 가장 쉬운 방법이지만, 항상 적절한 방법은 아니다.

 

예를 들어 서브클래스가 슈퍼클래스의 자리를 대체하더라도 잘 동작해야 한다는 LSP원칙을 생각해보자.

Stack을 사용할때 List를 상속하는것이 과연 적합할까? 아니다, Stack은 인덱스로 접근할수 없지만 부모인 List는 인덱스 접근이 가능하다.

 

따라서 상속은 항상 잘 생각해보고 장/단 점을 잘 생각하며 사용해야 한다.

 

다음 코드를 살펴보자!

▶ Scroll

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

 

15 - 3) 서브클래스를 위임으로 바꾸기

어떤 객체의 행동이 카테고리에 따라 바뀐다면, 보통 상속을 사용해서 일반적인 로직은 슈퍼클래스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현한다.

 

하지만, 대부분의 프로그래밍 언어에서 상속은 오직 한번만 사용할 수 있다.

 

1) 만약에 어떤 객체를 두가지 이상의 카테고리로 구분해야 한다면?

=> 위임을 사용하면 얼마든지 여러가지 이유로 여러 다른 객체로 위임을 할 수 있다.

 

슈퍼클래스가 바뀌면 모든 서브클래스에 영향을 줄 수 있다. 따라서 슈퍼클래스를 변경할 때 서브클래스까지 신경써야 한다.

 

2) 만약에 서브클래스가 전혀 다른 모듈에 있다면?

=> 위임을 사용한다면 중간에 인터페이스를 만들어 의존성을 줄일 수 있다.

 

상속 대신 위임을 선호하라.”는 결코 상속은 나쁘다.”라는 말이 아니다.

처음엔 상속을 적용하고 언제든지 이런 리팩토링을 사용해 위임으로 전환할 수 있다.

 

다음 예시코드를 살펴보자!

 

▶ Booking

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

}

 

▶ PremiumBooking

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을 잘 상속한 예시라 생각된다.

 

하지만 어떠한 이유 하에서 이러한 상속구조를 제거해야 한다면?

1) 위임자(PremiumDelegate)를 하나 만들어 기존의 PremiumBooking의 로직을 이동시키고

2) Booking의 로직에서 PremiumDelegate를 사용하도록 변경

2) PremiumBookgin은 제거하자!

 

PremiumDelegate로 이동 시킬 로직으로는 hasTalkback, basePrice, hasDinner가 있다.

 

▶ PremiumDelegate

public class PremiumDelegate {

    private Booking host;
    private PremiumExtra premiumExtra;

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

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

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

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

 

▶ Booking

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 premiumExtra) {
        Booking booking = createBooking(show, time);
        booking.premiumDelegate = new PremiumDelegate(booking, premiumExtra);
        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();
    }
}

Booking의 경우 위임자를 사용하게 된 후부터는, premiumDelegate가 Null인지? 에 따라서 로직이 나뉘게 된다.

Booking스스로가 premiumDelegate인지에 따라서 로직을 다르게 수행하게 된다.

서브클래스를 위임으로 바꾼 것 이다!

 

16. 내부자 거래

어떤 모듈이 다른 모듈의 내부 정보를 지나치게 많이 알고 있는 코드 냄새. 그로인해 지나 치게 강한 결합도(coupling)가 생길 수 있다.

적절한 모듈로 함수 옮기기 (Move Function)”필드 옮기기 (Move Field)”를 사용해 서 결합도를 낮출 수 있다.

 

여러 모듈이 자주 사용하는 공통적인 기능은 새로운 모듈을 만들어 잘 관리하거나, “위임 숨기기 (Hide Delegate)”를 사용해 특정 모듈의 중재자처럼 사용할 수도 있다.

 

상속으로 인한 결합도를 줄일 때는 슈퍼클래스 또는 서브클래스를 위임으로 교체하기를 사용할 수 있다.

 

17. 거대한 클래스

어떤 클래스가 너무 많은 일을 하다보면 필드도 많아지고 중복 코드도 보이기 시작한다.

클라이언트가 해당 클래스가 제공하는 기능 중에 일부만 사용한다면 각각의 세부 기능을 별도의 클래스로 분리할 수 있다.

 

1) "클래스 추출하기"를 통해 관련있는 필드를 한곳으로 모을 수 있다.

2) 상속구조를 만들 수 있다면 "슈퍼클래스 추출하기" 또는 "타입 코드를 서브클래스로 교체하기"를 적용할 수 있다.

3) 클래스 내부에 산재하는 중복코드는 메소드를 통해 추출한다.

댓글