앞 두 챕터의 JdbcTemplate과 NamedParameterJdbcTemplate은 강력하지만 두 가지 불편이 있다. (1) 위치 파라미터와 이름 파라미터를 쓰려면 서로 다른 클래스를 골라야 하고, (2) query/queryForObject/queryForList 등 메서드 이름으로 결과 형태를 선택해야 해 오버로드가 수십 개다. 이 챕터는 이 둘을 하나로 통합한 6.1의 유창한 파사드 JdbcClient, 그리고 DB 메타데이터를 읽어 SQL 자체를 없애 주는 SimpleJdbcInsert/SimpleJdbcCall, 마지막으로 객체지향 스타일의 jdbc.object를 다룬다.
┌─────────────────────────── JdbcClient (인터페이스, 6.1) ────────────────────────┐
│ sql(String) → StatementSpec │
│ │
│ StatementSpec ─ param(...)/params(...)/paramSource(...) 파라미터 바인딩 │
│ ─ withFetchSize/withMaxRows/withQueryTimeout │
│ ─ query() → ResultQuerySpec (Map/RowSet/단일값) │
│ ─ query(Class<T>) → MappedQuerySpec<T> │
│ ─ query(RowMapper) → MappedQuerySpec<T> │
│ ─ update() / update(KeyHolder) │
│ │
│ ResultQuerySpec ─ rowSet/listOfRows/singleRow/singleColumn/singleValue │
│ MappedQuerySpec ─ stream/list/set/single()/optional() │
└─────────────────────────────────────────────────────────────────────────────────┘
│ 구현
▼
DefaultJdbcClient ──▶ JdbcOperations + NamedParameterJdbcOperations (둘 다 보유)
JdbcClient는 인터페이스이고, create(DataSource)/create(JdbcOperations)/create(NamedParameterJdbcOperations) 정적 팩터리로 DefaultJdbcClient를 만든다. 기존 JdbcTemplate 설정을 그대로 재사용하려면 그것을 넘기면 된다.
Optional<Integer> value = client.sql("SELECT AGE FROM CUSTOMER WHERE ID = :id")
.param("id", 3)
.query(Integer.class)
.optional();sql(...) → StatementSpec 생성 (SQL 보관)
.param("id", 3) → 이름 파라미터 누적 (또는 .param(value)로 위치 파라미터)
.query(Integer) → MappedQuerySpec 생성. 단일 컬럼이므로 SingleColumnRowMapper 적용
.optional() → DataAccessUtils.optionalResult(list())
│
▼ 내부 위임 결정
파라미터가 이름 기반? → NamedParameterJdbcOperations 로
위치 기반? → JdbcOperations 로
JdbcClient의 영리함은 위치/이름 파라미터를 한 API에서 모두 받고, 바인딩 종류에 따라 내부적으로 적절한 템플릿을 고르는 데 있다. 결과 형태도 메서드 이름이 아니라 query().singleValue() / query(Class).list() 같은 2단계 스펙 선택으로 표현해 오버로드 폭발을 막는다. query(Class<T>)는 단순 타입이면 SingleColumnRowMapper, JavaBean/레코드면 SimplePropertyRowMapper를 자동 적용한다.
SimpleJdbcInsert는 INSERT 문을 직접 쓰지 않는다. 테이블 이름만 주면 JDBC DatabaseMetaData에서 컬럼 정보를 읽어 INSERT SQL을 자동 생성한다.
SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource)
.withTableName("users")
.usingGeneratedKeyColumns("id");
Number key = insert.executeAndReturnKey(Map.of("name", "Kim", "age", 30));핵심은 compile() → compileInternal()의 컴파일 단계다 (AbstractJdbcInsert).
protected void compileInternal() {
DataSource dataSource = getJdbcTemplate().getDataSource();
this.tableMetaDataContext.processMetaData(dataSource, getColumnNames(), getGeneratedKeyNames());
this.insertString = this.tableMetaDataContext.createInsertString(getGeneratedKeyNames());
this.insertTypes = this.tableMetaDataContext.createInsertTypes();
}processMetaData가 커넥션을 열어 테이블의 컬럼 목록과 타입을 읽고, 그것으로 INSERT INTO users (name, age) VALUES (?, ?)와 각 컬럼의 JDBC 타입 배열을 만든다. 이 작업은 최초 실행 시 한 번만(checkCompiled가 지연 컴파일) 일어나고, 이후엔 캐시된 SQL을 재사용한다. 실제 실행은 내부 JdbcTemplate이 한다.
SimpleJdbcInsert.execute(values)
└─ checkCompiled() ── 미컴파일이면 compile()
└─ TableMetaDataContext.processMetaData(dataSource, ...)
└─ 벤더별 TableMetaDataProvider (Oracle/Postgres/MySQL/...)
└─ jdbcTemplate.update(insertString, values, insertTypes)
벤더별 차이(식별자 대소문자, 생성 키 컬럼 처리, 스키마 규칙)는 core.metadata 패키지의 TableMetaDataProvider/CallMetaDataProvider 구현들이 흡수한다. SimpleJdbcCall은 같은 원리를 저장 프로시저에 적용해, 프로시저의 파라미터 메타데이터를 읽어 CallableStatement 호출을 자동 구성한다.
org.springframework.jdbc.object는 더 오래된, "SQL을 객체로 캡슐화"하는 스타일이다. 공통 부모 RdbmsOperation은 스스로를 "멀티스레드, 재사용 가능한, 질의/갱신/프로시저 호출을 나타내는 객체"로 정의한다.
RdbmsOperation (추상, InitializingBean)
│ JdbcTemplate jdbcTemplate
│ List<SqlParameter> declaredParameters
│ compile() ── SQL/파라미터 확정 후 재사용 가능 상태로 동결
│
├── SqlQuery<T> ─── MappingSqlQuery<T> (RowMapper 내장 쿼리 객체)
├── SqlUpdate (갱신 객체)
└── SqlCall ─────── StoredProcedure (프로시저 호출 객체)
RdbmsOperation은 SQL과 파라미터를 설정한 뒤 compile()로 동결하고, 이후 execute/update를 반복 호출한다. 한 번 컴파일하면 파라미터 변경이 막힌다. MappingSqlQuery를 상속해 mapRow만 구현하면 "이 쿼리는 이렇게 매핑한다"는 지식을 한 객체에 담을 수 있다. 다만 현대 코드에서는 JdbcClient+RowMapper가 더 권장된다.
- 파사드 + 위임:
JdbcClient/SimpleJdbcInsert/jdbc.object는 모두 새 실행 엔진이 아니라JdbcTemplate위의 편의 계층이다. 복잡한 케이스에서는 언제든 내부 템플릿으로 내려갈 수 있다. - 컴파일 = 지연 1회 초기화: 메타데이터 조회나 SQL 파싱처럼 비싼 준비 작업을 최초 실행 시 한 번만 하고 동결해 이후 호출을 빠르게 만든다.
- 유창한 빌더:
withTableName(...).usingColumns(...),sql(...).param(...).query(...)처럼 설정 메서드가this를 반환해 체이닝한다. - 벤더 추상화:
core.metadata의 제공자들이 DB별 특이사항을 캡슐화해, 애플리케이션 코드는 DB가 바뀌어도 동일하게 동작한다.
JdbcClient는 위치/이름 파라미터와 다양한 결과 형태를 하나의 유창한 API로 통합한 6.1의 권장 진입점이고, SimpleJdbcInsert/SimpleJdbcCall은 DB 메타데이터로 SQL을 아예 없애 주며, jdbc.object는 SQL을 재사용 가능한 객체로 캡슐화한 고전 스타일이다. 셋 다 본질은 JdbcTemplate 위에 얹은 편의 계층이라는 점이 일관된다. 다음 챕터는 이 모든 템플릿이 커넥션을 얻어오는 토대인 DataSource와 트랜잭션 연동을 본다.