본문 바로가기

클린코드

[Refactoring] 3. 긴 함수

 

 

코딩으로 학습하는 리팩토링 - 인프런 | 강의

리팩토링은 소프트웨어 엔지니어가 갖춰야 할 기본적인 소양 중 하나입니다. 이 강의는 인텔리J와 자바를 사용하여 보다 실용적인 방법으로 다양한 코드의 냄새와 리팩토링 기술을 설명하고 직

www.inflearn.com

| 인프런 - 백기선님의 코딩으로 학습하는 리팩토링 강의를 수강하며 정리한 글입니다.


긴 함수
  • 코드를 읽으면서 이해하기 난해하거나, 구현을 이해한다고 생각되면 긴 함수이다.
  • 과거에는 작은 함수를 사용하는 경우 더 많은 서브 루틴 호출로 인한 오버헤드가 존재했다.
    • 최근 사용하는 프로그래밍 언어는 최적화가 이루어지기 때문에 고려할 만한 오버헤드가 아니다.

  • 단점
    • 코드를 이해하기 어렵다.

  • 방법
    • 99% 함수 추출하기로 해결할 수 있다.
    • 함수로 분리하면서 해당 함수로 전달해야 할 매개변수가 많아진다면 고려해 볼 리팩토링
      • 임시 변수를 질의 함수로 바꾸기
      • 매개변수 객체 만들기
      • 객체 통째로 넘기기

    • 조건문 분해하기
    • 같은 조건의 여러 개의 Switch 문 -> 조건문을 다형성으로 바꾸기 적용 가능
    • 반복문 안에서 여러 작업을 하고 있어 하나의 함수로 추출하기 어렵다면, 반복문 쪼개기 사용 가능

Refactoring 1. 임시 변수를 질의 함수로 바꾸기

  • 변수 사용
    • 반복해서 동일한 계산을 안 해도 된다.
    • 이름을 사용해서 의미를 표현할 수 있다.

  • 임시 변수를 함수로 추출하여 분리한다면 추출한 함수로 전달해야 할 매개변수가 줄어든다.

Before
public static void main(String[] args) {
	long count = p.homework().values().stream()
                	.filter(v -> v == true)
                	.count();
	double rate = count * 100 / totalNumberOfEvents;

	String markdownForHomework = getMarkdownForHomework(totalNumberOfEvents, p, rate);
}

private String getMarkdownForHomework(int totalNumberOfEvents, Participant p, double rate) {
        return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), rate);
}
After
public static void main(String[] args) {
	String markdownForHomework = getMarkdownForHomework(totalNumberOfEvents, p);
}

private static double getRate(int totalNumberOfEvents, Participant p) {
    long count = p.homework().values().stream()
            .filter(v -> v == true)
            .count();
    double rate = count * 100 / totalNumberOfEvents;
    return rate;
}

private String getMarkdownForHomework(int totalNumberOfEvents, Participant p) {
    return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), getRate(totalNumberOfEvents, p));
}

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

  • 같은 매개변수들이 여러 메서드에서 사용된다면 해당 매개변수들을 묶은 자료 구조를 만들 수 있다.
  • 만들어진 자료구조
    • 해당 데이터간의 관계를 보다 명시적으로 나타낼 수 있다.
    • 함수에 전달할 매개변수 개수를 줄일 수 있다.
    • 도메인을 이해하는데 중요한 클래스로 발전할 수 있다.

Before
public static void main(String[] args) {
	String markdownForHomework = getMarkdownForHomework(totalNumberOfEvents, p);
}

private static double getRate(int totalNumberOfEvents, Participant p) {
    long count = p.homework().values().stream()
            .filter(v -> v == true)
            .count();
    double rate = count * 100 / totalNumberOfEvents;
    return rate;
}

private String getMarkdownForHomework(int totalNumberOfEvents, Participant p) {
    return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), getRate(totalNumberOfEvents, p));
}
After
public static void main(String[] args) {
	String markdownForHomework = getMarkdownForHomework(totalNumberOfEvents, p);
}

