Spring JDBC의 주요 개념과 요소 살펴보기

CJI0524·2025년 3월 21일
0

Spring/JDBC

목록 보기
1/3

1. Spring JDBC란?

Spring JDBC스프링 프레임워크가 제공하는 JDBC 추상화 계층으로 기존의 JDBC가 가진 불편한 요소들을 해결했다. 즉 Spring JDBC는 기존 JDBC 위에 추가적인 기능과 추상화 계층을 덧붙여서 개발자가 더 쉽게 데이터베이스 작업을 할 수 있도록 만들어졌다. JdbcTemplate 같은 클래스를 통해 JDBC의 번거로운 작업들을 줄여주고, 개발자가 핵심 로직에 집중할 수 있도록 도와준다. Spring JDBC의 특징은 다음과 같다.

1.1. Spring Java JDBC의 특징

  • JDBC API 위에 추상화 계층 제공 : 기본 JDBC API를 감싸서, 직접 Connection, Statement, ResultSet 관리 같은 반복되는 코드를 줄여준다. JdbcTemplate 같은 클래스로 필요한 기능만 간단하게 사용할 수 있다.

  • 자동 자원 관리 : DB 연결, Statement, ResultSet 등 리소스를 자동으로 열고 닫아줘서, try-catch-finally 구문을 직접 작성할 필요가 없다. 이 덕분에 자원 누수 걱정을 덜 수 있다.

  • 일관된 예외 처리 : JDBC의 SQLException을 스프링의 DataAccessException 계층으로 변환해준다. DBMS에 따라 달라질 수 있는 예외 정보를 공통된 런타임 예외로 처리해서, 코드가 깔끔하고 일관성 있게 에러 처리를 할 수 있다.

  • 간결한 쿼리 실행 메서드 : update(), query(), queryForObject(), batchUpdate() 등의 메서드를 제공해, CRUD 작업을 간단하게 처리할 수 있다. 복잡한 SQL 실행 로직을 단순화시켜준다.

  • RowMapper를 통한 결과 매핑 : SQL 쿼리 결과인 ResultSet의 각 행을 쉽게 객체로 매핑할 수 있도록 RowMapper 인터페이스를 사용한다. 람다식으로 간단하게 구현 가능해서, 코드가 더 깔끔해진다.


2. Spring JDBC의 주요 요소 살펴보기

2.1. JdbcTemplate 클래스

JdbcTemplate은 스프링에서 제공하는 JDBC 추상화 클래스이다. 기본적으로 JDBC를 사용할 때 번거로운 반복 작업 (커넥션 열고 닫기, 예외 처리 등)을 스프링이 알아서 처리해줘서, 개발자는 SQL 쿼리와 결과 매핑 같은 핵심 로직에만 집중할 수 있게 된다.

주요 특징은 다음과 같다.

  • 자원 관리 자동화

    JDBC에서 Connection, Statement, ResultSet 같은 자원 관리를 직접 해야 하는데, JdbcTemplate은 내부적으로 try-catch-finally 구조를 사용해서 이 부분을 대신 처리한다. 덕분에 자원 누수나 예외 처리를 신경 쓰지 않아도 된다.

  • 예외 변환
    JDBC에서 발생하는 SQLException을 스프링의 DataAccessException 계층으로 변환해준다. 이렇게 하면 특정 데이터베이스에 종속되지 않고, 공통된 예외 처리 로직을 만들 수 있다.


JdbcTemplate의 주요 메서드

1. update()

  • INSERT, UPDATE, DELETE 같은 데이터 변경 작업에 사용.
  • SQL 쿼리와 해당 파라미터들을 받아서 실행하고, 영향을 받은 행(row) 수를 반환.

✍️ 예제 코드

// 데이터 삽입 예제
jdbcTemplate.update("INSERT INTO users (name, email) VALUES (?, ?)", "kim", "kim@example.com");

// 데이터 업데이트 예제
jdbcTemplate.update("UPDATE users SET email = ? WHERE name = ?", "new.kim@example.com", "kim");

// 데이터 삭제 예제
jdbcTemplate.update("DELETE FROM users WHERE name = ?", "kim");

