✍️ 악취 : 긴 함수
메서드가 너무 길다면 가독성이 떨어지고 짧은 메서드는 읽는 이로 하여금 많은 문맥 전환을 요구한다. 대부분의 상황에서 너무 긴 메서드는 의도를 파악하기 어렵기 때문에 짧은 메서드에 비해 리팩토링 우선순위가 높다.
긴 메서드를 위해 세 가지 리팩토링을 활용할 수 있다.
1. 함수 추출하기 (임시 변수를 질의 함수로, 매개변수 객체 만들기, 객체 통째로 넘기기, 함수를 명령으로 바꾸기)
2. 조건문 분해햐기
3. 반복문 쪼개기
🍊 함수 추출하기
1. 임시 변수를 질의 함수로
변수를 활용하면 반복되는 동일한 계산을 피할 수 있고 변수의 이름으로 의미를 표현할 수도 있다. 하지만 메서드를 추출할 때 변수는 걸림돌이 될 수 있다. 이때 변수도 마찬가지로 별도의 메서드로 추출한다면 기존에 추출한 메서드의 매개변수를 줄일 수 있다.
String으로 빌드 하는 코드를 별도의 메서드로 추출하고 싶다면
public class StudentService {
/**
* 학생 성적 요약 출력
*/
public void printSummary(String name, int kor, int eng, int math) {
int total = (kor + eng + math);
double avg = total / 3.0;
String summary = new StringBuilder()
.append(name).append("_")
.append(kor).append("_")
.append(eng).append("_")
.append(math).append("_")
.append(avg).toString();
}
}
아래와 같이 평균 계산도 별도의 메서드로 빼낸다면 추출한 메서드의 매개변수를 줄일 수 있다.
public class StudentService {
public void printSummary(String name, int kor, int eng, int math) {
String summary = getSummary(name, kor, eng, math);
System.out.println(summary);
}
private String getSummary(String name, int kor, int eng, int math) {
return new StringBuilder()
.append(name).append("_")
.append(kor).append("_")
.append(eng).append("_")
.append(math).append("_")
.append(calculateAvg(kor, eng, math)).toString();
}
private double calculateAvg(int kor, int eng, int math) {
int total = (kor + eng + math);
return total / 3.0;
}
}
2. 매개 변수 객체 만들기
유사한 매개변수들이 여러 메서드에 걸쳐서 나타난다면 매개 변수들을 묶은 자료구조를 만들 수 있다.
그렇게 만든 자료구조는 데이터 간의 관계를 보다 명시적으로 나타낼 수 있고, 함수에 전달할 매개변수를 줄일 수 있고 도메인을 이해하는데 중요한 역할을 하는 클래스로 발전할 수 있다.
플랫한 name, kor, eng, math를 하나의 클래스로 묶어서 관리한다.
public class StudentService {
public void printSummary(Student student) {
String summary = getSummary(student);
System.out.println(summary);
}
private String getSummary(Student student) {
return new StringBuilder()
.append(student.getName()).append("_")
.append(student.getKorean()).append("_")
.append(student.getEnglish()).append("_")
.append(student.getMath()).append("_")
.append(calculateAvg(student)).toString();
}
private double calculateAvg(Student student) {
int total = (student.getKorean() + student.getEnglish() + student.getMath());
return total / 3.0;
}
}
3. 객체 통째로 넘기기
한 클래스에서 구할 수 있는 여러 값을 매개 변수로 전달하는 경우 해당 매개 변수를 하나의 클래스로 교체할 수 있다. 단 플랫한 매개변수를 클래스로 변경했을 때 발생하는 의존성을 고려해야 한다. 그리고 객체를 통째로 넘겨야 한다는 건 어쩌면 메서드의 위치가 적절하지 않아서 생기는 문제일 수 있다.
사실 학생의 평균 성적을 구하기 클래스 외부 메서드에 학생 객체를 통째로 넘기고, 해당 메서드에서 평균을 계산하는 것은 객체지향스럽지 않다고 생각한다.
@Getter
public class Student {
private String name;
private int korean;
private int english;
private int math;
public double getAvg() {
return (korean + english + math) / 3.0;
}
}
public class StudentService {
public void printSummary(Student student) {
String summary = getSummary(student);
System.out.println(summary);
}
private String getSummary(Student student) {
return new StringBuilder()
.append(student.getName()).append("_")
.append(student.getKorean()).append("_")
.append(student.getEnglish()).append("_")
.append(student.getMath()).append("_")
.append(student.getAvg()).toString();
}
}
4. 함수를 명령으로 바꾸기
함수를 독립적인 커맨드로 만들어 사용할 수 있다.
커맨드 패턴을 적용하면 다음과 같은 장점을 취할 수 있는데, 커맨드 클래스를 통해 복잡한 기능을 구현하는데 필요한 여러 메서드나 필드를 추가할 수 있고 클래스를 사용하므로 상속을 활용해서 확장을 할 수 있다.
파일 쓰기 코드를 별도의 커맨드로 분리할 수 있다.
public class StudentService {
public void printAndSaveSummary(List<Student> students) {
// print
List<String> summaries = students.stream().map(this::getSummary).collect(Collectors.toList());
summaries.forEach(System.out::println);
// save
String fileName = "/summaries.txt";
try (FileWriter fileWriter = new FileWriter(fileName)) {
PrintWriter printWriter = new PrintWriter(fileWriter);
summaries.forEach(printWriter::print);
} catch (IOException e) {
e.printStackTrace();
}
}
private String getSummary(Student student) {
return new StringBuilder()
.append(student.getName()).append("_")
.append(student.getKorean()).append("_")
.append(student.getEnglish()).append("_")
.append(student.getMath()).append("_")
.append(student.getAvg()).toString();
}
}
파일 쓰기 코드를 별도의 SummaryWriter 클래스로 분리했다.
클래스로 분리해서 얻어 갈 수 있는 이점으로 파일 쓰기 코드를 더 복잡하게 구성할 수 있고 향후에 엑셀 파일에 쓰기, 콘솔에 쓰기 등 여러 쓰기 기능이 추가된다면 인터페이스를 정의해서 각기 다른 구현체를 만들 수도 있다.
public class StudentService {
public void printAndSaveSummary(List<Student> students) {
// print
List<String> summaries = students.stream().map(this::getSummary).collect(Collectors.toList());
summaries.forEach(System.out::println);
// write 커맨드로 변경
new SummaryWriter().write(summaries);
}
```
}
public class SummaryWriter {
public void write(List<String> summaries) {
String fileName = "/summaries.txt";
try (FileWriter fileWriter = new FileWriter(fileName)) {
PrintWriter printWriter = new PrintWriter(fileWriter);
summaries.forEach(printWriter::print);
} catch (IOException e) {
e.printStackTrace();
}
}
}
🌱 조건문 분해하기
여러 조건에 따라 달라지는 코드를 작성하다 보면 종종 긴 메서드가 만들어지는 것을 목격할 수 있다.
가령 if문에 명시될 조건 자체가 길어지거나, if와 else에서 해야 할 코드가 길어지는 경우가 그러하다.
조건문에 명시될 조건 혹은 if-else에 포함될 코드를 메서드로 분해하면 보다 좋은 코드를 작성할 수 있다.
// Before
public Student findStudent(String name, List<Student> students) {
Student student = null;
if (students.stream().anyMatch(s -> s.getName().equals(name))) {
student = students.stream().
filter(s -> s.getName().equals(name))
.findFirst().get();
} else {
student = new Student(name);
}
return student;
}
// After
public Student findStudent(String name, List<Student> students) {
Optional<Student> optionalStudent = findExistingStudent(name, students);
if (!optionalStudent.isPresent())
return new Student(name);
return optionalStudent.get();
}
private Optional<Student> findExistingStudent(String name, List<Student> students) {
return students.stream()
.filter(s -> s.getName().equals(name))
.findFirst();
}
🍃 반복문 쪼개기
하나의 반복문에서 여러 다른 작업을 하는 코드를 쉽게 찾아볼 수 있다. 만약 해당 반복문을 수정해야 한다면 여러 작업을 모두 고려해서 수정해야 한다. 반복문을 쪼개면 보다 쉽게 이해할 수 있고 쪼개진 반복문을 메서드로 추출하기도 용이하다.
// Before
public void printMinMaxScores(List<Student> students) {
for (Student student : students) {
int max = Stream.of(student.getEnglish(), student.getKorean(), student.getMath()).
max(Comparator.naturalOrder()).get();
System.out.println("max :" + max);
int min = Stream.of(student.getEnglish(), student.getKorean(), student.getMath())
.min(Comparator.naturalOrder()).get();
System.out.println("min :" + min);
}
}
// After
public void printMinMaxScores(List<Student> students) {
for (Student student : students) {
int max = Stream.of(student.getEnglish(), student.getKorean(), student.getMath()).
max(Comparator.naturalOrder()).get();
System.out.println("max : " + max);
}
for (Student student : students) {
int min = Stream.of(student.getEnglish(), student.getKorean(), student.getMath()).
min(Comparator.naturalOrder()).get();
System.out.println("min : " + min);
}
}
🖥️ 조건문을 다형성으로 바꾸기
여러 타입에 따라 다른 로직을 처리해야 하는 경우 다형성을 적용해서 조건문을 보다 명확하게 분리할 수 있다.
가령 반복되는 switch문을 각기 다른 클래스로 만들어 제거할 수 있다. 공통으로 사용되는 로직은 상위 클래스에 두고 달라지는 하위 클래스에 둠으로써 달라지는 부분만 강조할 수 있다.
옵션을 받아서 평균, 국어, 영어, 수학 순으로 학생을 출력하는 메서드를 다형성을 활용해 리팩토링해보자.
public enum PrintOrder {
AVG, KOR, ENG, MATH;
}
public class PrintService {
public void printStudents(List<Student> students, PrintOrder order) {
switch (order) {
case AVG:
students.stream().
sorted(Comparator.comparing(Student::getAvg).reversed())
.collect(Collectors.toList()).forEach(System.out::println);
break;
case KOR:
students.stream()
.sorted(Comparator.comparing(Student::getKorean).reversed())
.collect(Collectors.toList()).forEach(System.out::println);
break;
case ENG:
students.stream()
.sorted(Comparator.comparing(Student::getEnglish).reversed())
.collect(Collectors.toList()).forEach(System.out::println);
break;
case MATH:
students.stream()
.sorted(Comparator.comparing(Student::getMath).reversed())
.collect(Collectors.toList()).forEach(System.out::println);
break;
}
}
}
기존의 PrintService를 인터페이스로 변경하고 각 case문에 포함된 로직을 별도의 구현체로 정의한다.
public interface PrintService {
void printStudents(List<Student> students);
}
public class AvgOrderPrintService implements PrintService {
@Override
public void printStudents(List<Student> students) {
students.stream().
sorted(Comparator.comparing(Student::getAvg).reversed())
.collect(Collectors.toList()).forEach(System.out::println);
}
}
public class KorOrderPrintService implements PrintService {
@Override
public void printStudents(List<Student> students) {
students.stream()
.sorted(Comparator.comparing(Student::getKorean).reversed())
.collect(Collectors.toList()).forEach(System.out::println);
}
}
```
외부에선 별도의 옵션을 넘길 필요 없이 구현체만 변경해서 원하는 형태로 출력할 수 있다.
public static void main(String[] args) {
PrintService printService = new AvgOrderPrintService();
printService.printStudents(Collections.emptyList());
printService = new KorOrderPrintService();
printService.printStudents(Collections.emptyList());
}
'리팩토링' 카테고리의 다른 글
[리팩토링] 악취 6 : 가변 데이터 (0) | 2022.11.27 |
---|---|
[리팩토링] 악취 5 : 전역 변수 (0) | 2022.11.13 |
[리팩토링] 악취 4. 긴 매개변수 목록 (0) | 2022.11.06 |
[리팩토링] 악취 2. 중복 코드 (0) | 2022.10.29 |
[리팩토링] 악취 1. 이해하기 힘든 이름 (0) | 2022.10.26 |