본문 바로가기
Backend

[Spring] Spring DB 연동: JDBC vs JPA

by persi0815 2024. 12. 14.
'초보 웹 개발자를 위한 스프링 5 프로그래밍 입문' 챕터 8을 읽고 정리한 내용입니다. 

 

스프링 애플리케이션에서 데이터베이스에 접근하는 방법에는 크게 JDBCJPA가 있다.

  • JDBC(Java Database Connectivity)
    Java 애플리케이션이 데이터베이스와 상호 작용하기 위해 제공되는 표준 API이다. SQL을 직접 작성하고 실행해야 하며, Connection, Statement, ResultSet 등의 객체를 통해 데이터를 처리한다.
  • JPA(Java Persistence API)
    ORM(Object-Relational Mapping) 기술의 표준 사양이다. 엔티티(Entity) 객체를 중심으로 데이터베이스를 다룰 수 있도록 해주며, SQL 대신 엔티티 중심의 코딩에 집중할 수 있다.

일반적으로 스프링 부트 프로젝트에서는 JPA + Spring Data JPA를 사용하는 것이 보편적이지만, 상황에 따라 직접 SQL 최적화가 필요한 경우에는 JDBC를 사용하기도 한다.

 

1. JDBC (Java Database Connectivity)란?

JDBC 개요

  • 자바에서 데이터베이스와 상호 작용하기 위한 가장 기본적인 방법이다.
  • SQL을 직접 작성하여 데이터베이스 쿼리를 실행하고, 그 결과를 처리한다.
  • Connection, Statement, PreparedStatement, ResultSet 등 다양한 인터페이스와 클래스를 다뤄야 한다.

 

JDBC의 핵심 인터페이스

Connection 데이터베이스와의 연결을 나타내는 객체
Statement SQL 쿼리를 실행하는 객체
PreparedStatement 미리 컴파일된 SQL 문을 실행하는 객체
ResultSet SQL 조회 결과를 저장하는 객체

 

JDBC 코드 예제 (SQL 직접 실행)

import java.sql.*;

public class JdbcExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String user = "user";
        String password = "password";

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            // 1. 데이터베이스 연결
            conn = DriverManager.getConnection(url, user, password);

            // 2. SQL 실행
            String sql = "SELECT * FROM users WHERE id = ?";
            pstmt = conn.prepareStatement(sql);
            pstmt.setInt(1, 1);
            rs = pstmt.executeQuery();

            // 3. 결과 처리
            while (rs.next()) {
                System.out.println("User: " + rs.getString("name"));
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try { rs.close(); pstmt.close(); conn.close(); } catch (Exception ignored) {}
        }
    }
}

 

코드에서 볼 수 있다싶이, SQL을 직접 다뤄야 하며, Connection, PreparedStatement, ResultSet 등을 모두 직접 제어해야 하고, 예외 처리 및 리소스 정리 코드도 반복적으로 작성해야 한다.

 

JDBC의 한계점

  • SQL 문에 대한 의존도가 높다. (DB 벤더 변경 시 SQL 수정 필요)
  • 객체-관계 매핑을 직접 처리해야 하므로(예: rs.getString("name") 등) 코드가 복잡해질 수 있다.
  • 트랜잭션을 명시적으로 다뤄야 하므로 setAutoCommit(false), commit(), rollback() 등을 직접 관리해야 한다.

 

Spring에서의 JDBC 지원

스프링은 이러한 단점을 완화하기 위해 JdbcTemplate 등의 편의 클래스를 제공한다.
JdbcTemplate를 사용하면 예외 처리를 단순화하고, Connection · Statement · ResultSet을 자동으로 정리해주어 JDBC 사용이 한결 편해진다.

 

2. JPA (Java Persistence API)란?

JPA 개요

  • 자바 진영의 ORM(객체-관계 매핑) 표준 사양이다.
  • 객체(Entity) 중심의 접근 방식을 제공하며, 반복적인 SQL 작성을 줄여주고 데이터베이스 접근 로직을 간소화할 수 있다.
  • 대표적인 구현체로 Hibernate가 있으며, Spring Data JPA는 JPA를 더욱 쉽게 사용할 수 있도록 지원한다.
ORM (객체-관계 매핑) 이란?
객체(Object)와 관계형 데이터베이스(Table) 간의 불일치를 해결하는 기술로, 객체 필드를 DB 컬럼과 매핑하여, 객체 지향적으로 데이터를 관리할 수 있도록 지원한다. 

 

JPA 코드 예제 (객체 중심 데이터 처리)

import javax.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    // Getter, Setter 생략
}

// JPA를 활용한 데이터 저장
public class JpaExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPersistenceUnit");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();

        // 객체 저장
        User user = new User();
        user.setName("John");
        em.persist(user);

        em.getTransaction().commit();
        em.close();
        emf.close();
    }
}

 

엔티티 객체(User)에 값을 설정하고 em.persist()만 호출하면, JPA가 내부적으로 SQL을 생성하고 실행한다. 트랜잭션(begin(), commit()) 관리도 JPA가 지원하므로, 개발자가 직접 Connection · PreparedStatement · ResultSet 등을 다룰 필요가 크게 줄어든다.

 

 

JDBC vs JPA 비교

비교 항목 JDBC JPA
SQL 작성 직접 SQL 작성 SQL 대신 Entity 사용
코드 복잡도 Connection, Statement 관리 필요 EntityManager가 자동 관리
객체 매핑 직접 매핑해야 함 (ResultSet) 자동 매핑 (@Entity)
트랜잭션 관리 수동 (commit(), rollback()) 자동 (@Transactional)
성능 직접 최적화 필요 1차 캐시, 지연 로딩 등 최적화 기능 제공

 

  • JDBC는 직접 SQL 최적화가 필요하거나, 대량의 배치성 작업 등에 유리하다.
  • JPA비즈니스 로직을 객체 모델로 단순화하고, 코드의 유지보수성을 높이는 데 유리하다.