private static double getRate(ParticipantPrinter participantPrinter) {
    long count = participantPrinter.p().homework().values().stream()
            .filter(v -> v == true)
            .count();
    double rate = count * 100 / participantPrinter.totalNumberOfEvents();
    return rate;
}

private String getMarkdownForHomework(ParticipantPrinter participantPrinter) {
    return String.format("| %s %s | %.2f%% |\n", participantPrinter.p().username(), checkMark(participantPrinter.p(), participantPrinter.totalNumberOfEvents()), getRate(participantPrinter));
}

public record ParticipantPrinter(int totalNumberOfEvents, Participant p) {
}

Refactoring 3. 객체 통째로 넘기기

  • 여러 개의 값을 함수에 전달할 경우, 레코드 하나로 교체할 수 있다.
  • 매개변수 목록을 줄일 수 있다.
  • 향 후 추가되는 매개변수를 레코드에 추가하면 된다.
  • 적용하기 전 의존성을 고려해야 한다.
  • 해당 메서드의 위치가 적절하지 않을 가능성이 존재한다.

Refactoring 4. 함수를 명령으로 바꾸기

  • 함수를 독립적인 객체인, Command로 만들어 사용 가능
  • 커맨드 패턴 적용 시 장점
    • 부가 기능으로 undo 기능을 만들 수 있다.
    • 더 복잡한 기능을 구현하는데 필요한 여러 메서드 추가 가능
    • 상속이나 템플릿을 활용할 수 있다.
    • 복잡한 메서드를 여러 메서드나 필드를 활용해 쪼갤 수 있다.

  • 단점
    • 복잡도 증가

  • 대부분의 경우 커맨드보단 함수를 사용하지만 커맨드 말고 다른 방법이 없을 경우에만 사용

Refactoring 5. 조건문 분해하기

  • 여러 조건에 따라 달라지는 코드를 작성하면 긴 함수가 작성된다.
  • 조건액션 모두 의도를 표현해야 한다.
  • 기술적으로는 함수 추출하기와 동일한 리팩토링이지만 의도만 다르다.
Before
private Participant findParticipant(String username, List<Participant> participants) {
    Participant participant = null;
    if (participants.stream().noneMatch(p -> p.username().equals(username))) {
        participant = new Participant(username);
        participants.add(participant);
    } else {
        participant = participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
    }
    return participant;
}
After
private Participant findParticipant(String username, List<Participant> participants) {
    Participant participant = null;
    if (isNewParticipant(username, participants)) {
        participant = createNewParticipant(username, participants);
    } else {
        participant = findExistingParticipant(username, participants);
    }
    return participant;
}

private static Participant createNewParticipant(String username, List<Participant> participants) {
    Participant participant = new Participant(username);
    participants.add(participant);

    return participant;
}

private static Participant findExistingParticipant(String username, List<Participant> participants) {
    return participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
}

private static boolean isNewParticipant(String username, List<Participant> participants) {
    return participants.stream().noneMatch(p -> p.username().equals(username));
}

Refactoring 6. 반복문 쪼개기

  • 반복문을 여러 개로 분리하면 코드를 보다 쉽게 이해하고 수정할 수 있다.
  • 성능 문제를 야기할 수 있지만, 리팩토링성능 최적화는 별개의 작업으로 리팩토링 후 성능 최적화를 시도할 수 있다.
Before
public static void main(String[] args) {
    for (GHIssueComment comment : comments) {
        Participant participant = findParticipant(comment.getUserName(), participants);
        participant.setHomeworkDone(eventId);

        if (firstCreatedAt == null || comment.getCreatedAt().before(firstCreatedAt)) {
            firstCreatedAt = comment.getCreatedAt();
            first = participant;
        }
    }
}
After
public static void main(String[] args) {
	checkHomework(comments, eventId);
	Participant first = findFirst(comments);
}

