bkdragon's log

JDBC 부터 Spring Data JPA 까지 본문

Java Spring

JDBC 부터 Spring Data JPA 까지

bkdragon 2024. 9. 20. 18:01

Spring Boot의 Data Access layer에 대해 알아보자.

JDBC

자바 애플리케이션이 관계형 데이터베이스와 상호작용할 수 있도록 해주는 표준 API이다.

JDBC는 3가지 기능을 표준 인터페이스로 정의하여 제공한다.

  • java.sql.Connection - 연결
  • java.sql.Statement, PreparedStatement - SQL을 담은 내용
  • java.sql.ResultSet - SQL 요청 응답

JDBC 드라이버는 특정 데이터베이스에 맞게 JDBC API 의 인터페이스를 구현한 구현체이다.

JDBC 드라이버 덕분에 개발 과정에선 JDBC 표준만으로 코드를 작성할 수 있다.

JDBC의 동작 흐름은 다음과 같다.

  • JDBC 드라이버 로딩 : 사용하고자 하는 JDBC 드라이버를 로딩한다. JDBC 드라이버는 DriverManager 클래스를 통해 로딩된다. (자동 가능)
  • Connection 객체 생성 : JDBC 드라이버가 정상적으로 로딩되면 DriverManager를 통해 데이터베이스와 연결되는 세션(Session)인 Connection 객체를 생성한다.
  • Statement 객체 생성 : Statement 객체는 작성된 SQL 쿼리문을 실행하기 위한 객체로 정적 SQL 쿼리 문자열을 입력으로 가진다.
  • Query 실행 : 생성된 Statement 객체를 이용하여 입력한 SQL 쿼리를 실행한다.
  • ResultSet 객체로부터 데이터 조회 : 실행된 SQL 쿼리문에 대한 결과 데이터 셋이다.
  • ResultSet, Statement, Connection 객체들의 Close : JDBC API를 통해 사용된 객체들은 생성된 객체들을 사용한 순서의 역순으로 Close 한다.

아래는 데이터를 추가하는 예제이다. 위 동작 흐름과 비교하며 코드를 보자.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class PostgresJdbcInsertExample {

    // JDBC URL, 사용자 이름, 비밀번호 설정
    private static final String URL = "jdbc:postgresql://localhost:5432/mydb"; // 데이터베이스 이름은 "mydb"로 가정
    private static final String USER = "postgres";
    private static final String PASSWORD = "password";

    public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            // 1. PostgreSQL JDBC 드라이버 로딩, 자동으로도 됨.
            Class.forName("org.postgresql.Driver");

            // 2. 데이터베이스 연결
            Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);

            // 3. SQL 삽입 쿼리 작성
            String insertQuery = "INSERT INTO users (name, email) VALUES (?, ?)";

            // 4. PreparedStatement 생성
            PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);

            // 5. 파라미터 설정
            preparedStatement.setString(1, "John Doe");
            preparedStatement.setString(2, "johndoe@example.com");

            // 6. 쿼리 실행
            int rowsAffected = preparedStatement.executeUpdate();

            // 7. 실행 결과 확인
            if (rowsAffected > 0) {
                System.out.println("데이터가 성공적으로 삽입되었습니다.");
            }

        } catch (ClassNotFoundException e) {
            System.out.println("PostgreSQL JDBC 드라이버를 찾을 수 없습니다.");
            e.printStackTrace();
        } catch (SQLException e) {
            System.out.println("데이터베이스 연결 또는 쿼리 실행 중 오류가 발생했습니다.");
            e.printStackTrace();
        } finally {
            // 8. 자원 해제
            try {
                if (preparedStatement != null) preparedStatement.close();
                if (connection != null) connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

코드 자체는 어렵지 않으나 리소스 해제, 쿼리 작성 등의 보일러 플레이트가 많아 보인다.

JDBC Template

JDBC Template 은 JDBC 의 복잡한 리소스 관리와 반복적인 쿼리 작성을 단순화해주는 추상화 레이어이다.

위에 JDBC 예제와 같은 유저를 추가하는 코드가 아래와 같이 바뀐다.

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public int addUser(String name, String email) {
        String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
        return jdbcTemplate.update(sql, name, email);
    }
}

보일러 플레이트는 많이 줄일 수 있었지만 결국 쿼리를 직접 작성해야하고 자바 객체와 데이터베이스 테이블 간 매핑과 객체지향적으로 코드를 작성하기는 어렵다.

JPA

이제 ORM의 등장이다. JPA 는 자바 객체와 관계형 데이터베이스 테이블 간의 매핑을 지원하는 자바 표준 ORM 이다. JPA는 표준 인터페이스이고 Hibernate 등의 구현체가 실제로 동작을 한다.

그리고 JPA 는 JDBC 위에서 동작한다. JPA 내부적으로 JDBC 를 사용해 쿼리를 실행한다.

기술 역할
JPA 자바 객체와 데이터베이스 테이블 간의 매핑을 처리하는 고수준의 API. SQL을 자동으로 생성하고, 객체 중심의 데이터베이스 접근을 가능하게 함.
JDBC 데이터베이스와 직접 통신하는 저수준의 API. SQL 쿼리를 실행하고 데이터베이스와 상호작용.

JPA를 사용하려면 Entity, Entity Manager, 영속성 컨텍스트 등의 주요 개념을 알아야한다.

Entity 는 JPA 에서 데이터베이스 테이블에 대응하는 자바 클래스이다. 테이블의 구조를 나타내고 각 인스턴스가 행(row)이 된다.

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    // Getters and setters...
}

