[CHAPTER 19] Visitor Pattern - Review
안녕하세요!
필명이 "성숙한 개발을 지향하는" 이라는 타이틀이 붙은 Ndroid 입니다. @.@
바로 패턴에 대한 리뷰를 하지 않고, 인사글을 쓰는 이유는 바로 아래 사진 때문입니다.
네, 맞습니다. 이번 글이 100회 기념 포스팅 이기 때문이죠. :-)
하지만, 기념 포스팅 이라고 특별하진 않습니다.
지인 기준으로 이름과 댓글을 작성해주면, 커피를 사주는 정도까지만... ^^;
(101 번째 게시글이 작성 되었으니, 이벤트는 종료. @.@)
또한, 100회이기 때문에 조금 더 정성들여 작성하고자 합니다.
(그렇다고, 여태까지 쓴 글에 정성이 없는 것은 아닙니다. ㅡㅡ^)
이제, 본론으로...
이번 포스팅의 주제는 Too much complexity(매우 복잡)한 클래스를 리팩토링할 때 사용하기 괜찮은 방문자(Visitor) 패턴에 대해 알아보려 합니다.
방문자 패턴은 복잡한 데이터를 유지하는 클래스에 다양한 연산과정이 추가되어 클래스가 복잡해지는 것을 방지하는 패턴으로, 일련의 알고리즘과 객체 구조를 분리하는 것을 목표로 합니다.
방문자라는 이름이 붙은 이유는 [알고리즘을 구현한 객체]가 [복잡한 데이터들]을 방문하며 처리하도록 모델링 되었기 때문입니다.
사실, 이 패턴은 [OOP 스터디] 첫 장에서 다뤄봤습니다.
한 번 과제로 진행했던 내용을 다시 한번 리마인드 하며, 아래 내용들을 보면 더 좋을 것 같습니다.
이번 리뷰에서는 대표적으로 알려진 방문자 패턴의 예제와 제가 조금 변형한 N식 예제를 작성해보려 합니다.
1. 복잡한 모델과 행위들, 결국은 복잡한 클래스로...
클래스의 존재 목적은 데이터와 데이터를 이용한 행위의 결합이라 할 수 있습니다.
그렇기 때문에, 이제까지 진행했던 OOP 의 많은 포스팅에서는 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 | /** * 계정 클래스 정의 * * Created by ndh1002 on 2017. 9. 10.. */ public class Account { // 계정정보 private String email; private String password; // 회원 기본정보. private String name; private Date birthDay; private String genderFlag; // 회원 연락처. private String mobile; private String mobileCompany; private String address; /** * 현재 입력된 계정정보의 유효여부를 출력. * * <pre> * Account 의 모든 멤버변수의 유효여부를 검사. * </pre> * * @return */ public Boolean checkValid() { // 유효여부 결과. // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행. Boolean validYn = true; // 이메일 유효성 검사. { } // 비밀번호 유효성 검사. { } // 이름 유효성 검사. { } // 생년월일 유효성 검사. { } // 이하 모든 정보 유효성 검사. return validYn; } // SETTER, GETTER 는 생략. } | cs |
Account 클래스는 계정에 대한 정보를 가진 클래스로, Account::checkValid 라는 메소드를 가지고 있습니다.
(아마도, 회원가입과 같은 비지니스 문제에서 사용하던 클래스로 보입니다. @.@)
Account::checkValid 에서는 순차적으로 Account 클래스의 멤버변수의 유효여부를 체크하고 있습니다.
해당 메소드가 Account 클래스의 모든 멤버변수를 검사하는 로직이라 가정할 때, 이 메소드의 복잡도는 Account 가 소유하는 데이터만큼의 책임을 가지며 그 책임만큼 수정, 변화가 일어날 것입니다.
뭐, 좋습니다. 잘 작동만 하면, 일단 넘어갈 수 있는 문제입니다.
그러나 언제나 요구사항은 계속 추가되는 법! Account 에 대하여, 아래와 같은 추가 기능(checkValidType2) 이 요청되었습니다.
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 | /** * 계정 클래스 정의 * * Created by ndh1002 on 2017. 9. 10.. */ public class Account { /** * 현재 입력된 계정정보의 유효여부를 출력. * * <pre> * Account 의 모든 멤버변수의 유효여부를 검사. * </pre> * * @return */ public Boolean checkValid() { ... } /** * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드. * * <pre> * 각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일. * </pre> * * @return */ public Boolean checkValidType2() { // 유효여부 결과. // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행. Boolean validYn = true; // 이름 유효성 검사. { } // 생년월일 유효성 검사. { } // 연락처 정보 유효성 검사. { } return validYn; } // SETTER, GETTER 는 생략. } | cs |
새로운 요구사항은 부분적으로 Account 의 정보를 유효성 검사하는 로직입니다.
기본적으로 Account::checkValid 의 알고리즘을 이용한다고 하였기 때문에, Account::checkValid 에서 특정로직을 메소드 추출 해야할 것 같습니다.
메소드 추출 리팩토링 후, 클래스의 상태는 아래와 같습니다.
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 | public class Account { /** * 현재 입력된 계정정보의 유효여부를 출력. * * <pre> * Account 의 모든 멤버변수의 유효여부를 검사. * </pre> * * @return */ public Boolean checkValid() { // 유효여부 결과. // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행. Boolean validYn = true; // 기타 검사 생략 // 이름 유효성 검사. checkValidByName(); // 생년월일 유효성 검사. checkValidByBirthDay(); // 연락처 정보 유효성 검사. { // 휴대전화번호 검사. checkValidByMobile(); // 통신사정보 유효성 검사. checkValidByMobileCompany(); // 주소 유효성 검사. checkValidByAddress(); } // 이후 검사 생략. } /** * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드. * * <pre> * 각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일. * </pre> * * @return */ public Boolean checkValidType2() { // 유효여부 결과. // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행. Boolean validYn = true; // 이름 유효성 검사. checkValidByName(); // 생년월일 유효성 검사. checkValidByBirthDay(); // 연락처 정보 유효성 검사. { // 휴대전화번호 검사. checkValidByMobile(); // 통신사정보 유효성 검사. checkValidByMobileCompany(); // 주소 유효성 검사. checkValidByAddress(); } return validYn; } /** * 이름 유효성 검사 처리. * * @return */ public Boolean checkValidByName() { } /** * 생년월일 유효성 검사 처리. * * @return */ public Boolean checkValidByBirthDay() { } // SETTER, GETTER 는 생략. } | cs |
Account::checkValidByName, Account::checkValidByBirthDay 등 Account 클래스 내부 멤버 변수 한 개 단위로 유효성 검사하는 메소드를 제공하며, 기존 Account::checkValid, Account::checkValidType2 등이 해당 메소드를 사용하도록 하고 있습니다.
많은 OOP 관련 내용에서 하라는 것과 같이 너무 많은 책임을 가진 메소드에 대하여 책임을 분리하였고, 그에 따라 특정 로직 수정에 있어서도 유지보수에 좋을 것이라 생각합니다.
그러나 한 단위로 유효성 검사하는 메소드를 모두 제작해버리면, 클래스의 멤버변수만큼 메소드가 늘어날 것입니다.
그래요. 그것까지는 OK!
그러나, 멤버변수들을 가지고 유효성 검사를 하는 것이 아닌 기타 다른 행위1, 행위2 등을 작성해야 한다면 어떻게 하죠?
결국 이 룰에 따라 한번 Account 클래스의 메소드 개수를 어림짐작해보죠..
최소 다음과 같습니다.
Account 클래스의 멤버변수 X Account 가 제공해야하는 기능 = Account 가 가질 최소 메소드 개수.
무언가 잘못되었습니다.. ㅜㅡㅜ
2. Single Dispatch vs Double Dispatch (방문자를 이용한 리팩토링)
앞서, 살펴본 예제에서 딱히 잘못된 것을 느끼지는 못하겠습니다.
사실 잘못된 것이 아닐 수도 있습니다. 심리적으로 많은 메소드를 가지는 것이 불편한 것이죠.... @.@
한번 알고있는 OOP rule 에 따라 고려해봐도, SRP 및 기타 리팩토링 지식을 철저하게 지킨 것 밖에 없습니다.
원칙에 따라 해본 것은 아래정도 될 것 같습니다.
- 클래스가 멤버변수를 가지고 있으니, 멤버변수를 이용한 행위는 해당 클래스에 작성합니다.
- 특정 메소드가 너무 많은 책임을 가지고 있으니, 메소드 추출을 하였습니다.
- 중복된 로직을 캡슐화하여, 재활용하고 있습니다.
그러나, 결과적으로는 많은 메소드들을 가진 복잡하다고 생각할 수 있는 클래스를 제작하게 되었습니다.
이와 같이 한 클래스에 모든 책임을 구현하는 방법을 Single Dispatch 라고 하며, 대부분 로직은 이와 같이 구현이 됩니다.
여기에서 Dispatch 란, 메소드를 호출하기 위해 하는 일련의 과정을 말합니다.
조금 더 이 개념을 살펴보면, 컴파일 시점부터 어떤 메소드가 호출될지 정해진 정적 디스패치(Static Dispatch)와 인터페이스의 참조에 따라 어떤 메소드가 호출될지 정해지는 동적 디스패치(Dynamic Dispatch)가 존재합니다.
한 클래스에서 모든 책임을 구현하게 된다면, 보통은 정적 혹은 동적 디스패치가 단일적으로 일어나게 될 것입니다.
(Dispatch 가 한번만 일어나기 때문에, Single Dispatch 라 부릅니다. @.@)
갑자기 Single Dispatch 를 언급한 것은 Double Dispatch 역시 존재하기 때문입니다.
Double Dispatch 는 메소드를 호출하기 위한 행위가 두번 일어나는 것을 말하며, 오늘 포스팅에서는 Double Dispatch 를 이용하는 방문자 패턴으로 복잡한 클래스를 리팩토링해볼 것입니다.
리팩토링의 목표는 모델의 데이터 구조에 따라 제공해야하는 기능 중 한 가지를 모두 구현하는 클래스를 제작할 것이며, 이를 묶어주는 인터페이스 한 개를 만들 것입니다.
일단, 첫 번째로 만들어볼 것은 데이터 구조에 따라 기능을 구현한 클래스입니다.
여기에서 기능을 구현하는 클래스를 방문자(Visitor)로 칭하겠습니다.
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 | /** * 유효성 체크 방문자 정의. * * Created by ndh1002 on 2017. 9. 11.. */ public class CheckVisitor { /** * 이름에 대한 유효성 검사를 수행하는 메소드. * * @param name */ public void check(Name name) { System.out.println("이름에 대한 유효성 검사."); } /** * 생년월일에 대한 유효성 검사를 수행하는 메소드. * * @param birthDay */ public void check(BirthDay birthDay) { System.out.println("생일에 대한 유효성 검사."); } // 기타 다른 모델들은 생략... } | cs |
오버로딩을 통해, 각 데이터 모델에 따라 유효성 검사를 수행하는 클래스입니다.
즉, 이곳에서는 기존 Account 에 있어야 했던 데이터 단위의 유효성 검사로직이 존재합니다.
이제 이를 묶어줄 인터페이스 한 가지를 제작할 생각입니다.
해당 인터페이스는 방금 작성한 CheckVisitor 를 파라미터로 받으며, 그에 따른 각 구현 클래스들은 자기자신을 넘김으로 CheckVisitor 의 어떤 check 메소드가 실행할지 결정하도록 할 것입니다.
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 | /** * 유효성 검사 인터페이스. * * Created by Doohyun on 2017. 9. 12.. */ public interface ICheckAble { void check(CheckVisitor visitor); } /** * 이름을 저장하는 클래스. * * Created by Doohyun on 2017. 9. 17.. */ public class Name implements ICheckAble { @Override public void check(CheckVisitor visitor) { visitor.check(this); } } /** * 생년월일에 대한 모델. * * Created by Doohyun on 2017. 9. 17.. */ public class BirthDay implements ICheckAble { @Override public void check(CheckVisitor visitor) { visitor.check(this); } } | cs |
이를 이용해, 새롭게 구현된 Account::checkValidType2 는 아래와 같습니다.
기타 다른 모델 클래스들은 살짝 생략했습니다. @.@
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 | public class Account { private Name name = new Name(); private BirthDay birthDay = new BirthDay(); private Mobile mobile = new Mobile(); private MobileCompany mobileCompany = new MobileCompany; /** * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드. * * <pre> * 각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일. * </pre> * * @return */ public Boolean checkValidType2() { CheckVisitor checkVisitor = new CheckVisitor(); // 유효성 검사를 수행할 객체를 방문자가 순회하며, 알고리즘 수행. for (ICheckAble checkAble : Arrays.asList(name, birthDay, mobileCompany, mobile)) { checkAble.check(checkVisitor); } return false; } // SETTER, GETTER 는 생략. } | cs |
사용 로직을 살펴보면, 원하는 모델에 따라 목록을 만들어 유효성 검사를 체크하는 방문자를 사용하도록 하고 있습니다.
Account 를 복잡하게 만들었던 알고리즘들을 특정 클래스로 분리해냈고, 추 후 Account 에 또 다른 기능이 추가된다면 방문자 클래스를 만드는 것으로 더 복잡하지 않게 만들 수 있을 것 같습니다.
즉, Account 에는 데이터들과 방문자를 사용하는 메소드만 남게 될 것입니다.
이와 같은 방법을 앞써 언급한 Double Dispatch 와 연관지어 보지 않을 수가 없을 것 같습니다. @.@
로직에서는 ICheckAble::check 를 사용하기 위해 다형성에 의한 동적 디스패치가 한 번 일어났고, 실제 CheckVisitor::check 에서 오버로드된 메소드들 중 어떤 메소드를 사용할 것인지 결정하는 동적 디스패치가 두 번째로 일어납니다.
결과론적으로 복잡한 모델에 대하여, 데이터 구조와 알고리즘을 분리해냄으로 클래스가 더 복잡해지는 것을 방지한 듯 합니다.
3. 제안하는 새로운 방문자
Double Dispatch 를 이용한 방문자 패턴을 통해 클래스를 복잡하게 만드는 것을 방지한 것은 괜찮은 아이디어인 듯 합니다.
하지만, 위에 제시된 방법은 단순하게 원시타입으로 가지고 있어도 되는 필드를 방문자를 사용해야하는 이유로 객체화시키는 것은 매우 불편해 보입니다.
저는 이 점에 착안하여, 새로운 발상을 하게 되었습니다.
기존의 방문자 패턴은 모델이 알고리즘 방문자를 사용하는 방식이라 한다면, 반대로 알고리즘이 모델을 사용하는 방식으로 변경하면 어떨지에 대해 고민했습니다.
알고리즘이 모델로부터 알아서 사용할 데이터를 PULL 방식으로 가져와 수행하는 것이죠..
이를테면, 위의 예제를 아래와 같은 열거형 처럼 변경할 수 있을 듯 합니다.
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 | /** * 계정의 유효성 정보 체커 * * <pre> * 전략패턴과 유사한 형태의 알고리즘 구현체. * </pre> * * Created by ndh1002 on 2017. 9. 17.. */ public enum AccountChecker { NAME { @Override public void check(Account account) { System.out.println("이름에 대한 유효성 검사." + account.getName()); } }, BIRTHDAY { @Override public void check(Account account) { System.out.println("생일에 대한 유효성 검사." + account.getBirthday()); } } // 이하 생략. ; public abstract void check(Account account); } | cs |
기존의 CheckVisitor 처럼 Account 에 존재하던 단일 개체에 대한 유효성 검사 로직을 해당 클래스에 구현하였습니다.
이런 형태의 열거형 사용 형태는 많이 익숙하지 않나요?
네, 이 것은 바로 전략패턴입니다.
이 전략 패턴을 이용하여, 다음과 같이 또 다른 형태의 Account::checkValidType2 를 만들어 볼 수 있을 것 같습니다.
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 | /** * 계정 클래스 정의 * * Created by ndh1002 on 2017. 9. 10.. */ public class Account { // 회원 기본정보. private String name; private String birthDay; // 회원 연락처. private String mobile; private String mobileCompany; private String address; /** * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드. * * <pre> * 각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일. * </pre> * * @return */ public Boolean checkValidType2() { CheckVisitor checkVisitor = new CheckVisitor(); // 유효성 검사를 수행할 객체를 방문자가 순회하며, 알고리즘 수행. for (AccountChecker checker : Arrays.asList( AccountChecker.NAME , AccountChecker.BIRTHDAY , AccountChecker.MOBILE_COMPANY , AccountChecker.MOBILE)) { checker.check(this); } return false; } // SETTER, GETTER 는 생략. } | cs |
이 방식은 Double Dispatch 활용을 위해 단일 개체 필드에 대한 모델을 제작할 필요가 없으며, 방문자 패턴의 목적처럼 데이터 구조와 알고리즘을 분리해낼 수도 있습니다.
또한 새로운 기능이 필요하다면, 또 다른 열거형을 제작하면 해결될 일이죠..
이 방식이 제가 새롭게 제안하는 N식 방문자 패턴입니다. @.@
(특별한 것을 바라셨다면, 죄송합니다. ㅜㅡㅜ)
이번에도 정말 긴 글이 된 것 같습니다. @.@
하지만, 이 패턴은 매우 뜻이 깊습니다.
OOP & FP 의 첫 번째 과제였고, 스터디에서 받은 질문을 해결해주며 엄청 기분이 좋았던 것이 기억나에요. 하하하하하..
(아마, 이 과제에 오므라이스가 걸려있었습니다. 잘 해결주셔서 지금도 감사합니다.^^)
또한, 제가 애용하는 전략패턴과 조합한 세 번째 방식은 최근에 개발한 것이라 조금 더 뜻이 깊습니다.
이 주제로 100회를 마무리할 수 있어서 행운입니다. ^^;
이제, Effective OOP 와 관련된 주제로 한 개의 포스팅을 남겨두고 있습니다. (Hello, MVC.)
한번, 마지막까지 파이팅 해보자구용. @.@
'스터디 > [STUDY] Effective OOP' 카테고리의 다른 글
[CHAPTER 20] Compound Pattern - Review (2) | 2017.11.19 |
---|---|
[CHAPTER 6] Decorator Pattern - Review [과제 리뷰] (0) | 2017.09.07 |
[CHAPTER 18] Composite Pattern - Review (0) | 2017.09.03 |
[CHAPTER 16] Iterator Pattern - Review (0) | 2017.08.02 |
[CHAPTER 13] Builder Pattern - Review (0) | 2017.07.20 |