SpEL

in spring, IoC Container

Spring Expression language 란?

SpEL이란 런타임시에 객체 그래프를 조회하고 조작하는 표현언어입니다. 스프링 3.0이상부터 지원하여 EL과 비슷하지만 메소드 호출 지원, 문자열 템플릿 기능까지 제공해 줍니다.

SpEL에서 지원하는 기능

  1. Literal expressions
  2. Boolean and relational operators
  3. Regular expressions
  4. Class expressions
  5. Accessing properties, arrays, lists, and maps
  6. Method invocation
  7. Relational operators
  8. Assignment
  9. Calling constructors
  10. Bean references
  11. Array construction
  12. Inline lists
  13. Inline maps
  14. Ternary operator
  15. Variables
  16. User-defined functions
  17. Collection projection
  18. Collection selection
  19. Templated expressions

SpelExpressionParser

SpelExpressionParser 클래스를 사용해서 리터럴 문자열 표현식을 파싱할수 있습니다. 싱글 쿼테이션으로 둘러싸인 문자열을 파싱 해줍니다.

1
2
3
4
5
6
7
8
@Test
public void test1() {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();

System.out.println(message);
}
1
Hello World

SpEL은 메소드를 호출하거나 프로퍼티에 접근하거나 생성자를 호출할수 있습니다. 아래의 예제는 String 메소드 중 하나인 concat을 호출하고 있습니다.

1
2
3
4
5
6
7
8
@Test
public void test2() {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();

System.out.println(message);
}
1
Hello World!

SpEL을 사용해서 객체(rootObject라고 부름)의 필드의 값을 가져 올수 있습니다. 그리고 Expression의 getValue의 파라미터 중에 하나인 desiredResultType에 반환받은 타입을 입력하면 위의 예제 처럼 캐스팅을 하지 않아도 됩니다.

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

public SpELVO(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

@Test
public void test3() {
SpELVO spELVO = new SpELVO("intelli");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
String message = (String) exp.getValue(spELVO, String.class);

System.out.println(message);
}
1
intelli

EvaluationContext

EvaluationContext 인터페이스는 프로퍼티, 메소드, 필드를 처리하고 타입변환을 수행하는 표현식을 평가할때 사용합니다. 스프링에서는 두개의 구현체를 제공합니다.


1. SimpleEvaluationContext : SpEL에서 필수적인 기능만 제공합니다.(4.3.15 버전부터 지원)
2. StandardEvaluationContext : SpEL의 모든 기능을 사용할수 있습니다.

지금까지의 예제에서도 내부에서는 EvaluationContext를 사용하고 있습니다. 아래처럼 EvaluationContext를 직접 생성 후에 사용할수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test4() {
SpELVO spELVO = new SpELVO("intelli");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
EvaluationContext evaluationContext = new StandardEvaluationContext();
String message = exp.getValue(evaluationContext, spELVO, String.class);

System.out.println(message);
}

Array

SpEL을 이용해서 배열에 접근 할수 있습니다.

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
public class ArrayList {
private List<String> names;

public List<String> getNames() {
return names;
}

public void setNames(List<String> names) {
this.names = names;
}
}


@Test
public void test5() {
ArrayList arrayList = new ArrayList();
arrayList.setNames(Arrays.asList("woman", "man"));

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("names[0]");
EvaluationContext evaluationContext = new StandardEvaluationContext(arrayList); // rootObject를 여기에 바로 쓸수도 있다.
String message = exp.getValue(evaluationContext, String.class);

System.out.println(message);
}
1
woman

값을 가져오는 것 말고도 값을 변경할수도 있습니다. 유의 해야할 점은 값을 추가해주는게 아닌 변경해주는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test6() {
ArrayLists arrayLists = new ArrayLists();
arrayLists.setNames(Arrays.asList("man"));

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("names[0]");
EvaluationContext evaluationContext = new StandardEvaluationContext(arrayLists);
exp.setValue(evaluationContext, "woman");

System.out.println(arrayLists.getNames().get(0));
}

값을 변경해주는 기능은 배열 뿐 아니라 일반 객체에서도 사용 가능합니다. 이때 필드는 public 이거나 setter 메소드가 구현되어 있어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test7() {
SpELVO spELVO = new SpELVO("jesi");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
EvaluationContext evaluationContext = new StandardEvaluationContext(spELVO);
exp.setValue(evaluationContext, "sara");

System.out.println(spELVO.getName());
}

SpelParserConfiguration

SpelParserConfiguration를 사용 하면 Collection에서 index가 null일 경우에 자동으로 생성해주는 기능을 활성화 할수 있습니다.

아래의 예제에서는 배열에서 index가 null인 요소에 접근하려고 할때 자동으로 배열에 빈값이 추가된것을 확인 할수 있습니다. SpelParserConfiguration 생성자의 두번째 파라미터를 true로 전달하면 기능을 활성화 할수 있습니다(기본값은 두개 모두 false).

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test8() {
ArrayLists arrayLists = new ArrayLists();

SpelParserConfiguration configuration = new SpelParserConfiguration(false, true);
ExpressionParser parser = new SpelExpressionParser(configuration);
Expression expression = parser.parseExpression("names[4]");
String name = expression.getValue(arrayLists, String.class);

System.out.println("result : " + name);
System.out.println(arrayLists.getNames().size());
}
1
2
result : 
5

String 뿐 아니라 일반 객체도 자동으로 생성해 줍니다. 이때 자동으로 생성 될 객체는 디폴트 생성자가 존재하여야 합니다.

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
public class SpELVO {
private String name;

public SpELVO() {
}
}

public class ArrayRefrence {
private List<SpELVO> spELVOs = new ArrayList<>();

public List<SpELVO> getSpELVOs() {
return spELVOs;
}

public void setSpELVOs(List<SpELVO> spELVOs) {
this.spELVOs = spELVOs;
}
}

@Test
public void test9() {
ArrayRefrence arrayLists = new ArrayRefrence();

SpelParserConfiguration configuration = new SpelParserConfiguration(false, true);
ExpressionParser parser = new SpelExpressionParser(configuration);
Expression expression = parser.parseExpression("spELVOs[4]");
String name = expression.getValue(arrayLists, String.class);

System.out.println("result : " + name);
System.out.println(arrayLists.getSpELVOs().size());
}

빈 정의를 정의하는 표현식

XML와 어노테이션 기반의 설정 메타데이터와 SpEL 표현식을 함께 사용할수 있습니다. 표현식을 정의 하기 위한 문법은 #{ <expression string> }입니다.

XML Configuration

표현식을 사용해서 프로퍼티나 생성자의 전달인자에 값을 할당할수 있습니다.

1
2
3
<bean id="spelVo" class="kr.co.spring.SpELVO">
<constructor-arg name="name" value="hihi"/>
</bean>
1
2
3
4
5
6
@Test
public void xml() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/spring-spel.xml");
SpELVO spelVo= (SpELVO) ctx.getBean("spelVo");
System.out.println(spelVo.getName());
}
1
hihi

