프로그래밍/Spring 검색 결과

51개 발견
  1. 미리보기
    2018.11.05

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

  2. 미리보기
    2018.11.05

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

  3. 미리보기
    2018.11.05

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

  4. 미리보기
    2018.11.05

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

  5. 미리보기
    2018.11.05

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

  6. 미리보기
    2018.11.05

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

  7. 미리보기
    2018.11.05

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

  8. 미리보기
    2018.11.05

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

이번 글은 이 연재의 마지막으로 해당 템플릿에서의 예외처리에 대해 설명하도록 하겠다. 우리가 만든 Spring Application에서는 이런저런 이유로 예외가 발생할 수 있다. 비즈니스적인 로직으로 인해 발생하는 예외도 있고 Runtime 예외도 있다. 예외가 발생할 경우 try-catch를 이용해서 발생시점에서 해당 예외에 대한 처리를 할 수 있지만 비즈니스적으로 이러한 예외처리를 해야 하는게 아니라면 발생된 예외는 호출한 곳으로 계속 던져지게 된다. 단계별로 예외가 계속 던져지게 되면 최종적으로는 Controller 까지 도달하게 되며 Controller에서도 예외 처리를 안하게 되면 Controller 에서는 다시 예외를 던져서 Spring에서 정해져있는 ExceptionResolver 객체에서 이를 처리하게 된다. ExceptionResolver 객체에는 처리해야 할 예외 클래스와 이 예외를 보여줘야 할 View를 정해주기 때문에 해당 예외에 대한 출력이 가능해진다. 그러나 여기서는 ExceptionResolver 객체를 별도로 만들어 등록하지 않고 @ControllerAdvice 와 @ExceptionHandler 를 이용해서 예외를 처리하게 된다. 다음은 이러한 예외를 처리하는 GlobalExceptionHandler 클래스이다.


@ControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(value={DataAccessException.class})
	public XplatformView processDataAccessException(DataAccessException ex){
		XplatformView xplatformView = new XplatformView("30", ex.getMessage());
		return xplatformView;
	}

	@ExceptionHandler(value={Exception.class})
	public ModelAndView processException(Exception ex){
		ModelAndView modelAndView = new ModelAndView();
		modelAndView.addObject("ErrorCode", "156");
		modelAndView.addObject("ErrorMsg", ex.getMessage());
		modelAndView.setViewName("errorView");
		return modelAndView;
	}
}


코드를 보면 클래스 레벨에 @ControllerAdvice 어노테이션을 붙이고 메소드 레벨에 @ExceptionHandler 어노테이션을 붙이고 있다. @ExceptionHandler 어노테이션에 처리해야 할 예외를 정의하게 되는데 예외를 하나의 메소드에서 여러개를 처리해야 할 경우 배열 형태로 정의해서 설정할 수 있다(코드에서도 보면 예외 클래스를 1개만 설정하고 있어도 표현하는 방법은 배열 형태의 {}를 사용해서 설정하고 있다) 세부 코드를 보면 예외를 설정해서 View로 표현하는 방법이 2가지의 방법으로 구현되고 있다. 하나는 이전 글에서 설명했던 글인 XplatformView 클래스 객체를 생성해서 이를 return 하는 방법이 있고, 다른 하나는 우리가 늘 익숙했던 코드인 ModelAndView 클래스 객체를 생성해서 여기에 ErrorCode와 ErrorMsg 란 이름으로 값을 설정한 뒤 이 객체를 return 하고 있다. 결과만 얘기하면 둘다 동일한 형태의 결과를 보여준다. 예전 글에서 XplatformView 클래스 코드에 대해 설명할 때 다음의 내용을 설명한 것이 있다.


if(!model.containsKey("ErrorCode")) {
	variableList.add("ErrorCode", ERROR_CODE_VALUE);
}

if(!model.containsKey("ErrorMsg")) {
	variableList.add("ErrorMsg", ERROR_MSG_VALUE);
}


XplatformView 코드를 보면 ERROR_CODE_VALUE와 ERROR_MSG_VALUE 멤버변수가 있고 이를 설정하는 생성자도 있다. GlobalExceptionHandler 클래스의 processDataAccessException 메소드는 XplatformView 생성자를 통해 이 멤버변수를 설정했고 위의 코드를 실행하는 시점에 model에 ErrorCode와 ErrorMsg 가 없을 경우 model에 ErrorCode와 ErrorMsg를 각각 key로 주어서 이 멤버변수 값이 들어가게 된다. 이러한 방법으로 processDataAccessException 메소드가 해당 예외에 대한 코드와 메시지를 설정하게 된다. 그러나 processDataAccessException 메소드는 model에 ErrorCode와 ErrorMsg를 설정한 상태에서 XplatformView에 전달되기 때문에 위의 코드가 실행이 되지 않는다. 만약 XPlatformView 생성자에서 에러코드와 에러메시지를 설정하고 model에도 ErrorCode와 ErrorMsg가 설정된 상태로 XplatformView에 전달되면 model에 있는 것으로 전달이 된다. 여기서는 예시로 DB 관련 예외인 DataAccessException 과 Exception 2개만 예로 들었지만 예외와 그 처리 방법에 따라 메소드를 늘려가며 작업하면 된다.


지금까지 Spring과 Xplatform을 연동한 템플릿에 대한 설명을 진행했다. 궁금한 내용이 있으면 댓글로 달아주길 바란다. 그리고 이것은 모든 상황에 대응할 수 있는 템플릿은 아니다. 이 템플릿은 Xplatform에서 제공하는 샘플 예제로 만들어놓은 템플릿이기 때문에 아주 기본적인 상황에서는 대응할 수 있지만 그거보다 확장된 상황에서는 대응할 수가 없다(내가 받은 Xplatform 예제엔 파일 업로드 기능이 없어서 이에 대한 내용을 템플릿에 구현하지를 못했다. 지인에세 물어본바로는 Xplatform도 Java단에서 MultipartRequest 로 받는다고 하기 때문에 XplatformArgumentResolver에서 MultipartRequest로 받아서 진행하면 될 것으로 생각한다) 그러니 적용시엔 커스터마이징 할 것을 생각하고 진행하길 바란다.


 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다

이전 글까지는 Xplatform에서 넘어온 DataSetList과 VariableList를 Controller의 메소드 파라미터에서 자바 객체로 변환해주는 HandlerArgumentResolver 인터페이스 구현 클래스에 대해 설명했다. 지금까지의 내용이 Xplatform에서 넘어온 데이터들을 Spring에서 사용하기 위해 적절하게 데이터를 변환한 작업이었고 지금부터 설명한 내용은 Spring에서 나온 데이터를 Xplatform에서 사용가능한 데이터로 변환하는 작업에 대해 설명하고자 한다. 이해를 돕기 위해 설명하자면 자바에서 제공하는 List 객체로 나온 결과물을 Xplatform에서 제공하는 DataSet 클래스 객체로 변환하는 것이다. 이 부분에서도 또한 Java의 Reflection 기능을 사용해서 변환하게 된다. 이전 글들에서 예기했던 DataSet 클래스 객체를 Java의 Collection 인터페이스 구현 객체로 변환할때 했던 설명들의 역방향이라고 생각하면 된다. 결과물이 List<Map<String, Object>> 객체일 경우 List 객체에 들어있는 Map 객체 갯수만큼 loop를 돌면서 Map 객체 하나하나를 DataSet의 레코드로 변환하는 것이다. Map의 key를 레코드의 컬럼 이름으로 설정해서 레코드를 만들게 된다. Map 객체가 아닌 POJO 스타일 Java 클래스 객체일 경우 클래스의 멤버변수를 레코드의 컬럼 이름으로 설정해서 해당 멤버변수의 값을 레코드의 컬럼값으로 넣게 된다. 이러한 개념을 알고 구체적인 내용을 보도록 하자.


먼저 현재의 상황에서는 Spring에서 Contoller를 통해 결과물을 출력할때 2가지의 클래스가 필요하다. 


  1. View 인터페이스를 구현한 클래스
  2. 1번의 클래스 객체를 생성하는 ViewResolver 인터페이스를 구현한 클래스


모든 상황이 반드시 이 2가지 클래스가 필요한 것은 아니다. Rest 방식의 경우는 HttpServletRequest의 Header 값을 통해 넘어오는 파라미터의 타입을 알아내어 그에 맞는 HttpMessageConverter를 이용해서 파라미터를 Java 클래스 객체로 변환하거나 또는 Java 클래스 객체를 해당 타입의 값으로 변환하기도 한다. 그래서 위에서 언급할때 현재의 상황 이란 단어를 사용했다. Xplatform의 경우는 XML로 전송하기 때문에 XML 관련 HttpMessageConverter를 이용해 객체를 만들거나 또는 객체를 XML로 변환할 수 있겠지만 이미 투비소프트에서 자신들이 정의한 DataSetList나 VariableList 등의 클래스로 변환을 해서 return 해주거나 또는 그 반대의 기능을 수행하는 PllatformRequest와 PlatformResponse 클래스를 제공하고 있기 때문에 굳이 HttpMessageConverter를 구현한 클래스를 별도로 만들 필요가 없다. 물론 구현에 대해 말리지는 않겠으나 투비소프트에서 정의한 XML에 대한 분석이 필요하기 때문에 이러한 쓸데없는 노가다를 피할려고 PlatformRequest나 PlatformResponse를 사용했다


Spring에서는 View인터페이스와 ViewResolver 인터페이스를 구현한 여러 클래스들을 제공하고 있다. 이러한 클래스는 추상클래스와 그렇지 않은 클래스들이 혼재되어 있는데 이 클래스들중에서 본인 용도에 맞는 클래스가 있으면 그걸 사용하면 되고 그런 클래스가 없을 경우 제공되는 추상클래스에서 상속받아 이를 구현하거나 추상클래스 상속받아서 해결될 문제가 아니면 해당 인터페이스를 구현한 클래스를 직접 만들어서 사용하면 된다. 여기서는 후자의 방법으로 진행했다(어떠한 클래스들이 있는지 궁금한 사람은 Spring API 문서의 이 두 인터페이스 관련 내용을 보면 알 수 있다.)


그러면 먼저 ViewResolver 인터페이스를 구현한 클래스 코드를 보도록 하자


public class XplatformViewResolver implements ViewResolver, Ordered {

	private int order = Ordered.LOWEST_PRECEDENCE;

	@Override
	public View resolveViewName(String viewName, Locale locale) throws Exception {
		// TODO Auto-generated method stub
		XplatformView xplatformView = new XplatformView();
		return xplatformView;
	}

	@Override
	public int getOrder() {
		// TODO Auto-generated method stub
		return order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

}


이 클래스 소스를 보면 2개의 인터페이스를 구현하고 있는데 하나는 위에서 말한 ViewResolver 인터페이스이고 또 하나는 Ordered 인터페이스이다. Ordered 인터페이스를 구현한 이유는 좀 이따가 설명하기로 하고 ViewResolver 인터페이스에 대해 설명하도록 하겠다. ViewResolver 인터페이스는 1개의 메소드를 제공하는데 View 인터페이스를 구현한 클래스 객체를 return 해주는 resolveViewName 메소드이다. 이 메소드가 하는 일은 단순하다. view 이름과 locale 정보를 받아서 데이터를 보여주는 View 객체를 return 해주면 된다. 즉 우리가 클라이언트에 전달할 데이터를 담은 View 객체를 여기서 만들어서 return 해주면 된다. 그래서 코드도 단순하다. View 인터페이스를 구현한 클래스인 XplatformView 클래스 객체를 만들어서 return 해주기만 하면 된다. 그리고 Ordered 인터페이스를 구현한 이유는 ViewResolver 인터페이스를 구현한 클래스가 여러개일 경우 우선순위를 설정해야 하는데 이것을 하기 위해 Ordered 인터페이스를 구현했다. Spring에서는 ViewResolver를 여러개 설정할 수 있다. 우리가 표현할 데이터를 여러 다양한 클라이언트를 통해 표현해야 하기 때문에 해당 클라이언트에 따른 ViewResolver를 구현하므로 여러개를 설정할 수 있게 된다. 이때 어떤것을 가장 먼저 적용해야 하는지 우선순위를 정할 수 있다. 이를 위해서 Ordered 인터페이스를 구현했다. 이 인터페이스에서 제공하는 메소드는 getOrder 메소드 하나뿐이 없다. getOrder 메소드에서 설정된 우선순위 값을 return 해주면 된다.


ViewResolver는 View를 만드는 단순한 작업만 하기 때문에 따로 설명할 내용은 없다. Java 객체를 Xplatform에서 사용하도록 만드는 핵심 기능은 View에 있다. 그럼 위에서 보여준 클래스 코드인 XplatformViewResovler 클래스가 만들어주는 View인 XplatformView 클래스를 살펴보도록 하자. 대강의 윤곽은 다음과 같다.


public class XplatformView implements View {

	/**
	 * Xplatform의 작업결과가 성공적이었을때의 ErrorCode 값을 설정한다
	 */
	private final String ERROR_CODE_VALUE;

	/**
	 * Xplatform의 작업결과가 성공적이었을때의 ErrorMsg 값을 설정한다
	 */
	private final String ERROR_MSG_VALUE;

	public XplatformView() {
		this.ERROR_CODE_VALUE = "0";
		this.ERROR_MSG_VALUE = "";
	}

	public XplatformView(String errorCodeValue, String errorMsgValue) {
		this.ERROR_CODE_VALUE = errorCodeValue;
		this.ERROR_MSG_VALUE = errorMsgValue;
	}

	@Override
	public String getContentType() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// TODO Auto-generated method stub
		String contentType = request.getHeader("Content-Type").startsWith("text/xml;") ? XplatformConstants.CONTENT_TYPE_XML
				: request.getHeader("Content-Type");

