Post

JPA

1. ORM이란

ORM(Object Relational Mapping)이란 RDB 테이블을 객체 지향적으로 사용하기 위한 기술이다. RDB 테이블은 객체 지향적 특징(상속, 다형성, 레퍼런스, 오브젝트 등)이 없고 자바와 같은 언어로 접근하기 쉽지 않다. 때문에 ORM을 사용해 오브젝트와 RDB 사이에 존재하는 개념과 접근을 객체 지향적으로 다루기 위한 기술이다.

2. SQL 중심 개발의 문제점

자바로 작성한 애플리케이션은 JDBC API를 이용해서 SQL을 데이터베이스에 전달한다. 데이터베이스는 객체 구조와는 다른 데이터 중심의 구조를 가지므로 개발자가 객체지향 애플리케이션과 데이터베이스 사이에서 SQL과 JDBC API를 이용해서 변환 작업을 해야 한다.

객체를 데이터베이스에 CRUD 하려면 너무 많은 SQL과 JDBC API 코드로 작성해야 한다는 문제점이 있다. 그리고 테이블마다 이런 비슷한 일을 반복해야 한다.

3. 패러다임 불일치

객체 지향 프로그래밍(OOP)과 관계형 데이터베이스(RDB)의 데이터 표현 방식이 다른 문제를 패러다임의 불일치라고 한다. OOP와 RDB에서 데이터를 표현하는 방식이 다른 이유는 애초에 이들의 목표와 동작 방식이 다르기 때문이다.

예를 들어, RDB에서는 데이터의 중복을 줄이고 및 일관성을 높이기 위해 하나의 데이터를 여러 개의 테이블로 쪼개어 저장하고 필요할 때 조인하여 사용한다. 그러나 OOP에서는 하나의 객체가 다른 객체에 대한 참조를 포함하며, 연관된 두 객체는 모두 메모리 상에 존재하기 때문에 하나의 객체로 이와 연관된 객체들의 데이터를 쉽게 얻을 수 있다.

1) 상속

객체는 상속이라는 개념이 있는 반면 테이블은 상속의 개념이 없다.

  • OOP 데이터 모델링

object-modeling

  • RDB 데이터 모델링

table-modeling

RDB에서 데이터를 가져올 때는 JOIN을 통해 가져온 데이터로 Car 객체를 생성해야 한다. 그러나 OOP에서는 단순히 list.add() 와 list.get() 명령어로 원하는 데이터를 저장하고 읽을 수 있다.

JPA 는 개발자가 OOP 스타일로 Car 객체를 저장하더라도 내부적으로 2개의 쿼리를 만들어 상속 관계를 RDB에 맞는 데이터로 변환하여 저장해준다.

2) 연관 관계

1
2
3
4
5
6
7
8
9
10
11
class Student {
    int studentId;
    String name;
    Class clazz;
}

class Class {
    int classId;
    String className;
    Teacher teacher;
}

OOP에서는 학생에 대한 객체가 반에 대한 객체를 포함한다. 즉, 참조를 가지고 있다. 그러나 RDB에서는 참조를 가지는 것이 아니라 반에 대한 참조를 외래키로 대체하고, 반에 대한 데이터를 분리하여 따로 저장해야 한다.

데이터 저장 시에 데이터를 분리하여 2개의 쿼리를 사용해야 하며, 다시 읽을 때는 조인을 통한 재조립이 필요하다. JPA는 이를 내부적으로 처리해주기 때문에 개발자는 OOP 스타일로 데이터를 저장하고 읽을 수 있게 된다.

3) 객체 그래프 탐색

  • 연관관계 그래프

relation-graph

1
person.getHouse().getCity().getCountry();

이렇게 연관된 객체를 얻기 위한 행위를 객체 그래프 탐색이라고 한다.

RDB에서 Person 데이터를 가져오려면 연관된 테이블과의 JOIN을 통해 가져와야 한다. 이때 정해진 쿼리문에 따라 탐색이 가능한 경계가 정해지게 된다. 이 경계를 넘는 객체를 얻어 사용하려 하면 Null Pointer Exception이 발생할 것이다.

처음 프로그램을 개발할 때는 Person에서 어디까지 그래프를 탐색해야 하는지 알 수 없다는 것이다. 연관된 모든 테이블을 조인하여 데이터를 가져올 수도 있다. 하지만 불필요하게 많은 테이블을 조인하여 모든 데이터를 가져오는 것은 좋은 방법이 아니다.

JPA는 지연 로딩을 사용하여 이 문제를 해결한다. 연관된 데이터가 필요할 때 데이터를 로딩하는 것이다.

4. JPA(Java Persistence API)란?

jpa

자바 퍼시스턴스 API 또는 자바 지속성 API(Java Persistence API, JPA)는 자바 플랫폼 SE와 자바 플랫폼 EE를 사용하는 응용프로그램에서 관계형 데이터베이스(RDB)의 관리를 표현하는 자바 API이다.

기존에 EJB에서 제공되던 엔터티 빈(Entity Bean)을 대체하는 기술이다. 자바 퍼시스턴스 API는 JSR 220에서 정의된 EJB 3.0 스펙의 일부로 정의가 되어 있지만 EJB 컨테이너에 의존하지 않으며 EJB, 웹 모듈 및 Java SE 클라이언트에서 모두 사용이 가능하다. 또한, 사용자가 원하는 퍼시스턴스 프로바이더 구현체를 선택해서 사용할 수 있다.

