백-엔드에서 가장 많이 해야할 행위 중 하나는 VO 들의 호환을 위한 편집일 수 있습니다. 


백-엔드에서는 프론트-엔드로 입맛에 맞게 데이터를 출력하도록 지원할 수도 있고, 백-엔드 간에도 서로 다른 모듈 간 호환을 맞추기 위해 필요합니다.



이런 VO 의 편집 과정은 간단할 수 있지만 약간의 패턴화를 하면, 


- 더욱 흐름에 집중할 수 있는 비지니스 로직 생성


- VO 편집 과정의 재활용 및 수정의 용이성 


등의 효과를 찾아볼 수 있습니다.



이미 제공하는 클래스들과 실제로 필요한 것의 차이를 극복하기 위한 호환과정을 Adapt 라고 정의하며, 고전적으로 보여지는 Adapter 패턴의 예시를 지난 스터디에서 확인해보았습니다.


자료는 아래에서 확인 :-)




1. Adapter 의 필요성


실제 코드의 리뷰 전, 필요한 요구사항부터 정의해 보겠습니다. 


아래와 같은 클래스 A,B 를 정의해보겠습니다.


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 class A {
 
    private Integer memberSubjectSn;
    private String name;
    private Integer companySubjectSn;
 
    private String field1;
    private String field2;
    private String field3;
    private String field4;
    private String field5;
    private String field6;
 
    // SETTER, GETTER 생략  
}
 
public class B {
    private Integer memberSubjectSn;
    private String name;
    private Integer companySubjectSn;
 
    private String field1;
    private String field2;
 
    // SETTER, GETTER 생략
}
cs


그리고 보통 우리는 어떠한 필요에 의하여, 객체 A 에서 객체 B 로 변환하기 위해 아래와 같은 비지니스 로직을 작성하곤 합니다.


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
44
45
46
public class Service {
 
    /**
     * 데이터 읽기.
     *
     * @return
     */
    public B readData() {
        A a = otherModule.getA();
 
        // something work;
 
        // A 에서 B 로 치환.
        B b;
        {
            b = new B();
            b.setCompanySubjectSn(a.getCompanySubjectSn());
            b.setMemberSubjectSn(a.getMemberSubjectSn());
            b.setName(a.getName());
            b.setField1(a.getField1());
            b.setField2(a.getField2());
        }
 
        return b;
    };
 
    /**
     * 데이터 저장.
     *
     * @param a
     */
    public void saveData(A a) {
        // A 에서 B 로 치환.
        B b;
        {
            b = new B();
            b.setCompanySubjectSn(a.getCompanySubjectSn());
            b.setMemberSubjectSn(a.getMemberSubjectSn());
            b.setName(a.getName());
            b.setField1(a.getField1());
            b.setField2(a.getField2());
        }
 
        // something work;
    }
}
cs


A 에서 B 로 변환하는 과정은 어렵지 않지만, 필요할 때마다 코드의 중복이 일어 납니다. 코드의 중복의 존재는 A 혹은 B 에 변경이 있을 때마다 변환하고 있는 모든 곳을 수정해야함을 의미합니다.


즉 현재 상태는 우아해 보이지 않으며, 코드 중복을 피하기 위해 A에서 B 로 변환하는 메소드를 제공함으로써 아래와 같이 리팩토링을 해볼 수 있습니다.


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
44
45
46
public class Service {
 
    /**
     * 데이터 읽기.
     *
     * @return
     */
    public B readData() {
        A a = otherModule.getA();
 
        // something work;
 
        // A 에서 B 로 치환.
        return adapteAtoB(a);
    };
 
    /**
     * 데이터 저장.
     *
     * @param a
     */
    public void saveData(A a) {
        // A 에서 B 로 치환.
        B b = adapteAtoB(a);
 
        // something work;
    }
 
    /**
     * A 객체에서 B 로 변환
     *
     * @param a
     * @return
     */
    public B adapteAtoB(A a) {
        B b = new B();
        b.setCompanySubjectSn(a.getCompanySubjectSn());
        b.setMemberSubjectSn(a.getMemberSubjectSn());
        b.setName(a.getName());
        b.setField1(a.getField1());
        b.setField2(a.getField2());
 
        return b;
    }
}
 
