DNA 포럼 API 서비스 모음 DNA Lens

웹어플리케이션 서비스 중에 실시간으로 디버깅하기


  • 김영민(미디어본부 미디어개발팀), 2006년 11월

들어가는 말 #

RDBMS를 사용하거나 socket과 같은 IO가 많은 곳이 성능의 병목을 보이는 경우가 많다. 이런 부분은 데이터를 연산하거나 가져오기 위해 많은 부하가 소요되기 때문에 한번 가져오거나 연산이 완료된 데이터를 메모리에 올려 놓고 재사용할려는 노력을 하게된다. 또한 이런 부분은 조회수를 빈번히 업데이트하거나 한꺼번에 많은 데이터 변환이 필요할 때도 많은 시간이 요구되지는 경우가 다반사이다. 이때는 buffer나 queue 전략을 고려한다. 즉, 해당 이벤트가 발생할 때마다 위의 해당 로직을 호출하기 보다는 일단 메모리에 기록하고 특정시점에 모아서 한번에 수행하는 방법을 택한다. 또한 요즘은 다중 사용자 환경을 고려치 않을 수없다. 그래서 단일 스레드 구조 보다는 멀티스레드 구조를 기본적으로 고려하게 된다. 멀티스레드를 고려하게되면 항상 문제되는 임계영역에 대한 고민은 필수가 된다.

정말 다 좋은 방법들이다. 어플리케이션의 로직은 복잡해지고, CPU나 메모리칩같은 하드웨어는 점점 싸지니 메모리를 cache나 buffer, 멀티스레드를 사용하는 일이 차츰 많이질 수 밖에 없다. 하지만 아무리 좋은 방법도 남용하면 나쁜 법이다.

그런데 위의 방법을 쓰게 되면 골칫거리가 하나 생기게 되니, 바로 디버깅이다. 우리가 눈으로 손으로 짜는 프로그램의 소스는 어디까지나 정적이다. 하지만, 프로그램은 정적으로 동작하는 것이 아니다. 일단 사용자가 어플리케이션을 사용하기 시작하면, 코드는 정적인 바이너리 포맷에서 동적인 모습으로 변모하게 된다. 물론 프로그래머들은 코드를 작성하고 설계하면서 머리 속으로 계속 시뮬레이션을 한다. 테스트도 한다. 요즘은 TDD가 유행하면서, 단위테스트와 설계, 코드 작성이 동시에 진행되기도 한다. 수많은 통합 테스트도 한다. 하지만, 버그는 남아 있을 수 밖에 없다.(아니라고 생각하시는 분은 저한테 메일 부탁드린다. 제가 읽은 논문과 책, 아티클에서는 적어도 그 방법을 찾을 수 없었다.) 버그가 남아있다면, 잡아야 한다. 복잡한 전산 이론을 동원하지 않더라도 시스템의 복잡도에 따라 디버그 시간은 많이 걸린다는 것을 알수 있을 것이다.

하물며, cache나 buffer를 사용하는 시스템을 어떠할까? 멀티스레드 시스템은 어떠한가? 위 세 가지를 모두 사용하는 시스템의 복잡도는 인간의 머리로 감당할 수 있을까? 적어도 우리가 주로 사용하는 몇가지 언어 중에 Java는 너무나 취약한 듯하다. 예를 들어 멀티스레드 관련된 라이브러리를 보면, Java5에 와서 비약적인 발전을 했다는 것도 그 정도이다.

기본적인 아이디어 #

자, 상상해보자. 한줄답변 식의 간단한 웹 어플리케이션 게시판을 만들었다. 구축 중에 RDBMS로 처리하기에는 너무 많은 트래픽이라는 제약사항이 있어 사용자들의 한줄답변을 메모리에 cache시켜 놓기로 했다. 보통 때는 정상작동 했다. 하지만 잠시후에 몇 개의 글이 중복되어 나타나기 시작했다. 이럴때 어떻게 디버깅할 것인가? 전통적인 콘솔이나 로그파일에 메시지를 찍는 방법으로 해결이 가능할까?

