7. Spring 표현 언어 (SpEL)
7.1 소개
스프링 표현언어 (줄여서 SpEL)는 런타임시에 객체 그래프를 조회하고 조작하는 강력한 표현언어이다. 언어의 문법은 통일된 EL(Unified EL)과 비슷하지만 추가적인 기능을 제공한다. 가장 눈에 띄는 것은 메서드 호출과 기본 문자열 템플릿 기능이다.
OGNL, MVEL, JBoss EL 등의 여러가지 자바 표현언어가 존재하지만 스프링 표현언어는 스프링 포트폴리오의 모든 제품에 걸쳐서 사용할 수 있는 하나의 표현언어를 스프링 커뮤니티에 제공하기 위해서 만들어졌다. 스프링 표현언어의 기능은 이클립스에 기반한 SpringSource Tool Suite에서 코드 자동완성같은 도구의 요구사항을 포함해서 스프링 포트폴리오의 제품들의 필요사항에 따라 만들어졌다. SpEL은 통합되어야 하는 다른 표현언어 구현체를 허용하기 위해 기술 불가지론 API(technology agnostic API)에 기반하고 있다.
스프링 포트폴리오내에서 표현식 평가(expression evaluation)의 기반으로 SpEL을 제공하지만 SpEL은 스프링에 직접 묶여있지 않고 독립적으로 사용할 수 있다. 독립적이기 위해서 이번 챕터의 많은 예제는 독립적인 표현언어처럼 SpEL을 사용한다. 독립적으로 사용하려면 파서같은 부트스트랩핑 기반 클래스 몇가지를 생성할 필요가 있다. 대부분의 스프링 사용자들은 이러한 기반 클래스를 다룰 필요 없이 평가할 표현문을 작성하기만 하면 된다. 일반적인 사용에 대한 예제는 빈 정의를 정의하는 표현식 섹션에서 보여주는 것과 같은 XML 생성이나 어노테이션 기반의 빈정의를 생성하는 곳에 SpEL을 통합하는 것이다.
이번 챕터에서는 표현언어의 기능, API, 언어의 문법을 다룬다. 여러 부분에서 Inventor와 Inventor의 Society 클래스를 표현식 평가에 대한 타겟객체로 사용한다. 이러한 클래스 선언과 선언에 관한 데이터들은 이 챕터의 마지막 부분에 나와있다.
7.2 기능 개요
표현 언어는 다음의 기능을 지원한다.
- 리터럴 표현식
- 불리언과 관계형 오퍼레이터
- 정규 표현식
- Class 표현식
- 프로퍼티, 배열, 리스트, 맵에 대한 접근
- 메서드 호출
- 관계형 오퍼레이터
- 할당
- 생성자 호출
- 빈(Bean) 참조
- 배열 생성
- 인라인 리스
- 삼항 연산자
- 변수
- 사용자 정의 함수
- 컬렉션 투영(Collection projection)
- 컬렉션 선택
- 템플릿화된 표현식
7.3 스프링의 Expression 인터페이스를 사용한 표현식 평가
이번 섹션에서는 SpEL 인터페이스의 간단한 사용방법과 SpEL의 표현언어를 설명한다. 완전한 언어 레퍼런스는 언어 레퍼런스 섹션에 나와 있다.
다음 코드는 리터럴 문자표현인 'Hello World'를 평가하는 SpEL API를 설명한다.
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();
message 변수의 값은 'Hello World'다.
아마도 가장 많이 사용할 SpEL 클래스와 인터페이스는 org.springframework.expression 패키지와 이 패키지의 하위 페이지와 spel.support 패키지안에 있다.
ExpressionParser 인터페이스는 문자열 표현을 파싱하는 책임이 있다. 이 예제에서 문자열 표현은 따옴표로 묶인 문자열 리터럴로 표현됐다. Expression 인터페이스는 앞에서 정의한 문자열 표현의 평가를 책임진다. 'parser.parseExpression'와 'exp.getValue'를 호출할 때 각각 ParseException와 EvaluationException의 두 가지 예외가 던져질 수 있다.
SpEL은 메서드 호출이나 프로퍼티 접근, 생성자 호출같은 넓은 범위의 기능을 지원한다.
메서드 호출의 예제로 문자열 리터럴에서 'concat' 메서드를 호출한다.
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();
message의 값은 이제 'Hello World!'이다.
JavaBean 프로터티를 호출하는 예제로 다음에서 보듯이 문자열 프로퍼티 'Bytes'를 호출할 수 있다.
ExpressionParser parser = new SpelExpressionParser();
// 'getBytes()' 실행
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();
SpEL은 표준 'dot' 표기법(예: prop1.prop2.prop3)을 사용해서 중첩된 프로퍼티와 프로퍼티의 값 설정도 지원한다.
퍼블릭 필드도 접근할 수 있다.
ExpressionParser parser = new SpelExpressionParser();
// 'getBytes().length' 실행
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
int length = (Integer) exp.getValue();
문자열 리터럴 대신 String의 생성자를 호출할 수 있다.
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()");
String message = exp.getValue(String.class);
제너릭 메서드 public <T> T getValue(Class<T> desiredResultType)의 사용을 보자. 이 메서드를 사용하면 표현식의 값을 원하는 결과 타입으로 캐스팅할 필요가 없어진다. 값이 타입 T로 캐스팅할 수 없거나 등록된 타입 컴버터를 사용해서 변환할 수 없다면 EvaluationException가 던져질 것이다.
SpEL의 더 일반적인 사용방법은 특정 객체 인스턴스(root 객체라고 부른다.)에 대해서 평가되는 표현식 문자열을 제공하는 것이다. 두가지 옵션이 있는데 표현식을 평가하는 각 호출과 함께 변경될 수 있는 평가될 표현식에 대한 객체에 따라 선택한다. 다음 예제에서 Inventor 클래스의 인스턴스에서 name 프로퍼티를 획득한다.
// 캘린더를 생성하고 설정한다
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// 생성자 아규먼트는 name, birthday, nationality이다.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
EvaluationContext context = new StandardEvaluationContext(tesla);
String name = (String) exp.getValue(context);
마지막 줄에서 문자열 변수 'name'의 값은 "Nikola Tesla"로 설정될 것이다. StandardEvaluationContext 클래스는 "name" 프로퍼티가 평가될 객체를 지정할 수 있는 곳이다. 이는 root 객체가 변경되지 않는 경우 사용할 메카니즘이고 평가 컨텍스트에서 설정할 수 있다. root 객체가 계속해서 변경될 것이라면 다음 예제에서 보여주듯이 각 getValue 호출에서 제공할 수 있다.
// 캘린더를 생성하고 설정한다
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// 생성자 아규먼트는 name, birthday, nationality이다.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
String name = (String) exp.getValue(tesla);
이 경우에 inventor tesla를 직접 getValue에 제공하고 표현식 평가의 기반은 내부적으로 기본 평가 컨텍스트를 생성하고 관리한다 - 이는 제공하는 것을 필요로 하지 않는다.
StandardEvaluationContext는 생성하는 비용이 상대적으로 비싸고 반복적으로 사용하는 동안 상태를 캐시해서 다음 표현식 평가를 더 빠르게 수행할 수 있도록 한다. 이 때문에 각 표현식 평가에서 새로운 것을 생성하는 것보다는 가능한한 캐시한 후에 재사용 하는 것이 더 좋다.
몇몇 경우에 평가 컨텍스트를 설정해서 사용하기를 원할 수 있고 getValue를 호출할 때마다 다른 루트객체를 제공하기 원할 수 있다. getValue는 같은 호출에서 두 가지를 모두 지정할 수 있다. 이러한 경우에 호출에 전달된 루트객체는 평가 컨텍스트가 지정한 것(null일수도 있다.)을 오버라이드한다.
Note
SpEL를 독립적으로 사용할 때 파서를 생성하고 표현식을 파싱할 필요가 있고 어쩌면 평가 컨텍스트와 루트 컨텍스트 객체를 제공할 필요가 있을 수도 있다. 하지만 설정 파일(예를 들면 스프링 빈이나 스프링 Web Flow 정의에서)의 일부로 SpEL 표현식 문자열만 제공하는 것이 더 일반적인 사용방법이다. 이 경우에 파서, 평가 컨텍스트, 루트 객체와 미리 정의한 어떤 변수도 암묵적으로 설정할 수 있어서 사용자는 표현식 외에는 아무것도 설정할 필요가 없다.
SpEL를 독립적으로 사용할 때 파서를 생성하고 표현식을 파싱할 필요가 있고 어쩌면 평가 컨텍스트와 루트 컨텍스트 객체를 제공할 필요가 있을 수도 있다. 하지만 설정 파일(예를 들면 스프링 빈이나 스프링 Web Flow 정의에서)의 일부로 SpEL 표현식 문자열만 제공하는 것이 더 일반적인 사용방법이다. 이 경우에 파서, 평가 컨텍스트, 루트 객체와 미리 정의한 어떤 변수도 암묵적으로 설정할 수 있어서 사용자는 표현식 외에는 아무것도 설정할 필요가 없다.
마지막 소개하는 예제로 불리언 연산자의 사용은 이전 예제의 Inventor객체의 사용을 보여준다.
Expression exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(context, Boolean.class); // true로 평가된다
7.3.1 EvaluationContext 인터페이스
프로퍼티, 메서드, 필드를 처리하고 타입변환을 수행하는 표현식을 평가할 때 EvaluationContext 인터페이스를 사용한다. 새로운 구현체 StandardEvaluationContext는 객체를 조작하려고 리플렉션을 사용하고 성능을 향상시키기 위해서 java.lang.reflect의 Method, Field, Constructor를 캐싱한다.
StandardEvaluationContext는 setRootObject() 메서드나 생성자에 루트객체를 전달해서 평가에 사용할 루트객체를 지정하는 곳이다. setVariable()와 registerFunction() 메서드를 사용해서 표현식에서 사용할 변수나 함수를 지정할 수도 있다. 변수와 함수의 사용방법은 언어 레퍼런스 섹션인 변수와 함수에 설명되어 있다. StandardEvaluationContext는 SpEL이 표현식을 평가하는 방법을 확장하기 위해서 커스텀 ConstructorResolver, MethodResolver, PropertyAccessor를 등록할 수 있는 곳이기도 하다. 이러한 클래스들에 대한 더 자세한 내용은 JavaDoc을 참고해라.
7.3.1.1 타입 변환
기본적으로 SpEL은 스프링 코어에서 사용할 수 있는 변환 서비스 (org.springframework.core.convert.ConversionService)를 사용한다. 일반적인 타입에 대해서 내장된 많은 컨버터들과 함께 온 이 컨버전 서비스는 추가할 수 있다. 게다가 제너릭에 친화적이라는 것이 변환서비스의 핵심기능이다. 즉 표현식에서 제너릭 타입을 사용할 때 어떤 객체를 만다더라도 SpEL은 제대로된 타입을 유지하려고 변환을 시도할 것이다.
이는 실사용에서 무엇을 의미하는가? List 프로퍼티를 설정하려고 setValue()를 사용하는 경우를 생각해 보자. 실제 프로퍼티의 타입은 List<Boolean>이다. SpEL은 엘리먼트가 위치하기 전에 리스트의 엘리먼트가 Boolean르오 변환되어야 한다는 것을 인지할 것이다. 다음은 간단한 예제다.
class Simple {
public List<Boolean> booleanList = new ArrayList<Boolean>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
StandardEvaluationContext simpleContext = new StandardEvaluationContext(simple);
// 여기서 false를 문자열로 전달한다. SpEL과 변환 서비스는 Boolean이 되어야 한다는 것을
// 인식하고 변환할 것이다
parser.parseExpression("booleanList[0]").setValue(simpleContext, "false");
// b느 false가 될 것이다
Boolean b = simple.booleanList.get(0);
7.4 빈 정의를 정의하는 표현식
BeanDefinition을 정의하는 XML이나 어노테이션 기반의 설정 메타데이터와 SpEL 표현식을 같이 쓸 수 있다. 두 경우에 모두 표현식을 정의하는 문법은 #{ <표현식 문자열> }의 형식이다.
7.4.1 XML기반의 설정
프로퍼티나 생성자 아규먼트의 값을 다음과 같이 표현식으로 설정할 수 있다.
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!-- 다른 프로퍼티들 -->
</bean>
'systemProperties' 변수는 미리 정의되었으므로 다음과 같이 표현식내에서 사용할 수 있다. 이 컨텍스트에서 미리 정의된 변수앞에 '#'기호를 붙히지 않은 것에 주의해라.
<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
<!-- 다른 프로퍼티들 -->
</bean>
다음 예제처럼 이름으로 다른 빈 프로퍼티를 참조할 수도 있다.
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!-- 다른 프로퍼티들 -->
</bean>
<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
<property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>
<!-- 다른 프로퍼티들 -->
</bean>
7.4.2 어노테이션 기반의 설정
기본값을 지정하기 위해 @Value 어노테이션을 필드나 메서드, 메서드/생성자의 파라미터에 붙힐 수 있다.
다음은 필드 변수의 기본값을 설정하는 예제다.
public static class FieldValueTestBean
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
public void setDefaultLocale(String defaultLocale)
{
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale()
{
return this.defaultLocale;
}
}
다음 예제는 프로퍼티 setter 메서드에 붙혔다는 점만 다르다.
public static class PropertyValueTestBean
private String defaultLocale;
@Value("#{ systemProperties['user.region'] }")
public void setDefaultLocale(String defaultLocale)
{
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale()
{
return this.defaultLocale;
}
}
자동연결된(Autowired) 메서드나 생성자에도 @Value 어노테이션을 사용할 수 있다.
public class SimpleMovieLister {
private MovieFinder movieFinder;
private String defaultLocale;
@Autowired
public void configure(MovieFinder movieFinder,
@Value("#{ systemProperties['user.region'] }"} String defaultLocale) {
this.movieFinder = movieFinder;
this.defaultLocale = defaultLocale;
}
// ...
}
public class MovieRecommender {
private String defaultLocale;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
@Value("#{systemProperties['user.country']}"} String defaultLocale) {
this.customerPreferenceDao = customerPreferenceDao;
this.defaultLocale = defaultLocale;
}
// ...
}
Comments