오늘의 포스팅은 살짝 특별합니다. 


왜냐하면, 기술블로그인 "초보프로그래머의 개발 이야기" 에서 첫 번째로 기술내용이 없는 포스팅이기 때문이죠. 

그렇습니다. 오늘 포스팅 주제는 잠시 쉬어가는 후기입니다. 


그럼 정식으로 다시 인사를...



 

뜬금없이 갑자기 웬 후기인가 생각해보면, 


조회수가 8000명이 돌파! 


는 아니고, 


현재 진행하는 이 블로그의 두 번째 장인 [OOP & FP 연구] 수업자료 제작이 끝이 났고, 연구 주제 중 OOP 의 마지막 챕터가 끝나가고 있기 때문이죠. 즉 1부 끝? ㅡㅡ^


마치 만화책 단행본에 있는 작가 후기같은 글을 한 번 써보고 싶었습니다.

일단 지금까지는 계획대로 스터디(한 번쯤은 꼭 제대로 해보고 싶었던 ㅜㅡㅜ)가 잘 진행되고 있고,  다음 주제도 잘해보자는 자축 뻘글입니다.




어쨌든 이 블로그와 스터디 의 머리글을 이제서야 한 번 써볼까 합니다.



1. JAVA8 카테고리


블로그를 처음 운영하게 된 것은, 할 일 없는 잉여 개발자가 갑자기 JAVA8 에 꽂혀서 보게된 JAVA8 in Action 이었습니다. 


우리 스터디의 두번 째 주제인 FP 진행을 할 수 있는 바탕이죠. (스포일러)

그런데 어쩌다 선정한 도서가 사내 표준이 되는 건 위험한데..... 뭐 그건 중요한게 아니니..



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



작년 이 맘 때, 일하다가 쉬는 시간에 페이스북의 어느 타임라인을 훑어보다가 우연히 JAVA8 을 익혀야 하는 이유에 대한 어느 삽화를 보게 되었습니다. 


그 삽화의 내용은 "나무를 도끼로 잘 베던 나무꾼이 전기톱이 나왔음에도 불구하고 도끼만 사용하는 이야기" 였습니다. 


이 삽화 내용은 단순했지만, 아는 것만 가지고 그 선에서 제품을 만들고 있는 저에게 꽤 신선한 충격을 주었습니다. 후배들에게 Comfort zone 을 벗어나야 한다고 말했지만, 벗어나지 못하는 것은 저였죠. ㅜㅡㅜ


그래서 무작정 책을 구입 했는데 내용이 꽤 어렵다는 생각도 들었고,

당시 사내에서는 JAVA8 을 쓰지 못했기 때문에 망각의 늪에 빠질 수 밖에 없다는 생각이 들었습니다.


그래서 생각한 것이 바로 "블로그" 였습니다.

혼자 공부를 하게 되면 모르는 것은 그냥 넘어갈 것이 뻔하고, 남에게 공개하는 내용이니 JAVA8 말고도 관련 내용을 깊게 알아야 할 수 밖에 없겠다는 생각을 했죠. 


무엇보다 이 곳은 공부한 후 핵심을 정리하는 곳이 되니, 여러모로 이득일 것이라 생각했습니다.


그래서 아래와 같이 첫 글을 쓰고,



열공모드에 들어갔습니다. 


하지만 작년 하반기에 일정이 빡세서(쿨럭) 망할 뻔했으나 ㅡㅡ^

다행히도 미친잠재력을 가진 의지 덕분에 약 9개월간의 포스팅은 무사히 잘 끝냈습니다.





2. OOP & FP 스터디 개설


스터디는 회사에 입사하기 전 부터도 많이 해봤었지만, 제대로 해 본 경험은 없었습니다. 


망하는 이유는 여러가지가 있는데 경험 상 간단히 추려보면,


- 다수 사람이 모이면, 약속시간을 잡기 애매해지는 문제 (각자의 사정이 너무 많은...)

- 스터디 이탈 인원들이 누적되는 문제 (한 주씩 빼먹는 사람들이 많으면.. 에효..)


그리고 가장 큰 문제는 처음의 열정이 지속되지 않는 문제 정도라고 할 수 있습니다.


의무감 없는 모임이기 때문에 쉽게 생각하는 경향이 많으며, 이러한 분위기가 스터디 내에 커지면 곧 망한다는 것을 많이 확인 했었습니다.


평소부터 스터디 그룹을 운영해보고 싶다는 생각은 계속 했었지만 시간을 굳이 내서 같이 할 사람들은 안타깝게도 없었고, 아마 이러한 사유로 블로그에 포스팅 정도 했었던 것 같네요..


그러나 회사에서 갑자기 웬 걸, 업무 시간에 시간내서 스터디를 하라고 하는군요.


처음에는 혼자 맥북들고 로비로 내려가서 공부하려 했으나, 

혼자만의 생각에 갇혀버리는 것을 견제하고 같이 연구하는 문화를 만들고 싶었습니다.

(개발자는 은퇴할 때까지 평생 연구를 해야 하니까요. ㅡㅡ^)


일단 가장 만만한 부사수와 막 들어온 신입사원을 포획했습니다.

(갑자기 공부하자고 하니 싫었을 것 같은데, 못난 선배를 만난 안타까운 현실...)

 


이제 사람은 모았고, 스터디를 시작하게 되었습니다. 


주제는 JAVA8 을 생각하고 있었습니다. 

작년부터 포스팅한 내용들이 있으므로 스터디 주체자 입장에서도 부담이 가지 않았고, 곧 JAVA8 역시 팀 내에서 사용할 수 있을 것 같았기 때문이었죠.


하지만 후배들에게 JAVA8 보다는 일단 학부시절 때부터 배운 OOP 의 기초부터 필요하지 않을까에 대한 고민을 했습니다. 


제 입장에서는 학부 때부터 계속 OOP 를 보왔고 최근까지 OOP & FP 열코딩, 열공을 했지만, 후배들 입장에서 갑자기 쌩뚱맞은 FP 가 포함된 JAVA8 을 잘 흡수할 수 있을까에 대한 고민을 했습니다. 

(공감이 안되면, 의미없는 시간이 아닌지...)


이러 저런 고민 후 공표는 했는데, 한 가지 간과한 사실이 있었습니다.


"아... 난 OOP 자료가 없지..   "


급 후회가 몰려 왔지만, 내가 모집한 스터디의 후배들이 자신의 시간을 써주겠다는 데 형편 없는 강의안을 만들 수는 없었습니다. 


매일같이 집 앞의 STARBUCKS 를 일요일마다 출근하며, 구글링+책을 통해 나름 복습의 시간을 거치면서 어떻게든 강의안과 샘플예제, 추가 후기들을 뽑아냈던 것 같습니다.



바쁜 일정에 몸은 조금 지쳤지만, 남에게 공유하기용으로 자료를 준비하면서 얻는 것이 많았고 다시 한번 가지게 된 OOP 복습의 시간도 의미있는 시간들이었습니다.


하지만 무엇보다도 스터디하는 시간이 좋았으며(생애 처음으로 잘 된 정규 모임),

실제 후배 소스에서 알려준 것이 등장 하거나, 도움이 된다는 말들(실제는 모르지만...)은 계속 자료를 만들 수 있는 원동력이 되었던 것 같습니다.



어쨌든 혼자가 아닌 같이 잘 따라와주는 후배들의 도움 덕분에, 스터디의 첫번째 목표였던 OOP 관련 내용도 슬슬 끝을 향해 달려가고 있습니다.


많은 디자인 패턴 책에 등장하는 공통적인 모든 패턴을 다루면 좋겠다고 생각하지만,

(스터디장의 부질없는 욕심 )


현재 사내에서 진행하는 웹 프로젝트에서는 사용할만한 패턴은 정해져 있다는 판단이 들었고, 당장 쓰기 힘든 패턴을 많이 공유하는 것이 의미가 있을지에 대한 생각이 들었습니다.

(나머지패턴들은 More OOP & FP 스터디에서 다루는 것도 방법일 듯 ^^;)


