저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의
ResultSet의 결과값을 자바빈 객체에 저장하는 방법에 대해서 살펴본다.

Reflection과 JDBC 프로그래밍

웹 어플리케이션을 개발할 때 가장 많이 수행되는 SQL 쿼리를 고르라면 당연 "SELECT" 쿼리일 것이다. 게시판이나 자료실 등을 말할 필요도 없고 웹에서 보여지는 대부분의 정보들은 이제 더 이상 텍스트 파일이 아닌 데이터베이스의 테이블속에 저장되어 있으며, 그러한 정보들을 참조하기 위해서 수많은 "SELECT" 쿼리가 사용되고 있다.

자바 프로그래머에게 있어 SELECT 쿼리가 많이 사용된다는 것은 java.sql.ResultSet 으로부터 정보를 읽어오는 부분의 코드가 상당량을 차지한다는 것을 의미한다. 여기에 자바빈을 사용할 경우에는 ResultSet의 결과 필드의 값을 하나씩 읽어와 빈 프로퍼티의 값을 지정해야만 한다. 여러분들은 아마도 다음과 같은 형태의 코드를 많이 작성해보았을 것이다.

    rs = pstmt.executeQuery();
    if (rs.next()) {
        SomeBean bean = new SomeBean();
        bean.setName(rs.getString("name"));
        bean.setAge(rs.getInt("age"));
        bean.setAddress(rs.getString("address"));
        ...
        ...
        return bean;
    } else {
        ...
    }

만약 null 값이 존재한다면 null 값의 여부를 검사하는 코드도 포함될 것이다. 단순하게 느껴지겠지만 위와 같이 ResultSet의 getXXX() 형태의 메소드를 호출하는 것은 개발자의 실수를 유발하기 쉽다는 특징이 있다. 예를 들어, getString("name")이라고 입력해야 하는데 getString("anme") 이라고 입력할 수도 있으며, getString(1)이어야 하는데, getString(2)라고 입력할 수도 있는 것이다. 또한 null인지의 여부를 검사해야 하는데, 검사하지 않아서 NullPointerException이 발생하는 경우도 있기 마련이며, 이런 경우 많은 개발자들은 버그를 찾아 해매는데 불필요한 시간을 낭비하게 된다.

이와 같이 ResultSet의 getXXX 계열의 메소드를 호출하여 값을 읽어온 후 그 값을 빈 프로퍼티에 저장하는 코드를 다음과 같이 단순화 시킬 수 있다면 어떨까?

    rs = pstmt.executeQuery();
    if (rs.next()) {
        SomeBean bean = new SomeBean();
        BeanPropertySetter.setProperties(rs, bean, true, true);
        return bean;
    } else {
        ...
    }

위와 같이 ResultSet.getXXX() 메소드를 호출하는 부분이 단 한줄로 처리될 수 있다면 개발자들은 더 이상 스펠링을 잘못입력하는 것과 같은 자잘한 버그를 잡는 데 시간을 낭비할 필요가 없어질 것이며, 개발의 효율역시 상승시킬 수 있을 것이다.

이 글에서는 위와 같이 ResultSet의 결과값을 빈 프로퍼티에 저장해주는 기능을 어떻게 구현할 수 있는지에 대해서 살펴보고 1부에서 살펴봤던 BeanPropertySetter 클래스에 위 기능을 구현한 메소드를 추가해보도록 하자.

ResultSetMetaData

이 글에서는 "SELECT" 쿼리를 통해서 읽어온 필드의 이름과 자바빈 객체의 프로퍼티 이름이 같을 경우에 ResultSet의 결과값을 빈객체에 저장하는 방식을 사용할 것이다. 우리는 이미 1부에서 Reflection을 사용하여 자바빈 객체의 프로퍼티 목록을 추출하는 방법을 살펴보았었다. 2부에서도 완전히 같은 방법을 사용하여 자바빈의 프로퍼티 목록을 추출할 것이다. 자바빈 객체의 프로퍼티를 추출할 수 있기 때문에 이제 남아 있는 것은 ResultSet이 어떤 데이터 필드를 포함하고 있는 지를 추출해는 것이다. 예를 들어, 다음과 같은 쿼리를 생각해보자.

    SELECT NAME, ADDRESS, AGE FROM MEMBER;

