본문 바로가기

프로그래밍/Spring

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

이전 글까지는 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) - 예외처리