리팩토링 10. 함수를 명령으로 바꾸기

    들어가기 전

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


    리팩토링 10. 함수를 명령으로 바꾸기 (Replace Function with Command)

    • 함수를 독립적인 객체인, Command로 만들어 사용할 수 있다. 
    • 커맨드 패턴을 적용하면 다음과 같은 장점을 취할 수도 있다.
      • 부가적인 기능으로 undo 기능을 만들 수도 있다.
      • 더 복잡한 기능을 구현하는데 필요한 여러 메소드들을 추가할 수 있다.
      • 상속이나 템플릿을 활용할 수도 있다. 
      • 복잡한 메서드를 여러 메서드나 필드를 활용해 쪼갤 수도 있다. 
    • 대부분의 경우에 '커맨드' 보다는 '함수'를 사용하지만, 커맨드 말고 다른 방법이 없는 경우에만 사용한다. 

     

    함수 하나를 독립적인 커맨드 클래스로 분리해내면 다음 장점을 가진다.

    1. 긴 메서드를 간추리는데 좋다.
    2. 커맨드로 분리된 오퍼레이션 자체에 필요한 필드, 메서드를 커맨드 클래스로 옮길 수 있다. 즉, 코드 복잡도를 낮출 수 있다. 

    단점은 그만큼 새로운 클래스가 생성되거나 구조가 변경되기 때문에 이 방면으로 복잡할 수도 있다는 점이다. 장단점이 있는데 언제 사용하면 좋을까? 우선은 '함수 추출'로 처리가 가능한지 고민해본다. 그런데 만약 다음과 같은 생각이 든다면 커맨드로 분리하는 것이 좋다.

    1. 함수가 이 클래스에 있으면 안될 것 같다
    2. 이 부분 향후 더 복잡해 질 수도 있을 것 같다

    이를 통해 한 클래스에 집중된 코드 복잡도를 분리해줘서 이익을 가져가는 것이다. 손익 분기점 쯔음에 커맨드 클래스로 분리를 해보자는 것이다. 

     


    Before

    아래 코드 중 FileWriter 쪽을 살펴보자. 이 코드는 마크다운을 생성하는 코드다. 먼저 이 코드만 봤을 때는 의미가 와닿지 않기 때문에 메서드로 추출한 후, execute()라는 이름을 붙여주자. 그런데 한 가지 고민거리가 있다. 

    execute() 메서드는 향후 확장 가능성이 있다. 예를 들어 지금은 마크다운만 출력하지만, 콘솔 출력 / CSV 출력 등의 작업이 있을 수 있다. 이 부분이 복잡해 질 수 있기 때문에 확장 가능성 + 코드 복잡도의 감소를 고려해서 커맨드 클래스로 분리해본다. 

    1. 해당 코드 부분은 execute() 메서드로 분리한다.
    2. 커맨드 클래스 역할을 할 StudyPrinter 클래스를 생성하고, execute() 메서드를 옮겨준다. 해당 메서드를 public으로 변경해준다. 
    3. execute() 메서드에서 필요한 header(), markdownForParticipant() 같은 메서드들을 StudyPrinter 클래스로 옮겨준다. 
    4. StudyPrinter에서 필요한 값이 totlaNumberOfEvents이기 때문에 StudyPrinter의 필드로 선언한다.
    5. 또한 StudyPrinter가 단순히 실행만 하는 녀석이라면, 굳이 execute() 메서드에 Participant 배열을 넘겨줄 필요없이 필드값으로 가진다. 

    이렇게 작성하면 execute()로 추출된 메서드는 커맨드 클래스로 전환되어 StudyPrinter는 완벽히 '하나의 커맨드'를 표현하는 클래스가 되어버린다. 

    // StudyDashboard.class
    try (FileWriter fileWriter = new FileWriter("participants.md");
        PrintWriter writer = new PrintWriter(fileWriter)) {
        participants.sort(Comparator.comparing(Participant::username));
    
        writer.print(header(participants.size()));
    
        participants.forEach(p -> {
            String markdownForHomework = getMarkdownForParticipant(p);
            writer.print(markdownForHomework);
        });
    }

    After

    리팩토링 후 처음 코드에 비해 절반 가량 코드가 정리가 되었다. 

    new StudyPrinter(totalNumberOfEvents, participants).execute();

    그리고 StudyPrinter 클래스는 다음과 같이 생성되었다.

    public class StudyPrinter {
    
        private final int totalNumberOfEvents;
        private final List<Participant> participants;
    
        public StudyPrinter(int totalNumberOfEvents, List<Participant> participants) {
            this.totalNumberOfEvents = totalNumberOfEvents;
            this.participants = participants;
        }
    
        public void execute() throws IOException {
            try (FileWriter fileWriter = new FileWriter("participants.md");
                 PrintWriter writer = new PrintWriter(fileWriter)) {
                participants.sort(Comparator.comparing(Participant::username));
    
                writer.print(header(participants.size()));
    
                participants.forEach(p -> {
                    String markdownForHomework = getMarkdownForParticipant(p);
                    writer.print(markdownForHomework);
                });
            }
        }
    
        private String header(int totalNumberOfParticipants) {
            StringBuilder header = new StringBuilder(String.format("| 참여자 (%d) |", totalNumberOfParticipants));
    
            for (int index = 1; index <= this.totalNumberOfEvents; index++) {
                header.append(String.format(" %d주차 |", index));
            }
            header.append(" 참석율 |\n");
    
            header.append("| --- ".repeat(Math.max(0, this.totalNumberOfEvents + 2)));
            header.append("|\n");
    
            return header.toString();
        }
    
        private String getMarkdownForParticipant(Participant p) {
            return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, this.totalNumberOfEvents),
                    p.getRate(this.totalNumberOfEvents));
        }
    
        /**
         * |:white_check_mark:|:white_check_mark:|:white_check_mark:|:x:|
         */
        private String checkMark(Participant p, int totalEvents) {
            StringBuilder line = new StringBuilder();
            for (int i = 1 ; i <= totalEvents ; i++) {
                if(p.homework().containsKey(i) && p.homework().get(i)) {
                    line.append("|:white_check_mark:");
                } else {
                    line.append("|:x:");
                }
            }
            return line.toString();
        }
    
    }

    Futuremore

    print 하는 로직은 향후에 더욱 복잡한 변경에도 대응될 수 있도록 커맨드 클래스로 생성되었다. 따라서 추후 확장할 때는 이런 것들을 고려해서 작성해 볼 수 있다. 

    • StudyPrinter를 인터페이스나 추상 클래스로 만들고 하위 클래스를 생성 → 다형성으로 접근
    • 함께 사용하는 totalNumberofEvents, participants는 상위 클래스로 올려버림. 

    댓글

    Designed by JB FACTORY