주요글: 도커 시작하기
반응형
HTTP 요청의 파라미터의 값을 자바빈 객체에 저장하는 방법에 대해서 알아본다.

Reflection과 웹 프로그래밍

JSP 시장이 확대되면서 많은 개발자들이 서블릿과 JSP를 이용한 모델 2 프로그래밍에 대한 중요성을 인식해가고 있으며, 인식 속도와 비례하여 자바빈을 웹 프로그래밍에서 사용하는 경우가 증가하고 있다. 하지만, 서블릿에서 자바빈을 생성할 때 몇가지 불편한 점이 있는데, 그 중 하나가 자바빈 프로퍼티의 값을 채워넣는 것이다.

예를 들어, 자바빈의 프로퍼티의 값을 HTTP 요청의 프로퍼티 값으로 지정하고 싶다고 해 보자. JSP에서는 <jsp:useBean>과 <jsp:setProperty< 액션 태그를 사용하여 단 2-3줄만으로 대부분의 자바빈 프로퍼티의 값을 HTTP 요청의 파라미터 값으로 채워 넣을 수 있게 된다. 하지만, 서블릿에서는 다음과 같은 형태의 코드를 사용하는 것이 일반적이다.

  SomeBean sb = new SomeBean();
  sb.setProperty1(request.getParameter("property1"));
  sb.setAge(Integer.parseInt(request.getParameter("age")));
  sb.setAddress(request.getParameter("address"));
  ....

보다시피 서블릿에서는 각각의 자바빈 프로퍼티에 대해서 일일이 값의 지정 및 예외 처리를 해 주어야만 한다. 이를 좀더 쉽게 할 수는 없을까? JSP에서 처럼 간단하게 HTTP 요청의 파라미터 값을 자바빈 프로퍼티의 값으로 지정할 수는 없을까? 이번 기사에서는 이 질문에 대한 답을 하고자 한다. 또한 더 나아가 ResultSet의 결과값을 자바빈 프로퍼티의 값으로 지정하는 방법에 대한 것도 살펴보기로 한다.

Reflection, 자바빈 그리고 HTTP 요청

HTTP 요청의 파라미터 값을 자바빈 프로퍼티의 값으로 지정하기 위해서 필요한 것은 파라미터의 이름과 자바빈 프로퍼티의 이름을 알맞게 연결하는 것이다. 이를 하기 위해서 필요한 것이 바로 리플렉션(reflection)이다. 리플렉션은 특정한 클래스가 정의하고 있는 메소드의 목록 및 각 메소드이 시그너쳐 등을 분석할 수 있도록 해 주는 유용한 API로서 자바빈 프러퍼티의 목록을 추출해낼 수 있도록 해 준다.

HTTP 요청의 파라미터 값을 읽어와서 자바빈 프로퍼티의 값에 넣어주기 위한 프로그래밍 순서는 다음과 같다.

  1. 자바빈 객체에 존재하는 메소드 목록을 구한다.
  2. 각각의 메소드를 검사한다.
    1. 메소드 이름이 set으로 시작하는 지 검사한다.
    2. set으로 시작할 경우 프로퍼티 이름을 구하고 (set 이후의 메소드 이름이 프로퍼티 이름이다.) 그 메소드의 인자 개수가 1개인지를 검사한다.
    3. 위 조건을 충족할 경우, request.getParameter("someProperty")의 값을 프로퍼티의 값으로 지정한다.
각 과정이 어떻게 구현되는 지 순차적으로 살펴보도록 하자. 지금부터 설명하는 부분은 이번 글에서 완성할 BeanPropertySetter 라는 클래스의 setProperties()라는 메소드를 순차적으로 살펴보는 것이기도 하다.

step 0: 기본적인 클래스의 형태

먼저 이 글에서 완성할 클래스인 BeanPropertySetter 클래스의 기본적인 형태는 다음과 같다.

    import javax.servlet.http.HttpServletRequest;
    
    public class BeanPropertySetter {
        public static void setProperties(HttpServletRequest request, Object bean) {
            ...
        }
   }

위 코드에서 setProperties() 메소드는 두 개의 파라미터를 받는데, 첫번째는 HTTP 요청이고 두번째는 요청 파라미터의 값으로 프로퍼티의 값을 지정할 자바빈 객체를 의미한다.

