👀 짧은 함수 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 하였다.
출처 : 인프런 강의 (백기선 - 코딩으로 학습하는 리팩토링)
'Programming > 리팩토링' 카테고리의 다른 글
[리팩토링 기술-6] 단계 쪼개기 / 클래스 추출하기 (0) | 2022.11.10 |
---|---|
[리팩토링 기술-5] 반복문을 처리하는 리팩토링 (0) | 2022.11.08 |
[리팩토링 기술-4] 가변데이터를 처리하는 리팩토링 (0) | 2022.11.08 |
[리팩토링 기술-3] 긴 매개변수를 처리하는 리팩토링 (0) | 2022.11.08 |
[리팩토링 기술-1] 중복코드를 처리하는 리팩토링 (0) | 2022.11.07 |