'FRAMEWORK'에 해당되는 글 126건

  1. 2019.01.16 spark architecuture
  2. 2017.10.20 클래스 리터럴, 타입 토큰, 수퍼 타입 토큰
  3. 2017.10.20 Super type token
  4. 2017.10.19 Introduction to Spring Converters and Formatters
  5. 2017.10.19 Formatter
  6. 2017.10.19 Convertor
  7. 2017.10.19 WebMvcConfigurerAdapter
  8. 2017.10.19 Spring support for @Controller given by <context:component-scan /> vs <mvc:annotation-driven>
  9. 2017.10.19 Difference between <context:annotation-config> vs <context:component-scan>
  10. 2017.01.04 WAS에서 트랜잭션 처리

spark architecuture

FRAMEWORK/Spark 2019. 1. 16. 17:33

Understanding Spark: Part 2: Architecture

  • After introducing the spark in the previous blog, I will try to explain the architecture of the spark in thi blog. The objective is to give an quick overview of various components in spark architecture, what their functinalities and how they enable spark to process large amount of data fast.
  • The assumtion is that the reader must have prior understanding of the map reduce paradigm and some knowedge on Hadoop architecture.

Spark Architecture

1. What are the key components of Spark application?

    Every spark application has two main components
    • One Driver
    • A set of Executors (one or many)
    Driver - Is the coordinator of the spark application and hosts the spark Context object, which is the entry point to the application.
    • Driver negotiates with the external resource managers to provision all required resources for the spark application.
    • Manages the executor tasks.
    • Converts all map reduce operations and create tasks for the execturs to perform.
    • Collects all metrics about the execution of spark application and its components.
  • Executors - are the actual work horses of the spark applications. There might be one or more executors provisioned for a spark applicaiton. Execturos are actually java containers running on physical or virtual machines, which in turn are managed under cluster mangers like YARN or Mesos.
    • Number of executor resources and their capacities in terms of virtual core and RAM must be specified before starting a spark application. (There is an exception to this where resources can be provisioned dynamically).
    • Let's assume that we are using YARN managed cluster.
    • Driver negotiates with the resoruce manager of YARN to provision these resources in the cluster.
    • Then node manger of YARN spawns these processes and then executors are registered ( handed over ) to the driver for control, allocation and coordination of tasks among executors.
  • The following diagram depicts the architecture of spark.

Fig 1: Spark Components: Driver and Executors

    Executors load the external data (for example, files from HDFS) and load onto the memory of executors. For example here two blocks loaded into each executor memory. The in memory representation of these data partitions are called RDD (Resilient Distributed Datasets). Each chuck of data in memory is called partitions. The algorithm is expressed in terms of map reduce stages and driver pushes these map reduce tasks to the executors. Mappers can run in parallel across each RDD partitions in executors. If a reduce operation is assgined, then executors wait until all paritions are completed and proceed for data shuffle. After data shuffle is over, then executors can again run operation in parallel on these shuffled partitions. Finally, the resulting partitions after completion of all map reduce task are saved into an external systems, which is defined in the code submitted to spark. These serializing of resulting partitions can be accomplished in parallel by the executors.As you can see, the executors actually load data in terms of RDD and its partitions and apply operations on those RDD partitions and driver only assignd and coordinates these task with the executors .

2. How the executors are provisioned?

    The number of executors and their capacity in terms of cpu and memory are specified during the submission of the application. Driver then negotiates with the cluster manager e.g. Resource Manager in YARN. Yarn manager finds the best resources to schedule the executors and instructs the node managers to spawn these processes. Once the exectuors are started, then register with the Driver for further assignment and coordination of tasks. The machines (physical or virtual) managed by cluster manager are typically called slaves or workers. The number of executors requested are optimally allocated in available workers. It is possible that some workers might have been assigned more than one executors. Irrespective of wherever the executors are assigned, the capacity requested by the spark application is guaranteed by the YARN resource manager.

3. How data is read into spark application?

    Data can be read into spark application from any external systems. Spark is not tightly coupled with any specific file system or storage systems. Data can be loaded onto spark by two methods. Driver can read data onto a buffer and then parallelize (divide into smaller chunks and send to) to executors. But the amount of data that can be read and processed in this fashion is very limited. Driver can give location of the files in external system and coordinate read of the data by executors directly. For example, which blocks would be read by which executors from HDFS file system.

How map reduce operations are executed optimally in spark?

    All operations are applied on RDD partitions in terms of map or reduce operations. All data analysis logics are expressed in terms of map and reduce operations. An example of map operation would be filtered or selecting data. An example of redue operation would be group by or sort by operations. Here is an example of a series of map and reduce operations.
    • Load data -> map1 -> map2 -> map3 -> reduce1 -> map4 -> reduce2 -> reduce3 -> save results
    Once driver read the sequence of operations, it sends these as tasks to the executors. But it has to coordinate the execution of task to resolve any dependency between the RDD partitions across multiple executors. In this case the first operation is read data and map1. Let's say executor 1 finished map1 operation on P0 partition, before P1 partition and executor 2 finishes the map1 operation on P2 and P3 partitions.
    • Does, the executor need to wait for map1 operation to complete across all partitions, before it start map2 operation?
    The answer is no, as map2 operation is independent of other partition data, so executor can proceed with map2 operation. The only time, executors need to wait before proceeding further is when there is a reduce operation. As reduce operation will depend on the data across all paritions. The data need to shuffled across exectuors before reduce operation can be applied. Driver understands this dependencies, given a sequence of map reduce tasks and then combine these operations into stages. Each stage can be processes in parallel across executors, but need to wait for all executors before proceeding to next stage. So, given the above sequence, driver divides the task into four stages as below.
      Stage 1: load -> map1 -> map2 -> map3 Stage 2: reduce1 -> map4Stage 3: reduce2 Stage 4: reduce3 -> save

  • The diagram above depicts the stages created by driver and executed by executors.
  • Not only the stages are executed in parallel, they can be done in parallel with an executor. Each Executor may have multiple paritions loaded onto their memory and can process these stages in parallel across partitions within the same executor. Processing the partitions in parallel is calle tasks.
  • But, to process partitions in parallel the executor should start multiple threads. And these threads can run in parallell in true sense, only if the executors have access to multiple CPUs.
  • So, each executor should be allocated with multiple CPUs or cores, if we intend to run the task in parallel.

Conclusion:

  • In this blog, we delved into spark architecture quickly to undestand its components and their internal workings. In the next blog, we will dive more deeper to understand how spark manages memory and when it actually it evaluates and executes tasks.




출처 - http://www.awesomestats.in/spark-architecuture-2/


:

클래스 리터럴, 타입 토큰, 수퍼 타입 토큰

FRAMEWORK/SPRING 2017. 10. 20. 19:39

클래스 리터럴, 타입 토큰, 수퍼 타입 토큰

이 글은 토비님의 방송 채널에서 소개해주신 수퍼 타입 토큰에 대한 내용을 바탕으로 Oracle의 리터럴과 런타임 타입 토큰 튜토리얼, 수퍼 타입 토큰 기법의 창시자로 알려진 Neal Gafter의 글과 Jackson에 사용되고 있는 TypeReference 클래스의 소스 코드를 참고로 작성했다.

클래스 리터럴과 타입 토큰의 의미

  • 클래스 리터럴(Class Literal)은 String.classInteger.class 등을 말하며, String.class의 타입은 Class<String>Integer.class의 타입은 Class<Integer>다.
  • 타입 토큰(Type Token)은 쉽게 말해 타입을 나타내는 토큰이며, 클래스 리터럴이 타입 토큰으로서 사용된다.
  • myMethod(Class<?> clazz) 와 같은 메서드는 타입 토큰을 인자로 받는 메서드이며, method(String.class)로 호출하면, String.class라는 클래스 리터럴을 타입 토큰 파라미터로 myMethod에 전달한다.

타입 토큰은 어디에 쓰나?

아하~~ 이거구나! 하고 금방 감이 오는 구체적인 사례로는 아래와 같은 ObjectMapper를 들 수 있다.

1
MyLittleTelevision mlt = objectMapper.readValue(jsonString, MyLittleTelevision.class);  // 아하~ 이거~~

범용적으로 말하자면 타입 토큰은 타입 안전성이 필요한 곳에 사용된다.

타입 안전성 적용 사례 - Heterogeneous Map