step 1: 빈 객체의 메소드 목록을 구한다.

setProperties(HttpServletRequest, Object) 메소드는 자바빈 객체의 메소드 목록을 구하는 것부터 시작하는데, 이는 java.lang.Class 클래스의 getDeclaredMethods() 메소드를 사용하면 간단하게 수행할 수 있다. 코드 형태는 다음과 같다.

    public static void setProperties(HttpServletRequest request, Object bean) {
        Class beanClass = bean.getClass();
        Method[] methods = beanClass.getDeclaredMethods();
        ...
    }

여기서 첫번째 코드는 빈 객체가 어떤 타입의 클래스인지 구하게 된다. 자바에 모든 클래스는 그 타입에 해당하는 Class 객체를 갖고 있다. 실제 객체의 Class 타입은 두 가지 방법으로 구할 수 있는데, 하나는 객체의 getClass() 메소드를 호출하는 것이며 또 하나는 클래스의 정적 필드인 class 를 참조하는 방법이다. 이 두 방법을 코드화하면 다음과 같다.

    String some = new String("");
    Class stringClass1 = some.getClass();
    Class stringClass2 = String.class;

위에서 stringClass1과 stringClass2는 모두 java.lang.String 클래스에 해당하는 Class 객체를 가리키게 된다. 여기서 구현할 setProperties() 메소드는 객체의 getClass() 메소드를 사용하여 해당하는 객체의 실제 클래스 타입에 해당하는 Class 객체를 구하였다.

일단 Class 객체를 구하게 되면, Class 객체의 getDeclaredMethods() 메소드를 사용하여 그 객체에 정의(또는 선언)되어 있는 메소드의 목록을 구할 수 있게 된다. 이 때, 이 메소드는 목록은 java.lang.refletc.Method 객체의 배열형태로 리턴된다.

step 2: 메소드의 분석

메소드 목록을 구했으므로 이제 해야할 일은 메소드를 분석하여 프로퍼티의 값에 알맞은 요청 파라미터를 할당하는 것이다. 메소드를 분석할 때 필요한 정보는 다음과 같다.

  • 메소드의 이름 - 자바빈 프로퍼티의 값을 지정하는 것과 관련된 메소드의 이름은 setPropertyName()의 형태를 띈다.
  • 메소드가 전달받는 인자 개수 - setPropertyName() 형태의 메소드는 인자의 개수가 1개인 것이 일반적이다. 배열을 값으로 갖는 프로퍼티가 존재하긴 하지만, 이 글에서는 간단하게 메소드의 인자가 1개인 것만 처리하도록 한다.
이 두가지 정보는 각각 Method 클래스의 getName() 메소드와 getParameterTypes() 메소드를 사용해서 구할 수 있다. getParameterTypes() 메소드는 Class의 배열을 리턴한다. 즉, setSomeProperty1(String a) 라는 메소드는 길이가 1인 Class 배열을 리턴하게 되며, 전달받는 인자가 없을 경우에는 길이가 0인 Class의 배열을 리턴한다. 따라서, 메소드를 분석하는 코드의 기본 구조는 다음과 같은 형태를 띄게 된다.

    public static void setProperties(HttpServletRequest request, Object bean) {
        Class beanClass = bean.getClass();
        Method[] methods = beanClass.getDeclaredMethods();
        for (int i = 0 ; i < methods.length ; i++) {
            String name = methods[i].getName();
            Class[] parameters = methods[i].getParameterTypes();
            
            if (name.indexOf("set") == 0 && name.length() > 3 &&
                parameters.length == 1) {
                String propertyName = name.substring(3,4).toLowerCase() + 
                       ((name.length() > 4) ? name.substring(4) : "");
                ...
            }
        }
    }

위 코드에서 if 블럭은 메소드의 이름이 set 으로 시작하고 길이가 3 이상이고(즉, setSome의 형태를 띄고) 전달받는 인자의 길이가 1인 메소드를 프로퍼티와 관련된 메소드로 가정하고 필요한 작업을 수행하게 된다. if 블럭의 시작은 프로퍼티의 이름을 구하는 것으로 시작하는데 프로퍼티 이름은 메소드의 이름 중 set 이후의 문장을 사용하게 되는데, 이때 나머지 문장의 첫번째 글자는 소문자로 변경하게 된다. 즉, setCompanyName(String) 메소드로부터 추출된 프로퍼티 이름은 'companyName'이 된다.

