자바5에서 Generic이 나온 이후로 특정 타입을 가지는 Map은 Map<String, String> 같은 식으로 키와 밸류의 타입을 명시적으로 지정해서 타입 안전성을 확보할 수 있는데, 정해진 특정 타입이 아니라 다양한 타입을 지원해야 하는 Heterogeneous Map이 필요하다면 타입 안전성을 확보하기 위해 다른 방법이 필요하다. 이럴 때 타입 토큰을 이용할수 있다.
// 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다. String v1 = simpleTypeSafeMap.get(String.class); Integer v2 = simpleTypeSafeMap.get(Integer.class);
System.out.println(v1); System.out.println(v2);
// 아래와 같은 List<String>.class라는 클래스 리터럴은 언어에서 지원해주지 않으므로 사용 불가!! // typeSafeMap.put(List<String>.class, Arrays.asList("a", "b", "c")); } }
수퍼 타입 토큰
수퍼 타입 토큰은 앞에서 살펴본 것처럼 List<String>.class라는 클래스 리터럴이 존재할 수 없다는 한계를 뛰어넘을 수 있게 해주는 묘수라고 할 수 있다. Neal Gafter라는 사람이 http://gafter.blogspot.kr/2006/12/super-type-tokens.html 에서 처음 고안한 방법으로 알려져 있다. 수퍼급의 타입 토큰이 아니라, 수퍼 타입을 토큰으로 사용한다는 의미다.
수퍼 타입 토큰은 상속과 Reflection을 기발하게 조합해서 List<String>.class 같은, 원래는 사용할 수 없는 클래스 리터럴을 타입 토큰으로 사용하는 것과 같은 효과를 낼 수 있다.
앞에서 클래스 리터럴을 설명할 때, String.class의 타입이 Class<String>이라고 했었다. Class<String>이라는 타입 정보를 String.class라는 클래스 리터럴로 구할 수 있었던 덕분에 타입 안전성을 확보할 수 있었다.
List<String>.class도 타입을 구할 수만 있다면 타입 안전성을 확보할 수 있다는 것은 마찬가지다. 다만, Class<String>와는 달리 Class<List<String>>라는 타입은 List<String>.class 같은 클래스 리터럴로 쉽게 구할 수 없다는 점이 다르다. 하지만 어떻게든 Class<List<String>>라는 타입을 구할 수 있다면, 우리는 타입 안전성을확보할 수 있다.
Class.getGenericSuperclass()
결론부터 말하면 우리의 구세주는 Class에 들어있는 public Type getGenericSuperclass() 이놈이다.
Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();
// ~~~$1Super<java.util.List<java.lang.String>> 라고 나온다!! System.out.println(typeOfGenericSuperclass);
// 수퍼 클래스가 ParameterizedType 이므로 ParameterizedType으로 캐스팅 가능 // ParameterizedType의 getActualTypeArguments()으로 실제 타입 파라미터의 정보를 구한다!! Type actualType = ((ParameterizedType) typeOfGenericSuperclass).getActualTypeArguments()[0];
단순한 클래스 리터럴로는 구할 수 없었던 Class<List<String>>라는 타입 정보를, 껍데기 뿐이지만 한 없이 아름다운 수퍼 클래스와 위대한 구세주 getGenericSuperclass(), 그리고 getActualTypeArguments()를 이용해서 구했다.
put(java.lang.Class<T>, T)in SimpleTypeSafeMap cannot be applied to (java.lang.reflect.Type, java.util.List<T>) reason: no instance(s) of type variable(s) T exist so that Type conforms to Class<T>
Class<?>만 받을 수 있는 SimpleTypeSafeMap은 이제 퇴장할 때가 된 것 같다. Class<?>보다 더 General한 java.lang.reflect.Type 같은 키도 받을 수 있도록 약간 고도화한 TypeSafeMap을 만날 때가 되었다.
그리고 빈 껍데기 였던 Super<T>도 이름을 TypeReference<T>로 바꾸고 고도화해보자. 먼저 Super<T>를 TypeReference<T>로 바꿔보자.
TypeReference
Super<T>를 TypeReference<T>로 바꾸는 것을 먼저하는 이유는 TypeReference<T>가 가진 정보가 TypeSafeMap의 키로 사용될 것이기 때문이다.
먼저 코드를 보고 설명을 이어가자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
publicabstractclassTypeReference<T> {
private Type type;
protectedTypeReference(){ Type superClassType = getClass().getGenericSuperclass(); if (!(superClassType instanceof ParameterizedType)) { // sanity check thrownew IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다."); } this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0]; }
public Type getType(){ return type; } }
맨 위에서부터 순차적으로 살펴보자.
abstract
TypeReference를 abstract로 선언했는데, 이유는 new TypeReference<List<String>>()이 아니라 항상 new TypeReference<List<String>>() {} 형식으로 생성하기 위해서다. 왜냐하면, 타입 파라미터 정보를 구하려면 수퍼 타입 토큰을 이용해야 하는데, 수퍼 타입 토큰을 이용하려면 언제나 누군가의 수퍼 클래스로 존재해야 하기 때문이다.
잘 와닿지 않는다면 앞에서 단순하게 Sub와 Super를 이용했을 때의 코드를 살펴보면 느낌이 올 것이다.
1 2 3 4 5 6 7 8 9 10 11
classSuper<T> {}
classSubextendsSuper<List<String>> {} Sub sub = new Sub(); Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();
// 위의 세 줄을 한 줄로 쓰면 아래와 같다. Type typeOfGenericSuperclass = new Super<List<String>>(){}.getClass().getGenericSuperclass();
// Super를 TypeReference로 바꾸면 Type typeOfGenericSuperclass = new TypeReference<List<String>>(){}.getClass().getGenericSuperclass();
타입 파라미터 정보를 담는 type
다음은 Type type이라는 인스턴스 변수다. 아래와 같이 생성자를 통해서 타입 파라미터의 타입 정보를 type에 담는다.
그리고 생성자가 항상 타입 파라미터와 함께 사용되도록, ParameterizedType를 이용해서 sanity check를 적용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
abstractclassTypeReference<T> {
private Type type;
protectedTypeReference(){ Type superClassType = getClass().getGenericSuperclass(); if (!(superClassType instanceof ParameterizedType)) { // sanity check thrownew IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다."); } this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0]; }
public Type getType(){ return type; } }
TypeReference는 준비가 되었다. 이제 TypeSafeMap 차례다.
TypeSafeMap
키의 타입 변경
먼저 사용했던 SimpleTypeSafeMap은 key로 Class<?> 타입만을 받을 수 있다는 제약 사항 때문에 퇴장했었다. 이를 개선한 TypeSafeMap은 Class<?>보다 더 일반화된 java.lang.reflect.Type을 key로 받는다.
먼저 SimpleTypeSafeMap의 이름을 TypeSafeMap으로 바꾸고, 내부의 map의 key로 사용되는 Class<?> 부분을 Type으로 바꾼다.
1 2 3 4 5 6 7 8 9 10 11 12 13
publicclassTypeSafeMap{ // private Map<Class<?>, Object> map = new HashMap<>(); private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
public <T> voidput(Class<T> k, T v){ map.put(k, v); } public <T> T get(Class<T> k){ return k.cast(map.get(k)); } }
put()의 개선
TypeSafeMap의 put()에는 수퍼 타입을 추출할 수 있는 TypeReference<T>를 key로 받도록 바꾼다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
publicclassTypeSafeMap{ // private Map<Class<?>, Object> map = new HashMap<>(); private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
// public <T> void put(Class<T> k, T v) { // map.put(k, v); // } public <T> voidput(TypeReference<T> k, T v){ // 수퍼 타입을 추출할 수 있는 TypeReference<T>를 인자로 받음 map.put(k.getType(), v); // key가 Type으로 바뀌었으므로 기존의 k 대신 k.getType()으로 변경 }
public <T> T get(Class<T> k){ return k.cast(map.get(k)); } }
get()의 개선
key로 사용되는 Type 자리에는 타입 파라미터를 사용하지 않는 String 같은 일반 클래스도 올 수 있고, 타입 파라미터를 사용하는 List<String>같은 ParameterizedType의 클래스도 올 수 있다. 이 두 경우를 모두 처리하기 위해 다음과 같이 get()을 개선한다.
// private Map<Class<?>, Object> map = new HashMap<>(); private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
// public <T> void put(Class<T> k, T v) { // map.put(k, v); // } public <T> voidput(TypeReference<T> k, T v){ // 수퍼 타입을 추출할 수 있는 TypeReference<T>를 인자로 받음 map.put(k.getType(), v); // key가 Type으로 바뀌었으므로 기존의 k 대신 k.getType()으로 변경 }
// public <T> T get(Class<T> k) { // return k.cast(map.get(k)); // } public <T> T get(TypeReference<T> k){ // key로 TypeReference<T>를 사용하도록 수정 if (k.getType() instanceof ParameterizedType) return ((Class<T>)((ParameterizedType)k.getType()).getRawType()).cast(map.get(k.getType())); else return ((Class<T>)k.getType()).cast(map.get(k.getType())); } }
조금 복잡해 보이지만, ParameterizedType인 경우에는 getRawType()을 이용해서 키에 사용된 타입 파라미터의 타입으로 캐스팅 해주도록 개선한 것 뿐이다.
protectedTypeReference(){ Type superClassType = getClass().getGenericSuperclass(); if (!(superClassType instanceof ParameterizedType)) { // sanity check thrownew IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다."); } this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0]; }
public Type getType(){ return type; } }
classTypeSafeMap{
private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경
public <T> voidput(TypeReference<T> k, T v){ // 수퍼 타입을 추출할 수 있는 TypeReference<T>를 인자로 받음 map.put(k.getType(), v); // key가 Type으로 바뀌었으므로 기존의 k 대신 k.getType()으로 변경 }
public <T> T get(TypeReference<T> k){ // key로 TypeReference<T>를 사용하도록 수정 if (k.getType() instanceof ParameterizedType) return ((Class<T>)((ParameterizedType)k.getType()).getRawType()).cast(map.get(k.getType())); else return ((Class<T>)k.getType()).cast(map.get(k.getType())); } }
// SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap(); TypeSafeMap typeSafeMap = new TypeSafeMap();
// 드디어 List<String> 을 쓸 수 있다!! // new TypeReference<List<String>>() {}를 사용해서 List<String>.class와 동일한 효과를!! typeSafeMap.put(new TypeReference<List<String>>() {}, Arrays.asList("A", "B", "C"));
// 바로 이거다! // List<String>.class 처럼 언어에서 지원해 주지 않는 클래스 리터럴을 사용하지 않고도 // List<String>라는 타입을 쓸 수 있게 되었다. List<String> listString = typeSafeMap.get(new TypeReference<List<String>>() {});
Super type token을 알기전에 일단 type token을 알아야 되는데 type token 이란 간단히 말해서 타입을 나타내는 토큰(?)이다. 예를들어 String.class는 클래스 리터럴이라하며 Class<String>가 타입토큰이라 말할 수 있다. 실제 Class은 String.class를 이용해서 메서드를 호출 할 수 있다.
Super type token 경우에는 제네릭과 관련이 많다. 일단 제네릭을 잘 모른다면 여기를 한번 보고 이글을 봐야 할 듯 하다.
우리가 흔히 API 통신을 하거나 특정한 데이터를 가공하기 위해 Object로 변환하기 위해 위와 같은 코드를 자주 이용한다. jackson도 마찬가지다. 아까 위에서 설명했듯이 Account.class 라는 클래스 리터럴을 이용해서 Account라는 타입을 파라미터로 넘겼다.
public <T> T fromJson(String json, Class<T> classOfT) throwsJsonSyntaxException {
Objectobject= fromJson(json, (Type) classOfT);
returnPrimitives.wrap(classOfT).cast(object);
}
위의 코드는 Gson의 fromJson 메서드이다. 아까 예제와 많이 비슷하다. Class<T>라는 타입을 파라미터로 받고 그 해당하는 T 타입을 리턴해 준다.
List
위의 경우에는 특별하게 주의 할 것 없지만 한개 주의 할 것이 있다. 만약 List 로된 json을 하고 싶다면 어떻게 할까? 아까 위에서 링크를 남겼던 곳에 가면 우리는 아래와 같은 코드를 작성할 수 없다고 했다.
LinkedTreeMap 을 Account 캐스팅을 할 수 없다는 것이다. 당연히 LinkedTreeMap은 Account로 형변환을 할 수 없다. 근데 왜 이런 에러가 발생할까? 그 이유는 List.class 클래스 리터럴에는 제네릭 정보가 없기 때문이다. 그렇기 때문에 gson은 List 제네릭 정보의 Account 라는 클래스 자체를 모른다. List의 어떤 값이 들어가야 될 지 모르니 그냥 Map으로 파싱하는 것이다. jackson의 경우에는 자바의 LinkedHashMap 으로 파싱한다. 그렇다면 어떻게 이 문제를 해결할까?
Super type token
이런 제네릭 정보가 지워지는 문제 때문에 Super type token 기법이 생겨났다. Super type token은 수퍼타입을 토큰으로 사용하겠다는 의미이다. 이건 또 무슨말인가? 제네릭 정보가 컴파일시 런타임시 다 지워지지만 제네릭 정보를 런타임시 가져올 방법이 존재한다. 제네릭 클래스를 정의한 후에 그 제네릭 클래스를 상속받으면 런타임시에는 제네릭 정보를 가져올 수 있다.
public classSuperTypeToken<T>{
}
public classTypeTokenextendsSuperTypeToken<String>{
}
System.out.println(TypeToken.class.getGenericSuperclass()); //SuperTypeToken<java.lang.String>
위와 같이 SuperTypeToken 을 제네릭으로 만든 후에 TypeToken 클래스에 SuperTypeToken 을 상속받으면된다. 그럼 위와 같이 SuperTypeToken<java.lang.String> 정보를 가져올 수 있다.
이걸 이용해서 우리는 아까 Gson에서 하지 못했던 (gson에서 하지 못한건 아니지..) List 를 형태로 파싱 할 수 있다. 아래의 TypeToken은 Gson에 있는 클래스이다. 필자가 만든 클래스와는 다르다.
TypeToken 을 익명 클래스로 작성하였다. 이렇게 한다면 상속한 클래스를 만들지 않았지만 실제로 내부적으로 임의의 클래스를 만든다. 그래서 그 클래스의 인스턴스만 한개 돌려주는 것 뿐이다. Gson, 혹은 기타 다른 TypeToken 클래스들은 {} 가 없다면 컴파일 에러를 발생시킨다. gson의 TypeToken 클래스 생성자는 protected 접근제한을 두고 있기 때문이다. 그래서 {}를 꼭 사용해야 한다. 필자도 처음에는 저걸 왜 사용할까 생각했는데 제네릭을 알고 나니 이해가 되었다. 물론 Jackson도 gson의 TypeToken 과 동일한 역할을 하는 TypeReference가 존재한다.
그냥 바로 메서드를 호출할 때 생성해서 사용해도 된다. 위의 코드가 제일 깔끔한 듯 싶다. 근데 왜 Gson은 TypeToken을 받는 fromJson메서드를 만들지 않았을까? 굳이 사용자가 getType() 메서드도 호출해야 한다니.. Jackson 경우에는 TypeReference를 받는 메서드가 존재하는데.. 이래서 Jackson을 더..
Spring RestTemplate
마지막으로 Spring의 Super Type Token도 살펴보자. Spring에서 자주 사용될 Super Type Token은 바로 RestTemplate 클래스이다. 이 클래스 용도는 클래스 이름과 동일하게 Rest API를 호출할 때 사용되는 클래스이다. 통신을 할때 Json이나 Xml로 받을 메세지를 Object로 변환 할 수 있는데 이때에도 List 같은 클래스 리터럴을 사용하고 싶다면 Spring의 ParameterizedTypeReference 클래스를 이용하면 된다.
RestTemplate restTemplate = new RestTemplate();
restTemplate.exchange("http://localhost:8080", HttpMethod.GET,
null, new ParameterizedTypeReference<List<Account>>() {});
아주 간편하게 사용가능 하다. 필자가 말한 세개의 Super Type Token 클래스 구현은 거의 동일하다. Gson의 TypeToken 와, Jackson의 TypeReference, Spring의 ParameterizedTypeReference 모두 구현은 비슷하게 되어 있다.
우리는 이렇게 Super Type Token에 대해서 살펴봤다. Gson과 Jackson을 사용하다보면 new TypeToken<List<String>>() {} 이런 익명클래스를 사용하곤 했는데 왜 저렇게 사용할까 생각은 했지만 무심코 넘어갔다. 이제는 왜 저렇게 사용하는지 알게 되었으니 필요하다면 자주 이용하자.
In this article, we will provide introductory details on Spring Converters and Formatters. Converter components are used for converting one type to another type and also to provide a cleaner separation by forcing to place all such conversion related code in one single place. Spring already supports built-in converters for the commonly used types and the framework is extensible enough for writing custom converters as well. Spring Formatters come into picture to format the data according to the display where it is rendered. Examples may include formatting date/timestamp values according to locales etc. The first section of this article deals with Converters whereas the rest deals with Formatters and plenty of code samples are given at appropriate places for better illustration. This article assumes that readers has the sufficient knowledge on Spring Framework and its workflow. If you are beginner looking for basic concepts on Spring Framework, please read Introduction to Spring Framework and Introduction to Spring Web Flow (SWF). The following section provides the list of populate articles in the Spring Framework. also read:
In this section, we will look into the series of Built-in Converters in Spring. It’s always worthwhile to see the exhaustive list of pre-built converters before even thinking of writing a custom converter that suits for a particular business need. Converters in Spring are available as services and typically client will make use of converter services while working with the conversion process.
package net.javabeat.articles.spring.converter.builtin;
import java.util.List;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;
public class BuiltInForStringTypeTest {
public static void main(String[] args) {
GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService();
testToArray(conversionService);
testToCollection(conversionService);
testToBoolean(conversionService);
testToCharacter(conversionService);
testToNumber(conversionService);
testToEnum(conversionService);
}
private static void testToArray(GenericConversionService conversionService){
String[] stringArray = conversionService.convert("One,Two,Three", String[].class);
for (String element : stringArray){
System.out.println("Element is " + element);
}
}
private static void testToCollection(GenericConversionService conversionService){
@SuppressWarnings("unchecked")
List listOfStrings = conversionService.convert("One,Two,Three", List.class);
for (String element : listOfStrings){
System.out.println("Element is " + element);
}
}
private static void testToBoolean(GenericConversionService conversionService){
Boolean data = null;
data = conversionService.convert("true", Boolean.class);
System.out.println("Boolean value is " + data);
data = conversionService.convert("no", Boolean.class);
System.out.println("Boolean value is " + data);
}
private static void testToCharacter(GenericConversionService conversionService){
Character data = null;
data = conversionService.convert("A", Character.class);
System.out.println("Character value is " + data);
data = conversionService.convert("Exception", Character.class);
System.out.println("Character value is " + data);
}
private static void testToNumber(GenericConversionService conversionService){
Integer intData = conversionService.convert("124", Integer.class);
System.out.println("Integer value is " + intData);
Float floatData = conversionService.convert("215f", Float.class);
System.out.println("Float value is " + floatData);
}
private static void testToEnum(GenericConversionService conversionService){
TaskStatus taskStatus = conversionService.convert("PENDING", TaskStatus.class);
System.out.println("Task Status is " + taskStatus);
}
}
Go through the above code listing which illustrates the concept of converters. In the above sample code, an attempt is made to convert a string object to various different types such as Array, Collection, Boolean, Character, Number and Enumeration. The example is pretty straightforward, though it is worthwhile to provide description on what each test method is doing. It is essential to the client to have an instance of ConversionService before working out with converts and the same is obtained through ConversionServiceFactory. For converting a string to an array (a string array), the string has to be comma-delimited, though there is currently no support for the client to pass the delimiter. The same thing holds good for converting a string to a Collection type. String objects can even be converted to Boolean objects. The valid string values for a corresponding ‘true’ Boolean object are ‘true’, ‘on’, ‘yes’ and ‘1’, whereas for a ‘falsify’ a Boolean object, the allowed values are ‘false’, ‘off’, ‘no’ and ‘0’. For converting a string to a character, the string holding the character must be of length one, else an ‘IllegalArgumentException’ will be thrown at the run-time.
package net.javabeat.articles.spring.converter.builtin;
public enum TaskStatus {
STARTED,
COMPLETED,
PENDING
}
Similarly, while converting a string to an enumeration, the appropriate valueOf() method will be called on the Enum object. In the above example, an attempt is made to convert the task status to a TaskStatus enumeration object.
Converting Array types to Collection and String
In the following example, we will see how to use Converters for converting an Array type to other different types such as Collection, String and to a Generic object.
package net.javabeat.articles.spring.converter.builtin;
import java.util.List;
import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.StringToArticleConverter;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;
public class BuiltInForArrayTypeTest {
public static void main(String[] args) {
GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService();
testToCollection(conversionService);
testToString(conversionService);
testToObject(conversionService);
}
private static void testToCollection (GenericConversionService conversionService){
@SuppressWarnings("unchecked")
List listOfColors = conversionService.convert(
new String[]{"Red","Blue","Green"}, List.class);
for (String color : listOfColors){
System.out.println("Color is " + color);
}
}
private static void testToString(GenericConversionService conversionService){
String colors = conversionService.convert(
new String[]{"Red","Blue","Green"}, String.class);
System.out.println("Colors is " + colors);
}
private static void testToObject(GenericConversionService conversionService){
conversionService.addConverter(new StringToArticleConverter());
Article article = conversionService.convert(
new String[]{"Introduction to Google Guice,Google Guice"}, Article.class);
System.out.println("Article name is " + article.getName());
System.out.println("Article category is " + article.getCategory());
}
}
In the above example, we have converted the array of string (representing colors) to a list of string objects by calling the convert() defined on the ConversionService object. The example also illustrates how a conversion can happen from a string array type to a string type. Note that the resultant string will have the comma delimiter strings from the string array. The method ‘testToObject()’ introduces a custom Domain object called ‘Article’ which will be explained later.
DO YOU REALLY KNOW YOUR MOBILES?
How many times does the average person unlock their phone a day?
In 1973, Martin Cooper made the first mobile phone call to whom?
In Japan, 90% of phones have which feature?
99% of all mobile malware is targeted at which mobile users?
Exposure to cell phone radiation right before sleep has been known to cause:
Nomofobia is a psychological condition where a person:
In Malaysia, it is legal to do what via text message?
In 1983, the first cell phones were sold in the U.S. for how much each?
What is the fate of 100,000 mobile phones a year in Britain?
At 250 million gadgets sold, the best-selling gadget in history is:
15
20
110
55
201
Veronica Lake
His mother
A rival telecommunications company
The president of Russia
Eyeball tracking
Two screens
Projector
Waterproof
iOS
Windows
Blackberry
Android
Insomnia
Forgetfulness
Incomplete digestion
Depression
Is afraid of being without their phone
Is repulsed by touch screens
Fears new technology
Misplaces their phone daily
Divorce your partner
Confess guilty to a crime
Buy a house
Declare bankruptcy
$62,000
$3,095
$25
$400
Stolen by family members
Overheated in parked cars
Broken by toddlers
Dropped down the toilet
3.4 Samsung Galaxy C
iPhone 6
Nokia 1100
Android 1.6. Donut
START NEXT QUIZ
You Scored A Fair
5/10
CHALLENGE
YOUR FRIENDS
NEXT QUIZ STARTS IN:
10
Advertisement
package net.javabeat.articles.spring.converter.builtin;
import java.util.Arrays;
import java.util.List;
import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.ArticleToStringConverter;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;
public class BuiltInForCollectionTypeTest {
public static void main(String[] args) {
GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService();
testToArray(conversionService);
testToString(conversionService);
testToObject(conversionService);
}
private static void testToArray(GenericConversionService conversionService){
List languages = Arrays.asList("C", "C++", "Java");
String[] stringArray = conversionService.convert(languages, String[].class);
for (String language : stringArray){
System.out.println("Language is " + language);
}
}
private static void testToString(GenericConversionService conversionService){
List languages = Arrays.asList("C", "C++", "Java");
String languagesAsString = conversionService.convert(languages, String.class);
System.out.println("All languages -->" + languagesAsString);
}
private static void testToObject(GenericConversionService conversionService){
conversionService.addConverter(new ArticleToStringConverter());
Article articleObject = new Article("Introduction to Google Guice", "Google Guice");
String articleAsString = conversionService.convert(
new Article[]{articleObject}, String.class);
System.out.println("Article -->" + articleAsString);
}
}
The above example shows how to convert Collection types to Array, string and Object type. Note that the test methods follow the very similar pattern as shown in the previous example.
Custom Converters using Spring Converters and Formatters
So far we have seen how to make use of Spring Converters for performing conversions between the basic and the very often used data types in the Java programming language. However that may not often suffice and there will be always a necessity to do conversions on user-defined objects. As always Spring’s framework is extensible and in this section, we will illustrate the usage of Custom converters for converting user-defined or custom objects.
package net.javabeat.articles.spring.converter.custom;
public class Article {
private String name;
private String category;
public Article(String name, String category){
this.name = name;
this.category = category;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
The user-defined class we will be using for illustrating the concept as well as throughout this article is ‘Article’. As you can see in the above definition, the structure of ‘Article’ class is very simple; it has two properties, the ‘name’ and the ‘category’.
package net.javabeat.articles.spring.converter.custom;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
public class ArticleToStringConverter implements Converter<Article, String>{
@Override
public String convert(Article article) {
if (article == null){
throw new ConversionFailedException(TypeDescriptor.valueOf(Article.class),
TypeDescriptor.valueOf(String.class), article, null);
}
StringBuilder builder = new StringBuilder();
builder.append(article.getName());
builder.append("-");
builder.append(article.getCategory());
return builder.toString();
}
}
In this section, we will see how to convert a given ‘Article’ object to a ‘String’ object by writing a custom converter. As you can see in the above listing, for writing any custom converter, the interface Converter needs to be implemented and the method convert() has to be overridden. This interface uses Java Generics to achieve maximum compile-time safety and that is obvious in the declaration of the interface itself. The interface accepts the source and the target types as type parameters and the method convert() accepts the same source parameter and returns the target parameter. In our example case, the source parameter will be of type ‘Article’ and the target parameter will be of ‘String’. In the implementation of the convert() method, after performing suitable null-conditional checks, we return an instance of string object after concatenating the various properties of the article. Before seeing the usage of this Custom converter, we will also write the reverse converter class which tries to convert a string object to an Article object. Note the following class definition below. Here the source type is ‘String’ and the target type is ‘Article’.
package net.javabeat.articles.spring.converter.custom;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
public class StringToArticleConverter implements Converter<String, Article>{
@Override
public Article convert(String articleAsString) {
if (articleAsString == null){
throw new ConversionFailedException(TypeDescriptor.valueOf(String.class),
TypeDescriptor.valueOf(String.class), articleAsString, null);
}
String[] tempArray = articleAsString.split(",");
Article article = new Article(tempArray[0], tempArray[1]);
return article;
}
}
In the above sample, it is expected that the name and the category properties of the article object are concatenated as a string with comma as a delimiter. Hence appropriate parsing is done within the implementation of convert() method and an appropriate instance of Article object is constructed from the string.
package net.javabeat.articles.spring.converter.custom;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;
public class CustomConverterTest {
public static void main(String[] args) {
// test1();
test2();
test3();
}
private static void test1(){
GenericConversionService conversionService = new GenericConversionService();
Converter<Article, String> customConverter = new ArticleToStringConverter();
conversionService.addConverter(customConverter);
String result = customConverter.convert(null);
System.out.println("Result is " + result);
}
private static void test2(){
GenericConversionService conversionService = new GenericConversionService();
Converter<Article, String> customConverter = new ArticleToStringConverter();
conversionService.addConverter(customConverter);
Article article = new Article("Introduction to Spring Converters", "Core Spring");
String result = conversionService.convert(article, String.class);
System.out.println("Result is '" + result + "'");
}
private static void test3(){
GenericConversionService conversionService = new GenericConversionService();
Converter<String, Article> customConverter = new StringToArticleConverter();
conversionService.addConverter(customConverter);
String articleAsString = new String(
"Introduction to Spring Converters,Core Spring");
Article result = conversionService.convert(articleAsString, Article.class);
System.out.println("Article name is " + result.getName());
System.out.println("Article category is " + result.getCategory());
}
}
It is necessary to make any Custom Converters visible to the Spring’s Conversion framework by appropriately registering them. Registration of a custom converter to the Converter registry can be achieved by calling the method addConverter() defined on the ConversionService object. In the ‘test2()’ method, we have registered the ‘ArticleToStringConverter’ and have called the method ‘convert()’ defined on ConversionService by passing in an article object. Similarly in the method test3(), the converter ‘StringToArticleConverter’ is registered and a similar attempt is made to convert the String object to an Article object.
Converter Factory
In this section, we will see the usage of Converter Factories which follow the Factory Design pattern for creating Converter objects. Converter Factory provides a centralized place for creating converter objects. This prevents the client from directly getting exposed to a series of custom converter classes in an application.
package net.javabeat.articles.spring.converter.factory;
import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.StringToArticleConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
public class StringToArticleConverterFactory implements ConverterFactory<String, Article>{
@SuppressWarnings("unchecked")
@Override
public Converter<String, T> getConverter(Class arg0) {
return (Converter<String, T>) new StringToArticleConverter();
}
}
Have a look at the above sample where we create a factory for ‘StringToArticle’ converter. Note that any converter factory class must implement ‘ConvertFactory’ interface and the method getConverter() has to overridden that will create and return a suitable converter object. Similarly, in the below listing, a factory is created for wrapping the implementation for ‘StringToArticleConverter’.
package net.javabeat.articles.spring.converter.factory;
import net.javabeat.articles.spring.converter.custom.Article;
import net.javabeat.articles.spring.converter.custom.ArticleToStringConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
public class ArticleToStringConverterFactory implements ConverterFactory<Article, String>{
@SuppressWarnings("unchecked")
@Override
public Converter<Article, T> getConverter(Class arg0) {
return (Converter<Article, T>)new ArticleToStringConverter();
}
}
When it comes to the client usage, we register the custom converter factory implementations by calling the method ‘addCustomFactory()’ defined on the ConversionService object. Have a glance over the following piece of client code.
package net.javabeat.articles.spring.converter.factory;
import net.javabeat.articles.spring.converter.custom.Article;
import org.springframework.core.convert.support.GenericConversionService;
public class FactoryTest {
public static void main(String[] args) {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverterFactory(new ArticleToStringConverterFactory());
conversionService.addConverterFactory(new StringToArticleConverterFactory());
String articleAsString = "Java Programming,Java";
Article article = conversionService.convert(articleAsString, Article.class);
System.out.println("Article name is " + article.getName());
System.out.println("Article category is " + article.getCategory());
articleAsString = conversionService.convert(article, String.class);
System.out.println("Article as string is '" + articleAsString + "'");
}
}
The custom converter factory implementations ‘ArticleToStringConversionFactory’ and ‘StringToArticleConversionFactory’ are appropriately registered by calling the method addConversionFactory(). Note that when this method is called, the converter instances returned from the converter factory will be registered and maintained in the Converter registry.
Built-in Formatters in Spring Converters and Formatters
Similar to built-in components available for Converter components, there are number of built-in Formatter components for formatting date, timestamp and numeric data.
package net.javabeat.articles.spring.formatter.builtin;
import java.util.Date;
import java.util.Locale;
import org.springframework.format.Formatter;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.format.number.NumberFormatter;
public class FormatterTest {
public static void main(String[] args) throws Exception{
testDateFormatter();
testNumberFormatter();
}
private static void testDateFormatter(){
Formatter dateFormatter = new DateFormatter();
String dateAsString = dateFormatter.print(new Date(), Locale.CHINA);
System.out.println("Date as string in chinese locale is " + dateAsString);
}
private static void testNumberFormatter() throws Exception{
NumberFormatter doubleFormatter = new NumberFormatter();
doubleFormatter.setPattern("#####.###");
String number = doubleFormatter.print(new Double(12325.1144d), Locale.ITALIAN);
System.out.println("Number is " + number);
}
}
Have a look at the above sample code. Formatters in Spring are modeled through the Formatter interface which in turn extends the Printer and the Parser interface. The interface Printer encapsulates the behavior of displaying the data on a particular locale whereas the interface Parser is responsible for parsing the data for the given locale. Implementations are free to ignore the locale if they want to. The above example illustrates the usage of Date and Number Formatters.
Custom Formatters
It is always possible to write Custom formatters especially for parsing and formatting user-defined objects. For illustration purposes, we will consider the user-defined Domain object CreditCardNumber in this example.
package net.javabeat.articles.spring.formatter.custom;
public class CreditCardNumber {
private int firstFourDigits;
private int secondFourDigits;
private int thirdFourDigits;
private int fourthFourDigits;
public CreditCardNumber(){}
public CreditCardNumber(int firstFourDigits, int secondFourDigits,
int thirdFourDigits, int fourthFourDigits){
this.firstFourDigits = firstFourDigits;
this.secondFourDigits = secondFourDigits;
this.thirdFourDigits = thirdFourDigits;
this.firstFourDigits = firstFourDigits;
}
public int getFirstFourDigits() {
return firstFourDigits;
}
public void setFirstFourDigits(int firstFourDigits) {
this.firstFourDigits = firstFourDigits;
}
public int getSecondFourDigits() {
return secondFourDigits;
}
public void setSecondFourDigits(int secondFourDigits) {
this.secondFourDigits = secondFourDigits;
}
public int getThirdFourDigits() {
return thirdFourDigits;
}
public void setThirdFourDigits(int thirdFourDigits) {
this.thirdFourDigits = thirdFourDigits;
}
public int getFourthFourDigits() {
return fourthFourDigits;
}
public void setFourthFourDigits(int fourthFourDigits) {
this.fourthFourDigits = fourthFourDigits;
}
}
The listing for the model class CreditCardNumber is shown below. As one can see, the structure is pretty simple as it holds four integer properties representing the four ‘four digits number that appear in a credit card. For writing a custom formatter, we need to define the behavior of parsing and printing (displaying) the domain objects. Parsing of an object is encapsulated in the Parserinterface and one needs to implement this interface to write a custom parser. Again, the Parser interface supports compile-time safety by defining the generic type T.
package net.javabeat.articles.spring.formatter.custom;
import java.text.ParseException;
import java.util.Locale;
import org.springframework.format.Parser;
public class CreditCardNumberParser implements Parser{
@Override
public CreditCardNumber parse(String ccNumber, Locale locale) throws ParseException {
String digitsArray[] = ccNumber.split("-");
if (digitsArray == null || digitsArray.length != 4){
throw new org.springframework.expression.ParseException(-1, "Invalid format");
}
CreditCardNumber ccNumberObject = new CreditCardNumber();
ccNumberObject.setFirstFourDigits(Integer.parseInt(digitsArray[0]));
ccNumberObject.setSecondFourDigits(Integer.parseInt(digitsArray[1]));
ccNumberObject.setThirdFourDigits(Integer.parseInt(digitsArray[2]));
ccNumberObject.setFourthFourDigits(Integer.parseInt(digitsArray[3]));
return ccNumberObject;
}
}
Note that the method parse() has to be overridden for parsing the credit card number which will be passed as a string as designated by the first parameter. Note that the parse() method also accepts a Locale object as the second parameter. It is up to the implementation whether to consider or to ignore the ‘Locale’ parameter. For simplicity, in our example, we have ignored this parameter. It is expected that the credit card number is passed as a string with the delimiter being ‘-‘ between every set of four digits. The implementation checks if the format of the credit card number is correct, if not, it throws a ParseException thereby aborting the parse operation. Once the incoming string proves to be of correct format, an instance of Credit Card Number class is created and the delimited values obtained from the string is appropriately set to the object and returned.
Displaying the data is encapsulated through the interface Printer and a custom displayable class for displaying an object is expected to implement this interface and thereby providing the functionality in the print() method. Note that the first parameter passed to print() is the object itself and the second parameter is the Locale object. Again, it is up to the implementation to consider or to ignore the Locale parameter. Within the implementation of the print() method we have constructed a suitable string from the CreditCardNumber object.
Let us have a look at the implementation of Formatter class for formatting the credit card number. Formatter interface defines the abstract methods parse() and print() whose return types are Parser and Printer respectively. Because we have already provided the implementation of Parser and Printer, the methods parse() and print() redirects the control to the parse() and print() methods present in CreditCardNumberParser and CreditCardNumberPrinter respectively.
package net.javabeat.articles.spring.formatter.custom;
import org.springframework.format.support.FormattingConversionService;
public class Client {
public static void main(String[] args) {
FormattingConversionService service = new FormattingConversionService();
CreditCardNumberParser parser = new CreditCardNumberParser();
CreditCardNumberPrinter printer = new CreditCardNumberPrinter();
service.addFormatterForFieldType(CreditCardNumber.class, printer, parser);
// CreditCardNumberFormatter formatter = new CreditCardNumberFormatter(
// parser, printer);
// service.addFormatterForFieldType(CreditCardNumber.class, formatter);
test1(service);
test2(service);
}
private static void test1(FormattingConversionService service){
String ccNumber = "1111-2222-3333-4444";
CreditCardNumber ccNumberObject = service.convert(ccNumber, CreditCardNumber.class);
System.out.println(ccNumberObject.getFirstFourDigits());
System.out.println(ccNumberObject.getSecondFourDigits());
System.out.println(ccNumberObject.getThirdFourDigits());
System.out.println(ccNumberObject.getFourthFourDigits());
}
private static void test2(FormattingConversionService service){
CreditCardNumber ccNumberObject = new CreditCardNumber(
1111, 2222, 3333, 4444);
String ccNumber = service.convert(ccNumberObject, String.class);
System.out.println("CC Number is " + ccNumber);
}
}
In the client program, we have to register the customized formatter implementation of Credit Card Number object by method addFormatterForFieldType() defined on the FormattingConversionService object. Note that the service ‘FormattingConversionService’ can be used to parse and print data as well as can be used for registering customized formatters. The method addFormatterForFieldType() accepts the class type for which the formatting has to be applied, printer object and the parser object as its arguments. An overloaded version of the same method is available which takes the class type and the formatter object directly.
Conclusion
This article started with explaining the needs to have converter components and went on to explaining the various built-in converters available in Spring framework. Code samples were provided to illustrate about writing custom converters also. As a closure to converters, the need for Converter Factories was also discussed in detail. The final section of the article explained the needs for formatters and discussed the various built-in formatters available in Spring. It also provided assistance in writing custom formatters with examples. also read:
Spring에서 타입 변환이 발생하는 영역은 크게 2가지이다. 하나는 Bean 정의 XML에서 <property />를 이용해 설정한 값을 실제 Bean 객체의 Property에 바인딩 시킬 때인데, XML에 String으로 정의한 값을 해당 Property의 타입으로 변환해서 셋팅해야한다.
예를 들어, Movie 클래스가 다음과 같이 정의되어 있고,
public class Movie {
String id;
String name;
int ticketPrice;
}
'name'이라는 Property는 같은 String 타입이기 때문에 문제가 없지만, 'ticketPrice'의 경우 String으로 작성된 '7500'값을 int 타입의 7500으로 변환하여 바인딩 해야한다.
타입 변환이 발생하는 다른 한가지 경우는, 아래 코드 예와 같이 HTTP Request 파라미터로 들어온 사용자 입력 값들을 'Movie'라는 Model 객체에 바인딩시킬 때이다. 여기서도 마찬가지로 문자열로 표현된 값을 특정 타입으로 변환하는 과정이 필요하다.
@RequestMapping("/movies/new", method=RequestMethod.POST)
public String create(@ModelAttribute Movie movie, BindingResult results) {
this.movieService.create(movie);
status.setComplete();
return "redirect:/movies";
}
또한 단순히 타입의 변환이 아니라, 사용자가 보는 View에서 값에 "$45.22"와 같은 특정 Format이 적용되어 변환되어야 하는 경우도 종종 있다.
이 장에서는 이러한 타입 변환을 위해서 Spring에서 지원하고 있는 기술들에 대해서 자세히 알아보도록 하겠다.
13.1.PropertyEditor
Spring에서는 위에서 언급한 타입 변환을 위해서 기본적으로 JavaBeans 표준에서 제공하는 PropertyEditor를 사용해왔다. PropertyEditor는 String과 특정 타입 객체 간의 변환 로직을 구현할 수 있는 인터페이스이다.
13.1.1.Implementing Custom Editor
타입 변환시 호출되는 PropertyEditor의 메소드는 setValue()/getValue(), setAsText()/getAsText() 4가지 이다. PropertyEditorSupport를 상속받아서 setAsText()/getAsText() 메소드만 오버라이드하면 특정 타입 변환을 위한 PropertyEditor를 구현할 수 있다.
Spring에서 제공하고 있는 CustomBooleanEditor 코드를 조금 살펴보면, 아래와 같이 setAsText() 메소드에는 String값을 받아서 boolean값으로 변환하여 setValue() 해주는 로직이 구현되어 있고, getAsText() 메소드에는 getValue() 호출해서 가져온 값을 String으로 변환하여 리턴하는 로직이 구현되어 있다.
@Override
public void setAsText(String text) throws IllegalArgumentException {
String input = (text != null ? text.trim() : null);
if (this.allowEmpty && !StringUtils.hasLength(input)) {
setValue(null);
} else if (this.trueString != null && input.equalsIgnoreCase(this.trueString)) {
setValue(Boolean.TRUE);
} else if (this.falseString != null && input.equalsIgnoreCase(this.falseString)) {
setValue(Boolean.FALSE);
// 중략
} else {
throw new IllegalArgumentException("Invalid boolean value [" + text + "]");
}
}
@Override
public String getAsText() {
if (Boolean.TRUE.equals(getValue())) {
return (this.trueString != null ? this.trueString : VALUE_TRUE);
} else if (Boolean.FALSE.equals(getValue())) {
return (this.falseString != null ? this.falseString : VALUE_FALSE);
} else {
return "";
}
}
13.1.2.Default PropertyEditors
위에서 본 CustomBooleanEditor와 같이 Spring에서는 기본 타입에 대해서 이미 구현해놓은 여러가지 Build-in PropertyEditor들을 제공한다. Built-in PropertyEditor들은 모두 org.springframework.beans.propertyeditors 패키지 하위에 존재한다.
ClassEditor, FileEditor, InputStreamEditor, LocaleEditor, PropertiesEditor 등의 Built-in PropertyEditor들의 이름에서 볼 수 있듯이 Built-in PropertyEditor들은 변환할 타입에 'Editor'라는 이름을 붙인 클래스들이다. CustomNumberEditorr와 같이 사용자가 Customizing이 가능한 PropertyEditor에는 'Custom'이라는 접두어가 붙기도 한다. 이들은 모두 디폴트로 등록되어 내부적으로 사용되지만, CustomDateEditor와 StringTrimmerEditor는 디폴트로 등록되지 않기 때문에, 사용이 필요한 경우에는 반드시 직접 코드에서 등록해 주어야 한다.
13.1.3.Register Custom Editor
기본적으로 Spring에서는 Built-in PropertyEditor들을 미리 등록해놓고 사용하고 있다. 이외에 추가로 Custom Editor 등록이 필요한 경우 따로 등록을 해주어야 하는데, 이 장에서는 Custom PropertyEditor를 어떻게 등록할 수 있는 지에 대해서 알아보도록 하겠다. Spring MVC에서 사용자가 추가로 개발한 Custom PropertyEditor를 등록하는 방법에는 아래와 같이 3가지가 있다.
개별 컨트롤러에 적용
Controller에서 @InitBinder annotation을 이용하여 PropertyEditor 등록하는 메소드 정의
@InitBinder
public void initBinder(WebDataBinder binder) {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
}
전체 컨트롤러에 적용
어플리케이션 전반에서 많이 사용되는 Custom PropertyEditor의 경우 WebBindingInitializer 이용
WebBindingInitializer를 구현한 클래스 생성
public class ClinicBindingInitializer implements WebBindingInitializer {
@Autowired
private Clinic clinic;
public void initBinder(WebDataBinder binder, WebRequest request) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
binder.registerCustomEditor(PetType.class, new PetTypeEditor(this.clinic));
}
}
AnnotationMethodHandlerAdapter에 webBindingInitializer 속성을 이용해서 설정
다수의 컨트롤러에서 자주 사용되는 여러 개의 Custom PropertyEditor 셋트로 관리할 경우 PropertyEditorRegistrar 이용
PropertyEditorRegistrars를 구현한 클래스 생성
package com.foo.editors.spring;
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// 새로운 PropertyEditor 인스턴스 생성
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// 필요한 Custom PropertyEditor들 추가
}
}
@Inject
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
@InitBinder
public void initBinder(WebDataBinder binder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
13.1.4.PropertyEditor의 단점
PropertyEditor는 기본적으로 String과 특정 타입 간의 변환을 지원한다. PropertyEditor는 변환 과정 중에, 변환하려고 하는 Object나 String을 PropertyEditor 객체에 잠깐 저장하였다가 변환하기 때문에, 여러 Thread에서 동시에 사용하는 경우, 변환 도중에 가지고 있던 값이 변경되어 엉뚱한 변환 값을 전달할 수도 있다. 이런 이유에서 PropertyEditor는 Thread-Safe하지 않기 때문에, Sington Bean으로 사용하지 못하고 위에서 봤던 예제 코드에서 처럼 항상 'new'를 통해서 새로 생성해야 한다.
13.2.Spring 3 Type Conversion
앞서 언급했듯이 JavaBeans의 표준인 PropertyEditor에는 몇가지 단점이 존재한다. 또한 Spring 내부적으로도 한쪽이 String으로 제한된 타입 변환이 아니라 좀 더 일반적인 타입 변환이 요구되기 시작했다. 그래서 Spring 3에서는 PropertyEditor의 단점을 극복하고 내부적으로 타입 변환이 일어나는 모든 곳에서 사용할 수 있는 범용적인 Type Conversion System을 내놓았다. 이와 관련된 클래스들은 모두 org.springframework.core.convert 패키지 하위에 존재한다. 이 장에서는 Spring 3에서 소개한 Type Conversion 서비스의 사용방법에 대해서 자세히 알아보도록 하겠다.
13.2.1.Implementing Conveter
Spring 3에서는 Converter 구현을 위해서 다음과 같이 여러가지 API를 제공하고 있다.
Converter
Spring 3 Type Conversion 시스템에서 타입 변환을 실제 담당하는 객체는 Converter이다. Converter를 작성하려면 Spring에서 제공하는 org.springframework.core.convert.converter.Converter<S, T> 인터페이스를 구현하면 된다. Generics를 이용해서 Converter를 정의하므로 Run-time Type-Safety를 보장해준다.
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
Converter 인터페이스에서 구현해야 할 메소드는 convert() 메소드 하나이다. 즉 PropertyEditor와는 달리 단방향 타입 변환만 제공한다. 'S'에는 변환 전인 Source 타입을 명시하고, 'T'에는 변환 할 Target 타입을 명시한다. Converter 객체가 변환과 관련된 상태 값을 저장하지 않기 때문에 Converter를 Singlton Bean으로 등록하여 Multi-thread 환경에서도 안전하게 사용할 수 있다.
다음은 Converter를 구현한 예제 코드이다.
final class StringToInteger implements Converter<String, Integer> {
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
ConverterFactory
클래스 계층으로 묶을 수 있는 java.lang.Number나 java.lang.Enum과 같은 타입 변환 로직을 한 곳에서 관리하고자 하는 경우, 아래의 ConverterFactory 인터페이스의 구현클래스를 작성하면 된다..
여기서 'S'에는 변환 전인 Source 타입을 명시하고, 'R'에는 변환할 클래스들의 상위 베이스 클래스를 명시한다. 그리고 getConverter() 메소드를 구현하는데, 이 때, 'T'는 'R'의 하위 클래스 타입이 될 것이다.
다음은 ConverterFactory의 구현클래스 예이다. (Spring에서 제공하는 StringToNumberConverterFactory이다.)
final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToNumber<T>(targetType);
}
private static final class StringToNumber<T extends Number> implements Converter<String, T> {
private final Class<T> targetType;
public StringToNumber(Class<T> targetType) {
this.targetType = targetType;
}
public T convert(String source) {
if (source.length() == 0) {
return null;
}
return NumberUtils.parseNumber(source, this.targetType);
}
}
}
GenericConverter
또한, 두 가지 이상의 타입 변환을 수행하는 Converter를 개발하고자 하는 경우에는 GenericConverter 인터페이스를 구현하면 된다. 여러개의 Source/Target 타입을 지정할 수 있고, Source나 Target 객체의 Field Context(Field에 적용된 Annotation이나 Generics 등을 포함한 Field와 관련된 모든 정보)를 사용할 수 있기 때문에 유연한 Converter이긴 하지만, 그만큼 구현하기가 어렵고 복잡하다. 일반적으로 Converter나 ConverterFactory만으로 커버할 수 있는 기본적인 변환에는 사용하지 않는 것이 좋다.
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
실제 GenericConverter 구현 모습을 보고 싶다면, Spring에서 제공하는 Built-in Converter 중 하나인 org.springframework.core.convert.support.ArrayToCollectionConverter 코드에서 확인할 수 있다.
ConditionalGenericConverter
만약 어떤 조건을 만족하는 경우에만 변환을 수행하는 Converter를 개발할 경우는 ConditionalGenericConverter 인터페이스 구현클래스를 작성한다. 참조할 수 있는 구현 예는 Spring의 org.springframework.core.convert.support.IdToEntityConverter 이다.
13.2.2.Default Converter
Spring에서는 Converter도 PropertyEditor처럼 기본적인 타입들에 대해서 이미 구현해놓은 Built-in Converter들을 제공한다. Built-in Converter들은 모두 org.springframework.core.convert.support 패키지 하위에 존재한다.
13.2.3.Register Converter
사용자 필요에 의해서 추가로 개발한 Custom Converter들을 사용하려면 Converter도 역시 PropertyEditor처럼 등록이 필요하다. 한가지 다른 점은 각각의 Converter를 개별적으로 등록하는 것이 아니라, 모든 Converter를 가지고 변환 작업을 처리하는 ConversionService를 Bean으로 등록한 후, ConversionService Bean을 필요한 곳에서 Inject 받아서 사용한다는 것이다.
실제 Run-time시에 Converter들의 변환 로직은 이 ConversionService에 의해서 실행된다. 기본적으로 Spring에서 사용되는 ConversionService 구현 클래스는 GenericConversionService이다. 대부분의 ConversionService 구현 클래스는 Converter 등록 기능을 가지고 있는 ConverterRegistry도 구현하고 있다.
ConversionService Bean 정의 시 'converters' 속성 이용
ConversionService 구현클래스인 GenericConversionService는 ConversionServiceFactoryBean을 이용해서 Bean으로 등록할 수 있다. ConversionServiceFactoryBean이 가진 'converters' 속성을 이용하면 Custom Converter를 추가할 수도 있다.
다음은 ConversionServiceFactoryBean을 사용하여 ConversionService를 Bean으로 정의한 모습이다.
ConversionServiceFactoryBean은 ConversionServiceFactory 클래스를 이용해서 디폴트 Converter들을 GenericConversionService에 등록하고, 'converters' 속성을 통해 추가된 Converter들을 등록한다.
'conversionService'이라는 Bean 이름은 Spring에게 양보!
Spring 3에서는 타입 변환을 위해 Run-time 시에 사용되는 ConversionService Bean을 'conversionService'라는 이름으로 찾는다. 따라서 다른 용도의 Bean을 'conversionService'라는 이름으로 등록해서는 안된다.
13.2.4.ConversionService 사용하기
앞서 PropertyEditor는 매번 new 키워드를 이용해서 매번 인스턴스를 새로 생성해야만 했기 때문에 개별 컨트롤러 적용방법과 전체 컨트롤러 적용방법이 달랐었지만, Converter의 경우는 모든 Converter들을 가지고 있는 ConversionService를 Singleton Bean으로 등록해서 사용하기 때문에 아래와 같이 개별 컨트롤러에서 사용하는 것과, WebBindingInitializer 구현클래스를 이용해서 전체 컨트롤러에서 적용하는 것이 차이가 없다.
따라서 WebBindingInitializer를 구현한 클래스를 이용하여 하나의 설정으로 등록하는 것이 편리하다. Spring에서는 WebBindingInitializer를 직접 구현하지 않고 선언적인 설정만으로도 WebDataBinder의 설정을 초기화할 수 있게 해주는 ConfigurableWebBindingInitializer를 제공한다.
아래와 같이 설정하기만 하면 Custom Converter들이 추가된 ConversionService가 타입 변환 시에 사용될 것이다.
위와 같은 복잡한 설정을 쉽고 간편하게 할 수 있도록 Spring 3에서는 mvc 네임스페이스를 제공한다.
<mvc:annotation-driven>에 대한 자세한 내용은 본 매뉴얼 Spring MVC >> Configuration에서 Configuration Simplification 내용을 참고하기 바란다.
13.3.Spring 3 Formatting
지금까지 설명한 Conversion System은 Spring에서 범용적인 사용을 목적으로 만들어졌다. Spring 컨테이너에서 Bean의 Property 값을 셋팅할 때, Controller에서 데이터를 바인딩할 때는 물론이고 SpEL에서 데이터 바인딩 시에도 이 Conversion System을 사용한다.
Conversion System은 하나의 타입에서 다른 타입으로의 변환 로직을 구현할 수 있는 일관성있는 API를 제공한다. 그러나 실제로 사용자 UI가 존재하는 어플리케이션에서는 단순한 타입 변환만이 아니라, 날짜나 통화 표현같이 특정 Format을 객체의 값에 적용하여 String으로 변환해야 하는 경우가 종종 있다. 범용적인 용도로 만들어진 Converter에는 이러한 Formatting에 대한 처리 방법이 명시되어있지 않다.
그래서 Spring 3에서는 다음과 같은 Formatter API를 제공한다.
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
import java.text.ParseException;
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}
13.3.1.Implementing Formatter
Formatter를 개발하기 위해서는 위의 Formatter 인터페이스를 구현하여야 한다. print() 메소드에서 format을 적용하여 출력하는 로직을 구현하고, parse() 메소드에는 format이 적용된 String 값을 분석해서 객체 인스턴스로 변환하는 로직을 구현하면 된다. 위의 인터페이스 정의에서 볼 수 있듯이, Locale 정보도 함께 넘겨주기 때문에 Localization 적용도 쉽게 처리할 수 있다.
다음은 구현된 Formatter 예제 코드이다.
public final class DateFormatter implements Formtter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
13.3.2.Default Formatter
Spring에서는 편의를 위해서 Formatter 역시 기본적인 Built-in Formatter를 제공하고 있다.
DateFormatter
Spring은 기본적으로 java.text.DateFormat을 가지고 java.util.Date 객체의 formatting 처리를 하는 DateFormatter를 제공한다. (org.springframework.format.datetime 패키지) 또한 Spring에서는 강력한 Date/Time 관련 기능을 지원하는 Joda Time Library를 이용한 formatting도 제공한다.(org.springframework.format.datetime.joda 패키지) 클래스패스상에 Joda Time Library가 존재한다면 디폴트로 동작한다.
NumberFormatter
Spring에서는 java.text.NumberFormat을 사용한 java.lang.Number 객체의 formatting처리를 위해서 NumberFormatter, CurrencyFormatter, PercentFormatter를 제공하고 있다.(org.springframework.format.number 패키지)
일반적으로는 위의 Formatter를 직접 사용하기 보다는 아래에서 살펴볼 Annotation 기반 Formatting 처리 방법, 특히 Spring에서 기본적으로 제공하는 Formatting 관련 Annotation 들을 주로 사용하게 될 것이다.
13.3.3.Annotation 기반 Formatting
다음 섹션에서 살펴보겠지만, 구현된 Formatter는 특정 타입의 변환 시에 사용되도록 등록할 수도 있지만, 특정 Annotation이 적용된 필드의 타입 변환 시에 사용되도록 등록할 수도 있다.
Implementation
Formatting 관련 Annotation을 정의하고 그 Annotation이 적용된 필드의 타입 변환에는 연결되어 있는 특정 Formatter가 사용되도록 하려면 필드에 사용할 Annotation과 AnnotationFormatterFacotry 구현체를 만들어야 한다.
그리고 다음 코드는 @NumberFormat이 적용된 필드에 어떤 Formatter가 사용되어야 하는지 연결한 AnnotationFormatterFacotry 구현체이다.
public final class NumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<NumberFormat> {
public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(new Class<?>[] {
Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, BigInteger.class }));
}
public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
if (!annotation.pattern().isEmpty()) {
return new NumberFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyFormatter();
} else {
return new NumberFormatter();
}
}
}
}
이렇게 구현한 Formatter가 실제 Run-time 타입 변환 시에 사용되려면 반드시 등록과정을 거쳐야 한다. Formatter 등록에 대해서는 다음 섹션에서 자세히 알아보도록 하자.
Default annotations
Spring에서 제공하는 Format 관련 Annotation은 아래와 같이 2가지가 있다.
@DateTimeFormat : java.util.Date, java.util.Calendar, java.util.Long, Joda Time 타입(LocalDate, LocalTime, LocalDateTime, DateTime)의 필드 formatting에 사용 가능
public class Movie {
// 중략
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date releaseDate;
}
위와 같이 필드에 @DateTimeFormat을 적용하기만 하면 @DateTimeFormat에 연결된 Formatter에 의해서 Formatting이 처리된다.
사용 가능한 속성은 다음과 같다.
Name
Description
style
'S'-Short, 'M'-Medium, 'L'-Long, 'F'-Full 4가지 문자를 날짜에 한글자, 시간에 한글자를 사용해서 두 개의 문자로 만들어 지정. 날짜나 시간을 생략하고자 하는 경우 '-'를 사용 (예: 'S-'). 디폴트 값은 'SS'. Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌
iso
ISO 표준을 사용하고자 하는 경우, @DateTimeFormat(iso=ISO.DATE)와 같이 지정. ISO.DATE, ISO.DATE_TIME, ISO.TIME, ISO.NONE 사용가능, Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌
pattern
Locale과 상관없이 임의의 패턴을 사용하고자 하는 경우, ‘yyyy/mm/dd h:mm:ss a’등의 패턴을 지정
@NumberFormat : java.lang.Number 타입의 필드 formatting에 사용 가능
public class Movie {
// 중략
@NumberFormat(pattern = "#,##0")
private int ticketPrice;
}
위와 같이 필드에 @NumberFormat을 적용하기만 하면 @NumberFormat에 연결된 Formatter에 의해서 Formatting이 처리된다. java.lang.Number 하위의 클래스인 Byte, Double, Float, Integer, Long, Short, BigInteger, BigDecimal 변환에도 사용할 수 있다.
사용 가능한 속성은 다음과 같다.
Name
Description
style
NUMBER, CURRENCY, PERCENT 중 선택 가능. Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌
pattern
Locale과 상관없이 임의의 패턴을 사용하고자 하는 경우, ‘#,##0’등의 패턴을 지정
13.3.4.Register Formatter
Converter 영역에서, 등록된 Converter들을 가지고 실제 Run-time시에 타입 변환을 처리하는 역할을 담당하는 것이 GenericConversionService라면, Formatter에서 GenericConversionService와 같은 역할을 담당하는 것은 FormattingConversionService이다. FormattingConversionService는 GenericConversionService를 상속받고 있다.
위에서 살펴본 과정을 통해서 구현한 Formatter를 등록하는 방법은 Converter 등록과는 달리 불편하다. 설정으로 등록할 수 있는 방법은 아직 제공하고 있지 않고, FormattingConversionService를 초기화해주는 FormattingConversionServiceFactoryBean을 상속받은 클래스를 만들어서, installFormatters() 메소드를 오버라이드하여 Custom Formatter를 추가해야한다.
public class CustomFormattingConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean {
@Override
protected void installFormatters(FormatterRegistry registry) {
super.installFormatters(registry);
// 필드 타입과 Formatter를 연결하여 등록하는 경우
registry.addFormatterForFieldType(FilmRatings.class, new FilmRatingsFormatter());
// Annotation과 Formatter를 연결하여 등록하는 경우
registry.addFormatterForFieldAnnotation(new FilmRatingsFormatAnnotationFormatterFactory());
}
}
위 코드에서 FormatterRegistry가 Formatter 등록과 관련된 메소드를 제공하는 것을 확인할 수 있다.
이렇게 확장한 FormattingConversionServiceFactoryBean를 아래와 같이 Bean으로 등록하고, Converter에서처럼 ConfigurableWebBindingInitializer를 이용하여 컨트롤러에서 사용할 수 있도록 설정할 수도 있고,
<mvc:annotation-driven>만 설정해주면 기본적으로 제공하는 Built-in Converter와 Built-in Formatter, 그리고 Formatting관련 Annotation인 @DateTimeFormat, @NumberFormat을 사용할 수 있다.
PropertyEditor와 Spring 3 Converter 간의 실행 순서
타입변환이 필요한 경우 기본적으로 ConversionService가 등록되지 않으면 Spring은 PropertyEditor를 기반으로 타입 변환을 수행한다. ConversionService가 등록된 경우라고 하더라도 Custom PropertyEditor가 등록된 경우는 Custom PropertyEditor가 우선적으로 적용된다. Even when ConversionService has been registered, Custom PropertyEditor takes priority when Custom PropertyEditor is registered.
xml의 은 @EnableWebMvc과 같은 설정이다. 위의 설정은 해주는게 많다. messageConverter, formatting, validating 기타 등등 아주 여러가지를 해주는 어노테이션 및 xml 설정이다. 나머지는 인터넷에서 쉽게 찾을 수 있으니 참고만 하길 바란다.
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
}
우리는 WebMvcConfigurerAdapter을 상속받아서 커스터마이징을 할 수 있다.
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addFormatters(FormatterRegistry registry) {
}
}
Both elements serve an entirely different purpose.
<context:component-scan /> is, as the name implies, for component scanning. It by default scans for all beans with the @Component annotation (or "sub"annotations like @Controller, @Serviceetc.). It will only register instances of those classes in the application context as beans. That is all.
<mvc:annotation-driven /> is for bootstrapping Spring MVC and it registers, amongst others, a RequestMappingHandlerMapping and RequestMappingHandlerAdapter. The first links requests to a certain method (the @RequestMapping annotation on methods in a @Controller annotated class). The last knows how to execute methods annotated with @RequestMaping.
Now <mvc:annotation-driven /> does nothing for scanning or detecting @Controllers if there are none in the application context then no request mappings are made. Now you have several ways of registering those beans in the application context and one of them is the aforementioned <context:component-scan />.
Basically a @Controller without <mvc:annotation-driven /> is, well, pretty useless as it does nothing but take up memory. It will not be bound to incoming requests, it just hangs around in the application context. It is just another bean like all other beans and nothing special is being done to it. (Recent, but deprecated, versions of Spring register the DefaultAnnotationHandlerMappingwhich processes the @Controller, this is however deprecated).
<context:annotation-config> is used to activate annotations in beans already registered in the application context (no matter if they were defined with XML or by package scanning).
<context:component-scan> can also do what <context:annotation-config> does but <context:component-scan> also scans packages to find and register beans within the application context.
I'll use some examples to show the differences/similarities.
Lets start with a basic setup of three beans of type A, B and C, with B and C being injected into A.
package com.xxx;publicclass B {public B(){System.out.println("creating bean B: "+this);}}package com.xxx;publicclass C {public C(){System.out.println("creating bean C: "+this);}}package com.yyy;import com.xxx.B;import com.xxx.C;publicclass A {private B bbb;private C ccc;public A(){System.out.println("creating bean A: "+this);}publicvoid setBbb(B bbb){System.out.println("setting A.bbb with "+ bbb);this.bbb = bbb;}publicvoid setCcc(C ccc){System.out.println("setting A.ccc with "+ ccc);this.ccc = ccc;}}
OK, this is wrong! What happened? Why aren't my properties autowired?
Well, annotations are a nice feature but by themselves they do nothing whatsoever. They just annotate stuff. You need a processing tool to find the annotations and do something with them.
<context:annotation-config> to the rescue. This activates the actions for the annotations that it finds on the beans defined in the same application context where itself is defined.
when I load the application context I get the proper result:
creating bean B: com.xxx.B@15663a2
creating bean C: com.xxx.C@cd5f8b
creating bean A: com.yyy.A@157aa53
setting A.bbb with com.xxx.B@15663a2
setting A.ccc with com.xxx.C@cd5f8b
OK, this is nice, but I've removed two rows from the XML and added one. That's not a very big difference. The idea with annotations is that it's supposed to remove the XML.
So let's remove the XML definitions and replace them all with annotations:
package com.xxx;import org.springframework.stereotype.Component;@Componentpublicclass B {public B(){System.out.println("creating bean B: "+this);}}package com.xxx;import org.springframework.stereotype.Component;@Componentpublicclass C {public C(){System.out.println("creating bean C: "+this);}}package com.yyy;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import com.xxx.B;import com.xxx.C;@Componentpublicclass A {private B bbb;private C ccc;public A(){System.out.println("creating bean A: "+this);}@Autowiredpublicvoid setBbb(B bbb){System.out.println("setting A.bbb with "+ bbb);this.bbb = bbb;}@Autowiredpublicvoid setCcc(C ccc){System.out.println("setting A.ccc with "+ ccc);this.ccc = ccc;}}
While in the XML we only keep this:
<context:annotation-config />
We load the context and the result is... Nothing. No beans are created, no beans are autowired. Nothing!
That's because, as I said in the first paragraph, the <context:annotation-config /> only works on beans registered within the application context. Because I removed the XML configuration for thethree beans there is no bean created and <context:annotation-config /> has no "targets" to work on.
But that won't be a problem for <context:component-scan> which can scan a package for "targets" to work on. Let's change the content of the XML config into the following entry:
<context:component-scan base-package="com.xxx"/>
When I load the context I get the following output:
If you look closelly at the classes, class A has package com.yyy but I've specified in the <context:component-scan> to use package com.xxx so this completely missed my A class and only picked up B and C which are on the com.xxx package.
creating bean B: com.xxx.B@157aa53
creating bean C: com.xxx.C@ec4a87
creating bean A: com.yyy.A@1d64c37
setting A.bbb with com.xxx.B@157aa53
setting A.ccc with com.xxx.C@ec4a87
Even if the bean for class A isn't obtained by scanning, the processing tools are still applied by <context:component-scan> on all beans registered in the application context, even for A which was manually registered in the XML.
But what if we have the following XML, will we get duplicated beans because we've specified both <context:annotation-config /> and <context:component-scan>?
No, no duplications, We again get the expected result:
creating bean B: com.xxx.B@157aa53
creating bean C: com.xxx.C@ec4a87
creating bean A: com.yyy.A@1d64c37
setting A.bbb with com.xxx.B@157aa53
setting A.ccc with com.xxx.C@ec4a87
That's because both tags register the same processing tools (<context:annotation-config /> can be omitted if <context:component-scan> is specified) but Spring takes care of running them onlyonce.
Even if you register the processing tools yourself multiple times, Spring will still make sure they do their magic only once; this XML:
웹 어플리케이션을 개발할 때 보안은 개발자를 힘들게 만드는 것 중 하나입니다. 허가된 사용자만 접근할 수 있도록 하고, 현재 접속하려는 사용자가 누구인지역시 확인해야 하죠. 또 사용자의 비밀번호는 암호화 하여 저장하기도 해야 합니다.
웹 보안은 3가지로 요약할 수 있습니다.
인증 : 현재 사용자가 누구인지 확인하는 과정입니다. 아이디/비밀번호로 인증을 처리합니다. 인가 : 현재 사용자가 특정 url에 사용한 권한이 있는지 검사합니다. ui처리 : 권한이 없는 사용자가 해다 url에 접근했을 경우 에러화면등을 보여줍니다.
이 세 가지는 웹 어플리케이션마다 고민하게 하는 요소중 하나입니다. 하지만 구현이 쉽지 않죠.
인증은 쉬우나 그 이후의 과정은 쉽지 않습니다.
하지만 스프링 시큐리티(Spring Security)가 제공하는 틀을 사용하고, 각 웹 어플리케이션에 맞게 커스터마이징 한다면 보다 빠르게 구현을 할 수 있습니다. 또한 스프링 시큐리티는 암호화 기능도 제공하고 있기 때문에 사용자의 비밀번호 등을 암호화 하여 보관할 수도 있습니다. 우선 기본적인 세팅이 필요합니다.
기본적인 세팅이 끝나면 각 웹 어플리케이션에 맞게 커스터마이징이 필요합니다. 현재 진행중인 프로젝트의 예제를 보며 실제 프로젝트에 스프링 시큐리티가 어떻게 적용 되는지에 확인해 보도록 하겠습니다. 진행중인 프로젝트의 필요한 기능은 다음과 같았습니다.
1. 슈퍼 관리자는 모든 권한을 다 갖고 있으며, 모든 기능, 모든 메뉴에 접근 가능하다. 2. 그 외 관리자는 슈퍼 관리자가 권한을 설정할 수 있으며, 해당 권한이 있는 경우에만 그 메뉴에 접근 가능하다.
해당 기능을 위해 우선 Spring Security의 설정 수정이 필요했습니다.
context-security.xml
각 권한별로 접속할 수 있는 url을 설정해주고 모든 url에 슈퍼관리자는 접근이 가능하도록 수정해줬습니다. 다음으로는 사용자가 로그인 했을 때 어떠한 사용자가가 로그인을 하였고, 해당 사용자의 권한은 어떤것들이 있는가를 판단하도록 설정을 해주었습니다. 또한 로그인 시 사용되는 페이지를 지정 해주고, 인증 성공 시 이동 페이지와 인승 실패 시 이동 페이지를 설정해주었습니다.
또 필요한 파일들에 대해 경로를 설정해주었습니다.
순서대로 1. 현재 로그인 하려는 유저의 인증절차 2. 로그인 성공시 이동 3. 로그인 실패시 이동 4. 비밀번호 암호화 입니다.
스프링 시큐리티에서는 UserDetails라는 인터페이스가 제공 되는데 정의된 메소드와 역할을 도표로 정리하면 다음과 같습니다.
이 인터페이스를 상속받아 로그인 관련 기능을 구현하도록 도와주는 것이 바로 UserDetailsService입니다. 기본적으로 스프링 시큐리티에서 제공하는 UserDetailsService상속받아 UserDetailsServiceImpl.java 만들어 다음과 같이 기능을 구현하였습니다.
UserDetailsServiceImpl.java
기본적으로 UserDetailsService의 loadUserByUsername이라는 메서드를 상속 받습니다. 이후 loginForm 에서 입력된 adminId를 통해 해당 유저에 관련된 권한을 가져옵니다. 슈퍼 관리자의 경우에는 ROLE_ADMIN이라는 권한을 authorities.add를 통해 저장시켜 주고, 그외의 관리자의 경우에는 슈퍼관리자가 설정해준 권한을 반복문을 통해 저장시켜줍니다.
이후 스프링 시큐리티에서 제공하는 User객체를 통해 인증을 시도하게 되고, 성공시 AuthenticationSuccessHandler를 호출하게 됩니다. 해당 인터페이스를 상속받는 클래스를 만들어서 로그인 이후의 해야할 액션을 설정시켜줍니다.
AdminAuthenticationSuccessHandler.java
필요한 액션들을 한 후 제일 하단 메소드를 통해 지정한 url로 이동을 하게 됩니다. 이미 스프링 시큐리티 설정에서 defulaltUrl에 대해 지정을 해주었기 때문에 해당 url로 이동을 시킵니다.
만약 인증이 실패(로그인 실패) 했을 경우에 AuthenticationFailureHandler를 호출하게 됩니다. 해당 인터페이스를 상속받는 클래스를 만들어서 로그인 이후의 해야할 액션을 설정시켜줍니다.
AdminAuthenticationFailureHandler.java
로그인 실패 시 어떠한 원인인지에 대해서도 스프링 시큐리티는 지원을 해주고 있습니다. 해당 원인에 대해 정의를 한 후 로그인 실패 페이지로 이동 시킵니다.
로그인 실패 페이지에서는 원인을 확인하고 해당 값에 맞는 alert창을 띄워줬습니다.
슈퍼관리자가 아닌 그외의 관리자가 로그인을 한 후 권한이 없는 페이지에 접근시에는 accessDenied 에러가 발생하게 됩니다. 말 그대로 권한이 없다는 것입니다. 만약 ui처리를 해주지 않는다면 기본 에러페이지로 표시되게 때문에 권한 확인 후 권한이 없을 시 ui처리가 필요하게 됩니다. 해당 설정 역시 스프링 시큐리티에서 지원하고 있습니다.
context-security.xml
권한이 없을 시 해당 url로 이동을 시켰습니다.
스프링 시큐리티는 보안이라는 이슈를 꽤 편리하게 잡아줄 수 있습니다. 이번 포스트에서는 매우 간단하게 설명을 했고, 많이 빠진 부분이 많기 때문에 스프링 시큐리티를 이해하고 실제로 프로젝트에 적용하기란 쉽지 않습니다. 하지만 스프링 시큐리티가 없었다면 해당 기능 구현을 훨씬 더 어려웠을 것이고, 기본적으로 제공해주는 설정이 매우 다양하고 강력하기 때문에 적절하게 사용한다면 보안에 강력한 웹 어플리케이션을 개발할 수 있을 것 입니다.