    	if(contentType == XplatformConstants.CONTENT_TYPE_XML) {
    		...
    	} else if(contentType == XplatformConstants.CONTENT_TYPE_CSV) {

    	}
	}

	...

}


위에서 보여주는 코드는 XplatformView 클래스의 전체 코드는 아니다. 설명에 필요한 부분만 남긴 것이며 설명할때마다 관련 코드는 그때그때 추가로 보여주도록 하겠다. 일단 이 클래스 코드를 보면 멤버변수로 ERROR_CODE_VALUE와 ERROR_MSG_VALUE가 있다. 이것은 Spring에서 Xplatform 관련 작업을 하면서 문제가 발생하여 예외가 발생했을 경우 이를 처리하기 위한 변수이다. ERROR_CODE_VALUE에는 에러 Code를 설정하고 ERROR_MSG_VALUE는 에러 메시지를 설정하게 된다. 이 변수들에 대한 설정은 생성자에서 하고 있다. 그러나 예외가 발생한 일이 없어서 작업 자체가 성공적으로 수행이 되면 파라미터가 없는 생성자를 이용해서 View를 생성함으로써 관련 코드와 메시지를 설정하고 있다. 이것에 대한 내용은 나중에 예외 관련 처리 글에 대한 설명에서 좀더 자세히 다루겠다. 지금은 이 변수들의 의미만 알아두고 넘어가자.


View 인터페이스의 핵심 메소드는 render 메소드이다. render 메소드에서는 3개의 파라미터를 받게 되는데 첫번째는 Controller의 메소드에서 설정한 Model 객체이고 두번째는 Controller의 메소드가 처리하고 있는 HttpServletRequest 객체이며, 세번째는 Controller의 메소드를 통해 출력하게 되는 HttpServletResponse 객체가 넘어오게 된다. 


처음으로 하는 작업은 해당 요청에 대한 content type을 request의 header에서 읽어오게 된다. content type을 읽어오는 이유는 content type에 맞춰서 출력하기 위함이다. 특별한 작업을 거치지 않는 한에는 content type은 text/xml; charset=UTF-8 로 넘어온다. 그래서 읽어온 header 값이 text/xml; 로 시작하면 XML 로 출력하는 것을 의미하는 뜻에서 Xplatform의 PlatformType 인터페이스를 상속받은 XplatformConstants 인터페이스의 상수인 CONTENT_TYPE_XML을 설정했다. 이 CONTENT_TYPE_XML은 PlatformType 인터페이스에 정의되어 있는 상수이다.


사실 이 View는 원래 목적에서 100% 구현된 것은 아니다. github을 통해 이 클래스의 render 메소드를 보면 contentType 변수가 XplatformConstants 인터페이스의 CONTENT_TYPE_CSV 일 경우에 대한 처리부분은 비어있다. 내가 Xplatform 전문 프로그래머가 아니어서 이 부분에 대한 지식이 약한데, Xplatform 전문 프로그래머인 지인의 말에 의하면 Xplatform은 출력할때 XML 포맷으로 출력하거나 또는 CSV 스타일로 출력할 수 있다고 한다. 근데 내가 가지고 있는 Xplatform 샘플 프로젝트는 CSV 형태로의 출력 기능이 구현되어 있지 않아 이 부분에 대한 구현을 할 수 없었다. 이 부분에 대해서는 투비소프트에서 이를 처리하는 jsp 소스를 받아 이를 구현해야 할 것임을 미리 밝혀둔다. 그래서 request의 content type이 xml이 아닌 경우 이 content type 값을 그대로 설정하도록 했다. (아마 추측에는 그냥 response stream에 CSV 형태의 문자열을 그대로 내려주면 될 것 같다는 추측은 해본다)


그러면 이제 XML인 경우 어떻게 처리하는지 알아보자. XML일 경우 다음의 코드를 실행하게 된다.


if(contentType == XplatformConstants.CONTENT_TYPE_XML) {
	VariableList variableList = new VariableList();
	DataSetList dataSetList = new DataSetList();
	HttpPlatformResponse httpPlatformResponse = new HttpPlatformResponse(response, XplatformConstants.CONTENT_TYPE_XML);

	if(model != null) {

		for(Entry<String, ?> entry : model.entrySet()) {
			String key = entry.getKey();
			Object object = entry.getValue();
			if(object instanceof Collection) {
				@SuppressWarnings("unchecked")
				DataSet dataSet = makeDataSet(key, (Collection<Object>)object);
				dataSetList.add(dataSet);
			} else {
				Variable variable = null;
				if(object instanceof Integer) {
					variable = new Variable(key, PlatformDataType.INT, (Integer)object);
				} else if(object instanceof Long) {
					variable = new Variable(key, PlatformDataType.LONG, (Long)object);
				} else if(object instanceof Float) {
					variable = new Variable(key, PlatformDataType.FLOAT, (Float)object);
				} else if(object instanceof Double) {
					variable = new Variable(key, PlatformDataType.DOUBLE, (Double)object);
				} else if(object instanceof Date) {
					variable = new Variable(key, PlatformDataType.DATE, (Date)object);
				} else if(object instanceof String){
					variable = new Variable(key, PlatformDataType.STRING, (String)object);
				} else if(object instanceof Variable) {
					variable = (Variable)object;
				} else {
					// model에 들어있는 클래스 객체중에 DataSet으로 변환할 수 없는 클래스 객체가 들어있는것은 bypass 하게끔 한다

					if(skipDataSet(object)) {
						continue;
					}

					// 객체의 멤버변수들 값을 읽어서 한 행짜리 데이터셋으로 return 하는 방법을 고민해보자
					DataSet dataSet = makeDataSet(key, object);
					dataSetList.add(dataSet);
				}
				if(variable != null) {
					variableList.add(variable);
				}
			}
		}

		// XplatformView를 만든다는 것은 그 이전단계까지는 예외없이 진행되었다는 뜻이기 때문에 Xplatform에서 읽어들일변수인 ErrorCode 와 ErrorMsg 변수에 작업이 성공했다는 내용을 설정한다
		// Controller에서 ErrorCode와 ErrorMsg를 설정한 것이 없으면 XplatformView에서 설정하도록 한다
		if(!model.containsKey("ErrorCode")) {
			variableList.add("ErrorCode", ERROR_CODE_VALUE);
		}

		if(!model.containsKey("ErrorMsg")) {
			variableList.add("ErrorMsg", ERROR_MSG_VALUE);
		}
	}

	PlatformData platformData = new PlatformData();
	platformData.setVariableList(variableList);
	platformData.setDataSetList(dataSetList);
	httpPlatformResponse.setData(platformData);
	httpPlatformResponse.sendData();

}


여기에서도 Java Reflection을 통해 Java에서 제공되는 클래스 객체를 Xplatform에서 제공되는 클래스 객체로 바꾸게 된다. 처음의 세 줄은 Xplatform 클라이언트에서 받게될 VariableList 객체와 DataSetList 객체를 생성하고 HttpServletResponse 객체를 넘겨서 Xplatform에서 제공하는 HttpPlatformResponse 객체를 생성하게 된다. 이때 XML로 출력할 것임을 설정하는 작업으로 XplatformConstants.CONTENT_TYPE_XML 상수를 같이 넘겨준다.  이렇게 출력과 관련된 기초작업을 마치면 본격적으로 model 안에 있는 데이터들을 그 성격에 따라 Variable 객체로 만들어서 VariableList 객체에 넣거나 또는 DataSet 객체로 만들어 DataSetList 객체에 넣으면 된다.


넘겨받은 model은 Map<String, ?> 구조이기 때문에 이에 대한 Entry객체가 들어있는 Set 객체를 이용해 반복적으로 Entry 객체 단위의 작업을 진행할 수 있다. Entry 객체를 통해 key와 value를 얻어오면 value로 얻어온 값이 어떤 타입인지을 알아내야 한다. 예전에 ArgumentResolver 에 대한 설명을 했을 당시 DataSet 클래스 객체는 Collection 인터페이스를 상속받은 인터페이스(List, Set)를 구현한 클래스 객체로 변환할 수 있었다. 마찬가지다. 지금은 이것의 반대방향으로 변환한다고 보면 된다. 즉 Collection 인터페이스 계열 객체이면 이를 DataSet 클래스 객체로 변환하는 것이다. 그래서 Collection 인터페이스 계열 객체이면(if(object instanceof Collection)) makeDataSet 메소드에 key와 해당 value를 넘겨서 DataSet 객체를 받은 뒤 이 DataSet을 위에서 만든 DataSetList 객체에 넣게 된다. Collection 인터페이스 계열 객체가 아니면 Java에서 제공하는 Type(ex:int, long, float, double 등)의 객체이거나 또는 사용자가 만든 VO 클래스 객체일 것이다. 그래서 Java에서 제공하는 Type의 객체일 경우 이를 Variable 객체로 변환하여 VaribableList 객체에 넣게 된다. Variable 객체로 만들때 지정하게 된 Variable 이름은 파라미터로 전달된 model 의 key 를 사용하게 된다. 변환해야 할 클래스 객체가 VO 클래스면 1개의 레코드를 가진 DataSet 클래스 객체로 변환하게 된다. model 파라미터에서 객체를 DataSet으로 변환할때는 model에서 사용했던 key를 DataSet 이름으로 사용하게 된다. 이 key는 나중에 Xplatform 코드에서 다음과 같이 사용하면 된다. 예를 들어 ds_output 이란 key로 Controller의 메소드에서 model에 결과를 저장했으면 Xplatform의 에서 ds_output 으로 DataSet 이름을 주면 된다(엄밀하게 말하면 ds_result=ds_output 이런 식으로 설정하게 된다. ds_output 이란 이름의 DataSet 을 서버에서 받아 Xplatform에 있는 ds_result DataSet에 전달한다는 의미로 해석하면 되겠다) 이 변환과정에서 사용되는 메소드로 makeDataSet 메소드를 별도로 만들어서 사용하고 있는데 이 부분에 대한 설명은 생략하도록 하겠다. 예전에 ArgumentResolver 메소드에서 사용했던 XplatformReflectionUtils 클래스의 메소드같이 Java Reflection을 이용해서 객체를 변환한다. 대신 ArgumentResolver 에서는 DataSet을 Java 객체로 변환했지만 이번엔 그 반대방향으로의 변환을 하게 된다. ArgumentResolver 글에서 설명한 내용을 잘 이해했으면 여기서 사용한 메소드를 이해하는데 큰 무리가 없으리라 본다


다른 메소드에 대한 설명은 생략하지만 VO 클래스 객체를 DataSet으로 변환할때 사용된 메소드인 skipDataSet 메소드에 대한 설명은 하고자 한다. model에서 Xplatform의 Variable 객체나 DataSet 객체로의 변환과는 무관한 클래스 객체들이 있다. 이러한 클래스가 있는지는 개발하는 과정에서 발생되는 예외를 보면 알게된다. 그래서 이런 상황일 경우 debug 모드로 프로젝트를 실행해서 파라미터도 넘겨받은 map에 어떤 클래스 객체들이 있는지 살펴봐서 해당 클래스를 알아내면 된다. 이런 클래스 객체인지 확인하기 위해 해당 객체를 파라미터로 넘겨서 변환에서 제외되어야 할 클래스이면 true, 그렇지 않으면 false를 return 하도록 했다. 지금은 이 skipDataSet 메소드에서 제외해야 할 클래스로 Spring에서 제공되는 BeanPropertyBindingResult 클래스와 RequestContext 클래스를 설정했으나 이것은 이 템플릿 상황에서 이 2개의 클래스가 불필요해서 넣은것이지 어떤 상황에서든 이 2개만 하면 되는건 아니다. 이러한 클래스는 비즈니스 로직 상황에서도 인위적으로 변환작업에 예외를 둘 수도 있다. 이것은 자신이 직접 작업해보면서 불필요한 클래스 객체가 발견되면 이 메소드에 해당 클래스를 추가해서 작업해주면 된다. 


이렇게 변환한 객체들을 VaribaleList 객체와 DataSetList 객체에 해당 변수들과 DataSet 들을 넣는 작업을 마치면 마지막으로 작업 결과 코드와 작업 결과 메시지를 넣어야 한다. 아래 코드를 보자


if(!model.containsKey("ErrorCode")) {
	variableList.add("ErrorCode", ERROR_CODE_VALUE);
}

if(!model.containsKey("ErrorMsg")) {
	variableList.add("ErrorMsg", ERROR_MSG_VALUE);
}


이 코드는 작업 결과 코드와 작업 결과 메시지를 넣는 코드이다. 파라미터로 받은 model에 ErrorCode란 key가 없을 경우(if문을 보면 조건에 !가 있다) VariableList 객체에 ErrorCode란 이름으로 XplatformView 클래스의 멤버변수로 설정한 ERROR_CODE_VALUE 값을 넣게 된다. 마찬가지로 model에 ErrorMsg란 key가 없을 경우 VariableList 객체에 ErrorMsg란 이름으로 XplatformView 클래스의 멤버변수로 설정한 ERROR_MSG_VALUE 값을 넣게 된다. 그러면 만약 model에 ErrorCode나 ErrorMsg란 key가 이미 있었다면 어떻게 됐었을까? 그랬다면 이 if문을 거치기 이전에 VariableList 객체에 Variable 객체를 만들어 넣는 과정에서 이미 해당 객체를 만들어서 VariableList 클래스 객체에 넣어 놓는 작업을 진행했을것이다. 여기서 작업결과 코드와 작업결과 메시지 넘겨줄때 반드시 ErrorCode와 ErrorMsg란 이름으로 넘겨줘야 한다. 예전 글에서도 한번 언급했지만 Xplatform에서 서버를 통해 작업결과와 메시지를 받을때는 ErrorCode 와 ErrorMsg란 이름으로 받아야하는 일종의 개발 규약이 있어서 그렇다.