전통적인 콘솔이나 로그파일에 메시지를 찍는 방법으로 버그를 잡을려면, 일단 의심되는 소스를 수정하고 컴파일 한 후 배포해야 한다. 그런 후 웹 어플리케이션을 다시 시작하면 해당 메시지를 볼 수있다. 여기서 두 가지 문제가 발생한다. 첫 번째, 이 방법은 너무나 많은 시간을 소요한다. 만일 예상했던 곳에 메시지를 찍도록 했는데 의심했던 결과가 나타나지 않고 정상적이면 어떻게할 것인가? 다시 다른 곳을 찾아보고 소스 수정하고 컴파일 후 배포할 것인가? 두 번째, 이 방법은 해당 어플리케이션이 오류가 나는 상황을 재현하는 것이 힘들다. 당연하다. 배포 후에 시스템을 다시 시작해야 하기 때문이다.
자. 그럼 어떻게 할까? 병원을 떠올려 보자. 의사들은 골절이 의심되는 환자들이 오면 Xray를 찍는다. MRI도 한다. 우리는 그런 툴을 만들수 없을까? 적어도 그렇게 멋진 툴이 아닐지언정 비슷한 기능을 하는 툴을 말이다.

Xray를 찍자 #

약간은 생뚱맞을지 모르나 아래 소스를 보자.

// call groovy expressions from Java code
Binding binding = new Binding();
binding.setVariable("foo", new Integer(2));
GroovyShell shell = new GroovyShell(binding);

Object value = shell.evaluate("println 'Hello World!'; x = 123; return foo * 10");
assert value.equals(new Integer(20));
assert binding.getVariable("x").equals(new Integer(123)); 

위의 소스는 [http]Embedding Groovy에 나오는 예제 소스를 일부 발췌한 것이다. 뭔가 떠오르는 것이 없는가? 그렇다. Groovy 문법에 따르는 디버그용 소스를 프로그래머 마음대로 만들어 shell.evaluate()를 호출하면 시스템을 다시 시작하지 않고 현재 구동되고 있는 상태에서 시스템의 결과를 볼수 있지 않겠는가?

아래는 사내 보드 시스템 Griffin에 적용한 예이다.

package net.daum.griffin.action.admin;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.MetaClass;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import net.daum.griffin.action.BaseAction;
import net.daum.griffin.bo.GriffinSession;
import net.daum.griffin.util.StringUtil;
import net.daum.stratus.ActionRequest;
import net.daum.stratus.ActionResponse;

public class Script extends BaseAction {
    @SuppressWarnings("unchecked")
    @Override
    public void execute(ActionRequest req, ActionResponse res,
            GriffinSession session) {
        String script = req.getString("script", "");
                
        Binding binding = new Binding();
        binding.setVariable("req", req);
        binding.setVariable("res", res);
        binding.setVariable("session", session);
        binding.setVariable("action", this);

        Object evaluate = null;
        synchronized (Script.class) {
            GroovyShell shell = new GroovyShell(binding);            
            MetaClass metaClass = shell.getMetaClass();
            MetaClass.setUseReflection(true);
            binding.setVariable("metaClass", metaClass);
            
            try {
                evaluate = shell.evaluate(script);
            } catch (Throwable e) {
                StringWriter stringWriter = new StringWriter();
                PrintWriter printWriter = new PrintWriter(stringWriter);
                e.printStackTrace(printWriter);
                res.addToTemplate("error", toPresentationString(stringWriter.toString()));
            }
        }
        res.addToTemplate("script", toPresentationString(script));
        res.addToTemplate("return", toPresentationString(evaluate));
        Set<Map.Entry> entrySet = binding.getVariables().entrySet();        
        
        Map<String, String> bindingMap = new HashMap<String, String>();
        for (Map.Entry entry : entrySet) {
            String key = toPresentationString(entry.getKey().toString());
            String value = toPresentationString(entry.getValue().toString());
            bindingMap.put(key, value);
        }
        res.addToTemplate("binding", bindingMap);
    }
    
