오늘은 Spring에서 제공해주는 Expression Language에 대해서 살펴보도록 하자. Spring Expression Language은 런타임시에 객체 그래프를 조회하고 조작하는 표현언어로 매우 강력하다. 이 표현언어는 아주 많은 기능을 지원하는데 문서를 참고하면 되겠다.

어떻게 사용하는지 한번 보고 어디에 유용하게 사용할 것인가를 살펴보도록 하자.
처음에는 문서에도 있다시피 아주 간단한 표현식을 살펴보자.

ExpressionParser

@Test
public void simple() {
  ExpressionParser parser = new SpelExpressionParser();
  Expression exp = parser.parseExpression("'Hello World'");
  String value = (String) exp.getValue();
  assertThat(value).isEqualTo("Hello World");
}

SpelExpressionParser 클래스를 사용해서 해당하는 표현식을 파싱할 수 있다. 아주 간단하다. 물론 운영에서는 저렇게 사용하라는 것은 아니다. 예제이니 한번씩만 살펴보자.

@Test
public void stringLength() {
  ExpressionParser parser = new SpelExpressionParser();
  Expression exp = parser.parseExpression("'Hello World'.length()");
  Integer length = exp.getValue(Integer.class);
  assertThat(length).isEqualTo(11);
}

위와 같이 String의 메서드를 호출할 수 도 있다. 뭐 별거 아닐 수도 있지만 어떻게 보면 조금 신기하다. 어쨋든 getValue() 메서드에는 아주 다양한 메서드가 존재한다. 첫 번째코드에서 봤던 getValue() 메서드는 아주 파라미터가 없는 메서드이다. 그래서 해당하는 타입에 맞게 형변환을 해주어야 한다. 이 보다는 두번째 코드에는 파라미터로 Class 타입을 받아 좀 더 깔끔하고 안전한 코드가 되었다. 필자의 경우에는 특별한 경우가 아니라면 두번째 코드를 사용하는 편이다.

EvaluationContext

위의 ExpressionParser는 아주 간단하게 사용법만 알아봤다. 아마도 저렇게는 쓸일이 거의 없을 듯하다. 좀 더 유용하게 사용하려면 EvaluationContext를 이용해서 코드를 작성해야 한다. 일단 코드로 보자.

class Foo {
  private String name;

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

  public String getName() {
    return name;
  }

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

@Test
public void context() {
  ExpressionParser parser = new SpelExpressionParser();
  Expression exp = parser.parseExpression("name.length() < 10");
  EvaluationContext context = new StandardEvaluationContext(new Foo("wonwoo"));
  Boolean result = exp.getValue(context, Boolean.class);
  assertThat(result).isTrue();
}

우의 코드는 Foo 클래의 속성중 name을 가져와 그 길이가 10보다 작으면 true를 던지고 그렇지 않으면 false를 던지는 그런 코드를 작성하였다. 위와 같이 어떠한 조건의 결과 값도 Boolean 형태의 값으로 리턴받을 수 있다. 이러한 코드는 어디서 많이 봤다. Spring에서 제공해주는 Cache를 사용해봤다면 아주 익숙한 코드이다.

@Cacheable(value = "test", key = "#id", condition = "#id.length() < 10")
public String fooBar(String id) {
  // ...
}

이것 또한 Spring의 Expression Language를 사용하여 Cacheable의 속성들을 파싱한다. 조금 유용하게 사용할 수 있을 것만 같다. 하지만 실체는..

위와 같이 EvaluationContext 를 사용해도 되지만 위와 같이 간단한 코드라면 EvaluationContext를 사용하지 않고 바로 getValue에 Object를 넣을 수 있다. 다음과 같이 말이다.

@Test
public void root() {
  ExpressionParser parser = new SpelExpressionParser();
  Expression exp = parser.parseExpression("name.length() < 10");
  Boolean result = exp.getValue(new Foo("wonwoo"), Boolean.class);
  assertThat(result).isTrue();
}

class Foo {
// 
} 

Array

Spring의 Expression Language은 배열도 접근 가능하게 해준다. 예제로 한번 살펴보도록 하자.

class Foo {
  public List<String> names = new ArrayList<>();