여기서 NAME 필드와 ADDRESS 필드의 값을 자바빈 객체의 name 속성과 address 속성에 저장하기 위해서는 NAME 필드와 ADDRESS 필드가 존재한다는 사실을 알 수 있는 방법이 필요하다. ResultSet은 ResultSet이 저장하고 있는 데이터들이 어떤 필드에 해당되고 SQL 타입이 무엇인지를 알려주는 메타 데이터를 구할 수 있는 메소드를 제공하고 있는데, 그 메소드가 바로 getMetaData()이다. Result의 getMetaData()는 ResultSetMetaData 객체를 리턴해주는 데, 바로 이 ResultSetMetaData를 사용하면 우리가 원하는 것을 할 수 있게 된다.

ResultSetMetaData는 다음과 같은 메소드를 사용하여 ResultSet이 저장하고 있는 데이터들에 대한 정보를 구할 수 있도록 도와준다.

  • getColumnCount() - ResultSet이 저장된 컬럼의 개수를 구한다. (행의 개수가 아니다!)
  • getColumnName(int column) - 지정한 컬럼의 이름을 구한다.
  • getColumnType(int column) - 지정한 컬럼의 SQL 타입을 구한다.
이 세가지 메소드 이외에 getTableName(int column), isNullable(int column), getColumnClassName(int column) 등 더욱 많은 정보를 얻을 수 있도록 해 주는 메소드를 제공해주고 있긴 하지만, 이번 글에서는 위의 3가지 메소드만을 필요로 한다.

위 메소드에서 getColumnCount() 메소드는 읽어오는 컬럼의 개수를 구해준다. 예를 들어, 다음 코드를 생각해보자.

    rs = stmt.executeQuery("select ID, NAME, LEVEL from MEMBER");
    ResultSetMetaData rsmd = rs.getMetaData();
    int columns = rsmd.getColumnCount();

위 코드에서 "select .." 쿼리는 ID, NAME, LEVEL 등 3개의 컬럼을 읽어오므로 ResultSetMetaData의 getColumnCount() 메소드는 3을 리턴하게 된다. 컬럼의 개수를 알게 되면 다음과 같은 방법으로 각 컬럼의 이름과 타입을 알 수 있게 된다.

    for (int i = 1 ; i <= columns ; i ++) {
        String name = rsmd.getColumnName(i);
        int type = rsmd.getColumnType(i);
        ...
    }

여기서 getColumnName()은 SELECT 쿼리에서 사용한 컬럼 이름을 리턴한다. 만약 select * from 과 같이 모든 필드를 나타내는 '*' 기호를 사용했다면 DBMS에 리턴해주는 컬럼 이름을 리턴할 것이다. getColumnType()은 int 타입을 리턴하는데, 이때 리턴되는 값은 java.sql.Types 클래스에 정의되어 있는 상수값들이다. 예를 들어, 컬럼의 SQL 타입이 VARCHAR일 경우 getColumnType() 메소드는 Types.VARCHAR 상수를 리턴한다. Types 클래스 중 이 글에서 사용되는 상수값들은 다음과 같다.

  • Types.CHAR
  • Types.VARCHAR
  • Types.LONGVARCHAR
  • Types.SMALLINT
  • Types.INTEGER
  • Types.TINYINT
  • Types.BIGINT
  • Types.DOUBLE
  • Types.FLOAT
  • Types.DECIMAL
  • Types.NUMERIC
  • Types.TIME
  • Types.TIMESTAMP
  • Types.DATE
구현 및 테스트