step 3: set 메소드를 호출하여 파라미터의 값을 프로퍼티에 할당하기

메소드의 이름으로부터 프로퍼티 이름을 추출한 다음에는 프로퍼티와 동일한 이름을 갖는 파라미터의 값을 프로퍼티의 값으로 지정해주어야 한다. 여기서 파라미터의 값이 존재하는지의 여부를 판단하는 것은 매우 간단하다. 단순히, request.getParameter(propertyName) 메소드를 호출하여 리턴값이 null인지의 여부만 판단하면 된다.

문제는 setPropertyName() 메소드를 호출하는 것인데, 이는 Method 클래스의 invoke(Object obj, Object[] args) 메소드를 사용하면 된다. 예를 들어, 특정 Method 객체인 m이 setCompanyName(String name) 메소드를 나타낸다고 해 보자. 이 경우, 다음과 같은 형태의 코드를 사용하여 setCompanyName() 메소드를 호출할 수 있게 된다.

    
    SomeBean sb = new SomeBean(); // setCompanyName() 메소드를 정의하고 있는 빈 객체
    Class beanClass = sb.getClass();
    Method[] m = beanClass.getDeclaredMethods();
    for (int i = 0 ; i < m.length ; i++) {
        if (m.getName().compareTo("setCompanyName") == 0 &&
            m.getParameterTypes().length == 1) {
            Object[] param = new Object[] {"인자값" };
            m.invoke(sb, param);
        }
    }

Method의 invoke() 메소드의 첫번째 파라미터는 메소드를 호출할 객체를 나타내며, 두번째 파라미터는 메소드를 호출할 때 전달할 파라미터를 나타낸다. 즉, 위 코드는 sb.setCompanyName("인자값")을 실행한 것과 같은 결과를 나타낸다. 물론, setCompanyName() 메소드를 직접 호출할 때에 비해 몇배로 코드 분량이 증가하지만, 프로퍼티의 이름을 알 수 없는 상황에서 동적으로 메소드를 호출하기 위해서는 위와 같이 리플렉션을 사용할 수 밖에 없다.

이제 위 코드를 우리가 하고자 하는 곳에 적용해보자. set 메소드를 호출하여 프로퍼티의 값을 파라미터의 값으로 지정해주는 것은 다음과 같은 코드를 통해서 이루어진다.

    if (name.indexOf("set") == 0 && name.length() > 3
        && parameters.length == 1) {
        String propertyName = name.substring(3,4).toLowerCase() + 
               ((name.length() > 4) ? name.substring(4) : "");
        // 첫번째 단계 - 파라미터 감사
        String paramVal = request.getParameter(propertyName);
        if (paramVal != null) {
            try {
                // 두번째 단계 - 메소드 호출시 전달할 인자 배열 생성
                Object[] callParameter = null;
                if (parameters[0] == String.class) {
                    callParameter = new Object[] { paramVal };
                } else if (parameters[0] == int.class) {
                    callParameter = new Object[] { new Intenger(paramVal) };
                }                
                // 세번째 단계 - 메소드 호출
                if (callParameter != null) {
                    methods[i].invoke(bean, callParameter);
                }
            } catch(Exception ex) {
                // 예외가 발생할 경우 단순히 다음 메소드로 넘어간다.
            }
        }
    }

위 코드는 크게 3가지 단계로 구성되어 있다. 첫번째 단계는 프로퍼티이름과 같은 이름을 갖는 파라미터가 존재하는 지 판단하는 단계이며, 두번째 단계는 파라미터의 타입에 따라 알맞게 메소드 호출시 전달할 인자 배열을 생성하는 것이며, 마지막으로 세번째 단계는 실제 메소드를 호출하는 것이다. 여기서 눈여겨 볼 부분은 두번째 단계인 인자 배열을 생성하는 단계이다.