남은 [FP 관련 주제들 + 내가 알고 있는 지식공유] 역시 잘 끝나길 바라며, 이 스터디가 끝날 때 쯤 다시 쓰는 후기에는 평판이 올라간 후배들 자랑을 쓰고 싶네요...



이상 후기 끝입니다.



참고 : 삽화들은 직접 그린 것임..



반응형
Posted by N'

백-엔드에서 가장 많이 해야할 행위 중 하나는 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'

지난 시간 Instance 수를 제한하는 Singleton 패턴을 배웠었고, 과제로 한 개의 Instance 만 제한하는 것이 아닌 특정 개수만큼의 Instance 를 제한하는 Multiton 을 만들어 보도록 하였습니다.


관련 내용은 아래 포스팅을 참고!



똑똑한 우리 스터디 구성원 모두 과제를 잘해왔고, 정리하는 차원에서 제가 만든 Multiton 을 Review 해보도록 하겠습니다.


일단 가정은 요구사항의 SetInstanceCount 시점에 Instance pool 을 만드는 것이 아닌(Eager-binding), 개수만 먼저 제한하고 필요시점마다 Instance 를 생성하는 게으른 할당(Lazy-binding)으로 제작해 보겠습니다.


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
public class Multiton {
    // INSTANCE POOL 관리.
    // 어떤 세팅도 하지 않았으면, Singleton 으로 관리하도록 처리.
    private static int INSTANCE_LIMIT_COUNT = 1;
 
    // Instance pool.
    private static HashMap<Integer, Multiton> POOL = new HashMap<>();
 
    private static Integer toggleCount = 0;
 
    private String toStringMessage;
 
    /**
     * 외부에서 Instance 를 만들지 못하도록 접근제한.
     */
    private Multiton(){
        // Instance 의 생성과 함께 POOL 에 넣는다.
        Integer number = POOL.size();
 
        toStringMessage = String.format("%d번 인스턴스", number);
        POOL.put(number, this);
    }
 
    /**
     * Instance 출력.
     *
     * <pre>
     *     Lock 을 메소드에 모두 걸어 Thread-safe 를 보장.
     * </pre>
     *
     * @return
     */
    public static synchronized Multiton GetInstance() {
        final Multiton result;
 
        if (POOL.size() < INSTANCE_LIMIT_COUNT) {
            // Instance 개수가 아직 제한된 POOL 만큼 생성안된 경우.
            result = new Multiton();
        } else {
            // Instance 개수가 아직 제한된 POOL 만큼 생성 된 경우.
            result = POOL.get(toggleCount);
            toggleCount = (toggleCount + 1) % INSTANCE_LIMIT_COUNT;
        }
 
        return result;
    }
 
    /**
     * 인스턴스의 개수 제한
     *
     * @param limitCount
     */
    public static final void SetInstanceCount(int limitCount) {
        if (limitCount < 0) {
            throw new RuntimeException("[에러] Instance 개수는 0보다 커야 합니다.");
        }
        INSTANCE_LIMIT_COUNT = limitCount;
    }
 
    @Override
    public String toString(){
        return toStringMessage;
    }
}
cs


코드 내부의 구체적인 주석을 명시 하였고, 주의깊게 봐야할 부분은 아래와 같습니다.


1. Thread-safe 보장을 위해 Instance 를 유일하게 외부로 내보낼 수 있는 GetInstance 메소드에 synchronized 키워드를 달았습니다.


2. 사용자가 SetInstanceCount 를 사용하지 않을 수 있기 때문에 Instance 개수 제한을 명시적으로 1로 초기화하였습니다.

(INSTANCE_LIMIT_COUNT = 1)


이렇게 제작된 코드를 Thread-safe 가 보장이 되는지 확인해보도록 하겠습니다.


아래 테스트는 Thread 를 50 개 제작하여, 실행하는 메소드입니다. 각 스레드에서는 각 Instance 의 toStringMessage 를 출력하도록 합니다.


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
// Thread Test using JAVA7
{
    System.out.println("멀티톤 테스트!!");
    Multiton.SetInstanceCount(5);
 
    // Thread 목록 생성.
    final ArrayList<Thread> multiThreadTestList = new ArrayList<>();
    {
         for (int i = 0; i < 50++i) {                    
            Thread newThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Multiton.GetInstance());
                }
            });
 
            multiThreadTestList.add(newThread);
        }
    }
 
    // Thread 실행
    {
        // Thread 생성 즉시 바로 실행하지 않고, 목록을 생성하고 실행하는 이유는 최대한 동시에 Thread 를 실행하고 싶기 때문!
        // Thread Instance 를 만드는 시간 때문에 제대로 테스트가 안될 가능성 존재.
        for (Thread thread : multiThreadTestList) {
            thread.start();
        }
    }
}
 
// Thread Test using JAVA8
{
    IntStream.rangeClosed(050).mapToObj(n -> new Thread(() -> System.out.println(Multiton.GetInstance()))).forEach(Thread::start);
}
 
// CONSOLE LOG
// 멀티톤 테스트!!
// 0번 인스턴스
// 1번 인스턴스
// 2번 인스턴스
// 3번 인스턴스
// 4번 인스턴스
// 1번 인스턴스
// 3번 인스턴스
cs


CONSOLE LOG 를 보면, Instance 가 꼭 순서대로 나오지는 않는 것을 볼 수 있습니다. 

비록 GetInstance 에서 LOCK 처리는 하였지만 Thread 의 실행 상태에 따라 순서가 바뀔 수는 있습니다. 

순서를 보장해야만 한다면 Blocking 처리 등 더 복잡한 과정이 필요하겠지만, 이번 주제는 Instance 의 개수를 제한하는 것이니 다루지 않을 예정입니다.


이 블로그가 스터디를 참여하고 있는 모두에게 좋은 정보가 되길 바랍니다. :-)

반응형
Posted by N'

5. 실무 활용 패턴 (하).pdf



디자인 패턴과 관련된 마지막 STUDY 입니다.


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


참고자료 

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

- [Head First] Design Pattern

반응형
Posted by N'

지난 스터디에서 다룬 Singleton 은 Instance 를 한 개로 제한함을 알 수 있었습니다.


이를 이용해서 우리는 Instance 를 두개로 제한하는 아래와 같은 Doubleton 을 구현할 수 있었습니다.


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
enum Doubleton {
    SINGLE("1번 인스턴스"), DOUBLE("2번 인스턴스");
 
    public static Main.Doubleton GetInstance() {
          // 토글
        a = (a + 1) % 2;
 
        Doubleton result = null;
            
        switch (a) {
        case 0:
            result = DOUBLE;
            break;
        
        case 1:
            result = SINGLE;
            break;
        
        default:
            new RuntimeException(String.format("[에러] 예상못한 TOGGLE 변수 : %d", a));
        }
        
        return result;
    }
 
    private static int a = 0;
    private String toStringMessage;
 
    Doubleton(String toStringMessage) {
        this.toStringMessage = toStringMessage;
    }
 
    @Override
    public String toString(){
        return toStringMessage;
    }
}
 
// TEST CODE
for (int i = 0; i < 100000++i) {
    new Thread(() -> {
        String message = Doubleton.GetInstance().toString();
        System.out.println(message);
    }).
    start();
}
 
// CONSOLE LOG
// 1번 인스턴스
// 2번 인스턴스
// 1번 인스턴스
// 2번 인스턴스
// 1번 인스턴스
// .........
cs


하지만 아쉽게도 enum 이용한 Doubleton 은 Instance 의 갯수를 동적을 조절할 수 없어보이네요. 

우리는 Instance 의 개수를 동적으로 조절할 수 있는 Multiton 을 만들어보는 것이 목적입니다.


아래와 같이 동적으로 미리 Instance 개수를 입력받도록 세팅을 먼저 했으면 좋겠습니다.

물론 Thread-safe 도 보장하면 좋겠죠? 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Multiton.SetInstanceCount(5);
 