2. query()

  • SELECT 쿼리를 실행하여 여러 행을 조회할 때 사용.
  • RowMapper를 사용해 ResultSet의 각 행을 원하는 객체로 매핑하고, 결과를 List로 반환.

✍️ 예제 코드

RowMapper<User> rowMapper = (ResultSet rs, int rowNum) -> new User(
    rs.getLong("id"),
    rs.getString("name"),
    rs.getString("email")
);
List<User> users = jdbcTemplate.query("SELECT * FROM users", rowMapper);
users.forEach(user -> System.out.println(user.getName() + " - " + user.getEmail()));

해당 코드에서 query 메서드는 "SELECT * FROM users" 쿼리를 실행한 후, 반환된 ResultSet의 각 행마다 rowMapper를 적용한다.
즉, 각 행을 User 객체로 변환하고, 그 결과들을 List에 담아서 반환한다.

3. queryForObject()

  • 단일 결과값을 조회할 때 사용.
  • 결과가 반드시 하나의 행이어야 하며, 값 하나를 바로 가져오거나, 단일 객체로 매핑할 때 유용.
  • 만약 결과가 없거나 여러 행이 반환되면 예외가 발생할 수 있음.

✍️ 예제 코드

// 특정 사용자 이름으로 이메일 조회 (단일 값)
String email = jdbcTemplate.queryForObject(
    "SELECT email FROM users WHERE name = ?", // 실행할 SQL 쿼리문. '?'는 나중에 값으로 대체됨.
    new Object[] {"kim"},  // 쿼리문의 '?'에 들어갈 값들. 여기서는 "kim"이 들어감.
    String.class  // 쿼리 결과로 반환받을 타입. 
);
System.out.println("Email: " + email);

4. batchUpdate()

  • 여러 SQL 쿼리를 한 번에 실행할 때 사용.
  • 주로 다수의 INSERT, UPDATE, DELETE 쿼리를 효율적으로 처리하기 위해 사용.
  • 파라미터 배열을 넘겨서 한 번에 여러 작업을 처리할 수 있음.

✍️ 예제 코드

String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
List<Object[]> batchArgs = new ArrayList<>();
batchArgs.add(new Object[]{"user1", "user1@example.com"});
batchArgs.add(new Object[]{"user2", "user2@example.com"});
batchArgs.add(new Object[]{"user3", "user3@example.com"});

int[] updateCounts = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println("Inserted rows count: " + Arrays.toString(updateCounts));

혹은 이렇게 사용 할 수도 있다.

  1. SQL 문자열에 파라미터 자리 (?)를 넣고,
  2. BatchPreparedStatementSetter 구현체를 통해
  3. setValues(PreparedStatement ps, int i)에서 각 배치별 파라미터를 바인딩하고
  4. getBatchSize()로 총 배치 건수를 알려 주면,
  5. jdbcTemplate.batchUpdate(sql, setter)를 호출해 한 번에 실행하는 방식이다.

✍️ 예제 코드

int[] counts = jdbcTemplate.batchUpdate(
    // 1) 실행할 SQL (파라미터 자리표시자 ? 사용)
    "INSERT INTO users(name, email) VALUES(?, ?)",
    new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            // 2) 배치 인덱스 i에 해당하는 User 객체 가져오기
            User u = users.get(i);
            // 3) 첫 번째 ? 에 사용자 이름 바인딩
            ps.setString(1, u.getName());
            // 4) 두 번째 ? 에 사용자 이메일 바인딩
            ps.setString(2, u.getEmail());
        }

        @Override
        public int getBatchSize() {
            // 5) 총 배치 크기 지정 (users 리스트 크기 만큼 반복)
            return users.size();
        }
    }
);

// 6) 반환된 counts 배열: 각 배치 실행마다 영향을 받은 행 수 (보통 1)
System.out.println("Batch update completed. Number of rows affected: " + Arrays.toString(counts));

2.2. RowMapper 인터페이스

RowMapper는 JdbcTemplate에서 SELECT 쿼리 결과로 나온 ResultSet의 각 행 (row)을 원하는 객체로 변환 (mapping)하는 역할을 하는 함수형 인터페이스이다.

  • 인터페이스 정의: RowMapper<T>는 제네릭 인터페이스로, T 타입의 객체를 반환한다..