메소드 호출시 넘겨줄 인자의 타입은 앞에서 Method.getParameterTypes() 메소드를 사용하여 구할 수 있다고 하였는데, 이때 넘겨줄 인자 배열의 값들은 이 타입에 맞춰서 생성해주어야 한다. 위 코드의 경우는 String 클래스와 int 기본데이터 타입에 대해서만 검사를 하였는데, 보다 범용적으로 동작하기 위해서는 자바빈 컴포넌트에서 많이 사용되는 나머지 데이터 타입도 지원해야 할 것이다. 참고적으로 위 코드를 보면 int 타입을 나타내는 클래스가 존재한다는 것을 알 수 있는데(int.class), 나머지 기본 데이터 타입 역시 각 타입에 대한 클래스가 존재하므로 알아두길 바란다.

완전한 소스 코드와 사용방법

지금까지 부분적으로 내용을 살펴보았는데, 이제 완전한 소스 코드를 살펴보도록 하자. 완전한 소스 코드는 다음과 같다.

  package bean.util;
  
  import javax.servlet.http.HttpServletRequest;
  import java.lang.reflect.*;
  
  /**
   * HttpServletRequest의 파라미터값을 자바빈의 특정 프로퍼티값에
   * 저장해준다.
   * <p>
   * 기본적으로 파라미터와 프로퍼티는 이름을 사용하여 매핑한다.
   * 매핑할 때 프로퍼티의 타입에 따라서 파라미터의 값에
   * 다음과 같은 제약사항이 따른다.
   * <ul>
   *  <li>String - 제약사항없음
   *  <li>int/long/double/float - Integer.parseInt(), Long.parseLong(), 
   *  Double.parseDouble(), Float.parseFloat()를 수행하여 에러가 발생하면 안 된다.
   *  <li>boolean - 파라미터값이 대소문자구분없이 "true"일 경우에만 true
   * </ul>
   *
   * @author 최범균 [madvirus@tpage.com]
   */
  public final class BeanPropertySetter {
     
     /**
      * request의 파라미터를 bean에 복사한다.
      * 이때 빈의 프로퍼티와 같은 이름을 갖는 파라미터의 값만 복사된다.
      * @param request HTTP 요청
      * @param bean 자바빈
      * @param nullToempty 파라미터의 값이 null일 경우 ""를 값으로 할지의 여부
      * @param useTrim String.trim() 메소드를 사용할지의 여부.
      */
     public static void setProperties(HttpServletRequest request, Object bean,
                             boolean nullToempty, boolean useTrim) {
        Class beanClass = bean.getClass();
        Method[] methods = beanClass.getDeclaredMethods();
        Class[] parameters = null; // 메소드의 파라미터
        String mName = null; // 메소드이름
        
        for (int i = 0 ; i < methods.length ; i++) {
           mName = methods[i].getName();
           parameters = methods[i].getParameterTypes(); 
          if (mName.indexOf("set") == 0 && mName.length() > 3
               && parameters.length == 1) {
              // 메소드 이름이 set 으로 시작하고 파라미터의 개수가 1개인 경우
              // 프로퍼티 이름을 구한다.
              String propertyName = mName.substring(3,4).toLowerCase() + 
                        ((mName.length() > 4) ? mName.substring(4) : "");
              // 프로퍼티 이름과 동일한 이름의 파라미터값을 구한다.
              String paramValue = request.getParameter(propertyName);
              if (paramValue != null) {
                 try {
                    // 값이 null이 아닌 경우에만 빈의 메소드 호출
                    Object[] callParameter = null; // 메소드를 호출할 때 전달한 인자
                    
                    if (parameters[0] == String.class) {
                       callParameter = new Object[] {
                          (useTrim ? paramValue.trim() : paramValue) };
                    } else if (parameters[0] == int.class) {
                       callParameter = new Object[] { new Integer(paramValue) };
                    } else if (parameters[0] == long.class) {
                       callParameter = new Object[] { new Long(paramValue) };
                    } else if (parameters[0] == double.class) {
                       callParameter = new Object[] { new Double(paramValue) };
                    } else if (parameters[0] == float.class) {
                       callParameter = new Object[] { new Float(paramValue) };
                    } else if (parameters[0] == Boolean.class) {
                       callParameter = new Object[] { new Boolean(paramValue) };
                    }
                    
                    if (callParameter != null) {
                       // 메소드를 호출한다.
                       methods[i].invoke(bean, callParameter);
                    }
                 } catch(Exception ex) {
                    ex.printStackTrace();
                    // 예외가 발생하면 단순히 넘어간다.
                    // 예를 들어, int 타입의 프로퍼티에 'abc'와 같은 문자열을
                    // 값으로 사용한 경우는 기본값 그대로 사용한다.
                 }
              } else {
                 // 파라미터 값이 null일 경우
                 // 프로퍼티 타입이 String이고 nullToEmpty 인자가 true라면
                 // ""를 값으로 지정한다.
                 if (parameters[0] == String.class) {
                    try {
                       methods[i].invoke(bean, new String[] {""});
                    } catch(Exception ex) {
                       // do nothing;
                    }
                 }
              }
           }
        }
     }
  }

