Effective OOP 스터디에서 첫 번째 주제는


"주어진 상황 및 용도에 따라 객체에 책임을 추가할 수 있는 Decorator(장식) Pattern" 

에 대해 알아보았습니다.


관련 내용은 아래 포스팅에서 참고. :-)

과제로 진행할 수 있는 파일까지 아래 포스팅에 제공되고 있습니다.



이 패턴은 기존 클래스의 기능 추가를 위해 is-a 관계(상속)를 하는 것 대신, has-a 관계(의존)를 취함으로써 다양하게 책임을 추가할 수 있고 또한 이 추가 작업을 실행시간에 할 수 있다는 것에 대해 알아보았습니다.

(Favor has-a over is-a, 모든 is-a 는 has-a 로 변경할 수 있습니다.)


또한 Decorator 에 대해 조금 더 알아보기 위해 해당 주제에 대한 과제가 있었고, 이번 포스팅에서는 과제에 대한 리뷰를 작성해보려 합니다.


총 세가지 정도의 과제가 있었고, 단계별로 글을 진행하고자 합니다.


1. 장식 벗기기


첫 번째 실습은 현재 제작된 장식과 장식대상 클래스 사이의 구조에서 장식된 객체의 장식을 한 개씩 제거하는 메소드(removeCondiment)를 만들어 보는 것이었습니다.


그러나 과제에서는 메소드를 제작하기 전, 사전조건이 두 가지가 있었습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TRY 1.
 
Coffee coffee = new HouseBlendingEspresso();
coffee = new MilkDecorator(coffee);
coffee = new LatteDecorator(coffee);
 
// 장식과 관련된 기능을 장식 대상 클래스에 제공하는 것은 올바르지 않아 보입니다.
// CondimentDecorator 가 없어도 Coffee 는 독립적으로 존재해야 합니다.
coffee = coffee.removeCondiment();
 
 
// TRY 2.
 
Coffee coffee = new HouseBlendingEspresso();
coffee = new MilkDecorator(coffee);
coffee = new LatteDecorator(coffee);
 
// TRY 1 의 조건에 따라, removeCondiment 를 CondimentDecorator 로 옮겼습니다.
// 그러나, 장식을 벗길 때마다 아래처럼 캐스팅을 해야하는 중복 코드가 발생하게 됩니다.
if (coffee instanceof CondimentDecorator) {
    coffee = ((CondimentDecorator)coffee).removeCondiment();
}
cs


위의 두 문제를 해결하기 위해서는 결국 Coffee 의 타입 체크 및 캐스팅과 관련된 작업이 적절한 곳에 캡슐화되어 사용할 수 있어야하는 것 처럼 보입니다.


하지만, removeCondiment 의 위치 적용에 있어서, Coffee 와 CondimentDecorator 두 클래스 모두  애매한 위치임을 앞의 사전 조건에서 볼 수 있었습니다.


보통 이런 기능들은 저같은 경우에는 유틸(util) 기능으로 분리하는 편입니다. 장식과 관련된 기능이니 CondimentDecorator 에 해당 기능을 제작해보려합니다.


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
public abstract class CondimentDecorator extends Coffee {
 
    private Coffee coffee;
 
    /**
     * 정적메소드로 구현된, RemoveCondiment 구현.
     *
     * <pre>
     *     해당 기능에서는 어떤 객체 자신의 상태를 사용하지 않으며, 유틸성 기능을 제공하고자 합니다.
     * 제 습관 중 하나는 정적 메소드(static)의 경우 앞에 대문자를 붙이곤 합니다.
     * </pre>
     *
     * @param coffee
     * @return
     */
    public static Coffee RemoveCondiment(Coffee coffee) {
        if (coffee instanceof CondimentDecorator) {
            // 장식 객체일 경우 장식 대상 객체를 내보냅니다.
            return ((CondimentDecorator) coffee).coffee;
        } else {
            // 아닐 경우 본인을 내보냅니다.
            return coffee;
        }
    }
}
 
