본문 바로가기
교재 실습/자바 웹 개발 워크북

88. 리플랙션 API를 이용하여 프런트 컨트롤러 개선하기 (4)

by Jint 2022. 8. 6.

5. ServletRequestDataBinder 클래스 생성

ServletRequestDataBinder 클래스는 클라이언트가 보낸 매개변수 값을 자바 객체에 담아주는 역할을 수행한다. spms.bind 패키지에 ServletRequestDataBinder 클래스를 생성하고 다음과 같이 편집한다.

package spms.bind;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.Set;

import javax.servlet.ServletRequest;

//클라이언트가 보낸 매개변수 값 자바 객체에 담아줌
public class ServletRequestDataBinder {
	//요청 매개변수의 값, 데이터 이름, 데이터 타입을 받아 데이터 객체를 만드는 일
	public static Object bind(ServletRequest request, Class<?> dataType, String dataName) throws Exception {
		if(isPrimitiveType(dataType)) {
			//기본 타입의 경우 셋터 메서드가 없기 때문에 객체를 생성하는 메서드
			return createValueObject(dataType, request.getParameter(dataName));
		}

		Set<String> paramNames = request.getParameterMap().keySet(); //매개변수 키목록
		Object dataObject = dataType.newInstance(); //해당 클래스의 인스턴스 얻는다
		Method m = null;

		for(String paramName : paramNames) {
			m = findSetter(dataType, paramName);
			if(m != null) {
				//dataObject에 대해 m메서드 호출 - createValueObject()로 객체생성한 매개변수 가지고 셋터 메서드 실행
				//createValueObject(셋터 메서드의 매개변수 타입(매개변수 목록 배열로 반환), 요청 매개변수 타입)
				m.invoke(dataObject, createValueObject(m.getParameterTypes()[0], request.getParameter(paramName)));
			}
		}
		return dataObject;
	}

	//dataType이 기본 타입인지 아닌지 검사
	private static boolean isPrimitiveType(Class<?> type) {
		if(type.getName().equals("int") || type == Integer.class
		|| type.getName().equals("long") || type == Long.class
		|| type.getName().equals("float") || type == Float.class
		|| type.getName().equals("double") || type == Double.class
		|| type.getName().equals("boolean") || type == Boolean.class
		|| type == Date.class || type == String.class) {
			return true;
		}
		return false;
	}

	//요청 매개변수의 값을 가지고 기본 타입의 객체를 생성
	private static Object createValueObject(Class<?> type, String value) {
		if(type.getName().equals("int") || type == Integer.class) {
			return new Integer(value);
		}else if(type.getName().equals("float") || type == Float.class) {
			return new Float(value);
		}else if(type.getName().equals("double") || type == Double.class) {
			return new Double(value);
		}else if(type.getName().equals("long") || type == Long.class) {
			return new Long(value);
		}else if (type.getName().equals("boolean") || type == Boolean.class) {
			return new Boolean(value);
		}else if (type == Date.class) {
			return java.sql.Date.valueOf(value);
		}else {
			return value;
		}
	}

	//데이터 타입(Class)과 매개변수 이름(String)을 주면 셋터 메서드 찾아서 반환
	private static Method findSetter(Class<?> type, String name) {
		Method[] methods = type.getMethods();
		String propName = null;
		for(Method m : methods) {
			if(!m.getName().startsWith("set")) continue; //메서드 이름이 set으로 시작하지 않으면 넘김
			propName = m.getName().substring(3); //index값 0~2까지 제외한 값 가져옴
			if(propName.toLowerCase().equals(name.toLowerCase())) {//toLowerCase() : 대상 문자열을 모두 소문자로 변환
				return m;
			}
		}
		return null;
	}
}

이 클래스는 외부에서 호출할 수 있는 한 개의 public 메서드와 내부에서 사용할 세 개의 private 메서드를 가지고 있다. 이 클래스에 있는 메서드 모두 static으로 선언하였다. 즉 인스턴스를 생성할 필요가 없이 클래스 이름으로 바로 호출하겠다는 의도이다. 이렇게 특정 인스턴스의 값을 다루지 않는다면 static으로 선언하여 '클래스 메서드'로 만드는 것이 좋다.

 

- bind() 메서드

프런트 컨트롤러에서 호출하는 메서드이다. 요청 매개변수의 값과 데이터 이름, 데이터 타입을 받아서 데이터 객체(예 : Member, String, Date, Integer 등)를 만드는 일을 한다.