위의 완전한 소스 코드에는 앞에서 설명하지 않았던 부분이 추가되었는데, 그것은 바로 파라미터의 값이 null일 경우 그에 해당하는 빈 프로퍼티의 값을 공백문자열("")로 지정하는 것과 문자열의 양쪽에 있는 공백문자를 제거(trim())하는 부분이다. 이 두 가지 기능은 setProperties() 메소드의 세번째와 네번째 인자를 통해서 사용할 지의 여부가 결정된다.

실제 사용

BeanPropertySetter 클래스의 사용 방법은 매우 간단하다. 예를 들어, 다음과 같은 폼이 있다고 해 보자.

    <form action="register">
    ID   : <input type="text" name="<b>id</b>">
    이름 : <input type="text" name="<b>name</b>">
    암호 : <input type="password" name="<b>password</b>">
    <input type="submit" value="전송">
    </form>
    

위 폼을 통해서 입력받은 데이터를 넣을 자바빈 객체는 다음과 같은 메소드를 정의하고 있다고 해 보자.

    public class MemberBean {
        public void setId(String id);
        public void setName(String name);
        public void setPassword(String password);
        
        public String getId();
        public String getName();
        public String getPassword();
    }

이제 위의 폼에서 데이터를 입력받는 서블릿을 생각해보자. BeanPropertySetter 클래스를 사용할 경우 빈 객체에 HTTP 요청 파라미터의 값을 넣는 것은 다음과 같이 간단하게 처리된다.

    public class RegisterServlet extends HttpServlet {
        public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {
            MemberBean mb = new MemberBean();
            BeanPropertySetter.setProperties(req, mb, false, false);
            ...
            ...
        }
    }

만약 이 부분은 request.getParameter() 메소드를 사용해서 처리한다면 다음과 같이 될 것이다.

    public class RegisterServlet extends HttpServlet {
        public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {
            MemberBean mb = new MemberBean();
            mb.setId(request.getParameter("id"));
            mb.setName(request.getParameter("name"));
            mb.setPassword(request.getParameter("password"));
            ...
            ...
        }
    }

이렇게 처리할 경우 코딩량도 늘어날 뿐만 아니라 파라미터의 이름을 잘못 입력하는 등의 사소한 실수 때문에 찾기 어려운 버그를 만들 가능성도 증가하게 된다. 이는 BeanPropertySetter 클래스를 사용하면 코딩량이 줄어들 뿐만 아니라 버그의 발생 가능성도 줄어든다는 것을 의미한다.

결론

이번 글에서는 Relfection을 사용하여 HTTP 요청 파라미터의 값을 자바빈 프로퍼티의 값에 할당해주는 방법에 대해서 살펴보았다. 모델 2 구조가 일반화되면서 서블릿의 역할이 중요해지는 만큼 HTTP 요청의 값을 빈 프로퍼티의 매핑시켜주는 기능은 편리한 개발을 위해서는 필수적인 기본 기능이라 할 수 있다. 이와 함께 또 하나 필요한 기본 기능이 있다. 그것은 바로 ResultSet에 있는 결과값을 빈 프로퍼티에 매핑시켜주는 기능이다. 다음 글에서는 바로 ResultSet과 빈 프로퍼티 사이의 매핑을 처리하는 방법에 대해서 알아보기로 하자. 물론, 다음글에서도 Reflection을 사용하여 필요한 기능을 구현할 것이다.

+ Recent posts