이번 포스팅은 마지막 스터디 시간에 잠시 언급을 했던 [리팩토링]이라는 주제에 대한 내용을 다뤄보고자 합니다. 

많은 개발자들이 리팩토링을 언급하는 경우가 종종있지만, 이 개념에 대해 오해하는 사람들도 종종 있으며 단순히 코드정리라고 생각하는 경우도 많이 있는 듯 합니다.


지난 시간에 진행한 스터디에서는 Martin Fowler 의 [리팩토링-코드 품질을 개선하는 객체지향 사고법] 에 서술된 내용을 기반으로 진행을 하였으며, 보다 자세한 리뷰를 이번 포스팅에서 작성하고자 합니다.


이 책이 궁금하신 분은 아래 링크를 참고해보세요. 


리팩토링 - 코드 품질을 개선하는 객체지향 사고법
국내도서
저자 : 마틴 파울러(Martin Fowler) / 김지원역
출판 : 한빛미디어 2012.11.09
상세보기


지난 스터디에서 진행했던 자료는 아래 링크에서 확인하실 수 있습니다. :-)



크게 다뤄볼 목차는 리팩토링의 개념과 필요성, 그리고 여러 방법들을 제시해 볼 생각입니다.


특히, 방법같은 경우는 새로운 개념이 아닌 우리가 보통 코드 정리를 위해 하고 있는 내용들이 주를 이룰 수도 있습니다. (즉 갑자기 새로운 개념이 등장하지는 않습니다. ㅡㅡ^)



1. 리팩토링의 개념.


리팩토링이란 이미 구현된 일련의 행위에 대한 변경 없이, 내부 구조를 변경하는 것을 말합니다.

내부 구조를 변경하는 이유는 코드의 가독성과 유지보수성을 향상시키기 위함이죠.


이 개념은 성능을 최적화하는 문제와는 다릅니다.

성능 최적화는 사실 기능을 추가하는 문제와 크게 다르지 않습니다. 


즉, 구현된 행위에 대한 스펙변경 없이 코드 품질을 위해 내부 구조를 변경하는 것을 의미합니다.


이 책의 저자인 Martin Fowler 는 


"컴퓨터가 이해하는 코드는 어느 바보나 짤 수 있다. 좋은 프로그래머는 사람이 이해하는 코드를 짠다."


라고 말했습니다.


즉, 같이 일하는 주체는 사람이며, 나중에 코드를 리뷰 해야할 주체(보통은 나... 종종 나..) 역시 사람이 때문에 이 과정은 매우 중요한 듯 합니다. 



2. 리팩토링의 필요성.


소프트웨어 개발에 있어서, 리팩토링을 하는 과정은 꽤 중요한 이슈입니다.


일단, 리팩토링 과정을 통해 중복된 코드를 함수로 만들거나 모듈로 빼내는 등의 작업들을 통해 소프트웨어 공학에서 말하는 유지보수성을 키울 수 있습니다. 즉 이러한 일련의 과정들은 전체적으로 소프트웨어 설계의 질적 향상을 불러올 수 있습니다.


이러한 소프트웨어 설계의 질적 향상은 일련의 행위에 대한 가독성을 높일 수 있을 것입니다.

같이 일하는 동료, 혹은 유지보수해야하는 본인을 위해서라도 가독성있는 코드를 작성하는 것이 중요하겠죠? 


또한, 전반적인 코드 개선의 과정을 통해 버그 수정을 수월하게 할 수 있습니다.


이런 전반적으로 좋은 코드를 바탕으로 요구사항을 수용하는 것은 과적으로 개발시간의 단축 및 강건한 프로그램을 만드는 것에 도움이 될 것입니다.



3. 리팩토링의 시점.


리팩토링 과정은 강건하고 유지보수성이 좋은 소프트웨어를 만들기 위한 과정이며, 계속해서 프로젝트를 좋은 설계 기반 위에 얹을 수 있을 것입니다.