지금까지 Xplatform Client로 출력하기 위해 사용되는 클래스인 XplatformViewResolver와 XplatformView 클래스에 대해 설명했다. 다음에는 여기서 잠깐 언급하게 되었던 ErrorCode와 ErrorMsg를 설정하게 되는 과정인 예외처리 부분에 대해 설명하도록 하겠다.


 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다

저번 글에서는 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스인 XplatformArgumentResovler 클래스에서 @RequestDataSet 어노테이션이 붙었을 경우에 대한 처리를 설명했다. 이제 3개의 어노테이션 중 마지막인 @RequestVariable 어노테이션에 대한 설명을 하도록 하겠다. 먼저 이 부분에 대한 전체적인 골격에 대한 설명을 하도록 하겠다


else if(annotationClass.equals(RequestVariable.class)) {	// @RequestVariable 어노테이션에 대한 처리
	RequestVariable requestVariable = (RequestVariable)annotation;
	String variableName = requestVariable.name();
	Variable variable = variableList.get(variableName);
	if(StringUtils.hasText(variableName)) {					// 특정 변수 이름이 있기 때문에 해당 이름에 대한 값을 낸다
		...
	} else {										// 특정 변수 이름이 없으면 List, Set, Map 또는 VO로 매핑하는 것이기 때문에 오히려 이런 경우 자바의 데이터타입과는 매핑을 할 수 없다
		List<String> keyList = variableList.keyList();

		if(Collection.class.isAssignableFrom(type)) {
			...
		} else if(Map.class.isAssignableFrom(type)) {
			...
		} else {
			// 객체로 변환하는 과정에서 예외가 발생하면 지원하지 않는 타입이기 때문에 그에 대한 처리를 한다
			try {

				Object obj = type.newInstance();
				...
				result = obj;
			} catch(Exception e) {
				result = WebArgumentResolver.UNRESOLVED;
			}
		}
	}
}


이 시점에서 Xplatform의 Variable과 VariableList에 대한 구조를 다시 한번 상기시킬 필요가 있어서 한번 상기시키도록 하겠다. Xplatform에서 DataSet이 아닌 1개 이상의 key=value 형태의 값을 넘길때 VariableList 객체로 받게 된다. 이때 VariableList에서 key를 통해 Variable 객체로 변환된 value를 저장하고 있게 된다. 어찌보면 VariableList는 개념적인것만 놓고 보면 Java의 Map과 같은 구조이다. 이러한 VariableList 객체에 있는 값들을 Controller의 메소드에서 Java 클래스 객체로 받아야 하는데 이때 사용하는 것이 @RequestVariable 어노테이션이 된다.


이 @RequestVariable 어노테이션을 이용해서 데이터를 가져올때 Xplatform에서 데이터를 전송하기 위해 사용했던 key를 이용해서 데이터를 가져오는 경우와 key를 이용하지 않고 전송된 모든 데이터를 가져오는 경우로 사용 방법이 나뉘게 된다. 이런 구조인것을 알고 이제 이러한 구조를 어떤 구조의 데이터 객체로 변환할 것인지를 요약하자면 다음과 같다.


  1. Collection 인터페이스를 상속받은 인터페이스(List, Set) 또는 이를 구현한 클래스 객체
  2. Map 인터페이스 또는 이를 구현한 클래스 객체
  3. VO 형태의 클래스 객체


Collection 인터페이스와 같이 key가 없는 구조에서는 value 값을 Java의 Object 객체로 변환해서 넣게 된다. Map의 경우는 key가 있기 때문에 key를 사용해서 value를 Object 객체로 변환해서 넣게 된다. VO 형태의 클래스 경우엔 key와 같은 이름의 멤버변수에 value를 멤버변수의 해당 타입으로 변환해서 멤버변수에 값을 넣게 된다. 이러한 변환 개념을 이해해두고 구체적인 코드 설명에 들어가도록 하겠다. 먼저 List, Set 인터페이스의 경우에 대해 보도록 하겠다.


Collection collectionResult = null;
if(type.isInterface()) {
	if(type == List.class) {
		collectionResult = new ArrayList<Object>();
	} else if(type == Set.class) {
		collectionResult = new HashSet<Object>();
	}
} else {
	collectionResult = (Collection)type.newInstance();
}

for(String key : keyList) {
	collectionResult.add(variableList.getObject(key));
}

result = collectionResult;


먼저 Collection 인터페이스를 구현한 클래스 객체를 저장하기 위해 Collection 인터페이스 변수를 정한뒤 Controller 메소드의 파라미터에 설정한 타입이 인터페이스일 경우 List 인터페이스면 ArrayList 객체를, Set 인터페이스이면 HashSet 객체를 Collection 인터페이스 변수에 설정한다. 만약 인터페이스가 아닐 경우엔 파라미터에 설정한 타입이 클래스이기 때문에 newInstance 메소드를 이용해서 해당 클래스 객체를 생성한다. 그리고 VariableList 객체에 있는 key들을 이용해서 값을 꺼내와 Collection 인터페이스 변수에 설정된 객체에 넣고 그 객체를 최종 return 할 객체로 설정한다. 설명이 좀 복잡할 수도 있는데 요약하자면 VariableList 객체에서 key를 이용해 VariableList에 있는 값들을 꺼내와서 Collection 인터페이스 구현 객체에 값들을 저장하게 되는 것이다. 이 경우 주의할 것이 있는데 List일 경우엔 중복을 허용하기 때문에 key가 다르더라도 값이 같은 상황에서는 중복되어서 들어가게 된다. 그러나 Set일 경우엔 중복을 허용하지 않기 때문에 key가 다르더라도 값이 같은 상황에서는 중복을 허용하지 않고 1개만 들어가지게 된다.


그러면 Map 인터페이스 또는 이를 구현한 객체에 넣을 경우를 보도록 하자


Map<String, Object> mapResult = null;
if(type.isInterface()) {
	mapResult = new HashMap<String, Object>();
} else {
	mapResult = (Map<String, Object>)type.newInstance();
}

for(String key : keyList) {
	mapResult.put(key, variableList.getObject(key));
}

result = mapResult;


Map 인터페이스도 위에서 다루었던 Collection 인터페이스의 경우와 같은 방법이다. Controller 메소드 파라미터에 Map 인터페이스로 선언했을 경우엔 HashMap 객체를, 그렇지 않은 경우는 사용자가 지정한 클래스 객체를 생성한다. 그리고 Map은 key를 가지고 있기 때문에 VariableList 객체의 key를 Map의 key로 삼아서 VaraibleList에 저장되어 있는 값을 Object 타입으로 넣게 된다.


마지막으로 Collection, Map 인터페이스도 아닌 VO 클래스 객체의 경우를 보자


try {

	Object obj = type.newInstance();

	for(String key : keyList) {
		Field keyField = ReflectionUtils.findField(type, key);
		if(keyField == null) continue;
		ReflectionUtils.makeAccessible(keyField);

		Class<?> keyFieldType = keyField.getType();

		if(keyFieldType == int.class) {
			keyField.setInt(obj, variableList.getInt(key));
		} else if(keyFieldType == Integer.class) {
			keyField.set(obj, new Integer(variableList.getInt(key)));
		} else if(keyFieldType == long.class) {
			keyField.setLong(obj, variableList.getLong(key));
		} else if(keyFieldType == Long.class) {
			keyField.set(obj, new Long(variableList.getLong(key)));
		} else if(keyFieldType == float.class) {
			keyField.setFloat(obj, variableList.getFloat(key));
		} else if(keyFieldType == Float.class) {
			keyField.set(obj, new Float(variableList.getFloat(key)));
		} else if(keyFieldType == double.class) {
			keyField.setDouble(obj, variableList.getDouble(key));
		} else if(keyFieldType == Double.class) {
			keyField.set(obj, new Double(variableList.getFloat(key)));
		} else if(keyFieldType == Date.class) {
			keyField.set(obj, variableList.getDateTime(key));
		} else if(keyFieldType == String.class) {
			keyField.set(obj, variableList.getString(key));
		} else {
			keyField.set(obj, variableList.getObject(key));
		}
	}

	result = obj;
} catch(Exception e) {
	result = WebArgumentResolver.UNRESOLVED;
}


VO 클래스의 경우엔 VariableList의 key와 같은 이름을 가진 멤버변수를 찾아 여기에 해당 key에 대한 값을 넣는 것이다. 그러나 이 경우도 해당 key에 대한 멤버변수가 발견되지 않았을 경우 그냥 bypass 한뒤 다음 key에 대한 작업을 하도록 했다. 멤버변수를 찾으면 해당 멤버변수에 대한 타입을 알아낸뒤 그 타입에 맞춰서 VariableList에 저장되어 있는 값을 꺼내와 설정하게 된다. 기본적으로 Java에서 제공하는 타입으로 했고 만약 지정된 타입이 아닐 경우 Object 객체 그대로 넣도록 했다. 이러한 과정을 진행하면서 예외가 발생했을 경우 처리할 수 없다는 의미로 WebArgumentResolver.UNRESOLVED를 return 하게끔 설정했다. 


지금까지 Xplatform에서 넘어온 DataSet 및 VariableList를 Controller의 메소드 파라미터에서 정의한 타입으로 받아오기 위해 만든 HandlerArgumentResolver 인터페이스 구현에 대해 설명했다. Java Reflection 등 좀 복잡한 내용이 있지만 궁금한 부분은 디버그 모드로 돌려보면서 해당 변수안에 어떤 값이 들어가는지 추적해보면 이해하는데 큰 문제는 없을 것이다. 다음 글부터는 서버쪽 작업이 마쳐지면 이제 Xplatform 으로 데이터를 전송해야 하는데 이때 사용되는 ViewResolver 인터페이스 구현 클래스인 XplatformViewResolver 클래스와 이 클래스에서 사용하는 클래스인 XplatformView 클래스에 대해 알아보도록 하겠다.


 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다

저번 글에서는 @DataSet 어노테이션을 이용해서 DataSet 객체를 사용자가 지정한 파라미터 타입으로의 변환에 대한 대강의 윤곽을 잡았다. 약간 요약식으로 여기서 다시 한번 언급한다면 DataSet에 저장되어 있는 insert-update, delete 레코드들을 각각 변환시키는데 이때 사용자가 Collection 인터페이스 계얄 타입으로 파라미터를 설정했는지 확인해서 그 내부에 정의한 Generic 클래스 타입으로 변환한다고까지 얘기했었다. 다만 저번글에서는 이 변환 부분에 대해 구체적인 설명을 하진 않고 XplatformReflectionUtils 클래스에서 정의한 staric 메소드들을 사용하는 것으로 설명했다. 이번글은 이 static 메소드들에 대한 구체적인 설명을 진행해보고자 한다. 이 변환에 대해 Java Reflection을 사용한다고 늘 언급했지만 구체적인 코드를 보여준적은 없었는데 이번글에서 이 코드들을 언급하도록 하겠다.


DataSet 객체에서 insert, update 레코드를 사용자가 Generic 클래스로 Map을 사용했을때 XplatformReflection 클래스의 makeDataAsMap 메소드를 사용한다고 설명했었다. 이 메소드 코드를 보자.


private static Map<String, Object> makeDataAsMap(Class<?> mapType, DataSet dataSet, int colCount, int rowIdx) throws InstantiationException, IllegalAccessException {    
	Map<String, Object> result = null;    
	if(mapType.isInterface()) {        
		result = new HashMap<String, Object>();    
	} else {        
		result = (Map<String, Object>) mapType.newInstance();    
	}
    
	result.put("rowType", dataSet.getRowType(rowIdx));    
	result.put("storeDataChanges", dataSet.isStoreDataChanges());    
	for(int j=0; j < colCount; j++) {
		// 데이터셋에서 해당 row에 대한 column 값을 Object로 가져온다        
		Object columnValue = dataSet.getObject(rowIdx, j);        
		ColumnHeader columnHeader = dataSet.getColumn(j);        
		String columnName = columnHeader.getName();        
		result.put(columnName, columnValue);    
	}
    
	return result;
}


makeDataAsMap 메소드는 XplatformReflectionUtils 클래스 내부에서 사용될 목적으로 만든 메소드라서 접근자는 private 으로 선언했다. 이 메소드는 4개의 파라미터를 받는데, mapType 파라미터는 사용자가 선언한 Generic Class 타입이 들어온다. 예를 들어 Controller의 메소드에서 파라미터로 List<Map<String, Object>>로 선언했으면 mapType 파라미터는 Map.class를 갖고 오게 된다. 파라미터 dataSet과 colCount, rowIdx는 DataSet 객체와 column 갯수, 레코드 idx 값을 갖게 된다. column 갯수만큼 loop를 돌면서 column header 에 있는 column 이름을 가지고 와 이를 Map의 key로 활용하고 거기에 저장되는 value에 column에 저장된 데이터를 저장하게 된다. 여기서도 mapType에 대해 인터페이스 여부를 체크하는데 왜냐면 위에서 언급한대로 Map<String, Object>로 선언했을수도 있고, LinkedHashMap<String, Object> 로 선언했을수도 있다. Map으로 선언했을 경우엔 인터페이스이므로 이에 대한 구현클래스인 HashMap 클래스를 만들게 되고(new HashMap<String, Object>) LinkedHashMap 같은 Map 구현 클래스일 경우엔 이 클래스 객체를 Reflection을 이용해서 생성((Map<String, Object>)mapType.newInstance()) 하게 된다.


