본문 바로가기

Programming/리팩토링

[리팩토링 기술-2] 긴 함수를 처리하는 리팩토링

👀 짧은 함수 vs 긴 함수

 

- 함수가 길 수록 이해하기 어렵다! vs 짧은 함수는 더 많은 문맥전환을 필요로 한다.

 

- 작은 함수에 "좋은 이름"을 사용했다면, 해당 함수의 코드를 보지 않고도 이해할 수 있다.

 

- 어떤 코드에 "주석"을 남기고 싶다면, 주석 대신 함수를 만들고

함수의 이름으로 "의도"를 표현해보자.

 

 

✍ 긴 코드를 리팩토링할 때 사용할 수 있는 기술

 

1. 99%는 "함수 추출하기"로 해결할 수 있다.

 

2. 함수로 분리하면서 해당함수로 전달해야 할 매개변수가 많아진다면,

다음과 같은 리팩토링을 고려해볼 수 있다.

(1) 임시 변수를 질의 함수로 바꾸기 (Replace Temp With Query)

(2) 매개변수 객체 만들기 (Introduce Parameter Object)

(3) 객체 통째로 넘기기 (Preserve Whole Object)

 

3. "조건문 분해하기 (Decompose Conditional)"를 사용해 분리할 수 있다.

 

4. 같은 조건으로 여러개의 Switch문이 있다면

"조건문을 다형성으로 바꾸기(Replace Conditinal WIth Polymorphism)"을 사용할 수 있다.

 

5. 반복문 안에서 여러 작업을 하고 있어서 하나의 메소드로 추출하기 어렵다면

"반복문 쪼개기(Split Loop)"을 사용할 수 있다.

 

 

 

아래에서 예시와 함께 리팩토링 방법에 대해서 살펴보고자 한다.

 

 


[  Berfore - 전체 코드 ]

 

* 코드 내용

: 이 전에 진행했던 스터디를 총 15번 진행했는데, 

15번 진행하면서 참여했던 모든 참가자들의 참석율을 계산하는 내용

 

 

 

 

👇 한 메소드 안의 코드가 너무 길...다.... 😅😥

 

package me.whiteship.refactoring._03_long_function._01_before;

import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHIssueComment;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StudyDashboard {

    public static void main(String[] args) throws IOException, InterruptedException {
        StudyDashboard studyDashboard = new StudyDashboard();
        studyDashboard.print();
    }

    private void print() throws IOException, InterruptedException {
        GitHub gitHub = GitHub.connect();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        List<Participant> participants = new CopyOnWriteArrayList<>();

        int totalNumberOfEvents = 15;
        ExecutorService service = Executors.newFixedThreadPool(8);
        CountDownLatch latch = new CountDownLatch(totalNumberOfEvents);

        for (int index = 1 ; index <= totalNumberOfEvents ; index++) {
            int eventId = index;
            service.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        GHIssue issue = repository.getIssue(eventId);
                        List<GHIssueComment> comments = issue.getComments();

                        for (GHIssueComment comment : comments) {
                            String username = comment.getUserName();
                            boolean isNewUser = participants.stream().noneMatch(p -> p.username().equals(username));
                            Participant participant = null;
                            if (isNewUser) {
                                participant = new Participant(username);
                                participants.add(participant);
                            } else {
                                participant = participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
                            }

                            participant.setHomeworkDone(eventId);
                        }

                        latch.countDown();
                    } catch (IOException e) {
                        throw new IllegalArgumentException(e);
                    }
                }
            });
        }

        latch.await();
        service.shutdown();

        try (FileWriter fileWriter = new FileWriter("participants.md");
             PrintWriter writer = new PrintWriter(fileWriter)) {
            participants.sort(Comparator.comparing(Participant::username));

            writer.print(header(totalNumberOfEvents, participants.size()));

            participants.forEach(p -> {
                long count = p.homework().values().stream()
                        .filter(v -> v == true)
                        .count();
                double rate = count * 100 / totalNumberOfEvents;

                String markdownForHomework = String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), rate);
                writer.print(markdownForHomework);
            });
        }
    }

    /**
     * | 참여자 (420) | 1주차 | 2주차 | 3주차 | 참석율 |
     * | --- | --- | --- | --- | --- |
     */
    private String header(int totalEvents, int totalNumberOfParticipants) {
        StringBuilder header = new StringBuilder(String.format("| 참여자 (%d) |", totalNumberOfParticipants));

        for (int index = 1; index <= totalEvents; index++) {
            header.append(String.format(" %d주차 |", index));
        }
        header.append(" 참석율 |\n");

        header.append("| --- ".repeat(Math.max(0, totalEvents + 2)));
        header.append("|\n");

        return header.toString();
    }

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

 