cs


중복된 부분을 메소드로 캡슐화한 것은 좋은 아이디어입니다.


그러나 아쉽게도 객체 A 와 B 는 Service1, Service2, Service3 등 여러 곳에서 활용될 가능성이 존재합니다. 


즉 adapteAtoB 메소드를 특정 한 모듈에 캡슐화하는 것은 바람직해보이지 않습니다. 다시 말해서, adatpeAtoB 에 대한 책임은 Service class 가 가지고 있을 것이 아닙니다. 책임의 분배가 필요합니다. (SRP : 단일책임의 원칙)



2. 책임의 분배


A 에서 B 로 변경하는 과정을 생각해봤을 때, 이미 제공되어 있는 것(A)과 필요한 것(B)의 차이를 극복하는 것이라 생각해볼 수 있습니다. 


다시 생각해보면, 해당 행위의 이해 관계자는 A 혹은 B 이며 둘 중 하나가 책임을 가져가면 될 것으로 보입니다. 각 클래스가 행위에 대한 책임을 가져가는 방식을 아래와 같이 정의해볼 수 있습니다.


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 class A {
    /**
     * B 로의 변환
     * 
     * <pre>
     *     A 클래스에서 B 로의 변환을 책임지는 경우
     * </pre>
     *
     * @return
     */
    public B toB(){
        B b = new B();
        b.setCompanySubjectSn(getCompanySubjectSn());
        b.setMemberSubjectSn(getMemberSubjectSn());
        b.setName(getName());
        b.setField1(getField1());
        b.setField2(getField2());
 
        return b;
    }
}
 
public class B {
    /**
     * B 클래스에서 A 를 이용하여 인스턴스를 생성
     *
     * <pre>
     *     B 클래스에서 A 를 이용해 자신로의 변환을 책임지는 경우
     * </pre>
     *
     * @return
     */
    public static B FromB(A a){
        B b = new B();
        b.setCompanySubjectSn(a.getCompanySubjectSn());
        b.setMemberSubjectSn(a.getMemberSubjectSn());
        b.setName(a.getName());
        b.setField1(a.getField1());
        b.setField2(a.getField2());
 
        return b;
    }
}
cs


즉 우리는 필요에 따라 클래스 A, B 를 사용하는 곳 어디에서든 변환하는 메소드를 사용할 수 있게 되었습니다. 특히 두 번째 방식은 DB 와 연관이 있는 ORM 에 특화된 클래스의 변환에도 쉽게 적용할 수 있습니다. 



3. 객체 어댑터로 리팩토링


앞써 살펴본 예제들은 결국 객체 B 를 생성하 후 내부 필드를 객체 A 를 이용하여 초기화하는 과정을 캡슐화했다고 생각해 볼 수 있습니다.


이 방식은 연관이 없는 두 클래스간의 의존성을 없앨 수 있는 장점이 있지만, 결국은 깊은 복사 과정이며 어느정도 비용은 있다고 생각합니다. 

(의존성이 없다는 것은 객체 A가 사라진다고 B가 사라지는 것은 아니라는 의미입니다. B 는 그대로 존재하고, A 메소드로부터의 변환과정만 삭제하면 되죠..)


고전적인 디자인 패턴에서 제시하는 객체어댑터 방식은 제공하는 객체 A 를 use-a 관계로 유지하면서, B 의 인터페이스의 기능을 제공해주는 방식입니다.


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
public class B {
    private A a;
 
    /**
     * A 를 내부에서 참조.  
     * 
     * B 의 제공 인터페이스들은 A 인스턴스를 이용하여 구현
     * 
     * @param a
     */
    public B(A a){
        this.a  = a;
    }
 
    public Integer getMemberSubjectSn() {
        return a.getMemberSubjectSn();
    }
 
    public String getName() {
        return a.getName();
    }
 
    public Integer getCompanySubjectSn() {
        return a.getCompanySubjectSn();
    }
 
    public String getField1() {
        return a.getField1();
    }
    
