개발 공부/백엔드

스프링 DB 접근 기술 : 2. JdbcTemplate

baby-t 2025. 9. 27. 19:47

 

https://www.inflearn.com/

 

인프런 - 라이프타임 커리어 플랫폼

프로그래밍, 인공지능, 데이터, 마케팅, 디자인등 입문부터 실전까지 업계 최고 선배들에게 배울 수 있는 곳.

www.inflearn.com

인프런 사이트의 김영한님의 강의를 보면서 작성한 글입니다.

 

이전 포스팅에서는 순수 JDBC를 사용하여 데이터베이스에 접근했습니다. 하지만 try-catch-finally를 반복하고, Connection, PreparedStatement, ResultSet 등의 리소스를 수동으로 닫아주는 등, 너무나 많은 반복적인 코드가 필요했습니다.

이번 포스팅에서는 이러한 불편함을 획기적으로 개선해주는 스프링 JdbcTemplate을 사용하여 리포지토리를 다시 구현해 보겠습니다. JdbcTemplate을 사용하면 반복적인 코드는 대부분 제거되지만, SQL은 여전히 개발자가 직접 작성해야 합니다. ⚙️


## 1. JdbcTemplateMemberRepository 구현

### 1.1. 설정 및 의존관계 주입

Java
 
public class JdbcTemplateMemberRepository implements MemberRepository {

    // JdbcTemplate은 DataSource가 있어야 동작합니다.
    private final JdbcTemplate jdbcTemplate;

    // 생성자가 딱 하나일 경우 @Autowired를 생략할 수 있습니다.
    // 스프링이 컨테이너에서 DataSource 빈을 찾아 자동으로 주입해줍니다.
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        // 이 시점에 JdbcTemplate 객체가 생성되고 의존성이 주입됩니다.
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    //...
}

### 1.2. save() - SimpleJdbcInsert 활용

순수 JDBC와 비교했을 때, try-catch-finally나 리소스 해제 코드가 전혀 없는 것을 볼 수 있습니다.

Java
 
@Override
public Member save(Member member) {
    // SimpleJdbcInsert는 INSERT SQL을 자동으로 만들어주는 편리한 기능입니다.
    SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
    // 'member' 테이블에, 'id' 컬럼을 Key로 하여 데이터를 삽입하겠다고 명시합니다.
    jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

    Map<String, Object> parameters = new HashMap<>();
    parameters.put("name", member.getName()); // "name" 컬럼에 member의 이름을 넣습니다.

    // 쿼리를 실행하고, 생성된 Key(id) 값을 Number 타입으로 반환받습니다.
    Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
    member.setId(key.longValue());
    return member;
}

 

dataSource는, 스프링 부트가 application.properties를 보고 자동으로 만들어준 HikariCP 커넥션 풀이 적용된 DataSource인 것입니다.

### 커넥션 풀 (Connection Pool)

현대적인 웹 애플리케이션에서는 DataSource가 **커넥션 풀(Connection Pool)**이라는 매우 중요한 기술을 포함하고 있습니다.

  • 개념: 사용자의 요청이 있을 때마다 데이터베이스와 새로 연결(Connection)하는 것은 매우 비효율적이고 시간이 오래 걸리는 작업입니다. 커넥션 풀은 미리 정해진 개수의 커넥션을 미리 만들어두고 '풀(pool)'에 보관했다가, 요청이 오면 빌려주고, 사용이 끝나면 반납받아 재사용하는 방식입니다.
  • HikariCP: 스프링 부트 2.0부터는 HikariCP라는 매우 빠르고 안정적인 커넥션 풀 라이브러리를 기본 DataSource 구현체로 사용합니다.

 


질문 - parameters의 name 컬럼은 어디있는거야 String Object 쌍인데

 

parameters의 "name"은 변수나 특별한 객체가 아니라, 데이터베이스 member 테이블에 있는 컬럼(column)의 이름을 의미하는 문자열(String) 값입니다.

## 동작 원리

