Skip to content

Latest commit

 

History

History
112 lines (87 loc) · 8.12 KB

File metadata and controls

112 lines (87 loc) · 8.12 KB

04 — JdbcClient와 메타데이터 기반 연산

무엇을 / 왜

앞 두 챕터의 JdbcTemplateNamedParameterJdbcTemplate은 강력하지만 두 가지 불편이 있다. (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 설정을 그대로 재사용하려면 그것을 넘기면 된다.

동작 흐름: JdbcClient 한 줄

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 / SimpleJdbcCall

SimpleJdbcInsertINSERT 문을 직접 쓰지 않는다. 테이블 이름만 주면 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 호출을 자동 구성한다.

객체지향 스타일: jdbc.object

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와 트랜잭션 연동을 본다.