자바 ORM 표준 JPA 프로그래밍 스터디 - 1주차

큰모래·2023년 4월 22일
0

1장. JPA 소개


SQL을 직접 다룰 때 발생하는 문제점

1. 수많은 반복

회원 테이블에 대한 CRUD 기능을 개발한다고 가정

  • 회원 객체 생성
    class Member {
    		private String memberId;
    		private String name
    }
  • 회원 조회 기능 개발
    1. 회원 조회 SQL 작성
    2. JDBC를 통해 SQL 실행
    3. 조회된 결과를 일일이 객체로 매핑 작업
  • 회원 등록 기능 개발
    1. 회원 등록 SQL 작성
    2. 회원 객체 값을 꺼내서 등록 SQL에 전달
    3. JDBC를 통해 SQL 실행
  • 회원 수정, 삭제 기능도 위의 조회, 등록 기능과 비슷한 작업을 반복할 것임.
  • 테이블이 100개라면 이런 기능들을 일일이 SQL 작성하는 것은 끔찍하다.

2. SQL에 의존적인 개발

  • 요구사항이 추가될 때 마다 개발자는 CRUD가 어떻게 동작하는지 SQL문을 일일이 확인해봐야 한다.
  • 이렇게 되면 요구 사항이 추가될 수록 개발자는 점점 엔티티 객체를 신뢰할 수 없게 된다.

패러다임의 불일치

관계형 데이터베이스는 데이터 중심적이고 집합적인 사고를 요구한다. 또한, 데이터베이스는 객체지향의 추상화, 상속, 다형성 같은 개념이 없다. 이러한 패러다임의 불일치를 개발자가 중간에서 해결해야 한다.

1. 연관관계

객체는 참조를 사용해서 연관관계를 맺고, 데이터베이스는 외래키를 통해 연관관계를 맺게 된다.

객체는 참조가 있는 방향으로만 조회가 가능하지만, 테이블은 외래 키 하나로

Member join Team, Team join Member 둘다 가능하다.

객체 모델은 외래 키가 필요 없는 대신 참조 값이 필요하고 테이블은 참조 값이 필요 없고 외래 키가 필요한데 이를 개발자가 중간에서 변환시켜줘야 한다.

하지만, JPA를 사용한다면 개발자는 객체간의 관계를 설정하고 객체를 저장하기만 하면된다.
JPA가 알아서 참조 값을 외래키로 변환해 적절한 쿼리를 DB로 전달한다.

2. 객체 그래프 탐색

참조를 사용해서 연관된 객체를 찾는 것

객체는 자유롭게 객체 그래프를 탐색할 수 있다.

// 회원이 소속된 팀
Team team = member.getTeam();

// 회원 주문의 주문 아이템
OrderItem orderItem = member.getOrder().getOrderItem();

하지만 SQL을 직접 다루면 처음 짠 SQL에 따라 객체 그래프 탐색이 어디까지 가능한지 정해지게 된다.

이렇게 되면, 개발자는 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수 없게 된다. 결국 또 SQL문을 일일이 확인해야 한다.

하지만, JPA를 사용하면 연관된 객체를 사용한 시점에 적절한 쿼리를 날려준다.

3. 비교

  • 데이터베이스 : 기본 키(Primary Key)값으로 각각의 Row를 식별한다.
  • 객체 : 동일성 및 동등성 비교
    • 동일성 : 객체 인스턴스의 주소 참조 값 비교 (hashCode())
    • 동등성 : 객체 내부 값 비교 ( equals())
//MemberDAO에 memberId를 통해 멤버를 반환하는 쿼리문이 있다 가정
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1member2는 객체의 동일성 비교에서 실패한다. (둘 다 새롭게 생성된 객체이므로)

JPA를 사용하면, 한 트랜잭션내에서 같은 로우에서 객체를 여러번 조회해도 동일성을 보장해준다.

4. 정리

  • 객체 지향적으로 설계할수록 데이터베이스와의 패러다임 불일치는 점점 커진다.
  • 이 틈을 메우기 위한 개발자가 소모해야 하는 비용도 그만큼 커지게 된다.
  • 이렇게 되면 점점 객체 모델링은 힘을 잃고 데이터 중심의 모델로 변하게 된다.
  • JPA는 객체 모델링을 유지하면서 최적의 데이터베이스가 설계되도록 도와준다.

JPA란 무엇인가?

Java Persistence API - 자바 진영의 ORM 기술 표준이다.

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

객체를 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 ORM 프레임워크는 적절한 쿼리를 만들어서 JDBC를 통해 DB에 전달된다. 개발자는 매핑 방법만 ORM 프레임워크에게 알려주면 된다.

자바 진영에는 이러한 ORM 프레임워크가 다양한데, 그 중 하이버네이트 프레임워크를 가장 많이 사용

