ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Expression Language (SpEL)
    카테고리 없음 2023. 4. 23. 14:05
    오늘은 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의 문서를 참고하여 각자가 좀 더 많은 기능을 살펴보는 것을 좋을 듯 싶다. 오늘 이 코드들은 여기에 있으니 관심있다면 한번씩 돌려보거나 살펴보는 것도 나쁘지 않다.

    댓글

Designed by Tistory.