어노테이션

@Value 어노테이션을 이용해서 SpEL을 사용할수 있습니다. 어노테이션이 위치할수 있는 곳은 필드, 메소드, 메소드나 생성자의 파라미터입니다.

필드에서의 사용

아래의 클래스는 Value 어노테이션을 사용하는 예제로 사용이 됩니다. 아래에서 보듯이 일반 문자열 및 빈으로 등록 된 객체의 메소드도 사용이 가능합니다(빈으로 등록되지 않으면 사용이 안됨).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ValueSpEL {
@Value("hi")
private String hello;

@Value("#{seplVo.getName()}")
private String name;

public String getName() {
return name;
}

public String getHello() {
return hello;
}
}

SpELVO와 ValueSpEL 클래스를 빈으로 등록합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class WebConfiguration {

@Bean
public SpELVO seplVo() {
return new SpELVO("sara");
}

@Bean
public ValueSpEL valueSpEL() {
return new ValueSpEL();
}
}
1
2
3
4
5
6
7
@Test
public void annotationField() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(WebConfiguration.class);
ValueSpEL valueSpEL = (ValueSpEL) ctx.getBean("valueSpEL");
System.out.println(valueSpEL.getHello());
System.out.println(valueSpEL.getName());
}
1
2
hi
sara

주의를 할 점은 xml이나 어노테이션 등에서 사용 되는 $와 #의 차이점을 구분할줄 알아야 한다. $는 프로퍼티 문법이고,
#는 SpEL 문법이다.

Comment and share

데이터 바인딩 추사황 : Converter와 Formatter

초기 스프링에서는 PropertyEditor를 사용하였습니다. 이후 이를 대체할 Converter, Formatter 인터페이스가 등장하였습니다.

Converter

Converter는 S타입을 T타입으로 변환할수 있습니다. PropertyEditor와는 다르게 상태정보가 없기 때문에 쓰레드 세이프합니다. 이로 인하여 빈으로 등록 후 사용하여도 문제가 없습니다.

xml을 이용한 Custom Converter 등록하기

Converter 만들기

Conterver를 만드는 방법은 간단합니다. Converter<>

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

public static class StringToEventConverter implements Converter<String, Event> {

public Event convert(String source) {
Event event = new Event();
event.setId(Integer.parseInt(source));
return event;
}
}

public static class EventToStringConverter implements Converter<Event, String> {

public String convert(Event source) {
return "convert : " + source.getId();
}
}
}

Comment and share

Validation 추상화

Validator 인터페이스는 어플리케이션에서 사용하는 객체 검증용 인터페이스 입니다. 어떠한 계층과도 관계가 없이 사용할수 있습니다. 주로 웹에서 많이 사용되지만 서비스, 데이터 어디에서도 사용해도 좋습니다.

구현해야 하는 메소드

Validator을 구현하는 클래스는 두개의 메소드를 오버라이딩 해야 합니다.

1. supports

객체를 검증 할 때 Validator가 검증 할수 있는 클래스인지를 판단하는 로직을 구현하는 메소드입니다. 반환값이 true 이면 검증할수 있다고 판단합니다.

2. validate

실제 검증 로직이 이루어지는 메소드입니다.


Validator 클래스 예제
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
public class EventValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Event.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "not empty", "Title must not be null");
}
}