하지만, 아무 때나 모든 코드를 깔끔하게 정리해야할까요?

그것은 조금 시간낭비일 수 있습니다. 이미 잘 작동하며, 요구사항 추가에 대한 명세가 없는 모든 모듈을 굳이 수정하는 것은 정말로 할 것이 없을 때 연습을 위해서 좋을 수도 있습니다.


[리팩토링-코드 품질을 개선하는 객체지향 사고법] 에서는 어느 정도 제안하는 리팩토링의 시점이 있으며, 이는 다음과 같습니다.


- The Rule of Three


유사한 것을 세 번하면, 리팩토링을 고려해 볼 필요가 있을 것 같습니다.

보통을 유사한 과정을 캡슐화하여, 추후 재활용을 도모합니다.


- 새로운 기능을 개발할 때.


현재 구현되어 있는 상태가 새로운 요구사항을 받아들이기 힘들 때 리팩토링을 합니다.

확장에 열려있게 수정하고(OCP), 기능을 추가하는 것이 더 수월할 것입니다.


이 때, 중요한 것은 리팩토링을 할 때는 기능을 추가하지 말고, 기능을 추가할 때는 리팩토링을 하지 않기를 권장합니다. (Two Hats)


- 버그를 수정할 때


당연한 이야기이지만, 코드에 대한 이해도가 높아지면 버그 수정이 쉬워질 것입니다.

난해한 코드 속에서 버그 수정을 하기 보다는, 리팩토링 과정을 통해 어느정도 가독성 및 이해도를 갖추고 수정하는 것이 빠른 경우가 있을 수 있습니다.


- 코드 리뷰를 할 때


팀원들과 진행하는 코드 리뷰 과정을 통해 리팩토링을 할 수 있습니다.


실력이 좋은 개발자일지라도 너무 본인이 작성한 코드를 집중해서 보면, 객관적인 가독성 및 유지보수성, 논리적 오류 등을 찾기 쉽지 않을 수 있습니다.


즉, 타인이 같이 봐주는 코드 리뷰나 페어 프로그래밍 등의 과정은 본인이 작성한 코드의 가독성 및 유지보수성을 높게 할 수 있으며, 특히 주니어 개발자들이 노하우 기반의 성장할 수 있는 기회일 수 있습니다.


하지만, 너무 많은 사람이 이 활동에 참여하면 비효율적이라고 합니다.

적당한 소수인원이 진행하는 코드리뷰가 권장됩니다.



4. 리팩토링 방법


언급한 바와 같이 리팩토링은 구현된 일련의 행위 변경 없이, 코드의 유지보수성, 확장용이성 등을 높게 하기 위해 내부 구조를 변경하는 작업입니다.


하지만, 막연하게 코드정리를 한다는 것은 아닌 듯 합니다. 

리팩토링 하는 방법과 그에 대한 이름 역시 존재합니다. 하지만, 이 방법들은 이미 여러분들이 알고 있는 방법일 수 있습니다.


- 테스트 코드 작성


리팩토링을 하기 전에 가장 먼저 작업해야할 것은 테스트 코드를 작성하는 것입니다.

기능을 유지하고 코드를 개선하는 것이 목적이며, 코드 수정 뒤에 기능이 이전과 동일하게 작동하는 지 확인을 할 필요가 있습니다.


이는 사실 리팩토링 과정에서만 필요한 것은 아닙니다.

보통 특정 기능을 만든다고 할 때, 코드 작성이 끝이 아닌 기능 단위의 검증이 수반되어야 합니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 개인별 평가종류에 대한 기능이 잘 작동되는지에 대한 테스트.
 */
