JPA

eunsiver·2023년 6월 22일
0

JPA와 같은 ORM을 사용하는 이유가 무엇인가요?
• 영속성은 어떤 기능을 하나요? 이게 진짜 성능 향상에 큰 도움이 되나요?
• N + 1 문제에 대해 설명해 주세요.

ORM 이란?

ORM(Object-relational mapping)의 약자로써, 객체와 관계의 연결을 시켜주는 것을 말한다.
Java와 같은 객체 지향 언어에서의 객체와 Oracle와 같은 RDB를 연결 시켜주는 방식을 말하며, Hibernate는 Java에서 사용하는 ORM의 open source Framework의 한 종류이다.

🤔ORM이 뭐죠? SQL Mapper와 ORM

  • ORM은 DB 테이블을 자바 객체로 매핑함으로써 객체간의 관계를 바탕으로 SQL을 자동으로 생성하지만 Mapper는 SQL을 명시해주어야 한다.

  • ORM은 RDB의 관계를 Object에 반영하는 것이 목적이라면, Mapper는 단순히 필드를 매핑시키는 것이 목적이라는 점에서 지향점의 차이가 있다.

SQL Mapper

  • SQL ←mapping→ Object 필드
  • SQL 문으로 직접 디비를 조작한다.
  • Mybatis, jdbcTemplate

ORM(Object-Relation Mapping/객체-관계 매핑)

  • DB 데이터 ←mapping→ Object 필드
  • 객체를 통해 간접적으로 디비 데이터를 다룬다.
  • 객체와 디비의 데이터를 자동으로 매핑해준다.
    • SQL 쿼리가 아니라 메서드로 데이터를 조작할 수 있다.
    • 객체간 관계를 바탕으로 sql을 자동으로 생성한다
  • Persistant API라고 할 수 있다.
  • JPA, Hibernate

JDBC

  • JDBC는 DB에 접근할 수 있도록 자바에서 제공하는 API이다.

  • 모든 JAVA Data Access 기술의 근간이다. ⇒ 모든 Persistance Framework는 내부적으로 JDBC API를 이용한다.

JPA 란?

  • (Java Persistent API)의 약자로 말 그대로 자바에서 사용하는 ORM 기술에 대한 API 표준 명세를 뜻합니다.
  • JPA는 ORM을 사용하기 위한 인터페이스를 모아둔 것입니다.
  • JAVA에서 제공하는 API이다. 스프링에서 제공하는 것이 아님!

JPA 동작 과정

  • JPA는 애플리케이션과 JDBC 사이에서 동작한다.
  • 개발자가 JPA를 사용하면, JPA 내부에서 JDBC API를 사용하여 SQL을 호출하여 DB와 통신한다.
  • 즉, 개발자가 직접 JDBC API를 쓰는 것이 아니다.

insert

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

JPA는

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

find

개발자는 member의 pk 값을 JPA에 넘긴다.

JPA는

  1. 엔티티의 매핑 정보를 바탕으로 적절한 SELECT SQL을 생성한다.
  2. JDBC API를 사용하여 SQL을 DB에 날린다.
  3. DB로부터 결과를 받아온다.
  4. 결과(ResultSet)를 객체에 모두 매핑한다.
    쿼리를 JPA가 만들어 주기 때문에 Object와 RDB 간의 패러다임 불일치를 해결할 수 있다.

JPA 특징

  • 데이터를 객체지향적으로 관리할 수 있기 때문에 개발자는 비즈니스 로직에 집중할 수 있고 객체지향 개발이 가능하다.

  • 자바 객체와 DB 테이블 사이의 매핑 설정을 통해 SQL을 생성한다.

  • 객체를 통해 쿼리를 작성할 수 있는 JPQL(Java Persistence Query Language)를 지원

  • JPA는 성능 향상을 위해 지연 로딩이나 즉시 로딩과 같은 몇가지 기법을 제공하는데 이것을 잘 활용하면 SQL을 직접 사용하는 것과 유사한 성능을 얻을 수 있다.

JPA를 왜 사용해야 할까

  1. sql 중심적인 개발에서 객체 중심적인 개발이 가능하다.
  • sql 코드의 반복, 객체지향과 관계지향 데이터베이스의 페러다임 불일치

  • Object -> [SQL 변환] -> RDB에 저장
    [개발자 == SQL 매퍼] 라고 할만큼 SQL 작업을 너무 많이 하고 있다.

  1. 생산성이 증가

    간단한 메소드로 CRUD가 가능하다

  2. 유지보수가 쉽다
    기존: 필드 변경 시 모든 SQL을 수정해야 한다.
    JPA: 필드만 추가하면 된다. SQL은 JPA가 처리하기 때문에 손댈 것이 없다.

  3. Object와 RDB 간의 패러다임 불일치 해결

  • 객체 지향

    • ‘객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다.’ - 어느 객체지향 개발자

    • 필드와 메서드 등을 묶어서 잘 캡슐화해서 사용하는 것이 목표
  • 관계형 데이터베이스 (RDB)

    데이터를 잘 정규화해서 보관하는 것이 목표

  • 패러다임이 다른 두 가지를 가지고 억지로 매핑하기 때문에 여러 가지 문제가 발생한다.
    Object를 RDB에 넣으려고 하니까 문제가 발생한다.

    https://gmlwjd9405.github.io/2019/08/03/reason-why-use-jpa.html