SimpleJdbcInsert는 개발자가 INSERT SQL 문을 직접 작성하지 않아도 되도록 도와주는 편리한 도구입니다. 이 도구가 INSERT 문을 자동으로 생성하려면, "어떤 테이블의, 어떤 컬럼에, 어떤 값을 넣을지" 알려줘야 합니다.

이 정보를 전달하기 위해 Map<String, Object>를 사용하는 것입니다.

  1. Map<String, Object> parameters = new HashMap<>();: 값을 담을 Map을 하나 준비합니다.
  2. parameters.put("name", member.getName());: 이 코드는 Map에 다음과 같은 정보를 넣는 과정입니다.
    • Key (문자열 "name"): "데이터를 넣을 DB 테이블의 컬럼 이름은 'name' 입니다."
    • Value (member.getName()): "그리고 그 'name' 컬럼에 넣을 실제 값은 member.getName() 입니다."

SimpleJdbcInsert는 이 parameters 맵을 전달받으면, 내부적으로 다음과 같은 SQL을 생성하고 실행합니다.

SQL
 
INSERT INTO member (name) VALUES (?); 
-- ? 자리에는 member.getName()의 값이 들어갑니다.

즉, Map의 KeyDB 컬럼명이 되고, Map의 Value해당 컬럼에 저장될 값이 되는 것입니다.


### 1.3. 조회 - jdbcTemplate.query()와 RowMapper

findById, findByName, findAll과 같은 조회 쿼리는 jdbcTemplate.query() 메소드를 사용하여 매우 간결하게 작성할 수 있습니다. 이는 템플릿 메소드 패턴이 적용된 대표적인 예시로, 개발자는 핵심 로직만 채워 넣으면 됩니다.

Java
 
@Override
public Optional<Member> findById(Long id) {
    // query() 메소드는 쿼리 실행 결과를 List 형태로 반환합니다.
    List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
    // 결과가 없거나 하나이므로, stream().findAny()를 통해 Optional로 변환하여 반환합니다.
    return result.stream().findAny();
}
//... findByName, findAll도 유사

[질문] RowMapper는 무엇이고, 파라미터는 어디서 오나요?

**RowMapper**는 자료구조가 아니라, 스프링 JDBC가 제공하는 인터페이스입니다.

1. 역할: ResultSet의 각 행(row)에 있는 데이터를 개발자가 원하는 자바 객체(여기서는 Member 객체)로 어떻게 변환(mapping)할지 그 방법을 정의하는 역할을 합니다.

2. 람다와 파라미터: memberRowMapper() 메소드는 이 RowMapper 인터페이스를 람다식으로 구현한 것입니다.

Java
 
private RowMapper<Member> memberRowMapper() {
    // 이 파라미터들(rs, rowNum)은 jdbcTemplate이 내부적으로 제공합니다.
    return (rs, rowNum) -> {
        Member member = new Member();
        member.setId(rs.getLong("id"));
        member.setName(rs.getString("name"));
        return member;
    };
}

jdbcTemplate.query()가 실행되면 내부적으로 다음과 같은 일이 일어납니다.

  1. query()가 JDBC를 통해 ResultSet을 받습니다.
  2. ResultSet의 각 행을 순회하면서, 우리가 넘겨준 람다식(RowMapper)을 실행합니다.
  3. 이때, 현재 행의 ResultSet 객체(rs)와 현재 행의 번호(rowNum)를 람다식의 파라미터로 넣어줍니다.
  4. 람다식은 이 rs를 받아 Member 객체를 만든 뒤 반환하고, jdbcTemplate은 이렇게 만들어진 Member 객체들을 List에 담아 최종적으로 반환합니다.

## 3. 통합 테스트 확인

이전 포스팅에서 작성했던 MemberServiceIntegrationTest 코드를 단 한 줄도 수정하지 않고 그대로 다시 실행해 봅시다. SpringConfig에서 리포지토리의 구현체만 JdbcTemplateMemberRepository로 교체했기 때문에, 모든 테스트는 이전과 동일하게 성공적으로 통과하는 것을 확인할 수 있습니다.

이는 우리가 인터페이스를 기반으로 유연하게 설계하고, 스프링의 DI를 적극적으로 활용한 덕분입니다.