JPA

최창효·2022년 4월 2일
0
post-thumbnail

JPA란

Java Persistence API의 약자로 자바 진영에서 ORM 기술의 표준으로 사용되는 인터페이스 모음입니다.

  • 인터페이스이기 때문에 JPA는 표준 명세와 같습니다. JPA 그 자체를 활용한다는 게 아닙니다.
  • JPA에 대한 구현체는 Hibernate, EclipseLink, DataNucleus등이 있습니다.
    • 가장 많이 쓰는 구현체는 Hibernate입니다.

ORM은

Object-Relational Mapping으로 객체Relational Database의 데이터를 자동으로 매핑(연결)해 주는 기술입니다.

당신은 age와 name을 가진 Person 데이터를 저장하고 싶습니다.

일반적인 과정은 아래과 같습니다.
1. DB에서 테이블을 생성합니다.

Create table Person{
	age int,
    name varChar
    PRIMARY KEY(name)
}
  1. DB테이블을 바탕으로 java에서 객체를 생성합니다.
static class Person{
	int age;
    String name;
}

하지만 ORM은 자바 객체를 생성하면 자동으로 DB 테이블을 생성해 줍니다.
1. java에서 객체를 생성한 뒤 2. 이를 DB에 Mapping(사상)합니다.

static class Person{
	int age;
    String name;
}

를 분석해 자동으로

Create table Person{
	age int,
    name varChar,
    PRIMARY KEY(name);
}

를 만들어 줍니다.

ORM은 한마디로 1+1 행사이벤트 입니다.
자바객체를 생성하면 테이블을 공짜로 만들어 줍니다.

등장배경

JDBC -> SQL Mapper -> ORM

SQL작업?

  • 서버 개발자에게 요구되는 SQL은 반복적이고 오랜 시간이 걸리는 작업이 대부분입니다.
  • 이러한 작업은 실력있는 개발자인 A가 작성하는 것과 주니어 개발자인 B가 작성하는 것에 큰 차이가 없습니다.
  • 한마디로 성능의 차이가 적고 시간만 잡아먹는 비효율적인 작업이 바로 SQL작업이었던 것입니다.

이러한 비효율을 해결하고기 위해 등장한 것이 바로 ORM입니다.

JDBC (초기)

  • JDBC는 자바에서 DB프로그래밍을 하기 위해 사용되는 API입니다.
  • JDBC에서는 개발자가 직접 Connection을 관리하고 SQL문을 작성해야 합니다.
    • JDBC는 위에서 언급한 SQL작업의 문제점을 고스란히 가지고 있습니다.

SQL Mapper (과도기)

  • SQL Mapper는 객체SQL문을 매핑시켜줍니다.
    • SQL문을 보내면 그 결과를 자바의 객체 모양으로 가져다 줍니다.
  • JDBC에서 하던 몇몇 기능을 자동으로 프레임워크에서 쉽게 처리해 줍니다.
  • 여전히 개발자가 SQL문을 직접 작성해야 하는 SQL위주의 개발입니다.
  • DBMS가 바뀌면 SQL문의 재사용이 어려워 집니다.
  • 종류: JdbcTemplate, Mybatis

ORM (현재)

  • ORM은 객체DB테이블을 매핑시켜줍니다.
  • 우리가 객체를 Persistence하게 저장하면 이를 분석해 DB테이블을 만들어 줍니다.
  • 또한 자바문법으로 DB를 관리할 수 있습니다.
    • 덕분에 객체지향적인 시스템 설계가 가능해 졌습니다.(SQL중심의 개발 탈피)
    • 패러다임의 불일치를 해소시켜 줍니다.
      • 자바와 데이터베이스에는 서로에게 없는 고유의 개념이 존재합니다.(상속, 컴포지션 ,외래키)
      • ORM은 서로에게 존재하지 않는 개념을 적절히 구현해 줍니다.
      • 만약 자바의 Person객체가 변수로 Family객체를 가지고 있다면 ORM은 이를 Person테이블과 Family테이블을 생성한 뒤 Family의 Foreign Key를 Person테이블에 부여하는 방식으로 둘의 관계를 정의해 줍니다.
    • SQL문으로 CRUD를 작성하지 않고 관련 메서드를 호출해 기능을 수행합니다.
      • SELECT age FROM person;이 아니라 person.age;으로 person의 age를 얻습니다.

JDBC는 근본입니다.

  • ORM이 편리하고 좋은 기술이기 때문에 ORM만 사용하고, JDBC는 몰라도 되는 건 아닙니다.
  • ORM역시 내부적으로 JDBC를 사용하며 JDBC의 문법을 따르고 있습니다.
  • 개발자가 하던 JDBC관리를 ORM이 해주는 것일뿐 JDBC와 다른 기술을 상용하는 게 아닙니다. ORM과 JSP를 잘 다루려면 JDBC의 구조와 기본에 대해 이해하고 있어야 합니다.

Persistance

이쯤에서 JPA의 이름을 다시 살펴보겠습니다. Java Persistence API로 Java는 Java고, API는 API입니다. 남는건 Persistence입니다.