    @Override
    protected String[] getRequiredPermissions(HttpServletRequest req, GriffinSession session) {
        return new String[] { "accessControl" };        
    }   

    private String toPresentationString(Object value) {
        return escapeJavaScript(toHTML(value));
    }

    private String toHTML(Object value) {
        return (value != null ? StringUtil.escapeHTMLTag(value.toString()).replace("\n", "<BR>").replace("\r","<BR>") : "");
    }

    private String escapeJavaScript(String str) {
        ...
    }
}

Griffin을 잘모르는 사람은 그냥 위의 소스가 어떤 Servlet이라고 생각하자. 대충 감은 올 것이다. 첨언을 하자면, GriffinSession session은 Griffin에서 어플리케이션 로직의 중심이되는 master에 해당하는 클래스이기 때문에 bind를 했다. 자 위의 소스를 컴파일하고 올려 놓은 다음 간단한 웹 인터페이스를 붙여 보자.

http://dna.daum.net/wiki/imgs/custom/shellOnGriffin.jpg

Ajax로 멋있게 짤수도 있지만, 그건 시간 날때 하도록하고 일단, 디버깅에 불편한 몇가지 문제부터 해결하도록 하자.

모로가도 서울만 가면 된다? #

이 장난감을 가지고 놀다가 중요한 걸 알아챌 수 있었다. 예로 부터 Java언어 스팩에서 항상 분쟁의 불씨가 되어왔던 영역. private 메소드 및 변수다. Groovy도 Java를 쓰던 사람들이 만든 언어 아니랄까봐 역시 Java와 비슷한 정책을 취하고 있다. 하지만, 우리는 Xray가 필요하다. 속을 훤히 볼수 있는 Xray.

별다른 방법이 없다. Groovy 소스를 고치는 수밖에. 아래가 Groovy소스를 고친 예이다.