private void checkHomework(List<GHIssueComment> comments, int eventId) {
    for (GHIssueComment comment : comments) {
        Participant participant = findParticipant(comment.getUserName(), participants);
        participant.setHomeworkDone(eventId);
    }
}

private Participant findFirst(List<GHIssueComment> comments) throws IOException {
    Date firstCreatedAt = null;
    Participant first = null;
    for (GHIssueComment comment : comments) {
        Participant participant = findParticipant(comment.getUserName(), participants);

        if (firstCreatedAt == null || comment.getCreatedAt().before(firstCreatedAt)) {
            firstCreatedAt = comment.getCreatedAt();
            first = participant;
        }
    }
    return first;
}

Refactoring 7. 조건문을 다형성으로 바꾸기

  • 여러 타입에 따라 각기 다른 로직으로 처리해야 할 경우 다형성을 적용하여 조건문을 보다 명확하게 분리할 수 있다.
  • 반복되는 조건문을 각기 다른 클래스를 만들어 제거할 수 있다.
  • 공통으로 사용되는 로직은 상위 클래스에 두고 달라지는 부분만 하위 클래스에 둠으로써, 달라지는 부분만 강조할 수 있다.
  • 모든 조건문을 다형성을 바꿔야 하는 것은 아니다. -> 정말 복잡하고 달라지는 부분이 있을 경우에만 적용하는 것이 좋다.
Before
public void execute() throws IOException {
    switch (printerMode) {
        case CVS -> {
            try (FileWriter fileWriter = new FileWriter("participants.cvs");
                 PrintWriter writer = new PrintWriter(fileWriter)) {
                writer.println(cvsHeader(this.participants.size()));
                this.participants.forEach(p -> {
                    writer.println(getCvsForParticipant(p));
                });
            }
        }
        case CONSOLE -> {
            this.participants.forEach(p -> {
                System.out.printf("%s %s:%s\n", p.username(), checkMark(p), p.getRate(this.totalNumberOfEvents));
            });
        }
        case MARKDOWN -> {
            try (FileWriter fileWriter = new FileWriter("participants.md");
                 PrintWriter writer = new PrintWriter(fileWriter)) {

                writer.print(header(this.participants.size()));

                this.participants.forEach(p -> {
                    String markdownForHomework = getMarkdownForParticipant(p);
                    writer.print(markdownForHomework);
                });
            }
        }
    }
}
After
public class CvsPrinter extends StudyPrinter {
    public CvsPrinter(int totalNumberOfEvents, List<Participant> participants) {
        super(totalNumberOfEvents, participants);
    }

    @Override
    public void execute() throws IOException {
        try (FileWriter fileWriter = new FileWriter("participants.cvs");
            PrintWriter writer = new PrintWriter(fileWriter)) {
            writer.println(cvsHeader(this.participants.size()));
            this.participants.forEach(p -> {
                writer.println(getCvsForParticipant(p));
            });
            }
    }
}

public class ConsolePrinter extends StudyPrinter {
    public ConsolePrinter(int totalNumberOfEvents, List<Participant> participants) {
        super(totalNumberOfEvents, participants);
    }

    @Override
    public void execute() throws IOException {
        this.participants.forEach(p -> {
            System.out.printf("%s %s:%s\n", p.username(), checkMark(p), p.getRate(this.totalNumberOfEvents));
        });
    }
}

public class MarkdownPrinter extends StudyPrinter {
    public MarkdownPrinter(int totalNumberOfEvents, List<Participant> participants) {
        super(totalNumberOfEvents, participants);
    }

    @Override
    public void execute() throws IOException {
        try (FileWriter fileWriter = new FileWriter("participants.md");
             PrintWriter writer = new PrintWriter(fileWriter)) {

             writer.print(header(this.participants.size()));

             this.participants.forEach(p -> {
                String markdownForHomework = getMarkdownForParticipant(p);
                writer.print(markdownForHomework);
            });
        }
    }
}