Spring JdbcTemplate

오늘은 Spring 초창기부터 있었던 JdbcTemplate대해서 간단히 알아보도록 하자.
JdbcTemplate은 엄청나게 많은 메서드들을 가지고 있다. 하나씩 다 살펴볼 수는 없지만 주로 사용하는 것들 위주로 살펴보도록 하자.

필자는 대부분 Data Access를 할 경우에는 JPA를 이용하고 어쩌다 mybatisJdbcTemplate을 이용한다. 그래도 굳이 따지자면 JdbcTemplate을 더 많이 사용하고 있다. 그래도 자주 사용하지 않다보니 잘 기억이 안난다. 사용할 때만 구글링을 해서 찾아봐서 자주 사용하는 것들을 포스팅을 해보자.

execute

execute 메서드는 매우 간단하다. 주로 DDL을 실행 시킬때 사용한다. 기본적은 메서드는 딱히 리턴타입도 없다.

void execute(String sql) throws DataAccessException;

물론 이외에도 몇가지 메서드가 있는데 자주 사용하지 않을 듯하다.
한번 사용해보자.

@Service
public class DdlJdbc {

  private final JdbcTemplate jdbcTemplate;

  public DdlJdbc(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  public void execute (String sql) {
    this.jdbcTemplate.execute(sql);
  }
}

ddlJdbc.execute("drop table persons if exists");
ddlJdbc.execute("create table persons("id serial, name varchar(255))");

딱히 어려운부분은 없다. 기본적은 데이터베이스의 DDL 문법이다. 솔직히 이것도 그렇게 많이 쓰이지 않을 듯 싶다.

update

이번엔 update 메서드이다. Read를 제외한 나머지 insert, update, delete 는 모두 이 메서드를 사용하면 된다. 필자는 맨처음엔 update 메서드밖에 없는 줄 알았다.

주로 사용하는 메서드는 아래와 같다.

int update(String sql, @Nullable Object... args) throws DataAccessException;

sql과 그에 따른 파라미터들이다. 물론 이것도 보다 많은 메서드들이 있으니 참고 하면 되겠다.

@Service
public class UpdateJdbc {

  private final JdbcTemplate jdbcTemplate;

  public UpdateJdbc(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  public int insert(String name) {
    return jdbcTemplate.update("insert into persons (name) values (?)", name);
  }
}

위와 같이 insert문 과 그에 따른 name을 파라미터로 넘기면 끝난다.

updateJdbc.insert("wonwoo");
updateJdbc.insert("kevin");

아주 간단하다. 또한 batchUpdate 또한 가능하다. 주로 사용될 만한 메서드는 아래와 같다.

int[] batchUpdate(String sql, List<Object[]> batchArgs) throws DataAccessException;

여러개를 넣기 때문에 파라미터가 List<Object[]> 형태로 되어있다.

public void batchUpdate(List<String> name) {
  List<Object[]> ts = name.stream().map(i -> new Object[]{i}).collect(Collectors.toList());
  this.jdbcTemplate.batchUpdate("insert into persons (name) values (?)", ts);
}

updateJdbc.batchUpdate(Arrays.asList("hello", "world"));

아주 간단하다. 각자가 원하는 메서드를 사용하면 되겠다. 여기까지는 너무 쉽다. 뭐든 Read가 문제다. read 메서드들은 너무 많다. 한번 살펴보도록 하자.

queryForObject

queryForObject는 하나의 도메인 객체를 리턴받거나 하나의 컬럼을 리턴 받을 때 사용한다. 필자가 조금 헷갈린게 있는데 마치 도메인으로 바로 바꿔줄 것 같지만 그렇지 않다.

<T> T queryForObject(String sql, Class<T> requiredType, @Nullable Object... args) throws DataAccessException;

<T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException;

자주 사용될만한 메서드는 위와 같다. 한번 사용해보자.

@Service
public class SelectJdbc {

  private final JdbcTemplate jdbcTemplate;