소스를 따라가다보면 '''groovy.lang.MetaClass'''를 만나게 된다. 이 클래스만 수정하면 우리가 원하는 방식대로 클래스 사이를 누비고 다닐 수있다.

 ...

    private Object getDeclaredFieldIncludeSuper(final Object object, final String property, Class targetClass) throws IllegalPropertyAccessException {
        Field field = null;        
        try {
            // lets try a public field
            field = targetClass.getDeclaredField(property);                    
            // ********************************** 이 부분을 수정 ********************************** 
            field.setAccessible(true);
            return field.get(object);
        } catch (IllegalAccessException iae) {
            throw new IllegalPropertyAccessException(field,targetClass);
        } catch (Exception e) {
            Class superClass = targetClass.getSuperclass();
            if(superClass != null) {
                return getDeclaredFieldIncludeSuper(object, property, superClass);
            }
            return null;
        }        
    }

  ...

    /**
     * Sets the property value on an object
     */
    public void setProperty(Object object, String property, Object newValue) {
        MetaProperty mp = (MetaProperty) propertyMap.get(property);
        if(mp != null) {
            try {
                mp.setProperty(object, newValue);
                return;
            }
            catch(ReadOnlyPropertyException e) {
                // just rethrow it; there's nothing left to do here
                throw e;
            }
            catch (TypeMismatchException e) {
                // tried to access to mismatched object.
                throw e;
            }
            catch (Exception e) {
                // if the value is a List see if we can construct the value
                // from a constructor
                if (newValue == null)
                    return;
                if (newValue instanceof List) {
                    List list = (List) newValue;
                    int params = list.size();
                    Constructor[] constructors = mp.getType().getConstructors();
                    for (int i = 0; i < constructors.length; i++) {
                        Constructor constructor = constructors[i];
                        if (constructor.getParameterTypes().length == params) {
                            Object value = doConstructorInvoke(constructor, list.toArray());
                            mp.setProperty(object, value);
                            return;
                        }
                    }

                    // if value is an array
                    Class parameterType = mp.getType();
                    if (parameterType.isArray()) {
                        Object objArray = asPrimitiveArray(list, parameterType);
                        mp.setProperty(object, objArray);
                        return;
                    }
                }

                // if value is an multidimensional array
                // jes currently this logic only supports metabeansproperties and
                // not metafieldproperties. It shouldn't be too hard to support
                // the latter...
                if (newValue.getClass().isArray() && mp instanceof MetaBeanProperty) {
                    MetaBeanProperty mbp = (MetaBeanProperty) mp;
                    List list = Arrays.asList((Object[])newValue);
                    MetaMethod setter = mbp.getSetter();

                    Class parameterType = setter.getParameterTypes()[0];
                    Class arrayType = parameterType.getComponentType();
                    Object objArray = Array.newInstance(arrayType, list.size());

                    for (int i = 0; i < list.size(); i++) {
                        List list2 =Arrays.asList((Object[]) list.get(i));
                        Object objArray2 = asPrimitiveArray(list2, arrayType);
                        Array.set(objArray, i, objArray2);
                    }

                    doMethodInvoke(object, setter, new Object[]{
                        objArray
                    });
                    return;
                }

                throw new MissingPropertyException(property, theClass, e);
            }
        }

        try {
            MetaMethod addListenerMethod = (MetaMethod) listeners.get(property);
            if (addListenerMethod != null && newValue instanceof Closure) {
                // lets create a dynamic proxy
                Object proxy =
                    createListenerProxy(addListenerMethod.getParameterTypes()[0], property, (Closure) newValue);
                doMethodInvoke(object, addListenerMethod, new Object[] { proxy });
                return;
            }

            if (genericSetMethod == null) {
                // Make sure there isn't a generic method in the "use" cases
                List possibleGenericMethods = getMethods("set");
                if (possibleGenericMethods != null) {
                    for (Iterator i = possibleGenericMethods.iterator(); i.hasNext(); ) {
                        MetaMethod mmethod = (MetaMethod) i.next();
                        Class[] paramTypes = mmethod.getParameterTypes();
                        if (paramTypes.length == 2 && paramTypes[0] == String.class) {
                            Object[] arguments = {property, newValue};
                            Object answer = doMethodInvoke(object, mmethod, arguments);
                            return;
                        }
                    }
                }
            }
            else {
                Object[] arguments = { property, newValue };
                doMethodInvoke(object, genericSetMethod, arguments);
                return;
            }

            /** todo or are we an extensible class? */

            // lets try invoke the set method
            // this is kind of ugly: if it is a protected field, we fall
            // all the way down to this klunky code. Need a better
            // way to handle this situation...

            String method = "set" + capitalize(property);
            try {
                invokeMethod(object, method, new Object[] { newValue });
            }
            catch (MissingMethodException e1) {
                Field field = null;
                try {
                    final Class clazz = object.getClass();
                    final String prop = property;
                    try {
                        field = (Field) AccessController.doPrivileged(new PrivilegedExceptionAction() {
                            public Object run() throws NoSuchFieldException {
                                return clazz.getDeclaredField(prop);
                            }
                        });
                        
                        // ********************************** 이 부분을 수정 ********************************** 
                        field.setAccessible(true);
                        field.set(object, newValue);
                    }
                    catch (PrivilegedActionException pae) {
                        if (pae.getException() instanceof NoSuchFieldException) {
                            throw (NoSuchFieldException) pae.getException();
                        } else {
                            throw new RuntimeException(pae.getException());
                        }
                    }
                } catch (IllegalAccessException iae) {
                    throw new IllegalPropertyAccessException(field,object.getClass());
                } catch (Exception e2) {
                    throw new MissingPropertyException(property, theClass, e2);
                }
            }

        }
        catch (GroovyRuntimeException e) {
            throw new MissingPropertyException(property, theClass, e);
        }

    }
  ...

