Spring Expression Language (SpEL)

오늘은 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의 문서를 참고하여 각자가 좀 더 많은 기능을 살펴보는 것을 좋을 듯 싶다.

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

@Profile 과 @ActiveProfiles

오늘은 Spring의 @Profile 어노테이션과 @ActiveProfiles 어노테이션에 대해서 알아보도록 하자. 가끔 헷갈리는 개발자분들이 있으니 다시 한번 짚고 넘어 가면 좋을 듯하다.

@Profile

@Profile 어노테이션은 굉장히 유용한 어노테이션이다. 각 환경에 맞게 Spring의 Bean들을 올릴 수 있어 아주 자주 사용되는 어노테이션이다. 물론 이 어노테이션은 Spring 3.1 부터 생성된 어노테이션이니 그 이하에서는 사용할 수 없다. 물론 xml에서도 사용할 수 있으니 참고하면 되겠다.

어떻게 사용하지는 한번 살펴보자.

public interface HelloService {

  String hello(String name);
}

일단 위와 같은 인터페이스가 있다고 가정하자. 그리고 구현체 두개를 만들어보자.

public class DefaultHelloService implements HelloService {

  @Override
  public String hello(String name) {
    return "hello " + name;
  }
}

public class WorldHelloService implements HelloService {
  @Override
  public String hello(String name) {
    return "hello world " + name + "!";
  }
}

각각 다른 일을 한다고 가정해서 만들었다. DefaultHelloService 클래스 경우에는 “hello ” + name 를 리턴하고 WorldHelloService 경우에는 “hello world ” + name + “!” 위와 같이 리턴한다고 가정하자. 그런데 만약 DefaultHelloService 경우에는 로컬환경에서 개발할 때 사용하고 WorldHelloService 클래스는 개발서버, 운영서버에 사용된다고 해보자.
그럼 매번 개발서버에 배포할 때 DefaultHelloService 클래스를 WorldHelloService 클래스로 변경해서 배포할 수 없는 노릇이다. 만약 그렇게 한다면 실수의 여지도 있고 매번 귀찮은 작업이 된다. 그때 유용한 어노테이션이 바로 @Profile 어노테이션이다. 어떻게 사용하는지 코드로 보자.

@Configuration
public class HelloServiceConfig {

  @Configuration
  @Profile("default")
  static class DefaultHelloConfig {
    @Bean
    HelloService helloService() {
      return new DefaultHelloService();
    }
  }


  @Configuration
  @Profile({"dev", "prod"})
  static class DevHelloConfig {
    @Bean
    HelloService helloService() {
      return new WorldHelloService();
    }
  }
}

필자의 경우에는 중첩클래스를 사용했는데 그러지 않고 메서드 위에 @Profile 어노테이션을 줘도 무관하다.

@Configuration
public class HelloServiceConfig {

  @Bean
  @Profile("default")
  HelloService defaultHelloService() {
    return new DefaultHelloService();
  }
  @Bean
  @Profile({"dev", "prod"})
  HelloService worldHelloService() {
    return new WorldHelloService();
  }
}

하지만 필자의 경우에는 위와 같이 중첩클래스를 이용해서 사용한다. 필자 생각에는 좀 더 보기가 쉽다. 그리고 혹시나 빈명이 달라서 에러가 날 수도 있으니 아예 다른 클래스로 분리하는게 더 좋아 보인다.

위와 같이 @Profile 어노테이션에는 String 배열이 들어간다. 그래서 다수의 환경을 한번에 설정할 수 있다. 또한 반대의 경우도 가능하다. 만약 dev환경이 아닌 경우에만 빈을 설정한다면 다음과 같이 설정하면 된다.

@Bean
@Profile("!dev")
HelloService defaultHelloService() {
  return new DefaultHelloService();
}

이렇게 위와 같이 !dev 로 작성한다면 profile이 dev가 아닐때에만 해당 클래스가 빈으로 등록이 된다.

@ActiveProfiles

ActiveProfiles 어노테이션은 Test 할때 유용한 어노테이션이다. Test 할 경우 profile을 지정할 수 있는데 그 어노테이션이 바로 @ActiveProfiles 이다.

바로 위에서 했던 그 설정 그대로 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("dev")
public class SpringProfilesTests {

  @Autowired
  private HelloService helloService;

  @Test
  public void profilesTest() {
    assertThat(helloService.hello("wonwoo")).isEqualTo("hello world wonwoo!");
  }
}

바로 위에서 했던 dev환경의 빈은 아래와 같은 클래스였다.

public class WorldHelloService implements HelloService {
  @Override
  public String hello(String name) {
    return "hello world " + name + "!";
  }
}

dev환경으로 테스트를 실행하므로 위의 코드는 테스트를 통과해야 한다. 한번 돌려보도록하자. 그럼 우리가 원하던 초록색불이 들어온다.
이와 같이 테스트할때 특정한 환경을 맞추어야 한다면 우리는 @ActiveProfiles 어노테이션을 이용하면 된다.