이제 이글에서 살펴볼 기능을 구현하기 위해서 필요한 정보들을 추출할 수 있게 되었으므로 실제 구현에 대해서 살펴보자. ResultSet의 컬럼에 있는 값을 자바빈 프로퍼티의 값으로 지정하는 과정은 다음과 같다.

  1. ResultSet으로부터 필드명과 각 필드의 SQL 타입을 구해서 저장한다.
  2. 자바빈 객체에 존재하는 메소드 목록을 구한다.
  3. 각각의 메소드를 검사한다.
    1. 메소드 이름이 set으로 시작하는 지 검사한다.
    2. set으로 시작할 경우 프로퍼티 이름을 구하고 (set 이후의 메소드 이름이 프로퍼티 이름이다.) 그 메소드의 인자 개수가 1개인지를 검사한다.
    3. 위 조건을 충족할 경우, ResultSet의 getSome() 메소드를 사용하여 값을 읽어온다.
    4. 읽어온 컬럼값의 SQL 타입과 자바빈 객체의 프로퍼티 타입을 비교하여 컬럼의 값을 알맞게 형변환한 후 프로퍼티의 값으로 저장한다.
1단계 - ResultSet으로부터 컬럼에 대한 정보 추출하기

ResultSet으로부터 추출해야 하는 컬럼 정보는 컬럼 이름과 컬럼 타입이다. 컬럼 이름과 컬럼의 SQL 타입은 앞에서 설명한 ResultSetMetaData로부터 추출할 수 있다. 추출한 컬럼 이름과 타입은 임의의 장소에 저장되어 있어야 하는데, 이 글에서는 HashMap을 사용하여 저장할 것이다. HaspMap은 Hashtable과 마찬가지로 <키, 값> 쌍을 저장할 수 있도록 해 주는데, 여기서는 컬럼 이름이 키가 되고 컬럼 타입이 값이 된다. 다음은 컬럼 정보를 추출하는 부분의 코드이다.

public static void setProperties(ResultSet rs, Object bean,
       boolean nullToEmpty, boolean useTrim) throws SQLException {
    // ResultSet Meta Data 추출
    ResultSetMetaData rsMetaData = rs.getMetaData();
    int columnCount = rsMetaData.getColumnCount();    
    String columnName = null; // 컬럼명 저장
    int columnType = 0; // 컬럼 타입 저장
    int underbarIndex = -1;
    String tabName = null; // underbar를 포함한 그 이전까지의 컬럼이름을 저장한다.
    
    // 컬럼이름과 컬럼타입 매핑을 저장한다.
    HashMap typeMap = new HashMap(columnCount);
    for (int i = 0 ; i < columnCount ; i++) {
        columnName = rsMetaData.getColumnName(i+1);
        underbarIndex = columnName.indexOf("_");
        if (underbarIndex < columnName.length()-1) {
            if (tabName == null) tabName =
                columnName.substring(0, underbarIndex+1);
            columnType = rsMetaData.getColumnType(i+1);
            typeMap.put(
                columnName.substring(underbarIndex+1).toUpperCase(),
                new Integer(columnType));
        }
    }
    
    ...
    ...
    
}

위 코드에서 컬럼 이름을 구할 때 '_'이 포함되어 있는지의 여부를 판단하는 부분이 있는데, 이는 많은 기업에서 테이블의 컬럼 이름을 지을 때 [테이블명]_[나머지컬럼명]의 형태를 사용하기 때문이다. 예를 들어, 테이블 이름이 MEMBER일 경우 컬럼이름은 MEM_NAME, MEM_ADDRESS와 같은 형태를 띄는 경우가 많다. 그래서 필자는 '_'을 포함하고 있는 컬럼 이름에 대해서는 '_' 이후의 컬럼 이름을 사용하는 방향으로 구현해보았다. 즉, '_'를 포함하지 않는 경우에는 컬럼 이름과 자바빈 프로퍼티 이름을 비교하고, '_'를 포함하는 경우에는 '_' 이후의 컬럼 이름과 프로퍼티 이름을 비교하게 된다.

HashMap인 typeMap은 컬럼 이름과 컬럼 타입을 저장하는데, 이때 컬럼 이름은 모두 대문자로 변환한 후 저장하는 것을 알 수 있다. 이는 대부분의 DBMS가 컬럼 이름에 대해서 대소문자를 구분하지 않기 때문에 가능한 것이다.