다음으로 봐야 할 것은 DataSet에서 해당 레코드(Row)에 대한 타입을 읽어오는 부분이다. 예전 글에서도 설명했지만 DataSet에서는 해당 레코드가 insert인지, update인지, delete인지를 구분해주는 타입이 있다고 설명했었다. 그것을 읽어오는 것이 getRowType 메소드이다. 이 메소드에 해당 레코드의 index를 주어서 그 레코드 타입을 읽어오는 것이다. 이렇게 읽어온 레코드 타입에 대한 값을 생성된 Map 객체에 rowType이란 key를 주어서 넣어주게 된다. 다음으로 읽어오는 것은 변경된 데이터 변경 정보에 대한 저장여부를 읽어오는 것이다. 이 부분을 활용하는 부분은 없으나 차후에 이 부분을 활용해서 작업해야 할 상황도 있을듯 해서 일종의 확장개념 차원에서 넣었다. 


이렇게 기본적인 Map 객체 설정을 마쳤으면 다음으로 할 것이 해당 레코드에 대한 컬럼 값들을 읽어오는 것이다. 컬럼 갯수 만큼 반복하면서 컬럼에 대한 값을 가져오고(Object columnValue = dataSet.getObject(rowIdx, j)) 컬럼의 이름을 알아내기 위해 컬럼정보를 얻은 뒤에(ColumnHeader columnHeader = dataSet.getColumn(j)) 컬럼 이름을 얻어내어(String columnName = columnHeader.getName()) 해당 컬럼 이름을 key로 삼아 컬럼 값을 넣게 된다(result.put(columnName, columnValue)) 이러한 컬럼 작업을 컬럼 갯수만큼 반복적으로 진행하면 DataSet의 하나의 레코드에 대한 Map 객체가 완성이 되는 것이다.


DataSet의 레코드를 Map 객체로 변환하기 위해 사용됐던 메소드인 makeDataAsMap에 대한 설명은 이정도로 마치고 지금은 Map 객체가 아닌 사용자가 별도로 만든 클래스 객체로 변환하기 위해 사용하는 메소드인 makeDataAsObject 메소드에 대한 설명을 하도록 하겠다. 먼저 이 메소드의 코드를 보자


private static Object makeDataAsObject(Class<?> innerClass, DataSet dataSet, int colCount, int rowIdx) throws InstantiationException, IllegalArgumentException, IllegalAccessException {
	Object obj = innerClass.newInstance();
	Field rowTypeField = ReflectionUtils.findField(innerClass, "rowType");
	Field storeDataChangesField = ReflectionUtils.findField(innerClass, "storeDataChanges");
	ReflectionUtils.makeAccessible(rowTypeField);
	ReflectionUtils.makeAccessible(storeDataChangesField);
	rowTypeField.setInt(obj, dataSet.getRowType(rowIdx));
	storeDataChangesField.setBoolean(obj, dataSet.isStoreDataChanges());
	
	for(int j=0; j < colCount; j++) {
		String columnValue = dataSet.getString(rowIdx, j);	// 데이터셋에서 해당 row에 대한 column 값을 String으로 가져온다
		
		ColumnHeader columnHeader = dataSet.getColumn(j);
		String columnName = columnHeader.getName();
		
		Field field = ReflectionUtils.findField(innerClass, columnName);
		
		if(field == null) {		// 컬럼이름으로 된 멤버변수를 찾지 못한 경우
			continue;
		} else {
			XplatformReflectionUtils.setField(field, columnHeader, obj, columnValue);
		}
	}
	
	return obj;
}


DataSet의 레코드를 Map 객체로 변환하기 위해 진행했던 작업순서는 비슷하지만 구체적인 코드는 다르다. 그럴수밖에 없는 것이 return 되어야 하는 객체가 Map이 아닌 사용자가 설정한 클래스 객체를 return 해야 하기 때문이다. 파라미터로 넘어오는 것중 innerClass의 경우 Map 객체로 변환할 때는 사용자가 설정한 Map 또는 Map 인터페이스를 구현한 클래스 타입이 넘어오지만 지금은 Map이 아닌 사용자가 설정한 클래스가 넘어오게 된다. 먼저 사용자가 설정한 클래스의 객체를 생성한다(Object obj = innerClass.newInstance()) 다음으로 진행할 것은 Map 객체로 변환할때도 진행했던 작업인 DataSet의 해당 레코드 타입과 변경된 데이터 저장여부 에 대한 값을 읽어와서 이를 생성한 객체에 저장하는 것이다. 이 부분은 Spring에서 Java Reflection 기능을 수행하기 위해 별도로 만든 Util 성 클래스인 ReflectionUtils 클래스를 이용해서 작업했다. 근데 여기서 주목해야 할 내용이 있다. Map의 경우는 해당 key를 직접 주어서 값을 넣었지만 클래스의 경우는 이러한 정보를 저장할 필드가 사전에 있어야 한다. 즉 사용자가 설정한 클래스 타입에 이 정보들을 저장하는 멤버변수가 정의되어 있어야 한다 는 의미다. 이를 위해서 여기서는 일종의 규약(?)식으로 다음의 클래스를 상속 받도록 되어 있다.


import com.terry.xplatform.vo.SampleDefaultVO;

import lombok.Getter;
import lombok.Setter;

/** 
* XPlatform의 DataSet에서 변환되는 VO는 이 클래스를 반드시 상속받아야 한다 
* @author Terry Chang * 
*/
@Getter
@Setter
public abstract class XplatformVO extends SampleDefaultVO {
	private int rowType;
	private boolean storeDataChanges;
}


DataSet에 저장되는 레코드를 객체로 변환되는데 사용되는 VO의 경우엔 이 XplatformVO 추상 클래스를 상속받게끔 규약을 설정했다. 그래서 이 추상클래스에 있는 rowType 멤버변수와 storeDataChanges 멤버변수에 DataSet의 해당 레코드 타입과 변경된 데이터 저장여부가 저장이 되도록 했다. makeDataAsObject 메소드에서 객체 생성 이후 for문 나오기 전까지의 코드가 XplatformVO 클래스를 상속받은 클래스의 객체에 이 정보를 저장하는 작업을 한다고 보면 되겠다. for문의 내부 내용은 makeDataAsMap 메소드의 for문과 마찬가지로 데이터 셋의 레코드에 있는 컬럼 값들을 생성된 객체의 멤버필드에 설정하는 작업이다. map의 경우는 레코드의 컬럼 이름을 key로 해서 map에 넣었지만 여기서는 레코드의 컬럼 이름과 같은 이름을 갖고 있는 클래스 필드에 값을 설정하는 것이다. 단 이때 차이가 있다면 레코드 컬럼 이름과 같은 클래스 필드가 없을 경우에 대한 처리가 추가된 것이 있다. 예를 들어 레코드에는 cnt란 컬럼이 있는데 막상 이 레코드를 넣을 클래스의 객체엔 cnt란 필드가 없을수가 있다. 그럴때는 예외를 던지지 않고 bypass 하도록 했다(물론 예외를 던질수도 있다. 다만 이런 작업의 프로그래밍은 bypass 하는 것들이 많아서 이렇게 한 것이지 예외를 던지는 식으로 rule을 잡았으면 예외를 던지는 것이 맞다) 해당 필드에 값을 넣는 기능은 XplatformRefelctionUtils 클래스의 setField 메소드가 하도록 했다. 이제 이 부분을 보도록 하자.


public static void setField(Field field, ColumnHeader columnHeader, Object obj, String columnValue)  {

	//Field가 private 등의 접근할 수 없는 상황일때도 접근할 수 있게 한다
	ReflectionUtils.makeAccessible(field); 					

	Class<?> fieldType = field.getType();

	if(fieldType == String.class) {
		ReflectionUtils.setField(field, obj, columnValue);
	} else if(fieldType == char.class || fieldType == Character.class) {
		ReflectionUtils.setField(field, obj, columnValue.charAt(0));
	} else if(fieldType == short.class || fieldType == Short.class) {
		ReflectionUtils.setField(field, obj, Short.parseShort(columnValue, 10));
	} else if(fieldType == int.class || fieldType == Integer.class) {
		ReflectionUtils.setField(field, obj, Integer.parseInt(columnValue, 10));
	} else if(fieldType == long.class || fieldType == Long.class) {
		ReflectionUtils.setField(field, obj, Long.parseLong(columnValue, 10));
	} else if(fieldType == float.class || fieldType == Float.class) {
		ReflectionUtils.setField(field, obj, Float.parseFloat(columnValue));
	} else if(fieldType == double.class || fieldType == Double.class) {
		ReflectionUtils.setField(field, obj, Double.parseDouble(columnValue));
	} else if(fieldType == Date.class) {
		int dataType = columnHeader.getDataType();
		Calendar calendar = Calendar.getInstance();
		if(dataType == DataTypes.DATE) {
			if(columnValue.length() == 8) {	// yyyyMMdd
				String year = columnValue.substring(0, 4);
				String month = columnValue.substring(4, 6);
				String day = columnValue.substring(6, 8);

				calendar.set(Calendar.YEAR, Integer.parseInt(year, 10));
				calendar.set(Calendar.MONTH, Integer.parseInt(month, 10) - 1);
				calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day, 10));

			} else {
				// DataTypes.DATE 타입에 8자리 숫자가 들어와야 하는데 그러지 않은 것이므로 예외처리 대상이다
				throw new IllegalArgumentException("dataType must be 8 characters in Xplatform DATE type");
			}
		} else if(dataType == DataTypes.TIME) {
			if(columnValue.length() >= 6) {
				String hour = columnValue.substring(0, 2);
				String minute = columnValue.substring(2, 4);
				String second = columnValue.substring(4, 6);
					
				calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour, 10));
				calendar.set(Calendar.MINUTE, Integer.parseInt(minute, 10));
				calendar.set(Calendar.SECOND, Integer.parseInt(second, 10));
					
				if(columnValue.length() > 6) {
					String milisecond = columnValue.substring(6, columnValue.length());
					calendar.set(Calendar.MILLISECOND, Integer.parseInt(milisecond, 10));
				}
			} else { 
				// DataTypes.DATE 타입에 6자리 이상의 숫자가 들어와야 하는데 그러지 않은 것이므로 예외처리 대상이다
				throw new IllegalArgumentException("dataType must be 6 charactersin Xplatform TIME type");
			}
		} else if(dataType == DataTypes.DATE_TIME) {
			if(columnValue.length() >= 17) {
				String year = columnValue.substring(0, 4);
				String month = columnValue.substring(4, 6);
				String day = columnValue.substring(6, 8);
				String hour = columnValue.substring(8, 10);
				String minute = columnValue.substring(10, 12);
				String second = columnValue.substring(12, 14);

				calendar.set(Calendar.YEAR, Integer.parseInt(year, 10));
				calendar.set(Calendar.MONTH, Integer.parseInt(month, 10) - 1);
				calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day, 10));
				calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour, 10));
				calendar.set(Calendar.MINUTE, Integer.parseInt(minute, 10));
				calendar.set(Calendar.SECOND, Integer.parseInt(second, 10));

				if(columnValue.length() > 17) {
					String milisecond = columnValue.substring(14);
					calendar.set(Calendar.MILLISECOND, Integer.parseInt(milisecond, 10));
				}

			} else { 
				// DataTypes.DATE 타입에 14자리 이상의 숫자가 들어와야 하는데 그러지 않은 것이므로 예외처리 대상이다
				throw new IllegalArgumentException("dataType must be 14 charactersin Xplatform DATE_TIME type");
			}
		}

		ReflectionUtils.setField(field, obj, calendar.getTime());
	}

}


먼저 이 메소드는 DataSet에 있는 레코드의 컬럼값을 해당 레코드가 변환될 클래스 객체의 필드에 매핑하는 기능을 한다는 설명을 해놓고 이 setField에 넘어오는 파라미터에 대한 설명부터 하겠다. 첫번째 파라미터로 오는 Field 객체는 변환될 클래스 객체의 Field 객체가 된다. 예를 들어 DataSet의 cnt라는 컬럼을 cnt라는 클래스의 멤버변수에 매핑할때 cnt 멤버변수의 정보(ex : 이름, 타입 등)가 저장되어 있는 객체가 바로 이 Field 클래스 객체가 되는 것이다. 두번째 파라미터로 오는 ColumnHeader 객체는 DataSet의 특정 컬럼 정보가 들어있는 객체가 된다. 위의 예를 인용하자면 DataSet의 cnt 라는 컬럼의 정보(ex : 이름, 타입 등)를 가지고 있는 객체가 되겠다. 세번째 Object 객체는 변환되는 클래스 객체가 여기에 들어온다. 컬럼 값을 실제 객체인 Object 객체에 넣게 되는 것이다. 네번째 String 객체는 해당 DataSet 컬럼에 들어있는 값을 String 객체로 받아온 것이다. String 객체로 받게끔 한 이유는 DataSet의 컬럼이 어떤 타입이든 해당 컬럼에 대한 값은 String 객체로는 표현이 가능하기 때문에 String 객체로 받아온뒤 이를 상황에 맞춰 변환하기 위해서이다.


