✏️ JPA란?

박상민·2023년 9월 17일
0

JPA

목록 보기
1/24
post-thumbnail

📌 ORM(Object-relational mapping) 이란

Object-relational mapping (객체 관계 매핑)

  • 객체는 객체대로 설계하고, 관계형 데이터베이스는 관계형 데이터베이스대로 설계한다.
  • ORM 프레임워크가 중간에서 매핑해준다.
  • 대중적인 언어에는 대부분 ORM 기술이 존재한다.
  • ORM은 객체와 RDB 두 기둥 위에 있는 기술 이다.

⭐️ JPA(Java Persistent API)

  • 자바 ORM 기술에 대한 표준 명세로, JAVA에서 제공하는 API이다. (스프링에서 제공하는 것이 아니다.)

    • 자바 플랫폼 SE와 자바 플랫폼 EE를 사용하는 응용프로그램에서 관계형 데이터베이스의 관리를 표현하는 자바 API이다.

    • 즉, JPA는 ORM을 사용하기 위한 표준 인터페이스를 모아둔 것이다.

      • 이때, JPA는 인터페이스이다. JPA는 특정 기능을 하는 라이브러리가 아니다. JPA 역시 자바 어플리케이션에서 관계형 데이터베이스를 어떻게 사용해야 하는지를 정의하는 한 방법일 뿐이다.

    • 기존에 EJB에서 제공되던 Entity Bean을 대체하는 기술이다.

      EJB는 코드가 지저분하고, API의 복잡성이 높고 속도가 느리다는 문제가 있었다.

  • JPA 구성 요소 (세 가지)

    • javax.persistance 패키지로 정의된 API 그 자체
    • JPQL(Java Persistence Query Language)
    • 객체/관계 메타데이터
  • ORM이기 때문에 자바 클래스와 DB테이블을 매핑한다.(sql을 매핑하지 않는다)

  • 사용자가 원하는 JPA 구현체를 선택해서 사용할 수 있다.
    • JPA의 대표적인 구현체로는 Hibernate, EclipseLink, DataNucleus, OpenJPA, TopLink Essentials 등이 있다.
    • 이 구현체들을 ORM Framework라고 부른다.

📌 JPA의 동작 과정

JPA는 애플리케이션과 JDBC 사이에서 동작한다.

  • 개발자가 JPA를 사용하면, JPA 내부에서 JDBC API를 사용하여 SQL을 호출하여 DB와 통신한다.
  • 개발자가 직접 JDBC API를 사용하는 것이 아니다.

✔︎ 저장 과정


MemberDAO에서 객체를 저장하고 싶다고 가정하자. 개발자는 JPA에 Member 객체를 넘긴다.
이때 JPA는

    1. Member 엔티티를 분석한다.
    1. INSERT SQL을 생성한다.
    1. JDBC API를 사용하여 INSERT SQL을 DB에 날린다.

✔︎ 조회 과정

Member 객체를 조회하고 싶다고 하자. 개발자는 member의 pk(Primary Key, 기본키) 값을 JPA에 넘긴다.
이때 JPA는

    1. 엔티티의 매핑 정보를 바탕으로 적절한 SELECT SQL을 생성한다.
    1. JDBC API를 사용하여 SELECT SQL을 DB에 날린다.
    1. DB로부터 결과를 받아온다.
    1. 결과(ResultSet)를 객체에 모두 매핑한다.

쿼리를 JPA가 만들어주기 때문에 Object와 RDB간의 패러다임 불일치를 해결할 수 있다.

📌 JPA를 왜 사용해야 할까?

✔︎ SQL 중심적인 개발에서 객체 중심으로 개발

이번 JPA 글을 작성하면서 정말 도움이 많이 된 글이 있다. 그 분이 작성하신 글 중 SQL 중심적인 개발의 문제점 참고에서 SQL 중심적인 개발의 문제점을 잘 다뤄주셨다. 해당 글을 참고하기 바란다.
SQL 중심적인 개발의 많은 문제들 때문에 객체 중심 개발로 트렌드가 바뀌었다.

✔︎ 생산성

JPA를 사용하는 것은 마치 Java Collection에 데이터를 넣었다 빼는 것처럼 사용할 수 있게 만든 것이다.
간단한 CRUD

  • 저장: jpa.persist(member)
  • 조회: Member member = jpa.find(memberId)
  • 수정: member.setName("변경할 이름")
  • 삭제: jpa.remove(member)

모두 간단하지만 특히나 수정이 간단한다.
객체를 변경하면 그냥 알아서 DB에서 UPDATE Query가 나간다.

✔︎ 유지보수

  • 기존: 필드 변경 시 모든 SQL을 수정해야 한다. 매우 번거롭다.

  • JPA 사용: SQL은 JPA가 처리한다. 필드만 추가하면 된다.

JPA 사용 전에는 필드 변경 시 모든 SQL을 수정하는 과정이 필요했다. JPA 사용하면 SQL은 JPA가 알아서 처리해주고 사용자는 필드만 추가하면 된다.

✔︎ Object와 RDB 간의 패러다임 불일치 해결

[JPA와 상속]

저장

  • 개발자가 할 일
    • jpa.persist(album);
  • JPA가 나머지를 처리
    • INSERT INTO ITEM ...
    • INSERT INTO ALBUM ... 등등

저장

  • 개발자가 할 일
    • Album album = jpa.find(Album.class, albumId);
  • JPA가 나머지를 처리
    • SELECT I.*, A.* FROM ITEM I JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID

[JPA와 연관관계]

  • 객체의 참조로 연관관계 저장 가능
    • member.setTeam(team);
    • jpa.persist(member);

[JPA와 객체 그래프 탐색]

  • 신뢰할 수 있는 엔티티, 계층
class MemberService { 
      ...
      public void process() { 
          /* 직접 구현한 DAO에서 객체를 가져온 경우 */
          Member member1 = memberDAO.find(memberId); 
          member1.getTeam(); // 엔티티를 신뢰할 수 없음 
          member1.getOrder().getDelivery(); 
          /* JPA를 통해서 객체를 가져온 경우 */
          Member member2 = jpa.find(Member.class, memberId); 
          member2.getTeam(); // 자유로운 객체 그래프 탐색
          member2.getOrder().getDelivery(); 
      } 
}
  • 내가 아닌 다른 개발자가 직접 구현한 DAO에서 가져오는 경우

    • DAO에서 직접 어떤 쿼리를 날렸는지 확인하지 않는 이상, 그래프 형태의 관련된 객체들을 모두 잘 가져왔는지 알 수가 없다.
    • 즉, 반환한 엔티티를 신뢰하고 사용할 수 없다.
  • JPA를 통해서 가져오는 경우

    • 객체 그래프를 완전히 자유롭게 탐색할 수 있게 된다.
    • 지연 로딩 전략(Lazy Loading) 사용
      • 관련된 객체를 사용하는 그 시점에 SELECT Query를 날려서 객체를 가져오는 전략

[JPA와 비교하기]

동일한 트랜잭션에서 조회한 엔티티는 같음을 보장한다.

String memberId = "100"; 
Member member1 = jpa.find(Member.class, memberId); // DB에서 가져옴 
Member member2 = jpa.find(Member.class, memberId); // 1차 캐시에서 가져옴

member1 == member2; //같다.

✔︎ JPA의 성능 최적화 기능

  • 중간 계층이 있는 경우 아래의 방법으로 성능을 개선할 수 있는 기능이 존재한다.
    • 모아서 쓰는 버퍼링 기능
    • 읽을 때 쓰는 캐싱 기능
  • JPA도 JDBC API와 DB 사이에 존재하기 때문에 위의 두 기능이 존재한다.

[1차 캐시와 동일성(identity) 보자 - 캐싱 기능]

같은 트랜잭션 안에서는 같은 엔티티를 반환, 약간의 조회 성능이 향상되지만 큰 도움은 안된다.

String memberId = "100"; 
Member m1 = jpa.find(Member.class, memberId); // SQL 
Member m2 = jpa.find(Member.class, memberId); // 캐시 (SQL 1번만 실행, m1을 가져옴)

m1 == m2 // 같다.
  • 결과적으로 SQL을 한 번만 실행한다.

DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장

[트랜잭션을 지원하는 쓰기 지연(transactional write-behind) - 버퍼링 기능]

INSERT

/** 1. 트랜잭션을 커밋할 때까지 INSERT SQL을 모음 */
transaction.begin();  // [트랜잭션] 시작
em.persist(memberA); 
em.persist(memberB); 
em.persist(memberC); 
// -- 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

// 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다. --
/** 2. JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송 */
transaction.commit(); // [트랜잭션] 커밋
  • 트랜잭션을 commit 할 때까지 INSERT SQL을 메모리에 쌓는다.

    • 이렇게 하지 않으면 DB에 INSERT Query를 날리기 위한 네트워크를 3번 타게 된다.
  • JDBC Batch SQL 기능을 사용해서 한 번에 SQL을 전송한다.

    • JDBC Batch를 사용하면 코드가 굉장히 지저분해진다.
    • 지연 로딩 전략(Lazy Loading) 옵션을 사용한다.

UPDATE

/** 1. UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화 */
transaction.begin();  // [트랜잭션] 시작
changeMember(memberA);  
deleteMember(memberB);  
비즈니스_로직_수행();     // 비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다.    
// 커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다.

/** 2. 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋 */
transaction.commit(); // [트랜잭션] 커밋

[지연 로딩 (Lazy Loading)]

지연 로딩 (Lazy Loading)

  • 객체가 실제로 사용될 때 로딩하는 전략

  • memberDAO.find(memberId)에서는 Member 객체에 대한 SELECT 쿼리만 날린다.

  • Team team = member.getTeam()로 Team 객체를 가져온 후에 team.getName()처럼 실제로 team 객체를 건드릴 때 Team에 대한 SELECT 쿼리를 날린다.

    • 즉, 값이 실제로 필요한 시점에 JPA가 Team에 대한 SELECT 쿼리를 날린다.
  • Member와 Team 객체 각각 따로 조회하기 때문에 네트워크를 2번 타게 된다.

    • Member를 사용하는 경우에 대부분 Team도 같이 필요하다면 즉시 로딩을 사용한다.
  • 즉시 로딩

    • JOIN SQL로 한 번에 연관된 객체까지 미리 조회하는 전략

    • Join을 통해 항상 연관된 모든 객체를 같이 가져온다.

  • 애플리케이션 개발할 때는 모두 지연 로딩으로 설정한 후에, 성능 최적화가 필요할 때에 옵션을 변경하는 것을 추천한다.


출처
JPA를 사용하는 이유
JPA란
자바 ORM 표준 JPA 프로그래밍 강의

profile
스프링 백엔드를 공부중인 대학생입니다!

0개의 댓글