2단계 - 빈 프로퍼티 검사

자바빈이 프로퍼티를 검사하는 과정은 지난 1부에서 살펴본 것과 크게 다르지 않다. 다른 점이 있다면 HttpServletRequest의 getParameter() 메소드를 사용하여 필요한 값을 읽어오는 대신 ResultSet의 getXXX() 메소드를 사용하여 값을 읽어온다는 것이다. 또 하나 차이점이 있다면, HttpServletRequest의 getParameter() 메소드의 리턴타입은 String인 반면에 ResultSet의 getXXX()의 메소드들은 저마다 리턴타입이 다르다는 점이다. 따라서, ResultSet의 결과값을 자바빈의 프로퍼티의 값으로 지정하기 위해서는 보다 복잡한 형변환 방법이 필요하다.

필자는 SQL 타입과 자바빈의 프로퍼티 타입 사이의 매핑을 다음과 같이 정의하였다.

 -------------------------------------------------------------------
 java.sql.Types타입   자바빈 프로퍼티타입  차선타입
 -------------------------------------------------------------------
 Types.CHAR           java.lang.String    
 Types.VARCHAR        java.lang.String
 Types.LONGVARCHAR    java.lang.String
 Types.SMALLINT       int                 long, String
 Types.INTEGER        int                 long, String
 Types.TINYINT        int                 long, String
 Types.BIGINT         long                String
 Types.DOUBLE         double              String
 Types.FLOAT          float               double, String
 Types.DECIMAL        double              float, int, long, String
 Types.NUMERIC        int                 long, double, float, String
 Types.TIME           java.sql.Date       java.util.Date, String
 Types.TIMESTAMP      java.sql.Timestamp  java.util.Date, String
 Types.DATE           java.sql.Date       java.util.Date, String

위 표의 첫번째 열과 두번째 열은 각각 SQL 타입과 그에 해당하는 자바빈 프로퍼티의 타입 사이의 매핑을 나타낸다. 에를 들어, 컬럼의 타입이 Types.INTEGER일 경우 자바빈 프로퍼티는 int 타입이어야 한다는 것을 의미한다. 그런데, 자바빈 프로퍼티가 정확하게 int 타입이 아닐 수도 있다. 이처럼 완전한 매핑이 이루어지지 않는 경우에는 차선타입을 사용하게 된다. 즉, 컬럼 타입이 Types.INTEGER인데 자바빈 프로퍼티의 타입이 int가 아닐 경우, 자바빈 프로퍼티 타입이 차선타입에 있는 long 또는 String 인지의 여부를 판단하게 되며, 그에 알맞게 형변환을 하게 된다.

지금까지 설명한 부분을 구현한 코드는 다음과 같다.

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) : "");
        // 프로퍼티 이름과 동일한 이름의 필드 타입을 구한다.
        Integer columnTypeInteger =
                (Integer)typeMap.get(propertyName.toUpperCase());        
        if (columnTypeInteger != null) {
            // 프로퍼티 이름과 동일한 이름의 컬럼이 존재하는 경우
            try {
                Object[] callParameter = null; // 메소드를 호출할 때 전달한 인자
                switch(columnTypeInteger.intValue()) {
                    case Types.CHAR:
                    case Types.VARCHAR:
                        // 자바빈 프로퍼티가 String 타입이냐?
                        if (parameters[0] == String.class) {
                            String varcharVal = 
                                rs.getString(tabName+propertyName);
                            if (varcharVal == null) {
                                if (nullToEmpty) {
                                    callParameter = new Object[] { "" };
                                }
                            } else {
                                callParameter = new Object[] {
 
                                  (useTrim ?
                                      varcharVal.trim() : varcharVal) };
                            }
                        }
                        break;
                    ...
                    // 나머지 SQL 타입에 대해서도 알맞은 처리를 한다.
                    ...
                    ...
                } // end of switch
                
                if (callParameter != null) {
                    // 메소드를 호출한다.
                    methods[i].invoke(bean, callParameter);
                }
            } catch(Exception ex) {
                // 예외가 발생하면 단순히 넘어간다.
                // 예를 들어, int 타입의 프로퍼티에 'abc'와 같은 문자열을
                // 값으로 사용한 경우는 기본값 그대로 사용한다.
            }
        }
    } // 빈 객체의 메소드가 setSome 형태의 메소드인 경우의 끝
}