  public SelectJdbc(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  public Long findId(Long id) {
    return this.jdbcTemplate.queryForObject("SELECT id FROM persons where id = ?", Long.class, id);
  }

  public Person findOne(Long id) {
    return this.jdbcTemplate.queryForObject("SELECT id, name FROM persons where id = ?",
        (rs, rowNum) -> new Person(rs.getLong("id"), rs.getString("name")),
        id);
  }
}

위와 같이 하나의 컬럼을 갖고 오거나 하나의 도메인으로 가지고 올때 유용하다. 하지만 도메인으로 변경을 할때는 위와 같이 콜백인터페이스를 사용해서 변환해야 한다.

this.jdbcTemplate.queryForObject("SELECT id FROM persons where id = ?", Person.class, id);

위 같이는 작동하지 않는다.

또한 예전에는 queryForLong, queryForInt 메서드가 존재했으니 최신 버전에는 삭제되었다. 보통 카운트를 가져올떄 자주 이용했으나 이제는 queryForObject 메서드를 이용해서 가져와야 한다.

public int count() {
  return this.jdbcTemplate.queryForObject("SELECT count(id) FROM persons", int.class);
}

Spring 4.2부터 삭제된 것으로 보인다. 왜냐하면 4.2 이전 문서에만 queryForInt 가 있고 4.2 버전에서는 문서에서 삭제 되었으니 아마 그때 삭제 되었을 것이라 판단된다.

queryForList, queryForMap

queryForList, queryForMap 메서드 또한 queryForObject 과 동일하다. 단지 List냐 Map냐의 차이 일뿐이지 하는 행위는 동일하다.

단지 컬럼 하나를 리스트로 가져오거나 컬럼 하나를 Map으로 가져오는 메서드이다. 이 중에서도 그렇게 많이 쓰일 것 같지는 않다.

자주 사용될 만한 메서드는 아래와 같다.

<T> List<T> queryForList(String sql, Class<T> elementType) throws DataAccessException;
<T> List<T> queryForList(String sql, Object[] args, Class<T> elementType) throws DataAccessException;

Map<String, Object> queryForMap(String sql, @Nullable Object... args) throws DataAccessException;

한번 사용해보도록 하자.

  public List<Long> findAllId() {
    return this.jdbcTemplate.queryForList("SELECT id FROM persons", Long.class);
  }

  public Map<String, Object> findAllMap(Long id) {
    return this.jdbcTemplate.queryForMap("SELECT id FROM persons where id = ?", id);
  }

여기서 주의할점은 queryForList에도 마치 도메인으로 바꿀 수 있을 것처럼 보이지만 그럴수 없다. queryForObject처럼 콜백으로 받을 수 있는 파라미터가 없다. 만약 그러고 싶다면 아래 설명할 query 메서드를 이용해야 한다. 굳이 없어도 될 것 같다.

query

아마 가장 많은 메서드를 갖고 있고 위의 설명한 것을 제외하면 모두 이 메서드를 이용하면 된다. 이것 역시 메서드들이 너무 많으니 이 역시 자주 이용될 만한 것들만 살펴보자.

자주 사용될만한 메서드들로는 아래와 같다.

<T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException;
<T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException;

RowMapper<T> 인터페이스를 해당 도메인에 맞게 구현하면 된다.

