본 내용은 인프런 - 백기선님의 리팩토링 강의를 듣고 정리한 내용입니다.
1. 짧은 함수 vs 긴 함수
함수가 길 수록 오히려 더 이해하기 어렵다. 그 이유는 긴 함수의 각 각 문장들을 해석하기 위해 에너지를 많이 써야하고 다양한 함수의 네이밍에 사용된 단어들을 해석하는데 사람마다 지식이 달라 다른 방향으로 해석될 여지가 있다.
짧은 함수는 더 많은 문맥 전환을 필요로 한다.
"과거"에는 작은 함수를 사용하는 경우 더 많은 서브 루틴 호출로 인한 오버헤드가 있었다. (그러나 "오늘"에서는 미미함 )
작은 함수에 "좋은 이름"을 사용했다면 해당 함수의 코드를 보지 않고도 이해가 가능하다.
어떤 코드에 "주석"을 남기고 싶다면, 주석 대신 함수를 만들고 함수의 이름으로 "의도"를 표현해보자
해당 내용에 사용할 수있는 리팩토링 기술은 다음과 같다.
1. 함수 추출하기 (Extract Function)
- 대부분의 내용을 해당 내용으로 리팩토링 가능
- 함수로 분리하면서 해당 함수로 전달해야할 매개변수가 많아진다면?
- 임시 변수를 질의 함수로 바꾸기 (Replce Temp With Query)
- 매개변수 객체 만들기 (Introduce Parameter Object)
- 객체 통째로 넘기기(Preserve Whole Object)
2. 조건문 분해하기 (Decompose Conditional)
3. 조건문을 다형성으로(Replace Conditional with Polymorphism)
- 같은 조건으로 여러개의 Switch 문이 있다면 사용 가능
4. 반복문 쪼개기(Split Loop)
- 반복문 안에서 여러 작업을 하고 있어 하나의 메소드로 추출하기 어렵다면 사용 가능
1. 함수 추출하기
1-1 임시 변수를 질의 함수로 바꾸기 (Replce Temp With Query)
:: 변수를 사용하면서 반복해서 동일한 식을 계산하는 것을 피할 수있고, 이름을 사용해 의미를 표현할 수 있음
:: 긴 함수를 리팩토링 할 때, 임시 변수를 함수로 추출해 분리한다면 뺴낸 함수로 전달해야할 매개 변수를 줄일 수 있음
1-2 매개변수 객체 만들기 (Introduce Parameter Object)
:: 같은 매개변수수들이 여러 메소드에 걸쳐 나타난다면 그 매개변수들을 묶은 자료구조를 만들 수 있음
:: 그렇게 만든 자료 구조는
- 해당 데이터 간의 관계를 명시적으로 나타낼 수 있음
- 함수에 전달할 매대변수 개수를 줄일 수 있음
- 도메인을 이해하는데 중요한 역할을 하는 클래스로도 발전할 수도 있음
public StudyDashboard(int totalNumberOfEvents) {
this.totalNumberOfEvents = totalNumberOfEvents;
}
public static void main(String[] args) throws IOException, InterruptedException {
StudyDashboard studyDashboard = new StudyDashboard(15);
studyDashboard.print();
}
이렇게 윗 코드처럼 생성자에 파라미터 값으로 받아서 해당 클래스의 전역에서 공통으로 사용이 가능하다.
1-3 객체 통쨰로 넘기기 (Preserve Whole Object)
:: 어떤 한 레코드에서 구할 수 있는 여러 값들을 함수에 전달하는 경우, 해당 매개변수를 레코드 하나로 교체 가능
:: 매개변수 목록을 줄일 수 있음
:: 이 기술을 적용하기 전 의존성을 고려해야함
[BEFORE]
participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p.username(), p.homework());
writer.print(markdownForHomework);
});
[AFTER]
participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p);
writer.print(markdownForHomework);
});
기존에는 객체를 전달하지 않고 값만 전달했는데, getMarkDownForParticipant()라는 메서드의 매개변수로 객체를 전달하게 되고, 해당 메서드에서 사용하는 여러 메소드의 값 역시 객체(record)로 바뀔 것이다.
double getRate(Participant participant) {
long count = participant.homework().values().stream()
.filter(v -> v == true)
.count();
return (double) (count * 100 / this.totalNumberOfEvents);
}
코드를 다 고치고나니 문득 이런 생각이 들었다.
Q.1 : 각각의 함수가 레코드형에 의존하는게 맞나? 이것을 고치기 전인 원시타입의 값을 전달하는게 맞나?
만약 이 함수를 다른 도메인에서 사용할 여지가 있다면 원시타입을 선택하는 방향이 맞지만, 반대라면 전자도 괜찮다.
강의에서는 이렇게 레코드형을 가지고 범용적으로 적용하기 전 해당 메소드가 타입에 의존을 해도 되는가?를 한번 고민해봐야한다고 했다.
Q.2 : 과연 이 메서드는 이 위치가 적절한가?
도메인의 의존성도 같이 고려하면서 리팩토링을 진행해야 한다. ( 메서드의 위치는 의존성과 관련있음. )
2. 함수를 명령으로 바꾸기 (Replace Function with Command)
:: 함수를 독립적인 객체, Command로 만들어 사용
:: 커맨드 패턴을 적용하면 다음과 같은 장점을 취할 수 있음
- 부가적인 기능으로 undo 기능 구현 가능
- 더 복잡한 기능을 구현하는 데 필요한 여러 메서드 추가 가능
- 상속이나 템플릿 활용 가능
- 복잡한 메소드를 여러 메소드나 필드를 활용해 쪼갤 수 있음
:: 대부분의 경우에 "커맨드"보다는 "함수"를 사용하지만, 커맨드 말고 다른 방법이 없는 경우에 사용
[BEFORE]
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(this.totalNumberOfEvents, participants).execute();
해당 코드 부분을 하나의 클래스로 추출 후 내부 구현부들도 클래스 내부로 이동
이렇게 분리한 이유는 향후에 해당 부분에 다른 것을 추가하거나 수정하기 더 용이하기 때문이다.
3. 조건문 분해하기 (Decompose Conditional)
:: 여러 조건에 따라 달라지는 코드를 작성하다 보면 긴 함수가 만들어지는 경우가 발생하는데 이 때 "조건"과 "액션"모두 "의도"를 표현해야 한다.
:: 기술적으로는 "함수 추출하기"와 동일한 리팩토링이지만 의도가 다른 차이점이 있다.
[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) {
return isNewParticipant(username, participants) ?
createNewParticipant(username, participants) :
findExistingParticipant(username, participants);
}
private Participant findExistingParticipant(String username, List<Participant> participants) {
return participants.stream().filter(p -> p.username().equals(username)).findFirst()
.orElseThrow();
}
private Participant createNewParticipant(String username, List<Participant> participants) {
Participant participant;
participant = new Participant(username);
participants.add(participant);
return participant;
}
private boolean isNewParticipant(String username, List<Participant> participants) {
return participants.stream().noneMatch(p -> p.username().equals(username));
}
해당 부분에서 다음과 같은 스탭을 밟을 수 있다.
1. [BEFORE] 코드에서 출발
2 다음과 같은 코드로 변경
if (isNewParticipant(username, participants)) {
participant = createNewParticipant(username, participants);
} else {
participant = findExistingParticipant(username, participants);
}
return participant;
3. 삼항 연산자를 이용해 최종 리팩토링[AFTER]
4. 반복문 쪼개기
:: 하나의 반복문에서 여러 다른 작업을 하는경우 반복문을 여러개로 쪼개다보면 보다 쉽게 이해하고 수정이 가능함.
:: 성능상의 문제가 발생할 여지가 있지만 "리팩토링"은 "성능 최적화"와 별개의 작업 ( 리팩토링 -> 성능 최적화 순서로 진행)
[BEFORE]
try {
GHIssue issue = ghRepository.getIssue(eventId);
List<GHIssueComment> comments = issue.getComments();
Date firstCreatedAt = null;
Participant first = null;
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;
}
}
firstParticipantsForEachEvent[eventId - 1] = first;
latch.countDown();
}
5. 조건문을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)
:: 타입에 따라 다른 로직으로 처리해야할 경우 다형성을 적용가능::
:: 공통으로 사용되는 로직은 상위 클래스에 두고, 달라지는 부분만 하위 클래스에 적용::
'Language > Java' 카테고리의 다른 글
[2] Duplicated Code - 중복코드 (0) | 2024.01.22 |
---|---|
[1] 이해하기 힘든 이름 (1) | 2024.01.22 |
Reflection(리플렉션)이란? (0) | 2023.04.01 |
객체 지향적 설계? (0) | 2022.11.21 |
문자열 나누기 - StringTokenizer (0) | 2022.08.03 |