위 코드를 보면 (대문자로 변환한) 프로퍼티의 이름을 키값으로 사용하여 typeMap에 저장되어 있는 값을 구한다. typeMap에는 컬럼 이름과 컬럼 타입의 매핑이 저장되어 있으므로, 자바빈의 프로퍼티 이름과 같은 이름을 갖는 컬럼이 존재할 경우 typeMap은 컬럼 타입값을 저장하고 있는 Integer 객체를 리턴하게 된다. 존재하지 않는다면 null을 리턴한다.

typeMap으로부터 Integer 객체를 구하게 되면 그 Integer 객체가 저장하고 있는 값(컬럼 타입을 나타내는 값)을 사용하여 SQL 타입과 자바빈 프로퍼티 사이의 매핑을 처리한다. 필자는 switch를 사용하여 이 부분을 처리하였으며, switch 블럭만 100여줄이 넘어가기 때문에 글에서 생략하였다. switch 블럭의 내용은 관련 링크의 소스 코드를 참고하기 바란다.

사용

BeanPropertySetter의 setProperties(ResultSet, Object, boolean, boolean) 메소드를 사용하는 방법은 매우 간단하다. 1부에서 setProperties(HttpServletRequest, Object, boolean, boolean) 메소드를 사용할 때와 마찬가지로 ResultSet을 메소드에 전달해주기만 하면 된다. 일반적으로 다음과 같은 형태로 메소드를 사용하게 될 것이다.

    try {
        ...
        stmt = conn.createStatement();
        rs = stmt.executeQuery("select ... ");
        if (rs.next()) {
            SomeBean bean = new SomeBean();
            BeanPropertySetter.setProperties(rs, bean, true, false);
            ...
        }
    } catch(SQLException ex) {
        ...
    }
    

결론

이제 BeanPropertySetter 클래스는 ResultSet으로부터 프로퍼티의 값을 읽어오는 기능이 추가되었으며, 이로써 웹 어플리케이션에서 값을 추출할 때 주로 사용되는 두 가지 객체인 HttpServletRequest와 ResultSet과 관련된 코딩량을 상당량 줄일 수 있게 되었다. 이는 코딩을 해야 하는 프로그래머에게 편리함을 제공해주며, 또한 코딩량이 줄어든 것에 비례하여 철자가 틀려서 비롯되는 버그의 양도 상당량 줄여준다.

빈과 관련된 유틸리티 클래스는 여기서 끝나지 않는다. 우리가 또 하나 생각해볼만한 것이 있다면, 그것은 바로 INSERT 쿼리와 UPDATE 쿼리에 자동으로 빈의 값을 채워 넣어주는 기능이다. 예를 들어, 다음과 같은 PreparedStatement 를 생성했다고 해 보자.

    pstmt = conn.prepareStatement(
        "UPDATE SOMETABLE SET ADDRESS=?, ... WHERE ID=?");
    pstmt.setString(1, memberBean.getName());
    ...
    pstmt.setString(n, memberBean.getId());

위와 같은 형태의 코드는 수도 없이 반복적으로 나온다. INSERT 쿼리 역시 마찬가지이다. 이러한 코드를 간결하게 만들수만 있다면 개바자들은 BeanPropertySetter 클래스를 사용하는 것 만큼이나 편리함을 느끼게 될 것이다. 다음 3부에서는 PreparedStatement의 각 인자에 자바빈 프로퍼티의 값을 할당해주는 유틸리티 클래스인 BeanToQueryMapper를 작성해보도록 하자.

Posted by 최범균 madvirus

댓글을 달아 주세요

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을 사용하여 필요한 기능을 구현할 것이다.

Posted by 최범균 madvirus

댓글을 달아 주세요