//요청 매개변수의 값, 데이터 이름, 데이터 타입을 받아 데이터 객체를 만드는 일
public static Object bind(ServletRequest request, Class<?> dataType, String dataName) throws Exception {
    if(isPrimitiveType(dataType)) {
        //기본 타입의 경우 셋터 메서드가 없기 때문에 객체를 생성하는 메서드
        return createValueObject(dataType, request.getParameter(dataName));
    }

    Set<String> paramNames = request.getParameterMap().keySet(); //매개변수 키목록
    Object dataObject = dataType.newInstance(); //해당 클래스의 인스턴스 얻는다
    Method m = null;

    for(String paramName : paramNames) {
        m = findSetter(dataType, paramName);
        if(m != null) {
            //dataObject에 대해 m메서드 호출 - createValueObject()로 객체생성한 매개변수 가지고 셋터 메서드 실행
            //createValueObject(셋터 메서드의 매개변수 타입(매개변수 목록 배열로 반환), 요청 매개변수 타입)
            m.invoke(dataObject, createValueObject(m.getParameterTypes()[0], request.getParameter(paramName)));
        }
    }
    return dataObject;
}

이 메서드의 첫 번째 명령문은 dataType이 기본 타입인지 아닌지 검사하는 일이다. 만약 기본 타입이라면 즉시 객체를 생성하여 반환할 것이다.

if(isPrimitiveType(dataType)) {
    //기본 타입의 경우 셋터 메서드가 없기 때문에 객체를 생성하는 메서드
    return createValueObject(dataType, request.getParameter(dataName));
}

① isPrimitiveType() 메서드는 이 클래스 내부에 정의된 메서드로서 int, long, float, double, boolean, java.util.Date, java.lang.String 타입에 대해 기본 타입으로 간주하며 true를 반환한다.

② createValueObject() 메서드는 기본 타입의 객체를 생성할 때 호출한다. 요청 매개변수의 값으로부터 String이나 Date 등의 기본 타입 객체를 생성한다.

Member 클래스처럼 dataType이 기본 타입이 아닌 경우는 요청 매개변수의 이름과 일치하는 셋터 메서드를 찾아서 호출한다. 먼저 요청 매개변수의 이름 목록을 얻는다.

Set<String> paramNames = request.getParameterMap().keySet(); //매개변수 키목록

request.getParameterMap() 메서드는 매개변수의 이름과 값을 맵 객체에 담아서 반환한다. 필요한 것은 매개변수의 이름이기 때문에 Map의 keySet() 메서드를 호출하여 이름 목록만 꺼낸다.

그리고 값을 저장할 객체를 생성한다. Class의 newInstance() 메서드를 사용하면 해당 클래스의 인스턴스를 얻을 수 있다. new 연산자를 사용하지 않고도 이런 식으로 객체를 생성할 수 있다.

Object dataObject = dataType.newInstance(); //해당 클래스의 인스턴스 얻는다

요청 매개변수의 이름 목록이 준비되었으면 for 반복문을 실행한다.

for(String paramName : paramNames) {
    m = findSetter(dataType, paramName);
    if(m != null) {
        //dataObject에 대해 m메서드 호출 - createValueObject()로 객체생성한 매개변수 가지고 셋터 메서드 실행
        //createValueObject(셋터 메서드의 매개변수 타입(매개변수 목록 배열로 반환), 요청 매개변수 타입)
        m.invoke(dataObject, createValueObject(m.getParameterTypes()[0], request.getParameter(paramName)));
    }
}

데이터 타입 클래스에서 매개변수 이름과 일치하는 프로퍼티(셋터 메서드)를 찾는다.

m = findSetter(dataType, paramName);

findSetter() 메서드는 내부에 선언된 메서드이다. 데이터 타입(Class)과 매개변수 이름(String)을 주면 셋터 메서드를 찾아서 반환한다. 셋터 메서드를 찾았으면 이전에 생성한 dataObject에 대해 호출한다.

if(m != null) {
    //dataObject에 대해 m메서드 호출 - createValueObject()로 객체생성한 매개변수 가지고 셋터 메서드 실행
    //createValueObject(셋터 메서드의 매개변수 타입(매개변수 목록 배열로 반환), 요청 매개변수 타입)
    m.invoke(dataObject, createValueObject(m.getParameterTypes()[0], request.getParameter(paramName)));
}

셋터 메서드를 호출할 때 요청 매개변수의 값을 그 형식에 맞추어 넘긴다.

createValueObject() 메서드는 앞에서 설명한 바와 같이, 요청 매개변수의 값을 가지고 기본 타입의 객체를 만들어 준다.

이렇게 요청 매개변수의 개수만큼 반복하면서, 데이터 객체(예 : Member)에 대해 값을 할당한다.

 

- isPrimitiveType() 메서드

isPrimitiveType() 메서드는 매개변수로 주어진 타입이 기본 타입인지 검사하는 메서드이다. 조건문을 이용하여 데이터 타입이 int인지 아니면 Integer 클래스인지 등을 검사한다.

if(type.getName().equals("int") || type == Integer.class
|| type.getName().equals("long") || type == Long.class
|| type.getName().equals("float") || type == Float.class
|| type.getName().equals("double") || type == Double.class
|| type.getName().equals("boolean") || type == Boolean.class
|| type == Date.class || type == String.class) {
    return true;
}

