[Refactoring] 냄새 3. 긴 함수 (1)
20 Sep 2022해당 포스트는 inflearn의 백기선님의 강의인 리팩토링 을 듣고 정리한 글입니다.
냄새 3. 긴 함수 (Long Function) (1)
- 짧은 함수 vs 긴 함수
- 함수가 길 수록 더 이해하기 어렵다 vs 짧은 함수는 더 많은 문맥 전환을 필요로 한다.
- “과거에는” 작은 함수를 사용하는 경우에 더 많은 서브루틴 호출로 인한 오버헤드가 있었다.
- 작은 함수에 “좋은 이름”을 사용했다면 해당 함수의 코드를 보지 않고도 이해할 수 있다.
- 어떤 코드에 “주석”을 남기고 싶다면, 주석 대신 함수를 만들고 함수의 이름으로 “의도”를 표현해보자.
- 사용할 수 있는 리팩토링 기술
- 99%는 “함수 추출하기 (Extract Function)“로 해결할 수 있다. (* 함수추출하기의 Hint는 주석이다, 주석을 토대로 기능을 쪼개자)
- 함수로 분리하면서 해당 함수로 전달해야 할 매개변수가 많아진다면 다음과 같은 리팩토링을 고려해볼 수 있다.
- 임시 변수를 질의 함수로 바꾸기 (Replace Temp with Query)
- 매개변수 객체 만들기 (Introduce parameter Object)
- 객체 통째로 넘기기 (Preserve Whole Object)
- 조건문이 너무 복잡해진다면, “조건문 분해하기 (Decompose Conditional)”를 사용해 조건문을 분리할 수 있다.
- 같은 조건으로 여러개의 Switch 문이 있다면, “조건문을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)”을 사용할 수 있다.
- 반복문 안에서 여러 작업을 하고 있다면, 그 함수는 하나의 일을 하는것이 아니다, 그래서 하나의 메소드로 추출하기 어렵다면, “반복문 쪼개기 (Splict Loop)”를 적용할 수 있다.
예제코드는 repository의 참여자에 대한 참석율을 계산하여 마크다운파일로 추출하는 로직이다.
길다.. 먼저 print()
내부의 메소드에의 for문에서 참석율을 저장해서 이후 try{}
내부에서 마크다운파일로 저정하는 로직이다.
리팩토링 7. 임시 변수를 질의 함수로 바꾸기
- 변수를 사용하면 반복해서 동일한 식을 계산하는 것을 피할 수 있고, 이름을 사용해 의미를 표현할 수도 있다.
- 긴 함수를 리팩토링 할 때, 그러한 임시변수를 함수로 추출하여 분리한다면 빼낸 함수로 전달해야 할 매개변수를 줄일 수 있다.
예제 코드
- Participant
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);
}
}
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();
}
}
먼저 try 내부의 파일 생성 로직은 건드려서 매개변수를 줄여보도록 하자.
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);
});
}
해당 로직은 참석자를 정렬 한 후, 참석자 만큼 반복문을 돌려서 String.format을 입혀 아래와 같이 출력한다.
| 참여자 (417) | 1주차 | 2주차 | 3주차 | 4주차 | 5주차 | 6주차 | 7주차 | 8주차 | 9주차 | 10주차 | 11주차 | 12주차 | 13주차 | 14주차 | 15주차 | 참석율 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0417taehyun |:white_check_mark:|:white_check_mark:|:white_check_mark:|:x:|:x:|:x:|:x:|:x:|:x:|:x:|:x:|:x:|:x:|:x:|:x: | 20.00% |
markdown으로 출력하는 코드의 양식을 읽기가 힘드므로, 따로 메소드를 추출하여보면 더 수월하기 확인 할 수 있다.
String markdownForHomework = String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), rate);
중복되는 파라미터인 p
, totalNumberOfEvents
, rate
를 매개변수로 빼준뒤 getFormat이라는 메소드로 추출해준다.
String markdownForHomework = getMarkdownForParticipant(totalNumberOfEvents, p, rate);
...
private String getMarkdownForParticipant(int totalNumberOfEvents, Participant p, double rate) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), rate);
}
하지만, 매개변수가 3개가 되면 조금 많다고 느껴질 수 있으니, rate를 제거해주도록 해보자.
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 = getMarkDownForParticipant(totalNumberOfEvents, p, rate);
writer.print(markdownForHomework);
});
}
...
private String getMarkDownForParticipant(int totalNumberOfEvents, Participant p, double rate) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), rate);
}
먼저 아래와 같이 getRate
라는 함수로 추출해주자.
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 -> {
double rate = getRate(totalNumberOfEvents, p);
String markdownForHomework = getMarkDownForParticipant(totalNumberOfEvents, p, rate);
writer.print(markdownForHomework);
});
}
...
private 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 getMarkDownForParticipant(int totalNumberOfEvents, Participant p, double rate) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), rate);
}
함수 추출 이후 자세히 살펴보면, getRate
의 메소드와 getMarkDownForParticipant
의 메소드는 rate를 제외하고는 동일한 매개변수를 사용하게 된다.
이후 getRate()
함수를 이용해 추출한 rate를 넘겨주는것이 아니라, getMarkDownForParticipant()
내부에서 직접 rate를 생성하면 매개변수를 하나 줄일 수 있게된다.
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 -> {
String markdownForHomework = getMarkDownForParticipant(totalNumberOfEvents, p);
writer.print(markdownForHomework);
});
}
...
private 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 getMarkDownForParticipant(int totalNumberOfEvents, Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), getRate(totalNumberOfEvents, p));
}
participants.forEach(p -> {})
내부의 getRate()
를 빼서 getMarkDownForParticipant()
로 넘겨준 rate를 지우고 내부에서 직접 호출하였다.
리팩토링 8. 매개변수 객체 만들기
- 같은 매개변수들이 여러 메소드에 걸쳐 나타난다면 그 메소드들은 밀접하게 관련이 있다는 뜻이다, 해당 메소드들은 그 매개변수들을 묶은 자료 구조를 만들 수 있다.
- 그렇게 만든 자료구조는:
- 해당 데이터간의 관계를 보다 명시적으로 나타낼 수 있다.
- 함수에 전달할 매개변수 개수를 줄일 수 있다.
- 도메인을 이해하는데 중요한 역할을 하는 클래스로 발전할 수도 있다.
두가지의 방법을 소개해보고자 한다.
8.1 중복되는 매개변수 객체 만들기
리팩토링을 통해 추출한 함수인 getRate
와 getMarkDownForParticipant
는 동일한 매개변수인 int totalNumberOfEvents
, Participant p
를 가지고 있다.
private 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 getMarkDownForParticipant(int totalNumberOfEvents, Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), getRate(totalNumberOfEvents, p));
}
int totalNumberOfEvents
, Participant p
의 매개변수를 ParticipantPrinter
라는 객체로 추출하여 전달하여 갯수를 줄이고, 보다 함수를 이해할 수 있도록 도와줄 수 있다.
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 -> {
String markdownForHomework = getMarkDownForParticipant(new ParticipantPrinter(totalNumberOfEvents, p));
writer.print(markdownForHomework);
});
}
...
private 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 getMarkDownForParticipant(ParticipantPrinter participantPrinter) {
return String.format("| %s %s | %.2f%% |\n", participantPrinter.p().username(), checkMark(participantPrinter.p(), participantPrinter.totalNumberOfEvents()), getRate(new ParticipantPrinter(participantPrinter.totalNumberOfEvents(), participantPrinter.p())));
}
8.2 클래스의 중복되는 매개변수를 생성자로 빼기
클래스의 코드들을 보면 totalNumberOfEvents
에 해당하는 필드가 정말 곳곳에서 쓰이고있다.
getRate(int totalNumberOfEvents, Participant p)
getMarkdownForParticipant(int totalNumberOfEvents, Participant p)
header(int totalNumberOfEvents, int totalNumberOfParticipants)
checkMark(Participant p, int totalEvents(totalNumberOfEvents))
거의 모든 메소드에서 사용된다고 보면 된다. 저 매개변수를 생성자를 통해 전역필드로 빼주자.
private final int totalNumberOfEvents;
public StudyDashboard(int totalNumberOfEvents) {
this.totalNumberOfEvents = totalNumberOfEvents;
}
public static void main(String[] args) throws IOException, InterruptedException {
StudyDashboard studyDashboard = new StudyDashboard(15);
studyDashboard.print();
}
totalNumberOfEvents
를 전역필드로 빼줌으로써 각 메소드에 전달되는 매개변수를 줄일 수 있을 뿐만 아니라, 전역변수를 통해 공통된 수를 사용 할 수 있게 된다.
- 리팩토링 후
public class StudyDashboard {
private final int totalNumberOfEvents;
public StudyDashboard(int totalNumberOfEvents) {
this.totalNumberOfEvents = totalNumberOfEvents;
}
public static void main(String[] args) throws IOException, InterruptedException {
StudyDashboard studyDashboard = new StudyDashboard(15);
studyDashboard.print();
}
private void print() throws IOException, InterruptedException {
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("whiteship/live-study");
List<Participant> participants = new CopyOnWriteArrayList<>();
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(participants.size()));
participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p);
writer.print(markdownForHomework);
});
}
}
private double getRate(Participant p) {
long count = p.homework().values().stream()
.filter(v -> v == true)
.count();
double rate = count * 100 / totalNumberOfEvents;
return rate;
}
private String getMarkdownForParticipant(Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p), getRate(p));
}
/**
* | 참여자 (420) | 1주차 | 2주차 | 3주차 | 참석율 |
* | --- | --- | --- | --- | --- |
*/
private String header(int totalNumberOfParticipants) {
StringBuilder header = new StringBuilder(String.format("| 참여자 (%d) |", totalNumberOfParticipants));
for (int index = 1; index <= totalNumberOfEvents; index++) {
header.append(String.format(" %d주차 |", index));
}
header.append(" 참석율 |\n");
header.append("| --- ".repeat(Math.max(0, totalNumberOfEvents + 2)));
header.append("|\n");
return header.toString();
}
/**
* |:white_check_mark:|:white_check_mark:|:white_check_mark:|:x:|
*/
private String checkMark(Participant p) {
StringBuilder line = new StringBuilder();
for (int i = 1 ; i <= totalNumberOfEvents ; i++) {
if(p.homework().containsKey(i) && p.homework().get(i)) {
line.append("|:white_check_mark:");
} else {
line.append("|:x:");
}
}
return line.toString();
}
}