public class Event {
private String title;
private String name;

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

ValidationUtils를 사용해서 검증 로직 구현

스프링에서 제공하는 검증 유틸인 ValidationUtils를 사용해서 검증을 할수 있습니다.

아래 코드는 값이 null 이거나 빈값이거나 길이값이 0인 경우 에러로 처리해줍니다.

1
ValidationUtils.rejectIfEmptyOrWhitespace(Errors인스턴스, 필드명, 에러코드, 에러발생시 출력할 메시지)

ValidationUtils를 사용하지 않고 검증 로직 직접 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
public class EventValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Event.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {
if (target == null || "".equals(target.getTitle())) {
errors.reject(필드명, 에러코드명, 에러발생시 출력할 메시지);
}
}
}

테스트 코드
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
public class ValidationTest {

@Test
public void testValidation() {
Event event = new Event(); // 타겟 객체
EventValidator eventValidator = new EventValidator(); // 검증 Validator

// BeanPropertyBindingResult는 Erros와 BindingResult의 구현체로써 보통은 웹에서는 MVC가 해당 객체를 생성하기 때문에 직접 생성할 일은 적다.
Errors errors = new BeanPropertyBindingResult(event, "event");

eventValidator.validate(event, errors); // 타겟 객체를 검증

for (ObjectError error : errors.getAllErrors()) { // 타겟 객체에서 Validation을 통과 못한 모든 에러를 가져옴(errors.getAllErrors)
System.out.println("=== error code ===");
System.out.println(Arrays.toString(error.getCodes()));
System.out.println(error.getDefaultMessage());
}
}
}

class EventValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Event.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {
Event event = (Event) target;

ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "not empty", "Title must not be null");

if (event.getName() == null || "".equals(event.getName())) {
errors.rejectValue("name", "not empty", "Name must not be null");
}
}
}

어노테이션을 이용한 검증

위의 방식은 복잡한 검증 로직을 구현할 때는 사용하나 빈값체크, Max, Min 값 체크 등 간단한 Validation은 어노테이션을 통해서 검증을 할수 있습니다.

타겟 객체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Event {

Integer id;

@NotEmpty
String title;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}
}
테스트 코드

직접 Validator를 구현하지 않고 어노테이션을 사용해서 간편한 검증 작업을 진행 할수 있습니다.

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

@Test
public void testValidation() {
Event event = new Event();
event.setEmail("aaa2");

Errors errors = new BeanPropertyBindingResult(event, "event");

eventValidator.validate(event, errors);

for (ObjectError error : errors.getAllErrors()) {
System.out.println("=== error code ===");
System.out.println(Arrays.toString(error.getCodes()));
System.out.println(error.getDefaultMessage());
}
}
}
pom.xml

Validation 어노테이션을 사용하기 위해서는 아래의 두개의 dependency를 추가해 주어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.3.11.Final</version>
</dependency>

Comment and share

스프링 이벤트 처리

이벤트를 사용하는 이유는 비지니스 로직과 사이드 이벤트와의 결합도를 낮추기 위해서 사용됩니다. 사이드 로직이 구현된 클래스를 직접 호출하는게 아닌 이벤트 처리를 통해서 결합도를 낮추어 줍니다.

스프링 4.2 이전 버전

이벤트 객체 생성

이벤트를 전달하기 위한 객체로써 ApplicationEvent를 상속해서 구현한다. 이벤트 발생시에 전달할 데이터 값을 해당 이벤트 객체에 주입해준다. 이벤트 객체는 Bean으로 등록하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
public class MyEvent extends ApplicationEvent {
private MyDomain myDomain;

public MyEvent(Object source, MyDomain myDomain) {
super(source);
this.myDomain = myDomain;
}

public MyDomain getMyDomain() {
return myDomain;
}
}

이벤트 핸들러 생성

이벤트가 발생했을 때 호출될 메소드를 정의하는 클래스입니다. ApplicationListener을 구현하고 이벤트 발생시 onApplicationEvent메소드가 호출됩니다. 이벤트 핸들러는 Bean으로 등록 합니다.

1
2
3
4
5
6
7
@Component
public class MyEventHandler implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent myEvent) {
System.out.println("이벤트 받았다. 데이터는 " + myEvent.getMyDomain());
}
}

이벤트 퍼블리싱

스프링에서 제공하는 ApplicationEventPublisher를 이용해서 이벤트를 퍼블리싱 할수 있습니다. ApplicationEventPublisher는 스프링에서 Bean으로 등록 되어 있어 @Autowired로 바로 사용할수 있습니다. publishEvent 메소드를 통해서 퍼블리싱 하면 등록 된 리스너 객체가 호출되게 됩니다.

1
2
3
4
5
6
7
8
9
10
@Component
public class EventService {
@Autowired
private ApplicationEventPublisher publisher;

public void run() {
MyDomain myDomain = new MyDomain();
publisher.publishEvent(new MyEvent(this, myDomain));
}
}

스프링 4.2 이후 버전(4.2 포함)

이벤트 객체 생성