Entity Mnager는 Entity의 CRUD 작업과 트랜잭션을 관리하는 핵심 인터페이스이다.

private final EntityManager entityManager;

public void saveUser(User user) {
    entityManager.persist(user);  // 새로운 엔티티 저장
}

public User findUser(Long id) {
    return entityManager.find(User.class, id);  // 엔티티 조회
}

@Transactional // 트랜잭션 관리
public void updateUser(User user) {
    entityManager.merge(user);  // 엔티티 업데이트
}

영속성 컨텍스트는 Entity Manager 에 의해 관리되는 환경이다. 데이터베이스와 자바 어플리케이션 사이의 저장 공간이라고 이해하면 된다. 영속성 컨텍스트에 저장된 Entity는 영속, 비영속, 준영속 등의 상태를 가지는데 이 상태를 이용해서 데이터베이스와의 효율적인 통신을 가능하게 해준다.

영속성 컨텍스트의 자세한 내용과 JPQL, 관계 매핑 등의 다른 개념들은 다른 글에서 정리해보겠다.

Spring Data JPA

Spring Data JPA 는 JPA를 더 쉽게 사용할 수 있게 해주는 추상화 계층이다. 기본적인 CRUD의 자동화와 복잡한 쿼리도 쉽게 구현을 할 수 있다.

이를 가능캐하는 핵심이 JpaRepository Interface 이다. JpaRepository는 여러 Interface를 상속 받아서 만들어진다.

Repository<T, ID>

public interface Repository<T, ID> {
    // 아무런 메서드도 정의되지 않은 마커 인터페이스
}

CrudRepository<T, ID>

모든 엔티티에 대한 기본 CRUD 작업을 처리할 수 있는 메서드를 제공한다.

public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);

    <S extends T> Iterable<S> saveAll(Iterable<S> entities);

    Optional<T> findById(ID id);

    boolean existsById(ID id);

    Iterable<T> findAll();

    Iterable<T> findAllById(Iterable<ID> ids);

    long count();

    void deleteById(ID id);

    void delete(T entity);

    void deleteAllById(Iterable<? extends ID> ids);

    void deleteAll(Iterable<? extends T> entities);

    void deleteAll();
}

앞에 List가 붙은 인터페이스도 있는데 이는 List로 엔티티를 다룰 수 있다.

public interface ListCrudRepository<T, ID> extends CrudRepository<T, ID> {
    <S extends T> List<S> saveAll(Iterable<S> entities);

    List<T> findAll();

    List<T> findAllById(Iterable<ID> ids);
}

PagingAndSortingRepository<T, ID>

Pageable 객체와 Sort 객체를 사용해서 페이징 처리와 정렬 작업을 쉽게 도와주는 Interface

public interface PagingAndSortingRepository<T, ID> extends Repository<T, ID> {
    Iterable<T> findAll(Sort sort);

    Page<T> findAll(Pageable pageable);
}

QueryByExampleExecutor

QueryByExampleExecutor은 Example 객체를 사용해 동적 쿼리를 쉽게 작성하게 해주는 기능이다. Example 문자 그래도 예시라는 의미로 받아들이면 된다. 예시 엔티티를 만들어서 그거와 같은 모양의 데이터를 뽑아 올 수 있는 것이다.

public interface QueryByExampleExecutor<T> {
    <S extends T> Optional<S> findOne(Example<S> example);

    <S extends T> Iterable<S> findAll(Example<S> example);

    <S extends T> Iterable<S> findAll(Example<S> example, Sort sort);

    <S extends T> Page<S> findAll(Example<S> example, Pageable pageable);

    <S extends T> long count(Example<S> example);

    <S extends T> boolean exists(Example<S> example);

    <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}

이 모든 인터페이스를 상속한 것이 JpaRespository를 이다.

JpaRespository를 상속하는 UserRepository를 만들면 User 테이블에 접근하는 계층이 만들어진다.

public interface UserRepository extends JpaRepository<User, Int> {
}

이제 이 유저 레포지토리를 사용하여 위에서 본 다양한 데이터베이스 접근을 할 수 있다. Example 관련 예제를 하나 살펴보자.

public List<User> findUsersByExample(String name, String email) {
    // 검색 조건이 되는 User 객체를 생성 (비어있지 않은 필드만 검색에 사용됨)
    User userProbe = new User();
    userProbe.setName(name);
    userProbe.setEmail(email);

    // Example 객체 생성
    Example<User> example = Example.of(userProbe);

    // Example 객체를 사용해 검색
    return userRepository.findAll(example);
}
SELECT 
    user.id, 
    user.name, 
    user.email, 
    user.age 
FROM 
    user 
WHERE 
    user.name = 'test' 
    AND user.email = 'test@example.com';

이런 쿼리가 날아가게 된다.

하나 신기한것은 직접 작성한 UserRepository 또한 Interface 라는 부분이다. 구현체가 없다.
Spring Data JPA는 JpaRepository 인터페이스를 상속받는 인터페이스를 만나면 이를 구현한 클래스를 동적으로 생성하고 이 클래스를 빈으로 등록해준다.

SimpleJpaRepository 라고 하는 클래스를 기반으로 생성이 된다.

@Repository
@Transactional(
    readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    //... 생략
    private final JpaEntityInformation<T, ?> entityInformation;
    private final EntityManager entityManager;

    //... 생략

     @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null");
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return this.entityManager.merge(entity);
        }
    }

    //... 생략

save 메서드만 가져와봤는데, 내부적으로 엔티티 매니저를 사용하는 것을 볼 수 있다. (JPA 기반)

JpaRespository 는 쿼리 메서드라고 강력한 기능을 제공하는데 이 기능은 메서드의 이름을 파싱하고 분석해서 쿼리를 직접 생성하는 기능이다.

public interface UserRepository extends JpaRepository<User, Int> {
    Optional<User> findByEmail(String email)
}

이렇게 메서드를 추가하면 위 인터페이스의 구현체를 생성할 때 이름을 파싱해서 JPQL 또는 SQL 쿼리로 변환한다.

SELECT u FROM User u WHERE u.email = :email

간단한 쿼리부터 복잡한 쿼리까지 개발자가 쿼리를 직접 작성하지 않고 데이터베이스와 통신이 가능해졌다.

그림을 통해 요약하면 아래와 같은 형태가 될 것이다.

'Java Spring' 카테고리의 다른 글

웹 기술의 발전과정  (0) 2024.11.12
Specification 과 Criteria API  (0) 2024.11.08
JPA와 하이버네이트  (2) 2024.10.24
Transactional 원리  (0) 2024.09.24