✍️ RowMapper 인터페이스 정의

public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

여기서 mapRow() 메서드는 ResultSet의 현재 행을 T 타입의 객체로 변환하는 로직을 구현하도록 한다.

JDBC로 SELECT 쿼리를 실행하면 ResultSet이라는 표 형태의 데이터를 받는다. RowMapper를 사용하면 이 ResultSet의 각 행을 원하는 POJO* (Plain Old Java Object)로 간단하게 매핑할 수 있다. 즉, DB에서 가져온 데이터를 자바 객체로 변환해서, 이후 비즈니스 로직에서 편리하게 사용할 수 있게 해준다.

POJO (Plain Old Java Object) 란?

간단히 말해, 특정 프레임워크나 라이브러리에 종속되지 않고, 순수하게 자바 문법만을 사용해 만든 객체를 의미한다.

예를 들어, 다음과 같은 User 객체가 있다고 가정하면,

✍️ 예시 코드 작성

public class User {
    private Long id;
    private String name;
    private String email;
    
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    // getter, setter 생략
} 

JdbcTemplate을 사용해 SELECT 쿼리를 실행하고, 각 행을 User 객체로 매핑하는 RowMapper의 예시는 이렇게 작성할 수 있다.

✍️ 예시 코드 작성

RowMapper<User> rowMapper = (ResultSet rs, int rowNum) -> new User(
    rs.getLong("id"),
    rs.getString("name"),
    rs.getString("email")
);
List<User> users = jdbcTemplate.query("SELECT * FROM users", rowMapper);  

위 코드에서 람다식으로 구현한 rowMapper는 ResultSet의 각 행을 User 객체로 변환하는 역할을 수행한다. JdbcTemplate의 query() 메서드는 이 rowMapper를 이용해, 쿼리 결과의 모든 행을 List로 만들어준다.

여기서 rowNum은 ResultSet안에서 현재 몇 번째 row (행)인지를 의미한다.


JdbcTemplate, RowMapper 종합 활용 예제

✍️ 예시 코드 작성

@SpringBootApplication
public class JdbcTemplateExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(JdbcTemplateExampleApplication.class, args);
    }

    // CommandLineRunner를 통해 애플리케이션 구동 후 아래 코드를 실행
    @Bean
    public CommandLineRunner demo(JdbcTemplate jdbcTemplate) {
        return args -> {
            // 1. update() 메서드 사용 예제
            // 데이터 삽입 (INSERT)
            int inserted = jdbcTemplate.update(
                "INSERT INTO users (name, email) VALUES (?, ?)", 
                "kim", "kim@example.com"
            );
            System.out.println("Inserted rows: " + inserted);

            // 2. query() 메서드 사용 예제
            // RowMapper를 람다식으로 구현해서, ResultSet의 각 행을 User 객체로 변환
            RowMapper<User> userRowMapper = (ResultSet rs, int rowNum) -> new User(
                rs.getLong("id"),
                rs.getString("name"),
                rs.getString("email")
            );
            // SELECT 쿼리 실행, 결과를 List<User>로 반환
            List<User> users = jdbcTemplate.query("SELECT * FROM users", userRowMapper);
            // 각 사용자 정보를 출력
            users.forEach(user -> System.out.println(user.getName() + " - " + user.getEmail()));

            // 3. queryForObject() 메서드 사용 예제
            // 단일 값 조회: 특정 사용자의 이메일을 조회
            String email = jdbcTemplate.queryForObject(
                "SELECT email FROM users WHERE name = ?", 
                new Object[] {"kim"},  // 쿼리 파라미터 배열
                String.class         // 반환 타입 지정
            );
            System.out.println("Email for 'kim': " + email);

            // 4. batchUpdate() 메서드 사용 예제
            // 여러 건의 데이터를 한 번에 삽입하는 배치 작업
            String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
            List<Object[]> batchArgs = new ArrayList<>();
            batchArgs.add(new Object[]{"user1", "user1@example.com"});
            batchArgs.add(new Object[]{"user2", "user2@example.com"});
            batchArgs.add(new Object[]{"user3", "user3@example.com"});
            // 배치 업데이트 실행, 각 쿼리의 결과(영향받은 행 수)를 배열로 반환
            int[] updateCounts = jdbcTemplate.batchUpdate(sql, batchArgs);
            System.out.println("Batch inserted rows: " + Arrays.toString(updateCounts));

            // 추가적으로 update() 메서드를 사용한 UPDATE 예제
            int updated = jdbcTemplate.update(
                "UPDATE users SET email = ? WHERE name = ?", 
                "new.kim@example.com", "kim"
            );
            System.out.println("Updated rows: " + updated);

            // update() 메서드를 사용한 DELETE 예제
            int deleted = jdbcTemplate.update(
                "DELETE FROM users WHERE name = ?", 
                "kim"
            );
            System.out.println("Deleted rows: " + deleted);
        };
    }
}