덤으로 Collection이나 Object[]를 디버그 할 때, 해당 element에 대한 속성을 찾는 것이 Groovy의 기본 성질인데, 이 부분은 디버깅 할때 대단히 불편하다. '''getProperty(final Object object, final String property)'''를 수정하자.


 ...

    /**
     * @return the given property's value on the object
     */
    public Object getProperty(final Object object, final String property) {
        // look for the property in our map
        MetaProperty mp = (MetaProperty) propertyMap.get(property);
        if(mp != null) {
            try {
                //System.out.println("we found a metaproperty for " + theClass.getName() +
                //  "." + property);
                // delegate the get operation to the metaproperty
                return mp.getProperty(object);
            }
            catch(Exception e) {
                throw new GroovyRuntimeException("Cannot read property: " + property);
            }
        }

        if (genericGetMethod == null) {
            // Make sure there isn't a generic method in the "use" cases
            List possibleGenericMethods = getMethods("get");
            if (possibleGenericMethods != null) {
                for (Iterator i = possibleGenericMethods.iterator(); i.hasNext(); ) {
                    MetaMethod mmethod = (MetaMethod) i.next();
                    Class[] paramTypes = mmethod.getParameterTypes();
                    if (paramTypes.length == 1 && paramTypes[0] == String.class) {
                        Object[] arguments = {property};
                        Object answer = doMethodInvoke(object, mmethod, arguments);
                        return answer;
                    }
                }
            }
        }
        else {
            Object[] arguments = { property };
            Object answer = doMethodInvoke(object, genericGetMethod, arguments);
            // jes bug? a property retrieved via a generic get() can't have a null value?
            if (answer != null) {
                return answer;
            }
        }

        if (!CompilerConfiguration.isJsrGroovy()) {
            // is the property the name of a method - in which case return a
            // closure
            List methods = getMethods(property);
            if (!methods.isEmpty()) {
                return new MethodClosure(object, property);
            }
        }

        // lets try invoke a static getter method
        // this case is for protected fields. I wish there was a better way...
        Exception lastException = null;
        try {
            MetaMethod method = findGetter(object, "get" + capitalize(property));
            if (method != null) {
                return doMethodInvoke(object, method, EMPTY_ARRAY);
            }
        }
        catch (GroovyRuntimeException e) {
            lastException = e;
        }

        /** todo or are we an extensible groovy class? */
        if (genericGetMethod != null) {
            return null;
        }
        else {
            /** todo these special cases should be special MetaClasses maybe */
            if (object instanceof Class) {
                // lets try a static field
                return getStaticProperty((Class) object, property);
            }
            // ********************************** 이 부분을 수정 ********************************** 
//            if (object instanceof Collection) {
//                return DefaultGroovyMethods.getAt((Collection) object, property);
//            }
//            if (object instanceof Object[]) {
//                return DefaultGroovyMethods.getAt(Arrays.asList((Object[]) object), property);
//            }
            if (object instanceof Object) {                
                try {
                    Class targetClass = object.getClass();
                    Object result = getDeclaredFieldIncludeSuper(object, property, targetClass);
                    if(result != null) return result;
                } catch (IllegalPropertyAccessException iae) {
                    lastException = iae;
                }   
            }

            MetaMethod addListenerMethod = (MetaMethod) listeners.get(property);
            if (addListenerMethod != null) {
                /* @todo one day we could try return the previously registered Closure listener for easy removal */
                return null;
            }

            if (lastException == null)
                throw new MissingPropertyException(property, theClass);
            else
                throw new MissingPropertyException(property, theClass, lastException);
        }
    }

 ...


맺는 말 #

위의 아이디어를 가지고 Griffin과 같은 많은 cache와 멀티스레드가 범벅이된 어플리케이션을 디버깅 해보라. 색다른 재미가 있다. 살아있는 시스템의 속을 꿰뚫어 보는 솔솔한 재미랄까? 하지만, 위의 툴을 사용할 정도면, 설계가 어디 잘못된 곳은 없는지 수정할 곳은 없는지 심각히 고려해야 한다.

간단할 수록 잘 된 디자인이므로!

ps. Groovy를 고치지 않고도 위의 기능을 하는 방법을 아시는 분은 연락부탁합니다.