영속성

  • Persistence(영속성)는 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 속성을 말합니다.
    • 영속성은 우리에게 너무나도 친숙합니다. 데이터가 사라지지 않으려면 우리는 데이터를 저장하면 됩니다. 저장이 곧 영속성이라고 생각해도 됩니다.
    • 데이터를 저장하는 방법은 파일로 저장하거나 DB에 저장하는 두 가지로 나뉩니다.
      • 자바는 데이터를 DB에 저장하는 게 일반적입니다.
  • 지금까지 JPA는 ORM중 하나, ORM은 객체와 DB를 매핑시키는 것이라고 했습니다.
    • DB와 편리하게 소통하기 위해 JPA를 씁니다. DB와 소통한다는 건 DB에 데이터를 저장하는 행위를 하겠다는 의미도 포함됩니다.
  • JPA는 Persistence한 객체만을 Mapping하여 table로 만들어 줍니다.

Persistence Context

  • JPA는 영속성 컨텍스트를 가지고 있습니다.
    • 영속성 컨텍스트란 Entity를 저장하는 환경입니다. Application과 DB사이에 존재하며 거기서 Entity를 저장하는 가상의 DB역할을 담당합니다.
      • Entity는 DB의 테이블, java의 DTO정도로 이해하시면 됩니다.
    • 영속성 컨텍스트는 세션 안에 존재합니다. 세션은 request요청 시점에 시작되기 때문에 영속성 컨텍스트 역시 request시점에 생성된다고 할 수 있습니다.
    • 영속성 컨텍스트는 사용자마다 개별로 생성됩니다.
  • 영속성 컨텍스트를 사용하면 다음의 장점이 있습니다.
    • 1차 캐시
      • key-value쌍으로 데이터를 가지고 있습니다.
        • key가 존재하지 않는 값을 요청했을 때만 DB를 들여다봅니다.
      • 아직 flush(DB에 삽입)하지 않았다면, DB까지 갈 필요없이 JPA내에서 해당 데이터에 접근할 수 있습니다.
    • 동일성 보장
      • Persistence Context에서 꺼낸 두 객체는 동일한 객체입니다.
    • 쓰기 지연
      • Insert할 SQL문들을 모아뒀다가 한꺼번에 DB에 요청할 수 있습니다.
        • 한마디로 트랜젝션 기능을 제공해 줍니다.
    • 변경 감지
      • Persistence Context와 DB는 계속해서 서로를 감시합니다.
        • Persistence Context의 값이 update되면 이를 감지하여 DB의 값도 자동으로 변경됩니다.
    • 지연 로딩
      • JPA의 fetchtype은 Eager와 Lazy가 존재합니다.
      • 지연(Lazy) 로딩이란 페이지를 불러오는 시점에 당장 필요하지 않은 리소스들을 추후에 로딩하게 하는 기술을 말합니다.
      • Entity가 실제로 사용되기 전까지는 DB에 직접 조회하지 않고 가짜 Entity(Proxy)를 사용합니다.
      • Lazy Loading은 비용감소 측면에서 효율적인 기술입니다.

Entity

  • EntityManagerFactory vs EntityManager
    • EntityManagerFactory는 EntityManager를 생성하는 객체입니다.
    • EntityManagerFactory는 비용이 비싸기 때문에 애플리케이션 내에서 하나만 생성합니다. EntityManagerFactory는 여러 스레드가 동시에 접근해도 안전합니다.
    • EntityManager는 생성비용이 저렴합니다. EntityManager는 여러 스레드 동시접근시 문제가 발생하기 때문에 스레드끼리 공유하지 않습니다.
  • Entity생명주기
    • 비영속(new)
      • Entity를 갓 생성했을 때, 영속성을 가지고 있지 않습니다.
    • 영속(mamaged)
      • 영속성 컨텍스트에 저장된 상태입니다
      • EntityManager.Persist(엔티티);를 통해 영속성 부여합니다.
    • 준영속(detached)
      • 영속성 컨텍스트에 저장되었다가 분리된 상태입니다.
      • 영속성 컨텍스트에서 해당 엔티티와 관련된 모든 정보가 삭제됩니다.
      • 이미 한번 영속상태였기 때문에 반드시 식별자 값을 가지고 있습니다.
      • 지연 로딩 시 문제가 발생합니다.
    • 삭제(removed)
      • 영속성 컨텍스트와 DB 모두에서 Entity 삭제합니다.
  • flush vs commit
    • flush: persistence context의 내용을 DB까지 옮긴 상태
    • commit: 옮긴 내용을 최종적으로 물리DB에 반영한 상태

JPA의 장단점

장점

  • 생산성 향상(개발자가 신경써야 할 부분이 줄어듬)
  • 유지보수의 용이
  • 성능 향상(Persistence Context)
  • 패러다임의 불일치 해소

단점

  • 매우 복잡한 SQL처리에 부적합
  • JPA가 항상 최선인 건 아니다.
    • 데이터가 static하고 대부분의 작업이 CRUD인 경우에만 활용하는 걸 권장