4.2버전 부터는 이벤트 객체 생성시에 ApplicationEvent를 상속 받을 필요가 없습니다. 스프링 코드가 들어가지 않게 되면서 결합도를 낮춰줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyEvent {
private MyDomain myDomain;
private Object source;

public MyEvent(Object source, MyDomain myDomain) {
this.source = source;
this.myDomain = myDomain;
}

public MyDomain getMyDomain() {
return myDomain;
}
}

이벤트 핸들러 생성

이벤트 핸들러도 더 이상 ApplicationListener를 구현하지 않아도 됩니다. 이벤트 발생시 호출될 메소드에 @EventListener 어노테이션을 붙여 줍니다.

1
2
3
4
5
6
7
8
@Component
public class MyEventHandler {

@EventListener
public void onApplicationEvent(MyEvent myEvent) {
System.out.println("이벤트 받았다. 데이터는 " + myEvent.getMyDomain());
}
}

결론

이벤트 객체와 이벤트 핸들러에서 스프링 관련 코드가 사라지면서 결합도가 낮아지게 되었습니다.
이벤트 리스너가 2개 이상인 경우에는 모두 실행이 되게 됩니다. 이때 순서는 보장 되지 않습니다.

  • @Order을 사용해서 순서를 정할수있습니다.
  • @Order 사용시에 숫자가 작을수록 먼저 실행이 됩니다.
  • 비동기적으로 실행을 원하면 @Async와 함께 사용합니다. 이때 @Order를 사용하더라도 순서가 보장되지 않습니다.

스프링에서 제공되는 이벤트

  1. ContextRefreshedEvent : 컨텍스트가 리프레시 될떄 발동
  2. ContextClosedEvent: 컨텍스트가 종료될때 발동

Comment and share

데이터 바인딩

기술적 관점

프로퍼티 값을 타겟 객체에 설정하는 기능입니다.

사용자 관점

사용자 입력값을 어플리케이션 도메인 모델이 동적으로 변환해 넣어주는 기능입니다. 클라이언트 입력값은 대부분 문자열인데, 그 값을 객체가 가지고 있는 int, long, boolean, date 등 심지어 Event, Book 과 같은 도메인 타입으로 변환해서 넣어주는 기능입니다. MVC할때 클라이언트에서 받은 데이터를 객체에 넣어주는 기능이 해당 기능입니다.


PropertyEditor

스프링 3.0 이전까지 DataBinder가 변환 작업시에 사용하던 인터페이스입니다.

단점

PropertyEditor는 값을 set 하는 경우 상태를 저장

PropertyEditor가 값을 set하는 경우 쓰레드마다 값을 공유할수 있어 쓰레드-세이프하지 않습니다. 그렇기 때문에 PropertyEditor의 구현체는 여러 쓰레드에서 공유해서 사용하서는 안됩니다.

Bean으로 등록해서 사용하면 안됩니다.


사용 방법

xml에서 빈 등록시 문자열로 넘어온 값을 타입에 맞게 데이터 바인딩 하기


DataBindingBean

데이터 바인딩에서 사용할 샘플 Bean 클래스

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
public class DataBindingBean {
private String str;
private Date date;
private boolean aBoolean;

public void setStr(String str) {
this.str = str;
}

public void setDate(Date date) {
this.date = date;
}

public void setaBoolean(boolean aBoolean) {
this.aBoolean = aBoolean;
}

@Override
public String toString() {
return "DataBindingBean{" +
"str='" + str + '\'' +
", date=" + date +
", aBoolean=" + aBoolean +
'}';
}
}

spring-bean.xml

DataBindingBean를 Bean으로 등록하기 위한 IoC 컨테이너

1
2
3
4
5
6
7
8
9
10
11
<bean id="dataBindingBean" class="kr.co.spring.DataBindingBean">
<property name="str">
<value>안녕하세요</value>
</property>
<property name="date">
<value>2020-01-09</value>
</property>
<property name="aBoolean">
<value>true</value>
</property>
</bean>
TestClass
1
2
3
4
5
6
7
8
9
public class TestClass {

@Test
public void test() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/spring-bean.xml");
DataBindingBean dataBindingBean = (DataBindingBean) ctx.getBean("dataBindingBean");
System.out.println(dataBindingBean.toString());
}
}

위의 코드를 작성 한 후에 실행 해보면 아래와 같이 타입이 맞지 않다고 에러를 발생시킵니다. xml에서 모든 값을 문자열로 넘겨주었으니 타입이 맞지 않다는 에러가 나오는 것이 당연합니다. 이제 문자열로 값이 넘어오더라도 타입게 맞게 데이터 바인딩이 되도록 수정해 보도록 하겠습니다.

1
...Failed to convert property value of type [java.lang.String] to required type [java.util.Date] for property 'date'...

CustomEditorConfigurer 등록하기


4.0 이전 버전

DataBinding에서 사용이 되는 CustomEditorConfigurer의 변화로 인해서 4.0 이전 버전과 이후 버전의 설정이 변경 되었습니다.

spring-bean.xml