setField 메소드 코드를 보면 별다른 것은 없다. 코드를 보면 알겠지만 Field 객체에 저장되어 있는 멤버변수의 타입을 읽어와서 해당 타입에 따라 String으로 받아온 컬럼 값을 해당 타입으로 변환한 뒤 넘겨받은 Object 객체에 컬럼 값을 넣는 그런 식이다. 그래서 따로 주석을 달아놓은것은 없다. 대신 객체의 멤버변수로 Date 타입에 대한 상황을 다룰때에 대한 설명은 해야 할 것 같아서 별도 설명을 하겠다. Xplatform에서는 컬럼에 날짜 & 시간을 저장할때 3가지의 타입을 가지고 있는데 날짜만 저장하는 타입(DataTypes.DATE), 시간만 저장하는 타입(DataTypes.TIME), 날짜와 시간을 같이 저장하는 타입(DataTypes.DATE_TIME) 이렇게 3가지를 사용하고 있다. 이를 Java의 Date 타입으로 변환해야 하는 것이다. 그래서 DataTypes.DATE일 경우엔 String으로 받은 컬럼값의 자리수가 8자리(yyyyMMdd)인지 체크하는 것이고, DataTypes.TIME일 경우엔 컬럼값의 자리수가 6자리 이상인지(HHmmssSSS), DataTypes.DATE_TIME일 경우엔 컬럼값이 컬람값의 자리수가 14자리 이상인지(yyyyMMddHHmmss)인지 체크하여 Calendar 클래스를 이용해 Date 객체를 얻어오게 된다. DataTypes.TIME 일 경우엔 6자리 이상의 문자열을 받아 6자리까지는 시분초로 변환하고 6자리가 넘어가는 부분은 밀리세컨드로 변환해서 작업하고 DataTypes.DATE_TIME의 경우엔 14자리 이상의 문자열을 받아 14자리끼지는 연월일시분초로 변환한뒤 14자리가 넘어가는 부분은 밀리세컨드로 변환해서 작업한다. 그리고 이렇게 setField 메소드를 통해 컬럼 값을 객체에 설정한 뒤에 객체는 return 되어지는 것이다.


makeDataAsMap 메소드와 makeDataAsObject 메소드가 DataSet에서 insert, update 되어야 할 레코드들을 변환하는 것이었다면 makeRemoveDataAsMap 메소드와 makeRemoveDataAsObject 메소드는 DataSet에서 delete 되어야 할 레코드들을 Map이나 객체로 변환하는 메소드이다. 이 두 메소드도 DataSet에서 삭제되어야 하는 레코드들을 대상으로 하는 것 말고는 makeDataAsMap 메소드와 makeDataAsObject 메소드와 별 차이가 없어서 설명은 생략하도록 하겠다. 이렇게 convertDataSetToCollection 메소드에서 List 또는 Set 인터페이스로 선언한 것에 대해 알아보았으니 이제 convertDataSetToCollection 메소드에서 Collection 계열 인터페이스가 아닌 이를 구현한 클래스 타입으로 선언했을 경우에 대해 알아보자


if(collectionType.isInterface()) {	// Colleciton 계열 인터페이스로 선언한 경우
	...
} else {	// List, Set 같은 인터페이스가 아니라 ArrayList, HashSet 같은 Collection 인터페이스 구현 클래스로 받은 경우
	Object checkObject = collectionType.newInstance();
	Class<?> checkType = checkObject.getClass();
	if(Collection.class.isAssignableFrom(checkType)) {

		// 저장(insert, update)되어야 할 DataSet의 row들에 대한 처리
		for(int i=0; i < insertUpdateRowCount; i++) {
			if(Map.class.isAssignableFrom(genericClass)) {
				Map<String, Object> obj = makeDataAsMap(genericClass, dataSet, columnCount, i);
				((Collection)(checkObject)).add(obj);
			} else {
				Object obj = makeDataAsObject(genericClass, dataSet, columnCount, i);
				((Collection)(checkObject)).add(obj);
			}
		}

		// 삭제(delete)되어야 할 DataSet의 row들에 대한 처리
		for(int i = 0; i < removeRowCount; i++) {
			if(Map.class.isAssignableFrom(genericClass)) {
				Map<String, Object> obj = makeRemovedDataAsMap(genericClass, dataSet, columnCount, i);
				((Collection)(checkObject)).add(obj);
			} else {
				Object obj = makeRemovedDataAsObject(genericClass, dataSet, columnCount, i);
				((Collection)(checkObject)).add(obj);
			}
		}
	}

	result = checkObject;
}


convertDataSetToCollection 메소드에서 인터페이스인 경우는 지금까지 설명했으니 ... 으로 생략해놨다. 이제 인터페이스가 아닌 경우를 보도록 하자. 인터페이스가 아닌 클래스이면 선언한 타입으로 객체 생성이 가능하다. 그래서 newInstance() 메소드를 통해 해당 타입에 대한 객체를 생성한다. 그리고 생성한 객체에서 Class 타입을 알아낸 뒤에 이 Class 타입이 Collection 인터페이스를 구현한 클래스(Collection.class.isAssignableFrom(checkType)) 타입이면 인터페이스 때와 마찬가지로 앞에서 설명했던 makeDataAsMap, makeDataAsObject, makeRemovedDataAsMap, makeRemovedDataAsObject 메소드를 사용하면서 레코드들을 자바 객체로 변환하여 Collection 인터페이스 계열 클래스 객체에 넣게 된다. 


이전 글과 지금 글 2개를 통해 XplatformArgumentResolver 클래스의 resolveArgument 메소드에서 @RequestDataSet 어노테이션이 선언되었을 경우의 DataSet 처리 방법에 대해 설명했다. 다음에는 XplatformArgumentResolver 클래스의 resolveArgument 메소드에서 @RequestVariable 어노테이션이 선언되었을 경우에 대한 설명을 하도록 하겠다.


 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다

저번 글에서는 HandlerMethodArgumentResolver 인터페이스에서 구현해야 하는 메소드에 대한 설명과 Xplatform 연동 템플릿에서 이 인터페이스를 구현한 클래스에 대한 설명을 진행했다. 저번 글로 설명이 모두 완료가 된것은 아니다. 이 템플릿에서 사용하는 어노테이션 3개 중 @RequestDataSetList 어노테이션에 대한 부분만 설명이 왼료되었다. 이번 글에서는 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스에서 @RequestDataSet 어노테이션에 대한 처리를 설명하고자 한다.


예전 글에서도 언급했듯이 @RequestDataSet 어노테이션은 Xplatform Client가 서버에 전송한 DataSet 중에서 이 어노테이션에 설정한 DataSet 이름을 갖고 있는 DataSet을 DataSetList에서 꺼내와서 이 어노테이션이 붙은 변수에 매핑하게 된다고 설명했다. HandlerMethodArgumentResolver 인터페이스를 구현한 XplatformArgumentResolver 클래스에서 이 부분에 대한 코드를 따로 빼서 보면 다음과 같다


else if(annotationClass.equals(RequestDataSet.class)) {
	RequestDataSet requestDataSet = (RequestDataSet)annotation;
	String dataSetName = requestDataSet.name();
	if(!StringUtils.hasText(dataSetName)) {
		result = WebArgumentResolver.UNRESOLVED;
	} else {
		DataSet dataSet = dataSetList.get(dataSetName);
		if(type.equals(DataSet.class)) {
			result = dataSet;
		} else {
			if(Collection.class.isAssignableFrom(type)) {
				Type genericType = ((ParameterizedType)parameter.getGenericParameterType()).getActualTypeArguments()[0];
				Class<?> genericClass = null;
				if(genericType instanceof Class) {
					genericClass = (Class<?>)genericType;
				} else {
					Type rawType = ((ParameterizedType)genericType).getRawType();
					genericClass = (Class<?>)rawType;
				}
				result = XplatformReflectionUtils.convertDataSetToCollection(dataSet, type, genericClass);
			} else {
				result = WebArgumentResolver.UNRESOLVED;
			}
		}
	}
	
	if(result == null) {
		result = WebArgumentResolver.UNRESOLVED;
	}
}


이전에 설명했던 코드에서도 자바 Reflection에 해당되는 코드를 일부 사용했지면 여기서는 정말이지 본격적으로 이 Reflection을 사용하게 된다. 이것을 그냥 사용하면 중복되는 코드도 많고 이러다보니 코드도 길어지고 지저분해져서 이런 부분을 정리한 클래스로 XplatformReflectionUtils 란 클래스를 만들었다. 우리가 흔히 사용하는 static 메소드로만 이루어진 Util 클래스 같은거라 보면 된다. 이 클래스에서 제공하는 메소드에서 Reflection 기능을 한다고 생각하고 코드를 보기 바란다. 이전에도 설명했지만 @RequestDataSet 어노테이션은 이 어노테이션에 지정된 이름을 가진 DataSet을 가져온다고 했었다. 그럴려면 @RequestDataSet 어노테이션을 가져와서 그 이름을 가져오는 코드를 구현해야 한다. 그래서 처음 두 줄은 이러한 역할을 하게 된다. 그리고 다음에 나오는 if문에서 가져온 이름이 null 이거나 빈문자열일 경우엔 처리할 수 없다는 의미로 WebArgumentResolver.UNRESOLVED 로 설정한다.  이름이 지정된게 있으면 해당 이름의 DataSet을 가져오게 된다. @RequestDataSet 어노테이션이 붙은 파라미터의 클래스가 DataSet 클래스라면 현재 가져온 DataSet 을 그대로 설정해서 return 해주면 된다. 그러나 파라미터의 클래스가 DataSet 클래스가 아니라면 DataSet 클래스를 파라미터에 설정한 클래스로 변환해야 한다. 이 부분에서 자바의 Reflection 을 사용하게 된다.


예전 글에서도 언급했지만 다시 상기시키는 차원에서 한번 더 말하면 Xplatform의 DataSet 클래스 구조는 DB로 보자면 1개 이상의 레코드들의 묶음과 같은 구조와 같다고 보면 이해하기가 쉽다. 이러한 구조를 Java 타입으로 구현한다면 레코드를 Java 클래스로 변환한 객체가 들어있는 Collection 인터페이스 구현 클래스라 보면 이해하기가 쉬울것이다. 그러면 이 Collection 인터페이스 부분에는 어떤것이 올 수 있을까? 다음의 2가지 경우로 보면 된다.


  1. Collection 인터페이스를 상속받은 하위 인터페이스
  2. 1번의 인터페이스를 구현한 클래스


위에서 언급한 1번과 2번을 생각한뒤 이 템플릿에서 변환하는 타입을 보면 다음과 같다.


  • List<T>
  • Set<T>
  • List<Map<String, Object>>
  • ArrayList<T>
  • HashSet<T>
  • List<HashMap<String, Object>>
  • ...


위에서 나열한 변환하는 타입이 1번과 2번의 경우를 모두 만족하고 있다. List나 Set 모두 Collection 인터페이스의 하위 인터페이스이고 ArrayList나 HashSet은 이러한 인터페이스를 구현한 클래스이다.

Collection 인터페이스의 내부에 들어가는 클래스는 VO 형태의 일반적인 자바 클래스(T) 이거나 Map 인터페이스 또는 Map 인터페이스를 구현한 클래스이다.  DataSet은 이러한 형태의 자바 클래스 타입으로 변환할 수 있다. 이 변환을 하기 위해 Java Reflection을 사용하게 되는 것이다. 이러한 내용들에 대해 알아둔 뒤 구체적으로 코드를 보도록 하자.


@RequestDataSet 어노테이션을 사용하는 파라미터의 타입이 DataSet 이 아닌 경우 먼저 타입이 Collection 인터페이스와 연관이 될 수 있는지를 체크한다(

if(Collection.class.isAssignableFrom(type))) 그래서 Collection 인터페이스의 하위 인터페이스이거나 또는 구현 클래스라면 그 안에 들어가는 객체의 Generic Class를 알아야 한다. 어떤 클래스인지를 알아야 DataSet을 구성하는 레코드를 해당 클래스로 변환하는 것이기 때문이다. 근데 이때 주의해야 할 것이 있다. 이 Generic Class는 인터페이스일수도 있고 클래스일수도 있다. 이 2가지의 경우에 대해 구분해서 처리해야 한다. 왜냐면 어떤 경우냐에 따라 읽어오는 방법이 다르기 때문이다. 클래스라면 단순히 Class 타입 캐스팅으로 변환하는 것(genericClass = (Class<?>)genericType)으로 타입을 알아낼 수 있지만 인터페이스일 경우엔 ParameterizedType으로 먼저 캐스팅을 한 뒤에 타입을 읽어서(Type rawType = ((ParameterizedType)genericType).getRawType()) 이를 Class 타입으로 캐스팅(genericClass = (Class<?>)rawType)을 하여 Generic Class를 알아낸다. 이렇게 Controller 메소드 파라미터의 타입이 Collection 인터페이스 계열인지, 그리고 그 안에 들어가지게 되는 Generic 클래스 타입을 알게 되면 그 2가지의 클래스 타입을 알게 되었으니 DataSet 클래스 객체와 그 안에 들어가 있는 레코드들을 Generic 클래스 객체가 들어가 있는 Collection 인터페이스 구현 클래스 객체로 변환하면 된다. 이제부터 자바 Reflection 기능을 사용해서 변환하게 된다. 위에서도 썼지만 이러한 기능만 모아놓은 클래스인 XplatformReflectionUtils 클래스를 만들었다고 했다. 그래서 여기서는 이러한 객체 변환을 위해 XplatformReflectionUtils 클래스의 convertDataSetToCollection 메소드를 사용하게 된다. 그래서 이 메소드를 통해 변환된 결과가 최종 return 되는 변수인 result에 넣어지게 된다. 그리고 Controller 메소드 파라미터의 타입이 Collection 인터페이스 계열이 아니거나 최종 return 하는 변수인 result가 null이면 이 HandlerMethodArgumentResolver에서 지원하지 않는다는 의미로 result에 WebArgumentResolver.UNRESOLVED를 설정해서 return이 되게끔 했다.


이제는 이런 변환의 핵심 역할을 하는 XplatformReflectionUtils 클래스의 convertDataSetToCollection 메소드에 대한 설명을 하도록 하겠다. 코드는 아래와 같다. 그리고 중복되는 부분은 별도 설명을 위해서 ...으로 표시했다. 이 부분에 대해서는 나중에 다시 설명하겠다.


public static Object convertDataSetToCollection(DataSet dataSet, Class<?> collectionType, Class<?> genericClass) throws InstantiationException, IllegalAccessException {
	Object result = null;
	int insertUpdateRowCount = dataSet.getRowCount();
	int removeRowCount = dataSet.getRemovedRowCount();
	int columnCount = dataSet.getColumnCount();
	if(collectionType.isInterface()) {
		if(collectionType.equals(List.class)) {
			...    
		} else if(collectionType.equals(Set.class)) {
			...    
		}
	} else {
		...
	}
	return result;
}