JPA는 ORM 표준 기술로 Hibernate, OpenJPA, EclipseLink, TopLink Essentials과 같은 구현체가 있고 이에 표준 인터페이스가 바로 JPA이다.

5. 동작 방식

  • 엔티티 생명주기

entity-lifecycle

1) 저장

엔티티는 생명주기를 갖는다. 엔티티에는 영속(Managed), 준영속(Detached), 비영속(Transient), 삭제(Removed) 이렇게 4가지 상태가 있으며 엔티티 매니저의 특정 메소드가 호출되거나 JPQL이 실행되면 상태가 변한다.

entityManager.persist(entity)의 형태로 엔티티를 저장하면 엔티티는 비영속(Transient) 상태에서 영속(Managed) 상태가 되며 영속성 컨텍스트 내에서는 다음과 같은 일이 일어난다.

  • persist() 수행시 내부 동작 과정

entity-save

① 엔티티 매니저의 persist() 메소드를 통해 엔티티가 영속성 컨텍스트에 들어온다.

② 엔티티가 영속성 컨텍스트의 1치 캐시에 저장되면서 초기 상태의 스냅샷이 따로 보관된다.

③ 엔티티를 DB에 저장하기 위한 쿼리가 자동 생성되어 쓰기 지연 SQL 저장소에 저장된다.

④ 트랜잭션을 커밋하면 내부적으로 먼저 flush() 를 수행하는데, 이는 쓰기 지연 SQL 저장소에 저장된 쿼리들을 DB 에 보내 수행하게 함으로써 영속성 컨텍스트와 DB의 상태의 동기화를 위한 것이다.

⑤ 트랜잭션을 커밋한다.

2) 조회

엔티티 매니저의 find() 메소드로 엔티티를 DB 에서 읽을 수 있다. 그러나 바로 DB에서 데이터를 찾는 것이 아니라 먼저 1차 캐시를 살펴본다.

1차 캐시에 해당 엔티티가 있으면 곧바로 이를 반환하고, 없다면 DB에서 데이터를 가져와 1차 캐시에 저장한 뒤 반환한다.

엔티티를 조회하는 다른 방법으로는 JPQL(Java Persistence Query Language)이 있다. JPQL은 SQL과는 달리 자바 객체에 대한 쿼리를 정의한다. 따라서 JPQL은 SQL에 대해서 전혀 모른다.

JPQL을 실행하기 전에 자동으로 flush()를 수행하고 JPQL을 실행하는데, 쓰기 지연 SQL 저장소에 있는 쿼리를 DB와 동기화해야 정상적인 결과가 나오기 때문이다.

3) 수정

저장과 삭제와는 달리 수정을 위한 메소드는 따로 존재하지 않는다. 수정을 위해서는 엔티티 매니저의 메소드를 호출할 필요 없이 영속 상태의 엔티티 객체를 수정하기만 하면된다.

엔티티 객체를 수정하였는데 DB에 반영되는 것은 영속성 컨텍스트 내부에서 변경 감지(dirty checking)를 하기 때문이다. 엔티티 매니저의 flush() 메소드가 실행되면 엔티티가 처음 persist 될 때 저장된 스냅샷과 현재 상태를 비교하여 상태가 달라졌으면 쓰기 지연 SQL 저장소에 업데이트 쿼리를 저장한다. 따라서 변경 사항이 DB에 반영된다.

만약 엔티티를 영속성 컨텍스트에 의해 관리되지 않는 준영속 상태로 만들고 싶다면 엔티티 매니저의 detach() 메소드 인자로 엔티티를 넘겨주거나, clear() 메소드를 통해 영속성 컨텍스트의 내용을 모두 지우거나, 또는 close() 메소드를 통해 엔티티 매니저를 종료시키면 된다. 엔티티 매니저를 종료시키면 영속성 컨텍스트는 소멸한다.

준영속 상태의 엔티티는 1차 캐시에 존재하지 않으므로 수정해도 DB에는 반영되지 않는다. 다시 영속 상태로 만들고 싶다면 merge() 메소드 인자로 엔티티를 넘겨주면 된다.

merge() 메소드가 실행되면 엔티티를 1차 캐시에서 찾고, 있으면 메소드 인자로 넘어온 값을 복사하고 1차 캐시에 있는 엔티티를 반환한다. 1차 캐시에 없으면 DB에서 값을 읽어 1차 캐시에 저장하고 동일한 동작을 수행한다.

그러나 DB에 해당 데이터가 없는 경우가 있다. 이런 경우 merge() 메소드는 사실상 persist() 메소드와 동일하게 동작한다. 즉, merge() 메소드는 엔티티의 상태가 준영속이건 비영속이건 상관없이 사용할 수 있어 엔티티의 생성과 수정 모두에 사용될 수 있다.

4) 삭제

엔티티 매니저의 remove() 메소드 인자로 삭제하고자 하는 엔티티를 넘겨주어 삭제할 수 있다. 엔티티를 저장하거나 수정할 때와 마찬가지로, 삭제 쿼리를 쓰기 지연 SQL 저장소에 저장했다가 flush 시에 실제로 DB에서 삭제된다.

[출처 및 참고]

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