// 테스트 코드
Coffee coffee = new HouseBlendingEspresso();
coffee = new MilkDecorator(coffee);
coffee = new LatteDecorator(coffee);
 
// 정적메소드로 제작된, RemoveCondiment 테스트
coffee = CondimentDecorator.RemoveCondiment(coffee);
cs


유틸 기능으로 제공함으로써, 안전하게 장식을 제거할 수 있군요.

기능 역시 잘 작동하는 것처럼 보이네요.


그러나 여담으로 한 가지를 언급해보자면,

보통 장식 패턴에서는 이미 장식된 객체에서 한번 장식된 기능을 제거하기 보다는 동일하게 다시 만드는 경우가 더 많다고 합니다. ㅡㅡ^



2. 전략패턴으로 장식 구현


제게 OOP 를 가르쳐 주셨던 교수님께서는 


프로그래머는 같은 문제를 다른 방법으로도 풀 수 있어야 하며,


그 중에 가장 좋은 방법을 선택해야 한다


고 하셨습니다.


그런 의미에서, 두 번째 과제는 이미 구현된 장식 패턴의 구조를 전략 패턴으로 제작해보는 것이었습니다.


전략패턴에 대한 설명은 아래 글을 참고. @.@



이를 위해, CondimentDecorator 가 Coffee 에 의존했던 구조Coffee 가 장식 전략에 의존하도록 변경할 계획입니다.


Coffee 에는 여러 장식을 저장할 수 있도록 목록(List) 형태로 장식들을 가지고 있을 생각이며, [장식 전략 인터페이스 ICondiment] 역시 제공해보려 합니다.


일단, Coffee 내부에는 장식 전략을 의존할 수 있도록 Coffee::addCondiment 를 제공해도록 하겠습니다.


아래 코드를 참고해주세요. ^^


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
36
/**
 * 커피 클래스
 * @author khj1219
 *
 */
public abstract class Coffee {
 
    // 장식을 유지할 수 있는 멤버 변수
    private List<ICondiment> condimentList = new ArrayList<>();
 
    /**
     * 장식을 추가하는 메소드
     * 
     * @param condiment
     */
    public void addCondiment(ICondiment condiment){
        this.condimentList.add(condiment);
    }
 
    /**
     * 커피 가격 출력
     * 
     * 
     * @return
     */
    public Integer getCost(){
        // Coffee 의 특정 기능이 수행될 때, 장식 목록을 이용할 예정..
        for(ICondiment condiment : condimentList){
            // something work....
            // 장식의 속성은 어떻게 사용을 하나요?? @.@
        }
        
        return cost;
    }
 
}
cs



추가된 장식의 순서를 유지하고자 컨테이너로 List 를 선택하였습니다.


또한 장식들은 추가될 때마다 Coffee 클래스의 속성을 바로 변경하는 것이 아닌, 특정 기능을 수행될 때 목록을 순회하며 장식의 속성을 반영할 생각입니다.


그러나 장식 패턴의 장식들은 장식 대상 객체를 has-a 관계로 가지고 있기 때문에 이전 속성들과 현재 속성을 반영하여 결과를 출력할 수 있던 반면, 전략 장식들은 더이상 장식 대상 객체들을 가지고 있지 않습니다.


즉 이전 속성들을 알 수 없기 때문에, ICondiment 에서는 특정 기능을 수행할 때 파라미터로 Coffee 의 현재 속성을 넘기는 방식을 이용할 생각입니다.


그에 따른 인터페이스 명세는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ICondiment{
    
    /**
     * 커피 이름 출력
     * 
     * @param name
     * @return
     */
    String getName(String name);
    
    
    /**
     * 코스트에 대한 메소드 서명 정의 
     * 
     * @param cost
     * @return
     */
    Integer getCost(Integer cost);
    
}
cs