하이버네이트는 거의 모든 패러다임 불일치 문제를 해결해준다.

JPA는 인터페이스로 자바 ORM 기술에 대한 API 표준 명세이다.

따라서, JPA를 사용하려면 인터페이스 구현체가 있어야하고 그것이 하이버네이트와 같은 프레임워크이다.

이러한 JPA라는 표준 덕분에 특정 구현 기술에 대한 의존도가 줄어들고 다른 기술로 유연하게 이동할 수 있다.

왜 JPA를 사용해야 하는가??

  • 생산성
    • 엔티티 객체를 자바 컬렉션에 저장하듯이 손쉽게 DB에 저장할 수 있다.
    • DML, DDL 문을 JPA가 자동으로 만들어주니까 개발자는 객체 지향 적인 설계에 집중할 수 있다.
  • 유지보수
    • SQL에 의존하면 엔티티에 필드 값이 하나만 추가되도 기존에 작성된 여러 쿼리문을 일일이 손봐야 했다.
    • JPA를 사용하면 이러한 처리를 대신해준다.
  • 패러다임 불일치 해결
    • 상속, 연관관계, 객체 그래프 탐색, 비교하기에 대한 DB와의 패러다임 불일치를 해소해준다.
  • 성능
    • JDBC API를 통해 쿼리를 직접 작성했다면, 한 트랜잭션내에서 DB와 두 번 통신 했을 것이다.

    • JPA를 사용하면 조회 쿼리를 DB에 한번에 보내고, 조회한 회원 객체는 재사용된다.

      String memberId = "abc";
      Member member1 = jpa.find(memberId);
      Member member2 = jpa.find(memberId);
  • 데이터 접근 추상화
    • 데이터베이스를 변경하면 JPA에게 다른 데이터베이스를 쓴다고 알려주기만 하면 된다.
    • 특정 데이터베이스 기술에 종속되지 않는다.

3장. 영속성 관리


Entity Manager Factory, Entity Manager

  • Entity Manager Factory
    • 생성하는 비용이 크므로 애플리케이션내에 1개만 생성해서 공유한다.
    • 여러 스레드가 동시 접근해도 안전하다.
  • Entity Manager
    • 공장에서 생성하는 비용이 거의 들지 않는다.
    • 여러 스레드가 동시에 접근 시 동시성 문제가 발생한다. ( 절대 공유 X)
  • Connection Pool
    • 커넥션 풀은 DB와의 연결을 관리하는 기술이다.
    • 미리 생성된 커넥션을 풀에 저장하고 필요할 때 풀에서 가져다쓰고 반납하는 형식으로 사용된다.
    • Entity Manager는 트랜잭션이 필요한 시점에 커넥션을 생성한다.

영속성 컨텍스트 (Persistence Context)

엔티티를 영구 저장하는 환경

엔티티 매니저를 통해 엔티티를 저장하거나 조회하면, 엔티티 매니저는 영속성 컨텍스트에 보관하고 관리한다.

em.persist(member); : 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장


엔티티의 생명주기

  • 비영속
    • 영속성 컨텍스트와 아무런 관계가 없는 상태 (엔티티 객체를 생성만한 경우)
  • 영속
    • 영속성 컨텍스트에 의해 관리되는 상태 (엔티티 객체를 영속성 컨텍스트에 저장한 경우)
  • 준영속
    • 영속성 컨텍스트가 관리하던 상태에서 관리하지 않는 상태로 바뀐 경우
    • em.detach(member) , em.clear() , em.close()
  • 삭제
    • 엔티티를 영속성 컨텍스트와 db에서 삭제한 경우
    • em.remove(member)

영속성 컨텍스트의 특징

주요 특징

  • 영속성 컨텍스트는 엔티티를 식별자 값(기본키)으로 구분하기 때문에 영속 상태의 엔티티는 반드시 식별자 값을 가지고 있다.
  • 트랜잭션 커밋이 되는 순간 영속성 컨텍스트에 저장된 엔티티가 DB에 저장되게 된다. 이것을 플러시라고 한다.
  • 1차 캐시
  • 동일성 보장
  • 쓰기 지연
  • 변경 감지
  • 지연 로딩

엔티티 조회

영속성 컨텍스트 내부에는 key로 식별자값, value로 인스턴스를 가지는 Map이 있고 이를 1차 캐시라고 부른다.

영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 식별자 값 즉, 데이터베이스 기본 키 값이다.

em.find(엔티티 클래스, 식별자 값); 으로 엔티티를 조회할 수 있다.

1차 캐시에 엔티티가 있으면 1차 캐시에서 바로 반환

1차 캐시에 엔티티가 없으면 DB에서 엔티티를 조회한 후 엔티티를 1차 캐시에 저장해 영속 상태로 만들어 반환한다. 이렇게 되면 이후 해당 엔티티를 조회할 때 1차 캐시에서 불러올 수 있으므로 성능이 향상된다.

