리팩토링 - 냄새 2. 중복 코드

김상운(개발둥이)·2022년 3월 27일
0

리팩토링

목록 보기
3/17
post-thumbnail

들어가기

해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.



냄새 2. 중복 코드

Duplicated Code

  • 중복 코드의 단점
  • 비슷한지, 완전히 동일한 코드인지 주의 깊게 봐야한다.
  • 코드를 변경할 때, 동일한 모든 곳의 코드를 변경해야 한다.
  • 사용할 수 있는 리팩토링 기술
  • 동일한 코드를 여러 메소드에서 사용하는 경우, 함수 추출하기 (Extract Function)
  • 코드가 비슷하게 생겼지만 완전히 같지는 않은 경우, 코드 분리하기 (Slide Statements)
  • 여러 하위 클래스에 동일한 코드가 있다면, 메소드 올리기 (Pull Up Method)


함수 추출하기

Extract Function

  • "의도"와 "구현" 분리하기
  • 무슨 일을 하는 코드인지 알아내려고 노력해야 하는 코드라면 해당 코드를 함수로 분리하고 함수 이름으로 "무슨 일을 하는지" 표현할 수 있다.
  • 한줄 짜리 메소드도 괜찮은가?
  • 거대한 함수 안에 들어있는 주석은 추출한 함수를 찾는데 있어서 좋은 단서가 될 수 있다.

예제코드

public class StudyDashboard {

    @Value("token")
    private static String token;

    private void printParticipants(int eventId) throws IOException {
        // Get github issue to check homework
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(eventId);

        // Get participants
        Set<String> participants = new HashSet<>();
        issue.getComments().forEach(c -> participants.add(c.getUserName()));

        // Print participants
        participants.forEach(System.out::println);
    }

    private void printReviewers() throws IOException {
        // Get github issue to check homework
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(30);

        // Get reviewers
        Set<String> reviewers = new HashSet<>();
        issue.getComments().forEach(c -> reviewers.add(c.getUserName()));

        // Print reviewers
        reviewers.forEach(System.out::println);
    }

    public static void main(String[] args) throws IOException {
        StudyDashboard studyDashboard = new StudyDashboard();
        studyDashboard.printReviewers();
        studyDashboard.printParticipants(15);

    }

}

냄새

printParticipants, printReviewers 함수에 '의도'가 너무 많아 함수를 추출하겠다.

해결

인텔리제이에서 윈도우 기준 ctrl + alt + m 을 눌러 의도를 표현한 부분을 함수로 추출한다.

리팩토링 후 코드

package me.whiteship.refactoring._02_duplicated_code.practice._04;

import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.springframework.beans.factory.annotation.Value;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class StudyDashboard {

    @Value("token")
    private static String token;

    private void printParticipants(int eventId) throws IOException {
        // Get github issue to check homework
        GHIssue issue = getGhIssue(eventId);

        // Get participants
        Set<String> participants = getUsernames(issue);

        // Print participants
        PrintParticipants(participants);
    }

    private void printReviewers() throws IOException {
        // Get github issue to check homework
        GHIssue issue = getGhIssue(30);

        // Get reviewers
        Set<String> reviewers = getUsernames(issue);

        // Print reviewers
        PrintParticipants(reviewers);
    }

    private void PrintParticipants(Set<String> participants) {
        participants.forEach(System.out::println);
    }

    private Set<String> getUsernames(GHIssue issue) throws IOException {
        Set<String> participants = new HashSet<>();
        issue.getComments().forEach(c -> participants.add(c.getUserName()));
        return participants;
    }

    private GHIssue getGhIssue(int eventId) throws IOException {
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        return repository.getIssue(eventId);
    }

    public static void main(String[] args) throws IOException {
        StudyDashboard studyDashboard = new StudyDashboard();
        studyDashboard.printReviewers();
        studyDashboard.printParticipants(15);

    }

}


코드 정리하기

Slide Statements

  • 관련있는 코드끼리 묶여있어야 코드를 더 쉽게 이해할 수 있다.
  • 함수에서 사용할 변수를 상단에 미리 정의하기 보다는, 해당 변수를 사용하는 코드 바로 위에 선언하자.
  • 관련있는 코드끼리 묶은 다음, 함수 추출하기 (Extract Function)를 사용해서 더 깔끔하게 분리할 수도 있다

예제 코드

package me.whiteship.refactoring._02_duplicated_code._05_slide_statements;

import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class StudyDashboard {
	
    @Value("token")
    private static String token;
    
    private void printParticipants(int eventId) throws IOException {
        // Get github issue to check homework
        Set<String> participants = new HashSet<>();
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(eventId);

        // Get participants
        issue.getComments().forEach(c -> participants.add(c.getUserName()));

        // Print participants
        participants.forEach(System.out::println);
    }

    private void printReviewers() throws IOException {
        // Get github issue to check homework
		Set<String> reviewers = new HashSet<>();
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(30);

        // Get reviewers
        issue.getComments().forEach(c -> reviewers.add(c.getUserName()));

        // Print reviewers
        reviewers.forEach(System.out::println);
    }

    public static void main(String[] args) throws IOException {
        StudyDashboard studyDashboard = new StudyDashboard();
        studyDashboard.printReviewers();
        studyDashboard.printParticipants(15);
    }




}

냄새

예제코드에서 paricipants, reviewers 변수가 사용되어지는 부분에서 멀리 떨어져 있어 쉽게 이해하기 힘들다.

해결

인텔리제이에서 윈도우 기준 shift + alt + (위, 아래) 를 사용하여 사용되어진 부분에 선언한다.

리팩토링 후 코드

package me.whiteship.refactoring._02_duplicated_code.practice._05;