@Test
public void 개인별_평가종류_테스트() {
 
    // 동일 조건 테스트 케이스 제작.
    final Integer companySubjectSn = 1001;
    final Integer hrAppraisalSn = 20;
    final Integer memberSubjectSn = 1;
 
    /**
     * 테스트 결과 목록
     */
    List<Integer> targetHrAppraisalKindSnList = hrAppraisalOperateExecuteService.readTargetHrAppraisalKinsSnList(
                                                                                            companySubjectSn
                                                                                            , hrAppraisalSn
                                                                                            , memberSubjectSn);
 
    // 테스트에 대한 결과 명시..
    System.out.println(String.format("회사순번 %d, 평가순번 : %d, 구성원 순번 : %d", companySubjectSn, hrAppraisalSn, memberSubjectSn));
    System.out.println("평가종류순번 테스트 : " + targetHrAppraisalKindSnList);
}
cs



- Extract Method


너무 길게 작성된 메소드에 대하여, 일련의 특정 행위를 캡슐화하여 메소드로 분리하는 작업입니다. 

(철자 그대로 메소드 추출입니다.)

메소드가 길게 작성 되었다는 것은 해당 메소드의 책임이 너무 많음을 의미할 수 있습니다. 

그렇기 때문에 일련의 특정 행위를 메소드로 추출하는 작업은 일종의 책임의 분배를 하는 작업일 수 있습니다. (SRP)


보통은 이해가 필요한 부분에 대하여, 분리 후 주석을 명시하는 편입니다.

주석을 명시하는 것 외에도 메소드의 이름은 짧고 기능을 잘 설명할 수 있어야 한다고 합니다.


하지만, 제 생각에는 메소드의 라인이 너무 길게 작성 되었다고 무조건 메소드로 추출하는 것은 반대입니다. 재활용할 여지가 없는 로직을 굳이 분리하여 코드의 흐름이나 디버깅 등의 불편함을 주고 싶지는 않습니다.


메소드 추출 대신에, [block] 을 이용해보는 방법을 저는 제안합니다.

길게 작성된 로직에 대하여, 아래와 같이 block 으로 단계를 나누고 주석을 명시한다면 아무리 긴 로직도 가독성을 크게 해치지 않을 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void tooLongMethod() {
 
    /**
     * Map 주석 쓰는법.
     *
     * 대상자순번에 대한 이름 그룹
     *
     * key : 대상자 순번
     * value : 이름
     */
    final Map<Integer, String> recipientByNameMap;
    {
        // recipientByNameMap 을 만들기 위해 어떠한 일련의 과정을 하는중..
        // Refactoring 원칙에 따르면, 이 곳은 새 메소드로써 정의 해야함.
    }
 
    // 대상자순번에 대한 이름 그룹을 통해 이름_순번 string 을 가진 목록을 출력        
    // 자료구조를 상수화. 내부를 변경하지도 말 것.
    // 메소드 내에 많은 변수는 버그를 양산하기 쉬움.       
    final List<String> recipientBarcodeList;
    {
 
    }
}
cs


block 방식의 코드 작성은 사실 실제 메소드로 추출만 안했을 뿐이지, 캡슐화를 한 것과 별반 다르지 않습니다. 변경사항이 생겼다면, 특정 block 만 수정을 함으로써 메소드를 추출했던 것과 같은 효과를 볼 수 있습니다.


추 후, 재사용성을 위해 메소드로 추출해야 한다면, 해당 block 을 메소드로 만들고 치환하면 됩니다. 


아래는 구성원에 대한 이름 그룹을 만드는 과정을 Extract method 한 결과입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void tooLongMethod() {
 
    /**
     * Map 주석 쓰는법.
     *
     * 대상자순번에 대한 이름 그룹
     *
     * key : 대상자 순번
     * value : 이름
     */
    final Map<Integer, String> recipientByNameMap = getRecipientByNameMap();
 
    // 대상자순번에 대한 이름 그룹을 통해 이름_순번 string 을 가진 목록을 출력        
    // 자료구조를 상수화. 내부를 변경하지도 말 것.
    // 메소드 내에 많은 변수는 버그를 양산하기 쉬움.       
    final List<String> recipientBarcodeList;
    {
 
    }
}
 