이 메소드에서는 3개의 파라미터를 받고 있다. DataSet 타입 파라미터는 우리가 변환해야 할 대상인 DataSet 클래스 객체이고, Class<?> collectionType 은 Controller 메소드 파라미터의 타입인 Collection 인터페이스 계열 타입 정보이고, Class<?> genericClass 는 Collection 인터페이스 계열 클래스에 들어가게 될 Generic 클래스 타입이 넘어가게 된다. 최종 return 하게 되는 변수인 result는 Object 타입으로 설장한다. 여기서 알아둬야 할 것이 있다. Xplatform의 DataSet에 들어가 있는 레코드는 그 내부에 insert, update, delete 할 레코드인지에 대한 정보가 저장되어 있다. 근데 이것에 대한 관리가 약간 다른 부분이 있다. insert와 update 할 레코드는 같은 공간에서 관리된다. 그러나 delete 할 레코드는 insert, update 할 레코드와는 달리 별도 공간에서 관리된다. 그래서 흔히 우리가 레코드 갯수를 구한뒤 배열 개념 식으로 0~갯수-1 까지의 index를 주면서 레코드 1개씩 접근하는 방법을 사용할때 insert, update 레코드와 delete 레코드를 각각 따로 구현해줘야 한다. 코드에서 보면 알 수 있게 되는데 int insertUpdateRowCount = dataSet.getRowCount(); 코드를 통해 insert와 update 레코드 갯수를 구하고 int removeRowCount = dataSet.getRemovedRowCount(); 코드를 통해 delete 레코드 갯수를 구한다. ... 부분에서 이 2가지의 갯수를 저장한 변수를 이용하는 구체적인 코드가 나오니 그때 다시 보도록 하겠다. 그리고 레코드의 컬럼 갯수를 구한다. 예전 글에서도 설명했지만 레코드의 컬럼은 나중에 변환할때 VO의 멤버변수 이름 또는 Map의 key 값으로 사용되기 때문에 컬럼 이름을 알기 위해 이 갯수를 구하게 된다. 이 부분도 ... 부분에서 구현되어 있으니 그때 설명하겠다.


이제 if 부분을 설명하겠다. 파라미터로 받은 Controller 메소드 파라미터 타입인 collection 변수를 이용해서 이 메소드 타입이 인터페이스 인지 또는 클래스 인지를 확인하게 된다(if(collectionType.isInterface())) 이것을 한 이유는 Controller 의 메소드 파라미터 타입이 인터페이스 일 경우엔 그것에 대한 구현 클래스를 우리가 정한 클래스로 만들어서 하고 인터페이스가 아닌 클래스일 경우엔 직접 지정한 클래스로 만들어야 해서 그렇다. 글로 설명하면 와 닿지 않을수도 있을듯 한데 예를 들어보겠다. 예를 들어 Controller 메소드를 다음과 같이 했다고 가정해보자


public void modify(@RequestDataSet(name="ds_input") List<SampleVO> dataSet)


@RequestDataSet(name="ds_input") List<SampleVO> dataSet 으로 메소드의 파라미터가 선언되어 있는데 이를 HandlerMethodArgumentResovler 를 통해 처리가 되면 XplatformReflectionUtils 클래스의 convertDataSetToCollection 메소드에서 파라미터로 넘어갈때 dataSet 파라미터 변수는 DataSetList 클래스 객체에서 ds_input 이란 이름의 DataSet 클래스 객체가 넘어가게 되고 collectionType 변수에는 List.class가, genericClass 변수에는 SampleVO.class가 넘어가게 된다. 지금까지의 글을 따라왔다면 이렇게 받게 되는 것에 대한 이해를 할 수 있으리라 생각한다. collectionType 변수가 List.class를 받았기 때문에 collectionType.isInterface()에서 true 가 된다. List는 인터페이스이기 때문이다. 그러나 우리가 객체로 만들때는 인터페이스로 객체를 만들수는 없다. 그 인터페이스를 구현한 클래스로 객체를 만들어야 한다. 그래서 그 인터페이스가 List일때는 자바에서 제공하는 여러 List 인터페이스 구현 클래스 중 ArrayList 클래스 객체를 만들고, 인터페이스가 Set 일때는 마찬가지로 HashSet 클래스 객체를 만들어서 여기에 레코드 클래스 객체를 넣은뒤에 return 해준다. 그러나 만약 다음과 같이 선언되면


public void modify(@RequestDataSet(name="ds_input") ArrayList<SampleVO> dataSet)


이럴 경우엔 convertDataSetToCollection 메소드의 collectionType 변수가 ArrayList.class 를 받게 된다. 그러면 collectionType.isInterface() 에서 false가 되기 때문에 else 문으로 가게 된다. else 부분에서 사용자가 선언한 ArrayList 클래스 객체를 만들어서 return 하게끔 해준다. 만약 ArrayList가 아닌 LinkedList나 LinkedHashSet으로 선언했으면 해당 선언된 클래스 객체를 만들게 된다. 


지금까지 인터페이스 여부를 체크하는 if문을 작성한 이유에 대해 설명했으니 이제 if문에서 true 인 경우, 즉 파라미터로 설정한 타입이 인터페이스인 경우를 보도록 하자. 예전 글에서도 언급했지만 DataSet 객체를 Collection 인터페이스 계열로 받기로 했기 때문에 지금부터 해야 할 것은 Collection 인터페이스의 대표격인 List 와 Set 인지를 체크하면 된다. List 로 선언했을 경우 DataSet 객체의 레코드들을 자바 객체로 변환해서 이를 ArrayList 객체에 담아 return 해주면 되고 Set 으로 선언했을 경우 레코드 변환 자바 객체를 HashSet에 만들어서 return 해주면 된다. List로 받았는지 체크하기 위해 if문을 사용하고 있는데 List일 경우에는 다음과 같은 코드를 실행하도록 되어 있다(... 으로 표시한 부분에 대한 코드이다)


List<Object< listResult = new ArrayList<Object<();

// 저장(insert, update)되어야 할 DataSet의 row들에 대한 처리
for(int i=0; i < insertUpdateRowCount; i++) {
	if(Map.class.isAssignableFrom(genericClass)) {
		Map<String, Object< obj = makeDataAsMap(genericClass, dataSet, columnCount, i);
		listResult.add(obj);
	} else {
		Object obj = makeDataAsObject(genericClass, dataSet, columnCount, i);
		listResult.add(obj);
	}
}

// 삭제(delete)되어야 할 DataSet의 row들에 대한 처리
for(int i = 0; i < removeRowCount; i++) {
	if(Map.class.isAssignableFrom(genericClass)) {
		Map<String, Object< obj = makeRemovedDataAsMap(genericClass, dataSet, columnCount, i);
		listResult.add(obj);
	} else {
		Object obj = makeRemovedDataAsObject(genericClass, dataSet, columnCount, i);
		listResult.add(obj);
	}
}

result = listResult;


List 인터페이스가 사용자가 지정한 파라미터 클래스로 넘어왔기 때문에 List 인터페이스를 구현한 클래스인 ArrayList 클래스 객체를 생성한다. 그리고 loop를 도는데 loop에 사용된 변수인 insertUpdateRowCount는 이전에 구했던 DataSet 클래스 객체의 레코드들 중에서 insert 또는 update인 레코드들의 갯수를 저장한 변수이다. 즉 이 loop를 통해 insert 또는 update인 레코드들을 자바 객체로 변환해서 방금 만들어 놓은 ArrayList 객체에 넣는 것이다. 이때 또 하나 더 체크해야 할 것이 있다. 파라미터 클래스로 List 인터페이스가 넘어왔으면 List 인터페이스 선언시에 같이 선언된 generic 클래스를 알아야 한다. 그래야 그 선언된 generic 클래스 객체를 만들어서 ArrayList 객체에 넣을 수 있는거니까.. 그래서 아까 알아봤던 generic 클래스 타입 값을 가지고 있는 genericClass 변수를 사용하게 되는데 이때 사용자가 generic 클래스 타입으로 Map을 선언했는지를 알아야 한다. 그래서 Map.class.isAssignableFrom(genericClass)를 통해 Map으로 선언했는지를 알아보고 Map으로 선언했으면 DataSet에 있는 레코드를 Map 인터페이스를 구현한 클래스로 객체를 만들어서 ArrayList 객체에 넣어준다. 만약 Map으로 선언되지 않았으면(이런 경우는 사용자가 특정 VO 클래스로 선언한 경우이다) 사용자가 지정한 클래스로 객체를 만들어서 ArrayList 객체에 넣어준다. 그리고 이런 모든 작업을 마치면 최종적으로 return 되는 변수인 result에 ArrayList 객체를 지정해주면 ArrayList 객체가 return 되는 것이다. 레코드 객체를 만들때 Generic Class가 Map 으로 선언됐을 경우는 XplatformReflectionUtils 클래스의 MakeDataAsMap 메소드를 이용해서 객체를 만들고 Map으로 선언되지 않았을경우엔 XplatformReflectionUtils 클래스의 MakeDataAsObject 메소드를 이용해서 객체를 만들게 된다. 또 DataSet 객체의 delete 레코드들의 변환을 위해 delete 레코드들의 갯수를 저장한 변수닌 deleteRowCount 변수를 loop에서 사용하면서 마찬가지 개념으로 DataSet 객체의 레코드들을 자바 객체로 변환하여 ArrayList 객체에 넣어주게 된다. 이때는 XplatformReflectionUtils 클래스의 MakeRemoveDataAsMap과 MakeRemoveDataAsObject 메소드를 사용하게 된다. 이러한 컨셉은 List 인터페이스 뿐만 아니라 Set 인터페이스를 사용한 경우에도 마찬가지로 적용된다. 대신 Set 인터페이스를 구현한 클래스인 HashSet를 만들어서 return 하는 것의 차이만 존재할 뿐이다.


다음 글에서는 XplatformReflectionUtils 클래스의 MakeDataAsMap 등의 메소드들에 대한 설명을 통해 DataSet의 레코드들을 어떻게 자바 객체로 변환하는지에 대해 살펴보도록 하겠다.


 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다

저번 글에서는 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다

저번 글에서는 HttpServeletRequest 클래스의 Wrapper 클래스인 HttpRequestWrapper 클래스의 필요성과 제작 및 설정에 대한 설명을 했다. 이번에는 HttpRequestWrapper 클래스를 통해 전달된 Xplatform의 데이터를 Spring에서 어떻게 사용할 수 있는지에 대해 살펴보도록 하자.


Spring MVC 에서는 Web Client가 전달한 데이터를 서버측에서 받아들일때 서버측에서 사용하기 좋은 형태의 타입으로 변환할 수 있다. 이때 사용되는 방법으로 2가지가 있다.


  1. HttpMessageConverter
  2. HandlerMethodArgumentResolver


HttpMessageConverter 와 HandlerMethodArgumentResolver 모두 Spring에서 제공하는 인터페이스이며 이 인터페이스를 구현하면 된다. 또한 Spring에서도 이들 인터페이스를 구현한 클래스를 제공해주고 있다. 그러나 이 2개의 인터페이스는 기능과 동작에서 차이점이 존재한다. 


HttpMessageConverter의 경우는 사용에 있어서 제한적인 부분이 있다. HttpMessageConverter를 사용해야 하는가에 대한 결정을 header의 MIME Type  정보를 통해서 결정이 된다. 특정 HttpMessageConverter를 사용하려면 그에 맞춰서 MIME Type 정보를 설정해주어야 하는데 이것이 사용에 있어서 불편한 점이 있을 수 있다. 이러한 상황은 Request 뿐만 아니라 Response에서도 적용된다. 즉 Response header에 MIME Type 정보를 설정해주어야 그거에 맞는 HttpMessageConverter가 사용되어서 Write 작업을 할 수 있게 된다. 이에 반해 HandlerMethodArgumentResolver 인터페이스는 MIME 정보를 사용하지는 않지만 어떤 HandlerMethodArgumentResolver 인터페이스를 사용해야 할 지를 판단하는 기준이 Request와 Response에 있는 것이 아니라 Controller 클래스에 있는 method의 parameter 정보를 가지고 판단하기 때문에 Spring Context에 등록되어 있는 모든 HandlerMethodArgumentResolver 인터페이스 구현 객체에 parameter 정보가 전달되는 방식으로 동작되게 된다.  parameter 정보를 받아서 해당 구현 클래스가 이를 사용할 수 있는지를 판단해서 진행하게 된다.


또 한가지의 차이점은 HttpMessageConverter의 경우는 특정 클래스 1개만을 대상으로 진행된다. 예를 들어 A란 클래스와 B란 클래스에 대해 HttpMessageConverter의 경우 A 클래스와 관련된 HttpMessageConverter 인터페이스 구현 클래스를 만들고, B 클래스와 관련된 HttpMessageConverter 인터페이스 구현 클래스를 만들어야 하는..2개의 클래스를 만들어야 한다. 이러한 방법은 변환 대상이 되는 클래스의 갯수가 적을때는 효과적이지만 그 갯수가 많거나 갯수를 예측할 수 없을때는 사용하기엔 한계가 있는 방법이다. 이에 반해 HandlerMethodArgumentResolver의 경우는 변환 대상 클래스를 지정하지 않는다. Controller의 메소드에 return이 되는 클래스가 Object 클래스이기 때문에 어떠한 클래스로든 적용이 가능해진다. 이러한 차이점때문에 여기서는 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스를 만들어서 사용하게 되었다. 