  public List<Person> findAll() {
    return this.jdbcTemplate.query("SELECT id, name FROM persons",
        (rs, rowNum) -> new Person(rs.getLong("id"), rs.getString("name")));
  }

딱히 어려운부분은 없는 것같다. 그리고 다양한 메서드들이 있으니 참고하면 되겠다. 필자의 경우에는 특별한 경우가 아니라면 그 외의 메서드들은 사용한적이 없다. 하지만 또 언젠가는 사용할 일이 있을테니 있다는 것만 알자.

참고로 설명한 query* 메서드들은 모두 (…) 가변인자로 설명했으니 Object[]로 받는 파라미터들도 존재한다.

예를들어 다음과 같다.

<T> T queryForObject(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException;

아마도 1.5 이전에는 가변인지가 없어서 위와 같이 사용했을 것으로 판단된다.

오늘은 이렇게 Spring에서 제공해주는 JdbcTemplate 에 대해서 알아봤다. 엄청 많은 메서드들이 존재하니 사용할 일이 있다면 문서를 좀 더 참고하면 되겠다.

그럼 오늘은 이만!

spring boot 2.0 actuator RC2

오늘은 저번에 알아봤던 spring boot 2.0 actuator의 내용이 조금 변경된 내용이나 추가할 내용이 있어 다시 포스팅을 작성한다. 마일스톤 버전으로 알아봤더니 몇가지 내용이 바뀌었다. 현재는 RC2 버전이니 이제는 바뀌지 않을테야.. 내일모레가 2.0인데 바뀌면…

prefix

마일스톤 버전에서는 http endpoint prefix가 기본적으로 /application이였지만 현재는 /actuator로 변경되었다. 하지만 기본적인 prefix이므로 언제든지 변경 가능하다.

management.endpoints.web.base-path=/application

위와 같이 작성했을 경우 다음과 같이 요청을 할 수 있다.

http http://localhost:8080/application

enabled

enabled 프로퍼티도 변경 되었다. 기존 마일스톤 버전에서는 endpoints.{id}.web.enabled=true 같이 사용했으나 이제는 management.endpoint.{id}.enabled=true 로 변경 되었다. endpoints.으로 시작하는 프로퍼티들은 모두 사용되지 않는다.
2.0에서 다음과 같이 사용해도 적용되지 않는다.

endpoints.env.enabled=false

다음과 같이 작성해야 Spring boot 가 적용 시킨다.

management.endpoint.env.enabled=false

web 과 관련된 endpoint들은 몇가지를 제외하고 모두 disabled되어 있다. info, health를 제외하고는 모두 disabled 처리 되어있다. 아마 각 정보들은 외부의 노출 시킬 필요가 없으니 disabled 처리 되어진 것으로 보인다. 만약 각 다른 정보들을 enabled 시키고 싶다면 다음과 같이 처리하면 안된다.

management.endpoint.env.enabled=true

만약 위와 같이 처리한다해도 Spring boot actuator는 env endpoint를 활성화 하지 않는다. 정말로 활성화하고 싶다면 다음과 같이 처리해야 한다.

management.endpoints.web.exposure.include=health,info,env,metrics

위와 같이 management.endpoints.web.exposure.include 를 사용해서 활성화하고 싶은 endpoint들의 id를 작성해주면 된다. include는 포함하는 것이지만 management.endpoints.web.exposure.exclude를 사용해서 제외시킬 수 도 있다.

그래서 만약 다음과 같이 처리한다면 해당 endpoint를 제외 시킬 수 있다.

management.endpoints.web.exposure.include=health,info,env,metrics
management.endpoints.web.exposure.exclude=health

위와 같이 작성한다면 info,env,metrics 관련 endpoint만 활성화가 된다. 또한 모든 endpoint들을 활성화 하고 싶다면 굳이 모든 id를 작성할 필요 없이 다음같이 작성하면 모든 enpoint들이 활성화가 된다.

management.endpoints.web.exposure.include=*

참고로 jmx는 모두 활성화 되어 있다. 기본값이 다음과 같다.

management.endpoints.jmx.exposure.include=*

path-mapping

기존의 2.0 이전에는 endpoint들의 path를 다음과 같이 변경하였다.

endpoints.info.path=/foo

spring boot 2.0 부터는 조금 더 길어졌다. management.endpoints.web.path-mapping.{id}=/bar 만약 info의 endpoint url을 변경하고 싶다면 다음과 같이 작성해야 한다.

management.endpoints.web.path-mapping.info=/bar

어노테이션

기존 포스팅에서 설명한 @WebEndpointExtension 어노테이션은 사라졌고 @EndpointWebExtension 으로 변경 되었다.

그리고 저번에 언급했던 @ConditionalOnEnabledEndpoint, @ReadOperation, @WriteOperation, @DeleteOperation 들은 동일하게 존재하고 하는일도 기존의 설명했던 내용과 동일하다.

어노테이션 @WebEndpoint, @JmxEndpoint이 새로 추가 되었다. @WebEndpoint 어노테이션을 사용할 경우에는 web에서만 노출이 되고 @JmxEndpoint 어노테이션을 사용할 경우에는 jmx에만 노출이 된다.

@WebEndpoint(id = "hello")
@Component
public class HelloWebEndpoint {

  @ReadOperation
  public String hello() {
    return "hello world";
  }
}

위와 같이 작성하면 web에서만 hello endpoint가 노출 된다.

@JmxEndpoint(id = "hello")
@Component
public class HelloJmxEndpoint {

  @ReadOperation
  public String hello() {
    return "hello world";
  }
}

만약 위와 같이 작성한다면 web에는 노출되지 않고 jmx에만 노출 된다. 만약 jmx web 모두 노출 시키고 싶다면 다음과 같이 작성하면 된다.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello() {
    return "hello world";
  }
}

@Endpoint를 이용해 코드를 작성하면 web, jmx 모두 노출 된다.

micrometer

spring boot 2.0 actuator 에서 micrometer Metrics 라이브러리를 지원해준다. 필자도 spring boot 2.0을 보다가 알게 되어서 아직 micrometer에 대해서 모른다. 나중에 필자가 관심이 가게 된다면 살펴보도록 하고 일단 spring boot 2.0에서 지원을 해준다고만 알자.

오늘은 이렇게 spring boot 2.0 actuator 의 변경된 사항을 알아봤다. 여러모로 spring boot 2.0 actuator는 기존 버전의 비해 많이 변경되었다. 우리가 사용할 때는 변경된 부분이 없어 보이지만 실제 개발한 소스 코드는 엄청나게 변경이 많이 된 듯 싶다.

곧 릴리즈 될 2.0을 기대하면서 오늘 포스팅은 여기서 마치겠다.

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

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