@ActiveProfiles 어노테이션에는 몇가지 속성이 있는데 resolverinheritProfiles 이다. 나머지는 위에 했던 그 내용이기에 생략한다.
resolver 경우에는 자신에 맞게 커스텀하게 구현할 수 있는 기능이다. Spring의 기본적인 구현체는 DefaultActiveProfilesResolver 클래스이다. 만약 DefaultActiveProfilesResolver 를 사용하지 않고 커스텀하게 사용하고 싶다면 다음과 같이 작성하면 된다.

public class SimpleActiveProfilesResolver implements ActiveProfilesResolver {

  @Override
  public String[] resolve(Class<?> testClass) {
    return new String[] {"test"};
  }
}

ActiveProfilesResolver를 구현하고 @ActiveProfiles 속성 중 resolver에 다음의 클래스를 작성하면 된다.

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles(resolver = SimpleActiveProfilesResolver.class)
public class SpringProfilesTests {
  //...
}

다음은 inheritProfiles 속성이다. 이 속성은 속성명 그대로 profile을 상속할 것인가 상속하지 않을 것인가를 주는 옵션이다. 기본적으로는 true이며 false로 설정할 경우에는 슈퍼클래스의 profile 적용하지 않는다.

@ActiveProfiles("dev")
public class AbstractSpringProfilesTests {
}


@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles(inheritProfiles = false)
public class SpringProfilesTests extends AbstractSpringProfilesTests {
  // ...
}

위와 같이 inheritProfiles를 false로 설정했을 경우 dev로 profile이 설정 되지 않는다. 기존과 동일하게 default로 profile이 설정된다.

@IfProfileValue

보너스로 @IfProfileValue 어노테이션도 살짝 살펴보자. 이 어노테이션은 어떤 환경에서 테스트를 실행할지 하지 않을지 결정하는 어노테이션이다.
예를 들어 다음과 같다.

@RunWith(SpringRunner.class)
@SpringBootTest
@IfProfileValue(name = "java.vendor", value = "Oracle Corporation")
public class SpringIfProfileValueTests {

  @Test
  public void ifTest() {
    assertThat(1).isEqualTo(2);
  }
}

위와 같이 설정해서 사용할 경우에 java vendor가 오라클일 경우에만 테스트를 실행시킨다는 것이다. 만약 여러분의 컴퓨터에 java vendor가 오라클이라면 이 테스트를 실패로 돌아갈 것이다.

또하나의 예제는 환경정보를 외부에서 받아와서 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
@IfProfileValue(name="test-groups", values={"unit", "integration"})
public class SpringIfProfileValueTests {

  @Test
  public void ifTest() {
    assertThat(1).isEqualTo(2);
  }
}

위와 같이 설정해서 IDEA로 돌릴 경우 테스트는 실행 되지 않는다. 왜냐하면 test-groups란 이름의 환경변수가 없기 때문이다. 메이븐으로 테스트를 실행시킨다면 다음과 같이 작성하면 된다.

mvn -Dtest-groups=unit test

그러면 test-groups이 unit이기 때문에 위의 테스트는 동작을 해여 실패로 돌아간다. 만약 다음과 같이 작성한다면 테스트를 실행하지 않고 통과가 된다.

mvn -Dtest-groups=unit1 test

test-groups에 맞는 value가 없기 때문에 위의 테스트는 실행시키지 않느다. values들은 OR로 동작한다. 작성된 values 중 하나만 동일해도 테스트가 동작을 한다.

오늘은 이렇게 @Profile@ActiveProfiles 그리고 보너스로 @IfProfileValue에 대해서 알아봤다. 조금 헷갈려하는 개발자들을 위해서 작성하였다. 해당 소스는 여기에 올라가 있으니 한번씩 이것저것 만져보도록 하자.

spring web 비동기

오랜만에 포스팅을 한다. 새해도 거의 보름이 지나가는데 요즘은 포스팅이 뜸했다. 다시 블로그를 열심히 해야 겠다. 물론 될지는 모르겠지만.. 어쨋든 오늘은 Spring에서 지원해주는 web 비동기 기술을 몇가지 살펴보도록 하자.

오늘은 이런 것들이 있다는 것만 알고 넘어가자. 추후에 좀 더 상세하게 살펴볼 수 있으면 그때 살펴보도록 하자. 너무 처음부터 깊게 파고 들면 어려우니.. 이 기술은 요즘 나오는 reactive streams 과 많이 비슷하므로 이 기술 먼저 알고 가면 좋을 듯하다.

실제 이 기술(오늘 말할려고 하는)은 최신 기술이 아니다. 정확히 말하면 대략 5년전 그러니까 spring3.2가 발표되면서 이 기술을 선보였다. 뭐 그건 그렇고 한번 살펴보도록 하자.

Callable