기타

OSIV

OSIV의 모든 설명과 이미지는 유튜브 메타코딩을 참고했습니다.

  • Open Session In View, 영속성 컨텍스트를 html을 만들어 유저에게 뿌려주는 시점까지 유지시킵니다.
  • 프록시에 대한 Lazy Loading을 수행할 수 있게 됩니다.
  • 2.0버전부터 OSIV는 기본 True로 설정되어 있으며 이를 False로 변경할 시 영속성 컨텍스트를 Trasaction, JDBC Connection과 마찬가지로 Server와 Controller간의 요청이 끝나는 시점에 종료합니다. -> Lazy Load 불가능
  • OSIV의 경우 Server와 Controller간의 요청 시점에 트랜잭션이 종료되었기 때문에 변경감지는 불가능합니다.

기존의 방식은 다음과 같았습니다.

그러다가 효율을 높이기 위해 다음과 같은 방식을 선택했습니다.

이러한 경우 Lazy Loading이 불가능 합니다. 그래서 OSIV를 설정했습니다.

예시)
야구선수 데이터가 있습니다. 야구선수의 팀 column은 Team이라는 테이블을 참조하고 있습니다.
requst로 야구선수 A의 데이터를 요청합니다. 우리는 1차 캐시에 A의 데이터를 가지고 있지 않기 때문에 DB에서 A의 데이터를 가져온 뒤 1차 캐시에 올려둡니다.
이때 야구선수와 팀의 관계는 M:1입니다. 이러한 경우 기본적으로 즉시로딩(Eager)을 사용하며 그 결과 영속성 컨텍스트의 1차 캐시에는 야구선수의 객체와 야구팀의 객체가 모두 올라와 있습니다.

하지만 지연로딩(Lazy)으로 데이터를 가져오면 1차 캐시에는 야구선수 객체와 야구팀Proxy 객체 가 올라와 있습니다.
이 경우 Controller에서 데이터를 받아 뒤늦게 데이터를 요청하려 해도(지연로딩) 이미 영속성 컨텍스트가 종료된 시점이라 Proxy데이터를 실제 데이터로 변경할 수 없습니다.

OSIV는 이러한 문제를 해결해 줍니다. OSIV를 설정함으로써 영속성 컨텍스트의 종료시점은 response시점이 됩니다.
Controller는 proxy데이터를 원래 데이터로 바꾸기 위해 영속성 컨텍스트로 가고, 아직 열려있기 때문에 프록시를 실제정보로 변경하는 게 가능합니다.

연관관계 매핑

JPA는 패러다임의 불일치를 해소해 줍니다. 하지만 그 과정에서 아래의 것들이 논리적으로 잘 고려되어야 합니다.

방향

  • 한쪽이 다른한쪽을 참조하는 형태면 단방향, 양쪽이 서로를 참조하는 형태면 양방향 입니다.
    • DB는 외래키로 양쪽 테이블을 모두 확인할 수 있지만 Java의 객체는 참조용 필드를 가지고 있어야만 참조가 가능합니다.
    • 양방향이라면 두 객체 모두에게 서로를 참조할 수 있는 변수를 부여합니다.

연관관계의 주인

  • 양방향 관계일 때 두 객체는 모두 서로를 참조할 수 있지만 그렇기 때문에 어떤 일이 일어났을 때 둘 중 어떤걸 기준으로 작업을 진행해야 할지 모호해 집니다.
  • 연관관계의 주인을 설정해 JPA가 누구를 기준으로 작업할지를 정해줍니다.

다중성

  • DB를 기준으로 다중성을 결정합니다.
    • 분류
      • N:1 -> N쪽에 외래키가 존재합니다.
      • 1:N -> 1쪽에 외래키가 존재합니다.
      • M:N -> 실무에서 사용을 지양합니다.

N+1문제

  • 1:N관계를 갖는 Entity의 하위 Entity를 조회할 때 비효율적으로 쿼리가 호출되는 상황을 얘기합니다.
  • N+1문제는 두 개의 객체가 1:N관계를 가지며 JPQL로 객체를 호출할 때
    • Eager방식으로 가져오거나
    • Lazy전략으로 데이터를 가져온 이후에 가져온 데이터에서 하위 엔티티를 다시 조회하는 경우 발생합니다.

JPQL이란 Java Persistence Query Language의 약자로, JPA 내부의 쿼리언어를 지칭하는 용어입니다.

해결방법

  • 패치조인을 활용합니다.
    • @ManyToOne의 fetch를 모두 Lazy로 변경합니다.
  • @EntityGraph를 활용합니다.

더티 체킹

  • 더티 체킹이란 (Transaction 또는 영속성 컨텍스트) 속 엔티티의 상태변화를 감지해 변경 내용을 자동으로 DB에 반영하는 JPA의 특징을 말합니다.
  • CRUD의 Update를 대신한다고 생각할 수 있습니다.

References

profile
기록하고 정리하는 걸 좋아하는 개발자.

0개의 댓글