Servlet 에서, 또는 Switch - Case 등 많은 분기를 하는 방법에 대한 디자인 & 퍼포먼스 관점에서의 구경.
1. if-else ¶
대강 다음의 코드 스타일일것이다.
WhySwitchStatementsAreBadSmell에 걸리지 않을까? 근데.. 그에 대한 반론으로 들어오는것이 '이건 mode 분기이므로 앞에서의 Switch-Statement 에서의 예와는 다른 상황 아니냐. 어차피 분기 이후엔 그냥 해당 부분이 실행되고 끝이다.' 라고 말할것이다. 글쌔. 모르겠다.
한편으로 느껴지는 것으로는, switch 로 분기를 나눌 mode string 과 웹 parameter 와의 중복이 있을 것이라는 점이 보인다. 그리고 하나의 mode 가 늘어날때마다 해당 method 가 늘어나고, mode string 이 늘어나고, if-else 구문이 주욱 길어진다는 점이 있다. 지금은 메소드로 추출을 해놓은 상황이지만, 만일 저 부분이 메소드로 추출이 안되어있다면? 그건 단 한마디 밖에 할말이 없다. (단, 저 논문을 아는 사람에 한해서) GotoStatementConsideredHarmful.
~cpp public class IfElse { public void printPerformance(String[] modeExecute) { long start; long end; start = System.currentTimeMillis(); for (int i = 0; i < ModeChoicePerformanceTest.LOOPING_COUNT; i++) { executeIfElse(modeExecute); } end = System.currentTimeMillis(); System.out.println("if - else elapsed time :" + (end - start) + "ms "); } private void executeIfElse(String[] modeExecute) { for (int i = 0; i < modeExecute.length; i++) { executeWithIfElse(modeExecute[i]); } } public void executeWithIfElse(String mode) { if (mode.equals("One")) { doOne(1); } else if (mode.equals("Two")) { doTwo(1); } else if (mode.equals("Three")) { doThree(1); } else if (mode.equals("Four")) { doFour(1); } else if (mode.equals("Five")) { doFive(1); } else { doDefault(1); } } public void doOne(int i) { i = 10; } public void doTwo(int i) { i = 10; } public void doThree(int i) { i = 10; } public void doFour(int i) { i = 10; } public void doFive(int i) { i = 10; } public void doDefault(int i) { i = 100; } }이 방법은 일단 속도상으론 가장 빠르다. 하지만, 한편으로
WhySwitchStatementsAreBadSmell에 걸리지 않을까? 근데.. 그에 대한 반론으로 들어오는것이 '이건 mode 분기이므로 앞에서의 Switch-Statement 에서의 예와는 다른 상황 아니냐. 어차피 분기 이후엔 그냥 해당 부분이 실행되고 끝이다.' 라고 말할것이다. 글쌔. 모르겠다.
한편으로 느껴지는 것으로는, switch 로 분기를 나눌 mode string 과 웹 parameter 와의 중복이 있을 것이라는 점이 보인다. 그리고 하나의 mode 가 늘어날때마다 해당 method 가 늘어나고, mode string 이 늘어나고, if-else 구문이 주욱 길어진다는 점이 있다. 지금은 메소드로 추출을 해놓은 상황이지만, 만일 저 부분이 메소드로 추출이 안되어있다면? 그건 단 한마디 밖에 할말이 없다. (단, 저 논문을 아는 사람에 한해서) GotoStatementConsideredHarmful.
두번째 - Method reflection ¶
~cpp import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; public class MethodFullReflection { public void printPerformance(String[] modeExecute) throws InvocationTargetException, IllegalAccessException { long start; long end; start = System.currentTimeMillis(); for (int i = 0; i < ModeChoicePerformanceTest.LOOPING_COUNT; i++) { executeReflection(modeExecute); } end = System.currentTimeMillis(); System.out.println("elapsed time :" + (end - start) + "ms "); } private void executeReflection(String[] modeExecute) throws InvocationTargetException, IllegalAccessException { for (int i = 0; i < modeExecute.length; i++) { Method method = null; try { method = this.getClass().getMethod("do" + modeExecute[i], new Class[]{int.class}); method.invoke(this, new Object[]{new Integer(1)}); } catch (NoSuchMethodException e) { this.doDefault(1); } catch (SecurityException e) { e.printStackTrace(); //To change body of catch statement use Options | File Templates. } } } private void doDefault(int i) { i = 100; } public void doOne(int i) { i = 10; } public void doTwo(int i) { i = 10; } public void doThree(int i) { i = 10; } public void doFour(int i) { i = 10; } public void doFive(int i) { i = 10; } }
장점 : MODE 가 추가될때마다 doXXX 식으로 이름을 정해주고 이를 실행하면 된다. 조건 분기 부분의 코드가 증가되지 않고, 해당 Mode 가 추가될때마다 메소드 하나만 추가해주면 된다.
단점 : 자바에서는 Method Reflection & Invoke 가 엄청 느리다.; 속도는 밑의꺼 참조.
단점 : 자바에서는 Method Reflection & Invoke 가 엄청 느리다.; 속도는 밑의꺼 참조.
세번째. 위의 방법을 보완한 방법이다. 바로 일종의 Table Lookup. ¶
~cpp import java.util.HashMap; import java.util.Map; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; /** * User: Administrator Date: 2003. 7. 12. Time: 오전 12:48:38 */ public class MethodTableLookupReflection { public void printPerformance(String[] modeExecute) throws InvocationTargetException, IllegalAccessException { long start; long end; start = System.currentTimeMillis(); initReflectionMap(modeExecute); end = System.currentTimeMillis(); System.out.println("reflection with method initialize table elapsed time :" + (end - start) + "ms "); start = System.currentTimeMillis(); for (int i = 0; i < ModeChoicePerformanceTest.LOOPING_COUNT; i++) { executeReflectionWithMapping(modeExecute); } end = System.currentTimeMillis(); System.out.println("reflection with method elapsed time :" + (end - start) + "ms "); } private static Map methodMap; private void executeReflectionWithMapping(String[] modeExecute) throws InvocationTargetException, IllegalAccessException { for (int i = 0; i < modeExecute.length; i++) { Method method = (Method) methodMap.get(modeExecute[i]); if (method != null) method.invoke(this, new Object[]{new Integer(1)}); else doDefault(1); } } private void initReflectionMap(String[] methodNames) { methodMap = new HashMap(); for (int i = 0; i < methodNames.length; i++) { try { methodMap.put(methodNames[i], this.getClass().getMethod("do" + methodNames[i], new Class[]{int.class})); } catch (NoSuchMethodException e) { } catch (SecurityException e) { } } } private void doDefault(int i) { i = 100; } public void doOne(int i) { i = 10; } public void doTwo(int i) { i = 10; } public void doThree(int i) { i = 10; } public void doFour(int i) { i = 10; } public void doFive(int i) { i = 10; } }
이 방법은 위의 방법과 같은 장점을 지니면서 퍼포먼스를 거의 10배가량 향상시킨다.
네번째. Inner Class 에 대해 Command Pattern 의 사용. ¶
~cpp import java.util.Map; import java.util.HashMap; /** * User: Administrator Date: 2003. 7. 12. Time: 오전 12:57:7 */ public class InterfaceTableLookup { protected Map modeMap = new HashMap(); public void printPerformance(String[] modeExecute) { long start; long end; start = System.currentTimeMillis(); initModeMap(); end = System.currentTimeMillis(); System.out.println("interface table lookup init table elapsed time :" + (end - start) + "ms "); start = System.currentTimeMillis(); for (int i = 0; i < ModeChoicePerformanceTest.LOOPING_COUNT; i++) { executeInnerclassMapping(modeExecute); } end = System.currentTimeMillis(); System.out.println("interface table lookup elapsed time :" + (end - start) + "ms "); } private void executeInnerclassMapping(String[] modeExecute) { for (int i = 0; i < modeExecute.length; i++) { executeMode(modeExecute[i]); } } private void executeMode(String s) { IMode mode = (IMode) modeMap.get(s); if (mode == null) doDefault(1); else mode.execute(1); } public class ExOne implements IMode { public void execute(int i) { i = 10; } } public class ExTwo implements IMode { public void execute(int i) { i = 10; } } public class ExThree implements IMode { public void execute(int i) { i = 10; } } public class ExFour implements IMode { public void execute(int i) { i = 10; } } public class ExFive implements IMode { public void execute(int i) { i = 10; } } private void initModeMap() { modeMap.put("One", new ExOne()); modeMap.put("Two", new ExTwo()); modeMap.put("Three", new ExThree()); modeMap.put("Four", new ExFour()); modeMap.put("Five", new ExFive()); } }
이 방법은 initModeMap 에서 매번 Mode에 대한 등록을 해줘야 한다. 퍼포먼스는 Method Reflection 보다 훨씬 빠르다.
마지막 방법 - interface & reflection ¶
위의 방법에 initModeMap 을 reflection 으로 처리한 것이다.
~cpp import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; /** * User: Administrator Date: 2003. 7. 12. Time: 오전 1:2:16 */ public class InterfaceTableLookupReflection { public void printPerformance(String[] modeExecute) throws NoSuchMethodException, InstantiationException, ClassNotFoundException, InvocationTargetException, IllegalAccessException { long start; long end; start = System.currentTimeMillis(); initModeMapWithReflection(modeExecute); end = System.currentTimeMillis(); System.out.println("interface reflection & table lookup init able elapsed time :" + (end - start) + "ms "); start = System.currentTimeMillis(); for (int i = 0; i < ModeChoicePerformanceTest.LOOPING_COUNT; i++) { executeInnerclassMapping(modeExecute); } end = System.currentTimeMillis(); System.out.println("interface reflection & table lookup elapsed time :" + (end - start) + "ms "); } private void executeInnerclassMapping(String[] modeExecute) { for (int i = 0; i < modeExecute.length; i++) { executeMode(modeExecute[i]); } } private void executeMode(String s) { IMode mode = (IMode) modeMap.get(s); if (mode == null) doDefault(1); else mode.execute(1); } protected Map modeMap = new HashMap(); public final static String modeClassHeader = "Ex"; String expectedClassNameHeader = this.getClass().getName() + "$" + modeClassHeader; private void initModeMapWithReflection(String[] modeExecute) throws InstantiationException, InvocationTargetException, IllegalAccessException, NoSuchMethodException { Class[] consParamClasses = new Class[]{this.getClass()}; Object[] consParams = new Object[]{this}; Class[] inners = this.getClass().getClasses(); for (int i=0;i<inners.length;i++) { if (inners[i].getName().startsWith(expectedClassNameHeader)) { Constructor innerCons = inners[i].getDeclaredConstructor(consParamClasses); modeMap.put(modeExecute[i], innerCons.newInstance(consParams)); } } } public void doDefault(int i) { i = 100; } public class ExOne implements IMode { public void execute(int i) { i = 10; } } public class ExTwo implements IMode { public void execute(int i) { i = 10; } } public class ExThree implements IMode { public void execute(int i) { i = 10; } } public class ExFour implements IMode { public void execute(int i) { i = 10; } } public class ExFive implements IMode { public void execute(int i) { i = 10; } } }이 방법은 Mode 추가시 그냥 ExModeName 식으로 추가해주면 된다. 그러면서 Mode 조건 분기 부분이 변하지 않는다. Reflection으로 table lookup 채우는 부분이나 Mode 조건 분기 부분을 아에 상위 클래스로 추출할 수 있다. 퍼포먼스면에서는 의외로 앞에서 수동으로 map 을 채우는 방법과 같다. 유연성과 퍼포먼스 두가지가 적절히 어울어지는 방법이다.
이건 위의 테스트들을 한번에 실행시키기 위한 runner class.
~cpp import java.lang.reflect.InvocationTargetException; /* 평가하려는 조건들 : 1. 해당 method 의 naming 으로 reflection call. 2. switch - case 로 mode 구분. 3. interface & class - command pattern 의 구현. 평가요소들 1. 해당 코드 작성대비 늘어나는 반복 작업들 2. performance 3. maintance */ public class ModeChoicePerformanceTest { public static final int LOOPING_COUNT = 1000000; public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, InstantiationException { String[] modeExecute = new String[]{"One", "Two", "Three", "Four", "Five", "hugu"}; new IfElse().printPerformance(modeExecute); new MethodFullReflection().printPerformance(modeExecute); new MethodTableLookupReflection().printPerformance(modeExecute); new InterfaceTableLookup().printPerformance(modeExecute); new InterfaceTableLookupReflection().printPerformance(modeExecute); } }
결과 ¶
~cpp if - else elapsed time :611ms elapsed time :61889ms reflection with method initialize table elapsed time :0ms reflection with method elapsed time :6459ms interface table lookup init table elapsed time :10ms interface table lookup elapsed time :741ms interface reflection & table lookup init able elapsed time :10ms interface reflection & table lookup elapsed time :731ms