//해당 인스턴스가 영속성 컨텍스트에 관리되는 영속 상태라면 1차 캐시에 있는 같은 인스턴스를 반환한다.
Member a = em.find(Member.class, 1);
Member b = em.find(Member.class, 1);

엔티티 등록

엔티티를 영속화할 때 엔티티 매니저는 1차 캐시에 엔티티를 저장하고, 쓰기 지연 저장소에 Insert 쿼리를 모아둔다. 이때, 엔티티 매니저는 트랜잭션이 커밋되기 직전까지 쓰기 지연 저장소에 Insert 쿼리를 모아둔다.

마지막으로, 트랜잭션이 커밋되면 엔티티 매니저는 flush를 통해 쓰기 지연 저장소에 저장된 쿼리를 DB로 보낸다. 이렇게 영속성 컨텍스트의 변경 내용을 DB에 동기화한 후에 실제 DB 트랜잭션을 커밋하게 된다.

쿼리를 한번에 보냄으로써 성능을 최적화할 수 있다.

엔티티 수정

트랜잭션 커밋이 발생하면 우선 flush를 호출해서 데이터베이스와의 동기화 작업을 진행한다.

이때, 1차 캐시에 저장된 엔티티 인스턴스와 스냅샷을 비교한다. (스냅샷에는 영속성 컨텍스트 저장 시점의 최초 상태가 저장되어 있다.) 다른 부분에 대한 update 쿼리를 만들어 쓰기 지연 저장소에 저장한다. 쓰기 지연 저장소의 쿼리를 db로 보내고 최종적으로 트랜잭션 커밋이 종료된다. 이를 변경감지라고 한다.

당연한 얘기지만, 변경감지는 영속상태의 엔티티에만 적용된다.

그리고, 변경감지를 통해 update 쿼리가 생성되고 db로 전송될 때, 쿼리문을 보면 사실상 모든 필드에 업데이트가 적용된 것을 확인할 수 있다. 이렇게 하면 수정 쿼리가 항상 동일하므로 재사용이 가능하다.

만약, @DynamicUpdate 어노테이션을 엔티티 클래스에 적용한다면 수정된 데이터만 사용해서 Update 쿼리를 전송하는 것이 가능하다. ( 보통 칼럼이 30개 이상이면 동적 수정 쿼리를 사용하는 것이 더 빠르다고 한다.)

엔티티 삭제

Member memberA = em.find(Member.class, "memberA");
em.remove(memberA);

엔티티 등록과 마찬가지로 즉시 삭제되는 것이 아닌 쓰기 지연 저장소에 삭제 쿼리가 저장되었다가 커밋이 발생하면 flush를 호출해서 db와 동기화되면서 쿼리가 db로 전송된다.


플러시

영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업

플러시를 실행하면 일어나는 일

  1. 변경감지 동작
  2. 쓰기 지연 저장소의 쿼리를 DB에 전송

플러시 호출

  • em.flush()로 직접 호출
    • 강제로 호출하는 것이기 때문에 거의 사용안함.
  • 트랜잭션 커밋 시 자동 호출
    • 플러시를 호출해야 영속성 컨텍스트의 변경 내용이 db에 동기화되므로 커밋이 되기전에 꼭 해야 한다.
  • JPQL 쿼리 실행 시 자동 호출

준영속

영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태

영속성 컨텍스트가 지원하는 어떤 기능도 동작하지 않는다.

만드는 방법

  • em.detach(entity)
    • 특정 엔티티만 준영속 상태로 전환된다.
    • 1차캐시, 쓰기 지연 저장소에 저장된 해당 엔티티에 대한 정보가 모두 삭제된다.
  • em.clear()
    • 영속성 컨텍스트 초기화
  • em.close()
    • 영속성 컨텍스트 종료

특징

  • 거의 비영속 상태에 가깝다.
    • 영속성 컨텍스트가 지원하는 어떤 기능도 동작하지 않으므로
  • 식별자 값을 가지고 있다.
    • 영속 상태였던 엔티티이므로 반드시 식별자 값을 가지고 있다.
  • 지연 로딩을 할 수 없다.

병합

준영속 상태의 엔티티를 영속 상태로 변경하려면 병합을 사용할 수 있다. (em.merge(entity))

준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다.

(출처 : 자바 ORM 표준 JPA 프로그래밍)
  1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
  2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다.(병합한다.)
  3. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행

병합은 파라미터로 넘어온 엔티티의 식별자 값으로 조회해서 없으면 새로 생성해서 병합한다.

병합을 사용하면 파라미터로 넘어온 엔티티의 모든 필드 값으로 속성이 변경된다. 따라서 병합 시 값이 없으면 null로 업데이트 할 위험도 있다. (실무에서는 잘 사용 안함)

profile
큰모래

0개의 댓글