이 연재를 시작하는 시점에 잠깐 언급했지만 HandlerMethodArgumentResolver를 이용해서 구체적으로 어떤 결과물이 나오는 것을 목표로 하는 것인지 한번 더 정리하도록 하겠다. 앞서 얘기했던 사전지식 내용을 통해 Xplatform은 DataSet과 Variable 이란 2가지 형태의 데이터 개념이 있다고 얘기했다. Xplatform Client는 자신이 서버에 보내고자 하는 데이터들을 DataSet과 Variable을 적절하게 사용하여 보내지게 될텐데 Spring Controller에서 이를 액면 그대로 받아들이는 경우 서버 코드가 클라이언트에 종속이 되는 상황이 벌어진다고 얘기했다. 그래서 Spring Controller 에서는 Xplatform Client가 보내는 DataSet과 Variable을 다음과 같은 형식으로 받아들이려 한다. 다음부터 언급하는 것은 Spring Controller의 메소드 예시이다.


public void list1(Model model
					, SampleVO sampleVO
					, @RequestVariable Map<String, Object> requestVariableMap
					, HttpServletRequest httpServletRequest) {
	...
}

public void list2(@RequestVariable SampleVO requestVariableSampleVO
					, @RequestVariable(name="firstIndex") int firstIndex) {
	...
}

public void list3(@RequestVariable(name="recordCountPerPage") String recordCountPerPage) {
	...
}

public void modify1(@RequestDataSetList DataSetList dataSetList) {
	...	
}

public void modify2(@RequestDataSet(name="ds_input") List<SampleVO> dataSet
					, @RequestDataSetList DataSetList dataSetList) {
	...	
}

public void modify3(@RequestDataSet(name="ds_input")ArrayList<SampleVO> dataSet) {
	...	
}

public void modify4(@RequestDataSet(name="ds_input")List<Map<String, Object>> dataSet) {
	...	
}

public void modify5(@RequestDataSet(name="ds_input")Set<SampleVO> dataHashSet) {
	...	
}


위의 코드들은 Spring Controller 에서 url과 매핑되는 메소드를 구현할때의 예시를 들은 것이다. @RequestMapping 같은 URL 매핑 어노테이션은 지금부터 하게 되는 설명과는 무관한 사항이라 생략한것이니 오해가 없길 바란다. 위의 예시들을 보면 @RequestVariable, @RequestDataSetList, @RequestDataSet 이렇게 3종류의 어노테이션이 사용되고 있다. 이 어노테이션들에 대한 설명을 먼저 한 뒤에 메소드 예시들에 대한 설명을 이어가도록 하겠다.


● @RequestVariable


Xplatform Client가 보내는 Variable을 이 어노테이션 뒤에 설정되는 변수에 받아들이게 된다. 어노테이션의 name 속성에 해당 Variable의 이름을 지정해주면 이름에 대한 Variable 값을 변수에 설정해주게 된다. name을 주지 않게 되면 전체 Variable을 읽어오는 개념으로 Map<String, Object> 형태의 변수에 Xplatform Client가 전달하는 모든 Variable을 저장하게 된다. Map으로 받을 경우 key 부분에는 Variable의 이름이 설정된다. Map이 아니라 VO 타입으로 받을 경우엔 해당 VO의 멤버변수명과 Variable의 변수이름이 같을 경우 해당 변수에 값이 설정된다. 받아들일수 있는 변수 타입은 HandlerMethodArgumentResolver 설명시에 별도로 더 언급하도록 하겠다.


● @RequestDataSetList


Xplatform Client가 보내는 DataSetList를 이 어노테이션 뒤에 설정되는 변수에 받아들이게 된다. DataSetList 는 이전에 작성했던 글에서 언급한 구조를 생각해보면 자바에서 이것을 받아들일 Native 한 구조가 없다(여기서 Native 한 구조를 언급한 것은 별도 가공 없이 자바가 제공하는 데이터 구조체에서 이 DataSetList와 match 되는게 없다는 것을 의미하는 것이지 자바 변수로 받아들이는 방법이 아주 불가능하진 않다) 부연설명에서 처럼 하지 않은 것은 이런 구조로 만드는거 보단 그냥 Xplatform이 제공하는 DataSetList 클래스로 받아들이는 것이 더 낫다고 판단했다. 이것 또한 상세적인 코드 내용은 HandlerMethodArumentResolver 설명시에 언급하도록 하겠다


● @RequestDataSet


Xplatform Client가 보내는 DataSet을 이 어노테이션 뒤에 설정되는 변수에 받아들이게 된다. 어노테이션의 name 속성에 해당 DataSet의 이름을 지정해주면 이름에 대한 DataSet 을 변수에 설정해주게 된다. 이전에 작성했던 글에서 DataSet 구조를 생각해보면 이것은 단일 타입의 객체 여러개를 저장하는 자바 데이터 타입으로 변환이 가능하다고 생각할 수 있다. 그래서 자바의 Collection 인터페이스를 구현한 클래스 객체를 이용해서 단일 타입의 객체 여러개를 저장하도록 했다. 더 자세한 내용은 HandlerMethodArgumentResolver 설명시에 언급하도록 하겠다


지금까지 어노테이션에 대한 설명을 마치도록 하겠다. 다음은 위에서 언급한 Controller 메소드 예시에 대한 설명을 하도록 하겠다. 위에서 언급한 어노테이션 설명과 다음의 설명을 알아둔 뒤에 이 메소드에 대한 설명을 진행하도록 하겠다. Xplatform에서는 서버와의 통신을 위해 transaction 이란 메소드를 사용하게 된다. 이 메소드를 Xplatform에서 다음과 같이 사용했다고 가정해보자


var strSvcid 		= "selectSvc";
var strController 	= "egovSampleSelect.do?id=50&firstIndex=0&recordCountPerPage=10®User=terry";
var strInputDs 		= "ds_input=ds_list:U";
var strOutputDs 	= "ds_list=ds_output";
var strParam 		= "firstIndex=5 recordCountPerPage=20 id=100 regUser=chang";
var strFnCallback 	= "fn_callBack";
transaction(svcid, 
				"svc::" + strURL, 
				inputDs, 
				outputDs,
				params,
				"fn_callBack");


이 글은 Xplatform 개발을 하는 사람들은 다 아실만한 내용이겠지만 순수 자바 서버 개발자는 이러한 코드를 알지 못할수 있다. 그러나 이 글을 보는 서버개발자는 Xplatform 개발을 하는 사람과 같이 일하기때문에 이 코드가 무엇을 의미하는지 물어볼수는 있을꺼라 생각한다. 그래서 여기서는 구체적으로 언급은 하지않겠다. 그러나 설명하는 과정에 있어서 이 코드에 대한 설명이 필요할 시에는 부분부분 설명하도록 하겠다. 구체적인 설명을 하기에 앞서 미리 설명을 하자면 strController 변수에는 접속하고자 하는 서버 URL을 설정한다. 여기서는 일부러 Get 방식으로 변수를 좀더 붙여넣었는데 왜 이렇게 했는지는 좀 이따가 언급하도록 하겠다. strInputDs에는 입력 DataSet에 대한 설정을 진행한다. 여기서는 Xplatform Client에서 ds_list 란 이름의 DataSet을 서버에서는 ds_input으로 매치해서 읽어올 수 있다. strOutputDs는 출력 DataSet에 대한 설정을 진행한다. ds_output 이란 이름의 서버 DataSet을 Xplatform Client의 ds_list 에 넣어지게 해서 Xplatform Client에서는 ds_list로 읽어올 수 있다. strParam 변수에 설정하는 것은 Xplatform 에서 설정하는 Variable 변수를 설정하는 것으로 이름=값 형태를 가지고 있으며 여러개를 설정할 경우 공백을 두어 여러개를 설정할 수 있다. 이러한 것을 알고 다음의 Controller 메소드 예시를 보도록 하자


 public void list1(Model model, SampleVO sampleVO, @RequestVariable Map<String, Object> requestVariableMap, HttpServletRequest httpServletRequest)


Spring 개발자라면 Model이나 HttpServletRequest로 받는게 어떤것인지는 알것이니 이 부분에 대한 설명은 넘어가도록 하겠다. SampleVO 클래스 객체로 받는 것은 URL을 통해 넘어오는 변수들에 대한 값들을 SampleVO 란 클래스 객체로 받는 것이다. 일반적인 web page 방식에서 <form> 태그로 GET, POST 방식을 설정하는 것과는 달리 Xplatform Client 에서는 GET, POST 방식을 지정해서 전달하는 방법이 없기 때문에 기본적으로 url에 querystring을 통해 전달하는 GET 방식을 사용하게 된다(POST 방식의 전송이 지원 안되는 것은 아니다. 다만 여기 템플릿에서는 Xplatform 샘플 코드 기반으로 개발하다보니 POST로의 전송이 존재하질 않아서 여기서는 구현하지 않았다). 그래서 SampleVO 클래스 객체로 받아지는 것은 querystring으로 전달되는 값들을 받게 된다. querystring에 있는 변수명이 SampleVO 클래스의 멤버변수와 매핑되어서 값이 설정된다. HttpServletRequest 객체를 받는 것은 현재 요청 자체를 받는 것이 된다. 여기까지는 기존 Spring 에서의 변수값 전달과 동일하다. 이제껏 해왔던 Spring MVC에서 프로그래밍 했던 것과 차이가 있는 부분은 @RequestVariable Map<String, Object> requestVariableMap 이 부분이다. 이것은 기존의 Spring MVC에서 사용하던게 아니라 우리가 Xplatform 연동을 위해 새로이 만들 HandlerMethodArgumentResolver 가 requestVariableMap 변수에 값을 할당할 것이다. 아까 Xplatform transaction 메소드 설명시에 Variable을 설정하는 변수인 strParam 변수에 대한 설명과 @RequestVariable 어노테이션에서 Map으로 받는 내용에 대해 같이 묶어 생각해보면 이 변수에 무엇이 들어갈것인지 대강 짐작할 수 있다. 즉 strParam으로 전달되는 key=value 값들이 Map에 <key, value> 형태로 저장이 되는 것이다. 이렇게 Map으로 받은 Variable들을 변수명을 key로 잡고 해당 값을 꺼내와서 원하는 작업을 할 수 있게 된다


 public void list2(@RequestVariable SampleVO requestVariableSampleVO, @RequestVariable(name="firstIndex") int firstIndex)


list2 메소드에서는 파라미터로 @RequestVariable 어노테이션만을 사용하고 있다. 근데 사용 방법이 약간 다르다. 바로 위에서 사용했던 방법과도 다른 부분이 있다. 위에서는 @RequestVariable을 Map으로 받았지면 여기서는 특정 클래스로 받았다. 특정 클래스로 받을 경우에는 클래스의 멤버변수 이름과 Variable 이름이 같을 경우 해당 멤버변수에 값이 설정된다. 그러면 예를 들어 RequestVariable에 사용된 클래스의 멤버변수 이름이 Variable에 사용된 변수 이름에 있지 않거나 거꾸로 Variable에 사용된 변수 이름이 클래스의 멤버변수 이름으로 있지 않을 경우엔 어떻게 동작하나? 아무 변화도 없다. 클래스의 멤버변수 이름이 Variable 변수 이름에 존재하지 않으면 해당 클래스의 멤버변수에는 아무 값도 할당되지 않는다. 역으로 Variable 변수 이름은 존재하지만 이에 대한 해당 클래스의 멤버변수 이름이 없을 경우에도 아무런 할당 작업 없이 그냥 넘어가게 된다. 이와는 달리 @RequestVariable 어노테이션에 name 속성에 Variable 이름을 주어 해당 Variable 값을 가져오는 경우에는 단일 형태의 값을 가져오는 것이기 때문에 멤버변수들이 1개 이상으로 구성되어 있는 특정 클래스로 받는 것이 아니라 java에서 제공하는 데이터형으로만 받게 된다. 위의 예에서는 firstIndex 란 이름의 Variable을 java의 int 타입으로 받게 된다.


● public void list3(@RequestVariable(name="recordCountPerPage") String recordCountPerPage) 


list3 메소드에서도 파라미터로 @RequestVariable 어노테이션만을 사용하고 있다. recordCountPerPage 란 이름의 Variable 을 java의 String 타입으로 받게 된다. 위에서도 이와 같은 케이스의 사용 방법에 대해 설명했으니 구체적인 설명은 안하겠다. 


● public void modify1(@RequestDataSetList DataSetList dataSetList) 


modify1 메소드에서는 @RequestDataSetList 어노테이션을 사용했다. 위에서도 설명했지만 여러개의 DataSet이 들어있는 DataSetList 객체를 받을 때 이 어노테이션을 사용하게 된다.


● public void modify2(@RequestDataSet(name="ds_input") List<SampleVO> dataSet, @RequestDataSetList DataSetList dataSetList)


modify2 메소드에서는 @RequestDataSet과 @RequestDataSetList 어노테이션을 사용했다. @RequestDataSetList 어노테이션에 대한 설명은 바로 위에서 얘기했기 때문에 더는 언급하지 않도록 하겠다. @RequestDataSet 어노테이션의 name 속성에 ds_input 을 주어 ds_input 이란 이름의 DataSet을 가져오게 된다. 위에서 @RequestDataSet 어노테이션을 설명했을때 DataSet을 Collection 인터페이스를 구현한 객체로 받는다고 했다. 그래서 Collection 인터페이스를 상속받은 List 인터페이스를 구현한 객체로 받게 된다. List에 들어가는 클래스 객체로 여기서는 SampleVO 클래스 객체로 설정되어 있다. DataSet을 구성하는 row 갯수 만큼 SampleVO 클래스 객체가 만들어져서 List 객체에 들어가게 된다. 그리고 해당 row와 매치되는 SampleVO의 객체에서 해당 column 이름과 SampleVO의 멤버변수 이름이 같으면 DataSet의 row, column에 해당되는 값이 멤버변수 값으로 할당된다. 여기서는 List 인터페이스를 구현한 클래스(ex: ArrayList, LinkedList 등)로 설정한게 아니라 List 인터페이스로 되어 있는데 List 인터페이스를 사용할 경우 HandlerMethodArgumentResolver 에서는 ArrayList 클래스 객체로 만들어준다. 구체적인 내용에 대해서는 HandlerMethodArgumentResolver에 대한 설명을 할때 좀더 언급하겠다


