본문 바로가기

프로그래밍/Spring

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

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