JdbcTemplate으로 조회 로직을 짜다 보면 이런 코드를 자주 작성하게 된다.
private RowMapper<Item> itemRowMapper() {
return (rs, rowNum) -> {
Item item = new Item();
item.setId(rs.getLong("id");
item.setItemName(rs.getString("item_name");
item.setPrice(rs.getInt("price");
item.setQuantity(rs.getInt("quantity");
return item;
};
}
여기서 문득 의문이 생긴다. JdbcTemplate은 어떻게 저 람다식을 보고 "아, 이게 RowMapper 구나"라고 인식하는 걸까? 그리고 rs, rowNum 같은 변수명은 반드시 이렇게 지어야 하는걸까?
결론부터 말하면, RowMapper가 함수형 인터페이스(@FunctionalInterface)이기 때문이다.
RowMapper 인터페이스를 뜯어보면 메서드가 딱 하나뿐이다.
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
자바 8+에서는 구현해야 할 메서드가 하나뿐인 인터페이스를 람다식으로 대체할 수 있다.
여기서 중요한 포인트는, JdbcTemplate이 람다식을 직접 해석하는 것이 아니라는 점이다. 우리가 람다를 넘기면 자바 컴파일러가 이미 컴파일 시점에 이를 RowMapper 인터페이스를 구현한 객체로 변환해서 전달한다. 런타임에는 람다가 아니라 일반적인 RowMapper 구현체로 동작하는 셈이다.
JdbcTemplate은 쿼리 실행 후 ResultSet을 순회하면서 각 행마다 mapRow()를 호출하고, 우리가 람다로 작성한 로직이 그때 실행된다. 즉, 껍데기(인터페이스)는 RowMapper로 고정하고, 알맹이(mapRow 구현)는 람다로 채우는 것이다.
참고로, 람다 안에서는 throws SQLException을 명시적으로 쓰지 않아도 된다. 자바가 암묵적으로 처리해준다.
람다식에서 쓰는 (rs, rowNum)이라는 이름은 사실 아무렇게나 지어도 상관없다.
자바 컴파일러와 스프링은 변수의 이름이 아니라 함수형 인터페이스의 메서드 시그니처(타입, 순서, 개수)를 기준으로 판단하기 때문이다.
// 변수명을 마음대로 바꿔도 똑같이 동작한다.
return (result, n) -> {
Item item = new Item();
item.setId(result.getLong("id");
...
return item;
};
JdbcTemplate은 쿼리를 실행하고 한 줄씩 읽을 때마다 값을 던져주는데, 이때 첫 번째 파라미터에는 ResultSet 객체, 두 번째 파라미터에는 int(현재 행 번호)가 들어가도록 이미 약속되어 있다.
이름을 a, b로 짓든, r, i라고 짓든 이 타입과 순서만 맞으면 동일하게 동작한다.
rs, rowNum이라고 쓸까?이름이 상관없음에도 대부분의 예제에서 rs와 rowNum을 사용하는 이유는 단순하다. 바로 관례(Convention) 때문이다.
rs → ResultSet의 약자rowNum → row number(현재 행 번호)다른 개발자나 미래의 내가 코드를 봤을 때 별도의 고민 없이 "아, DB 결과구나"라고 바로 이해할 수 있다.
JdbcTemplate의 람다식은 별다른 마법이 아니라, 함수형 인터페이스의 명세(시그니처)를 따르는 자바 문법일 뿐이다. 변수명은 자유롭게 바꿀 수 있지만, 협업과 가독성을 생각하면 rs, rowNum 같은 관례를 따르는 편이 훨씬 낫다.
결국 핵심은 이름이 아니라 "각 행의 데이터(ResultSet + 행번호)를 받아 객체로 변환한다"는 흐름과 약속을 이해하는 것이다.