/**
 * 대상자 순번에 대한 이름 그룹 출력.
 *
 * <pre>
 * key : 대상자 순번
 * value : 이름
 * </pre>
 *
 * @return
 */
public Map<Integer, String> getRecipientByNameMap() {
    // recipientByNameMap 을 만들기 위해 어떠한 일련의 과정을 하는중..
    // Refactoring 원칙에 따르면, 이 곳은 새 메소드로써 정의 해야함.
}
cs



- Move Method 


한 클래스의 메소드가 타 모듈에서 너무 많이 사용되거나, 해당 클래스의 상태를 전혀 사용하지 않는다면 메소드의 위치를 변경할 것을 제안합니다.


다른 모듈에서 훨씬 많이 사용하는 경우.


해당 메소드를 많이 사용하는 모듈로 이동시키고, 기존 클래스에서는 이동시킨 모듈을 이용한 위임호출로 기존 기능을 유지해 볼 수 있을 것 같습니다.



- 상태를 전혀 사용하지 않는 경우.


클래스는 상태와 행위를 결합하여 관리하고자 생긴 개념입니다.


하지만, 행위 자체가 상태를 동반하지 않는다면 이 메소드를 정적 메소드로 변경을 해볼 수 있을 것 같습니다. 

(특정 주체에 대하여, 정적 메소드가 많아진다면 Util 클래스의 제작을 고려합시다..)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MemberVo {
    private String name;
 
    public MemberVo(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    /**
     * 상태를 사용하지 않는 메소드는 정적 메소드로 만들길 권장.
     * 
     * @param name
     * @return
     */
    public static String GetMemberFormatter(String name) {
        return String.format("그 사람의 이름 : ", name);
    }
}
cs



- 불필요한 임시변수 제거


대부분의 임시 변수는 사실 메소드로 대체할 수 있습니다. 

임시 변수의 사용은 메소드의 길이를 증가시키고, 버그를 양산할 여지가 있습니다.

(임시변수를 많은 곳에서 변경하면, 디버깅 및 유지보수가 힘들 수도....)


하지만, STEP-BY-STEP 으로 작성된 코드를 리뷰함에 있어서, 상태를 잠시 어느 순간까지 가지고 있는 것은 오히려 가독성에 더 좋을 수 있습니다.

그렇지만, 변수 상태로 남겨두면 [사이드-이펙트]를 일으킬 여지가 있으니, 상수형으로 상태를 유지시키는 것도 하나의 방법이라고 할 수 있습니다. 자료구조 역시, 영속 자료구조(내부가 바뀌지 않는 구조)를 유지 해야겠죠?


아래는, 임시변수를 메소드로 대체한 것과 특정 상태를 상수로 유지하는 방법입니다.

제 입장은 가독성 및 디버깅을 위해서라도 상수형태의 상태를 남겨, STEP 별로 읽을 수 있도록 하는 것을 권장합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void tempMethodProcessing() {
 
    // 임시 변수 제거.
    // 메소드로 변수를 대체. 코드 길이를 줄일 수 있음.
    {
        for (MemberVo memberVo : memberDao.selectList(companySn)) {
            // something work!!
        }
    }
 
    // 상수로써, 데이터를 잠시 관리
    // 이 것이 오히려 디버깅이나 가독성면에서 보다 좋을 수 있음
    {
        final List<MemberVo> memberVoList = memberDao.selectList(companySn);
 
        for (MemberVo memberVo: memberVoList) {
            // something work!!
        }
    }
}
cs



- 분기 제거


일련의 분기처리 과정은 조건의 추가 및 삭제에 유연하지 못할 수 있습니다.

이는 요구사항 추가에 따라 분기한 과정들을 모두 살펴보며, 수정을 해야함을 의미합니다.


객체지향 다형성(Polymorphism)으로 이 문제를 해결할 수 있습니다.

특정 조건들 즉 상태를 객체로 관리함으로써, 기존 구조의 변경 없이 확장을 하는 것을 지향합니다.(OCP)


우리는 이에 대한 내용을 꽤 많이 다뤄보았습니다.



대표적으로 분기를 제거할 수 있는 패턴에는 방문자(Visitor), 전략(Strategy), 상태(State) 패턴 등이 있습니다. 


하지만, 모든 구조를 if 없이 구현하겠다는 의지는 좋지 않습니다. 

패턴에 의한 코드의 파편화는 결국 코드의 난이도를 올리며, 극한으로 작성된 패턴들은 오히려 가독성을 많이 저해할 수 있습니다.


요즘의 대세는 주니어가 봐도 이해하기 좋은 코드를 작성하는 것인 듯 합니다.



5. 리팩토링의 한계와 극복


리팩토링은 매우 중요한 일이지만, 모든 상황에 대하여 리팩토링할 수는 없는 것 같습니다. ㅡㅡ^

상황은 언제나 좋은 방향으로만 흘러가는 것이 아니니 말이죠. (가끔 좋지 않은 방향으로, 아니 종종...)


그렇기 때문에 이번 절에서는 리팩토링의 한계와 극복방법에 대해 다뤄보려 합니다.



- DataBase 스키마의 변경


우리가 작성하는 많은 응용 프로그램들의 로직들은 DB 스키마와 매우 밀접한 의존을 가지고 있습니다. 

아무리 잘 작성된 코드도 DB 스키마의 변경에서 자유로울 수는 없을 것 같군요....


DB 스키마의 변경에 따라 당연히 응용 프로그램 역시 변화에 대처를 해야하지만, 그 대처를 최소화하는 방법이 없는 것은 아닌 듯 합니다. 


DB 스키마 클래스와 비지니스 로직간의 중간 층(Layer)을 두고, 많은 응용프로그램에서 이 중간 층을 이용하여 개발을 한다면 생각보다 많은 부분을 변경하지 않아도 될 수도 있습니다.

스키마 변경에 따라 중간 층에만 최대한의 수정을 할 수 있다면 좋겠죠? :-)