일반적으로 Spring Boot에서는 JPA + Spring Data JPA를 사용하며, 필요에 따라 JDBC를 혼합하여 사용할 수도 있다. JPA를 사용하면 개발 생산성이 크게 향상되므로, 가능하면 JPA를 기본 선택지로 고려하는 것이 좋다

 

3. 스프링과 데이터베이스 연동

스프링 부트에서는 데이터베이스 연동을 위해 자동 설정(Auto Configuration)을 제공하며, 개발 편의성을 크게 높여준다.

 

Spring Boot에서 의존성 주입

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    ..

 

→ spring-boot-starter-jdbc와 같은 의존성을 추가했을 때, 데이터베이스와 관련된 설정을 자동으로 처리해준다. 

 

spring-boot-starter-data-jpa안에 spring-boot-starter-jdbc가 존재하며, 추가로 아래 의존성들도 포함된다. 

  • hibernate-core:JPA 구현체로 Hibernate를 제공
  • javax.persistence: JPA 표준 인터페이스를 포함
  • HikariCP:기본적으로 사용하는 커넥션 풀(Connection Pool) 라이브러리

 

DataSource 설정

DataSource는 데이터베이스 연결을 관리하는 인터페이스이다. 

 

Spring에서는 데이터베이스 연결을 효율적으로 관리하기 위해 DataSource 빈을 설정하고 사용한다. 

스프링 부트는 application.yml 또는 application.properties 파일에 설정된 내용을 토대로 자동으로 DataSource 빈을 생성하며, Spring 컨테이너에서 빈으로 관리된다. 

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: user
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  sql:
    init:
      mode: never

 

 

spring.datasource.* 속성을 정의하고, MySQL JDBC 드라이버(com.mysql:mysql-connector-j)가 클래스패스에 존재한다면, 스프링 부트가 내부적으로 HikariCP(기본 커넥션 풀 라이브러리)를 사용하여 DataSource 빈을 생성한다.

 

이렇게 생성된 DataSource 빈은 스프링 컨테이너 내에서 관리되며, 다른 빈에서 의존성 주입을 받아 데이터베이스와 연결할 때 사용된다. 즉, 개발자가 직접 DriverManager.getConnection을 호출할 필요 없이, 스프링이 미리 구성해 둔 DataSource 빈을 통해 데이터베이스 커넥션을 보다 손쉽게 활용할 수 있다.

 

커넥션 풀 (Connection Pool)

 

커넥션 풀은 미리 일정 개수만큼 데이터베이스 연결을 만들어 두고, 필요할 때 재사용함으로써 DB 연결 비용을 줄인다. 스프링 부트는 기본적으로 HikariCP를 사용하며, 최대 커넥션 수나 대기 시간 등의 설정을 조절할 수 있다.

 

@Bean
public DataSource dataSource() {
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
    dataSource.setUsername("user");
    dataSource.setPassword("password");
    dataSource.setMaximumPoolSize(10); // 최대 커넥션 수
    return dataSource;
}

 

이렇게 생성된 DataSource 빈은 JDBCJPA든 공통적으로 사용된다.

 

5. 스프링 트랜잭션 관리

선언적 트랜잭션(@Transactional)

스프링에서는 @Transactional 애노테이션을 통해 선언적 트랜잭션을 지원한다.
메서드나 클래스에 @Transactional을 선언해두면, 해당 메서드가 실행될 때 자동으로 트랜잭션이 시작되고, 예외가 발생하면 자동으로 롤백되며, 정상 종료 시 자동으로 커밋이 일어난다.

 

@Service
public class UserService {

    @Transactional
    public void transferMoney(Long fromUserId, Long toUserId, int amount) {
        userRepository.decreaseBalance(fromUserId, amount);
        userRepository.increaseBalance(toUserId, amount);
    }
}

 

 

JPA를 사용할 때는 보통 JpaTransactionManager가 동작하고, JDBC만 사용하는 경우 DataSourceTransactionManager가 동작한다.

 

 

Spring Data JPA와 DataSource 사용 흐름

Spring Data JPA에서 @Repository 애노테이션과 함께 JpaRepository를 상속한 인터페이스를 작성하면, 내부적으로 DataSource를 사용해 데이터베이스와 연결한다. 이 과정은 Spring Data JPA와 Hibernate(또는 다른 JPA 구현체)가 자동으로 처리한다.

  1. DataSource 빈 생성
    • Spring Boot의 자동 설정을 통해 HikariCP 등의 커넥션 풀을 사용하여 DataSource 빈이 생성된다.
  2. EntityManagerFactory 생성
    • 스프링 부트가 JPA 설정을 읽고, 내부적으로 EntityManagerFactory를 생성한다.
    • 생성된 DataSource는 JPA의 EntityManager 및 Hibernate의 Session과 연결된다.
  3. Hibernate에 의한 SQL 실행
    • Hibernate는 DataSource를 통해 필요한 커넥션을 가져오고, SQL 쿼리를 실행한다.
  4. JpaRepository와의 연결
    • JpaRepository를 상속한 인터페이스에서 메서드(findById, save 등)를 호출하면, Spring Data JPA가 이를 프록시 객체를 통해 처리한다.
    • 메서드 실행 시 내부적으로 EntityManager를 호출하고, 이 EntityManager는 DataSource를 사용하여 데이터베이스와 상호작용한다.
    • 결국 Spring Data JPA에서 호출하는 모든 데이터베이스 작업은 내부적으로 DataSource를 통해 연결된다.