4.0 이전 버전에서는 customEditors의 type이 Map<String, ?> 이었습니다. 아래와 같이 PropertyEditor를 구현한 클래스들을 Bean으로 등록해 주면 됩니다.

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
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="java.util.Date">
<bean class="org.springframework.beans.propertyeditors.CustomDateEditor">
<constructor-arg>
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd"/>
</bean>
</constructor-arg>
<constructor-arg value="true"/>
</bean>
</entry>

<entry key="java.lang.String">
<bean class="org.springframework.beans.propertyeditors.StringTrimmerEditor">
<constructor-arg value="true"/>
</bean>
</entry>

<entry key="java.lang.Boolean">
<bean class="org.springframework.beans.propertyeditors.CustomBooleanEditor">
<constructor-arg value="true"/>
</bean>
</entry>
</map>
</property>
</bean>

4.0 이후 버전(4.0 포함)

4.0 이후 버전에서는 customEditors의 type이 Map<Class<?>, Class<? extends PropertyEditor>>로 바뀌었습니다. 이제는 Bean이 아니라 클래스 경로를 넘겨 주면 됩니다.

spring-bean.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<!-- 1. entry 태그의 key와 value 속성을 이용하여 작성 -->
<entry key="java.util.Date" value="org.springframework.beans.propertyeditors.CustomDateEditor"/>

<!-- 2. value 태그를 이용하여 작성 -->

<!-- 2가지 모두 사용 가능 -->
<entry key="java.lang.String">
<value>org.springframework.beans.propertyeditors.StringTrimmerEditor</value>
</entry>

<entry key="java.lang.Boolean">
<value>org.springframework.beans.propertyeditors.CustomBooleanEditor</value>
</entry>
</map>
</property>
</bean>

변화가 되면서 PropertyEditor 구현체가 default 생성자가 없으면 아래처럼 에러가 발생합니다.

1
No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.beans.propertyeditors.CustomDateEditor.<init>()

위의 방법은 모든 데이터바인딩에 적용이 되는 것임. DataBindingBean를 위해서 작성 된게 아니라 문자열로 넘어온 값을 Date 또는 Boolean으로 데이터바인딩해야 할 모든 상황에서 사용되게 됨. 데이터바인딩에서 사용될 사용자 PropertyEditor 구현체를 등록 하는 절차임.


PropertyEditorRegistrar

PropertyEditorRegistrar를 구현함으로써 PropertyEditor 등록을 커스텀하게 작성할수 있습니다. 이를 이용하면 default 생성자가 없는 경우에도 등록 할수 있습니다.

CustomEditorRegistrar
1
2
3
4
5
6
7
public class CustomEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
registry.registerCustomEditor(String.class, new StringTrimmerEditor(true));
registry.registerCustomEditor(Boolean.class, new CustomBooleanEditor(true));
}
}
1
2
3
4
5
6
7
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<bean class="kr.co.spring.CustomEditorRegistrar"/>
</list>
</property>
</bean>

정상적으로 잘 출력이 되는 것을 확인할 수 있습니다.

1
DataBindingBean{str='안녕하세요', date=Thu Jan 09 00:00:00 KST 2020, bool=true}

Comment and share

Resource 추상화

스프링의 Resource 객체는 java.net.URL을 추상화한 인터페이스입니다. Resource 객체는 스프링 내부에서 가장 많이 사용이 되는 인터페이스이며 스프링 IoC 컨테이너가 생성 될때, 컨테이너 설정 정보를 담는 파일들을 가져올때도 사용합니다.

Resource 인터페이스를 통해 추상화한 이유 java.net.URL의 한계로 클래스 패스를 기준으로 리소스를 읽어오는 기능이 존재하지 않기 때문입니다.


주요 메소드
  1. exists
  2. isOpen
  3. isFile
  4. isDirectory
  5. getFile(항상 파일로 가져올수 있는 것은 아님)

구현체
  1. UrlResource : URL을 기준으로 리소스를 읽어들이며 기본으로 제공하는 프로토콜에는 http, https, ftp, file, jar
  2. ClassPathResource : 클래스패스를 기준으로 리소스를 읽어들이며 접두어로 classapth:를 사용
  3. FileSystemResource : 파일 시스템을 기준으로 읽어들임
  4. ServletContextResource : 웹 어플리케이션 루트에서 상대경로로 리소스를 읽어들임

ResourceLoader

ResourceLoader는 리소스를 읽어오는 기능을 제공하는 인터페이스 입니다. ApplicationContext도 ResourceLoader를 상속하고 있습니다. 기능은 말그대로 리소스를 읽어오는 기능만 제공하고 있습니다.

구현체
  1. DefaultResourceLoader : UrlResource(경로가 http, https 등 프로토콜로 시작)와 ClassPathResource(경로가 classapth:로 시작)를 가져올때 사용
  2. FileSystemResourceLoader : DefaultResourceLoader를 상속하고 있으며 경로가 /로 시작하는 경우 FileSystemResource를 반환.
  3. GenericWebApplicationContext : DefaultResourceLoader를 상속하고 있으며 경로가 /로 시작하는 경우 ServletContextResource를 반환
    • DefaultResourceLoader를 직접 상속 하고 있지는 않음

리소스를 가져오는 코드는 아래와 같습니다.

1
resourceLoader.getResource("location 문자열");