우리는 이 방법 역시 배운적이 있습니다.

맞습니다. 여기서 말하는 중간 층은 바로 어댑터(Adapter) 입니다.



저는 이 방법을 매우 지향합니다. 

사내에서 제 역할인 [백-엔드 개발자]로써, [프론트-엔드 개발자]에게 특정 요청에 필요한 정보만 적재적소하게 줄 수 있도록 제어를 하는 편입니다.


이 방법은 DB 스키마 변경에 따른 프론트-엔드 개발자의 영역의 변화를 최소화할 수 있으며, 최근 마감기한을 앞두고 새로운 요구사항에 의해 엎어질 뻔한 프로젝트를 구할 수 있는 [최고의 한방]이 되었습니다.



- 메소드 서명의 변경


우리는 경우에 따라 이미 개발되었고 잘 사용하고 있는 메소드를 수정할 경우가 종종 있습니다.


메소드 내부의 구조의 개선이나 최적화 등은 그나마 괜찮지만, 메소드의 서명을 변경하는 해당 메소드를 사용하는 모든 곳을 고쳐야 하기 때문에 안타까운 일이 발생할 수 있습니다.


이런 경우, 우리는 오버로드 방식을 한 번 고려해볼 수 있습니다.


즉, 기존 서명은 남기고, 새 서명을 가진 메소드를 제작하는 것입니다. 

물론 기존 서명은 새 서명을 가진 메소드를 사용하게 함으로써, 코드중복을 없앨 수 있겠죠? 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * 회사 순번과 구성원 순번으로 이용하여 데이터를 조회
 * 
 * <pre>
 *     하지만, 이제는 구성원 순번 목록을 이용하여 조회해야함.
 *     즉 해당 메소드는 구서명임..
 *     
 *     새로 작성된 서명을 사용하는 방식으로 내부구조를 개선..
 * </pre>
 * 
 * @param companySubjectSn
 * @param memberSubjectSn
 * @return
 */