// 간단한 User POJO 클래스 예제
class User {
    private Long id;
    private String name;
    private String email;

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getter 메서드들
    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
}  

2.3. BeanPropertyRowMapper

BeanPropertyRowMapper는 스프링 JDBC에서 ResultSet의 컬럼 (열) 값을 자바 빈 (POJO) 객체의 프로퍼티에 자동으로 매핑해 주는 편리한 클래스라고 할 수 있다.

자동 매핑 규칙은 다음과 같다.

  • ResultSet의 컬럼명 (예: user_name)을 자바 객체의 프로퍼티명 (예: userName)에 맞춰 카멜 케이스로 변환해서 설정해줌.
  • 매핑할 프로퍼티에 맞는 setter 메서드가 있어야 함.

✍️ 예시 코드 작성

String sql = "SELECT id, name, email FROM users WHERE id = :id";
Map<String, Object> params = Map.of("id", userId);

User user = jdbcTemplate.queryForObject(
    sql,
    params,
    new BeanPropertyRowMapper<>(User.class)
);  

위 코드에서 BeanPropertyRowMapper<>(User.class)를 사용하면,

  • id 컬럼 값 → User.setId(...)
  • name 컬럼 값 → User.setName(...)
  • email 컬럼 값 → User.setEmail(...)

이렇게 자동으로 호출해서 User 객체를 만들어 준다.

장점

  • 코드 간결 : RowMapper 구현을 일일이 작성할 필요 없이, 단 한 줄로 매핑 가능
  • 유지보수 편리 : 매핑 로직을 반복 작성하지 않아도 됨

주의점

  • 컬럼명과 프로퍼티명이 일치하거나 카멜 케이스로 변환 가능한 경우에만 제대로 동작

2.4. ResultSetExtractor

ResultSetExtractor는 Spring JDBC에서 쿼리 실행 후 결과인 ResultSet 전체를 한 번에 처리해서 원하는 형태의 객체로 만들어주는 역할을 한다. 쉽게 말하면, RowMapper가 한 행씩 매핑하는 반면에 ResultSetExtractor는 ResultSet 전체를 보고 복잡한 가공이나 집계가 필요할 때 쓴다.

예를 들어, 단순한 경우엔 RowMapper로 충분해서 한 행마다 User 객체로 변환하지만,
만약 여러 행의 데이터를 조합해서 하나의 복합 객체를 만들거나, 데이터 집계를 진행해야 한다면 ResultSetExtractor를 사용하게 된다.

✍️ 예시 코드 작성

ResultSetExtractor<List<User>> extractor = rs -> {
    List<User> userList = new ArrayList<>();
    while(rs.next()){
        User user = new User(
            rs.getLong("id"),
            rs.getString("name"),
            rs.getString("email")
        );
        userList.add(user);
    }
    return userList;
};  

이 코드는 ResultSet 전체를 순회하면서 각 행의 데이터를 User 객체로 만들어 리스트로 반환하는 예시이다.
이처럼 ResultSetExtractor를 사용하면 결과 집합을 자유롭게 가공할 수 있어서, 단순 매핑 이상으로 복잡한 로직을 구현할 때 유용하다.


3. 이 글을 작성하는데 참고한 글 목록

멋쟁이사자처럼 강의자료

profile
개발돌이

0개의 댓글