이를 이용하여, Coffee::getCost 를 구현을 완성해보려 합니다.


아 물론 처음에 제공을 했던 Coffee::getCost 는 추상 메소드였고, 현재는 ICondiment 목록을 이용하기 위해 정의를 할 생각입니다.


기존 Coffee::getCost 의 추상적인 역할은 Coffee::getTemplateCost 를 따로 정의함으로써, 기존 구조를 유지하고자 합니다.


그에 따라 작성된 Coffee 클래스는 다음과 같습니다.

(Coffee::getName 과 관련된 내용은 작성하지 않았습니다. 이 글을 확인하고, 한번 직접 작성해보세요.)


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
/**
 * 커피 클래스
 * 
 * @author ndh1002
 */
public abstract class Coffee {
 
    // 장식을 유지할 수 있는 멤버 변수
    private List<ICondiment> condimentList = new ArrayList<>();
 
    /**
     * 커피 가격 출력
     * 
     * @return
     */
    public Integer getCost(){
        Integer cost = coffee.getTemplateCost();
        
        for(ICondiment condiment : this.condimentList){
            cost = condiment.getCost(cost);
        }
        
        return cost;
    }
 
    /**
     * 가격에 대한 메소드 서명 정의
     *
     * <pre>
     *    기존 Coffee::getCost 의 추상적인 역할을 수행하기 위한 추상 메소드 서명 정의
     * </pre>
     * @return
     */
    public abstract Integer getTemplateCost();
}
cs


전략 패턴에 따라 구조를 변경 하였고, 그에 따른 테스트 코드는 다음과 같습니다.


모든 장식들을 전략으로 제공하지는 않고, 샘플로 ICondiment 를 구현하는 MilkDecorator 를 제공하고자 합니다.

(이 글을 보고 있는 스터디 그룹원들은 모두 잘하니, 나머지는 잘 구현할 수 있겠죠? ^^)


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
36
37
38
39
40
41
/**
 * 우유 데코레이터
 * 
 * <pre>
 * 샘플로 작성된 우유 데코레이터..
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public class MilkDecorator implements ICondiment {
 
    /**
     * 장식대상 객체이름을 그대로 사용.
     *
     * @return
     */
    @Override
    public String getName(String name) {
        return name;
    }
 
    /**
     * 우유 가격인 900원 추가.
     *
     * @return
     */
    @Override
    public Integer getCost(Integer cost) {
        return cost + 900;
    }
}
 
// TEST CODE
 
Coffee coffee = new HouseBlendingEspresso();
 
// 우유 -> 라떼 -> 샷 -> 샷
coffee.addCondiment(new MilkDecorator());
coffee.addCondiment(new LatteDecorator());
coffee.addCondiment(new ShotDecorator());
coffee.addCondiment(new ShotDecorator());
cs



작성된 코드는 기존 장식 패턴 구조와 비교하여 잘 작동하는 듯 합니다.


이제 한 번 고민해 볼만한 내용이 또 있습니다.

Coffee 의 각 기능에 대한 책임을 덧붙이기 위한 문제는 아래와 같이 세 가지 방법 정도를 구현해 보았습니다.


- 상속구조를 이용하는 방법


- 장식 패턴을 이용하는 방법


- 전략 패턴을 이용하는 방법


한 문제를 다양한 방법으로 생각해보았고, 어떤 방법이 가장 적절하지 한번 다시 리뷰를 해보면 좋을 것 같습니다. ^^;



3. 장식 비교


마지막 과제는 장식된 객체들 간의 비교를 구현하는 내용이었습니다.


비교에 있어서, 사전 조건은 아래와 같았습니다.


- 장식의 순서에 상관없이 동일하고, 같은 커피(예를들어 에스프레소)를 사용할 때 동일


- 자바의 객체 비교 메소드 equals 를 재정의할 것.