for (int i = 0; i < 100000++i) {
    new Thread(() -> {
        String message = Multiton.GetInstance().toString();
        
        System.out.println(message);
    }).start();
}
 
// CONSOLE LOG
// 1번 인스턴스
// 2번 인스턴스
// 3번 인스턴스
// 4번 인스턴스
// .....
cs


그럼 우리 스터디 멤버들 파이팅!!!! :-)

반응형
Posted by N'

이번 주에는 실무에서 아마 가장 많이 사용하는 패턴 중 하나인 Singleton 패턴을 다뤄보았습니다.


자료는 아래 포스팅에서 참고!



물론 웹 프로젝트에서는 직접 Singleton 을 만들기보다는 Bean 등록을 통해 Component 들을 사용하지만, 안드로이드 프로젝트 혹은 자바가 아닌 다른 환경에서는 직접 만들어야 할 일이 있을 수 있습니다.


그렇기 때문에 세 번째 실무 주제로 Singleton 을 선택하였습니다.


Singleton 은 클래스 내에서 한 개의 instance 만 생성하여, 그 인스턴스를 광역적으로 사용하기 위한 패턴입니다. 

(웹 프로젝트에서는 각종 Component 를 Controller 가 광역적으로 사용하고 있습니다.)


1. Singleton 의 기본적인 컨셉


기본적인 Singleton 의 컨셉은 아래와 같이 정의해 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 싱글톤 예제
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스는 클래스 내부에서 공용으로 한개만 관리.
    private static Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {}
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
 
        return instance;
    }
}
cs


instance 를 한 개만 생성하고 관리하는 것이 목적이므로, 클래스 내부에서 instance 를 공용으로 관리하고 instance 를 접근할 수 있는 정적 메소드를 제공해줍니다.


하지만 이 기본적인 컨셉은 안타깝게도 동시성 처리에 취약합니다.

내부 GetInstance 를 동시에 여러 Thread 가 접근할 시, instance 의 null 체크의 Thread-safe 를 보장할 수 없습니다.



2. Thread-safe 한 Singleton


Singleton 의 Thread-safe 문제를 해결하기 위해 생각해 볼 수 있는 방법은 공용으로 관리하려는 instance 에 대한 명시적 초기화를 해주는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 명시적 초기화한 싱글톤 예제
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = new Singleton();
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {}
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        return instance;
    }
}
cs


클래스 로드 타임에 미리 instance 가 생성되기 때문에 Thread-safe 합니다. 

이런  방식을 Eager-binding (부지런한 할당)이라 합니다. 하지만 Eager-binding  방식은 싱글톤의 instance 를 사용하지 않음에도 바로 초기화가 되어버릴 수 있습니다.


아래 예제 처럼 말이죠.


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 Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = new Singleton();
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
        System.out.println("Create Singleton!!");
    }
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        return instance;
    }
 
    /**
     * 싱글톤의 정적 메소드
     */
    public static void RunStaticTask() {
    }
}
 
Singleton.RunStaticTask();
 
// CONSOLE LOG
// Create Singleton!!
cs


하지만 만약 Singletone 을 만들기 위한 비용이 크고, 많은 Singleton 들이 Eager-binding을 시도한다면 부하가 클 수도 있습니다. 즉 Instance 를 사용할 때 생성하는 Lazy-binding(게으른 할당)이 필요해 보입니다.


Eager-binding 이 무조건 나쁜 것은 아닙니다. 서버 프로젝트의 경우 오히려 서버가 켜질 때 각 컴포넌트들이 미리 Instance 를 생성하고, Client 의 요청에 딜레이 없이 바로 응답해주는 것이 좋습니다.


3. Lazy-binding 방식의 Singleton 생성


다시 기본적인 컨셉으로 돌아와서 생각해 봤을 때, 직관적인 방법은 오직 한 개의 스레드 만 접근하도록 Lock 을 걸어주는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 스레드의 Lock 을 이용한 싱글톤 예제 (1)
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
 
    // synchronized 로 Lock 을 건다.
    // 이 메소드는 오직 한 스레드 씩 접근 가능
    public static synchronized Singleton GetInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
 
        return instance;
    }
}
cs


메소드에 Lock 을 걸었기 때문에 Thread-safe 함은 보장합니다. 

하지만 매번 사용할 때마다 한 개의 Thread 만 접근 가능하다는 것은 조금 불편한 진실인 듯 합니다. ㅡㅡ^


그렇기 때문에 정말 Thread-safe 한 부분만 Lock 이 걸리도록 변경해보려 합니다. Thread-safe 함이 보장되야할 부분은 instance 의 생성 부분입니다.


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
/**
 * 스레드의 Lock 을 이용한 싱글톤 예제 (2)
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 스레드 간 최산 값을 읽어주기 위해 volatile 을 이용한 가시성 변수 사용
    private static volatile Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
    
    // 개선된 Lock 처리
    public static Singleton GetInstance() {
        if (instance == null) {
            // instance 의 생성 부분만을 Lock 처리
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
 
        return instance;
    }
}
cs


하지만 이 방식은 가시성 변수를 사용하고 있습니다. 

instance 변수는 모든 스레드가 최신 값을 읽게 하기 위해 CPU 의 캐시를 사용하지 않으며, 그렇기 때문에 무조건 메모리로부터 데이터를 불러오게 됩니다.


- 간략한 운영체제 속 내용 소개 ㅡㅡ^


데이터는 모두 메모리(Heap, Data, Stack 영역) 위에 저장되어 있고, 보통은 CPU가 계산을 하기 위해 데이터를 읽어온 뒤 캐싱 작업을 합니다. 

캐싱작업을 하는 이유는 CPU 에서 미리 데이터를 저장함으로써, 보다 빠르게 읽고 계산하기 위해서죠.


하지만 여러 스레드가 변수를 동시접근을 하여 데이터를 조작하면, CPU 캐시의 값이 최신 값임을 보장할 수 없게 됩니다. 

즉 변경된 값을 못 읽어올 수 있죠.


그렇기 때문에 변수에 volatile 키워드를 붙여 가시성 변수임을 선언하며, 그 변수는 항상 메모리에서 읽어오게 됩니다. 

하지만 CPU 의 캐시에서 읽어오는 것보다 비용이 언제나 크겠죠? ㅜㅡㅜ



3. Lock 을 사용하지 않은 Lazy-bindig 방식의 Singleton


MARYLAND 대학의 연구원인 Bill puge 는 Lock 을 사용하는 방식은 단점을 해결하기 위해 새로운 Singleton 생성방식을 고안했습니다.


그 방식의 이름은 "Initialization on demand holder idiom" 으로 내부 클래스를 이용한 Holder 를 사용하는 기법은 클래스 로더 규칙에 의해 Lazy-binding 을 보장합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Initialization on demand holder idiom 방식의 싱글톤 생성
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // Instance 를 감싸는 홀더 생성. 클래스로더 규칙에 의해, ManagerHolder 가 로드될 때 Instance 생성
    private static final class ManagerHolder {
        private static final Singleton Instance = new Singleton();
    }
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
 
    // 개선된 Lock 처리
    public static Singleton GetInstance() {
        return ManagerHolder.Instance;
    }
}
cs


내부에 있는 ManagerHolder 는 Private 으로 접근제한 되어 있으며, 오직 GetInstance 를 통해서만 사용할 수 있습니다. 즉 GetInstance 가 호출 될 때 ManagerHolder 클래스가 로드되며 그 순간 명시적 초기화를 하고있는 내부의 Instance 가 생성됩니다.



4. Enum 을 사용하는 Singleton


Enum(열거형) 을 통해서도 Singleton 을 만들 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * ENUM 을 이용한 싱글톤
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public enum Singleton {
    INTANCE;
    
    public void doSometing(){}
}
 
// Enum 싱글톤 사용법
Singleton.INTANCE.doSometing();
cs


간결한 사용법 때문에 최근 권장하는 스타일입니다. 


이 방식은 열거형 특성 상, 컴파일 시점에 미리 Instance 가 생성되기 때문에 Thread-safe 함을 보장합니다. 하지만 무조건 Eager-binding 입니다.


Singleton 을 만드는 여러 방식을 알아보았습니다. 우리는 Spring 이 아니더라도 Singleton 을 직접만들어 사용할 수 있게 되었습니다. 


하지만 주의할 것은 스터디에서도 살펴본 것 처럼 싱글톤 병에는 안 걸리게 조심합시다. ㅡㅡ^

반응형
Posted by N'

지난 주에 이어, 계속 디자인 패턴 종류를 실습하고 있습니다.


이번 주에는 지난 스터디에서 실습한 전략패턴과 비슷한 구조의 Template Method 패턴을 다뤄보았습니다.


자료는 아래 포스팅에서 참고! 



이번 추가 내용 포스팅 역시 스터디에서 진행한 코드를 기반으로 진행하고자 합니다.


Template Method 패턴 역시 변경소지가 있는 부분을 분리하고, 위임을 통해 각 클래스가 행위를 하도록 하는 전략패턴과 비슷한 구조를 취하고 있습니다. 차이점은 전략패턴의 경우 각 클래스의 행위가 크게 다른 반면, Template Method 의 경우에는 크게 보면 비슷한 알고리즘을 사용하지만 세부 행위가 조금씩 다를 경우 사용할 수 있는 패턴입니다.


조금 더 쉬운 예제를 생각해보면, 우리가 흔히 사용하는 Collections 클래스의 sort 는 동일한 정렬 알고리즘을 사용하지만 비교하는 부분인 Comparable 은 다르기 때문에 따로 구체화하여 사용합니다.


Template Method 의 정의는 이 정도로 하고, 스터디에서 진행했던 코드를 리뷰해보도록 하겠습니다.


우리는 DB 접근의 편의성 때문에 ORM 을 사용하곤 합니다.

ORM 을 사용하면, Table 과 실제 객체간의 관계를 호스트 코드에서 쉽게 관리할 수 있기 때문에 생산성 면에서 좋다고 생각할 수 있습니다.


하지만 종종 아래와 같이 비슷한 구조의 필드를 가진 테이블이 존재할 수 있으며, ORM 클래스들은 아쉽게도 다른 테이블을 표현한 클래스들과 상속 또는 구현 관계를 취할 수 없습니다.


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 commonField1;
    private String commonField2;
    private String commonField3;
    private String commonField4;
 
    // 클래스 고유 필드
    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 commonField1;
    private String commonField2;
    private String commonField3;
    private String commonField4;
 
    // 클래스 고유 필드
    private String model2SpecialField1;
    private String model2SpecialField2;
    private String model2SpecialField3;
    private String model2SpecialField4;
 
    // setter, getter 는 생략
}
cs


ORM 의 특성 상 ORM 라이브러리에 의존해서 데이터의 CRUD 를 하기 위해서는 위의 클래스를 이용하는 방법밖에 없습니다. 그렇기 때문에 우리는 비지니스 로직 처리 중 이러한 문제를 겪을 수 있습니다.


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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
 * Model1 과 Model2 를 사용하는 서비스 클래스
 * 
 * Created by Doohyun on 2017. 5. 14..
 */
