Post

JPA와 영속성

ORM이 도입되기 이전에는, 객체지향 프로그래밍 언어와 관계형 데이터베이스 간의 매핑을 개발자가 직접 구현하였다.

주로 JDBC를 이용하여 데이터베이스와 연결하여 쿼리를 실행하고 결과를 가져오는 방식이다.

Connection 으로 데이터베이스 연결을 설정하고 관리하였고, 개발자는 쿼리를 직접 작성하여 데이터베이스에 보낸 후 반환된 결과를 받아 자바 객체로 매핑하는 작업을 수동을 하였다.

이 과정에서 SQL 실행 중 발생할 수 있는 예외를 처리하는 것도 개발자가 해야 했다.

이러한 방식은 SQL을 사용하여 데이터베이스와 상호작용하는 데 반복적인 코드 작성을 요구하였고, 쿼리를 직접 다루는 과정에서 다양한 오류가 발생할 수 있다. 또한, 객체 지향 언어의 클래스와 객체를 관계형 데이터베이스의 테이블과 매핑하는 과정에서 일관성을 유지하기가 어렵다.

이러한 문제를 해결하기 위해 등장한 것이 ORM 이다.




ORM 프레임워크

ORM은 객체와 데이터베이스 간의 매핑을 자동화하여 개발자가 더 적은 코드로 쉽고 간편하게 데이터베이스와 상호작용할 수 있도록 도와준다.


객체와 데이터베이스 매핑 자동화

  • ORM 프레임워크는 객체와 데이터베이스 간의 매핑을 자동으로 처리한다.

CRUD 작업의 객체지향적 처리

  • CRUD(Create, Read, Update, Delete) 작업을 객체 지향적으로 처리할 수 있다.

표준화된 인터페이스 제공

  • JPA와 같은 ORM프레임워크는 표준화된 인터페이스를 제공하여, 다양한 데이터베이스와도 쉽게 연동할 수 있다.

성능 최적화 및 편의성 제공

  • 내부적으로 캐싱, 지연로딩 등의 기법을 사용하여 성능을 최적화하고, 개발자가 데이터베이스와의 상호작용을 편리하게 처리할 수 있다.




JPA

JPA는 개발자가 객체지향적으로 데이터를 다루면서 이것을 관계형 데이터베이스에 저장하고 조회할 수 있도록 준다.

객체와 테이블 간의 매핑 규칙을 정의하고 이를 바탕으로 JPA 구현체가 SQL을 생성하여 데이터베이스와 상호작용한다.

ORM을 통해 객체와 데이터베이스 간의 매핑을 자동화하기 때문에 개발자는 객체지향적인 코드를 집중적으로 작성할 수 있다.

데이터베이스 스키마의 변경이 발생해도 JPA가 자동으로 SQL을 생성하여 매핑을 처리해주기 때문에 유지보수가 편리하다.

구성

  • 엔티티
    • 영속성을 가진 객체
    • 데이터베이스 테이블과 연결된 자바 클래스로 각 인스턴스는 테이블의 행을 나타낸다.
  • 엔티티 매니저
    • 엔티티의 생명주기를 관리하고 데이터베이스와 상호작용한다.
    • 엔티티를 저장, 삭제, 조회하는 작업을 한다.
  • 영속성 컨텍스트
    • 엔티티 객체를 관리하는 메모리상의 공간
    • 데이터베이스와의 상호작용을 중재하는 역할을 한다.
  • JPQL
    • 엔티티 객체를 대상으로 쿼리를 작성하는 언


영속성(Persistence)이란?

데이터가 일시적인 저장소가 아닌 영구적인 저장소에 저장되어, 프로그램 종료 후에도 데이터가 손실되지 않고 계속 유지되도록 하는 것을 의미한다.

  • 영속성을 통해 프로그램이 종료되더라도 데이터가 손실되지 않고 유지될 수 있으며, 시스템의 오류나 충돌이 발생했을 때 데이터를 복구할 수 있다.
  • 여러 프로그램이나 사용자가 동일한 데이터를 사용할 수 있으며 트랜잭션을 통해 데이터의 일관성을 유지할 수 있다.

ex)

  • 데이터를 데이터베이스에 저장하면 프로그램이 종료되더라도 데이터가 유지된다.
  • 데이터를 파일에 저장하면 프로그램이 종료되더라도 데이터가 파일에 남아있는다.



JPA에서의 “영속성”은 엔티티 객체의 생명 주기 중 하나로,
엔티티 객체가 영속성 컨텍스트에 의해 관리되는 상태를 말한다.

엔티티 객체를 영속성 컨텍스트에 저장하고 이를 통해서 데이터베이스와의 지속적인 동기화를 유지한다.

