본문 바로가기

프로그래밍/Spring

Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (5) - HandlerMethodArgumentResolver 분석(1)

저번 글에서는 Spring Controller의 메소드 예시를 통해 HandlerMethodArgumentResolver가 하게 되는 역할을 보게 되었다. 이전 글 예시를 보면 Spring Controller의 메소드 파라미터에 특정 어노테이션을 사용하는것만으로도 우리가 기존에 사용하던 방법을 그대로 사용할 수 있다. 저번글이 이번 글에서 하고자 하는 최종적인 목표를 얘기한거라면 이번 글에서는 그 목표를 이루기 위한 과정에 대해 알아보는 시간을 가져보도록 하겠다. 이번 연재글에서 하고자 하는 말들의 시작이라 보면 되겠다. 지금부터 시작하도록 하겠다.

 

먼저 HandlerMethodArgumentResovler가 하는 역할에 대한 얘기를 잠깐 해보도록 하겠다. 눈치 빠른 사람이면 이전 글을 통해 이 클래스가 하는 역할을 짐작했을수도 있겠지만 이 클래스의 역할은 Controller의 메소드에서 사용중인 특정 타입의 변수에 값을 할당해주는 역할을 진행한다. 이 값을 설정하는 기준은 HandlerMethodArgumentResolver 클래스에서 자신이 정하면 된다. 흔히 Web Client는 Http 통신을 이용하여 정보를 전달할텐데 그 정보는 url에 있을수도 있고, header에 있을수도 있고, body에 있을수도 있다. 이 시점에서 서버 개발자가 알고 있는 것은 원하는 정보가 어디에 있고 어느 key 값을 통해 가져올수 있다는 것은 알고 있다. Http 통신을 이용하여 넘어오는 정보는 서버에서는 HttpServletRequest 객체에 담겨져 있기 때문에 서버 개발자는 이 객체를 이용해서 원하는 정보를 가져오게 된다. HandlerMethodArgumentResolver 클래스가 어떤 기능을 하는지는 알았으니 좀더 구체적으로 파보도록 하겠다. 엄밀하게 말하면 HandlerMethodArgumentResolver는 클래스가 아니라 인터페이스이다. 그래서 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스를 만들어야 한다. 그래서 이 인터페이스를 구현한 XplatformArgumentResolver 클래스를 만들어서 Xplatform 에서 전송한 데이터를 변환하는 기능을 하게 된다. 그러면 이 HandlerMethodArgumentResovler 인터페이스에서 정의한 2개의 메소드에 대해 알아보도록 하겠다. 인터페이스의 메소드는 정의만 있는 것이지 이에 대한 구체적인 구현은 이 인터페이스를 구현한 클래스에서 해줘야 하는 것을 알고 있길 바란다(즉 구현은 개발자의 몫이라는 얘기!)

 

● public boolean supportsParameter(MethodParameter parameter)

 

이 메소드는 현재 HandlerMethodArgumentResovler 를 구현한 클래스가 Controller의 메소드에 정의된 파라미터를 지원하는지의 여부를 결정하여 지원할 경우 true, 그렇지 않으면 false를 return 하게 해주면 된다. 예를 들어 Controller의 메소드가 다음과 같이 되어 있다면

 

public String list(@RequestParam(value="idx") int idx, @RequestParam(value="search") String search)

 

supportsParameter 메소드의 파라미터로 넘어오는 MethodParameter 객체는 @RequestParam(value="idx") int idx 로 정의된 Controller 메소드의 파라미터 정보가 들어가 있는 객체이다. 예를 들어 MethodParameter 객체를 통해 Controller 메소드의 파라미터에 붙어있는 Annotation 이 무엇인지, 클래스 타입이 무엇인지등을 알 수 있게 된다. 방금 얘기했던 list 메소드에서는 파라미터가 2개(int idx, String search)가 있기 때문에 supportsParameter 는 2번 실행이 된다. 이 파라미터 정보를 이용해서 이 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스를 이용할 수 있는지를 파악하여 그에 맞춰 true/false를 return 해주면 된다.

 