자바5에서 Generic이 나온 이후로 특정 타입을 가지는 Map은 Map<String, String> 같은 식으로 키와 밸류의 타입을 명시적으로 지정해서 타입 안전성을 확보할 수 있는데, 정해진 특정 타입이 아니라 다양한 타입을 지원해야 하는 Heterogeneous Map이 필요하다면 타입 안전성을 확보하기 위해 다른 방법이 필요하다. 이럴 때 타입 토큰을 이용할수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TypeTokenMain {

public static void main(String[] args) {

class SimpleTypeSafeMap {

private Map<Class<?>, Object> map = new HashMap<>();

public <T> void put(Class<T> k, T v) {
map.put(k, v);
}

public <T> T get(Class<T> k) {
return k.cast(map.get(k));
}
}

SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap();

simpleTypeSafeMap.put(String.class, "abcde");
simpleTypeSafeMap.put(Integer.class, 123);

// 타입 토큰을 이용해서 별도의 캐스팅 없이도 타입 안전성이 확보된다.
String v1 = simpleTypeSafeMap.get(String.class);
Integer v2 = simpleTypeSafeMap.get(Integer.class);

System.out.println(v1);
System.out.println(v2);
}
}

앞에서 나온 SimpleTypeSafeMap에는 아쉬운 단점이 있는데, List<String>.class와 같은 형식의 타입 토큰을 사용할 수 없다는 점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TypeTokenMain {

public static void main(String[] args) {

SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap();

simpleTypeSafeMap.put(String.class, "abcde");
simpleTypeSafeMap.put(Integer.class, 123);

// 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다.
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() 이놈이다.

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getGenericSuperclass-- 의 설명을 요약해보면 다음과 같다.

getGenericSuperclass()

  • 바로 위의 수퍼 클래스의 타입을 반환하며,
  • 바로 위의 수퍼 클래스가 ParameterizedType이면, 실제 타입 파라미터들을 반영한 타입을 반환해야 한다.
  • ParameterizedType에 대해서는 별도 문서를 참고하라.

오~ 뭔가 타입 파라미터들을 반영한 타입을 반환한단다. 우리는 Class<List<String>>라는 타입을 어떻게든 구하려고 하고 있었다. 그런데 getGenericSuperclass() 이놈이 뭔가 파라미터 정보를 포함하는 타입을 반환한단다!!

위의 설명을 조금 각색하면 다음과 같다.

  • 어떤 객체 sub의 바로 위의 수퍼 클래스가 List<String>라는 파라미터를 사용하고 있는ParameterizedType이면,
  • sub.getClass().getGenericSuperclass()는 List<String> 정보가 포함되어 있는 타입을 반환해야 한다.

앞에서 살펴본 것을 코드로 작성해보면, 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TypeTokenMain {

public static void main(String[] args) {

// SimpleTypeSafeMap 부분 생략


class Super<T> {}

// 수퍼 클래스에 사용되는 파라미터 타입을 이용한다. 그래서 수퍼 타입 토큰.
class Sub extends Super<List<String>> {}

Sub sub = new Sub();

// 파라미터 타입 정보가 포함된 수퍼 클래스의 타입 정보를 구한다.
Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();

// ~~~$1Super<java.util.List<java.lang.String>> 라고 나온다!!
System.out.println(typeOfGenericSuperclass);
}
}

ParameterizedType.getActualTypeArguments()

위에 getGenericSuperclass()의 설명을 보면, 수퍼 클래스가 ParameterizedType이면 타입 파라미터를 포함한 정보를 반환해야 한다고 했으며, ParameterizedType은 별도의 문서를 보라고 했다.

ParameterizedType의 API 문서 를 보면 Type[] getActualTypeArgumensts()라는 메서드가 있다. 느낌이 팍 온다.. 앞의 코드를 조금 보완해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TypeTokenMain {

public static void main(String[] args) {

// SimpleTypeSafeMap 부분 생략


class Super<T> {}

class Sub extends Super<List<String>> {}

Sub sub = new Sub();


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];

// 심봤다! java.util.List<java.lang.String>가 나온다!!
System.out.println(actualType);
}
}

오.. 구했다.

단순한 클래스 리터럴로는 구할 수 없었던 Class<List<String>>라는 타입 정보를,
껍데기 뿐이지만 한 없이 아름다운 수퍼 클래스와 위대한 구세주 getGenericSuperclass(), 그리고 getActualTypeArguments()를 이용해서 구했다.

이제 게임 끝났네..

아줌마 났어요~~

그럼 바로 써보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class TypeTokenMain {

public static void main(String[] args) {

class SimpleTypeSafeMap {

private Map<Class<?>, Object> map = new HashMap<>();

public <T> void put(Class<T> k, T v) {
map.put(k, v);
}

public <T> T get(Class<T> k) {
return k.cast(map.get(k));
}
}

SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap();

simpleTypeSafeMap.put(String.class, "abcde");
simpleTypeSafeMap.put(Integer.class, 123);

// 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다.
String v1 = simpleTypeSafeMap.get(String.class);
Integer v2 = simpleTypeSafeMap.get(Integer.class);

System.out.println(v1);
System.out.println(v2);


// 수퍼 타입 토큰을 써보자
class Super<T> {}

class Sub extends Super<List<String>> {}

Sub sub = new Sub();

Type typeOfGenericSuperclass = sub.getClass().getGenericSuperclass();

System.out.println(typeOfGenericSuperclass);

Type actualType = ((ParameterizedType) typeOfGenericSuperclass).getActualTypeArguments()[0];

System.out.println(actualType);

simpleTypeSafeMap.put(actualType, Arrays.asList("a", "b", "c")); // 여기서 에러!!
}
}

나긴 났는데.. 에러가 나네.. ㅋ 실행도 하기 전에 컴파일 에러다 ㅋㅋ

1
2
3
4
5
6
Wrong 1st argument type. Found: 'java.lang.reflect.Type', required: 'java.lang.Class<T>' less...

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>

SimpleTypeSafeMap은 키로 Class<?>을 받는데, java.lang.reflect.Type를 넘겨주면 어쩌냐는 푸념이다.

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
public abstract class TypeReference<T> {

private Type type;

protected TypeReference() {
Type superClassType = getClass().getGenericSuperclass();
if (!(superClassType instanceof ParameterizedType)) { // sanity check
throw new 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
class Super<T> {}

class Sub extends Super<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
abstract class TypeReference<T> {

private Type type;

protected TypeReference() {
Type superClassType = getClass().getGenericSuperclass();
if (!(superClassType instanceof ParameterizedType)) { // sanity check
throw new 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
public class TypeSafeMap {

// 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> 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
public class TypeSafeMap {

// 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> void put(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()을 개선한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TypeSafeMap {

// 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> void put(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()을 이용해서 키에 사용된 타입 파라미터의 타입으로 캐스팅 해주도록 개선한 것 뿐이다.

자, 이제 지금까지 해본 것을 한데 모아 보자.

All in One

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class SuperTypeTokenMain {

public static void main(String[] args) {

abstract class TypeReference<T> {

private Type type;

protected TypeReference() {
Type superClassType = getClass().getGenericSuperclass();
if (!(superClassType instanceof ParameterizedType)) { // sanity check
throw new IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보와 함께 생성되어야 합니다.");
}
this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0];
}

public Type getType() {
return type;
}
}


class TypeSafeMap {

           private Map<Type, Object> map = new HashMap<>(); // key로 사용되던 Class<?> 대신 Type으로 변경

public <T> void put(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();

// simpleTypeSafeMap.put(String.class, "abcde");
typeSafeMap.put(new TypeReference<String>() {}, "abcde");

// simpleTypeSafeMap.put(Integer.class, 123);
typeSafeMap.put(new TypeReference<Integer>() {}, 123);

// 드디어 List<String> 을 쓸 수 있다!!
// new TypeReference<List<String>>() {}를 사용해서 List<String>.class와 동일한 효과를!!
typeSafeMap.put(new TypeReference<List<String>>() {}, Arrays.asList("A", "B", "C"));

// List<List<String>> 처럼 중첩된 ParameterizedType도 사용 가능하다!!
typeSafeMap.put(new TypeReference<List<List<String>>>() {},
Arrays.asList(Arrays.asList("A", "B", "C"), Arrays.asList("a", "b", "c")));

// Map<K, V>도 된다.
Map<String, String> strMap1 = new HashMap<>();
strMap1.put("Key1", "Value1");
strMap1.put("Key2", "Value2");
typeSafeMap.put(new TypeReference<Map<String, String>>() {}, strMap1);


// 수퍼 타입 토큰을 이용해서 별도의 캐스팅 없이도 안전하다.
// String v1 = typeSafeMap.get(String.class);
String v1 = typeSafeMap.get(new TypeReference<String>() {});

//Integer v2 = typeSafeMap.get(Integer.class);
Integer v2 = typeSafeMap.get(new TypeReference<Integer>() {});

// 바로 이거다!
// List<String>.class 처럼 언어에서 지원해 주지 않는 클래스 리터럴을 사용하지 않고도
// List<String>라는 타입을 쓸 수 있게 되었다.
List<String> listString = typeSafeMap.get(new TypeReference<List<String>>() {});

// List<List<String>> 처럼 중첩된 ParameterizedType도 사용 가능하다!!
List<List<String>> listListString =
typeSafeMap.get(new TypeReference<List<List<String>>>() {});

// Map<K, V>도 된다.
Map<String, String> strMap = typeSafeMap.get(new TypeReference<Map<String, String>>() {});

System.out.println(v1);
System.out.println(v2);
System.out.println(listString);
System.out.println(listListString);
System.out.println(strMap);
}
}

Spring의 ParameterizedTypeReference

전지전능하신 Spring느님께서는 ParameterizedTypeReference라는 클래스를 우리에게 하사하시었다.

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/ParameterizedTypeReference.html

허무하지만 앞에서 알아본 건 걍 공부했다 치고, 실전에서는 앞에서 만든 TypeReference 대신 ParameterizedTypeReference를 사용하자. ㅋㅋ

정리

  • 타입 안전성을 확보하려면 타입 정보가 필요하다.
  • 일반적인 클래스의 타입 정보는 String.classInteger.class와 같은 클래스 리터럴로쉽게 구할 수 있다.
  • List<String>.class 같은 클래스 리터럴은 언어에서 지원해주지 않으므로 사용할 수 없다.
  • 수퍼 타입 토큰 기법을 사용하면 클래스 리터럴로 쉽게 구할 수 없는, List<String> 형태의 타입 정보를 구할 수 있다.
  • 따라서 List<String>.class라는 클래스 리터럴을 쓸 수 없더라도, List<String>라는 타입을 쓸 수 있어서 타입 안전성을 확보할 수 있다.
  • 수퍼 타입 토큰 기법은 Spring이 ParameterizedTypeReference를 통해 제공해주고 있으므로 써주자.

더 읽을 거리

더 생각할 거리

참조 - http://homoefficio.github.io/2016/11/30/%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A6%AC%ED%84%B0%EB%9F%B4-%ED%83%80%EC%9E%85-%ED%86%A0%ED%81%B0-%EC%88%98%ED%8D%BC-%ED%83%80%EC%9E%85-%ED%86%A0%ED%81%B0/

'FRAMEWORK > SPRING' 카테고리의 다른 글

Super type token  (0) 2017.10.20
Introduction to Spring Converters and Formatters  (0) 2017.10.19
Formatter  (0) 2017.10.19
Convertor  (0) 2017.10.19
WebMvcConfigurerAdapter  (0) 2017.10.19
:

Super type token

FRAMEWORK/SPRING 2017. 10. 20. 19:38

Super type token

오늘은 Super type token에 대해서 알아보자.

Super type token을 알기전에 일단 type token을 알아야 되는데 type token 이란 간단히 말해서 타입을 나타내는 토큰(?)이다. 예를들어 String.class는 클래스 리터럴이라하며 Class<String>가 타입토큰이라 말할 수 있다. 실제 Class은 String.class를 이용해서 메서드를 호출 할 수 있다.

Super type token 경우에는 제네릭과 관련이 많다. 일단 제네릭을 잘 모른다면 여기를 한번 보고 이글을 봐야 할 듯 하다.

Type token

타입 토큰의 간단한 예제를 만들어서 살펴보자.

public static <T> T typeToken(T value, Class<T> clazz) {
  return clazz.cast(value);
}

위의 코드는 의미가 없지만 예제이니.. 위는 T 타입의 value를 받아서 해당하는 타입토큰으로 형변환하는 그런 코드이다. 한번 사용해보자.

public static void main(String[] args) {
  System.out.println(typeToken(10, Integer.class));
  System.out.println(typeToken("string", String.class));
}

위와 같이 int일 경우에는 Integer.class 파라미터로 넘기고 String일 경우에는 String.class 클래스 리터럴을 파라미터로 넘기면 된다. 만약 형을 맞게 넘기지 않았을 경우에는 컴파일 에러가 발생한다.

System.out.println(typeToken(10, String.class)); //컴파일 에러

이와 같이 좀 더 안전하게 타입을 지정해서 사용할 수 있는 큰 장점이 있다.

Gson Example

위와 같은 예제말고 좀 더 실용적인 사용법이 있다. Gson과 jackson 등 json, xml을 Object으로 변경할 때 사용이 많이 된다.
필자는 jackson을 더 좋아하지만 여기서는 Gson을 사용했다. (그냥)

public class Account {
    private String username;
    private String password;

    public String getUsername() {
      return username;
    }

    public void setUsername(String username) {
      this.username = username;
    }

    public String getPassword() {
      return password;
    }

    public void setPassword(String password) {
      this.password = password;
    }
    @Override
    public String toString() {
      return "Account{" +
          "username='" + username + '\'' +
          ", password='" + password + '\'' +
          '}';
    }
  }
}

public static void main(String[] args) {
  String json = "{\"username\" : \"wonwoo\", \"password\" : \"test\"}";
  Gson gson = new Gson();
  Account account = gson.fromJson(json, Account.class);
  System.out.println(account);
}

우리가 흔히 API 통신을 하거나 특정한 데이터를 가공하기 위해 Object로 변환하기 위해 위와 같은 코드를 자주 이용한다. jackson도 마찬가지다. 아까 위에서 설명했듯이 Account.class 라는 클래스 리터럴을 이용해서 Account라는 타입을 파라미터로 넘겼다.

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
  Object object = fromJson(json, (Type) classOfT);
  return Primitives.wrap(classOfT).cast(object);
}

위의 코드는 Gson의 fromJson 메서드이다. 아까 예제와 많이 비슷하다. Class<T>라는 타입을 파라미터로 받고 그 해당하는 T 타입을 리턴해 준다.

List

위의 경우에는 특별하게 주의 할 것 없지만 한개 주의 할 것이 있다. 만약 List 로된 json을 하고 싶다면 어떻게 할까? 아까 위에서 링크를 남겼던 곳에 가면 우리는 아래와 같은 코드를 작성할 수 없다고 했다.

List<Account>.class

그럼 그냥 List로 타입을 넘기면 될까? 한번 해보자.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, List.class);
System.out.println(accounts);

위와 같이 List.class만 사용해서 코드를 작성하였다. 잘 된다. 딱히 문제는 없다. 출력도 원하는 값으로 된 듯 싶다. 하지만 안타깝게 이 코드에서 특정한 인덱스의 값을 가져올 때 에러가 발생한다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, List.class);
System.out.println(accounts);
System.out.println(accounts.get(0).getPassword());

위의 코드를 동작시켜 보자. 그럼 아래와 같은 에러를 발생시킨다.

com.google.gson.internal.LinkedTreeMap cannot be cast to ... Account..

LinkedTreeMap 을 Account 캐스팅을 할 수 없다는 것이다. 당연히 LinkedTreeMap은 Account로 형변환을 할 수 없다. 근데 왜 이런 에러가 발생할까? 그 이유는 List.class 클래스 리터럴에는 제네릭 정보가 없기 때문이다. 그렇기 때문에 gson은 List 제네릭 정보의 Account 라는 클래스 자체를 모른다. List의 어떤 값이 들어가야 될 지 모르니 그냥 Map으로 파싱하는 것이다. jackson의 경우에는 자바의 LinkedHashMap 으로 파싱한다.
그렇다면 어떻게 이 문제를 해결할까?

Super type token

이런 제네릭 정보가 지워지는 문제 때문에 Super type token 기법이 생겨났다. Super type token은 수퍼타입을 토큰으로 사용하겠다는 의미이다. 이건 또 무슨말인가? 제네릭 정보가 컴파일시 런타임시 다 지워지지만 제네릭 정보를 런타임시 가져올 방법이 존재한다. 제네릭 클래스를 정의한 후에 그 제네릭 클래스를 상속받으면 런타임시에는 제네릭 정보를 가져올 수 있다.

public class SuperTypeToken<T> {

}
public class TypeToken extends SuperTypeToken<String> {

}

System.out.println(TypeToken.class.getGenericSuperclass()); //SuperTypeToken<java.lang.String>

위와 같이 SuperTypeToken 을 제네릭으로 만든 후에 TypeToken 클래스에 SuperTypeToken 을 상속받으면된다. 그럼 위와 같이 SuperTypeToken<java.lang.String> 정보를 가져올 수 있다.

이걸 이용해서 우리는 아까 Gson에서 하지 못했던 (gson에서 하지 못한건 아니지..) List 를 형태로 파싱 할 수 있다. 아래의 TypeToken은 Gson에 있는 클래스이다. 필자가 만든 클래스와는 다르다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
class AccountTypeToken extends TypeToken<List<Account>> {

}
List<Account> accounts = gson.fromJson(jsons, new AccountTypeToken().getType());
System.out.println(accounts);

메서드의 내부 클래스를 만들어서 손쉽게 List 형태로 만들 수 있다. 위에서 본 Map과 다르게 특정한 정보도 가져올 수 있다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
class AccountTypeToken extends TypeToken<List<Account>> {

}
List<Account> accounts = gson.fromJson(jsons, new AccountTypeToken().getType());
System.out.println(accounts);
System.out.println(accounts.get(0).getPassword()); //test

하지만 코드가 좀 지저분하다. 메서드 안에 내부 클래스를 만들고 나니 가독성도 그닥 좋지 않는 듯 하다. 좀 더 줄여 보자.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
TypeToken<List<Account>> typeToken = new TypeToken<List<Account>>() {};
List<Account> accounts = gson.fromJson(jsons, typeToken.getType());

TypeToken 을 익명 클래스로 작성하였다. 이렇게 한다면 상속한 클래스를 만들지 않았지만 실제로 내부적으로 임의의 클래스를 만든다. 그래서 그 클래스의 인스턴스만 한개 돌려주는 것 뿐이다. Gson, 혹은 기타 다른 TypeToken 클래스들은 {} 가 없다면 컴파일 에러를 발생시킨다. gson의 TypeToken 클래스 생성자는 protected 접근제한을 두고 있기 때문이다. 그래서 {}를 꼭 사용해야 한다. 필자도 처음에는 저걸 왜 사용할까 생각했는데 제네릭을 알고 나니 이해가 되었다. 물론 Jackson도 gson의 TypeToken 과 동일한 역할을 하는 TypeReference가 존재한다.

좀 더 간결하게 할 수 도 있다. 아래와 같이 말이다.

String jsons = "[{\"username\" : \"wonwoo\", \"password\" : \"test\"},{\"username\" : \"seungwoo\", \"password\" : \"test1\"}]";
List<Account> accounts = gson.fromJson(jsons, new TypeToken<List<Account>>() {}.getType());
System.out.println(accounts);

그냥 바로 메서드를 호출할 때 생성해서 사용해도 된다. 위의 코드가 제일 깔끔한 듯 싶다. 근데 왜 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>>() {} 이런 익명클래스를 사용하곤 했는데 왜 저렇게 사용할까 생각은 했지만 무심코 넘어갔다. 이제는 왜 저렇게 사용하는지 알게 되었으니 필요하다면 자주 이용하자.



참조 - http://wonwoo.ml/index.php/post/1807

'FRAMEWORK > SPRING' 카테고리의 다른 글

클래스 리터럴, 타입 토큰, 수퍼 타입 토큰  (0) 2017.10.20
Introduction to Spring Converters and Formatters  (0) 2017.10.19
Formatter  (0) 2017.10.19
Convertor  (0) 2017.10.19
WebMvcConfigurerAdapter  (0) 2017.10.19
:

Introduction to Spring Converters and Formatters

FRAMEWORK/SPRING 2017. 10. 19. 17:13

Introduction

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:

Built-in Converters

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

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.

package net.javabeat.articles.spring.formatter.custom;

import java.util.Locale;

import org.springframework.format.Printer;

public class CreditCardNumberPrinter implements Printer{

	@Override
	public String print(CreditCardNumber ccNumberObject, Locale locale) {

		return
				ccNumberObject.getFirstFourDigits() + "-"
			+	ccNumberObject.getSecondFourDigits() + "-"
			+ 	ccNumberObject.getThirdFourDigits() + "-"
			+ 	ccNumberObject.getFirstFourDigits();
	}

}

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.

package net.javabeat.articles.spring.formatter.custom;

import java.text.ParseException;
import java.util.Locale;

import org.springframework.format.Formatter;
import org.springframework.format.Parser;
import org.springframework.format.Printer;

public class CreditCardNumberFormatter implements Formatter{

	private Parser parser;
	private Printer printer;

	public CreditCardNumberFormatter(Parser parser,
		Printer printer){
		this.parser = parser;
		this.printer = printer;
	}

	@Override
	public String print(CreditCardNumber ccNumberObject, Locale locale) {
		return printer.print(ccNumberObject, locale);
	}

	@Override
	public CreditCardNumber parse(String ccNumber, Locale locale) throws ParseException {
		return parser.parse(ccNumber, locale);
	}
}

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:

 

출처 - http://javabeat.net/introduction-to-spring-converters-and-formatters/

'FRAMEWORK > SPRING' 카테고리의 다른 글

클래스 리터럴, 타입 토큰, 수퍼 타입 토큰  (0) 2017.10.20
Super type token  (0) 2017.10.20
Formatter  (0) 2017.10.19
Convertor  (0) 2017.10.19
WebMvcConfigurerAdapter  (0) 2017.10.19
:

Formatter

FRAMEWORK/SPRING 2017. 10. 19. 17:12

Spring MVC - Creating a new Formatter

[Updated: Nov 19, 2016, Created: Feb 26, 2016]

In this example we are going to define our own formatter using spring Formatter SPI. Spring Formatter interface extends Printer and Parser interfaces:

 package org.springframework.format;
 public interface Formatter<T> extends Printer<T>, Parser<T> {
 }

We have to implement print() and parse() methods of Printer and Parser respectively.



we are going to demonstrate how to format an object of Address instance in Customer object.

Creating Backing object classes

public class Customer {
    private Long id;
    private String name;
    private Address address;

   //getters and setters
}

public class Address {
    private String street;
    private String city;
    private String county;
    private String zipCode;

    //getters and setters
}



Creating our Formatter

import org.springframework.format.Formatter;
import java.text.ParseException;
import java.util.Locale;

public class AddressFormatter implements Formatter<Address> {
    private Style style = Style.FULL;

    public void setStyle (Style style) {
        this.style = style;
    }

    @Override
    public Address parse (String text, Locale locale) throws ParseException {
        if (text != null) {
            String[] parts = text.split(",");
            if (style == Style.FULL && parts.length == 4) {
                Address address = new Address();
                address.setStreet(parts[0].trim());
                address.setCity(parts[1].trim());
                address.setZipCode(parts[2].trim());
                address.setCounty(parts[3].trim());
                return address;
            } else if (style == Style.REGION && parts.length == 3) {
                Address address = new Address();
                address.setCity(parts[0].trim());
                address.setZipCode(parts[1].trim());
                address.setCounty(parts[4].trim());
                return address;
            }
        }
        return null;
    }

    @Override
    public String print (Address a, Locale l) {
        if (a == null) {
            return "";
        }
        switch (style) {
            case FULL:
                return String.format(l, "%s, %s, %s, %s", a.getStreet(), a.getCity(),
                                     a.getZipCode(), a.getCounty());
            case REGION:
                return String.format(l, "%s, %s, %s", a.getCity(), a.getZipCode(),
                                     a.getCounty());
        }
        return a.toString();
    }

    public enum Style {
        FULL,
        REGION
    }
}



Registering The Formatter

import org.springframework.context.annotation.Configuration;
 import org.springframework.format.FormatterRegistry;
 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

 @EnableWebMvc
 @Configuration
 public class MyWebConfig extends WebMvcConfigurerAdapter {

     @Override
   public void addFormatters (FormatterRegistry registry) {
   AddressFormatter addressFormatter = new AddressFormatter();
   addressFormatter.setStyle(AddressFormatter.Style.REGION);
   registry.addFormatter(addressFormatter);
   }
 }

Alternatively we could have used @InitBinder approach, if we want to customize AddressFormatter per request basis.




Creating Controller

@Controller
@RequestMapping("/customers")
public class CustomerController {
    @Autowired
    private CustomerDataService customerDataService;

    @RequestMapping(method = RequestMethod.GET)
    private String handleRequest (Model model) {
        model.addAttribute("customerList", customerDataService.getAllUsers());
        return "customers";
    }
}




customer.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<html>
<body>
<h3>Customer List </h3>
<table style="width:100%">
<c:forEach var="customer" items="${customerList}" varStatus="status">
<tr>
<td>
    <spring:eval expression="customer.id" />
</td>
<td>
    <spring:eval expression="customer.name" />
</td>
<td>
   <spring:eval expression="customer.address" />
</td>
</tr>
</c:forEach>
</table>
</body>
</html>

출처 - http://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/spring-define-formatter/

:

Convertor

FRAMEWORK/SPRING 2017. 10. 19. 17:12

13.Data Binding and Type Conversion

Spring에서 타입 변환이 발생하는 영역은 크게 2가지이다. 하나는 Bean 정의 XML에서 <property />를 이용해 설정한 값을 실제 Bean 객체의 Property에 바인딩 시킬 때인데, XML에 String으로 정의한 값을 해당 Property의 타입으로 변환해서 셋팅해야한다.

예를 들어, Movie 클래스가 다음과 같이 정의되어 있고,

public class Movie {
    String id;
    String name;
    int ticketPrice;
}

'movie' Bean을 아래와 같이 정의했다고 하면,

<bean id="movie" class="sample.Movie">
    <property name="name" value="Avatar"/>
    <property name="ticketPrice" value="7500"/>
</bean>

'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 이용

    1. 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));
          }
      }
    2. AnnotationMethodHandlerAdapter에 webBindingInitializer 속성을 이용해서 설정

      <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
          <property name="webBindingInitializer">
              <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
          </property>
      </bean>
  • 여러 개의 PropertyEditor를 여러 컨트롤러에 적용

    다수의 컨트롤러에서 자주 사용되는 여러 개의 Custom PropertyEditor 셋트로 관리할 경우 PropertyEditorRegistrar 이용

    1. 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들 추가
          }
      }
    2. 구현한 Custom PropertyEditorRegistrar를 Bean으로 등록

      <bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
    3. @InitBinder를 이용하여 Controller에서 사용

      @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 인터페이스의 구현클래스를 작성하면 된다..

    package org.springframework.core.convert.converter;
    				
    public interface ConverterFactory<S, R> {
        <T extends R> Converter<S, T> getConverter(Class<T> targetType);
    }
    여기서 '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 받아서 사용한다는 것이다.

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);
    
    <T> T convert(Object source, Class<T> targetType);
    
    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