자바에서는 JPA를 사용하여 영속성을 관리할 수 있으며, 객체를 데이터베이스 테이블에 쉽게 매핑하고 CRUD 작업을 간편하게 수행할 수 있다.




엔티티 객체의 생명 주기

  • 비영속 (Transient) 엔티티 객체가 영속성 컨텍스트에 의해 관리되지 않은 상태로 데이터베이스와 연관이 없다.

  • 영속 (Persistent) 엔티티 객체가 영속성 컨텍스트에 의해 관리되는 상태로 데이터베이스에 저장된다.
    • 엔티티 매니저를 통해 저장된 객체
  • 준영속 (Detached)
    한 번 영속상태 였지만 현재는 영속성 컨텍스트에 의해 관리되지 않는 상태
    영속성 컨텍스트가 닫히거나 detch() 메서드를 호출하여 엔티티가 분리될 때 발생한다.
    • 세션이 종료된 후 메모리에 남아있는 객체
  • 삭제 (Removed) 엔티티 객체가 영속성 컨텍스트에 의해 삭제된 상태로 데이터베이스에서 해당 엔티티가 삭제된다.
    • 엔티티 매니저의 remove() 메서드 호출 후 상태

이러한 생명 주기로 인해 JPA는 객체와 데이턴베이스 간의 일관성을 유지할 수 있다.




주요 어노테이션

  • @Entity :
    • 해당 클래스가 엔티티임을 선언한다.
    • 데이터베이스의 테이블에 매핑되는 자바 객체를 정의할 때 사용한다.
  • @Table :
    • 엔티티가 매핑될 데이터베이스 테이블의 이름과 기타 속성을 지정한다.
    • 기본적으로 클래스 이름과 같은 테이블 이름을 사용한다.
  • @Id :
    • 엔티티의 기본키를 지정한다.
    • 데이터베이스에서 각 엔티티의 고유성을 보장한다.
  • @GeneratedValue :
    • 기본키 값을 자동으로 생성하는 전략을 지정한다.
    • GenerationType.IDENTITY, GenerationType.SEQUENCE 등
  • @Column :
    • 엔티티 필드와 매핑되는 데이터베이스 컬럼을 지정한다.
    • 컬럼의 이름, 길이, Nullable 여부 등 다양한 속성을 저장할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Table(name = "user")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter @Setter
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id", updatable = false)
    private Long id;

    private String email;

    private String password;

    private String phone;
    
}

JPA는 자동으로 엔티티와 데이터베이스 테이블 간의 매핑을 처리하기때문에 개발자는 별도의 SQL 쿼리를 작성할 필요가 없다.




영속성 컨텍스트

엔티티 매니저가 관리하는 엔티티 객체들의 집합

엔티티를 관리하고 데이터베이스와의 통신을 처리한다.
이 영속성 컨텍스트는 엔티티 매니저의 생명주기와 일치하며, 하나의 트랜잭션 단위로 관리된다.

영속성 컨텍스트는 엔티티의 상태를 추적하고, 데이터베이스와의 동기화를 수행하며, 엔티티 객체를 캐시로 관리하여 데이터베이스와의 상호작용을 최적화한다.

엔티티 매니저

  • 영속성 컨텍스트를 생성하고 관리하며, 영속성 컨텍스트에 있는 엔티티에 대해 CRUD(Create, Read, Update, Delete) 작업을 수행한다.

트랜잭션 관리

  • 하나의 영속성 컨텍스트는 일반적으로 하나의 트랜잭션에 매핑된다.
  • 즉, 트랜잭션이 시작되면 영속성 컨텍스트가 생성되고, 트랜잭션이 완료됨면 영속성 컨텍스트가 닫히고 데이터베이스와 동기화가 이루어진다.

상태 변화 추적

  • 엔티티의 상태를 추적하여, 트랜잭션이 커밋될 때 변경된 내용을 자동으로 데이터베이스에 저장한다.

캐싱

  • 엔티티 객체를 1차 캐시로 관리하여 같은 트랜잭션 내에서 동일한 엔티티에 대한 중복 조회를 방지한다. 이를 통해 데이터베이스 접근을 최적화할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
@Transactional
public class UserService {
    
    @PersistenceContext
    private EntityManager em;
    
    public void saveUser(User user) {
        // 엔티티를 영속성 컨텍스트에 저장
        em.persist(user);
    }
    
    public User findUser(Long userId) {
      // 1차 캐시에서 엔티티를 조회 (데이터베이스 조회 필요 없음)
      // em.find(User.class, user.getId()) 는 JPA 에서 제공하는 메서드로 데이터베이스에 접근하여
      // 지정된 엔티티 클래스와 키를 기반으로 엔티티를 조회한다.
      return em.find(User.class, userId);
      
    }
    