Resource의 타입과 ApplicationContext 타입의 관계

Resource의 타입은 location 문자열과 ApplicationContext의 타입에 따라 결정됩니다. 위의 ResourceLoader 구현체를 통해서 Resource를 얻어오는게 아니라 bean으로 등록 된 ApplicationContext를 통해서 Resource를 가져올때는 아래와 같이 적용됩니다.

  1. ClassPathXmlApplicationContext => ClassPathResource
  2. FileSystemXmlApplicationContext => FileSystemResource
  3. WebApplicationContext => ServletContextResource

만약 ApplicationContext의 타입과 상관없이 리소스 타입을 강제하고 싶다면 접두어를 사용하면 됩니다.

  1. classpath
  2. file
  3. http

그래서 좀더 명확한 코드를 작성하기 위해 접두어를 사용해서 Resource를 가져오는 것이 좋습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanConfig.class)
public class TestControllerTest {
@Autowired
ResourceLoader resourceLoader;

@Test
public void test() {
System.out.println(resourceLoader.getClass());

Resource resource = resourceLoader.getResource("test.properties");
System.out.println(resource.getClass());

ResourceLoader defulatResourceLoader = new DefaultResourceLoader();
Resource defaultResource = defulatResourceLoader.getResource("classpath:test.properties");
System.out.println(defaultResource.getClass());
}
}
1
2
3
class org.springframework.context.support.GenericApplicationContext
class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource
class org.springframework.core.io.ClassPathResource

Comment and share

MessageSource

스프링 메시지소는 국제화(i18n)을 제공하는 인터페이스입니다. 메시지 설정 파일을 통해서 각 국가에 해당하는 언어로 메세지를 제공할수 있습니다. ApplicationContext는 MessageSource를 구현하고 있습니다.

메시지 설정 파일

메시지 설정 파일은 프로퍼티파일을 사용하며 파일 이름에 [파일이름][언어][국가].properties 형식으로 파일을 추가해주면 됩니다. 아래와 같이 2개의 파일을 생성하게 되면 인텔리제이에서는 Bundle로 묶이는 것을 확인 할수 있습니다.

1
2
messages.properties : 기본 메시지       
messages_ko_KR.properties: 한국 메시지

파일이름이 messages로 시작하지 않아도 된다. 위의 형식만 맞춰주면 된다. 스프링 부트를 쓸 경우에는 messages로 시작하면 자동으로 등록 해준다.

메시지 가져오기

위와 같은 형식으로 파일을 생성한 후에 프로퍼티 작성 방식인 key=value 형식으로 값을 입력합니다.

1
2
// messages.properties
greeting=Hello, so good {0}
1
2
// messages_ko_KR.properties
greeting=안녕하세요 {0}

ReloadableResourceBundleMessageSource를 Bean으로 등록 해 줍니다. 여기에서 basename은 경로를 포함한 파일이름까지 적어주면 됩니다. 예를 들어 클래스패스에서 common/message-common_ko_KR.properties로 구성할 예정이라면 messageSource.setBasename("classpath:common/message-common") 이렇게 입력해주면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ComponentScan
public class BeanConfig {

@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}

위처럼 설정이 완료가 되면 메시지를 가져올 준비가 되었습니다. messageSource.getMessage(“이름”, new String[]{“파라미터1”, “파라미터2..”}, Locale) 순으로 작성해주면 됩니다. 두번째 파라미터인 배열을 넘기면 프로퍼티에서 작성했었던 {0}에 값이 설정이 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanConfig.class)
public class TestControllerTest {

@Autowired
MessageSource messageSource;

@Test
public void test() {
System.out.println(messageSource.getMessage("greeting", new String[]{"1"}, Locale.KOREA));
}
}

메시지소스 리로딩

ReloadableResourceBundleMessageSource는 리로딩 기능을 가지고 있습니다. 프로퍼티의 변경을 감지해서 적용 해주는 기능을 가지고 있습니다. 설정은 bean 생성시에 아래 한줄을 추가 해주면 됩니다.

1
messageSource.setCacheSeconds(60);
xml에서 MessageSource bean 등록 하기
1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:/messages.properties/message-common</value>
</list>
</property>
<property name="defaultEncoding">
<value>UTF-8</value>
</property>
<property name="cacheSeconds">
<value>60</value>
</property>
</bean>

Comment and share

Environment

ApplicationContext는 EnvironmentCapable 인터페이스를 구현하고 있습니다. 이 인터페이스는 getEnvironment 메소드를 제공하며 호출시 Environment를 반환해 줍니다. Environment 클래스는 프로파일 및 프로퍼티 값과 관련이 있습니다.

프로파일

개발을 하다보면 로컬, 개발, 운영등 각 환경마다 설정을 달리 해주어야 하는 경우가 발생합니다. 이때 각 환경마다 활성화할 Bean을 관리해주는 역할을 하는게 프로파일입니다.

프로파일을 설정하면 앱 구동시에 설정된 프로파일 active 값에 따라서 해당 bean을 등록 할지 여부를 결정하게 됩니다. 예를 들어 A라는 Bean은 테스트시에만 쓰고 싶다면, 해당 Bean을 테스트 프로파일로 구성하면 테스트시에만 활성화 되게 됩니다.