실제 Run-time시에 Converter들의 변환 로직은 이 ConversionService에 의해서 실행된다. 기본적으로 Spring에서 사용되는 ConversionService 구현 클래스는 GenericConversionService이다. 대부분의 ConversionService 구현 클래스는 Converter 등록 기능을 가지고 있는 ConverterRegistry도 구현하고 있다.

  • ConversionService Bean 정의 시 'converters' 속성 이용

    ConversionService 구현클래스인 GenericConversionService는 ConversionServiceFactoryBean을 이용해서 Bean으로 등록할 수 있다. ConversionServiceFactoryBean이 가진 'converters' 속성을 이용하면 Custom Converter를 추가할 수도 있다.

    다음은 ConversionServiceFactoryBean을 사용하여 ConversionService를 Bean으로 정의한 모습이다.

    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <!-- 추가할 Custom Converter를 설정 -->
        <property name="converters">
            <list>
                <bean class="org.anyframe.sample.moviefinder.StringToFilmRatingConverter" />
                <bean class="org.anyframe.sample.moviefinder.FilmRatingToStringConverter" />
            </list>
        </property>					
    </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 구현클래스를 이용해서 전체 컨트롤러에서 적용하는 것이 차이가 없다.

@Inject
private ConversionService conversionService;

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.setConversionService(this.conversionService);
}