이것은 java 1.5부터 가능한 Callable 인터페이스다. Callable 인터페이스는 추상 메서드가 하나 있는 FunctionalInterface 이다. 해당 타입을 리턴하면 Spring에서 적절하게 리턴값을 만들어 준다.

@GetMapping("/callable")
public Callable<List<Person>> persons(){
  return () -> {
    return generatorPersons.getPersons();
  };
}

이것의 단점은 아무 설정을 할 수 없다. 타임아웃이라던지 어떤 스레드풀을 사용할지 결정할 수 없다.

CompletableFuture

이것은 java 1.8부터 가능한 CompletableFuture 클래스이다. 해당 타입도 마찬가지로 mvc에서 리턴 타입으로 정의하면 Spring에서 적절하게 리턴해준다.

@GetMapping("/future")
public CompletableFuture<List<Person>> future() {
  return CompletableFuture.supplyAsync(() -> generatorPersons.getPersons());
}

supplyAsync 경우에는 Supplier를 파라미터로 받고 있다. 그래서 위와 같이 적절하게 람다를 사용해서 좀 더 나은 코드를 만들 수 있다. CompletableFuture.supplyAsync 경우에는 Executor를 추가로 파라미터로 받고 있다. 그래서 좀 더 나은 설정을 할 수 있다.


@GetMapping("/future") public CompletableFuture<List<Person>> future() { return CompletableFuture.supplyAsync(() -> generatorPersons.getPersons(), this.asyncExecutor); }

ListenableFuture

ListenableFuture는 spring 4.0에 추가된 인터페이스이다. 4.0 이전 버전에서는 사용할 수 없으니 참고하면 되겠다. 실제로 AsyncRestTemplate의 리턴타입은 ListenableFuture로 정의 되어있다. 비동기 적으로 특정한 API를 호출할 때 유용한 AsyncRestTemplate은 리턴 타입 그대로 spring mvc에게 넘겨줘도 된다.

@GetMapping("/async")
public ListenableFuture<ResponseEntity<List<Person>>> persons() {
  return asyncRestTemplate.exchange("http://localhost:8081/persons",
      HttpMethod.GET,
      null,
      new ParameterizedTypeReference<List<Person>>() {});
}

API 통신후에 적절히 사용한다면 매우 유용한 인터페이스이다. 다음 시간이나 혹은 추후에 좀 더 자세히 이야기 해보도록 하고 일단 이정도만 알고 있자.

WebAsyncTask

WebAsyncTask 은 Spring에서 제공해주는 클래스이다. spring 3.2 부터 제공되고 있으니 참고하면 되겠다. 이 클래스는 Callable 인터페어스보다 좀 더 나은 설정을 갖고 있다. 실제로 WebAsyncTask 클래스 안에는 Callable을 사용하고 있으며 타임아웃 설정, executor 등을 설정 할 수 있다.

@GetMapping("/webAsyncTask")
public WebAsyncTask<List<Person>> webAsyncTaskPerson() {
  return new WebAsyncTask<>(() -> generatorPersons.getPersons());
}

위는 기본적인 사용법이다.

@GetMapping("/webAsyncTask")
public WebAsyncTask<List<Person>> webAsyncTaskPerson1() {
  return new WebAsyncTask<>(1000L, this.executor, () -> generatorPersons.getPersons());
}

타임아웃 설정과 Executor를 설정 할 수 있다. Callable 보다 설정할게 좀 더 있으니 Callable 보다는 WebAsyncTask를 사용하길 권장한다.

DeferredResult

DeferredResult 클래스 역시 spring 3.2부터 사용가능 하다. 이건 조금 특이하다. 이건 전혀 다른 스레드에서도 사용가능하다.(물론 위의 것도 전혀 다른 스레드이긴하지만..) 아무튼 설명하기 좀 힘드니 소스부터 보자..

private final Queue<DeferredResult<List<Person>>> personsQueue = new ConcurrentLinkedQueue<>();

@GetMapping("/deferred")
public DeferredResult<List<Person>> persons() {
  DeferredResult<List<Person>> result = new DeferredResult<>();
  personsQueue.add(result);
  return result;
}

@Scheduled(fixedRate = 2000)
public void processQueues() {
  for (DeferredResult<List<Person>> result : this.personsQueue) {
    result.setResult(generatorPersons.getPersons());
    this.personsQueue.remove(result);
  }
}

음 먼저 DeferredResult를 생성해서 큐에 담고 바로 리턴하면 된다. 그리고 나서 전혀 다른스레드에서 setResult를 호출하면 그때 뷰에 전달된다. 아주 특이한 아이이다. 적절하게 잘 쓰면 좋은 클래스인듯 하다.

일단 오늘은 어떤 클래스 혹은 인터페이스들이 Spring mvc가 지원해주는지 알아봤다. 이외에도 몇가지 더 있긴한데 그것까지는 알아보지 않았다. 나중에는 이것저것 테스트도 해보고 어떤 의미를 갖고 있는지도 한번 살펴보도록 하자.

오늘은 이만!