import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.springframework.beans.factory.annotation.Value;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class StudyDashboard {

    @Value("token")
    private static String token;

    private void printParticipants(int eventId) throws IOException {
        // Get github issue to check homework
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(eventId);

        // Get participants
        Set<String> participants = new HashSet<>();
        issue.getComments().forEach(c -> participants.add(c.getUserName()));

        // Print participants
        participants.forEach(System.out::println);
    }

    private void printReviewers() throws IOException {
        // Get github issue to check homework
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(30);

        // Get reviewers
        Set<String> reviewers = new HashSet<>();
        issue.getComments().forEach(c -> reviewers.add(c.getUserName()));

        // Print reviewers
        reviewers.forEach(System.out::println);
    }

    public static void main(String[] args) throws IOException {
        StudyDashboard studyDashboard = new StudyDashboard();
        studyDashboard.printReviewers();
        studyDashboard.printParticipants(15);

    }

}


메소드 올리기

Pull Up Method

  • 중복 코드는 당장은 잘 동작하더라도 미래에 버그를 만들어 낼 빌미를 제공한다.
    • 예) A에서 코드를 고치고, B에는 반영하지 않은 경우
  • 여러 하위 클래스에 동일한 코드가 있다면, 손쉽게 이 방법을 적용할 수 있다.
  • 비슷하지만 일부 값만 다른 경우라면, “함수 매개변수화하기 (Parameterize Function)” 리팩토링을 적용한 이후에, 이 방법을 사용할 수 있다.
  • 하위 클래스에 있는 코드가 상위 클래스가 아닌 하위 클래스 기능에 의존하고 있다면, “필드 올리기 (Pull Up Field)”를 적용한 이후에 이 방법을 적용할 수 있다.
  • 두 메소드가 비슷한 절차를 따르고 있다면, “템플릿 메소드 패턴 (Template Method Pattern)” 적용을 고려할 수 있다.

예제 코드

부모 클래스

public class Dashboard {

    @Value("token")
    protected static String token;

    public static void main(String[] args) throws IOException {
        ReviewerDashboard reviewerDashboard = new ReviewerDashboard();
        reviewerDashboard.printReviewers();

        ParticipantDashboard participantDashboard = new ParticipantDashboard();
        participantDashboard.printParticipants(15);
    }

}

자식 클래스 - 1

public class ParticipantDashboard extends Dashboard {

    public void printParticipants(int eventId) throws IOException {
        printUsernames(eventId);
    }

    protected void printUsernames(int eventId) throws IOException {
        // Get github issue to check homework
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(eventId);

        // Get usernames
        Set<String> usernames = new HashSet<>();
        issue.getComments().forEach(c -> usernames.add(c.getUserName()));

        // Print usernames
        usernames.forEach(System.out::println);
    }
}

자식 클래스 - 2

public class ReviewerDashboard extends Dashboard {

    public void printReviewers() throws IOException {
        printUsernames(30);
    }

    protected void printUsernames(int eventId) throws IOException {
        // Get github issue to check homework
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(eventId);

        // Get usernames
        Set<String> usernames = new HashSet<>();
        issue.getComments().forEach(c -> usernames.add(c.getUserName()));

        // Print usernames
        usernames.forEach(System.out::println);
    }
}

냄새

각 자식 클래스에서 printUsernames 함수는 비슷한 동작을 수행하는 중복 코드이다.

자식 클래스에서의 반복되는 중복 코드의 수정은 일괄 수정이 어렵다.

해결

이를 해결하기 위해서는 자식 클래스의 중복되는 함수를 부모 클래스로 올리는 Pull Up Method 를 사용한다.

윈도우 기준 인텔리제이에서 메서드 이름 우클릭 -> refactor -> pull members up 을 사용한다.

리팩토링 후 코드

부모 클래스

package me.whiteship.refactoring._02_duplicated_code.practice._06;

import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.springframework.beans.factory.annotation.Value;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class Dashboard {

    @Value("token")
    protected static String token;

    public static void main(String[] args) throws IOException {
        ReviewerDashboard reviewerDashboard = new ReviewerDashboard();
        reviewerDashboard.printReviewers();

        ParticipantDashboard participantDashboard = new ParticipantDashboard();
        participantDashboard.printParticipants(15);
    }

    protected void printUsernames(int eventId) throws IOException {
        // Get github issue to check homework
        GHIssue issue = getGhIssue(eventId);

        // Get usernames
        Set<String> usernames = getUsername(issue);

        // Print usernames
        usernames.forEach(System.out::println);
    }

    private Set<String> getUsername(GHIssue issue) throws IOException {
        Set<String> usernames = new HashSet<>();
        issue.getComments().forEach(c -> usernames.add(c.getUserName()));
        return usernames;
    }

    private GHIssue getGhIssue(int eventId) throws IOException {
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        GHRepository repository = gitHub.getRepository("whiteship/live-study");
        GHIssue issue = repository.getIssue(eventId);
        return issue;
    }
}

자식 클래스 - 1

package me.whiteship.refactoring._02_duplicated_code.practice._06;

import java.io.IOException;

public class ParticipantDashboard extends Dashboard {

    public void printParticipants(int eventId) throws IOException {
        super.printUsernames(eventId);
    }

}

자식 클래스 - 2

package me.whiteship.refactoring._02_duplicated_code.practice._06;

import java.io.IOException;

public class ReviewerDashboard extends Dashboard {

    public void printReviewers() throws IOException {
        super.printUsernames(30);
    }
}

부모 클래스에 printUsernames 함수를 선언하여 각 자식 클래스에서 super.printUsernames() 를 통해 접근하여 부모 쪽에서 관리한다.

profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글