supportsParameter 메소드의 기능 설명을 지금까지 들었는데 우리는 여기서 한가지 고민을 해야 하는 부분이 있다. Spring MVC를 이용해서 Web을 구현할 경우 HandlerMethodArgumentResovler 구현 클래스는 여러개가 등록될 수 있다. 일단 Spring이 기본적으로 만들어놓은 것들이 있어서 설정에 따라 그런 것들이 등록되고, 또 지금 우리가 만들 XplatformArgumentResolver 클래스와 같이 사용자의 목적에 따라 만들어진 클래스도 여러개가 등록될 수 있다. 그러나 Contoller 메소드의 파라미터에 값이 부여되는 것은 오직 1개만 되어야 한다. 예를 들어 단순하게 int idx 로 파라미터를 넘기게 되면 Spring에서 자체적으로 만든 HandlerMethodArgumentResolver 구현 클래스도 이에 대한 해석이 가능하고, 우리가 만든 HandlerMethodArgumentResolver 구현 클래스도 가능한 상황이 올 수 있다. 이런 상황은 피해야 한다. 파라미터 1개의 정보를 2개 이상의 구현 클래스가 해석할 수 있다는건 이 파라미터에 대한 값은 이렇게 될 수도 있고 저렇게 될 수도 있다는 의미가 된다. 때문에 이런것에 대해 좀더 명확한 구분을 주기 위해 사용하는 것이 파라미터에 특정 HandlerMethodArgumentResolver 구현 클래스가 해석할 수 있는 어노테이션을 붙여주는 것이다. 예를 들어 위에서 언급한 @RequestParam 어노테이션의 경우 이 어노테이션이 있는지를 체크해서 그게 있을 경우 구체적인 기능을 수행하는 식의 HandlerMethodArgumentResovler 구현 클래스를 만든다면 중복해석에 대한 우려는 피할 수 있을 것이다.  이 내용을 기억해두고 XplatformArgumentResolver 클래스의 supportParameter 메소드 소스를 잠깐 보도록 하자

 

public boolean supportsParameter(MethodParameter parameter) {
	// TODO Auto-generated method stub
	boolean result = false;
	if(parameter.hasParameterAnnotation(RequestDataSetList.class)){
		result = true;
	}else if(parameter.hasParameterAnnotation(RequestDataSet.class)){
		result = true;
	}else if(parameter.hasParameterAnnotation(RequestVariable.class)){
		result = true;
	}

	return result;
}

 

이 supportsParameter 메소드 소스는 단순하다. 파라미터 정보에서 우리가 이전에서 언급한 어노테이션인 RequestDataSetList, RequestDataSet, RequestVariable 이 존재하면 true로 설정해서 return 하도록 되어 있다. 이 어노테이션들은 우리가 사용할 목적으로 만든 어노테이션이기 때문에 Spring에서 이 어노테이션을 이용할 일도 없으므로 우리가 만든 HandlerMethodArgumentResolver 구현 클래스 외의 다른 구현 클래스가 이용할 상황이 없다. 그래서 이러한 목적으로 어노테이션을 효율적으로 이용할 수 있다.

 

● public Object resolveArgument(MethodParameter parameter

, ModelAndViewContainer mavContainer

, NativeWebRequest webRequest

, WebDataBinderFactory binderFactory) throws Exception

 

HandlerMethodArgumetResolver의 가장 핵심이 되는 메소드이다. 이 메소드에서 HttpServletRequest를 우리가 원하는 데이터 타입으로 변환하여 return 하게 된다. MethodParameter 클래스 객체는 supportsParameter 메소드 설명시에 언급했듯이 Controller 메소드의 파라미터 정보가 넘어오게 된다. ModelAndViewContainer 클래스 객체는 우리가 작업을 하는 과정에서 Controller에서 사용할 Model 과 View에 대한 설정을 해야 할 경우 이 객체를 통하여 작업하게 된다. NativeWebRequest 클래스 객체는 Client에서 넘어온 Web Request 객체를 받는다. 이것은 일종의 Wrapping 클래스 객체여서 이것을 바로 이용할 수는 없다. 그래서 나중에 언급되겠지만 이 클래스 객체에서 Client 에서 넘어온 HttpServletRequest 객체로 변환해서 사용해야 한다. WebDataBinderFactory 클래스 객체는 Web Request를 통해 넘어온 파라미터들을 Java Bean으로 변환하는데 사용되는 DataBinder를 생성하는 객체이다. Controller 메소드의 파라미터 정보와 Client Web Request가 넘어오기 때문에 파라미터 정보에서 명시한 내용을 통해 원하는 클래스 객체를 만들수가 있고 부가적으로 Model과 View에 대한 작업을 하거나 Data Binding과 관련된 작업을 할 수가 있다.

 

