Octoping의 블로그

중복 코드는 여러 클래스나 메소드에 걸쳐서 완전히 동일하거나 혹은 비슷한 형태로 나타나는 등, 여러 형태로 나타날 수 있다.

중복 코드의 단점은 다음과 같다.
1. 한 코드를 변경할 때 동일한 모든 곳의 코드를 변경해야 한다.
2. 중복되는 코드들이 완전히 동일한 기능을 하는지, 혹은 비슷한 일을 하는지 잘 들여다봐야 한다.


이를 해결할 수 있게 해주는 리팩토링은 다음과 같다.

1. 함수 추출하기 (Extract Function)
2. 코드 분리하기 (Slide Statements)
3. 메소드 올리기 (Pull Up Method)

 


1. 함수 추출하기 (Extract Function)
동일한 코드를 여러 메소드에서 사용하는 경우

'함수 추출하기'는 중복 코드를 제거하는 상황 뿐 아니라, 정말 여러 상황에서 쓰일 수 있는 유용한 기능이다.
리팩토링 책에서는 '의도'와 '구현'을 분리하는 데에 함수 추출을 사용하라고 권장한다.

코드를 읽을 때 어떤 메소드가 무슨 일을 하는지 잘 파악이 되지 않는다면 그것을 '구현'​​이라고 하며, 잘 파악이 된다면 '의도'라고 한다. 어떠한 코드가 의도를 드러내고 있는지, 구현을 드러내고 있는지를 중점적으로 바라본다면 함수 추출에 있어 도움이 될 수 있다.

복잡한 메소드에서 '구현'들을 모두 추출하여 '의도'로만 이루어진 메소드를 만드는 것이 함수 추출의 핵심이다. 함수를 추출하는 것의 가장 큰 장점은 함수에 이름을 줄 수 있다는 것이다. 모든 구현들을 추출하여, 그것을 의도가 드러나게 만들 수 있다면 비록 한 줄 짜리 메소드를 추출하더라도 이는 바람직하다.

 

public class Refactoring {
    
    private void printParticipants(String id) throws IOException {
        // 블로그에 접속해서 이웃 구하기
        Naver naver = Naver.connect();
        NaverBlog blog = naver.getBlog(id);
        NaverBlogNeighbor neighbor = blog.getNeighbor();
        
        // 파티 참석자 구하기
        Set<String> party = new HashSet<>();
        neighbor.getIDs.forEach(p -> party.add(p));

        // 참석자 출력
        party.forEach(System.out::println);
    }

    public static void main(String[] args) throws IOException {
        Refactoring r = new Refactoring();
        r.printParticipants("myc1365");
    }
}

예제 코드로 실습을 해보자. (Naver 및 NaverBlog 클래스 부분은 제가 가상으로 만든 클래스입니다)
블로그의 이웃들의 ID를 구해서 내가 열 파티의 참석자를 출력하는 코드이다.

현재 printParticipants 함수는 복잡하여 한 눈에 그 의도를 알아보기가 쉽지 않다.
이 함수는 현재 3개의 의도를 수행하고 있는데, 우리는 이 함수에서 의도를 잘 추출하여 printParticipants의 의도가 잘 드러나게 만들 수 있다.

 

 

public class Refactoring {
    
    private void printParticipants(String id) {
        // 블로그에 접속해서 이웃 구하기
        NaverBlogNeighbor neighbor = getNeighbors(id);
        // 파티 참석자 구하기
        Set<String> party = getParty(neighbor);
        // 참석자 출력
        printPartyMember(party);
    }

    private NaverBlogNeighbor getNeighbors(String id) {
        Naver naver = Naver.connect();
        NaverBlog blog = naver.getBlog(id);
        return blog.getNeighbor();
    }

    private Set<String> getParty(NaverBlogNeighbor neighbor) {
        Set<String> party = new HashSet<>();
        neighbor.getIDs.forEach(p -> party.add(p));
        return party;
    }

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

    public static void main(String[] args) throws IOException {
        Refactoring r = new Refactoring();
        r.printParticipants("myc1365");
    }
}

