JdbcTemplate을 직접 구현하는 미션 덕에 이것저것 공부하던 도중, 다른 크루의 미션 제출 현황을 보고 궁금한 게 생겼다.
우선 옵셔널이 무엇인지는 조이썬의 테코톡을 한 번 보고 오자 🤙🏻, Optional이 등장하게 된 배경과 주의할 점을 잘 설명해 주었다! 희선이 최고 🙌🏻
📦 Optional
Java 8과 함께 등장한 Optional. 값을 감싸는 Wrapper class의 역할을 한다. 추가적인 클래스이므로 orElse()
, orElseGet()
와 같은 편의 메서드 또한 제공한다. 존재 여부를 null
로 처리하지 않고, 한 번 더 추상화해 코드를 사용하는 사람에게 “정말 없어”라는 점을 환기한다.
그렇다고 모든 null
을 Optional
로 치환해도 되는가? 라고 한다면.. 나는 괜찮을 것이라고 생각하지만 이에 대한 논쟁은 끊이지 않고 있다. Optional
을 써서 발생하는 가독성을 우려하는 목소리도 크다. 단순하게 null
검증으로 끝날 수 있는 것을 굳이 Optional
로 감싸야 해? 와 같은 이야기다. 실제 성능이 많이 뒤쳐진다는 벤치마킹 결과도 있다. 아래가 결과인데, 자세한 코드는 링크를 참고하자.
🤔 JdbcTemplate은 왜 null
을 반환할까?
Spring Jdbc Core의 JdbcTemplate
은 여러 메서드를 통해 SQL 쿼리를 실행하고, 그 결과를 반환한다. 다만 우리가 흔히 알고 있는 Spring JPA의 JPA Repository
들과는 다르게, Optional
을 찾아볼 수 없었다.
아니 Optional 줘도 될 것 같은데 왜 안 하지? 라는 생각이 들어서 좀 찾아봤다. 유서깊은 토론이 이미 오고 간 흔적이 있었다. 2015년 1월에 열린 두 이슈 #724, #17262에 아래와 같은 내용이 적혀 있었다.
문제 상황 1: 값이 존재하지 않음을 확인하려면 JdbcTemplate.queryForObject
에서 EmptyResultDataAccessException
을 잡아야 한다. 이는 리턴 값이 DataAccessUtils.nullableSingleResult
함수를 호출하기 때문이며, 함수의 문서에도 명시돼 있다.
문제 상황 2: 다른 방법으로 query
, queryForList
로 결과를 받아 리스트의 크기를 측정해야 한다. 이 또한 매우 번거롭다.
제안: 아래와 같은 규칙을 따르는 queryForOptional
메서드를 새로 만들 것을 제안했다.
- 결과 없음 →
Optional.Empty()
- 결과 하나 →
Optional.ofNullable(result)
- NULL →
Optional.empty()
- 결과 2개 이상 →
IncorrectResultSizeDataAccessException
암만 봐도 괜찮아 보이는데… 왜 도입되지 않았을까? 스프링의 입장은 1) ‘Template이 할 일이 아니다’와 , 2) ‘많은 오버로드가 누적돼 변형에 취약하다’는 점이다.. 실제 코드를 들여다보면 서로 다른 메서드를 불러 리턴하는 경우가 너무 많다보니, 특정 메서드 하나를 Optional
로 만들기가 까다로워진 것이다. 또한 마무리로 아래와 같은 말을 덧붙였는데,
The same can be achieved with an independent interface/class that delegates to a JdbcTemplate internally.
JdbcTemplate을 활용하는 다른 클래스 / 인터페이스를 두면 (추상화) Optional을 충분히 반환할 수 있다는 말이다. 라이브러리를 사용하는 사람 입장에서는 뭔가 꺼림칙하긴 하다. 편하게 해준대서 썼더니만.. 내가 다시 추상화를 해야 한다고?
🚀 JdbcClient의 등장
Spring Framework 6.1.0-M3 부터 DataAccessUtils.optionalResult
메서드가 추가됐다 (PR#27735). 이를 기점으로 다시 재조명된 게 JdbcTemplate
에도 Optional
지원해달라~ 였는데, 이미 논의된 사항이었으므로 duplicated
로 처리되었다 (#30927).
Optional
이 Template이 아닌 DataAccessUtils
에 추가된 건 의도되었다. Template
내부에서 값을 강제할 때에는 내부 메서드를 호출하지 않는다. Template은 정말 SQL 자체에만 집중한다. 쿼리의 결과값에 제한을 걸어 두는 것은 DataAccessUtils
에게 맡긴다. 따라서 단 하나의 결과를 다루는 queryForObject
의 값 검증은 DataAccessUtils
에서 진행한다. 이 관점에서 본다면 Optional
또한 DataAccessUtil
에 존재하는 것이 조금 더 알맞을지도 모른다. 완벽하게 객체 책임의 관점에서 바라보았다고 생각했다. 🥹
JdbcTemplate
에 수정을 가하기는 어려우니, WebClient
와 같은 추상화 방식을 Jdbc에도 적용하면 어떨까? 라는 이야기를 유겐 휠러가 2017년 코멘트에서 언급했다. 이는 #30931에서 구체화되었고, Spring Framework 6.1.0-M4부터 지원한다. 2023년 8월 즈음에 커밋을 한 이력이 있으니, 1년 좀 더 된 따끈따끈한 친구다.
실제로 JdbcClient를 쓰면 굉장히 편하다. 추상화를 더 해버리니 사용하는 사람 입장에서는 정말 실수하기 어렵게 설계되었음을 느꼈다. JPQL을 쓰는 것처럼 :
파라미터 연산자를 사용할 수도 있고, query
인자로 대상 클래스를 삽입해 Stream, Set, List, Optional, T
타입으로 변환해올 수도 있다. 아래 예시를 이해하는 데 어려움이 있는가?!
record Fruit(int id, String name) {} private final JdbcClient jdbcClient = JdbcClient.create(dataSource); @Test void selectOptional() { Optional<Fruit> actual = jdbcClient.sql("SELECT * FROM fruit WHERE id = :id") .param("id", 1) .query(Fruit.class) .optional(); assertThat(actual).isPresent(); }
(JPA 왜 쓸까..?)
💬 미션으로 돌아와서
라이브러리나 프레임워크를 개발한다면, 무엇보다 중요한 건 일관적으로 작동하게끔 설계하는 것이다. 사용자가 코드를 작성할 때 추가 인지부하 없이 빠르게 기대하고자 하는 것을 찾게 돕는 것도 라이브러리/프레임워크가 할 일이다. 스프링은 정말 논리적으로 자신들이 설계한 목적을 잘 드러냈고, 다른 개발자들의 염원에 힘입어 새로운 JdbcClient
까지 선보였다.
바닥부터 만드는 JdbcTemplate
이니 null
을 리턴하거나 Optional
로 감싸거나는 순전히 나의 책임이다. 의도를 잘 설명하고 일관된 명령 집합을 가지게끔 설계하는 것도 나의 책임이 되겠다. 이렇게 정리해두고 보니 나는 null
리턴에 조금 더 마음이 기울기도 하지만, Optional
을 리턴하자는 주장에도 충분히 설득력이 있다고 느낀다. 저번에도 이야기한 것 같은데, 코드는 문학이라는 말이 참 잘 어울린다. 모든 사람을 끄덕이게 만들기는 쉽지 않지만, 7-8명 정도가 나의 코드에 수긍해준다면 그걸로 만족할 수 있겠다. 참 어렵다 미션 🥴