Servlet 에서, 또는 Switch - Case 등 많은 분기를 하는 방법에 대한 디자인 & 퍼포먼스 관점에서의 구경.
1. if-else ¶
대강 다음의 코드 스타일일것이다.
~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 가 엄청 느리다.; 속도는 밑의꺼 참조.
세번째. 위의 방법을 보완한 방법이다. 바로 일종의 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 추가시 그냥 Ex
ModeName 식으로 추가해주면 된다. 그러면서 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