    public void updateUser(User user) {
      // 엔티티의 상태 변경 (Dirty Checking)
      User managedUser = em.find(User.class, user.getId());
      managedUser.setUsername(user.getUsername());
      managedUser.setEmail(user.getEmail());

      // 트랜잭션이 커밋될 때 데이터베이스에 자동으로 반영되어
      // 반복적으로 조죄할 때 데이터베이스에 대한 추가적인 조회를 피할 수 있으며 성능이 향상된다.
    }    
    
    public void deleteUser(Long userId) {
        // 1캐시는 영속성 컨텍스트의 생명주기에 의존하기 때문에 트랜잭션이 종료되면 1차 캐시도 함께 종료된다. 
        // 엔티티를 영속성 컨텍스트에서 제거
        // 이후에 같은 엔티티를 조회할 경 다시 데이터베이스에서 조회해야 한다.
        User user = em.find(User.class, userId);
        em.remove(user);
        // 트랜잭션이 커밋될 때 데이터베이스에서 삭제됨
    }
}



1차 캐시

영속성 컨텍스트의 일부로 JVM 메모리에 위치하기 때문에, 데이터베이스에 접근하는 것보다 훨씬 빠른 접근이 가능하다.

엔티티의 식별자를 키로 사용하여 엔티티를 저장하며, 식별자를 사용하여 빠르게 엔티티를 조회할 수 있다.

1
2
3
4
    public User findUser(Long userId) {
        // 1차 캐시에서 엔티티를 조회 (데이터베이스 조회 필요 없음)
        return em.find(User.class, userId);
    }

em.find(User.class, user.getId())메서드는 데이터베이스에서 엔티티를 조회하고 이 엔티티를 영속성 컨텍스트에 저장한다.

이때 저장된 엔티티는 1차 캐시에 저장되며, 이후 동일한 엔티티를 다시 조회하는 경우, 영속성 컨텍스트에서 먼저 조회하여 1차 캐시에 저장된 엔티티를 반환한다.

1차 캐시를 통해 동일한 엔티티를 반복적으로 조회할 때 데이터베이스에 대한 추가적인 조회를 하지 않는다. 영속성 컨텍스트는 트랜잭션 범위 내에서 관리되기 때문 동일한 엔티티에 대해 일관된 상태를 유지할 수 있다.

하지만 컨텍스트의 생명주기에 의존하기 때문에 트랜잭션이 종료되면 1차 캐시도 함께 종료된다. 따라서 이후 같은 엔티티를 조회할 때는 다시 데이터베이스에서 조회해야 한다.

1차 캐시는 메모리에 저장되기 때문에 많은 수의 엔티티를 동시에 처리할 경우 메모리 사용에 주의해야 한다.



지연 로딩

연관된 엔티티나 컬렉션을 실제로 사용할 때까지 데이터베이스에서 로딩하지 않고 필요한 시점에 로딩하는 기능이다.

지연로딩은 FetchType.LAZY로 설정하며, 실제 엔티티가 필요한 시점에 데이터베이스에서 조회한다.

반면에 즉시로딩(FetchType.EAGER)은 엔티티를 조회할 때 연관된 엔티티들을 즉시 한번에 조회한다.
이는 성능 저하를 초래할 수 있으므로 주의해서 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id")
    private User user;

}

단순한 관계나 적은 데이터 양을 다룰 때는 즉시 로딩을 사용하는 것이 좋으며, 대규모 데이터를 다루거나 성능 최적화가 필요한 경우에는 지연로딩을 고려해야 한다.

기본적으로는 지연로딩을 사용하고, 성능 문제나 데이터 접근 패턴에 따라 필요할 때 즉시로딩으로 변경하는 것이 좋다.



변경 감지

트랜잭션 내에서 엔티티의 상태 변경을 감지하여 자동으로 데이터베이스와 동기화한다.

트랜잭션 내에서 엔티티의 수정이 일어나면 변경 감지가 동작하며, 트랜잭션이 커밋될 때 변경 사항이 데이터베이스에 반영된다. 트랜잭션이 커밋되기 전까지는 데이터베이스에 변경 사항이 반영되지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@PersistenceContext
private EntityManager entityManager;

public void updateProduct(Long productId, String newName, double newPrice) {
    // 엔티티 조회
    Product product = entityManager.find(Product.class, productId);

    // 엔티티 수정
    product.setName(newName);
    product.setPrice(newPrice);

    // 변경 감지가 발생하여 자동으로 데이터베이스에 반영됨
}

별도의 save 메서드 호출이 필요하지 않다.



트랜잭션 범위

영속성 컨텍스트는 트랜잭션 범위 내에서 엔티티를 관리하기 때문에, 트랜잭션이 커밋되거나 롤백되기 전까지는 엔티티의 변경 사항을 추적하고, 커밋하는 시점에 최종적으로 데이터베이스에 반영한다.

This post is licensed under CC BY 4.0 by the author.