  @Override
  public String toString() {
    return "Foo{" +
        "names=" + names +
        '}';
  }
}

@Test
public void array() {
  ExpressionParser parser = new SpelExpressionParser();
  Foo foo = new Foo();
  foo.names = Arrays.asList("wonwoo", "kevin");
  StandardEvaluationContext context = new StandardEvaluationContext(foo);
  Expression expression = parser.parseExpression("names[0]");
  String value = expression.getValue(context, String.class);
  assertThat(value).isEqualTo("wonwoo");
}

우리가 배열의 원소를 가져올때 처럼 마찬가지로 [i]를 이용해서 동일하게 가져오면 된다. 보기엔 그렇게 어렵지 않다. 뿐만 아니라 배열의 속성도 변경가능하다. 물론 배열만 되는 것은 아니고 아까 봤던 예제도 마찬가지로 조작가능하다.

@Test
public void arrayValue() {
  ExpressionParser parser = new SpelExpressionParser();
  Foo foo = new Foo();
  foo.names = Arrays.asList("wonwoo", "kevin");
  StandardEvaluationContext context = new StandardEvaluationContext(foo);
  parser.parseExpression("names[0]").setValue(context, "test");
  assertThat(foo.names.get(0)).isEqualTo("test");
}

첫 번째 배열인 wonwootest로 변경하는 그런 코드이다. 그리 어려운 코드가 아니기에 각자 이것저것 한번씩 해보면 좋을 듯 싶다.

Message

필자가 그냥 지은 제목이다. 어떠한 message format을 지정한뒤 그에 맞게 값을 넣어주면 원하는 값을 받을 수 있는 그런 기능이다. 이게 가장 유용하게 쓰일듯 싶다. 물론 필자 생각이다. 예제를 보자.

@Test
public void message() {
  String message = "my foo is #{name}, i bar #{age}";
  SpelExpressionParser parser = new SpelExpressionParser();
  Foo foo = new Foo("wonwoo", 33);
  StandardEvaluationContext context = new StandardEvaluationContext(foo);
  Expression expression = parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION);
  String value = expression.getValue(context, String.class);
  assertThat(value).isEqualTo("my foo is wonwoo, i bar 33");
}

message 포맷은 "my foo is #{name}, i bar #{age}"라는 String 문자열을 파싱하는 코드이다. Foo라는 클래스에 이름과 나이를 넣어주면 자동으로 프로퍼티에 맞게 메시지를 만들어 준다. 여기서 중요한건 ParserContext.TEMPLATE_EXPRESSION 라는 인스턴스이다. Spring에서 이미 만들어 놓은 기본 템플릿이다. 이 코드는 다음과 같다.

public static final ParserContext TEMPLATE_EXPRESSION = new ParserContext() {

  @Override
  public String getExpressionPrefix() {
    return "#{";
  }

  @Override
  public String getExpressionSuffix() {
    return "}";
  }

  @Override
  public boolean isTemplate() {
    return true;
  }

};

prefix는 #{ 로 시작하고 suffix는 }로 끝나는 템플릿을 자동으로 파싱해준다. 만약 위와 같이 #{이 아니라 ${ 로 시작하고 싶다면 다음과 같이 만들어서 사용하면 된다.

@Test
public void templateMessage() {
  String message = "my foo is ${name}, i bar ${age}";
  SpelExpressionParser parser = new SpelExpressionParser();
  Foo foo = new Foo("wonwoo", 33);
  StandardEvaluationContext context = new StandardEvaluationContext(foo);
  Expression expression = parser.parseExpression(message, new ParserContext() {
    @Override
    public boolean isTemplate() {
      return true;
    }

    @Override
    public String getExpressionPrefix() {
      return "${";
    }

    @Override
    public String getExpressionSuffix() {
      return "}";
    }
  });
  String value = expression.getValue(context, String.class);
  assertThat(value).isEqualTo("my foo is wonwoo, i bar 33");
}

따로 클래스를 만드는 것이 더 좋아보이지만 여기서는 예제이므로 익명클래스를 만들어 사용했다.
만약 객체말고 Map으로 하고 싶다면 어떻게 할까? 이것 또한 간단하다. StandardEvaluationContext 클래스에는 property를 무엇으로 접근할지 셋팅하는 부분이 있다. 이걸 이용해서 객체가아닌 Map으로 사용할 수 있다. 예제를 보자.

@Test
public void message() {
  String message = "my foo is #{name}, i bar #{age}";
  SpelExpressionParser parser = new SpelExpressionParser();
  Map<String,String> map = new HashMap<>();
  map.put("name", "wonwoo");
  map.put("age", "33");
  StandardEvaluationContext context = new StandardEvaluationContext(map);
  context.addPropertyAccessor(new MapAccessor());
  Expression expression = parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION);
  String value = expression.getValue(context, String.class);
  assertThat(value).isEqualTo("my foo is wonwoo, i bar 33");
}

Spring에서 이미 만들어 놓은 MapAccessor 클래스를 사용하면 된다. 그럼 객체가 아닌 Map으로 해당 메세지를 파싱할 수 있다. 아마 기본은 ReflectivePropertyAccessor를 사용하고 있는 것으로 보인다.

이렇게 오늘은 유용하면 유용하지만 잘 사용하지 않는다면 잘 모르는 Spring의 Expression Language를 살펴봤다. Spring의 Expression Language는 더욱 많은 기능을 제공해준다. 하지만 필자는 기본적인 예제와 자주 사용할만한 것으로 살펴봤다. 좀 더 관심이 있는 개발자라면 Spring의 문서를 참고하여 각자가 좀 더 많은 기능을 살펴보는 것을 좋을 듯 싶다.

오늘 이 코드들은 여기에 있으니 관심있다면 한번씩 돌려보거나 살펴보는 것도 나쁘지 않다.