package me.whiteship.refactoring._03_long_function._01_before;

import java.util.HashMap;
import java.util.Map;

public record Participant(String username, Map<Integer, Boolean> homework) {
    public Participant(String username) {
        this(username, new HashMap<>());
    }

    public double getRate(double total) {
        long count = this.homework.values().stream()
                .filter(v -> v == true)
                .count();
        return count * 100 / total;
    }

    public void setHomeworkDone(int index) {
        this.homework.put(index, true);
    }
}

 

 

 

 


🙌 리팩토링 - 임시 변수를 질의 함수로 바꾸기 (Replace Temp With Query)

 

- 변수를 사용하면 반복해서 동일한 식을 계산하는 것을 피할 수 있고,

이름을 사용해 의미를 표현할 수도 있다.

 

- 긴 함수를 리팩토링할 때, 임시 변수를 함수로 추출하여 분리한다면

빼낸 함수로 전달해야 할 매개변수를 줄일 수 있다.

 

 

[  Berfore  ]

 

아래 예제 코드 print() 메서드의 마지막 부분이다.

  • rate 를 계산해주기 위한 일련의 로직이 길게 작성되어 있으며 그 과정에 있어 임시 변수를 사용한다. 또한 formating 해주는 부분을 읽기 힘들다..
  • 위 코드는 의도를 표현한 것이 아닌 구현을 표현한 코드이다. → 리팩토링 해줄 필요가 있어 보인다.

 

 

 