이 클래스에 대한 전체적인 소스는 위에서 링크를 걸어놨기 때문에 링크를 통해 보면 되고 지금부터는 부분부분을 언급하면서 코드에 대한 설명을 하도록 하겠다.

 

Class<?> type = parameter.getParameterType();
Annotation[] annotations = parameter.getParameterAnnotations();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);

DataSetList dataSetList = null;
VariableList variableList = null;

if(request.getAttribute(DATASETLIST_NAME) == null || request.getAttribute(VARIABLELIST_NAME) == null) {
	PlatformRequest platformRequest = new HttpPlatformRequest(request.getInputStream());
	platformRequest.receiveData();
	PlatformData platformData = platformRequest.getData();
	dataSetList = platformData.getDataSetList();
	variableList = platformData.getVariableList();

	request.setAttribute(DATASETLIST_NAME, dataSetList);
	request.setAttribute(VARIABLELIST_NAME, variableList);
} else {
	dataSetList = (DataSetList)request.getAttribute(DATASETLIST_NAME);
	variableList = (VariableList)request.getAttribute(VARIABLELIST_NAME);
}

 

처음의 두 줄은 Controller 메소드의 파라미터 정보를 통해서 해당 파라미터의 클래스 타입과 파라미터에 붙어있는 어노테이션들을 얻어온다. 파라미터에서는 여러개의 어노테이션이 붙을 수 있기 때문에 배열로 받아야 한다. 그리고 세번째 라인에 있는 것이 Web Client에서 전송한 HttpServletRequest 객체를 받는 작업이 된다. 이때 주의할 것이 있다. 여기서 받게되는 것은 순수한 HttpServletRequest 객체가 아니다. 예전 글에서 언급했던 별도의 filter를 등록하여 HttpServletRequest 클래스를 감싸는 클래스인 HttpRequestWrapper 클래스를 받게 된다(그러나 여기서는 설명의 원할함을 위해 request 객체라고 표현하겠다). 이 부분을 착오하지 말기를 바라는 마음에서 상기시켰다. 왜 상기시켰는지는 조금이따가 언급하겠다.

 

Xplatform의 DataSetList 클래스 객체와 VariableList 클래스 객체를 null로 설정한뒤에 if문에서 request 객체에서 DATASETLIST_NAME과 VARIABLELIST_NAME 변수값으로 지정된 데이터가 존재하는지를 확인하는 작업을 하고 있다. 이것을 하게된데에는 이유가 있다. Xplatform에서 전송되는 데이터는 XML 형태를 가지고 있다. 만약 전송되는 데이터 양이 작을 경우에는 이 XML을 파심하여 DataSetList 클래스 객체와 VariableList 클래스 객체에 설정하는데 오랜 시간이 걸리진 않을 것이다. 그러나 데이터 양이 크면 클수록 이것을 파싱해서 변환하는데는 시간이 오래 걸린다. 더군다나 이러한 파싱 작업은 반복적으로 발생한다. 무슨 뜻이냐면 예전 글에서 이 HandlerMethodArgumentResolver는 해당 Resolver를 만족하는 파라미터 갯수만큼 실행이 된다고 언급했었다. 만약 Controller의 메소드에서 이 Resolver를 만족하는파라미터 갯수가 5개가 있으면 5번이 실행이 된다. 이 시점에서 Xplatform에서 전송한 데이터 양이 많을 경우 같은 XML을 파싱하는 작업이 5번이나 발생하는 상황이 온다. 동일한 request에서는 전송되는 데이터가 변화하는게 없는데도 불구하고 여러번 XML을 파싱하는 상황이 벌어지므로 처리시간이 오래 걸릴수 있다. 그래서 동일한 request 에서는 처음 XML을 파싱하면서 DataSetList 클래스 객체와 VariableList 클래스 객체를 만든 뒤에 이를 request에 저장해놓음으로써 차후에 다른 파라미터에서 이를 처리하려 할때 반복적으로 파싱하지 말고 기존 request에 저장해놓은 것을 가지고 재활용하는 형태로 작업하도록 했다. 그래서 if문은 이러한 의도로 작성했음을 알아두었음 한다. 설명시엔 XML 파싱이란 단어를 사용했지만 그 역할을 하는 것이 PlatformRequest 클래스 객체가 만들어지는 과정이 그 데이터를 파싱해서 만들어지는 것이기 때문에 이렇게 설명했다.

 