따라서 WebBindingInitializer를 구현한 클래스를 이용하여 하나의 설정으로 등록하는 것이 편리하다. Spring에서는 WebBindingInitializer를 직접 구현하지 않고 선언적인 설정만으로도 WebDataBinder의 설정을 초기화할 수 있게 해주는 ConfigurableWebBindingInitializer를 제공한다.

아래와 같이 설정하기만 하면 Custom Converter들이 추가된 ConversionService가 타입 변환 시에 사용될 것이다.

<!-- AnnotationMethodHandlerAdapter에 webBindingInitializer DI -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="webBindingInitializer" ref="webBindingInitializer" />
</bean>

<!-- 사용자가 변경한 conversionService를 WebBindingInitializer 구현체에 DI -->
<bean id="webBindingInitializer" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
    <property name="conversionService" ref="conversionService" />
</bean>

<!-- Custom Converter들을 추가한 conversionService Bean 정의 -->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <list>
            <bean class="org.anyframe.sample.moviefinder.StringToFilmRatingConverter" />
        	<bean class="org.anyframe.sample.moviefinder.FilmRatingToStringConverter" />
        </list>
    </property>
</bean>

위와 같은 복잡한 설정을 쉽고 간편하게 할 수 있도록 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 구현체를 만들어야 한다.

    package org.springframework.format;
    
    public interface AnnotationFormatterFactory<A extends Annotation> {
    
        Set<Class<?>> getFieldTypes();    
      
        Printer<?> getPrinter(A annotation, Class<?> fieldType);
        
        Parser<?> getParser(A annotation, Class<?> fieldType);
    }
    'A'에는 연결할 Annotation을 명시하고, getFieldTypes()은 해당 Annotation을 적용할 수 있는 필드 타입을 리턴하도록 구현하고, getPrinter()/getParser()는 각각 사용될 Printer와 Parser를 리턴하도록 구현한다.

    실제로 Spring에서 제공하고 있는 @NumberFormat의 경우 Annotation과 AnnotationFormatterFacotry가 어떻게 구현되어 있는지 살펴보자.

    다음은 @NumberFormat Annotation 구현 코드이다.

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NumberFormat {
    
        Style style() default Style.NUMBER;
    
        String pattern() default "";
    
        public enum Style {
            NUMBER,
            CURRENCY,
            PERCENT
        }
    }

    그리고 다음 코드는 @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이 처리된다.

      사용 가능한 속성은 다음과 같다.

      NameDescription
      style

      'S'-Short, 'M'-Medium, 'L'-Long, 'F'-Full 4가지 문자를 날짜에 한글자, 시간에 한글자를 사용해서 두 개의 문자로 만들어 지정. 날짜나 시간을 생략하고자 하는 경우 '-'를 사용 (예: 'S-'). 디폴트 값은 'SS'. Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌

      isoISO 표준을 사용하고자 하는 경우, @DateTimeFormat(iso=ISO.DATE)와 같이 지정. ISO.DATE, ISO.DATE_TIME, ISO.TIME, ISO.NONE 사용가능, Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌
      patternLocale과 상관없이 임의의 패턴을 사용하고자 하는 경우, ‘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 변환에도 사용할 수 있다.

      사용 가능한 속성은 다음과 같다.

      NameDescription
      style

      NUMBER, CURRENCY, PERCENT 중 선택 가능. Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌

      patternLocale과 상관없이 임의의 패턴을 사용하고자 하는 경우, ‘#,##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를 이용하여 컨트롤러에서 사용할 수 있도록 설정할 수도 있고,

