리팩토링 9. 객체 통째로 넘기기

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다.


    리팩토링 9. 객체 통째로 넘기기 (Preserve Whole Object)

    • 어떤 한 레코드에서 구할 수 있는 여러 값들을 메서드에 전달하는 경우, 해당 매개변수를 레코드 하나로 교체할 수 있다. 
    • 객체를 통째로 넘기면서, 매개변수 목록을 줄일 수 있다.
      • 향후에 추가할지도 모를 매개변수까지도 줄일 수 있음. 레코드에 포함하는 방식
    • 이 기술을 적용하기 전에 함수가 해당 인스턴스에 의존해도 되는지를 고려해야 함. 
      • 확장 가능성을 고려해야한다. 만약 해당 메서드가 특정 인스턴스에 의존한다고 하면, 다른 도메인에서는 해당 메서드를 사용할 수 없을 수 있다.
      • 예를 들어 Order 도메인 객체에 메서드가 의존하게 된다면, 이 메서드는 Person 도메인에서는 전혀 사용할 수 없게 되어버리기 때문이다.
    • 어쩌면 해당 메서드의 위치가 적절하지 않을 수도 있다. (기능 편애 'Feature Envy" 냄새에 해당한다)

     

    메서드에 전달되는 매개변수가 여러 개 있을 때, 어쩌면 이 매개변수들이 하나의 인스턴스에 속한 경우도 있을 수 있다. 이 때, 각 매개변수를 넘기는 것이 아니라 '인스턴스 통째'로 넘기는 방법도 고려해 볼 수 있다. 각각의 장단점은 다음과 같다.

    • 장점 : 전달되는 매개변수의 숫자를 줄여 읽기 쉬운 메서드를 생성한다.
    • 단점 : 메서드가 특정 객체에 대한 의존성이 생김. 

    따라서 '객체 통째로 넘기기'를 통해서 매개변수를 줄이려고 할 때, 메서드가 이 '객체'에 의존해도 되는지에 대한 고민이 필요하다. 이 부분은 확장 가능성을 고려해야한다.  이후에 고려해야 할 부분은 '해당 메서드의 위치가 적절하지 않을 수도 있다'라는 것을 고려해야 한다. 


    Before

    리팩토링이 되는 대상은 getMarkdownForParticipant()라는 메서드다. 이 메서드를 살펴보면 이렇다.

    • username, Map을 매개변수로 전달받는다.
    • 이 매개변수는 p.username(), p.homework() 메서드를 통해서 전달받는다.  (Participant 타입의 객체 p)

    getMarkdownForParticipant()라는 메서드가 전달받는 두 개의 매개변수는 모두 Participant 인스턴스가 가지고 있는 필드 값이다. 따라서 Participant 인스턴스를 통째로 넘겨서 매개변수의 숫자를 2 → 1로 줄일 수 있다. 이렇게 하려고 했을 때 고려해야 할 부분은 getMarkdownForParticipant() 메서드가 Participant 도메인에서만 사용되어도 괜찮은지다. 

    • getMarkdownForParticipant() 메서드가 Participant를 매개변수로 받으면, Participant 도메인에서만 사용된다.
    • 만약 다른 도메인에서도 사용될 계획이 있는 메서드라면 기존처럼 Primitive 타입을 받아야 한다. 

    그런데 이 메서드는 Participant에서만 사용될 것이기 때문에 매개변수를 Participant로 사용하기로 했다! 

    double getRate(Map<Integer, Boolean> homework) {
        long count = homework.values().stream()
                .filter(v -> v == true)
                .count();
        return (double) (count * 100 / this.totalNumberOfEvents);
    }
    
    // 현재 Participant 객체의 값을 각각 primitive 타입으로 받고 있음.
    // 이 메서드가 Participant 객체에만 의존해도 될까? (다른 도메인에서는 사용되지 않을까?)
    private String getMarkdownForParticipant(String username, Map<Integer, Boolean> homework) {
        return String.format("| %s %s | %.2f%% |\n", username,
                checkMark(homework, this.totalNumberOfEvents),
                getRate(homework));
    }

    Before2

    getMakrdownForParticipant 클래스에서 Participant에 의존하기로 결정했다. 따라서 getMarkdownForParticipant() 내부에 있는 메서드들도 Participant에 따라서 정리할 수 있는지 살펴봐야하는데, 특정 메서드가 이 클래스에 있는 것이 타당한지를 살펴보는 것이다. getRate() 함수가 그 예시다. getRate()는 StudyDashboard 클래스에 존재하는 것이 맞을까? 

    • getRate()에 필요한 모든 값으니 Participant의 homework 필드에 존재한다. 필요한 모든 값은 Participant 인스턴스에 존재함. 
    • 또한 Participant의 참석율이기 때문에 문맥상 Participant에 있는 것이 더욱 적절함. 

    이런 이유 때문에 getRate()는 StudyDashboard 클래스보다는 Participant 클래스로 이동하는 것이 더욱 적절할 것이다. 

    // 얘는 이 곳에 위치하는게 적절하지 않을 수 있다.
    // 참석율을 계산하는 것인데, 필요한 모든 값은 Participant 객체 내부에 존재함.
    // 특정 참석자의 참가율은 참석자 인스턴스에서 구하는게 더 합리적이다.
    double getRate(Map<Integer, Boolean> homework) {
        long count = homework.values().stream()
                .filter(v -> v == true)
                .count();
        return (double) (count * 100 / this.totalNumberOfEvents);
    }

     

    After

    위의 내용을 참고해서 아래와 같이 리팩토링 했다.

    1. getMarkdownForParticipant()는 특정 도메인 Participant에 의존하기로 결정되었다. 다른 곳에서 사용되지 않을 것이기 때문이다.
    2. 따라서 getMarkdownForParticipant() 내부에서 사용되고 있는 메서드들의 수정도 필요하다. getRate()는 Participant 객체의 값으로만 처리되는데, 의미를 봤을 때 Participant 인스턴스의 멤버로 있는 것이 더욱 합당하기 때문에 이동함. 
    // StudyDashboard.class
    // 현재 Participant 객체의 값을 각각 primitive 타입으로 받고 있음.
    // 이 메서드가 Participant 객체에만 의존해도 될까? (다른 도메인에서는 사용되지 않을까?)
    // -> 다른 도메인에서는 사용되지 않을 것이기 때문에 Participant 객체를 받도록 수정
    private String getMarkdownForParticipant(Participant participant) {
        return String.format("| %s %s | %.2f%% |\n",
                participant.username(),
                checkMark(participant.homework(), this.totalNumberOfEvents),
                participant.getRate(this.totalNumberOfEvents));
    }
    public record Participant(String username, Map<Integer, Boolean> homework) {
        public Participant(String username) {
            this(username, new HashMap<>());
        }
        public void setHomeworkDone(int index) {
            this.homework.put(index, true);
        }
    
        public double getRate(int totalNumberOfEvents) {
            long count = this.homework.values().stream()
                    .filter(v -> v == true)
                    .count();
            return (double) (count * 100 / totalNumberOfEvents);
        }
    }
    

    FutureMore

    이렇게 Participant Record에는 메서드가 하나 추가되었다. 이렇게 Record에 메서드가 추가되다보면, 이 녀석은 언젠가 Record에서 하나의 Class로 발전할 수도 있게 될 것이다.

    댓글

    Designed by JB FACTORY