    public String getField2() {
        return a.getField2();
    }
}
cs


이 방식은 얕은 복사로 어댑터를 수행할 수 있으며, B 의 목적을 충실히 달성할 수 있습니다. 그러나 A 클래스에 매우 의존적이라는 것은 염두해야합니다.



4. 상속관계를 가질 수 없는 두 클래스 간 상속관계 표현하기


ORM 은 백-엔드 개발자가 DB 를 다루는 것의 생산성을 향상 시켰습니다. 하지만 아쉽게도 ORM 클래스는 아쉽게도 개발자의 제어영역이 아닙니다. 

예를들어 IBatis에서 DB generate 를 하게되면, 해당 클래스 작업한 내역은 모두 사라집니다. ㅡㅡ^


사실 클래스의 목적 자체가 DB 의 CRUD 를 위한 목적이기 때문에 그 목적에만 충실해야하지만, 실제 비지니스 로직에서 이 클래스를 직접적으로 사용하는 것 역시 자유로워야 합니다. 


즉 이러한 사유로 아래의 두 ORM class는 특정 부모클래스나 인터페이스로 묶을 수 없습니다.


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
44
45
/**
 * 제너레이트 클래스 예제1
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class GenerateModel1 {
    // 공통필드
    private Integer memberSubjectSn;
    private String name;
    private String commomField1;
    private String commomField2;
    private String commomField3;
    private String commomField4;
 
    // 클래스 고유 필드
    private String model1SpecialField1;
    private String model1SpecialField2;
    private String model1SpecialField3;
 
    // setter, getter 는 생략
}
 
/**
 * 제너레이트 클래스 예제2
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class GenerateModel2 {
    // 공통필드
    private Integer memberSubjectSn;
    private String name;
 
    private String commomField1;
    private String commomField2;
    private String commomField3;
    private String commomField4;
 
    // 클래스 고유 필드
    private String model2SpecialField1;
    private String model2SpecialField2;
    private String model2SpecialField3;
    private String model2SpecialField4;
 
    // setter, getter 는 생략
}
cs


두 클래스를 특정 인터페이스로 묶고 싶은 이유는 상위 추상 클래스를 이용하여 동일한 작업을 하고 싶기 때문입니다.


해당 요구사항을 처리하기 위한 방법으로 제시한 첫 번째 방법은 Template-method 패턴을 이용하는 것이었습니다.



하지만 모든 공용 로직을 일일이 Template-method 화 시킬 수는 없습니다. 

너무 많은 패턴화는 심각한 코드-파편화를 불러오며, 오히려 유지보수가 쉽지 않을 수 있습니다.


이 문제를 Adapter 를 이용하여 보다 쉽게 풀어볼 수 있을 것 같습니다. 아래와 같이 말이죠..


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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
 
/**
 * GenerateModel1 과 GenerateModel2 두 클래스의 슈퍼클래스 정의
 * 
 * Created by Doohyun on 2017. 5. 21..
 */
public class AbstractGenerateModel {
    private Integer memberSubjectSn;
    private String name;
    private String commomField1;
    private String commomField2;
    private String commomField3;
    private String commomField4;
    
    // 남용 방지를 위한 생성자 접근제한
    private AbstractGenerateModel(){}
 
    /**
     * GenerateModel1 을 이용한 AbstractGenerateModel 생성
     * 
     * @param model1
     * @return
     */
    public static AbstractGenerateModel FromGenerateModel1(GenerateModel1 model1) {
        AbstractGenerateModel model = new AbstractGenerateModel();
 
        model.memberSubjectSn = model1.getMemberSubjectSn();
        model.name = model1.getName();
 
        model.commomField1 = model1.getCommonField1();
        model.commomField2 = model1.getCommonField2();
        model.commomField3 = model1.getCommonField3();
        model.commomField4 = model1.getCommonField4();
 
        return model;
    }
 
    /**
     * GenerateModel2 을 이용한 AbstractGenerateModel 생성
     *
     * @param model2
     * @return
     */
    public static AbstractGenerateModel FromGenerateModel1(GenerateModel2 model2) {
        AbstractGenerateModel model = new AbstractGenerateModel();
 
        model.memberSubjectSn = model2.getMemberSubjectSn();
        model.name = model2.getName();
 
        model.commomField1 = model2.getCommonField1();
        model.commomField2 = model2.getCommonField2();
        model.commomField3 = model2.getCommonField3();
        model.commomField4 = model2.getCommonField4();
 
        return model;
    }
 
    /**
     * GenerateModel1 로 치환
     * 
     * @return
     */
    public GenerateModel1 toGenerateModel1(){
        GenerateModel1 model1 = new GenerateModel1();
        model1.setMemberSubjectSn(memberSubjectSn);
        model1.setName(name);
        model1.setCommonField1(commomField1);
        model1.setCommonField2(commomField1);
        model1.setCommonField3(commomField1);
        model1.setCommonField4(commomField1);
        
        return model1;
    }
 
    /**
     * GenerateModel2 로 치환
     *
     * @return
     */
    public GenerateModel2 toGenerateModel2(){
        GenerateModel2 model2 = new GenerateModel2();
        model2.setMemberSubjectSn(memberSubjectSn);
        model2.setName(name);
        model2.setCommonField1(commomField1);
        model2.setCommonField2(commomField1);
        model2.setCommonField3(commomField1);
        model2.setCommonField4(commomField1);
 
        return model2;
    }
 
    // setter, getter 는 생략
}
 
cs


공용 필드를 가진 클래스를 하나 작성하며 이 클래스는 어댑터 하고자 하는 두 클래스의 양방향 Adapter 역할을 수행할 수 있다면, 충분히 상위클래스의 역할을 수행해 줄 수 있습니다.


이제 비지니스 로직을 아래와 같이 작성해 볼 수 있을 것 같군요. :-)


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
44
45
46
47
public class Service {
 
    public void task() {
 
        // func1 수행
        {
            // model1 으로부터의 실행
            GenerateModel1 model1 = new GenerateModel1();
            func1(AbstractGenerateModel.FromGenerateModel1(model1));
 
            // model2 으로부터의 실행
            GenerateModel2 model2 = new GenerateModel2();
            func1(AbstractGenerateModel.FromGenerateModel1(model2));
        }
 
        // func2 수행
        {
            // model1 저장!!
            AbstractGenerateModel data1 = func2();
            GenerateModel1 model1 = data1.toGenerateModel1();
            model1Dao.create(model1);
 
            // model2 저장!!
            AbstractGenerateModel data2 = func2();
            GenerateModel2 model2 = data2.toGenerateModel2();
            model2Dao.create(model2);
        }
    }
 
    /**    
     * 공용 메소드 1
     * 
     * @param model
     */
    public void func1(AbstractGenerateModel model) {
        // SOMETHING WORK.
    }
 
    /**
     * 공용 메소드 2
     *
     * @param model
     */
    public AbstractGenerateModel func2() {
        // SOMETHING WORK.
    }
}
cs


이 방법은  Template-method 를 이용하는 방식에 비하여 제작 공수나 코드 파편화가 적습니다. 더군다나 객체 형변환을 위해 instanceof 도 사용할 필요가 없겠군요. 지원한다는 class 는 메소드명으로 확인할 수 있으니 말이죠.


하지만, 변환과정을 수행할 때마다 비용이 있다는 것은 무시할 수 없어보입니다. 게다가 깊은 복사과정을 수행하기 때문에, 어댑터 객체의 변화를 원본 객체에게 알려줄 필요가 있습니다. 마지막으로 비슷한 로직을 사용하지만 각 구체화 전략마다 특이점에 꽤 차이가 있다면, strategy, template-method 등을 다시 고려하는 것이 좋아보입니다. 



Adapter 만 잘 활용해주더라도 비지니스 로직의 큰 문제를 줄일 수 있다고 생각합니다. 보다 더욱 가독성과 유지보수에 좋은 코드를 작성할 수 있으며, 추 후 스터디함수형 메소드에 적극적으로 활용할 수 있는 여지를 만들어 줄 수 있습니다.


이 포스팅이 모두의 기술향상에 도움이 되길 바랍니다. :-)




반응형
Posted by N'