<bean id="conversionService" class="org.anyframe.sample.format.CustomFormattingConversionServiceFactoryBean" />
아래와 같이 mvc 네임스페이스의 <mvc:annotation-driven>를 이용하면 간편하게 설정할 수도 있다.
<mvc:annotation-driven conversion-service="conversionService" />
    
<bean id="conversionService" class="org.anyframe.sample.format.CustomFormattingConversionServiceFactoryBean" />

<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.

* 우선 순위

  1. Custom PropertyEditor

  2. Converter

  3. Default PropertyEditor

출처 - http://dev.anyframejava.org/docs/anyframe/plugin/essential/core/1.0.0/reference/html/ch13.html

:

WebMvcConfigurerAdapter

FRAMEWORK/SPRING 2017. 10. 19. 16:25

spring mvc의 xml과 java config를 비교해서 한번 살펴보겠다.

EnableWebMvc

@Configuration
@EnableWebMvc
public class WebConfig {
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven/>

</beans>

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) {
  }
}

Conversion and Formatting

<mvc:annotation-driven conversion-service="conversionService"/>

<bean id="conversionService"
      class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="org.example.MyConverter"/>
        </set>
    </property>
    <property name="formatters">
        <set>
            <bean class="org.example.MyFormatter"/>
            <bean class="org.example.MyAnnotationFormatterFactory"/>
        </set>
    </property>
    <property name="formatterRegistrars">
        <set>
            <bean class="org.example.MyFormatterRegistrar"/>
        </set>
    </property>
</bean>

위의 xml과 javaconfig는 똑같은 설정이다. 간단하게 addFormatters 메서드를 재정의하면 된다.

Validation

@Override
public Validator getValidator() {
  return null;
}
<mvc:annotation-driven validator="globalValidator"/>

마찬가지로 getValidator를 재정의하면 위의 xml과 같은 설정이 된다.

Interceptors