● public void modify3(@RequestDataSet(name="ds_input") ArrayList<SampleVO> dataSet)


modify3 메소드에서도 @RequestDataSet 어노테이션을 사용했다. modify2 메소드에서는 List 인터페이스를 사용한것과는 달리 List 인터페이스를 구현한 클래스인 ArrayList 클래스를 사용했다.


● public void modify4(@RequestDataSet(name="ds_input") List<Map<String, Object>> dataSet)


modify4 메소드에서도 @RequestDataSet 어노테이션을 사용했다. modify2, modify3 메소드와는 달리 List 객체에 들어가는 객체로 특정 클래스가 아니라 Map을 사용했다. Map을 사용할 경우 DataSet의 row 갯수 만큼 Map 객체가 만들어지게 된다. 그리고 Map 객체를 만들때 DataSet의 column 이름을 Map의 key로 사용해서 Map 객체 하나를 만들게 된다. 이때 만드는 Map은 HashMap 클래스 객체를 만들게 된다. 이것 또한 구체적인 내용은 HandlerMethodArgumentResolver에 대한 설명을 할때 좀더 언급하겠다.


● public void modify5(@RequestDataSet(name="ds_input") Set<SampleVO> dataHashSet) 


modify5 메소드에서도 @RequestDataSet 어노테이션을 사용했다. 지금까지 설명했던 @RequestDataSet 어노테이션에서는 List 인터페이스 또는 List 인터페이스 구현 클래스를 사용한것과는 달리 Set 인터페이스를 사용했다. Set 인터페이스 또한 Collection 인터페이스를 상속받은 인터페이스이기 때문에 사용이 가능하다. Set 인터페이스를 사용할 경우엔 HashSet 클래스를 만든뒤에 여기에 Generic으로 설정한 클래스 객체를 만들어 넣는다. 


지금까지 Xplatform에서 전달되는 DataSet 및 Variable을 Controller 에서 HandlerMethodArgumentResolver 를 통해 어떠한 형태로 받을지에 대한 설명을 했다. 다음글에서는 HandlerMethodArgumentResolver 에 대한 구체적인 설명을 진행하도록 하겠다.


 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다

이번 글에서는 이번 템플릿에서 사용하게 될 HttpServletRequestWrapper 클래스에 대해 얘길 해볼까 한다. 이것도 연재하는 내용을 알기 위한 선제지식 성격이 강하긴 한데 이전 글에서 언급하기에는 내용이 긴데다가 이 내용은 이번 템플릿에서뿐만 아니라 다른 상황에서도 쓰일 성격이 다분히 존재하기 때문에 글을 별도로 할당해서 쓰도록 하겠다.


Xplatform 같은 Ria Tool 뿐문 아니라 웹브라우저와 같은 웹 클라이언트는 WAS에 접속해서 데이터를전송하게 되면 HttpServletRequest 객체가 생성이 된다. 엄밀하게 말하면 통신이기 때문에 header 정보와 body가 있게 된다.  HttpServletRequest 객체에서 header 정보는 언제든 읽어올 수 있지만 body는 일단 한번 읽으면 다시 읽을수가 없다. 생각해보면 내가 보낸 통신이란 하나의 흐름을 그 상태 그대로 다시 재현할 수는 없는것이다. 같은 내용을 다시 보내면 보내는 것이지 그때의 그 상태 그대로를 재현할 수는 없는 것이다. 이러한 상황은 Spring을 이용한 Web 개발을 할 때도 만나게 되는데 우리가 Restful 프로그래밍을 할때 사용하는 어노테이션으로 @RequestBody 란 어노테이션을 사용하게 된다. 이 어노테이션이 하는 역할은 HttpServletRequest 객체에 있는 Request Body를 읽어서 이를 어노테이션이 붙은 변수에 저장하게 된다. 근데 이 어노테이션을 사용할 때 주의사항이 있다. @RequestBody 어노테이션을 두 번 이상 사용할 수 없다는 것이다. 다음의 예를 보자


@RequestMapping("/restfulTest")
public void restfulTest(@RequestBody String requestBody1
						, @RequestBody String requestBody2) {
...		
}


다음의 예는 메소드의 파라미터에 String 변수 2개를 사용하고 있는데 이 2개 모두 @RequestBody 어노테이션을 사용하고 있다. 이럴 경우에는 "Required request body is missing" 이란 메시지를 가진 HttpMessageNotReadableException 예외가 발생하게 된다. 이유는 첫번째 변수인 requestBody1 변수에서는 정상적으로 Request Body를 읽게 되지만 requestBody2 변수에서는 이미 requestBody1 변수에서 Request Body를 읽어버렸기 때문에 위에서 설명한 이유로 인해 읽어올 수 없어서 이러한 예외가 발생하게 된다. 그러면 어떻게 하면 Request의 Body를 여러번 읽어올 수 있을까? Request Body에 있는 내용을 어딘가에 저장해 놓은 뒤에 다시 읽을때는 Request Body에 있는 내용을 읽는게 아니라 저장해 놓은 곳에서 읽어오는 방법으로 이 상황을 풀어볼 수 있다. 이때 사용되는 것이 Wrapper 클래스이다.


Java Servlet Spec 에서는 HttpSerlvetRequest 인터페이스를 구현하고 이를 서브클래스 형태로 상속받아 만들수 있는 HttpServletRequestWrapper 클래스를 제공한다. 이 클래스를 상속받아서 지금 말했던 재사용을 구현 할 수 있다. 이 HttpServletRequestWrapper 클래스를 상속받아 만든 소스는 github에 있으니 여기에서 전체 소스를 볼 수 있으므로 이 글에서는 설명을 위해 부분부분을 보이면서 설명하도록 하겠다. 다음의 소스는 HttpRequestWrapper 클래스에서 알아두어야 할 부분만 남겨놓은 소스이다.


public class HttpRequestWrapper extends HttpServletRequestWrapper {

	private byte[] bodyData;
	
	public HttpRequestWrapper(HttpServletRequest request) throws IOException {
		super(request);
		// TODO Auto-generated constructor stub
		InputStream is = request.getInputStream();
		bodyData = StreamUtils.copyToByteArray(is);
	}

	@Override
	public ServletInputStream getInputStream() throws IOException {
		// TODO Auto-generated method stub
		final ByteArrayInputStream bis = new ByteArrayInputStream(bodyData);
		return new ServletImpl(bis);
	}
}

class ServletImpl extends ServletInputStream {

	private InputStream is;

	public ServletImpl(InputStream bis) {
		is = bis;
	}

	...
}


이 클래스에서는 2가지를 봐야 하는 것이 있는데, 하나는 HttpRequestWrapper 클래스의 멤버변수로 있는 byte 배열 변수인 bodyData이고, 또 하나는 이 클래스에서 다시 만들고 있는 ServletImpl 클래스이다. bodyData 변수가 하는 역할은 Web 클라이언트가 보낸 HttpServletRequest의 Request Body 부분을 저장해놓게 된다. HttpRequestWrapper 클래스의 생성자를 보면 Web Client가 보낸 HttpServletRequest 객체를 받아 getInputStream 메소드를 통해 body 부분을 Stream으로 읽어온뒤 이를 bodyData 부분에 복사하고 있다. 이런 식으로 저장해놓으면 언제든 해당 요청에 대한 body 부분을 이 변수를 필요한 시점마다 읽어와서 Request의 Body 부분을 여러번 읽어들여 재활용 할 수가 있게 된다. 이 bodyData를 어떻게 읽어오는가에 대한 고민을 하게 될텐데 HttpRequestWrapper 클래스의 getInputStream 메소드를 호출하는 시점에 이 bodyData를 return 해주면 된다. getInputStream 메소드가 Request의 Body 부분을 Stream 형태로 return 해주면 되는 것이기 때문에 그 형태에 맞춰서 bodyData 변수를 return 해주면 되는 것이다. 그래서 이 클래스의 getInputStream 메소드를 보면 bodyData 변수를 이용해서 Stream 형태의 객체를 만들어서 return 하게 되는데 바로 그 객체의 클래스가 ServletImpl 클래스이다. 요청할때 마다 새로 객체를 생성해서 return 하기 때문에 요청하는 스레드들간에 bodyData가 공유가 되지 않으므로 thread-safe 한 구조를 가지고 있는 형태가 된다. 그래서 이 Request Input Stream 을 읽어오는 입장에서는 bodyData가 있는 ServletImpl 클래스 객체를 통해서 작업을 진행하게 된다.


이렇게 만든 HttpRequestWrapper 클래스를 어떻게 사용할까? HttpRequestWrapper 클래스가 HttpServletRequest 객체를 변환하는 역할을 하는 것은 맞지만 이 글을 읽어보면 한가지 궁금증이 생길것이다. 


HttpRequestWrapper 클래스 객체를 어디서 생성하게 되는 거지?


이 클래스를 만들었다고 Spring에서 HttpServletRequest 객체가 HttpRequestWrapper 클래스 객체로 변환되는 것이 아니다. Spring MVC에서 Controller 클래스에서 HttpServletRequest 객체가 들어오기 전에 HttpServletRequest 객체가 HttpRequestWrapper 클래스 객체로 변환되어서 사용되는 것이다. 그러면 이러한 역할을 어디서 하는 걸까? web.xml의 filter에서 이러한 역할을 하게 된다. 여기서는 HttpServletRequestWrapperFilter 클래스를 filter 클래스로 만들어서 사용하고 있다. web.xml에서 filter 설정을 보면 다음과 같이 되어 있다.


<filter> 
	<filter-name>httpServletRequestWrapperFilter</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
	<filter-name>httpServletRequestWrapperFilter</filter-name>
	<url-pattern>*.do</url-pattern>
</filter-mapping>


여기서 보면 HttpServletRequestWrapperFilter 클래스를 사용하고 있지 않다. filter 이름은 HttpServletRequestWrapperFilter 라고 사용하고 있지만 이것은 어디까지나 filter 이름을 지정하는 것이기 때문에 아무 이름이나 가능하고 실질적으로는 filter-class 태그에 설정된 클래스가 filter 로 사용되는 것인데 여기서는 HttpServletRequestWrapperFilter 클래스를 사용하고 있는게 아니라 org.springframework.web.filter.DelegatingFilterProxy 클래스를 사용하고 있다. 왜 이런 설정을 하는 것일까?


Filter 인터페이스는 Spring과는 무관한 인터페이스이다. Filter 인터페이스는 Filter 인터페이스가 실행이 되는 WAS가 제어하는 것이지 Spring이 제어하는 것이 아니다. Spring이 제어하는 것은 Spring Bean으로 등록된 클래스이기 때문이다. 물론 Spring Bean으로 등록할 수도 있다. Spring Bean으로 등록할 경우의 이점은 Spring Bean으로 등록된 다른 Bean class 객체를 DI를 할 수 있기 때문이다. DI가 가능한 클래스는 Spring Bean으로 등록된 클래스만 가능하기 때문에 Filter 구현 클래스도 Spring Bean으로 등록해야 한다. 약간 설명이 장황했는데 정리하자면..


  1. Filter 구현 클래스가 Spring Bean으로 등록된다
  2. web.xml에서 Spring Bean 으로 등록된 Filter 구현 클래스를 사용한다


요렇게 된다. 그러면 web.xml에서 직관적으로 Spring Bean으로 등록된 Filter 구현 클래스를 사용할 수 있을까? 그렇지가 않다. 왜냐면 Spring Bean은 Context 안에서 등록되어져서 움직이기 때문에 Context에서 Filter 구현 클래스를 찾아서 등록되어져야 하기 때문이다. 이로 인해 Spring에서는 이러한 역할을 해주는 Proxy Class가 존재하게 되는데 그 역할을 하는 것이 DelegatingFilterProxy 클래스이다. 이 클래스가 Spring Context에서 Spring Bean으로 등록된 Filter 구현 클래스를 찾아 Filter로 등록해주게 된다.  이러한 과정이기 때문에 주의할 점이 하나 존재하게 되는데 그것은 Root Context와 Servlet Context로 분리해서 Spring Bean을 등록할 경우 Root Context에 Bean을 등록해야 한다는 것이다. Filter 구현 클래스이기 때문에 Servlet Context에 이를 등록하려 하겠지만 Servlet Context는 filter에 대한 설정이 마쳐진 뒤에 Servlet Context가 구현되어지기 때문에 DelegatingFilterProxy에서는 Filter 구현 클래스 Spring Bean을 찾을수 없게 된다. 때문에 비록 Filter 구현 클래스이지만 Root Context에 이를 등록하여야 제대로 된 동작이 이루어진다. 그러면 이 클래스를 Root Context에서 어떻게 등록했을까? 이 클래스의 소스를 보면 @Service 어노테이션이 붙어있다. 프로젝트의 전체적인 설정을 보면 @Service 어노테이션이 붙은 클래스는 Root Context에 등록되도록 했기 때문에 이 클래스에 @Service 어노테이션을 붙이면 Root Context에 Spring Bean이 등록되는 것이다.


이번에는 HttpServletRequest 클래스의 Wrapper 클래스인 HttpRequestWrapper 클래스의 제작 이유와 이를 설정하는 방법에 대해 얘기했다. 사전지식은 이정도로 마치고 다음 글부터는 본격적인 내용에 들어가도록 하겠다.


 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) - 예외처리


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Spring 카테고리의 포스트 목록을 보여줍니다