영속성

데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.

영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.

그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.

JPA에서 가장 중요한 2가지

  • 객체와 관계형 데이터베이스 매핑하기
  • 영속성 컨텍스트

영속성 컨텍스트

  • JPA를 이해하는데 가장 중요한 용어
  • 엔티티를 영구 저장하는 환경이라는 뜻
  • EntityManager.persist(entity)

• 영속성 컨텍스트는 논리적인 개념
• 눈에 보이지 않는다.
• 엔티티 매니저를 통해서 영속성 컨텍스트에 접근

엔티티의 생명주기

• 비영속 (new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
• 영속 (managed)
영속성 컨텍스트에 관리되는 상태
• 준영속 (detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
• 삭제 (removed)
삭제된 상태

영속성 컨텍스트의 이점

• 1차 캐시
• 동일성(identity) 보장
• 트랜잭션을 지원하는 쓰기 지연
(transactional write-behind)
• 변경 감지(Dirty Checking)
• 지연 로딩(Lazy Loading)

1차 캐시

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

같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상 (크게 도움 X)

String memberId = "100";

Member m1 = jpa.find(Member.class, memberId); // SQL

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

println(m1 == m2) // true

결과적으로, SQL을 한 번만 실행한다.

쓰기 지연

/** 1. 트랜잭션을 커밋할 때까지 INSERT SQL을 모음 */

transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);

em.persist(memberB);

em.persist(memberC);

// -- 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

// 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다. --

transaction.commit(); // [트랜잭션] 커밋

엔티티 수정

플러시

영속성 컨텍스트의 변경내용을 데이터베이스에 반영

지연 로딩(Lazy Loading)

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

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

Team team = member.getTeam()로 Team 객체를 가져온 후에 team.getName()처럼 실제로 team 객체를 건드릴 때!

즉, 값이 실제로 필요한 시점에 JPA가 Team에 대한 SELECT 쿼리를 날린다.

Member와 Team 객체 각각 따로 조회하기 때문에 네트워크를 2번 타게 된다.

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

즉시 로딩

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

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

즉시 로딩 주의

• 가급적 지연 로딩만 사용(특히 실무에서)
• 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
• 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
• @ManyToOne, @OneToOne은 기본이 즉시 로딩
-> LAZY로 설정
• @OneToMany, @ManyToMany는 기본이 지연 로딩

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

N+1 문제란

N+1문제란
연관 관계에서 발생하는 이슈로 연관관계가 설정된 엔티티를 조회할 때 조회한 갯수보다 쿼리가 더 나오는 것이다.

예를 들어 멤버 테이블과 팀 테이블이 N:1 관계에 있을 때, 멤버 테이블을 조회하고 조회한 테이블에서 Team을 꺼내면

그 순간 데이터베이스에 등록된 Team의 칼럼 수 만큼 쿼리가 나간다. 만약 Team의 칼럼이 10000개가 있다면 하나를 조회하기 위해

쿼리가 10000+1번 나가는 것이다.

N+1은 1:N, 혹은 N:1 관계에서 발생한다.

즉시 로딩
Member 엔티티를 조회하는 상황이 이라고 가정할때, 즉시 로딩의 경우 Member뿐만 아니라 Team의 속성도 모두 조회해서 가져온다.
그렇다면 실제 SQL 쿼리는 Member를 조회하는 쿼리 1개 Team을 조회하는 쿼리 1개가 나가서 총 2개의 쿼리가 나가게 된다.


//멤버를 조회하는 쿼리 
select 
	member0_.id as id1_0_,
	member0_.team_id as team_id3_0_, 
	member0_.username as username2_0_ 
from 
	Member member0_ 
//팀을 조회하는 쿼리 
select 
	team0_.id as id1_3_0_, 
    team0_.name as name2_3_0_ 
from 
	Team team0_ 
 where 
 	team0_.id=?

즉, 개발자는 Member만 조회하려고 하는데 한번엔 Team도 다 같이 가져오는 것이다. 물론 Team까지 한번에 조회하는게 유리한 상황이 있다. 하지만 엔티티에 이러한 설정이 되어있을 경우 그러한 상황이 아닌데도 쓸데없이 쿼리가 나가는 것이다.
만약 관계가 여러개 얽혀있다면 그 관계의 수만큼 추가적인 쿼리가 나갈 것이다. (그래서 사실 N+1문제는 1+N문제라고 하는게 맞는거 같다) 그렇다면 그만큼 성능이 떨어지게 될 것이다!

지연 로딩

지연 로딩의 경우 Member를 조회하는 시점에 Member의 속성만을 쿼리한다. 그리고 .getTeam().getName()같이 실제로 Team속성을 사용하는 시점에 Team을 조회하는 쿼리가 나간다.

select 
	member0_.id as id1_0_,
	member0_.team_id as team_id3_0_,
    member0_.username as username2_0_ 
from
	Member member0_
    

정리하자면 기본적으로는 Member만을 조회하고 필요할 때 연관 관계 쿼리가 나가는 것이다. 이 방법을 사용하면 즉시 로딩에서 발생하는 문제를 간단하게 해결할 수 있다.

https://ojt90902.tistory.com/640

https://www.youtube.com/watch?v=ni92wUkAmQI

profile
Let's study!

0개의 댓글