@Override
public void addInterceptors(InterceptorRegistry registry) {
  registry.addInterceptor(new LocaleInterceptor());
  registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
  registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
}
<mvc:interceptors>
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/admin/**"/>
        <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/secure/*"/>
        <bean class="org.example.SecurityInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

위의 설정은 인터셉터이다. addInterceptors를 재정의해서 등록하면 된다.

Content Negotiation

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
  configurer.mediaType("json", MediaType.APPLICATION_JSON);
}
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="mediaTypes">
        <value>
            json=application/json
            xml=application/xml
        </value>
    </property>
</bean>

위의 xml설정은 java config의 configureContentNegotiation 재정의해서 등록하면 된다.

View Controllers

@Override
public void addViewControllers(ViewControllerRegistry registry) {
  registry.addViewController("/").setViewName("home");
}
<mvc:view-controller path="/" view-name="home"/>

static한 html을 바로 사용하기 좋은 설정이다. addViewControllers만 재정의해서 등록하면 된다.

Serving of Resources

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  registry.addResourceHandler("/resources/**").addResourceLocations("/public-resources/");
}
<mvc:resources mapping="/resources/**" location="/public-resources/"/>

리소스들은 등록한다 addResourceHandlers만 재정의하면 된다.

<mvc:resources mapping="/resources/**" location="/public-resources/" cache-period="31556926"/>

캐쉬 설정도 가능하다.

Falling Back On the “Default” Servlet To Serve Resources

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
  configurer.enable();
}
<mvc:default-servlet-handler/>

위는 같은 설정이다.

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
  configurer.enable("myCustomDefaultServlet");
}
<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>

Path Matching

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
  configurer
    .setUseSuffixPatternMatch(true)
    .setUseTrailingSlashMatch(false)
    .setUseRegisteredSuffixPatternMatch(true)
    .setPathMatcher(antPathMatcher())
    .setUrlPathHelper(urlPathHelper());
}

@Bean
public UrlPathHelper urlPathHelper() {
  //...
}

@Bean
public PathMatcher antPathMatcher() {
  //...
}
<mvc:annotation-driven>
    <mvc:path-matching
        suffix-pattern="true"
        trailing-slash="false"
        registered-suffixes-only="true"
        path-helper="pathHelper"
        path-matcher="pathMatcher"/>
</mvc:annotation-driven>

<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>

configurePathMatch만 재정의 하면 된다.

Message Converters

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
  Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
    .indentOutput(true)
    .dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
    .modulesToInstall(new ParameterNamesModule());
  converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
  converters.add(new MappingJackson2XmlHttpMessageConverter(builder.xml().build()));
}
<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper" ref="objectMapper"/>
        </bean>
        <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
            <property name="objectMapper" ref="xmlMapper"/>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
      p:indentOutput="true"
      p:simpleDateFormat="yyyy-MM-dd"
      p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>

<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>

위의 설정도 같은 설정이다.

솔직히 거의다 아는 내용이지만 아예 모르거나 긴가민가 하는 부분도 몇개 있다. 다음에는 저 설정이 어떤 역할을 하는지 알아보자!

출처 : http://docs.spring.io/spring/docs/4.3.0.RC2/spring-framework-reference/htmlsingle/#mvc-config-

:

Spring support for @Controller given by <context:component-scan /> vs <mvc:annotation-driven>

FRAMEWORK/SPRING 2017. 10. 19. 10:52

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).


출처 - https://stackoverflow.com/questions/20551217/spring-support-for-controller-given-by-contextcomponent-scan-vs-mvcannot

'FRAMEWORK > SPRING' 카테고리의 다른 글

Convertor  (0) 2017.10.19
WebMvcConfigurerAdapter  (0) 2017.10.19
Difference between <context:annotation-config> vs <context:component-scan>  (0) 2017.10.19
스프링 시큐리티(SPRING SECURITY)  (0) 2016.10.05
파일 업로드  (0) 2015.01.07
:

Difference between <context:annotation-config> vs <context:component-scan>

FRAMEWORK/SPRING 2017. 10. 19. 10:47

<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 AB and C, with B and C being injected into A.

package com.xxx;
public class B {
  public B() {
    System.out.println("creating bean B: " + this);
  }
}

package com.xxx;
public class C {
  public C() {
    System.out.println("creating bean C: " + this);
  }
}

package com.yyy;
import com.xxx.B;
import com.xxx.C;
public class A { 
  private B bbb;
  private C ccc;
  public A() {
    System.out.println("creating bean A: " + this);
  }
  public void setBbb(B bbb) {
    System.out.println("setting A.bbb with " + bbb);
    this.bbb = bbb;
  }
  public void setCcc(C ccc) {
    System.out.println("setting A.ccc with " + ccc);
    this.ccc = ccc; 
  }
}

With the following XML configuration :

<bean id="bBean" class="com.xxx.B" />
<bean id="cBean" class="com.xxx.C" />
<bean id="aBean" class="com.yyy.A">
  <property name="bbb" ref="bBean" />
  <property name="ccc" ref="cBean" />
</bean>

Loading the context produces the following output:

creating bean B: com.xxx.B@c2ff5
creating bean C: com.xxx.C@1e8a1f6
creating bean A: com.yyy.A@1e152c5
setting A.bbb with com.xxx.B@c2ff5
setting A.ccc with com.xxx.C@1e8a1f6

OK, this is the expected output. But this is "old style" Spring. Now we have annotations so lets use those to simplify the XML.

First, lets autowire the bbb and ccc properties on bean A like so:

package com.yyy;
import org.springframework.beans.factory.annotation.Autowired;
import com.xxx.B;
import com.xxx.C;
public class A { 
  private B bbb;
  private C ccc;
  public A() {
    System.out.println("creating bean A: " + this);
  }
  @Autowired
  public void setBbb(B bbb) {
    System.out.println("setting A.bbb with " + bbb);
    this.bbb = bbb;
  }
  @Autowired
  public void setCcc(C ccc) {
    System.out.println("setting A.ccc with " + ccc);
    this.ccc = ccc;
  }
}

This allows me to remove the following rows from the XML:

<property name="bbb" ref="bBean" />
<property name="ccc" ref="cBean" />

My XML is now simplified to this:

<bean id="bBean" class="com.xxx.B" />
<bean id="cBean" class="com.xxx.C" />
<bean id="aBean" class="com.yyy.A" />

When I load the context I get the following output:

creating bean B: com.xxx.B@5e5a50
creating bean C: com.xxx.C@54a328
creating bean A: com.yyy.A@a3d4cf

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.

If I change my XML to this:

<context:annotation-config />
<bean id="bBean" class="com.xxx.B" />
<bean id="cBean" class="com.xxx.C" />
<bean id="aBean" class="com.yyy.A" />

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;
@Component
public class B {
  public B() {
    System.out.println("creating bean B: " + this);
  }
}

package com.xxx;
import org.springframework.stereotype.Component;
@Component
public class 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;
@Component
public class A { 
  private B bbb;
  private C ccc;
  public A() {
    System.out.println("creating bean A: " + this);
  }
  @Autowired
  public void setBbb(B bbb) {
    System.out.println("setting A.bbb with " + bbb);
    this.bbb = bbb;
  }
  @Autowired
  public void 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:

creating bean B: com.xxx.B@1be0f0a
creating bean C: com.xxx.C@80d1ff

Hmmmm... something is missing. Why?

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.

To fix this, I add this other package also:

<context:component-scan base-package="com.xxx,com.yyy" />

and now we get the expected result:

creating bean B: com.xxx.B@cd5f8b
creating bean C: com.xxx.C@15ac3c9
creating bean A: com.yyy.A@ec4a87
setting A.bbb with com.xxx.B@cd5f8b
setting A.ccc with com.xxx.C@15ac3c9

And that's it! Now you don't have XML definitions anymore, you have annotations.

As a final example, keeping the annotated classes AB and C and adding the following to the XML, what will we get after loading the context?

<context:component-scan base-package="com.xxx" />
<bean id="aBean" class="com.yyy.A" />

We still get the correct 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

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>?

<context:annotation-config />
<context:component-scan base-package="com.xxx" />
<bean id="aBean" class="com.yyy.A" />

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:

<context:annotation-config />
<context:component-scan base-package="com.xxx" />
<bean id="aBean" class="com.yyy.A" />
<bean id="bla" class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
<bean id="bla1" class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
<bean id="bla2" class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
<bean id="bla3" class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />

will still generate the following result:

creating bean B: com.xxx.B@157aa53
creating bean C: com.xxx.C@ec4a87
creating bean A: com.yyy.A@25d2b2
setting A.bbb with com.xxx.B@157aa53
setting A.ccc with com.xxx.C@ec4a87

OK, that about raps it up.


출처 - https://stackoverflow.com/questions/7414794/difference-between-contextannotation-config-vs-contextcomponent-scan

:

WAS에서 트랜잭션 처리

FRAMEWORK 2017. 1. 4. 14:13

이번 프로젝트는 대부분의 Process Layer Component에서 다양한 DB 접근하여 데이터를 조회하고, 조회된 DATA을 요구사항에 따라 조합하여 화면에 보여주는 형태의 프로세스 개발이 많은 프로젝트였다. 다양한 DB을 사용하면서, WAS에서의 Transaction은 One Phase Commit만 지원하도록 설정되어 있었다. 따라서 GLOBAL TRANSACTION을 안됨에 SERVICE/PROVIDER LOGIC 중간 중간에 Transaction을 분리해야만 했다. 우선 Transaction의 속성을 먼저 정리하고, 적용했던 Transaction 분리 했던 방법을 설명하려 한다.

Transaction 속성은 Transaction의 영역을 제어한다. 그림 1은 EJB Bean-1의 method-A가 TX1 Transaction을 가지고 실행 중, EJB Bean-2의 method-B을 호출하는 그림으로써, Bean-2의 methjod-B가 실행할 때, TX1 Transaction을 가지고 실행할 지?, 새로운 TX? Transaction을 가지고 실행할 지?, 또는 Transaction 없이 실행할지? 는 Transaction의 속성에 따라 Transaction의 영역이 달라짐을 보여주고 있다.

그림1. Transaction Scope[3]

Transaction Propagation에는 7가지(REQUIRED, REQUIRESNEW, MADATORY, NOTSUPPORTED, SUPPORTS, NEVER, and NESTED)을 가진다. 7가지 속성은 다음과 같다.

REQUIRED 속성[3]

Bean-2의 b-Method에 Transaction 속성을 REQUIRED로 설정하였다면, 그림2, 3과 같은 경우가 발생한다. 그림 2는 Bean-1의 A-method가 TX1 Transaction을 가지고 있는 경우, B-method 호출 시 B-method은 A-method의 TX1 Transaction을 가지고 수행한다. 따라서, A-method와 B-method는 항상 같이 Commit 또는 Roll-Back 된다. 예를 들어 설정하자면, A-method의 transaction을 REQUIRED/REQUIRESNEW로 하고, B-method의 transaction을 REQUIRED로 설정한 경우이다.

그림2. Bean-1의 A-method가 TX1 Transaction을 가진 경우

그림 3은 만약 Bean-1의 A-method가 Transaction을 가지고 있지 않다면, 컨테이너는 새로운 TX2 Transaction을 생성해서 Bean-2의 B-Method를 실행한다. 예를 들어 설정하자면, A-method의 transaction을 NOTSUPPORTED로 하고, B-method의 transaction을 REQUIRED로 설정한 경우이다.

그림 3. Bean-1의 A-method가 Transaction을 가지지 않는 경우

결론적으로, B-method는 항상 Transaction 속에서 실행된다. B-method를 호출하는 method의 transaction 존재여부에 따라 Transaction을 공유할 지, 새로 생성할지가 결정된다.

참고로, 컨테이너로부터 관리되는 Transaction을 가진 모든 Enterprise Bean Method는 암묵적으로 REQUIRED을 가지고 있다. 따라서 REQUIRED외 다른 Transaction을 사용할 것이 아니면 REQUIRED을 설정하지 않아도 된다.

REQUIRESNEW 속성[3]

Bean-2의 b-Method에 Transaction 속성을 REQUIRESNEW로 설정하였다면, 그림4, 5과 같은 경우가 발생한다. 그림 4는 Bean-1의 A-method가 TX1 Transaction을 가지고 실행 중, Bean-2의 B-method를 호출한 경우를 나타낸다. 예를 들어 설정하자면, A-method의 transaction을 REQUIRED/REQUIRESNEW로 하고, B-method의 transaction을 REQUIRESNEW로 설정한 경우이다.

그림 4. Bean-1의 A-method가 TX1 Transaction을 가진 경우

그림 4의 Transaction의 흐름은 아래 절차에 따른다.

  1. B-method 호출 시, A-method TX1 Transaction을 일시 중지한다.
  2. 컨테이너는 새로운 TX2 Transaction을 시작한다.
  3. 새로운 TX2 Transaction 속에서 B-Method를 호출한다.
  4. B-Method가 완료된 후, A-Method Transaction을 다시 시작한다.

그림 5는 A-method가 Transaction 없이 B-Method를 호출한 것을 나타낸다. A-method가 B-method를 호출하는 시점에, 컨테이너는 새로운 TX2 Transaction을 생성하여, 그 TX2 Transaction 속에서 B-method를 실행한다. 예를 들어 설정하자면, A-method의 transaction을 NOTSUPPORTED로 하고, B-method의 transaction을 REQUIRESNEW로 설정한 경우이다.

그림 5. Bean-1의 A-method가 Transaction을 가지지 않는 경우

결론적으로, method의 Transaction 속성을 REQUIRESNEW로 설정하면, 항상 새로운 Transaction 속에서 실행된다.

참고로, A-method(호출자)가 Transaction이 없고, B-method(호출받는 자)의 Transaction 속성을 REQUIRED/REQUIRESNEW로 설정하면 동일한 결과를 보인다(그림 3, 그림 5).

MADATORY 속성[3]

Bean-2의 b-Method에 Transaction 속성을 MADATORY로 설정하였다면, 그림6, 7과 같은 경우가 발생한다. 그림 6은 A-method가 Transaction을 가지고 B-method을 호출하는 경우를 나타내며, B-method 호출 시, B-method은 A-method의 TX1 Transaction을 가지고 수행한다. 예를 들어 설정하자면, A-method의 transaction을 REQUIRED/REQUIRESNEW로 하고, B-method의 transaction을 MADATORY로 설정한 경우이다.

그림 6. Bean-1의 A-method가 TX1 Transaction을 가진 경우

그림 7은 A-method가 Transaction 없이, B-method을 호출한 경우를 나타낸다. B-method 호출 시, 컨테이너는 A-method에게 TransactionRequiredException을 던진다. 예를 들어 설정하자면, A-method의 transaction을 NOTSUPPORTED로 하고, B-method의 transaction을 MADATORY로 설정한 경우이다.

그림 7. Bean-1의 A-method가 Transaction을 가지지 않는 경우

이 MADATORY 속성은 호출하는 A-method가 반드시 Transaction을 가지고 수행해야 할 때, 사용한다. 다시말해, B-method가 독립적으로 Transaction을 진행하면 안 되는 경우 사용한다. 예를 들어, A-method가 계좌이체 서비스이고, B-method는 출금 서비스, C-method 입금 서비스라고 가정해 보자. A-method는 B-method를 호출한 후, C-method를 호출할 것이다. B, C-method를 MADATORY로 설정하면, A-method는 Transaction을 가질 수 밖에 없게되고, A, B, and C-method는 하나의 Transaction으로 묶이게 된다. 누군가는 A,B, and C-method를 REQUIRED로 설정한다면, 이전과 같은 결과를 가지지 않느냐라고 반문할 수 있을 것이다(동일한 결과를 얻음으로). 그러나 만약 누군가가 실수로 A- method를 REQUIRED에서 NOTSUPPORTED로 변경하였다면, 오류가 발생해도 찾아 내기 쉽지 않을 것이다. 따라서, 호출자와 Transaction을 반드시 묶어야 할 때는 명시적으로 사용하는 것이 낫다고 생각한다.

NOTSUPPORTED 속성[3]

Bean-2의 b-Method에 Transaction 속성을 NOTSUPPORTED로 설정하였다면, 그림8, 9과 같은 경우가 발생한다. 그림 8은 A-method TX1 Transaction을 가지고, B-method을 호출한 경우를 나타낸다. B-method 호출 시, 컨테이너는 B-Method를 호출하기 전에 A-Method의 TX1 Transaction을 일시 중지하고, B-Method가 완료된 후, 컨테이너는 A-Method의 TX1 Transaction을 다시 시작한다. 예를 들어 설정하자면, A-method의 transaction을 REQUIRED로 하고, B-method의 transaction을 NOTSUPPORTED로 설정한 경우이다.

그림 8. Bean-1의 A-method가 TX1 Transaction을 가진 경우

그림 9는 A-method가 Transaction 없이 B-method을 호출하는 것을 나타낸다. B-method 호출 시점에, 컨테이너는 B-Method을 진행하기 전까지 새로운 Transaction을 시작하지 않는다. 예를 들어 설정하자면, A-method의 transaction을 NOTSUPPORTED로 하고, B-method의 transaction을 NOTSUPPORTED로 설정한 경우이다.

그림 9. Bean-1의 A-method가 Transaction을 가지지 않는 경우

이 NOTSUPPORTED 속성은 Transaction이 필요하지 않는 Method을 위해 사용한다. Transaction은 항상 overhead가 따르기 때문에, 이 속성을 사용하면 performance을 개선할 수 있다.

SUPPORTS 속성[3]

Bean-2의 b-Method에 Transaction 속성을 SUPPORTS로 설정하였다면, 그림10, 11과 같은 경우가 발생한다. 그림 10은 A-method가 TX1 Transaction을 가지고, B-method을 호출하는 것을 나타낸다. B-method를 호출 시, A-method의 TX1 Transaction을 가지고 수행한다. 예를 들어 설정하자면, A-method의 transaction을 REQUIRED로 하고, B-method의 transaction을 SUPPORTS로 설정한 경우이다.

그림 10. Bean-1의 A-method가 TX1 Transaction을 가진 경우

그림 11은 A-method가 Transaction 없이 B-method을 호출하는 것을 나타낸다. B-method 호출 시, 이 B-Method는 Transaction 없이 실행한다. 컨테이너는 B-Method를 진행하기 전까지 새로운 Transaction을 시작하지 않는다. 예를 들어 설정하자면, A-method의 transaction을 NOTSUPPORTED로 하고, B-method의 transaction을 SUPPORTS로 설정한 경우이다.

그림 11. Bean-1의 A-method가 Transaction을 가지지 않는 경우

다시 말해, B-method의 Transaction은 A-method의 Transaction 속성을 따라간다. Method의 Transaction 행위가 다양함에 따라 이 SUPPORTS 속성은 주의해서 사용해야 한다.

NEVER 속성[3]

Bean-2의 b-Method에 Transaction 속성을 NEVER로 설정하였다면, 그림12, 13과 같은 경우가 발생한다. 그림 12는 A-method가 TX1 Transaction을 가지고 B-method을 호출하는 것은 나타낸다. B-method 호출 시, 컨테이너는 RemoteExcepiton 발생한다. 예를 들어 설정하자면, A-method의 transaction을 REQUIRED로 하고, B-method의 transaction을 NEVER로 설정한 경우이다.

그림 12. Bean-1의 A-method가 TX1 Transaction을 가진 경우

그림 13은 A-Method가 Transaction 없이, B-Method을 호출하는 것을 나타낸다. B-method 호출 시, 컨테이너는 B-Method을 진행하기 전까지 새로운 Transaction을 시작하지 않는다. 예를 들어 설정하자면, A-method의 transaction을 NOTSUPPORTED로 하고, B-method의 transaction을 NEVER로 설정한 경우이다.

그림 13. Bean-1의 A-method가 Transaction을 가지지 않는 경우

Manatory와 반대로 Transaction이 없이 실행해야 한다.

NESTED 속성[6, 7]

이 속성은 중접된 Transaction을 지원하는 WAS에서만 지원되는 Transaction 속성이다. Bean-2의 b-Method에 Transaction 속성을 NESTED로 설정하였다면, 그림14, 15과 같은 경우가 발생한다. 그림 14는 A-method가 TX1 Transaction을 가지고, B-method을 나타낸다. 이 경우 컨테이너는 A-method의 TX1 Transaction 내의 nested transaction형태로 TX1’ transaction을 만들어 B-Method을 실행한다. B-Method에서 발생한 변경사항이 commit이 되기 전까지는 A-method의 TX1 Transaction에서 보이지 않는다. 또한 TX1’ transaction은 자체적으로 commit, rollback이 가능하다

그림 14. Bean-1의 A-method가 TX1 Transaction을 가진 경우

A-method의 TX1 Transaction의 상태는 B-method에게 영향을 주고, B-method의 TX1’ Transaction의 상태는 A-method TX1 Transaction에게 영향을 주지 않는다[7].

그림 15는 A-method가 Transaction 없이 B-method을 호출하는 것을 나타낸다. 이 경우 컨테이너는 B-method을 REQUIRED 속성으로 실행한다.

그림 15. Bean-1의 A-method가 Transaction을 가지지 않는 경우

NESTED 속성은 WAS에 따라 지원여부가 결정된다.

  1. 지원하지 않는 WAS는 WebLogic, JEUS, and, Java EE 5 등 이다. WebLogic Server implements the flat transaction model. Nested transactions are not supported[1]. The Enterprise JavaBeans architecture supports flat transactions. A flat transaction cannot have any child (nested) transactions[2].
  2. 지원하는 WAS는 ODBC, OLE DB, and, SQL Server 등이다. Neither Open Database Connectivity (ODBC), nor Microsoft OLE DB Provider, supports Nested Transactions [4, 5].

Transaction 적용 예

그림 16은 한 Process에서 영업, 인사, 고객, 계약 등 정보를 조회하여 데이터를 조합하는 보여주는 시나리오이고, 여기의 계약 DB, 인사 DB, 고객 DB, 영업 DB가 별도로 존재한다고 가정한다.

그림 16. Transaction 분리 예

이 시나리오는 특정 계약에 가입한 고객이 다른 어떤한 종류의 계약을 가입했는 지 조회하여 목록으로 보여주는 것이다. 이 시나리오를 수행하는 흐름은 1) 로그인 사용자 정보를 기반으로 권한을 체크하고, 2) 계약번호로 계약 정보를 조회한 후, 3) 계약의 고객 번호로 고객 정보를 조회 하고, 4) 이 고객이 가지고 있는 다른 계약을 모두 조회, 5) 영업한 사람이 누구인지 조회하여 데이터를 조회하여 보여준다.

Transaction의 분리 고려사항

필자는 Transaction 분리하기 위해 4가지를 고려하였다.

  1. 이 기능은 주요 목적은 무엇인가?
  2. 어떤 Service를 어느 Transaction에 묶을 것인가?
  3. Transaction을 분리하는 횟수에 따라 overhead는 가중된다.
  4. Overhead는 성능에 영향을 준다.

위 고려 사항에 따라서 그림 16의 시나리오에서는 아래와 같이 설정하였다.

  1. 이 기능은 계약을 조회하는 목적이다.
  2. 주 목적에 따라, A-method의 TX1의 Transaction에 계약 조회 서비스(C, E - methods)를 Requried로써 하나의 Transaction으로 처리하였다.
  3. 인사, 고객, 영업은 이 기능의 주요 목적의 sub 정보임에 따라, RequiresNew로 Transaction을 나누었다.

더군다나 같은 계약 DB를 한 Transaction으로 묶어서, Transation의 Overhead도 약간이나마 줄일 수 있다.

여기에서 RequiresNew 대신에 NotSupported로 설정을 하여도 실행은 될 것이다. 단지 실행하는 method가 Transation을 가지고 실행할 것인가? 아닌가? 하는 선택의 문제이다. 오히려 NotSupported으로 설정하였을 때, Transaction Overhead가 줄어들어 성능을 개선시켜주는 효과를 줄 것이다(단. 한, 두번 테스트가 아닌 스트레스 테스트를 한 경우). 필자는 이 프로젝트가 금융과 관련되어있어 안정성을 위해서 NotSupported보다 RequiresNew를 사용하였다.

Reference

http://docs.oracle.com/cd/E23943_01/web.1111/e13731/trxsvc.htm#i1054388 
http://download.oracle.com/otn-pub/jcp/ejb-3_0-fr-eval-oth-JSpec/ejb-3_0-fr-spec-ejbcore.pdf?AuthParam=1386778361_5a64f7580be6d01251ff147c957effbc pp.316 
http://docs.oracle.com/javaee/5/tutorial/doc/bncij.html#bncim 
http://support.microsoft.com/kb/177138/ko 
http://technet.microsoft.com/ko-kr/library/ms189336(v=sql.105).aspx 
http://msdn.microsoft.com/en-us/library/windows/desktop/ms716985(v=vs.85).aspx 
http://docs.oracle.com/cd/E17276_01/html/gsg_xml_txn/java/nestedtxn.html

부록

1. EJB Transaction 설정

Weblogic에서 Transaction의 속성을 weblogic-ejb-jar.xml에 Method 단위로 정의하게 된다.

<transaction-isolation>  
<isolation-level>...</isolation-level>  
<method>  
<description>...</description>  
<ejb-name>...</ejb-name>  
<method-intf>...</method-intf>  
<method-name>...</method-name>  
<method-params>...</method-params>  
</method>  
</transaction-isolation>  

2. Transaction 속성 요약

Transaction 속성A TransactionB Transaction
RequiredNoneTX2
TX1TX1
RequiresNewNoneTX2
TX1TX2
MandatoryNoneError
TX1TX1
NotSupportedNoneNone
TX1None
SupportsNoneNone
TX1TX1
NeverNoneNone
TX1Error
NestedNoneTX1
TX1TX1’



'FRAMEWORK' 카테고리의 다른 글

라이브러리와 프레임워크에 대해  (0) 2016.12.20
Oracle Application Development Framework (ADF)  (0) 2016.12.12
Service Data Objects (SDO)  (0) 2016.12.12
Maven 을 이용한 프로젝트 생성 및 활용  (1) 2015.06.05
JNDI, TOMCAT  (0) 2013.11.06
: