Dev/Spring

[Spring] JPA

syuare 2025. 5. 15. 22:07

지난 JDBC를 이용하여 프로젝트를 진행하면서 기존 Spring Boot에 직접 DB를 연결하고 테이블도 생성하며, JDBC를 활용하여 SQL 쿼리로 DB를 다루는등 개발자가 직접 많은 것을 해야되었다.

또한 이 과정에서 반복되는 코드가 너무 많기도 하고 객체 지향 설계에서도 멀어져갔다.

 

그래서 도입된 기술이 바로 JPA (Java Persisterce API) 이다.


JPA (Java Persisterce API)

Java 어플리케이션에서 객체 지향 프로그래밍 모델을 사용하여 DB와 상호작용할 수 있도록 해주는 기술이다. 

  • 객체 지향 프로그래밍에서 객체를 DB 테이블로 매핑해주는 역할
  • 객체와 DB 간의 데이터를 자동으로 변환해주는 도구
    • 대충 Java 어플리케이션에서 데이터 베이스(DB)와 상호작용 가능하도록 도와주는 기술로 이해해도 될 듯하다.

주요 개념

객체와 테이블 매핑 (ORM / Object-Relational Mapping)

  • JPA는 Java에서 객체를 DB 테이블에 맞게 변환한다.
  • Java 클래스는 DB 의 테이블과 연결되고, 클래스의 속성은 테이블의 컬럼(column)과 매핑된다.

참고로 ORM 개념을 Java언어로 접목시킨 것이기 때문에 JPA라고 부르며, 다른 언어들 마다 각자 부르는 ORM 명칭이 있다.

  • JavaScript: Sequelize
  • Python: SQLAlchemy

Entity

  • DB 의 테이블에 대응되는 자바 클래스를 말한다.
    • DB 의 레코드를 객체로 변환한 것
    • (예시) Schedule 클래스 = schedules 테이블에 대응되는 Entity
@Entity
@Table(name = "schedules")
public class Schedule {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private LocalDateTime startTime;
    private LocalDateTime endTime;

    // getters and setters
}

Repository

  • DB 의 CRUD 작업을 처리하는 인터페이스로 JPA에서는 JpaRepository를 상속받아 구현할 수 있다.
  • JpaRepository를 상속받음에 따라 기본적인 CRUD 기능 작업을 간단하게 처리할 수 있다.
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
    // 필요한 추가 메서드를 정의할 수 있습니다.
}

Persistence Context

  • JPA는 DB 와의 연결을 Persistence Context라는 개념으로 관리한다
  • DB 와의 연결을 통해 어플리케이션에서 사용되는 Entity 객체를 관리하고 이 객체들이 DB와 어떻게 연관되는지 객체 상태를 추적한다
    • 객체가 수정되면 DB에 자동으로 반영되도록 도와준다.
더보기

Persistence Context ?

 

JPA에서 관리되는 영속성 상태를 추적하는 저장소를 말한다.

 

*영속성: 프로그램 종료 후에도 데이터가 사라지지 않도록 지속적으로 유지되는 특성

 

Persistence Context 는 EntityMangaer가 관리하며, 트랜잭션 단위로 존재한다

  • Entity 객체가 Persistence Context 에 들어가면, 해당 객체는 영속 상태(Managed 상태)로 변경된다
  • 객체는 DB와 연동되어 JPA가 자동으로 변경 사항을 추적하고 필요에 따라 DB에 반영한다
// 엔티티 객체 생성
Book book = new Book();
book.setTitle("Java Programming");
book.setAuthor("John Doe");

// Persistence Context에 저장된 엔티티 객체
entityManager.persist(book);

 

entityManager.persist(book)는 book 객체를 Persistence Context에 추가하며,

이제 해당 객체는 영속성 상태로 관리되어서 이후 변경 사항은 자동으로 DB 반영할 수 있다.

특징

Persistence Context는 Entity 객체를 즉시 캐시한다.

  • 동일한 Entity를 여러 번 조회할 경우 DB를 조회하지 않고
  • Persistence Context 내에서 이미 존재하는 Entity를 반환한다.
Book book1 = entityManager.find(Book.class, 1L);  // 첫 번째 조회
Book book2 = entityManager.find(Book.class, 1L);  // 두 번째 조회
System.out.println(book1 == book2);  // true, 같은 엔티티 객체를 반환

 

Persistence Context는 영속성 상태로 관리되는 Entity 객체의 상태를 추적하고 반영된다.

  • Entity 객체 값을 수정하면 그 변경 사항이 자동으로 DB에 반영될 수 있다.
book.setTitle("Advanced Java Programming");  // 엔티티 객체 수정
// 트랜잭션 커밋 시 자동으로 데이터베이스에 반영

 

Persistence Context는 트래잭션 단위로 관리된다. 

  • 트랜잭션이 커밋되면  Persistence Context 내의 모든 변경사항이 DB에 반영된다.
  • 트랜잭션이 롤백되면 변경사항이 모두 취소 된다.

EntityManager

객체를 저장하거나 조회하는데 사용된다. (JPA 핵심 객체)

보통은 JpaRepository가 내부적으로 처리한다.

더보기

EntityManager ?

 

DB와 Entity 객체 간의 상호작용을 관리하는 역할을 하는데,

Entity를 저장 / 수정 / 삭제 / 조회하는 작업을 담당한다.

 

EntityManager는 

  • Persistence Context를 통해 엔티티의 상태를 추적하고, 데이터베이스와의 동기화를 관리한다.
  • CRUD 작업을 위해 다양한 메서드를 제공한다.
    • persist(Object entity): 신규 Entity를 Persistence Context에 추가한 후 DB에 저장
    • find(Class<T> entityClass, Object primaryKey): 주어진 기본 키(Primary Key)를 사용하여 Entity를 조회
    • remove(Object entity): Entity 객체를 Persistence Context 에서 제거한 후 해당 Entity 삭제
    • merge(Object entity): Entity 객체 갱신 ( Persistence Context에 이미 존재하는 Entity를 업데이트할 때 사용)
    • flush(): 변경된 Entity를 즉시 DB에 반영하도록 강제로 내보낸다. (트랜잭션이 커밋될 시 자동으로 flush()가 호출)
// 엔티티 객체 저장
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();

// 새로운 책 객체 저장
Book book = new Book();
book.setTitle("Java Programming");
book.setAuthor("John Doe");
entityManager.persist(book);  // Persistence Context에 저장

entityManager.getTransaction().commit();  // 트랜잭션 커밋 후 데이터베이스에 반영

Persistence Context와 EntityManager의 관계

  • Persistence Context는 EntityManger가 관리한다 > 트랜잭션 범위 내에서 Entity 객체를 관리한다
  • Entity 객체가 Persistence Context에 존재하는 동안 영속성 상태로 간주되며, 변경 사항은 자동으로 추적된다.
  • EntityManage는 객체들의 상태를 변경하고, 해당 변경 사항을 DB에 반영하는 역할을 담당한다.
  • 트랜잭션이 커밋되면 Persistence Context 내 모든 변경 사항을 실제 DB에 반영한다.

예시 코드

더보기

간단한 책 관리 앱

  • 책을 저장 / 조회 / 수정 / 삭제 > CRUD 기능을 가진 앱
  • Book이라는 Entity를 사용하여 책 정보를 관리

1. Book Entity 클래스

책 정보를 나타내는 Book 엔티티 클래스로 DB books 테이블과 매핑된다.

import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;

@Getter
@Setter
@Entity
@Table(name = "books")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String author;
    private int yearPublished;
}

2. BookRepository 인터페이스

Jparepository를 상속받아, 기본적인 CRUD 작업을 자동으로 제공한다

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
    // 필요에 따라 추가적인 쿼리 메서드를 정의할 수 있습니다.
}

3. BookService 클래스

비즈니스 로직을 담당하는 클래스로 책을 추가 / 조회 / 수정 / 삭제 메서드를 구현한다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class BookService {
    @Autowired
    private BookRepository bookRepository;

    // 책 추가
    public Book createBook(Book book) {
        return bookRepository.save(book);
    }

    // 책 조회
    public Optional<Book> getBook(Long id) {
        return bookRepository.findById(id);
    }

    // 모든 책 조회
    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    // 책 삭제
    public void deleteBook(Long id) {
        bookRepository.deleteById(id);
    }

    // 책 수정
    public Book updateBook(Long id, Book newBookData) {
        Book book = bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found"));
        book.setTitle(newBookData.getTitle());
        book.setAuthor(newBookData.getAuthor());
        book.setYearPublished(newBookData.getYearPublished());
        return bookRepository.save(book);
    }
}

4. BookController 클래스

HTTP 요청을 처리한다.

책을 추가 / 조회 /삭제 / 수정하는 API 엔드포인트를 제공한다

 

*API 엔드포인트: API를 통해 제공되는 특정한 URL 주소 > Mapping 어노테이션으로 지정하는 URL을 의미한다.

*(ex) /books, /books/{id})

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private BookService bookService;

    // 책 추가
    @PostMapping
    public Book createBook(@RequestBody Book book) {
        return bookService.createBook(book);
    }

    // 책 조회
    @GetMapping("/{id}")
    public Book getBook(@PathVariable Long id) {
        return bookService.getBook(id).orElseThrow(() -> new RuntimeException("Book not found"));
    }

    // 모든 책 조회
    @GetMapping
    public List<Book> getAllBooks() {
        return bookService.getAllBooks();
    }

    // 책 삭제
    @DeleteMapping("/{id}")
    public void deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
    }

    // 책 수정
    @PutMapping("/{id}")
    public Book updateBook(@PathVariable Long id, @RequestBody Book newBookData) {
        return bookService.updateBook(id, newBookData);
    }
}

5. Spring Boot 설정

application.properties 파일에 DB 연결 설정을 추가한다 

(MySQL 사용 예시)

spring.datasource.url=jdbc:mysql://localhost:3306/bookstore
spring.datasource.username=username
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

6. MySQL 테이블 생성

위와 같이 Book Entity가 정의되어 있을 경우 Spring Boot가 어플리케이션 실행 시 자동으로 books 테이블을 생성한다.

  • ddl-auto=update 설정에 의해 테이블과 컬럼이 자동으로 생성된다.

JPA 장점

  • 객체와 DB 간의 변환을 자동으로 처리해준다.
    • 이로 인해 DB 데이블을 매핑할 필요없이 Java 객체로 쉽게 처리할 수 있다.
  • 복잡한 쿼리없이 CRUD 작업을 자동으로 처리할 수 있다.
    • 필요하다면 커스텀 쿼리를 작성하는 것도 가능하다
  • JPA가 DB와의 상호작용을 담당해주기 때문에 개발자는 비즈니스 로직에만 집중할 수 있다.

JPA 단점

객체 지향적인 접근 방식과 자동화된 DB 연동과 같은 많은 장점이 있지만 단점도 분명히 존재한다.

 

1) JPA는 처음 접할 경우 학습하기가 쉽지 않다.

  • 객체와 관계형 DB간의 매핑을 자동화하는 과정에서 JPA 개념 (Entity 상태, Persistence Context 등)을 잘 이해해야 한다.
  • JPA를 잘 사용하려면 ORM의 원리와 다양한 설정을 이해해야 하기 때문에 기본적인 CRUD 이상의 작업을 할 때는 학습에 시간이 많이 필요할 수 있다.

2) 복잡한 쿼리로 코드 작성이 어려울 수 있다.

  • 기본적인 CRUD 작업은 매우 간편하게 할 수 있지만, 복잡한 쿼리를 작성할 때는 쉽지만은 않을 수 있다.
    • 복잡한 JOIN이나 집계 함수 등 사용 시 SQL처럼 직관적인 방식으로 쿼리를 작성하기 어려울 수 있다.
  • 이로 인해 복잡한 쿼리를 작성해야할 경우 SQL을 그대로 사용하는 방법이나 JPA의 @Query를 사용하는 방법도 있다고 한다.
// Native Query 예시
@Query(value = "SELECT * FROM orders o WHERE o.customer_id = ?1", nativeQuery = true)
List<Order> findOrdersByCustomerId(Long customerId);

 

3) 성능 문제 > N+1 쿼리 문제

  • 연관된 Entity를 조회 시도할 경우 쿼리가 너무 많이 발생한다
  • 예를 들면
    • Order 엔티티와 OrderItem  엔티티가 있을 때
    • 1회의 Order 조회 후 각 Order마다 그에 해당하는 OrderItem을 조회하는 방식으로 데이터를 가져올 경우 
    • Order가 10개 있다면 실제로 1개의 쿼리 + OrderItme을 위한 10개의 추가 쿼리가 실행되기 때문에 100번의 쿼리가 발생하게 된다.
  • 이처럼 실제로 연관된 객체들이 많이 을 때 쿼리 수가 급증하는 문제가 발생할 수 있다.
    • Fetch Join, @EntityGraph, @Query 등으로 성능 개선이 가능하다고 한다 (다음 시간에..)

4) 추상화 수준이 높다

  • JPA는 추상화된 방법으로 DB와의 상호작용을 처리하는데, 
  • 이러한 처리로 인해 SQL 쿼리의 세부 동작을 직접 확인하기가 어렵다. 즉, 디버깅이 어렵다.
    • 어떤 쿼리가 실행되는지 정확히 알고 싶을 때는 로그를 통해 SQL 쿼리를 출력하도록 설정해야하는데
    • 기본적으로 JPA는 SQL 실행을 추상화하여 처리하기 때문에 디버깅 시 문제가 발생할 수 있다.
  • 또한 이와 같은 추상화 방법으로 DB 상호작용을 하다보니 DB의 특화 기능을 직접적으로 활용하기 어렵다. (DBMS 고유한 기능, 인덱스,나 저장 프로시저를 활용한 최적화 방법 등)

5) 대용량 데이터 처리 시 메모리 문제가 발생할 수 있다.

  • JPA는 기본적으로 Entity 객체를 메모리에 로딩하기 때문에 대용량 데이터를 처리 시 메모리 부족 문제가 발생할 수 있다.
    • findAll()로 모든 데이터 조회 시 모든 데이터를 조회하기 위해 많은 메모리를 사용하게 되어 메모리 문제나 성능 저하가 발생할 수 있다.

단점이 많아 보이지만 이것을 해결할 수 있는 방법도 많고

JPA를 사용할 경우 개발 효율성과 유지보수 측면으로 봤을 때 이러한 단점을 덮을 정도로

JPA의 장점이 매우 크고 강력하기 때문에 현재 백엔드 개발에는 JPA를 적극 사용하고 있다 볼 수 있다.