public List<MemberVo> selectMemberList(Integer companySubjectSn, Integer memberSubjectSn) {
    return selectMemberList(companySubjectSn, Arrays.asList(memberSubjectSn));
}
 
/**
 * 새로 만든 서명.
 * 
 * 회사순번과 구성원 순번 목록으로 데이터를 조회.
 * 
 * @param companySubjectSn
 * @param memberSubjectSnList
 * @return
 */
public List<MemberVo> selectMemberList(Integer companySubjectSn, List<Integer> memberSubjectSnList) {
    // something Work!!
}
cs


만약 기존 서명을 더이상 팀의 구성원들이 지금부터라도 사용하길 지양한다면, 사장(Deprecated) 를 고려하는 것도 좋습니다.


사장을 시켰다면, 문서에 아래와 같이 사장 사유와 우회법을 명시하세요. :-)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 회사 순번과 구성원 순번으로 이용하여 데이터를 조회
 * 
 * <pre>     
 *     Deprecated 사유 : 이 메소드는 오직 구성원 순번만 받고 있음. 
 *                      새 서명을 가진, 구성원 순번 목록을 이용하여 조회하는 새 서명을 가진 MemberDao::selectMemberList 을 사용하길 권장.
 * </pre> 
 * 
 * @param companySubjectSn
 * @param memberSubjectSn
 * @return
 */
@Deprecated
public List<MemberVo> selectMemberList(Integer companySubjectSn, Integer memberSubjectSn) {
    return selectMemberList(companySubjectSn, Arrays.asList(memberSubjectSn));
}
cs



- 리팩토링 보다는 재개발을....


작성된 기존 코드의 설계가 새 요구사항을 수용하기에 있어서, 많은 변화가 있어야 한다면 처음부터 다시 작성하는 편이 좋을 수도 있습니다.


단, 대부분의 기능이 정상적으로 작동을 할 때 이 방법을 해야할지도 모릅니다.

[버그 투성이 코드]를 참고하여 만든 새로운 코드 역시 [버그 투성이] 겠죠... ㅡㅡ^


하지만, 새로 작성하기에 시간이 부족하거나 너무 많을 경우 기존 코드를 세부 컴포넌트로 분리를 해볼 것을 제안합니다. 분리된 세부 컴포넌트들 중 특정 컴포넌트만 재개발을 하면 되는 경우도 종종 존재합니다.



- 마감시간이 다되었다면, 리팩토링은 잠시 보류...


마감시간이 다되었을 때, 리팩토링 등의 코드개선은 좋아 보이지 않습니다.

(당신의 상사에게 뒷통수를 맞을 수 있습니다... )


이미 잘 작동하며 테스트까지 끝낸 모듈에 마감시간을 앞두고 리팩토링을 하다가 버그를 심는 멍청한 짓은 좋지 못합니다.


또한, 프로그래머의 코드 철학도 중요하지만 이 제품을 쓸 고객과의 약속이 더 중요합니다.


이 내용들의 기반은 저에게 OOP 를 알려주신 교수님의 자료와 [리팩토링-코드품질을 개선하는 객체지향 사고법] 의 내용을 바탕으로 작성되었지만, 실무에서 약 2년정도 참여해보며 후배들에게 꼭 알려주고 싶은 이야기들을 담은 것 같습니다.


아직 부족한 것이 많지만, 경험을 통해 이 사례가 좋았다고 느낀 점들을 특히 부각시켜 서술하였습니다. 

(그래서 약간 꼰대 같을 수도 ㅜㅡㅜ)



어찌하든, 이 것으로 [CHAPTER X] 도 끝입니다. 


끝까지 읽어주신 여러분 정말로 감사합니다. :-)


반응형
Posted by N'