설정 방법

설정 파일에 클래스에 정의를 하면 해당 설정 파일에서 정의한 모든 Bean을 한번에 정의할수 있습니다.

1
2
3
4
5
6
@Configuration
@ComponentScan
@Profile("test")
public class BeanConfig {

}

메소드로 정의하여 개별적으로 정의 할수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<!-- DevBean.class -->
@Component
public class DevBean {
}

<!-- LocalBean.class -->
@Component
public class LocalBean {
}

<!-- AllBean.class -->
@Component
public class AllBean {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class BeanConfig {

@Bean
@Profile("local")
public LocalBean localBean() {
return new LocalBean();
}

@Bean
@Profile("dev")
public DevBean devBean() {
return new DevBean();
}

@Bean
public AllBean allBean() {
return new AllBean();
}
}

Profile을 설정하지 않은 allBean와 활성화 프로파일로 설정한 dev만 bean이 등록된것을 확인할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanConfig.class)
@ActiveProfiles("dev")
public class TestControllerTest {

@Autowired
ApplicationContext ctx;

@Test
public void test() {
System.out.println(Arrays.toString(ctx.getBeanDefinitionNames()));
}
}
1
[...,allBean, devBean]]

위의 방법 이외에도 Bean 등록할 클래스에 직접 사용할수도 있습니다.

1
2
3
4
@Component
@Profile("local")
public class LocalBean {
}
프로파일 문자열

지금까지 사용했었던 프로파일 문자열은 사용자가 임의로 만들수 있습니다. 그리고 지금까지는 활성화할 프로파일을 하나만 설정하였지만 연산자를 통해서 한번에 여러개의 프로파일을 활성화할수 있습니다.

!를 문자열 앞에 붙이면 반대의 의미가 됩니다.

1
!dev 는 dev가 아닌 것만 활성화

&는 and의 의미를 가지고 있습니다.

1
abc & def는 abc 이면서 def인것만 활성화

|는 or의 의미를 가지고 있습니다.

1
abc | def는 abc이거나 def인것만 활성화

프로퍼티

프로퍼티는 다양한 방법으로 정의할수 있는 설정값입니다. 프로퍼티는 key=value로 구성이 됩니다.

추가 방법

@PropertySource("classpath:파일위치")로 추가가 가능합니다. XML에서도 프로퍼티 등록이 가능합니다. 되도록 @Configuration이 선언된 클래스에 함께 사용하도록 합시다.

프로퍼티 값 가져오기

등록 된 프로퍼티는 Environment에서 가져올수 있습니다. Environment도 Bean으로 등록 되어 있어 의존 주입 받아 쓰거나, ApplicationContext에서 getEnvironment 메소드로 가져올수 있습니다.

1
2
Environment en = application.getEnvironment();
en.getProperty("key");

Comment and share

싱글톤

스프링에서 Scope에 대한 설정을 하지 않고 Bean 등록을 하게 되면 싱글톤으로 등록이 됩니다. 이 경우에는 하나의 bean을 사용하게 됩니다.

테스트용으로만 사용 할 Library 클래스입니다.

1
2
3
4
@Component
public class Library {

}

동일한지 테스트 코드를 실행해 봅니다. 정상 실행이 되는 것을 확인할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestControllerTest {

@Test
public void test() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
Library library1 = (Library) ctx.getBean("library");
Library library2 = (Library) ctx.getBean("library");

System.out.println(library1);
System.out.println(library2);
assertEquals(library1, library2);
}
}
1
2
kr.co.spring.Library@6293abcc
kr.co.spring.Library@6293abcc
프로토타입

프로토타입으로 설정을 하는 경우 사용할때 마다 새로운 객체를 받습니다. 사용하는 방법은 프로토타입으로 사용할 객체 위에 @Scope("prototype")를 추가해주면 됩니다.

1
2
3
4
5
@Component
@Scope("prototype")
public class Library {

}

동일한 테스트 코드를 실행했을때 이번에는 두개의 객체가 서로 다르다를 에러가 발생하엿습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestControllerTest {

@Test
public void test() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
Library library1 = (Library) ctx.getBean("library");
Library library2 = (Library) ctx.getBean("library");

System.out.println(library1);
System.out.println(library2);
assertEquals(library1, library2);
}
}
1
2
3
4
5
6
kr.co.spring.Library@2133814f
kr.co.spring.Library@4c15e7fd

java.lang.AssertionError:
Expected :kr.co.spring.Library@2133814f
Actual :kr.co.spring.Library@4c15e7fd
싱글톤 타입에서 프로토 타입 의존주입

프로토타입 bean에서 싱글톤타입의 bean을 의존주입 받아서 사용할때는 아무런 문제가 발생하지 않습니다. 하지만 반대의 경우에는 개발자의 의도와는 다른 결과를 발생시킬수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- SingletonBean -->
@Component
public class SingletonBean {
@Autowired
PrototypeBean prototypeBean;

public PrototypeBean getPrototypeBean() {
return prototypeBean;
}
}