@Service
public class ModelSampleService {
    @Autowired
    private Model1Dao model1Dao;
 
    @Autowired
    private Model2Dao model2Dao;
 
    /**
     * 데이터를 저장한다.
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
 
        // GenericModel1 로직.
        createByGenerateModel1(addTargetMemberSnList, excludeTargetMemberSnList);
 
        // GenericModel2 로직.
        createByGenerateModel2(addTargetMemberSnList, excludeTargetMemberSnList);
    }
 
    /**
     * GenerateModel1 에서
     *
     * <pre>
     * "addTargetMemberSnList" 의 구성원를 저장하고,
     * "excludeTargetMemberSnList" 를 제외한다.
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    private void createByGenerateModel1(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList){
        /**
         * 새로 저장된 대상자를 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, GenerateModel1> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                GenerateModel1 generateModel1 = new GenerateModel1();
                generateModel1.setMemberSubjectSn(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel1);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<GenerateModel1> existList = model1Dao.selectList(groupByMemberSnMemberMap.keySet());
                for (GenerateModel1 model1 : existList) {
                    excludeTargetMemberSnSet.add(model1.getMemberSubjectSn());
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (GenerateModel1 model1 : groupByMemberSnMemberMap.values()) {
                model1Dao.create(model1);
            }
 
            // 제외대상 삭제.
            model1Dao.deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
 
    /**
     * GenerateModel2 에서
     *
     * <pre>
     * "addTargetMemberSnList" 의 구성원를 저장하고,
     * "excludeTargetMemberSnList" 를 제외한다.
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    private void createByGenerateModel2(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList){
 
        /**
         * 새로 저장된 대상자를 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, GenerateModel2> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                GenerateModel2 generateModel2 = new GenerateModel2();
                generateModel2.setMemberSubjectSn(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel2);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<GenerateModel2> existList = model2Dao.selectList(groupByMemberSnMemberMap.keySet());
                for (GenerateModel2 model1 : existList) {
                    excludeTargetMemberSnSet.add(model1.getMemberSubjectSn());
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (GenerateModel2 model2 : groupByMemberSnMemberMap.values()) {
                model2Dao.create(model2);
            }
 
            // 제외대상 삭제.
            model2Dao.deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
}
 
cs


해당 서비스에서는 Model1, Model2 에 추가하고자하는 대상자순번 목록과 삭제하고자하는 대상자 순번 목록을 파라미터로 받고 있습니다.


내부 로직인 createByGenerateModel1, createByGenerateModel2 에서는 


1. 추가하고자 하는 대상자순번으로 추가대상 ORM 객체를 생성.

2. 이미 있는 데이터를 조회 후, 추가대상에서 제외

3. 제외대상에 있는 데이터를 추가대상에서 제외

4. 최종으로 남아있는 추가대상 데이터를 저장하고, 제외대상 데이터를 삭제


하고 있습니다.


두 메소드의 내부역할은 비슷한 알고리즘을 사용하지만,

두 ORM 클래스는 상속관계를 가질 수 없고, 더군다나 데이터 관리 Dao 도 다르기 때문에 copy&paste 로 다시 정의하게 되었습니다.


하지만 우리는 이 메소드를 작성하면서, 코드 내에서 변화하는 부분과 변하지 않는 부분을 찾아볼 수 있었습니다. 

(단순하게 바로 패턴을 적용하기 보다는 이 것을 찾아내는 능력이 중요합니다. ㅡㅡ^)


두 메소드는 동일한 알고리즘을 사용하지만 아래와 같은 차이점을 찾아낼 수 있습니다.


- ORM 객체를 만들어내는 과정.

- 이미 있는 데이터를 찾아내는 과정.

- 데이터를 추가하거나 삭제하는 과정.


즉 우리는 변하지 않는 알고리즘 베이스는 남기고, 변화하는 부분을 추상적으로 생각할 수 있을 것 같습니다. 다시말하면, 변하지 않는 부분을 정의하는 클래스변하는 부분을 구체화시킨 클래스를 작성하여 각 행위에 대한 책임을 나눠볼 수 있을 것 같습니다. 

(책임의 분리면에 있어서 SRP 를 생각해볼 수 있습니다. ㅡㅡ^) 


일단은 알고리즘 베이스에서 변화하는 부분을 추상화시킨 베이스 클래스를 아래와 같이 제작해 볼 수 있을 것 같습니다.


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
95
96
97
98
99
100
101
102
103
104
105
/**
 * Model 객체의 알고리즘을 템플릿메소드화 시킨 베이스 클래스.
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public abstract class BaseTargetComponent<T> {
 
    /**
     * 해당 테스크를 통해 DB 에 데이터를 저장.
     *
     * <pre>
     *     해당 메소드를 자식클래스들이 재정의하지 못하도록 final 화
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public final void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
        /**
         * 새로 저장할 구성원을 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, T> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                T generateModel = toGenerateModel(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<T> existList = selectList(groupByMemberSnMemberMap.keySet());
                for (T model : existList) {
                    excludeTargetMemberSnSet.add(toMemberSubjectSn(model));
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (T model : groupByMemberSnMemberMap.values()) {
                insertData(model);
            }
 
            // 제외대상 삭제.
            deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
 
    /**
     * 구성원 주체순번으로 모델 생성.
     *
     * @param memberSn
     * @return
     */
    protected abstract T toGenerateModel(Integer memberSn);
 
    /**
     * 모델로부터 구성원주체순번 추출
     *
     * @param t
     * @return
     */
    protected abstract Integer toMemberSubjectSn(T t);
 
    /**
     * 구성원 순번목록으로 모델 데이터 조회
     *
     * @param memberSnList
     * @return
     */
    protected abstract List<T> selectList(Collection<Integer> memberSnList);
 
    /**
     * 데이터 추가.
     *
     * @param t
     */
    protected abstract void insertData(T t);
 
    /**
     * 구성원순번 목록으로 데이터를 삭제.
     *
     * @param memberSnList
     */
    protected abstract void deleteByMemberSnList(List<Integer> memberSnList);
}
 
cs


앞서 살펴본 변화하는 후보 요구사항들은 추상 메소드로 정의하였으며, 변하지 않는 알고리즘은 정의된 추상 메소드를 사용하도록 하였습니다.


주목할 점은 알고리즘이 정의된 메소드는 final 입니다. 

자식클래스들이 혹시나 알고리즘이 정의된 메소드를 재정의할 수 없도록 의도적으로 막았습니다.


이제 정의된 베이스 클래스를 사용하여, Model1 과 Model2 에 특화된 클래스를 만들어 볼 수 있을 것 같습니다. 아래와 같이 말이죠!


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
/**
 * GenerateModel1 에 특화된 구체화 클래스.
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class Model1Compoent extends BaseTargetComponent<GenerateModel1> {
 
    private Model1Dao model1Dao;
 
    /**
     * 구성원순번 을 이용하여, GenerateModel1 을 생성한다.
     *
     * @param memberSn
     * @return
     */
    @Override
    protected GenerateModel1 toGenerateModel(Integer memberSn) {
 
        GenerateModel1 generateModel1 = new GenerateModel1();
        generateModel1.setMemberSubjectSn(memberSn);
 
        return generateModel1;
    }
 
    /**
     * GenerateModel1 으로 부터 구성원순번을 추출한다.
     *
     * @param model1
     * @return
     */
    @Override
    protected Integer toMemberSubjectSn(GenerateModel1 model1) {
        return model1.getMemberSubjectSn();
    }
 
    /**
     * 구성원순번 목록을 이용하여, 모델목록을 조회한다.
     *
     * @param memberSnList
     * @return
     */
    @Override
    protected List<GenerateModel1> selectList(Collection<Integer> memberSnList) {
        return model1Dao.selectList(memberSnList);
    }
 
    /**
     * 데이터를 추가한다.
     *
     * @param model1
     */
    @Override
    protected void insertData(GenerateModel1 model1) {
        model1Dao.create(model1);
    }
 
    /**
     * 구성원순번 목록으로 데이터를 삭제한다.
     *
     * @param memberSnList
     */
    @Override
    protected void deleteByMemberSnList(List<Integer> memberSnList) {
        model1Dao.deleteByMemberSnList(memberSnList);
    }
}
 
cs


Model1Component 는 변화하는 추상적인 개념을 Model1 클래스 전용으로 구체화만 시킨 클래스입니다. 


이제 아래와 같이 ModelSampleService 의 로직을 작성된 컴포넌트를 활용하여 완성해보도록 하죠.


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
/**
 * Model1 과 Model2 를 사용하는 서비스 클래스
 *
 * Created by Doohyun on 2017. 5. 14..
 */
@Service
public class ModelSampleService {
    @Autowired
    private Model1Dao model1Dao;
 
    @Autowired
    private Model2Dao model2Dao;
 
    private Model1Compoent model1Compoent;
 
    /**
     * 데이터를 저장한다.
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
 
        // GenericModel1 로직.
        model1Compoent.createByGenerateModel(addTargetMemberSnList, excludeTargetMemberSnList);
 
        /**
         * GenericModel2 로직.
         *
         * <pre>
         *     구체화상태를 즉시 정의하는 방식.
         *     함수형 프로그래밍에서는 기존의 클래스에만 의존하던 동작을 즉시 정의하는 것이 핵심.
         * </pre>
         */
        new BaseTargetComponent<GenerateModel2>() {
            @Override
            protected GenerateModel2 toGenerateModel(Integer memberSn) {
 
                GenerateModel2 generateModel2 = new GenerateModel2();
                generateModel2.setMemberSubjectSn(memberSn);
 
                return generateModel2;
            }
 
            @Override
            protected Integer toMemberSubjectSn(GenerateModel2 generateModel2) {
                return generateModel2.getMemberSubjectSn();
            }
 
            @Override
            protected List<GenerateModel2> selectList(Collection<Integer> memberSnList) {
                return model2Dao.selectList(memberSnList);
            }
 
            @Override
            protected void insertData(GenerateModel2 generateModel2) {
                model2Dao.create(generateModel2);
            }
 
            @Override
            protected void deleteByMemberSnList(List<Integer> memberSnList) {
                model2Dao.deleteByMemberSnList(memberSnList);
            }
        }.createByGenerateModel(addTargetMemberSnList, excludeTargetMemberSnList);
    }
}
cs


리팩토링한 코드의 라인 수만 본다면, 기존 코드에 비해 코드가 크게 줄어들지는 않았습니다. 오히려, 호스트 코드의 파편화가 생겨 더 복잡해졌다고 생각할 수도 있습니다.


하지만, 이 방식은 변하지 않는다고 생각한 알고리즘 코드에 문제가 있다고 생각되었다면 한 번에 모든 코드를 수정할 수 있고, 비슷한 테이블이 생겼다고 했을 경우 확장에도 유연할 수 있습니다. (OCP 역시 지켜졌다고 생각할 수 있습니다.)


추가적으로 Template method 의 전문적인 용어를 조금 말하면, 각각의 정의된 메소드 역할을 아래와 같이 나눠볼 수 있습니다.


- Concrete method 

알고리즘의 뼈대가 작성된 곳입니다. 

final 화하여, 자식클래스들이 상속을 받지 못하게 합니다.


- Abstract method

변하는 부분을 추상적인 형태로 정의합니다. (추상 메소드)

자식클래스들은 모두 이 메소드를 반드시 구현해야 합니다.


- Hooker

Abstract method 와 같이 변하는 부분을 추상적으로 정의합니다.

그러나 자식클래스들은 이 메소드를 선택적으로 구현하도록 합니다.


포스팅을 마치고 나니, 샘플코드의 양이 조금 복잡함을 느꼈습니다.

모르는 부분이 있으면 언제나 질문해도 좋습니다. 피드백을 남겨주면 더욱 감사 :-)





반응형
Posted by N'

해당 포스팅에서 언급된 내용은 Ndroid 에서 제공합니다.

https://github.com/skaengus2012/Ndroid


매일 코딩을 하던 중 같은 것을 반복한다고 느끼는 경우가 있습니다.


그 중 하나가 아마 예외처리를 하는 것이라 생각합니다.


예외처리는 개발자가 의도적으로 어떤상황이 일어났을 때의 상황을 처리 못하니, 위의 stack 에서 알아서 하라고 던지는(throw) 행위입니다. (사실 이 행위는 프로그램코드가 아닌 실세계에서도 많이 일어납니다. ㅡㅡ^)


일단 예외처리에 대한 제 입장은 이러한 예외처리를 꼼꼼하게 계속해야 한다고 생각합니다. 


조금 회사에서 일을 해보니, 프로그램 작성 시에는 일부로 어떠한 경우들을 일부로 체크안해서 UnCheckException (꼭 처리를 안해도 되는 예외) 을 내보내도 되지 않을까(쉽게는 NullPointerException)생각하여 작성해보았는데 디버깅 시, 유지보수가 더 쉽지가 않음을 깨달았습니다. 


로직의 스탭(모듈 등)간의 관계에 대하여 강결함(?) 보장이 의도적으로 되야 하는데, 의도적으로 예외를 내지 않아버리면 갑자기 어디선가 죽어버리는 녀석을 계속 찾아야하는 것 같습니다. (같은 NullPointer 에 대한 예외를 처리한다 하더라도, 아무것도 안하고 Runtime 중 일으키는 것보다는 의도적으로 RuntimeException 이라도 넘기는 것이 좋다고 생각합니다. 메시지도 같이 말이죠....)


어쨌든 이러한 처리를 아마 아래와 같이 코드를 작성하곤 합니다.


1
2
3
if (a == null) {
    throw new MessageException("잘못된 접근입니다.");
}
cs


비지니스로직에 흔히 작성될 수 있는 코드입니다. 

간단해보이지만 아래와 같이 체크해야할 경우가 많다면 어떨까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (a == null) {
    throw new MessageException("잘못된 접근입니다.");
}
 
if (list == null || list.isEmpty()) {
    throw new MessageException("잘못된 접근입니다.");
}
 
if (string == null || string.toString() == null || string.toString().isEmpty()) {
    throw new RuntimeException("잘못된 접근입니다.");
}
 
if (map == null || map.isEmpty()) {
    throw new MessageException("잘못된 접근입니다.");
}
 
if (!map.containsKey("a")){
   throw new RuntimeException("잘못된 접근입니다.");
}
cs


뭔가 꼼꼼하긴 하지만, if 블럭 안에 예외 한개씩 입력하고 있는 공통점이 있어보입니다. 이러한 단순 작업을 요즘은 툴(인텔리J)이나 어노테이션(NonNull)등으로 어느정도 커버할 수 있지만, 안드로이드나 웹프로젝트에서 같이 작업하는 입장에서 생각할 때는 호스트코드에 작성하는 방식을 아에 무시할 수는 없어보입니다. 

(잘 모르는 것일 수도 있습니다. ~ @.@ ~)


일단은 같이 일하는 선배님께서 이러한 부분을 간결하게 해보자는 것을 제안하셔서, 아래와 같은 코드를 한번 배포해 보았습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 단순한 NullCheck 매크로
 *
 * @param object
 * @param message
 */
public void NullCheck(Object object, String message) throws MessageException {
   if (object == null) {
      throw new MessageException(message);
   }
}
 
 
NullCheck(vo, "잘못된 접근입니다.");
cs


MessageException은 CheckedException 종류로 컨트롤러로 이 예외가 던져진다면, 프론트에서 사용자가 메시지를 볼 수 있도록 합니다.


이 코드로 인하여, 예외를 처리하는 것은 매우 단순하게 간결 해졌습니다. 


복잡한 방식을 간결하게 바꾸는 시도는 여러가지로 시도 되고 있고(인터페이스를 람다로 치환한다는 등..), 이러한 방식을 제안한 선배님께는 감사하고 있습니다.


단순하게 Null 뿐 아니라, string 빈 값, Container 의 빈 상태 등 자주 사용하는 여러 상황을 체크하는 메소드를 만들었고, 잘 사용하고 있었지만 이 코드에는 문제가 있음을 깨닫게 되었습니다.


간결 예외처리 코드는 아쉽게도 MessageException 밖에 출력을 못합니다. 

경우에 따라서는 RuntimeException 을 내보내야할 때도 있으며, 안드로이드와 라이브러리를 공유한다고 했을 때 웹에서만 사용하는 MessageException 을 안고 갈 수는 없었습니다. 


이를 해결하기 위해 예외의 형태를 제네릭으로 받아 동적으로 예외를 만들자는 생각이 들었습니다. 아래와 같은 초안 메소드를 작성하게 되었고, 오버로딩으로 간략하게 사용할 수 있는 메소드도 같이 제공하였습니다.


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
/**
 * Check with CustomException.
 *
 * @param check
 * @param message
 * @param exceptionClass
 * @param <T>
 * @throws T
 */
public final static <extends Exception> void Check(
            @NonNull Boolean check
            , @NonNull String message
            , @NonNull Class<T> exceptionClass) throws T{
    if (!check) {
        final T exception;
 
        try {
            exception = exceptionClass.getConstructor(String.class).newInstance(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
         }
 
        throw exception;
    }
}
 
/**
 * Check.
 *
 * @param check
 */
public final static void Check(@NonNull Boolean check) {
    Check(check, ERROR_BAD_ASSESS, RuntimeException.class);
}
 
 
Check(object != null"[에러] object 는 Null 일 수 없음", RuntimeException.class);
Check(object != null);
cs


이러한 형태로 자주 사용하는 예외체크 타입을 간략화한 유틸클래스를 릴리즈하였으며, 사용은 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Check
CheckUtil.Check(object != null"Occurred Error! Please ask administrator!", RuntimeException.class);
 
// Null check
CheckUtil.NullCheck(object, "Occurred Error! Please ask administrator!", MessageException.class);
 
// String empty check.
CheckUtil.EmptyToStringCheck(object);
 
// Maybe Empty check.
CheckUtil.EmptyMaybeCheck(MaybeUtil.JustNullable(object));
 
// Container Check.
CheckUtil.EmptyContainerCheck(Collections.emptyList());   // Collection
CheckUtil.EmptyContainerCheck("Test""Test2");           // Array
CheckUtil.EmptyContainerCheck(Collections.emptyMap());    // map
cs


RxJava2 에서 제공하는 Maybe 개념까지 CheckUtil 에 넣게 되었습니다.


보다 자세한 내용은 아래 url 에서 확인하실 수 있습니다.



이 포스팅이 보다 간결한 코딩을 하는 것에 도움이 되길 바래요 ~ @.@ ~

반응형
Posted by N'

4. 실무 활용 패턴 (중).pdf



이어지는 패턴이야기입니다.


프린트해오면 최고, 사실 굳이 안해와도 되지 않을까? ~~ @.@ ~~


참고자료 

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

- [Head First] Design Pattern

반응형
Posted by N'

이번주는 본격적인 디자인패턴 종류를 실습해보았습니다. 


책에 있는 내용을 그대로 공부하기 보다는, 실무에서 활용되는 예제를 보면서 진행하는 것이 참신하지 않을까 고민을 했었습니다.


그렇기 때문에 프로젝트를 진행하면서, 느꼈던 노하우들을 같이 정리할 생각입니다. 최대한 현재 직장에 의존적이지 않게 작성하려고 합니다. :-)


자료는 아래 포스팅에서 참고.



서버쪽 프로그램을 작성할 때 우리는 DB 에 어떤 논리적인 상태를 표현하고자 할 경우가 많이 있으며, 보통은 개발자 사이의 약속으로 CODE 를 정의하고 CODE 에 따라 일처리를 다르게 합니다.


일반적으로 아래와 같이 CODE 만을 따로 한 군데에 정의하거나, 혹은 기타 파일로 뺄 수 있겠죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final CODE {
    /**
     * SAMPLE TEST 로 진행할 코트 정의
     */
    public static final class CODE_FOR_SAMPLE {
        public static final String CODE_A   = "CODE_A";                     // 코드 A
        public static final String CODE_A_1 = "CODE_A_1";                   // 코드 A 의 하위 개념1
        public static final String CODE_A_2 = "CODE_A_2";                   // 코드 A 의 하위 개념2
 
        public static final String CODE_B   = "CODE_B";                     // 코드 B
        public static final String CODE_B_1 = "CODE_B_1";                   // 코드 B 의 하위 개념1
        public static final String CODE_B_2 = "CODE_B_2";                   // 코드 B 의 하위 개념2
        public static final String CODE_B_3 = "CODE_B_3";                   // 코드 B 의 하위 개념3
    }
}
 
cs


보통의 비지니스 로직들은 일반적으로 CODE에 의존적일 수 밖에 없습니다. 

코드에 따라 행위가 달라져야 하기 때문이죠. 

아마 아래와 같이 직접적으로 분기처리를 하게 될 것입니다. (각 switch 에서 하는 행위는 중요하지 않습니다.)


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
public class CommonService {
    /**
     * Code 종류에 따라서 업데이트를 수행한다.
     *
     * @param codeForSample
     */
    public void saveData(final String codeForSample) {
        switch (codeForSample) {
            case CODE.CODE_FOR_SAMPLE.CODE_A: {
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A);
 
                System.out.println("하위 컬럼 업데이트");
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A_1);
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A_2);
            }
 
            break;
 
            case CODE.CODE_FOR_SAMPLE.CODE_B: {
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B);
 
                System.out.println("하위 컬럼 업데이트");
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B_1);
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B_2);
                System.out.printf("%s관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B_3);
            }
 
            break;
        }
    }
}
cs


일단 직관적인 면은 좋아보입니다. 각 분기 따라 무슨 일을 하는지 알겠군요. 


그러나 아쉽게도 유연해보이지는 않습니다. CODE_FOR_SAMPLE 에 CODE 가 추가되거나, 삭제되면 이를 사용하고 있는 로직은 모두 수정해야합니다. 


예제의 경우에는 saveData 한 가지를 언급했지만, 저런 형태의 분기문이 많거나 혹은 아래처럼 CODE 간의 상하관계 표현 등의 행위가 많다면 수정이 쉽지만은 않을 것입니다. (빨리 퇴근해야죠. ㅡㅡ^)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
  * 하위코드 목록을 출력한다.
  *
  * <pre>
  *     잘못된 입력에 대하여 에러가 나타남을 보장.
  * </pre>
  *
  * @param codeForSample
  * @return
  */
public List<String> getChildDataList(final String codeForSample) {
    switch (codeForSample) {
    case CODE.CODE_FOR_SAMPLE.CODE_A.CODE_FOR_SAMPLE.CODE_A:
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
    case CODE.CODE_FOR_SAMPLE.CODE_B:
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_B_1, CODE.CODE_FOR_SAMPLE.CODE_B_2, CODE.CODE_FOR_SAMPLE.CODE_B_3);
    }
 
    throw new RuntimeException(String.format("[에러] 코드의 입력이 잘못되었음!!!", codeForSample));
}
cs


이러한 비지니스 로직의 CODE 직접의존성을 제거하기 위해, SOLID 의 원칙 중 DIP(의존성 역전 원칙)에 따라 상위개념과 하위개념 모두 추상적인 것에 의존해볼 생각입니다. 


이 문제에서는 비지니스 로직을 작성하는 클래스가 CODE를 사용하고 있다는 느낌이니, 비지니스 로직을 상위개념 CODE는 하위개념인 상황으로 생각해 볼 여지가 있습니다.


또한 생각해볼 것은 비지니스로직은 CODE 가 중요하기보다는 CODE 를 통해 어떤 일을 통한 결과가 중요함을 생각해 볼 수도 있을 것 같습니다.




1. 전략패턴을 통한 리팩토링


직접적인 의존성을 제거하기 위해 추상적인 인터페이스 한 개를 작성할 생각이며, 해당 인터페이스는 일단 예시에서 작성한 saveData와 getChildDataList 를 지원할 생각입니다.


작성한 인터페이스는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * CODE_FOR_SAMPLE 을 위한 전략 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public interface CodeForSampleStrategy {
 
    /**
     * CommonService 의 saveData 메소드를 지원
     */
    void saveData();
 
    /**
     * 본인의 항위 코드를 출력.
     * 
     * @return
     */
    List<String> getChildCodeList();
}
cs


이들의 구현체는 아래정도로 작성해 볼 수 있겠군요. 

편의 상, CODE_A, CODE_A1, CODE_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
47
48
49
50
51
52
53
54
55
56
57
58
/**
 * CODE A 의 구체화 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public class CodeAStrategy implements CodeForSampleStrategy {
    @Override
    public void saveData() {
        System.out.printf("%s 관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_A);
 
        System.out.println("하위 컬럼 업데이트");
        for (String code :  getChildCodeList()) {
            System.out.printf("%s 관련 테이블 업데이트\n", code);
        }
    }
 
    @Override
    public List<String> getChildCodeList() {
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
    }
}
 
/**
 * CODE A_1 의 구체화 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public class CodeA_1Strategy implements CodeForSampleStrategy {
    @Override
    public void saveData() {
        // 하위코드는 saveData 를 지원하지 않음.
    }
 
    @Override
    public List<String> getChildCodeList() {
        // 하위코드는 getChildCodeList 를 지원하지 않음.
    }
}
 
/**
 * CODE B 의 구체화 클래스
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public class CodeBStrategy implements CodeForSampleStrategy {
    @Override
    public void saveData() {
        System.out.printf("%s 관련 테이블 업데이트\n", CODE.CODE_FOR_SAMPLE.CODE_B);
        System.out.println("하위 컬럼 업데이트");
        for (String code :  getChildCodeList()) {
            System.out.printf("%s 관련 테이블 업데이트\n", code);
        }
    }
    @Override
    public List<String> getChildCodeList() {
        return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_B_1, CODE.CODE_FOR_SAMPLE.CODE_B_2, CODE.CODE_FOR_SAMPLE.CODE_B_3);
    }
}
cs


기존 switch 문에 있던 구체행위들을 각 구체화 클래스로 이주하였습니다. 

더이상 비지니스로직이 갓클래스가 아닌 책임을 수행해야 할 각각의 클래스로 적절한 분배가 이루어 진 듯 합니다. (SRP : 단일 책임의 원칙)


이렇게 각 속성에 따라 책임을 지니고 행위를 수행하는 클래스를 지금부터 "전략클래스"라고 정의하겠습니다.


이제 실제 비지니스로직들은 아래와 같이 사용이 가능할 것 같습니다.


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
public class CommonService {
 
    private Map<String, CodeForSampleStrategy> codeForSampleStrategyMap;
 
    private static final class ManagerHolder {
        private static final CommonService unique = new CommonService();
    }
 
    /**
     * CommonService 가 초기화 될 때 해야할 일 정의.
     */
    private CommonService() {
        codeForSampleStrategyMap = new HashMap<>();
        codeForSampleStrategyMap.put(CODE.CODE_FOR_SAMPLE.CODE_A, new CodeAStrategy());
        codeForSampleStrategyMap.put(CODE.CODE_FOR_SAMPLE.CODE_A_1, new CodeA_1Strategy());
        codeForSampleStrategyMap.put(CODE.CODE_FOR_SAMPLE.CODE_B, new CodeBStrategy());
        
    }
 
    public static final CommonService GetInstance() {
        return ManagerHolder.unique;
    }
    
    /**
     * 각 코드에 대한 전략 인스턴스를 출력한다
     * 
     * @param code
     * @return
     */
    public CodeForSampleStrategy getStrategyMapper(String code) {
        if (codeForSampleStrategyMap.containsKey(code)) {
            return codeForSampleStrategyMap.get(code);
        } else {
            throw new RuntimeException("[에러] : 잘못된 코드 요청");
        }
    }
 
    /**
     * Code 종류에 따라서 업데이트를 수행한다.
     *
     * @param codeForSample
     */
    public void saveData(final String codeForSample) {
        getStrategyMapper(codeForSample).saveData();
    }
 
    /**
     * 하위코드 목록을 출력한다.
     *
     * <pre>
     *     잘못된 입력에 대하여 에러가 나타남을 보장.
     * </pre>
     *
     * @param codeForSample
     * @return
     */
    public List<String> getChildDataList(final String codeForSample) {
        return getStrategyMapper(codeForSample).getChildCodeList();
    }
}
cs


현재 구조에서는 더이상 비지니스 로직들은 더이상 CODE 를 직접적으로 의존하지 않고 있습니다. CODE 가 변경된다면, 그에 대한 전략클래스들만 변경하고 Mapper 만 조정해주면 될 것 같아보입니다.