[  After 

- 함수 추출하기를 1차적으로 진행한 후, 

- getRate()라는 메소드 자체를 매개변수로 넘긴것을 확인할 수 있다.

 

 

 


🙌 리팩토링 - Introduce Field

 

- 같은 매개변수가 여러 메소드에 걸쳐 나타난다면,

그 매개변수를 필드로 선언하여 

함수에 전달할 매개변수 개수를 줄일 수 있다.

 

 

[  Berfore  ]

 

 

 

 

 

[  After 

 

- header() 메소드, getMarkdownForParticipant() 메소드, getRate() 메소드에서

매개변수로 보내던 totalNumberOfEvents가 빠진 것을 확인할 수 있다.

 

totalNumberOfEvents을 아래와 같이 필드로 선언했기 때문이다. 

 

 

 

 

 

 


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

 

- 메소드로 넘기는 파라미터가 여러개인 경우 -> 하나의 오브젝트에서 파생된 값들인 경우가 종종있다.

이 때 각각 넘기지 않고 오브젝트 타입으로 넘김으로써 파라미터 개수를 줄이는 기법이다.

 

- 단, 이 기술을 적용하기 전에 의존성을 고려해야 한다.

 

 

[  Berfore  ]

매개변수를 객체화할 수 있을 것 같다.

 

 

 

[  After 

  • 위의 코드의 매개변수를 Participant 객체로 만들어 매개변수를 줄인다.
  • 더 나아가서 내부의 getRate() 함수도 Participant 내부에서 사용한다.

 

 

 


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

 

- 함수를 독립적인 객체인 Command로 만들어 사용할 수 있다.

 

- 커맨드 패턴을 적용하면, 다음과 같은 장점을 취할 수 있다.

 (1) 부가적인 기능으로 undo 기능을 만들 수도 있다.

 (2) 더 복잡한 기능을 구현하는데 필요한 여러 메소드를 추가할 수 있다.

 (3) 상속이나 템플릿을 활용할 수도 있다.

 (4) 복잡한 메소드를 여러 메소드나 필드를 활용해 쪼갤 수도 있다.

 

- 하지만 새로운 클래스가 늘어나 복잡도가 증가한다는 단점이 있기는 하다.

 

- 먼저는 함수를 분리해내되,

이 코드가 다른 곳에 있어야 할 것 같다거나 향후 더 복잡해질 수 있을 것 같다면 

별도의 클래스로 분리해내는 것도 좋은 방법중 하나이다.

 

 

 

[  Berfore  ]

- print() 함수 내부에서 실질적으로 print를 해주는 로직을 객체화하여 사용하면 깔끔해지지 않을까?

 

 

 

[  After 

 

- StudyPrinter라는 클래스를 새로 만들어 분리했기 때문에,

print를 하는 로직은 향후 복잡하게 만들기 유리해졌다.

 

 

 

 

 

 


🙌 리팩토링 -  조건문 분해하기 (Decompose Conditional)

 

- 여러 조건에 따라 달라지는 코드를 작성하다보면

종종 긴 함수가 만들어지는 것을 목격할 수 있다.

 

- "조건"과 "액션" 모두 "의도"를 표현해야 한다.

 

- 기술적으로는 "함수 추출하기"와 동일한 리팩토링이지만, 의도만 다를 뿐이다.

 

 

 

[  Berfore  ]

 

- 조건에 맞게 분기처리를 하다보니 함수가 길어졌고, 구현을 표현한 코드들이 다분하다.

조건에 맞게 분기처리를 하다보니 함수가 길어졌고, 구현을 표현한 코드들이 다분하다.

 

 

[  After 

 

- 새롭게 가입한 참가자면 저장하고, 기존 가입자라면 정보를 찾아온다는 "의도"를 보다 직관적으로 파악할 수 있다. 

- 조건문을 isNewParticipant() 함수로 추출하였으며, 각 조건에 맞는 구현을 추출하여 의도로 표현하였다.

 

 

 

 


🙌 리팩토링 -  반복문 쪼개기 (Split Loop)

 

- 하나의 반복문에서 여러 다른 작업을 하는 코드를 쉽게 찾아볼 수 있다.

 

- 해당 반복문을 수정할 때, 여러 작업을 모두 고려해야 한다.

 

- 반복문을 여러개로 쪼개면 보다 쉽게 이해하고 수정할 수 있다.

 

- 성능문제를 야기할 수 있지만, "리팩토링"은 "성능 최적화"와 별개작업이다.

리팩토링을 마친 이후에 성능 최적화를 시도할 수 있다.

 

 

[  Berfore  ]

 

- 쓰레드 run() 함수 내부 for문에서 과제를 끝낸 참가자를 찾는 로직과, 첫번째로 끝낸 참가자를 찾는다.

=> 하나의 반복문에서 여러 작업을 한다.

 

 

 

[  After 

- participants는 매개변수로 많이 전달되며, 클래스 내에서 공용으로 쓰이는 필드이다. 

그래서 지역변수에서 필드로 올려줄 수가 있다.

 

지역변수였던 participants와 firstParticipantsForEachEvent를 필드로 올려주었다.

 

- checkGithubIssues() 메소드와 findFirst() 메소드를 통해 

반복문을 분리해냈다. 

 

 

 

 


🙌 리팩토링 -  조건문을 다형성으로 바꾸기 (Replace Conditinal with Polymorphism)

 

- 여러 타입에 따라 각기 다른 로직으로 처리해야 하는 경우에

다형성을 적용해서 조건문을 보다 명확하게 분리할 수 있다.

 

예를들어, switch문이나 if문을 각기 다른 클래스를 만들어 제거할 수 있다.

 

- 공통으로 사용되는 로직은 상위클래스에 두고,

달라지는 부분만 하위클래스에 둠으로써

달라지는 부분만 강조할 수 있다.

 

- 모든 조건문을 다형성으로 바꿔야 하는 것은 아니다. 

 

 

[  Berfore  ]

 

- excute() 함수 내부 printerMode 조건에 따라 수행이 다르다.

 

 

 

 

[  After 

 

- StudyPrinter를 상속하는 

1. CvsPrinter

2. ConsolePrinter

3. MarkdownPrinter 를 만든다.

 

- 조건문이 들어가는 함수를 추상 메서드로 바꾸고, 해당 클래스를 추상 클래스로 변경하였다.


- 또한 checkMark() 함수는 어떠한 printerMode 에서도 공통적으로 사용하기 때문에 pull up method 하였다.

 

 

 

 

 

 

 

 

 

 

 

 

출처 : 인프런 강의 (백기선 - 코딩으로 학습하는 리팩토링)