[CHAPTER 2] OOP 개념의 재검토 (with SOLID) + 과제 리뷰
지난 주에는 객체지향의 개념을 재검토 및 SOLID 개념을 한 번 맛보는 자리였었습니다. 또한 배운 주제를 통하여 과제를 진행했었고, 예상대로 멋진고 인텔리한 우리 스터디 멤버들은 문제를 잘 해결해주었습니다.
지난 주차에 했던 내용에 대한 포스팅은 아래 링크에서 확인!
2017/04/16 - [일상/[STUDY] OOP & FP 연구] - [CHAPTER 2] OOP 개념의 재검토 (with SOLID)
2017/04/18 - [일상/[STUDY] OOP & FP 연구] - [CHAPTER 2] OOP 개념의 재검토 (with SOLID) + 추가내용
2017/04/21 - [일상/[STUDY] OOP & FP 연구] - [CHAPTER 2] OOP 개념의 재검토 (with SOLID) + 과제
해당 글에서는 과제에 대한 리뷰를 진행해 보고자 합니다.
1. 기존 구현된 Task 클래스에 goMBM() 이라는 메소드를 추가.
스터디에서 진행했던, Task 에 요구사항 추가가 있었습니다.
우리 회사에 다니는 모든 직군은 MBM 이라는 행사에 참여할 가능성이 있지만, 보통은 마케팅 팀에서 대부분 진행을 하는 것으로 압니다.
(맞나요? ㅡㅡ^)
무튼, 아래와 같이 코드를 작성하려 하였습니다.
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 | /** * 업무 처리 * * Created by Doohyun on 2017. 4. 17.. */ public abstract class Task { /** * 일을 한다는 약속 * * <pre> * 이 메소드를 실행시키면 일을 하는 것. * </pre> */ public abstract void runTask(); /** * MBM 을 수행하는 메소드 * * <pre> * 구체적인 것이 추상적인 상태로 올라옴. * </pre> */ public void goMBM() { if (this instanceof Marketing) { // 마켓팅 main. } else if (this instanceof Devlopment) { // 일단 여지는 있어보임. } } /** * 면접관을 담당하는 메소드 * * <pre> * 면접관을 수행하는 메소드도 추가되길 바람. * 물론 이 업무 역시, 특정 직군 (마켓팅,개발) 만 할 수도 있음. * </pre> * */ public void goInterview() { } } | cs |
구현된 goMBM 을 보니, 좋지 않은 냄새가 납니다.
일단 추상적인 것이 구체적인 것에 의존하면 안된다는 DIP (의존성 역전 원칙) 을 지키지 않고 있으며, 만약 해당 소스를 사용하는 어플리케이션에서 Task 의 구현체를 변경한다면, Task 자체 소스도 변경해 줘야하기 때문에 OCP (개발-폐쇄 원칙)도 지키지 않는 것으로 보입니다.
[Marketing 이 삭제가 된다면 Task 내부 메소드를 수정해야 하며, 새로운 작업을 MBM 으로 추가하고 싶다면 또 다른 구현체에 의존해야 합니다.]
이 소스의 리팩토링 여지는 있으며, 어떻게 생각해보면 간단하게 해결될 수 도 있습니다. 위 소스의 가장 큰 문제는 추상적인 것이 구체적인 것에 의존한고 있다는 것이며, 그 고리만 끊어주면 파생되는 문제는 해결될 것으로 보입니다.
일단 아래와 같이 Task 클래스 내부의 goMBM() 은 아무 일도 하지 않게 하려 합니다. Task 라는 추상개념으로 goMBM() 을 실행하는 것이 목적이기 때문에 invoker 는 살려두려 합니다.
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 | /** * 업무 처리 * * Created by Doohyun on 2017. 4. 17.. */ public abstract class Task { /** * 일을 한다는 약속 * * <pre> * 이 메소드를 실행시키면 일을 하는 것. * </pre> */ public abstract void runTask(); /** * MBM 을 수행하는 메소드 * * <pre> * 추상적인 곳에서 추상클래스가 아닌 아무 일도 하지 않는 메소드를 hooker 라고 합니다. * </pre> */ public void goMBM() { } /** * 면접관을 담당하는 메소드 * * <pre> * 추상적인 곳에서 추상클래스가 아닌 아무 일도 하지 않는 메소드를 hooker 라고 합니다. * </pre> * */ public void goInterview() { } } | cs |
즉 필수적인 runTask() 만이 추상의 구현을 하위 개념에게 강제하였고, 나머지 메소드 (goMBM(), goInterview()) 는 hooker 로써, 구현의 여부를 선택하게 하였습니다.
goMBM() 메소드는 현재 요구사항으로 보았을 때, 일단 Marketing 에 구현될 필요가 있어 보입니다. goMBM() 에서 Marketing 이 해야할 일을 구체개념으로 빼도록 하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * 마케팅 업무 * Created by Doohyun on 2017. 4. 17.. */ public class Marketing extends Task{ @Override public void runTask() { System.out.println("마케팅 일을 하는 중..."); } @Override public void goMBM() { System.out.println("마케팅은 MBM 을 떠난다."); } } | cs |
주목할 점은 goInterview() 를 오버라이딩 하지 않고 있으며, 즉 선택적으로 행위를 추가할 수 있음을 알 수 있습니다.
2. 가위바위보의 승패를 출력하는 메소드 제작
두 상태에 대한 승패여부를 if 나 자료구조 없이, 나타내는 방법을 생각해보라는 주제였습니다. 아래 소스는 초기에 주어진 요구사항으로, 각 케이스에 대한 모든 분기가 되어있음을 알 수 있습니다.
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 | /** * 심플한 가위,바위,보 결과 * * @param me * @param enemy * @return */ public static String 가위바위보_결과 (final String me, final String enemy) { if (me.equals("가위")) { switch (enemy) { case "가위": return "무승부"; case "바위": return "패"; case "보": return "승"; } } else if (me.equals("바위")) { switch (enemy) { case "가위": return "승"; case "바위": return "무승부"; case "보": return "패"; } } else if (me.equals("보")) { switch (enemy) { case "가위": return "패"; case "바위": return "승"; case "보": return "무승부"; } } throw new RuntimeException("입력 오류"); } | cs |
참여자 전원 모두 과제를 잘 해왔으며, 제출자의 코드를 기반으로 포스팅을 하고자합니다.
리팩토링을 생각해볼 부분은 분기처리이며, 각 분기처리에 대해서 책임소지가 있는 클래스로 기능을 이관(SRP)하는 작업이 필요할 것 같습니다.
책임 분배를 위해 아래와 같은 구체화 클래스를 만들려고 합니다.
서로의 매치를 if 없이 관계를 주기 위하여, 오버로딩 개념을 사용하였습니다.
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 | /** * 클래스 간의 서로의 사용개념을 처리하기 위한 오버로딩 인터페이스 */ public interface StrategyVisitor { String visit(가위 가위); String visit(바위 가위); String visit(보 가위); } // 구체화된 가위 클래스 public class 가위 implements StrategyVisitor{ @Override public String visit(가위 enemy) { return "무승부"; } @Override public String visit(바위 enemy) { return "패"; } @Override public String visit(보 enemy) { return "승"; } @Override public String toString() { return "가위"; } } // 구체화된 바위 클래스 public class 바위 implements StrategyVisitor { @Override public String visit(가위 enemy) { return "승"; } @Override public String visit(바위 enemy) { return "무승부"; } @Override public String visit(보 enemy) { return "패"; } @Override public String toString() { return "바위"; } } // 구체화된 보 클래스 public class 보 implements StrategyVisitor { @Override public String visit(가위 enemy) { return "패"; } @Override public String visit(바위 enemy) { return "승"; } @Override public String visit(보 enemy) { return "무승부"; } @Override public String toString() { return "보"; } } | cs |
클라이언트 코드에서는 아래와 같이 제출을 하였었습니다. 자료구조에 데이터를 넣고, 모든 상황에 대한 시뮬레이션을 수행하는 코드입니다.
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 | List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보()); // 가위에 대한 응용처리 { 가위 a = new 가위(); System.out.println("============================"); System.out.println("게입 시작합니다! \n"); for (StrategyVisitor game1 : me) { System.out.println("me:" + game1.getClass().getSimpleName() + " enemy:" + a.getClass().getSimpleName()); System.out.println("result:" + game1.visit(a) + "\n"); } } // 바위에 대한 응용처리 { 바위 b = new 바위(); System.out.println("============================"); System.out.println("게입 시작합니다! \n"); for (StrategyVisitor game1 : me) { System.out.println("me:" + game1.getClass().getSimpleName() + " enemy:" + b.getClass().getSimpleName()); System.out.println("result:" + game1.visit(b) + "\n"); } } // 보에 대한 응용처리 { 보 c = new 보(); System.out.println("============================"); System.out.println("게입 시작합니다! \n"); for (StrategyVisitor game1 : me) { System.out.println("me:" + game1.getClass().getSimpleName() + " enemy:" + c.getClass().getSimpleName()); System.out.println("result:" + game1.visit(c) + "\n"); } } | cs |
요구사항처럼 if 없이 관계는 처리가 되었습니다.
하지만 제출자의 입장에서는 위와 같이 세 가지 케이스에 대하여 모든 경우를 비지니스 로직으로 만들고 싶지는 않았고, 자료구조를 두 개 선언하여 2중 for 문으로 해결하고 싶었다고 합니다.
하지만 아래와 같은 사유로 실패했습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보()); List<StrategyVisitor> enemy = Arrays.asList(new 가위(), new 바위(), new 보()); for (StrategyVisitor a : me) { System.out.println("============================"); System.out.println("게입 시작합니다! \n"); for (StrategyVisitor b : enemy) { System.out.println("me:" + a.getClass().getSimpleName() + " enemy:" + b.getClass().getSimpleName()); // 오버로딩 전략으로 가려고 했지만, enemy 리스는 추상적인 StrategyVisitor!!!! System.out.println("result:" + a.visit(b) + "\n"); } } | cs |
두 자료구조의 for-loop 을 방문하면서, 로직을 처리하기 위하여 고전적인 방문자 패턴을 응용해보려 합니다.
3. 방문자패턴을 이용한 3차 리팩토링
오버로드를 수행하는 StrategyVisitor 를 일종의 방문자라 생각한다면, 방문할 Element 만 구현해주면 그만입니다.
방문할 Element 객체를 아래와 같이 정의합니다.
1 2 3 4 5 6 7 8 | /** * 방문 대상 Element * * Created by Doohyun on 2017. 4. 27.. */ public interface Strategy { String accept(StrategyVisitor strategyVisitor); } | cs |
방문대상인 Strategy 는 기존 StrategyVisitor 를 매개변수로 받는 메소드를 정의했습니다. 즉 방문자가 해당 Element 를 방문했을 때, 해야할 일을 구현체에 작성해주면 됩니다.
이를위해 저는 기존 만들어진 가위,바위,보를 재활용하고자 합니다. 아래와 같이 말이죠.
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 | /** * Element 와 Visitor 를 한 곳에 구현. * 구체화된 가위 클래스 */ public class 가위 implements StrategyVisitor, Strategy{ @Override public String visit(가위 enemy) { return "무승부"; } @Override public String visit(바위 enemy) { return "패"; } @Override public String visit(보 enemy) { return "승"; } // 방문자가 본인을 다녀갔을 때의 처리를 수행합니다. @Override public String accept(StrategyVisitor strategyVisitor) { return strategyVisitor.visit(this); } @Override public String toString() { return "가위"; } } // 구체화된 바위 클래스 public class 바위 implements StrategyVisitor, Strategy { @Override public String visit(가위 enemy) { return "승"; } @Override public String visit(바위 enemy) { return "무승부"; } @Override public String visit(보 enemy) { return "패"; } @Override public String accept(StrategyVisitor strategyVisitor) { return strategyVisitor.visit(this); } @Override public String toString() { return "바위"; } } // 구체화된 보 클래스 public class 보 implements StrategyVisitor, Strategy { @Override public String visit(가위 enemy) { return "패"; } @Override public String visit(바위 enemy) { return "승"; } @Override public String visit(보 enemy) { return "무승부"; } @Override public String accept(StrategyVisitor strategyVisitor) { return strategyVisitor.visit(this); } @Override public String toString() { return "보"; } } | cs |
accept 메소드를 모두 구현했으니 아래와 같이 클라이언트 코드를 작성해볼 수 있을 것 같습니다. 앞써, 제작된 코드와는 달리 다형화의 선택조건을 구현체 내부에서 구현하고 있기 때문에 아래와 같은 구현이 가능합니다.
1 2 3 4 5 6 7 8 9 10 11 12 | List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보()); List<Strategy> enemy = Arrays.asList(new 가위(), new 바위(), new 보()); for (StrategyVisitor visitor : me) { System.out.println("============================"); System.out.println("게입 시작합니다! \n"); for(Strategy strategy : enemy){ System.out.println(String.format("me : %s, enemy: %s", visitor, strategy)); System.out.println(String.format("result : %s\n", strategy.accept(visitor))); } } | cs |
나름 신경을 써 볼 만한 문제 였던 것 같습니다. 다들 한 번이라도 신경을 써서 문제를 해결해줘서 고맙고, 다시 이 글을 봐주셔서 감사합니다. [꾸벅]
다음 주, Study 도 파이팅!!!
'스터디 > [STUDY] OOP' 카테고리의 다른 글
[CHAPTER 4] 실무 활용 패턴 (중) (0) | 2017.05.08 |
---|---|
[CHAPTER 3] 실무 활용 패턴 (상)[Strategy 패턴] + 추가내용 (4) | 2017.04.27 |
[CHAPTER 3] 실무 활용 패턴 (상) (0) | 2017.04.25 |
[CHAPTER 2] OOP 개념의 재검토 (with SOLID) + 과제 (0) | 2017.04.21 |
[CHAPTER 2] OOP 개념의 재검토 (with SOLID) + 추가내용 (0) | 2017.04.18 |