즉 추가,확장에 대해서 비지니스 로직을 건드리지 않아도 되니, 개방-폐쇄원칙(OCP)도 지켜진 듯 보입니다. 상-하위개념도 모두 추상적인 것을 바라보니, 앞써 언급한 DIP 도 지켜졌습니다.


하지만 안타깝게도 현재 상황에서 두 가지 안 좋은 냄새를 느꼈습니다.


- CODE 만큼 클래스가 늘어날 여지가 존재 (디버깅, 관리가 복잡)

- 전략을 쓰고자하는 곳은 모두 Mapper 를 일일이 초기화해야 할 필요 존재


특히, Mapper 문제는 CODE 의 추가,삭제가 될 때마다 해당 전략을 사용하는 클래스 전부 Mapper 과정을 수정해야합니다. 

물론 이에 대한 해결책을 많지만, 이번 포스팅에서는 Enum(열거형)이란 개념을 이용하여 수정해보려 합니다.



2. Enum 을 통한 전략 클래스 관리


Enum 자체는 어떤 개념을 나열하기 위한 형태라고 생각해 볼 수 있습니다. 

어떻게 생각해보면, CODE 의 개념과 같을 수 있습니다. 


Java 에서는 이러한 열거형에 행위(메소드)를 추가할 수 있습니다. 

즉 우리는 위에 작성된 CODE 개념에 행위를 추가할 수 있어 보이며, 이는 각 전략클래스들을 만드는 공수를 덜 수 있어보입니다.


아래는 Enum 을 이용한 각 전략의 정의를 한 형태입니다.


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
/**
 * CODE_FOR_SAMPLE 의 Enum 전략
 * 
 * Created by Doohyun on 2017. 4. 25..
 */
public enum EnumCodeForSampleStrategy {
 
    CODE_A (CODE.CODE_FOR_SAMPLE.CODE_A){
        @Override
        public void saveData() {
            saveDataForRootCode();
        }
 
        @Override
        public List<String> getChildCodeList() {
            return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
        }
    },
 
    CODE_B (CODE.CODE_FOR_SAMPLE.CODE_B){
        @Override
        public void saveData() {
            saveDataForRootCode();
        }
 
        @Override
        public List<String> getChildCodeList() {
            return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_B_1, CODE.CODE_FOR_SAMPLE.CODE_B_2, CODE.CODE_FOR_SAMPLE.CODE_B_3);
        }
    },
 
    /**
     * 기존 아무 일도 하지 않던 CODE A_1 은 아무것도 재정의하지 않음.
     */
    CODE_A_1 (CODE.CODE_FOR_SAMPLE.CODE_A_1);
 
    String code;
 
    EnumCodeForSampleStrategy(String code) {
        this.code = code;
    }
 
    public String getCode() {
        return code;
    }
 
    /**
     * 오직 루트코드만이 해야할 일 정의
     *
     * <pre>
     *     기존의 공통된 루트코드의 기능을 Concrete 메소드로 제작
     * </pre>
     */
    protected final void saveDataForRootCode() {
        System.out.printf("%s 관련 테이블 업데이트\n", getCode());
 
        System.out.println("하위 컬럼 업데이트");
        for (String code :  getChildCodeList()) {
            System.out.printf("%s 관련 테이블 업데이트\n", code);
        }
    }
 
    /**
     * 아무 행위도 하지 않는 Hooker
     *
     * <pre>
     *     선택적으로 기능을 정의할 것!
     * </pre>
     */
    public void saveData(){
    }
 
    /**
     * 아무 행위도 하지 않는 Hooker
     *
     * <pre>
     *     선택적으로 기능을 정의할 것!
     * </pre>
     */
    public List<String> getChildCodeList() {
        return Collections.emptyList();
    }
}
 
cs


Enum 을 통하여 전략 패턴을 했을 경우, 각 클래스로 분산된 행위들을 한 곳에 모을 수 있어보입니다. 즉 CODE 개수만큼 클래스를 만들지 않아도 되며, 열거형에 항목만 추가해주면 됩니다. (첫 번째 문제는 해결.)


Enum 에서는 또한 values() 같은 각 열거항목을 관리하기 위한 메소드가 존재합니다. 이를 이용하여, 위의 Mapping 과정을 아래와 같이 수정해볼 수 있을 것 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CommonService {
 
    private Map<String, EnumCodeForSampleStrategy> codeForSampleStrategyMap;
 
    /**
     * CommonService 가 초기화 될 때 해야할 일 정의.
     *
     */
    private CommonService() {
        codeForSampleStrategyMap = new HashMap<>();
        
        // values 를 활용한 모든 전략 맵핑.
        // 전략이 추가되었다고, 더 이상 이 곳을 수정하지 않아도 됨!!
        for (EnumCodeForSampleStrategy enumStrategy : EnumCodeForSampleStrategy.values()) {
            codeForSampleStrategyMap.put(enumStrategy.getCode(), enumStrategy);
        }
 
    }
}
cs


또한 values 같은 속성의 유틸 메소드 역시 작성이 가능해보입니다. 


아래와 같이 필요에 따라 상위코드만 출력하거나, 하위코드만 출력하는 유틸(util)성 기능을 만들 수 있습니다. 


물론 이 것 역시 Enum 의 속성을 이용하여, 유연하게 개발을 해야겠죠! ㅡㅡ^


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
public enum EnumCodeForSampleStrategy {
 
    CODE_A (CODE.CODE_FOR_SAMPLE.CODE_A){
        @Override
        public void saveData() {
            saveDataForRootCode();
        }
 
        @Override
        public List<String> getChildCodeList() {
            return Arrays.asList(CODE.CODE_FOR_SAMPLE.CODE_A_1, CODE.CODE_FOR_SAMPLE.CODE_A_2);
        }
 
        @Override
        public Boolean getRootCodeYn() {
            return true;
        }
 
        @Override
        public Boolean getChildCodeYn() {
            return false;
        }
    },
    
    /**
     * 기존 아무 일도 하지 않던 CODE A_1 은 아무것도 재정의하지 않음.
     */
    CODE_A_1 (CODE.CODE_FOR_SAMPLE.CODE_A_1) {
        @Override
        public Boolean getRootCodeYn() {
            return false;
        }
 
        @Override
        public Boolean getChildCodeYn() {
            return true;
        }
    };
    
    /**
     * 부모 코드 여부
     * @return
     */
    public abstract Boolean getRootCodeYn();
 
    /**
     * 자식 코드 여부
     * @return
     */
    public abstract Boolean getChildCodeYn();
 
    /**
     * 루트 코드 목록 출력.
     * 
     * @return
     */
    public static List<EnumCodeForSampleStrategy> GetRootCodeList() {
        return Arrays.stream(values()).filter(EnumCodeForSampleStrategy::getRootCodeYn).collect(Collectors.toList());
    }
 
    /**
     * 자식 코드 목록 출력.
     *
     * @return
     */
    public static List<EnumCodeForSampleStrategy> GetChildCodeList() {
        return Arrays.stream(values()).filter(EnumCodeForSampleStrategy::getChildCodeYn).collect(Collectors.toList());
    }
}
 
cs



이로써, CODE 와 비지니스 로직간의 직접적인 의존성을 제거하였고 보다 유연한 코드가 탄생한 듯 보입니다. 이런 방식으로 전략 클래스를 만들어, 그 클래스가 행위를 하도록 위임한 것을 전략패턴이라 칭합니다. 


즉, 어플리케이션에서 변경소지가 있는 부분을 분리하여 캡슐화하고, 위임을 통하여 행위를 지정하는 것을 말합니다. 해당 사례의 경우 CODE 의 개념에 따라 행위를 캡슐화하였고, Mapper 를 이용해 각 전략클래스에게 행위를 위임하였습니다.


첫 번째 패턴 부터 긴 포스팅이 된 것 같습니다. 이해가 안가는 부분이 있다면, 언제든 찾아와서 질문을 해주시면 감사합니다. 


그리고, 여기까지 읽어줬으면, 공감한 번 눌러 주면 감사! ^^ 





반응형
Posted by N'