비교 연산를 함에 있어서, CondimentDecorator 나 Coffee 든 동일하게 비교는 가능해야 할 것 같습니다.


일단 해당 문제를 해결하기 위해서, Coffee 와 CondimentDecorator 에 각각 다른 equals 를 제작하려 합니다.


각자 구현 함으로써 Coffee 는 장식이 안된 순수 클래스, CondimentDecorator 는 장식이 된 클래스로 분류를 시킬 수가 있습니다.

이는 Coffee::equals 를 구현 시 장식에 대해 신경쓰지 않아도 되며, 오직 CondimentDecorator 에서만 장식 관련 역할을 수행하도록 구현할 수 있음을 의미합니다.

(SRP : 단일 책임 원칙)


한번 이 논리에 따라 기능을 구현해보도록 하겠습니다.


첫 번째는 Coffee::equals 구현입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Coffee {
 
    /**
     * Coffee 객체의 동일 여부 판단
     * 
     * @param coffee
     * @return
     */
    @Override
    public Boolean equals(Object coffee) {
        // 장식을 하지 않은 단일 객체, 오직 본인과 동일 클래스 여부를 확인합니다.
        return this.getClass().getName().equals(coffee.getClass().getSimpleName());
    }
}
cs


Coffee::equals 의 경우 장식이 안된 순수 클래스임이 위의 논리에 따라 정해졌습니다.

오직 파라미터로 넘어온 비교 대상 객체가 자신과 동일한 클래스인지만 확인하면 될 것 같습니다.


두 번째는 CondimentDecorator::equals 의 구현입니다.

장식 클래스에서 재정의를 하기 때문에 Coffee::equals 를 따라가지 않으며, 장식의 비교만 이 곳에서 구현하면 될 것 같습니다.


CondimentDecorator 의 동일여부 판단은 사전조건에서 나타난 것처럼, 여태까지 장식된 종류 및 베이스 커피가 무엇인지 확인하는 것이 먼저일 것 같습니다.

장식된 종류 목록을 구하기 위해서는 CondimentDecorator 가 has-a 관계로 가지고 있는 장식 대상 커피 객체를 이용할 수 있을 것 같습니다.


이를 이용한 구현은 아래와 같습니다.


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
36
37
38
39
40
41
42
43
public abstract class CondimentDecorator extends Coffee {
 
    /**
     * Coffee 를 받아, 장식된 목록을 출력하는 메소드 정의.
     *
     * <pre>
     *     - 클래스명에 따라 정렬이 됨을 보장.
     *     - 객체의 상태를 따로 사용하지 않음으로, 정적메소드 형태로 구현.
     * </pre>
     *
     * @param coffee
     * @return
     */
    private static List<String> GetSortedDecoratorList(Coffee coffee) {
 
        ArrayList<String> decoratorList = new ArrayList<>();
 
        // 커피 목록 세팅.
        {
            Coffee targetCoffee = coffee;
 
            do {
                // 커피 이름 추가.
                decoratorList.add(targetCoffee.getClass().getSimpleName());
 
                if (targetCoffee instanceof CondimentDecorator) {
                    // 장식 커피라면, 현재 장식 대상이 된 커피를 출력.
                    targetCoffee = ((CondimentDecorator) targetCoffee).coffee;
                } else {
                    targetCoffee = null;
                }
 
            } while (targetCoffee != null);
        }
 
        // 이름에 따라 정렬을 수행.
        {
            decoratorList.sort(String::compareTo);
        }
 
        return decoratorList;
    }
}
cs


장식 목록에 정렬을 수행한 이유는 장식의 순서에 상관없이 비교를 해야하기 때문입니다.

클래스명을 오름차순으로 정렬하면, 동일한 장식이 사용된 경우 쉽게 비교를 할 수 있겠죠?



이 메소드를 이용한 CondimentDecorator::equals 의 구현은 아래와 같습니다.


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
public abstract class CondimentDecorator extends Coffee {
 