이렇게 3개의 의도를 추출하여 printParticipants 함수가 한눈에 알아보기 쉽게 되었다.

긴 함수는 이해를 어렵게 만든다. 긴 함수를 만드는 것을 피하기 위해서는 적극적으로 함수를 쪼개야하며, 함수를 짧게 만드는 작업의 99%는 함수 추출하기가 차지한다.

 

 

 

2. 코드 분리하기 (Slide Statements)
코드가 비슷하게 생겼지만 완전히 같지는 않은 경우

 

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

 

 

 

3. 메소드 올리기 (Pull Up Method)
여러 하위 클래스에 동일한 코드가 있을 경우

 

중복 코드는 당장은 잘 동작하더라도 미래에 버그를 만들어낼 가능성이 높다.
여러 하위 클래스에 동일한 코드가 존재한다면 손쉽게 '메소드 올리기'를 적용할 수 있다.

비슷하지만 일부 값만 다를 경우, '함수 매개변수화하기' 리팩토링을 적용한 이후에 이 방법을 사용할 수도 있다.

하위 클래스의 코드가 상위가 아닌, 하위 클래스 기능에 의존하고 있다면 '필드 올리기'를 적용한 이후에 이 방법을 사용할 수도 있다.

두 메소드가 비슷한 절차를 따르고  있다면, '템플릿 메소드 패턴'이라는 디자인 패턴을 적용해볼 수도 있다.

 

 

public class Blog {
    public static void main(String[] args) throws IOException {
        Comment c = new Comment();
        c.printComments("myc1365", 100);
 
        RecentComment rc = new RecentComment();
        rc.printRecentComments("myc1365");
    }
}

public class Comment extends Blog {
    public void printComments(String id, int cnt) {
        Naver naver = Naver.connect();
        NaverBlog blog = naver.getBlog(id);
        blog.getComments(cnt).forEach(System.out::println);
    }
}

public class RecentComment extends Blog {
    public void printRecentComments(String id) {
        Naver naver = Naver.connect();
        NaverBlog blog = naver.getBlog(id);
        blog.getComments(30).forEach(System.out::println);
    }
}

예제 코드로 실습을 해보자.
printCommenters와 printRecentVisitors는 중복 코드를 가지고 있지만 어딘가 미묘하게 다른 구석이 있다.

 

 

 

public class Blog {
    public static void main(String[] args) throws IOException {
        Comment c = new Comment();
        c.printComments("myc1365", 100);
 
        RecentComment rc = new RecentComment();
        rc.printRecentComments("myc1365");
    }
}

public class Comment extends Blog {
    public void printComments(String id, int cnt) {
        Naver naver = Naver.connect();
        NaverBlog blog = naver.getBlog(id);
        blog.getComments(cnt).forEach(System.out::println);
    }
}

public class RecentComment extends Blog {
    public void printRecentComments(String id) {
        printComments(id, 30);
    }

    public void printComments(String id, int cnt) {
        Naver naver = Naver.connect();
        NaverBlog blog = naver.getBlog(id);
        blog.getComments(cnt).forEach(System.out::println);
    }
}

일단은 두 함수가 완전 똑같도록 만들기 위해 '함수 매개변수화하기' 리팩토링을 사용해준다.
이제 메소드 올리기(Pull Members Up)를 사용해보자.

public class Blog {
    public void printComments(String id, int cnt) {
        Naver naver = Naver.connect();
        NaverBlog blog = naver.getBlog(id);
        blog.getComments(cnt).forEach(System.out::println);
    }

    public static void main(String[] args) throws IOException {
        Comment c = new Comment();
        c.printComments("myc1365", 100);
 
        RecentComment rc = new RecentComment();
        rc.printRecentComments("myc1365");
    }
}

public class Comment extends Blog {
}

public class RecentComment extends Blog {
    public void printRecentComments(String id) {
        super.printComments(id, 30);
    }
}

짜잔! 이렇게 중복 코드가 제거되었다.
이제 코드를 수정할 일이 생기면 두 코드를 모두 수정하는게 아닌, 상위 클래스의 printComments를 한번만 수정하면 된다.

profile

Octoping의 블로그

@Octoping

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!