[CHAPTER 4] 실무 활용 패턴 (중) [Adapter 패턴] + 추가내용
백-엔드에서 가장 많이 해야할 행위 중 하나는 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 만 잘 활용해주더라도 비지니스 로직의 큰 문제를 줄일 수 있다고 생각합니다. 보다 더욱 가독성과 유지보수에 좋은 코드를 작성할 수 있으며, 추 후 스터디할 함수형 메소드에 적극적으로 활용할 수 있는 여지를 만들어 줄 수 있습니다.
이 포스팅이 모두의 기술향상에 도움이 되길 바랍니다. :-)
'스터디 > [STUDY] OOP' 카테고리의 다른 글
[CHAPTER 5] 실무 활용 패턴 (하) [Factory method 패턴] + 추가내용 (2) | 2017.06.04 |
---|---|
[CHAPTER @] Break Time (잠깐 쉬는 후기) (0) | 2017.06.04 |
[CHAPTER 4] 실무 활용 패턴 (중) + 과제리뷰 (0) | 2017.05.20 |
[CHAPTER 5] 실무 활용 패턴 (하) (0) | 2017.05.18 |
[CHAPTER 4] 실무 활용 패턴 (중) + 과제 (0) | 2017.05.14 |