[리팩토링] 악취 3. 긴 함수

 

✍️ 악취 : 긴 함수

메서드가 너무 길다면 가독성이 떨어지고 짧은 메서드는 읽는 이로 하여금 많은 문맥 전환을 요구한다. 대부분의 상황에서 너무 긴 메서드는 의도를 파악하기 어렵기 때문에 짧은 메서드에 비해 리팩토링 우선순위가 높다.

 

긴 메서드를 위해 세 가지 리팩토링을 활용할 수 있다.

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());
}