그리고 한가지 참고할 것이 있는데 PlatformRequest platformRequest = new HttpPlatformRequest(request.getInputStream()) 이 코드에서 request.getInputStream() 을 통해 넘어오는 것은 HttpRequestWrapper 클래스의 멤버변수인 byte 배열인 bodyData를 InputStream 형태의 객체로 만들어서 return 되는 것이다. bodyData 변수가 HttpServletRequest의 InputStream을 byte 배열로 변환한것이기 때문에 InputStream에 대한 재활용도 같이 하고 있다. HttpServletRequest 객체의 InputStream이 넘어오는 것이 아님을 알아두었음 한다. 이제 다음의 부분을 보자.

 

for(Annotation annotation : annotations){
	Class<? extends Annotation< annotationClass = annotation.annotationType();

	if(annotationClass.equals(RequestDataSetList.class)){		
		if(type.equals(DataSetList.class)) {
			result = dataSetList;
		}else{
			result = WebArgumentResolver.UNRESOLVED;
		}
	} else if(annotationClass.equals(RequestDataSet.class)){	
		...
	} else if(annotationClass.equals(RequestVariable.class)) {
		...
	}

}

return result;

 

위에서 구했던 Controler 메소드 파라미터에 붙어 있는 어노테이션들을 for 문을 이용해서 반복적으로 체크하면서 우리가 작업하고자 하는 어노테이션이 붙어있는지를 체크하게 된다. 예전 글에서 우리가작업하게 될 대상 어노테이션은 @RequestDataSetList, @RequestDataSet, @RequestVariable 이 3개였다. 그래서 이 3개의 어노테이션이 붙어있는지를 체크해서 해당 어노테이션이 붙어 있으면 그에 맞는 작업을 하게 된다. 여기서는 if 문 안의 내용은 다음 글부터 이 부분에 대한 상세내용을 설명할 것이기 때문에 지금은 ...으로 생략했다. 그러나 @RequestDataSetList 어노테이션에 대해서는 코드 양이 많지 않아서 지금 설명하도록 하겠다. @RequestDataSetList 어노테이션이 파라미터에 있으면 위에서 알아놨던 Controller 메소드 파리미터의 클래스 타입이 DataSetList 클래스 타입인지 확인해서 DataSetList 타입이면 위에서 request 객체의 XML을 파싱하여 만든 dataSetList 변수를 result로 설정해둔다. 그래서 if문을 나가게 되면 result가 return이 된다. 그러나 DataSetList 클래스 타입이 아니면 @RequestDataSetList 어노테이션이 처리할 수 없는 클래스 타입이기 때문에 result에WebArgumentResolver.UNRESOLVED 를 주어 이 파라미터는 처리할 수 없다고 해준다.

 

이번 글에서는 HandlerMethodArgumentResovler 인터페이스에서 제공되는 메소드에 대한 설명과 그 메소드 중 핵심이 되는 메소드인 resolveArgument 메소드의 구현 내용을 살펴보았다. 다음 글에서는 @RequestDataSet 어노테이션에 대한 처리에 대해 얘기하도록 하겠다.

 

 1. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (1) - 개요

 2. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (2) - 사전지식

 3. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (3) - HttpServletRequestWrapper

 4. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (4) - Spring Controller에서 하려는 것

 5. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (5) - HandlerMethodArgumentResolver 분석(1)

 6. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (6) - HandlerMethodArgumentResolver 분석(2)

 7. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (7) - HandlerMethodArgumentResolver 분석(3)

 8. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (8) - HandlerMethodArgumentResolver 분석(4)

 9. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (9) - XplatformView 분석

 10. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (10) - 예외처리