다만, 여기서는 int, long, float, double, boolean, Date, String에 대해서만 기본 타입으로 간주하도록 하였다. byte와 short도 포함하고 싶다면 조건문을 추가하면 된다.

 

- createValueObject() 메서드

기본 타입의 경우 셋터 메서드가 없기 때문에 값을 할당할 수 없다. 보통 생성자를 호출할 때 값을 할당한다. 그래서 createValueObject() 메서드를 만든 것이다. 이 메서드는 셋터로 값을 할당할 수 없는 기본 타입에 대해 객체를 생성하는 메서드이다.

if(type.getName().equals("int") || type == Integer.class) {
    return new Integer(value);
}

 

- findSetter() 메서드

findSetter() 메서드는 클래스(type)를 조사하여 주어진 이름(name)과 일치하는 셋터 메서드를 찾는다.

//데이터 타입(Class)과 매개변수 이름(String)을 주면 셋터 메서드 찾아서 반환
private static Method findSetter(Class<?> type, String name) {
    ...
}

제일 먼저 데이터 타입에서 메서드 목록을 얻는다.

Method[] methods = type.getMethods();

메서드 목록을 반복하여 셋터 메서드에 대해서만 작업을 수행한다. 만약 메서드 이름이 "set"으로 시작하지 않는다면 무시한다.

for(Method m : methods) {
    if(!m.getName().startsWith("set")) continue; //메서드 이름이 set으로 시작하지 않으면 넘김
    ...
}

셋터 메서드일 경우 요청 매개변수의 이름과 일치하는지 검사한다. 단 대소문자를 구분하지 않기 위해 모두 소문자로 바꾼 다음에 비교한다. 그리고 셋터 메서드의 이름에서 "set"은 제외한다.

propName = m.getName().substring(3); //index값 0~2까지 제외한 값 가져옴
if(propName.toLowerCase().equals(name.toLowerCase())) {//toLowerCase() : 대상 문자열을 모두 소문자로 변환
    return m;
}

일치하는 셋터 메서드를 찾았다면 즉시 반환한다.

 

마지막으로 spms.servlets.DispatcherServlet 클래스의 service() 메서드 상단에

request.setCharacterEncoding("UTF-8");

코드를 추가하여 한글이 깨짐을 방지했다.

 

이제 모든 준비가 끝났다. 톰캣 서버를 재시작시킨 후, 모든 기능을 테스트 한다. 이전과 같이 정상적으로 동작한다.

이렇게 프로그램이라는 것은 똑같이 실행되더라도 내부에 어떤 구조로 되어 있느냐에 따라 유지 보수의 수준이 달라진다. 이번 작업을 통해 페이지 컨트롤러가 추가될 때마다 프런트 컨트롤러를 변경해야 하는 문제가 해결되었다. 내부 구조는 좀 더 복잡해졌지만, 유지 보수는 훨씬 쉬워졌다.

 

6. 리플랙션 API

이번 절에서 최고의 수훈자는 리플랙션 API이다. 이 도구가 없다면 클래스에 어떤 메서드가 있는지, 메서드의 이름은 무엇인지, 클래스의 이름은 무엇인지 알 수가 없다. '리플랙션(Reflection)'의 한글 뜻을 보면, "어떤 것에 대한 설명 또는 묘사", "거울 등에 비친 모습"이다. 즉 클래스나 메서드의 내부 구조를 들여다 볼 때 사용하는 도구라는 뜻이다. 이번 절에서 사용한 리플랙션 API를 정리해 보면 다음 표와 같다.

메서드 설명
Class.newInstance() 주어진 클래스의 인스턴스를 생성
Class.getName() 클래스의 이름을 반환
Class.getMethods() 클래스에 선언된 모든 public 메서드의 목록을 배열로 반환
Method.invoke() 해당 메서드를 호출
Method.getParameterTypes() 메서드의 매개변수 목록을 배열로 반환

 

이번 절의 모든 내용과 예제 코드는 실무 개발의 기초이다. 지금 만드는 것은 스프링 프레임워크의 미니 버전이다. 스프링 프레임워크를 배울 때 친숙하라고 클래스 이름이나 메서드 이름들도 스프링 프레임워크와 비슷하게 작성하고 있다. 이번 절을 제대로 이해한다면 스프링 프레임워크의 내부 구조도 자연스레 이해할 수 있다.

실무 개발자들 중에도 이런 것을 제대로 이해하지 못하고 프레임워크를 사용하는 경우가 많다. 프레임워크를 쓰더라도 어떻게 동작하는지 알고 써야 제대로 사용할 수 있다. 지금 교재의 모든 내용은 실무 개발의 핵심 중에 필수 내용만 정리한 것이므로, 모든 내용을 빠뜨리지 말고 공부한다.

 

참고도서 : https://freelec.co.kr/book/1674/

 

[열혈강의] 자바 웹 개발 워크북

[열혈강의] 자바 웹 개발 워크북

freelec.co.kr

댓글