<!-- PrototypeBean -->
@Component
@Scope("prototype")
public class PrototypeBean {

}
1
2
3
4
5
6
7
8
9
10
11
public class TestControllerTest {

@Test
public void test() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
SingletonBean singletonBean = (SingletonBean) ctx.getBean("singletonBean");

System.out.println(singletonBean.getPrototypeBean());
System.out.println(singletonBean.getPrototypeBean());
}
}
1
2
kr.co.spring.PrototypeBean@37918c79
kr.co.spring.PrototypeBean@37918c79

위의 결과를 보았을때 프로토타입으로 Scope를 설정했지만 동일한 객체인것을 확인 할수 있습니다.

해결방법1. 프록시객체를 의존주입하기

@Scope 어노테이션에 프록시 설정을 하여 해당 프로토타입 빈을 감싸는 프록시 객체를 반환하는 방식을 사용하여 해결할수 있습니다.

1
2
3
4
5
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeBean {

}
1
2
kr.co.spring.PrototypeBean@28eaa59a
kr.co.spring.PrototypeBean@3427b02d
해결방법2. ObjectProvider 사용

프로토타입 bean을 의존주입 받을 때 ObjectProvider<타입>을 사용 할수 있습니다. 실제 bean을 사용할 때는 getIfAvailable메소드를 이용합니다.

1
2
3
4
5
6
7
8
9
@Component
public class SingletonBean {
@Autowired
ObjectProvider<PrototypeBean> prototypeBean;

public PrototypeBean getPrototypeBean() {
return prototypeBean.getIfAvailable();
}
}
1
2
kr.co.spring.PrototypeBean@222114ba
kr.co.spring.PrototypeBean@3d121db3

위의 방법의 경우에는 스프링 클래스를 사용하기 때문에 스프링에 의존이 되어 첫번째 방법을 추천.

Comment and share

IoC 컨테이너는 Bean 라이프 사이클을 관리하여 주고 특정한 시점에 Bean에게 이를 알려줄수 있는 메커니즘을 제공합니다. 스프링에서는 주로 init와 destroy 이벤트를 제공합니다.

InitializingBean와 DisposableBean 인터페이스

InitializingBean 인터페이스를 활용해서 Bean의 초기화 작업을 진행할수 있습니다. 해당 인터페이스에서 제공하는 afterPropertiesSet메소드를 구현하면 Spring이 Bean의 생성 후에 해당 메소드를 호출해 줍니다.

1
2
3
4
5
6
7
8
9
10
@Component
public class Book implements InitializingBean {
@Autowired
BeanFactory beanFactory;

public void afterPropertiesSet() throws Exception {
System.out.println("Bean이 등록되었습니다.");
System.out.println(beanFactory.getBean("book"));
}
}
1
2
Bean이 등록되었습니다.
kr.co.spring.Book@3745e5c6

DisposableBean 인터페이스에서 제공하는 destroy메소드를 구현하면 Bean의 소멸 직전에 해당 메소드를 호출해 줍니다.

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Book implements DisposableBean {

@Autowired
BeanFactory beanFactory;

public void destroy() throws Exception {
System.out.println("Bean이 삭제됩니다..");
System.out.println(beanFactory.getBean("book"));
}
}

xml 빈 등록시 사용자 라이프사이클 메소드 정의

xml에서 빈을 수동으로 등록시에 위의 InitializingBean, DisposableBean을 사용하지 않고 사용자 메소드를 정의 가능합니다.

1
<bean id="book" class="kr.co.spring.Book" init-method="initBook" destroy-method="destroyBook"/>
1
2
3
4
5
6
7
8
9
public class Book {
public void initBook() {
System.out.println("빈이 등록되었습니다.");
}

public void destoryBook() {
System.out.println("빈이 삭제되었습니다.");
}
}

@PostConstruct와 @PreDestroy

좀 더 간편하게 어노테이션을 이용해서 위와 동일하게 사용할수 있습니다.

@PostConstruct는 Bean이 생성 된 후에 호출 될 메소드 위에 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Book {
@Autowired
BeanFactory beanFactory;

@PostConstruct
public void init() throws Exception {
System.out.println("Bean이 등록되었습니다.");
System.out.println(beanFactory.getBean("book"));
}
}
1
2
Bean이 등록되었습니다.
kr.co.spring.Book@644baf4a

@PreDestroy는 Bean이 소멸 직전에 호출 될 메소드 위에 사용합니다.

1
2
3
4
5
6
7
8
@Component
public class Book {

@PreDestroy
public void destroy() throws Exception {
System.out.println("빈이 삭제되었습니다.");
}
}

기타

ApplicationContext 클래스도 Bean으로 등록되어 있기 때문에 의존주입 받을수 있습니다. 이 뿐 아니라 ApplicationContext가 상속하고 있는 BeanFactory, ResourceLoader 등도 Bean으로 등록되어 있기 때문에 ApplicationContext를 의존주입 받지 않고 필요한 기능을 가진 Bean을 의존주입해서 사용하는 것이 가시성에 좋습니다(ex : Bean과 관련된 처리를 할때는 BeanFactory사용).

Comment and share

Moon Star

author.bio


author.job