    /**     
     * Coffee 객체의 동일 여부 판단
     *
     * @param targetCoffee
     * @return
     */
    @Override
    public boolean equals(Object targetCoffee) {
        if (targetCoffee instanceof Coffee) {
 
            List<String> myDecoratorCoffeeList = GetSortedDecoratorList(this);
 
            List<String> targetDecoratorCoffeeList = GetSortedDecoratorList((Coffee) targetCoffee);
 
            // 목록이 서로 같은지 비교.
            // List 의 equals 는 내부적으로 데이터가 동일한지 비교합니다.
            // khj1219 감사합니다.
            return myDecoratorCoffeeList.equals(targetDecoratorCoffeeList);
        } else {
            // 비교 대상 객체가 커피가 아니라면, 동일하지 않다고 볼 수 있음.
            return false;
        }
    }
}
cs


커피의 장식 목록을 구할 수 있으니, 본인과 비교대상의 장식들을 조회하여 비교하는 로직입니다.

재미있는 점은 List::equals 는 내부적으로 목록간 데이터가 동일한지 이미 구현이 되어있습니다.


과제를 해준 한 스터디원이 알려줬습니다. 감사합니다. @.@

스터디 내에서는 이에 대해 불안해서 못쓰겠다고 하였지만, 잘 숙지하고 좋은 툴을 쓰는 것이 올바른 자세인 듯 합니다. 아직 옹졸한듯... ^^;


모두 구현을 하였고, 이에 대한 테스트 코드는 다음과 같습니다.


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
 Coffee coffee1 = new HouseBlendingEspresso();
 
// 우유 데코레이터로 치장.
coffee1 = new MilkDecorator(coffee1);
// 라떼 데코레이터로 치장.
coffee1 = new LatteDecorator(coffee1);
// 더블샷 데코레이터 치장.
coffee1 = new ShotDecorator(coffee1);
coffee1 = new ShotDecorator(coffee1);
        
        
Coffee coffee2 = new HouseBlendingEspresso();
 
// 더블샷 데코레이터 치장.
coffee2 = new ShotDecorator(coffee2);
// 우유 데코레이터로 치장.
coffee2 = new MilkDecorator(coffee2);
coffee2 = new ShotDecorator(coffee2);
// 라떼 데코레이터로 치장.
coffee2 = new LatteDecorator(coffee2);
 
 
 
System.out.println("coffee1과 coffee2는 같나요? " + coffee1.equals(coffee2));
 
// CONSOLE LOG
// coffee1과 coffee2는 같나요? true
cs


잘 작동하는 듯 합니다.... ㅎㅎ



장식 패턴과 관련된 과제였지만, 

조금 더 중요하게 생각했던 것은 리팩토링에 대한 내용을 다룰 때 다뤘던 것들을 해볼 수 있었던 것 같습니다.



이를테면, 아래 내용에 대해 한 번 생각해주세요.


- 정적(static) 메소드를 왜 사용했는가?


- Coffee 에 장식과 관련된 내용을 담지 않고, CondimentDecorator 로 분리한 이유.

 (CondimentDecorator::RemoveCondiment, Coffee 와 CondimentDecorator 의 다른 equals 재정의)


또한, 해당 문제 해결을 위해 주말동안 수고 많으셨습니다.

이 노력들이 꼭 도움이 되길 바랍니다. 


감사합니다. :-)

반응형
Posted by N'

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

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


지난 시간에 진행한 스터디에서는 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'

6. 리팩토링.pdf



마지막 스터디에서 잠시 진행했던, Extra 챕터입니다. 


모두들 수고하셨습니다. @.@ [꾸벅][꾸벅]



참고자료 

- 한국기술교육대학교 - 객체지향개발론및실습 (김상진 교수님)

- Refactoring


추가내용!


Refactoring.zip


이 곳의 내용을 보충할 실습 코드를 첨부합니다.




반응형
Posted by N'