Skip to content

Latest commit

 

History

History
3194 lines (2473 loc) · 170 KB

자바ORM표준JPA프로그래밍.md

File metadata and controls

3194 lines (2473 loc) · 170 KB

자바 ORM 표준 JPA 프로그래밍

목차

1장

패러다임의 불일치

  • 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 집학적인 사고를 요구한다. 그리고 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.
  • 객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라 한다.
  • 문제는 이러 객체와 관계형 데이터베이스 사이의 패러다임 불일치 문제를 해결하는데 너무 많은 시간과 코드를 소비하는 데 있다.

상속

  • 객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없다.
  • 그나마 데이터베이스 모딜링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계할 수 있다.

JPA와 상속

  • JPA는 상속과 관련돤 패러다임의 불일치 문제를 개발자 대신 해결해준다. 개발자는 마치 자바 컬렉션에 객체를 저장하듯이 JPA에게 객체를 저장하면 된다.

연관관계

  • 객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다. 반면에 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 해서 연관된 테이블을 조회한다.

객체를 테이블에 맞추어 모델링

class Member {
    String id; // MEMBER_ID 칼럼사용
    Long teamId;  // TEAM_ID PK 칼럼사용
    String username; //USERNAME 칼럼사용
}

class Team {
    Loing id; //TEAM_ID PK 사용
    String name; //NAME 칼럼사용
}
  • 관계형 데이터베이스는 조인이라는 기능이 있음으로 외래 키의 값을 그대로 보관해도 된다. 하지만 객체는 연관된 객체의 참조를 보관해야한다 다음 처럼 구조를 통해 연관돤 객체를 찾을 수 있다.

객체지향 모델링

class Member {
    String id; // MEMBER_ID 칼럼사용
    Team team; // 참조로 연관관계를 맺는다.
    String username; //USERNAME 칼럼사용
}

class Team {
    Loing id; //TEAM_ID PK 사용
    String name; //NAME 칼럼사용
}
  • 외래 키의 값을 그대로 보관하는 것이 아니라 연관된 Team의 참조를 보관한다.
  • 그런데 이처럼 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지 않다.
    • Member 객체는 team 필드로 연관관계를 맺고 Member 테이블은 TEAM_ID 외래 키로 연관관계를 맺기 때문이다.
    • 반면에 테이블은 참조가 필요 없고 외래 키만 있으면 된다. 결국 개발자가 중간에 변화 역할을 해야한다.

저장

  • 객체를 데이터베이스에 저장하면 team 필드를 TEAM_ID 외래 키 값으로 변환 해야한다.
  • 다음처럼 외래 키 값을 찾아서 INSERT SQL을 만들어야 한다.
member.getId(); //Member_ID PK에 저장
member.getTeam.getId(); //Team_ID PK에 저장
member.getUsername(); //Username 칼럼에 저장

조회

  • 조회할 때는 TEAM_ID 외래 키 값을 Member 객체의 team 참조로 변환해서 객체에 보관해야 한다.
SLELECT M.*, T.*
    FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

public Member find(String memberId) {
    //SQL 실행
    Member member = new Member();
    ...
    
    //데이터베이스에 조회한 회원 관련 정보를 모두 입력
    Team team = new Team();

    member.setTeam(team);
    return member;
}
  • 이러한 과정들은 모두 패러다임 불일치를 해결햐려고 소모하는 비용이다. 만약 자바 컬렉션에 회원 객체를 저장한다면 이런 비용이 전혀들지 않는다.

JPA와 연관관계

  • JPA는 연관관계와 관련된 패러다임의 불일치 문제를 해결해준다.
member.setTeam(team); //회원과 팀 연관관계 설정
jpa.persust(member); //회원과 연관관계 함께 저장
  • 개발자는 회원과 팀의 관계를 설정하고 회원 객체를 저장하면 된다.
  • JPA는 team의 참조를 외래 키로 변환해서 적적한 INSERT SQL을 데이터베이스에 절달한다.

객체 그래프 탐색

  • 객체에서 회원이 소속된 팀을 조회할 때는 다음처럼 참조를 사용해서 연관된 팀을 찾으면 되는데 이것을 객체 그래프 탐색이라고 한다.
Team team = member.getTeam(); // 객체를 참조해서 객체 그래프 탐색
member.getOrder().goetOderItem().... // 자율운 객체 그래프 탐색

member.getOder(); // null 인경우에는 객체 그래프 탐색을 할 수 가 없다.
  • 에를들어 MemberDAO에서 member 객체를 조회할 때 이런 SQL을 실행해서 회원과 팀에 대한 데이터만 조회했다면 member.getTeam()은 성공하지만 다음처럼 다른 객체 그래프는 데이터가 없다면 탐색할 수 없다.
  • SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있을지 정해야한다.
  • 이것은 객체지향 개발자에겐 너무 큰 제약이다. 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없기 때문이다.

JPA와 객체 그래프 탐색

  • JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다.
  • JPA는 연관된 객체를 사용하는 시점에서 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다.
  • 이 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라고 한다.
  • Member를 사용할 때 마다 Order를 함께 사용하면, 이렇게 한 테이블씩 조회하는 것보다 Member를 조회하는 시점에서 SQL 조인을 사용해서 Member와 Order를 함께 조회하는 것이 효과적이다.

비교

  • 데이터베이스는 기본키의 값으로 로우를 구분한다. 반면에 객체는 동일성 비교와 동등성 비교라는 두 가지 비교 방법이 있다.
  • 동일성 비교는 == 비교다. 객체 인스턴스의 주소 값을 비교한다.
  • 동등성 비교는 equals() 메서드를 사용해서 객체 내부의 값을 비교한다.

JPA란 무엇인가?

  • JPA는 자바 진영의 ORM 기술 표준이다.
  • ORM은 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.
  • ORM 표준 프레임워크는 단순히 SQL을 개발자 대신 생성해서 데이터베이스에 전달해주는 것 뿐만아니라 앞서 이야기한 다양한 패러다임 의 불일치 문제를 해결해 준다.

왜 JPA를 사용해야 하는가?

생산성

  • 자바 컬렉션에 객체를 저장하듯이 JPA에서 저장할 객체를 전달하면 된다
  • 반복적인 코드와 CRUD용 SQL을 개발자가 직접 작성하지 않아도 된다.
  • DDL 문을 자동으로 생성해주는 기능들도 데이터베이스 걸계 중심의 패러다임에서 객체 설계 중심으로 역전시킬 수 있다.

유지보수

  • SQL을 직접다루면 엔티티에 필드를 하나만 추가해도 관련된 등록, 수정, 조회 SQL과 결과를 매핑하기 위한 JDBC API 코드를 모두 변경해야 했다. 반면 JPA를 사용하면 이런 과정을 JPA가 대신 처리해줌으로 필드를 추가하거나 삭제해도 수정해야 할 코드가 줄어 든다.
  • 객체지향 언어가 가진 장점들을 활용해서 유연하고 유지보수하기 좋은 도메인 모델을 편리하게 설계할 수 있다.

패러다임의 불일치 해결

  • JPA는 상속, 연관관계, 객체 그래프 탐색 비교하기가와 같은 패러다임의 불일치 문제를 해결해준다.

성능

  • JPA는 애플리케이션과 데이터베이스 사이에서 다양한 서능 최적화 기회를 제공한다.
  • 예를들어 SELECT SQL을 한 번만 데이터베이스에 전달하고 두 번째는 조회한 회원 객체를 재사용한다.
  • 그 밖에 다양한 캐싱 기술들이 있고 이것을 적극 활용하면 성능이 좋아질 수 있다.

데이터접근 추상화와 벤더 독립성

  • 관계형 데이터베이스는 같은 기능도 벤더마다 사용벙이 다른 경우가 많다. 단적인 예로 페이징 처리는 데이터베이스마다 달라서 사용법이 각각 배워야한다.
  • 결국 애플리케이션은 처음 선택한 데이터베이스 기술에 종속되고 다른 데이터 베이스고 변경하기 매우 어렵다.

2장

애플리케이션 개발

pulbic class JpaMain {
    
    public static void main(String[] args) {
    // [엔티티 매니저 팩토리] - 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
    // [엔티티 매니저] - 생성
    EntityManger em = emf.createEntityManager();
    // [트랜잭션] - 획득
    EntityTransaction tx = em.getTransaction();

    try {
        tx.begin(); // [트랜잭션] - 시작
        logic(em) // 비즈니스 로직 실행
        tx.commit(); // [트랜잭션] - 커밋
    } catch (Exception e) {
        tx.rollback(); // [트랜잭션] - 롤백
    } finally {
        em.close(); // [엔티티 매니저 종료]
    }
    emf.close(); // [엔티티 매니저 팩토리 종료]-
    }
    
    //비지스 로직
    private static void logic (EntityManager em) {...}
}
  • 엔티티 매니저 설정
  • 트랜잭션 관리
  • 비즈니스 로직

엔티티 매니저 설정

Persistence             2. 생성 -> EnityManagerFactory

1. 설정 정보                            3. 생성  
    
META-INF/                           EntityManager...
persistence.xml                     EntityManager...

엔티티 매니저 팩토리 생성

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
  • JPA를 시작하려면 우선 persistence.xml의 설정 정보를 사용해서 엔티티 매니저 팩토리를 생성 해야한다.
  • 이때 persistence.xml의 설정정보를 읽어서 JPA를 동작시키기 위한 기반 객체를 만들고 JPA 구현체에 따라서 데이터베이스 커넥션 풀도 생성하므로 엔티티 매니저 팩토리르 생성하는 비용은 아주 크다.
  • 엔티티 매니저 팩토리는 애플리케이션 전체에서 딱 한번만 생성하고 공유해서 사용 해야한다.

엔티티 메니저 생성

EntityManger em = emf.createEntityManager();
  • 엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다. JPA의 기능 대부분은 이 엔티티 매니저가 제공한다. 대표적으로 엔티티 매니저를 사용해서 엔티티를 데이터베이스에 등록/수정/삭제/조회 할 수 있다.
  • 엔티티 매니저는 내부에 데이터소스를(데이터베이스 커넥션)를 유지하면서 데이터베이스와 통신한다.
  • 따라서 애플리케이션 개발자는 엔티티 매니저를 가상의 데이터베이스로 생각할 수 있다.
  • 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있음으로 스레드 간에 공유하거나 재사용하면 안된다.

종료

  • 마지막으로 사용이 끝난 엔티티 매니저는 다음처럼 반드시 종료 되어야 한다.
em.close(); // [엔티티 매니저 종료]
  • 애플리케이션 종료할 때 엔티티 매니저 팩토리도 다음처럼 종료 해야한다.
emf.close(); // [엔티티 매니저 팩토리 종료]-

트랜잭션 관리

EntityTransaction tx = em.getTransaction();
try {
    tx.begin(); // 트랜잭션 시작
    logic(em); // 비즈니스 로직 실행
    tx.commit(); // 트랜잭션 커밋
}catch (Exception e) {
    tx.rollback(); // 예외발생시 트랜잭션 롤백
}
  • JPA를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야 한다.
  • 트랜잭션 없이 데이터 베이스를 변경하면 예외가 발생한다. 트랜잭션을 시작하려면 엔티티 메니저 에서 트랜잭션 API를 받아와야 한다.
  • 트랜잭션 API를 사용해서 비즈니스 로직이 정상 동작하면 트랜잭셔을 커밋하고 예외가 발생하면 롤백 한다.

비즈니스 로직

public static void logic(EntityManager em){
    String id = "id";
    Member emember = new Member();
    member.setId(id);
    member.setUsername("yun");
    member.setAsge(28);

    // 등록
    em.persist(member);

    // 수정
    member.setAge(20);

    // 한건 조회
    Member findMember = em.find(Member.class, id);
    
    // 목록 조회
    List<Member> members = em.createQuery("select m from member m", Member.class);
    
    // 삭제
    em.remvoe(member);
    
}

JPQL

  • 테이블이 아닌 엔티티 객체를 대상으로 검색하려면 데이터베이스의 모든 데이터를 애플리케이션으로 불러와서 엔티티 객체로 변경한 다음 검색 해야하는데, 이는 사실상 불가능하다.
  • 애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 결국 검색 조건이 포함된 SQL을 사용해야한다. JPA는 JPQL 이라는 쿼리 언어로 이런 문제를 해결한다.
  • JPQL은 엔티티 객체를 대상으로 쿼리한다. 쉽게 이야기해서 클래스와 필드를 대상으로 쿼리한다.
  • SQL은 데이터베이스 테이블을 대상으로 쿼리한다.
  • JPA는 JPQL을 분색해서 다음과 같은 적절한 SQL을 만들어 데이터베이스에서 데이터를 조회한다.

3장 영속성 관리

  • JPA가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다.
  • 이 장에서는 매핑한 엔티티를 엔티티 매니저를 통해서 어떻게 사용하는지 알아보자.
  • 엔티티 매니저는 엔티티를 저장하고, 수정하고, 삭제하고, 조회하는 등 엔티티와 관련된 모든 일을 처리한다. 이름 그대로 엔티티를 관리하는 관리자다.
  • 개발자 입장에서 엔티티 메니저를 엔티티를 저장하는 가상의 데이터베이스로 생각하면 된다.

엔티티 매니저 팩토리와 엔티티 매니저

  • 데이터베이스를 하나만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성한다.
//공장 만들기, 비용이 아주 많이 든다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");

Persistence.createEntityManagerFactory("jpabook");를 호출하면 META-INF/persistence.xml에 있는 정보를 바탕으로 EntityManagerFactory를 생성한다.

이제부터 필요할 때마다 엔티티 매니저 팩토리에서 엔티티 매니저를 생성하면 된다.

//공장에서 엔티티 매니저 생성, 비용이 거의 안든다.
EntityManager em = emf.createEntityManager();

엔티티 매니저 팩토리는 이름 그대로 엔티티 매니저를 만드는 공장인데, 공장을 만드는 비용은 상당히 크다. 따라서 한 개만 만들어서 애플리케이션 전체에서 공유 하도록 설계되어 있다. 반면 공장에서 엔티티 매니저를 생성하는 비용은 거의들지 않는다. 그리고 엔티티 매니저 팩토리라는 여러 스레드가 동시에 접근해도 안전함으로 서로 다른 스레드간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생함으로 스레드 간에 절대 공유하면 안된다.

  • 하나의 EntityManagerFactory에서 다수의 엔티티 매니저를 생성했다.
  • EntityManager1은 아직 데이터베이스 커넥션을 사용하지 않는데, 엔티티 매니저는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다.예를 들어 트랜잭션을 시작할 때 커넥션을 획득한다.
  • 하이버네이트를 포함한 JPA 구현체들은 EntityMnagerFactory를 생성할때 커넥션풀도 만드는데(persistence.xml에 보면 데이터베이스 접속 정보가 있다).

영속성 컨텍스트란 ?

  • JPA를 이해하는데 가장 중요한 용어는 영속성 컨텍스트다.
  • 우리말로 변역하기 어렵지만 해석하자면 엔티티를 영구 저장하는 환경 이라는 뜻이다
  • 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
em.persist(member);
  • 지금까지 이 코드를 단순히 회원 엔티티를 저장한다고 표현했다. 정확히 이야기하면 persist() 메서드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.
  • 지금까지 영속성 컨텍스트를 직접 본 적은 없을 것이다. 이것은 논리적인 개념에 가깝고 눈에 보이지도 않는다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다. 그리고 엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고 영속성 컨텍스트를 관리할 수 있다.

엔티티의 생명주기

비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태

  • 엔티티 객체를 생성했다. 지금은 순수한 객체 상태이며 아직 저장하지 않았다. 따라서 영속성 컨텍스트나 데이터베이스 와는 전혀 관련이 없다. 이것을 비영속성 상태라고한다
// 객체를 생성한 상태(비영속)
Member member= new Member();
member.setId("member1");
member.setUsername("회원1");

영속(managed): 영속성 켄텍스트에 저장된 상태

  • 엔티티 매니저를 통해서 엔티티 영속성 컨텍스트에 저장했다. 이렇게 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 한다. 이제 회원 엔티티는 비영속 상태에서 영속 상태가 되었다.
  • 영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻이다.
  • em.find(), JPQL을 사용해서 조회한 엔티티 영속성 컨텍스트가 관리하는 영속성 상태이다.
// 객체를 저장한 상태 (영속)
em.persist(member);

준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태

  • 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다.
  • 특정 엔티티를 준영속 상태로 만들러면 em.detach()를 호출하면 된다. em.close()를 호출해서 영속성 컨텍스트를 닫거나 em.clear()를 호출해서 영속성 컨텍스트를 초기화해도 영속성 컨텍스트가 관리하던 영속 상태의 엔티티는 준영속 상태가 된다.
// 객체를 삭제한 상태(삭제)
em.detach(member);

삭제(removed): 삭제된 상태

  • 엔티티를 영속성 컨텍스와 데이터베스에서 삭제한다.
// 객체를 삭제한 상태 (삭제)
em.remove(member);

영속성 컨텍스트의 특징

영속석 컨텍스트와 식별자(PK) 값

  • 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분한다. 따라서 영속성 상태는 실벽자 값이 반드시 있어야 한다. 식별자 값으 없으면 예외가 발생한다.

영속성 컨텍스트와 데이터베이스 저장

  • 영속성 컨텍스트에 엔티티를 저장하면 이 엔티티는 언제 데이터베이스에 저장될까? JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터 베이스에 반영하는데 이것을 플러시(flush) 라고 한다

영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지
  • 지연 로딩

엔티티 조회

  • 영속성 컨텍스트 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 한다.
  • 영속성 상태의 엔티티는 모두 이곳에 저장된다. 쉡게 이야기하면 영속성 컨텍스트 내부에 Map 이 하나 있는데 키는 @Id로 매핑한 식별자고 값은 엔티티 인스턴스다.

데이터베이스에서 조회

만약 em.find()를 호출했느데 엔티티가 1차 캐시에 없다면 엔티티 매니저는 데이터베이스를 조회해서 엔티티를 생성한다. 그리고 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환한다.

Member findMember2 = em.find(Member.class, "member2");
@Id Entity
"member1" member1
"member2" member2
  1. em.find(Member.class, "member2")를 실행한다
  2. member2가 1차 캐시에 없음으로 데이터베이스에서 조회한다
  3. 조회한 데이터로 member2 엔티티를 생성해서 1차 캐시에 저장한다(영속 상태)
  4. 조회한 엔티티를 반환한다.

이제 member1, member2 엔티티 인스턴스는 1차 캐시에 있다. 따라서 이 엔티티들을 조회하면 메모리에 있는 1차 캐시에서 바로 불러온다. 따라서 성능상 이점을 누릴 수 있다.

영속 엔티티의 동일성 보장

다음 코드를 통해 식별자가 같은 엔티티 인스턴스를 조회해서 비교해보자

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

println(a == b) // 동일성 비교

em.find(Member.class, "member1")를 반복해서 호출해도 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다. 따라서 둘은 같은 인스턴스고 결과도 참이다.

영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다.

엔티티 등록

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다
transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
// 여기까지는 INSERT SQL을 데이터베이스에 보내지 않는다

// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장서에 INSERT SQL을 차곡차곡 모아둔다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이이터베이스에 보내데 이것을 트랜잭션을 지원하는 쓰기 지연 이라고 한다.

동작흐름
  • 회원 A를 영속화했다. 영속성 컨텍스트는 1차 캐시에 회원 엔티티를 저장하면서 동시에 회원엔티티 정보로 등록 쿼리를 만든다. 그리고 만들어진 등록 쿼리를 쓰기 지연 SQL 저장소에 보관한다.
  • 회원 B를 영속화 했다. 마찬가지로 회원 엔티티 정보로 등록 쿼리를 생성해서 쓰기지연 SQL 저장소에 보관한다. 현재 쓰기 지연 SQL 저장소에는 등록 쿼리가 2건 저장되었다.
  • 마지막으로 트랜잭션을 커밋했다. 트랜잭션을 커밋하면 엔티티 매니저는 우선 영속성 컨텍스트를 플러시한다.
  • 플러시는 영속성 컨텍스트의 변경 내용들을 데이터베이스에 동기화하는 작업인데 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보낸다. 이렇게 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화한 후에 실제 데이터베이스 트랜잭션을 커밋한다.

트랜잭션을 지원하는 쓰기 지연이 가능한 이유

begin(); // 트랜잭션 시작

save(A);
save(B);
save(C);

commit(); // 트랜잭션 커밋
  1. 데이터를 저장하는 즉시 등록 쿼리를 데이터베이스에 보낸다. 예제에서 save 메서드를 호출 할 때 마다 즉시 데이터베이스에 등록 쿼리를 보낸다. 그리고 마지막에 트랜잭션을 커밋한다.
  2. 데이터를 저장하면 등록 쿼리를 데이터베이스에 보내지 않고 메모리에 모아 둔다. 그리고 트랜잭션을 커밋할 때 모아둔 등록 쿼리를 데이터베이스에 보낸후에 커밋한다.

트랜잭션 범위 안에서 실행되므로 둘이 결과는 같다. A, B, C 모두 트랜잭션을 커밋하면 함께 저장되고 롤백하면 함께 롤백된다. 등록 쿼리를 그때 그때 데이터베이스에 전달해도 트랜잭션을 커밋하지 않으면 아무 소용이 없다. 어떻게든 커밋 직전에만 데이터베이스에 SQL 전달하면 된다. 이것이 트랜잭션 지원하는 쓰기 지연이 가능한 이유다. 이 기능을 잘 활용하면 모아둔 등록 쿼리를 데이터베이스에 한 번에 전달해서 성능성을 최적화 할 수 있다. (어차피 1, 2 커밋 롤백의 트랜잭션 원자성을 동일하니 모아서 한번에 하는게 더 빠르다는 이야기인거 같다.)

엔티티 수정

SQL 수정의 문제점

  • SQL을 사용하면 수정 쿼리를 직접 작성해야 한다. 그런데 프로젝트가 점점 커지고 요구사항이 늘어나면 수정 쿼리도 점점 추가 된다. 다음은 회원의 이름 변경하는 SQL이다
UPDATE MEMBER
SET
    NAME = ?,
    AGE = ?
WHERE
    ID = ?

회원의 이름과 나이를 변경하는 기능을 개발했는데 회원 등급을 변경하는 기능이 추가되면 회원의 등급을 변경하는 쿼리도 수정 해야한다.

UPDATE MEMBER
SET
    GRADE = ?
WHERE
    ID = ?

보통은 이렇게 2개의 수정 쿼리를 작성한다. 물론 둘은 합쳐서 다음과 같은 하나의 수정 쿼리만 사용해도 된다.

UPDATE MEMBER
SET
    NAME = ?,
    AGE = ?,
    GRADE = ?
WHERE
    ID = ?

하지만 합친 쿼리를 사용해서 이름과 나이를 변경하는데 실수로 등급 정보를 입력하지 않거나, 등급을 변경하는데 실수로 이름과 나이를 입력하지 않을 수도 있다. 결국 부담스러운 상황을 피하기 위해 수정 쿼리를 상황에 따라 계속 추가한다. 이런 개발 방식의 문제점은 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 한다. 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 된다.

변경감지

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작

//  영속성 엔티티 조회
Member memberA = em.find(Member.class, "memberA");

// 영속성 엔티티 데티어 수정
memberA.setUsername("hi");
memberA.setAge(10);

// em.update(member) 이런 코드가 있어야하지 않을까?
transaction.commit(); // [트랜잭션] 커밋
  • JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다. 트랜잭션 커밋 직전에 주석으로 처리된 em.update()메서드를 실행 해야 할거 같지만 이런 메서드는 없다. 엔티티의 데이터만 변경했는데 어떻게 데이터베이스에 반영되는걸까 ?
  • 이렇게 엔티티의 변경사항을 데이터베이스에 자동으로 반영해야하는 기능을 반경 감지 라고 한다

JPA는 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.

  1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시가 호출된다.
  2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
  3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
  4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
  5. 데이터베이스 트랜잭션을 커밋한다.

변경 감지 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용 된다. 비영속, 준영속 처럼 영속성 컨텍스트의 관리를 받지 못하는 엔티티는 값을 변경해도 데이터베이스에 반영되지 않는다.

변경 감지로 인해 실행된 UPDATE SQL을 자세히 알아보자. 방금 본 예제처럼 회원의 이름과 나이만 수정하면 변경된 부분만 사용해서 다음과 같은 동적 수정 쿼리가 생성될 것으로 예상할 수 있다.

UPDATE MEMBER
SET 
    NAME = ?,
    AGE = ?
WHERE
    ID = ?

하지만 JPA의 기본전략은 엔티티의 모든 필드를 업데이트한다.

UPDATE MEMBER
SET 
    NAME = ?,
    AGE = ?,
    GRADE = ?,
    ....
WHERE
    ID = ?

이렇게 모든 필드를 사용하면 데이터베이스에 보내는 데이터 전송량이 증가하는 단점이 있지만, 다음과 같은 장점으로 인해 모든 필드를 업데이트 한다.

  • 모든 필드를 사용하면 수정 쿼리가 항상같다.(물론 바인딩되는 데이터는 다르다). 따라서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다.
  • 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파생된 쿼리를 재사용할 수 있다.

필드가 많거나 저장되는 내용이 너무 크면 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성하는 전략을 선택하면 된다. 단 이 때는 하이버네이트 확장 기능을 사용 해야 한다

@Entity
@org.hibernate.annotations.DymaicUpdate
@Talbe(name = "meber")
public class Member {...}

DymaicUpdate 어노테이션을 사용하면 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성한다. 참고로 데이터를 저장할 때 데이터가 존재하는 (null이 아닌) 필드만으로 INSERT 생성하는 @DynamicInsert도 있다.

상황에 따라 다르지만 칼럼이 대략 30개 이상되면 기본 방법인 정적 수정 쿼리보다 DymaicUpdate를 사용한 동적 수정이 빠르다고 한다. 가장 정확한 것은 본인의 환경에서 직접 테스트 해보는 것이다. 추천하는 방법은 기본전략을 사용하고, 최적화가 필요할 정도로 느리다면 그 때 전략을 수정하면 된다. 참고로 한 테이블 칼럼이 30개 이상이 된다는 것은 테이블 설계상 책임이 적절히 분리되지 않았을 가능성이 높다.

엔티티 삭제

엔티티를 삭제하려면 먼저 삭제 대상 엔티티를 조회해아 한다.

Member memberA = em.find(Member.class, "memberA"); // 삭제 대상 엔티티 조회
em.remove(memberA); // 엔티티 삭제
  • em.remove()에 삭제 대상 엔티티를 넘겨주면 엔티티를 삭제한다. 물론 엔티티를 즉시 삭제하는 것이 아니라 엔티티 등록과 비슷하게 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록한다. 이후 트랜잭션을 커밋해서 플러시를 호출하면 실제 데이터베이스에 삭제 쿼리를 전달한다.

플러시

  • 플러시는 속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
  • 플러시를 실행하면 구체적으로 다음과 같은일이 일어난다.
  1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샵과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 들어간다.
  2. 쓰기 지연 SQL 저장소에 쿼리를 데이터베이스에 전송한다.(등록, 수정, 삭제 쿼리)

영속성 컨텍스트를 플러시하는 방법은 3가지다.

  1. em.flush()를 직접 호출한다.
  2. 트랜잭션 커밋시 플러시가 자동 호출 된다.
  3. JPQL 쿼리 실행시 플러시가 자등 호출된다.

직접 호출

엔티티 매니저의 flush() 메서드를 직접 호출해서 영속성 컨텍스트를 강제로 풀러시한다. 테스트나 다른 프레임워크와 JPA를 함께 사용 할 때를 제외하고 거의 사용하지 않는다.

트랜잭션 커밋시 플러시 자동호출

데이터베이스에 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 데이터베이스에 반영되지 않는다. 따라서 트랜잭션을 커밋하기 전에 꼭 플러시를 호출해서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해야 한다. JPA는 이런 문제를 예방하기 위해 트랜잭션을 커밋할 때 플러시를 자동적으로 호출한다.

JPQL 쿼리 실행 시 플러시 자동 호출

JPQL이나 Criteria 같은 객체지향 쿼리를 호출할 때도 플러시가 실행된다. 왜 JPQL 쿼리를 실핼 할 때 플러시가 자동으로 호출될까 ?

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

// 중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

먼저 em.persist()를 호출해서 엔티티 memberA, memberB, memberC를 영속 상태를 만들었다. 이 엔티티들은 영속성 컨텍스트에는 있지만 아직 데이터베이스에는 반영되지 않았다. 이때 JPQL을 실행하면 어떻게될까? JPQL은 SQL로 변횐되어 데이터베이스에서 엔티티를 조회한다. 그런데 memberA, memberB, memberC는 아직 데이터베이스에 없음으로 쿼리 결과로 조회되지 않는다. 따라서 쿼리를 실행 하기직전에 영속성 컨텍스트를 플래시해서 변경 내용을 데이터베이스에 반영해야 한다. JPA는 이런 문제를 예방하기 위해 JPQL을 실행할 때도 플러시를 자동 호출한다. 따라서 memberA, memberB, memberC 쿼리 결과에 포함된다. 참고로 식별자를 기준으로 죄하는 find() 메서드를 호출 할 때는 플러시가 실행되지 않는다.

플러시 모드 옵션

엔티티 매니저에 플러시 모드를 직접 지정하려면 FlushModeType을 사용하면 된다.

  • FlushModeType.AUTO: 커밋이나 쿼리를 시핼할 때 플러시(기본값)
  • FlushModeType.COMMIT: 커밋할 때 만 플러시

플러시 모드를 별도로 설정하지 않으면 AUTO로 동작한다. 따라서 트랜잭션 커밋이나 쿼리 실행 시에 플러시를 자동으로 호출된다. 대부분 AUTO 기본설정을 그대로 사용 한다. 성능 최적화로 COMMIT을 사용할 수 도 있다.

혹시라도 플러시라는 이름으로 인해 영속성 컨텍스트에 보관된 엔티티를 지운다고 생각 하면 안된다. 다시 한번 한 번 강조하지만 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 것이 플러시다. 그리고 데이터베이스와 동기화를 최대한 늦추는 것이 가능한 이유는 트랜잭션이라는 작업 단위가 있기 때문이다. 트랜잭션 커밋직적에만 변경 내용을 데이터베이스에 보내 동기화하면 된다.

준영속

지금까지 엔티티의 비영속 -> 영속 -> 삭제 상태 변화를 알아보았다. 이번에는 영속 -> 준영속의 상태 변화를 알아보자

영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된것을 준영속 상태라 한다. 따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

영속 상태의 엔티티를 준영속 상태로 만드는 방법은 크게 3가지다.

  1. em.detach(entity): 특정 엔티티만 준영속 상태로 전환한다.
  2. em.clear(): 영속성 컨텍스트를 완전히 초기화한다.
  3. em.close(); 영속성 컨텍스트를 종료한다.

엔티티를 준영속 상태로 전환 : detach

Member member = new Member();
member.setId("memberA");
member.setUsername("회원A");

// 회원 엔티티 영속 상태
em.persist(member);

// 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);

transaction.commit()" // 트랜잭션 커밋

회원 엔티티를 생성하고 영속화한 다음 em.detach(member)를 호출했다. 영속성 컨텍스트에게 더는 해당 엔티티를 관리하지 말라는 것이다. 이 메서드를 호출하는 순간 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거 된다.

영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태를 준영속 상태라 한다 이미 준영속 상태임으로 영속성 컨텍스트가 지원하는 어떤 기능도 동작하지 않는다. 심지어 쓰기 지연 SQL 저장소의 INSERT SQL도 제거되어 데이터베이스에 저장되지도 않는다.

정리하자면 영속 상태가 영속성 컨텍스트로부터 관리 되는 상태라면 준영속 상태는 영속성 컨텍스트로부터 분리된 상태다

영속성 컨텍스트 초기화 : clear()

em.detach()가 특정 엔티티 하나를 준영속 상태로 만들었다면 em.clear()는 영속성 컨텍스트를 초기화해서 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다

// 엔티티 조회, 영속 상태
Member member = em.find(MEmber.class, "memberA");

em.clear(); // 영속성 컨텍스트 초기화

// 준영속 상태
member.setUsername("changeName");

준영속 상태이므로 영속성 컨텍스트가 지원하는 변경 감지는 동작하지 않는다 따라서 회원 이림을 변경해도 데이터베이스에 반영되지 않는다.

영속성 컨텍스트 종료 : close()

영성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 된다.

준영속상태의 특징

거의 비영속성에 가깝다

  • 영속성 컨텍스트가 관리하지 않음으로 1차 캐시, 쓰기 지연, 변경 감지, 지연로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.

식별자 값을 가지고 있다.

  • 비영속 상태는 식별자 값이 없을 수도 있지만 준영속 상태느는 이미 한 번 영속 상태 였음으로 반드시 식별자 값을 가지고 있다.

지연 로딩 할 수 없다.

지연 로딩은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트틀 통해서 데이터를 불러오는 방법이다. 하지만 준영속 상태는 영속성 컨텍스트가 더는 관리하지 않으므로 지연로딩 시 문제가 발생한다.

병합 : merge()

준영속 상태의 엔티티를 다시 영속 상태로 변경하라면 병합을 사용하면 된다. merge() 메서드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다.

Member mergeMeber = em.merge(Member);

정리

  • 엔티티 매니저는 엔티티 매니저 팩토리에서 생성한다. 자바를 직접다루는 환경에서는 엔티티 매니저를 만들면 그 내부에 영속성 컨텍스트도 함께 만들어야 진다.
  • 영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 영속성 컨텍스트 덕분에 1처 캐시, 동일성 보장, 트랜잭션 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능을 사용 할 수 있다.
  • 영속성 컨텍스트에 저장한 엔티티는 플러시 시점에 데이터베이스에 반영되는데 일반적으로 트랜잭션 커밋을 할 때 영속성 컨텍스트가 풀러시 된다.
  • 영속성 컨텍스트가 관리하는 엔티티를 영속 상태의 엔티티라 하는데, 영속성 컨텍스트가 해당 엔티티를 더이상 관리하지 못하면 그 엔티티는 준영속 상태의 엔티티라 한다. 준영속 상태의 엔티티는 더이상 영속성 컨텍스트의 관리를 받지 못하므로 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

4장 엔티티 매핑

  • JPA를 사용하는데 가중 중요한 일은 엔티티와 테이블을 정확히 매핑하는 것이다.
이름 어노테이션
객체와 테이블 매핑 @Entity, @Table
기본 키 매핑 @Id
필드와 칼럼 매핑 @Column
연관관계 매핑 @ManyToOne, @JoinColumn

@Entity

  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙어야한다.
  • 저장할 필드에 final을 사용하면 안된다.
  • 기본 생성자는 필수로 만들어야 한다
    • default, public, protected 중에 하나를 만들어야한다.
    • 개인적으로는 protected로 외부에서 쉽게 객체를 생성못하게 하는게 좋은거 같다
    • 자바 리플랙션을 통해서 값들을 바인딩 해야되서이다 (다른 이유도 있을 거같다)

@Table

  • 엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름 그래도 테이블을 사용한다.
  • 개인적으로 생략하지 않는 것이 좋다. 생갹했을 경우 엔티티 클래스의 리네임이 테이블 이름 변경까지 가져올 수 있다.

기본 키 매핑

직접할당

  • 기본 키를 애플리케이션에서 직접 할당한다.

자동 생성

  • 대리 키 사용 방식
  • IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.
  • SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
  • TABLE: 키 생성 테이블을 사용한다.

자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다. 기본 키를 직접할당하려면 @Id만 사용하면 되고, 자동 생성 전략을 사룡하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 전략을 선택하면 된다.

기본 키 직접 할당 전략

@Id
@Colum(name = "id")
private String id;
  • 자바 기본형
  • 자바 래퍼형
  • String
  • java.util.Date
  • java.math.Bigdeimal
  • java.match.BigInteger

기본 키 직접 할당 전략은 em.persist()로 엔티티를 저장하기 전에 애플리케이션에서 기본 키를 직접 할당하는 방법이다.

Board board = new Board();
board.setId("id1"); // 기본 키 직접 할당
em.persist(board);

IDENTITY 전략

IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다. 주로 mysql, postgreSQL, SQL Server, DB2에서 사용한다. mysql은 AUTO_INCREMENT를 사용한다. 이 것은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용 한다.

개발자가 엔티티에 집적 식별자를 할당하려면 @Id 어노테이션만 있으면 되지만 지금 처럼 식별자가 생성되는 경우에 @GneratedValue 어노테이션을 사용하고 식별자 생성 전략을 선택 해야한다. IDENTITY 전략을 사용 하면 @GeneratedValue의 strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다. 이 전략을 사용하려면 JPA는 기본 키 값을 얻어 오기 위해 데이터베이스를 추가로 죄하한다.

@Entity
class Board {
    @Id
    @GeneratedValue(strategy = GerationType.IDENTITY)
    private Loing id;
    ...
}
private statid void logic(EntityManager em){
    Barod baord = new Board();
    em.persist(board);
    System.out.pirntln(board.getId()); // 1(출력) 
}

em.persist()를 호출해서 엔티티를 저장한후 직후에 할당된 식별자 값을 출력했다. 출력된 값 1은 저장 시점에서 데이터베이스가 생성한 값을 JPA가 조회한 것이다.

IDENTITY 전략과 최적화

IDENTITY 전략은 데이터베이스에 INSERT한 후에 기본 키 값을 조회할 수 있다. 따라서 엔티티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회 한다. 하이버네이트는 Statment.getGeneratedKeys() 메서드를 사용해서 데이터베이스와 한 번만 통신한다

주의

엔티티가 영속 상태가 되려면 식별자가 반드시 필요하다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있음으로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다. 따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.

SEQYENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. SEQYENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다. 이 전략은 시퀀시를 지원하는 오라클, H2 등 데이터베이스에서 사용할 수 있다.

IDENTITY 전략과 같지만 내부 동작 방식은 다르다. SEQYENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다. 그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다.(PK 값을 생성하고 그값을 인설트)

반대로 이전에 설명했던 IDENTITY 전략은 먼저 엔티티를 데이터베이스에 저장한 후에 식별자를 조회해서 엔티티 식별자에게 할당한다.

TABLE 전략

키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 칼럼을 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다. 이 전략은 테이블 사용하므로 모든 데이터베이스에 적용 할 수 있다.

AUTO 전략

데이터베이스의 종류도 많고 기본 키를 만든느 방법도 다양하다. generationType.AUTO는 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다. 예를 들어 오라클을 선택하면 SEQUENCE를 mysql을 선택하면 IDENTITY를 사용한다.

AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 것이다. 특히 키 생성 전략이 아직 확동죄지 않은 개발 초기 다계나 프로토타입 개발 시 편리하게 사용할 수 있다.

기본 키 매핑 정리

영속성 컨텍스트는 엔티티를 식별자 값으로 구분하므로 엔티티를 영속 상태로 만들려면 식별자 값이 반드시 있어야 한다. em.persist()를 호출한 직후에 발생하는 일을 식별자 할당 전략로 정리하면 다음 과같다.

  • 직접 할당 : em.persist()를 호출하기 전에 애플리케이션에서 직접 식별자 값을 할당한다. 만약 식별자 값이 없으면 예외가 발생한다.
  • SEQUENCE: 데이터베이스 시숸스에서 식별자 값을 회득한후 영속성 컨텍스트에 저장한다
  • TABLE: 데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다
  • IDENTITY: 데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다

권장하는 식별자 선택 전략 데이터베이스는 기본 키는 다음 3가지 조건을 모두 만족해야한다

  1. null값은 허용하지 않는다
  2. 유일해야한다
  3. 변해서는 안된다

필드와 칼럼 매핑: 래퍼렌스

매핑 어노테이션 설명
@Colnum 칼럼을 매핑한다.
@Enumerated 자바의 enum 타입을 매핑한다.
@Temporal 날짜 타입을 매핑한다.
@Lob BLOG, CLOB 타입을 매핑한다.
@Transient 특정 필드를 데이터베이스에 매핑하지 않는다.
@Access JPA가 엔티티에 접근하는 방식을 지정한다.

@Colnum

@Colnumdms 객체 필드를 테이블 칼럼에 매핑한다. name, nullable이 주로 사용되고 나머지는 잘 사용되지 않는다. insertable, updatealbe 속성은 데이터베이스에 저장되어 있는 정보를 읽기만 하고 실수로 변경하는 것을 바징하고 싶을 때 사용한다.

@Enumerated

자바의 enum 타입을 매핑할 때 사용 한다.

  • EnumType.ORDINAL: enum 순서를 데티어베이스에 저장
  • EnumType.STRING: enum 이름을 데티어베이스에 저장

EnumType.ORDINAL 정의된 순서대로 데티어베이스에 저장 될 수 있어 데이터 크가가 작은 장점이 있지만 이미 지정된 enum의 순서를 변경할 수없다. 또 사람이 인식하기 어렵다는 단점이 있어 String 타입으로 관리하는 것이 좋다고 생각한다

@Temporal

날짜 타입을 매핑할 때 사용 한다.

@Temporal(TemporalType.DATE)
private Date date // 날짜

@Temporal(TemporalType.DATE)
private Date time // 시간

@Temporal(TemporalType.TIMESAMP)
private Date timestamp; // 날짜 + 시간

자바의 Date 타입에는 년월일 시분초가 있지만 데이터베이스에는 date(날짜), time(시간), timestamp(날짜와 시간) 세 가지 타입이 별도로 존재한다.

@Temporal을 생략하면 자바의 Date와 가장 유사한 timestamp로 정의된다.

@Lob

데이터베이스 BLOB, CLOB 타입과 매핑한다.

@Transient

이 필드는 매핑하지 않는다 따라서 데이터베이스에 저장하지 않고 조회하지도 않는다.

@Access

JPA가 엔티티 데이터에 접근하는 방식을 지정한다.

  • 필드 접근
    • AccessType.FIELD로 지정한다.
    • 필드에 직접 접근한다. 필드 접근권한이 private 이어도 접근할 수 있다.
  • 프로퍼티 접근
    • AccessType.PROPERTY 지정한다.
    • 접근자 getter를 사용한다
// 필드 접근 코드
@Entity
@Access(AccessType.FIELD)
public class Member {
    @Id
    private String id;

    private String data1;
    private String data2;
    ...
}

@Id가 필드에 있으므로 @Access(AccessType.FIELD)로 설정한 것과 같다. 따라서 @Access는 새략해도 된다.

// 프로퍼티 접근 코드
@Entity
@Access(AccessType.FIELD)
public class Member {
    private String id;

    private String data1;
    private String data2;

    @Id
    publoc String getId(){
        return id;
    }

    @Column
    pubic String getData1(){
        return data1;
    }

    @Column
    pubic String getData2(){
        return data2;
    }
}

@Id가 프로퍼티에 있으므로 @Access(AccessType.PROPERTY)로 설정한 것과 같다. 따라서 @Access는 생략해도 된다.

정리

JPA는 다양한 기본 키 매핑 전략을 지원한다. 기본 키를 애플리케이션에서 직접 할당하는 방법부터 데이터베이스가 제공하는 기본 키를 사용하는 전략들을 살펴 보았다.

5장 연관관계 매핑 기초

객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 가진다. 객체 관계 매핑ORM 에서 가장 어려운 부분이 바로 객체 연과관계와 테이블 연관관계를 매핑하는 일이다.

객체의 참조와 테이블의 외래 키를 매핑하는 것이 이 장의 목표다. 시작 하기 전에 연관관계 매핑을 이해하기 위한 핵심 키워드를 정리 해보았다

  • 방향
    • [단방향, 양방향]이 있다. 예를들어 회원과 팀이 관계가 있을 때 회원 -> 팀 또는 팀 -> 회원 둘중 한 쪽만 참조하는 것을 당뱡향 관계라 하고, 회원 -> 팀, 팀 -> 회원을 모두 참조하는 것을 양방향 관계라 한다. 방향은 객체에서만 존재하고 테이블 관계는 항상 양방향이다.
  • 다중성
    • [N:1, 1:1, N:M] 다중성이 있다.
  • 연관관계의 주인
    • 객체를 양방향 연곤관계로 만들려면 연관고계의 주인을 정해야 한다.

단방향 연관관계

연관관계 중에서 N:1 단방향 관계를 가장 먼저 이해야 해야한다.

  • 회원과 팀이 있다
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.

객체 연관관계

  • 회원 객체는 Member.team 필드(멤버변수)로 팀 갹체와 연관관계를 맺는다.
  • 회원 객체와 팀 객체는 단방향 관계다.
    • 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알수 없다.
    • member -> team 의 조회는 member.getTeam()으로 가능
    • team -> member를 접근하는 필드는 없다.

테이블 연관관계

  • 회원 테이블은 TEAM_ID 왜리 키로 팀 테이블과 연관관계를 맺는다
  • 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래키를 통해서 회원과 팀을 조인할 수 도 있고 반대로 팀과 회원도 조이을 할 수 있다. 외래키 하나로 양방향으로 조인 할 수 있다.

객체 연관관계와 테이블 연관관계의 가장 큰 차이

참조를 통한 연관관계는 언제나 단방향이다. 객체간의 연관관계를 방향을만들고 싶으면 반대쪽도 필드를 추가해서 참조를 본관해야 한다. 결국 연관관계를 하나 더 만들어야 한다. 이렇게 양쪽에서 서로 참조하는 것을 앙뱡향 연관관계라고 한다. 더 정확히 이야기하자면 이것은 양방향 관계가 아니라 서로 다른 방향 2관계이다. 반면 테이블은 외래 키 하나로 양뱡으로 조인 할 수 있다.

// 단방향 연관관계
Class A {
    B b;
}

// 양방향 연관관계
Class A{
    B b;
}

Class B {
    A a;
}

객체 연관관계 vs 테이블 연관관계

  • 객체는 참조(주소)러 연관관계를 맺는다.
  • 테이블은 외래 키로 연관관계를 맺는다.

이 둘은 비슷해 보이지만 매우 다른 특징을 가진다. 연관된 데이터를 조회할 때 객체는 (a.getB(.getC()))를 사용하지만 테이블은 조인을 사용한다.

  • 참조를 사용하는 객체의 연관관계는 단방향이다.
    • A - > B (a.b)
  • 외래 키를 사용하는 테이블의 연관관계는 양방향이다.
    • A Joinb B 가가능하면 반대로 B Join A도 가능하다
  • 객체를 양방향으로 참조하려면 단뱡향 연관관계를 2개 만들어야 한다.
    • A -> B (a.b)
    • B -> A (b.a)

순수한 객체 연관관계

JPA를 사용하지 않은 순수한 회원과 팀 클래스

class Member {
    private String id;
    private String username;

    private Team team; // 팀잠조를 보관
    
    public void setTeam(Team team){
        this.team = team;
    }

    //getter, setter
}

class Team {
    private String id;
    private String name;

    //getter, setter
}

Team findTeam = member.getTeam(); 

이 처럼 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라고 한다.

테이블 연관관계

데이터베이스는 외래 키를 사용해서 연관관계를 탐색 할 수 있는데 이것을 조인이라 한다.

객체 관계 매핑

@Entity
class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;
    
    
    // 연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team;

    // 연관관계 설정

    public void setTeam(Team team){
        this.them = team;
    }
    //getter, setter
}

@Entity
class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;

    //getter, setter
}
  • 객체 연관관계 : 회원 객체의 Member.team 필드사용
  • 테이블 연관관계 : 회원 테이블의 Member.TEAM_ID 외래 키 칼럼을 사용
  • @ManyToOne
    • 이름 그대로 다대일 관계라는 매핑정보
    • 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션은 필수로 사용해야 한다.
  • @JoinColumn(name = "TEAM_ID")
    • 외래 키를 매핑할 대 사용한다
    • name 속성에는 매핑할 외래 키 이름을 지정한다

연관관계 사용

연관관계를 등록, 수정, 삭제, 조회하는 예제를 통해 연관관계를 어떻게 사용하는지 알아보자

저장

public void testSave() {
    // 팀 1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team);
    
    // 회원1 저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1); // 연관관계 설정 member1 -> team1
    em.persist(member1)


    // 회원2 저장
    Member member1 = new Member("member2", "회원2");
    member2.setTeam(team1); // 연관관계 설정 member2 -> team2
    em.persist(member2)   
}

회원 엔티티는 팀 엔티티를 참조하고 저장했다. JPA는 참조한 팀의 식별자를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.

조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지이다.

  • 객체 그래프 탐색(객체 연관관계를 사용한 조회)
  • 객체지향 쿼리 JPQL을 사용

객체 그래프 탐색

member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.

Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색

이처럼 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라 한다.

객체지향 쿼리 사용

//JPQL 

SELECT 
    m
FROM
     Member m
JOIN 
    m.team 
WHERE
    t.name = :teamName

JPQL의 from Member m joun m.team t 부분을 보면 회원 팀과 관계를 가지고 있는 필드(m.team)을 통해서 Member와 Team을 조인했다 그리고 where 절을 보면 t.name을 검색 조건으로 해서 팀1에 속한 유저들만 검색 했다.

수정

팀1 소속이었던 회원을 새로운 팀2에 소속 하도록 수정 코드

private statid void update(){
    // 새로운 팀 2
    Team team2 = new Team("team2", "팀2")
    em.persist(team2);

    // 회원1에 새로운 팀2 설정
    Member member = em.find(Member.class, "member1");
    member.setTeam(team2);
}

연관관계 제거

회원1을 팀에 소속하지 않도록 변경

private statid void deleteRelration(){
    Member member = em.find(Member.class, "member1");
    member1.setTeam(null); // 연관관계 제거
}

/* SQL 문은 다음과 같다
UPDATE
SET
    TEAM_ID = null, ....
WHERE
    id = 'member1'
*/

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 여관관계를 먼저 제거하고 삭제해야한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다. 팀1에는 회원1과 회원2가 소속되어 있다. 이때 팀1을 삭제하려면 연관관계를 먼저 끊어야 한다.

member1.setTeam(null); // 회원1 연관관계 제거
member2.setTeam(null); // 회원2 연관관계 제거
em.remove(team); // 팀 제거

양방향 연관관계

이번에는 반대 방향인 팀에서 회원으로 접근하는 관계를 추가하자. 그래서 회원에서 팀으로 접근하고 반대 방향인 팀에서도 회원으로 접근 할 수 있도록 양방향 연관관계로 매핑해보자

데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. 따라서 데이터베이스에는 추가할 내용은 전혀 없다.

양방향 연관관계 매핑

@Entity
class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;
    
    
    // 연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // 연관관계 설정

    public void setTeam(Team team){
        this.them = team;
    }
    //getter, setter
}

@Entity
class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;

    // 추가된 부분
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    // 추가된 부분

    //getter, setter
}

팀과 회원은 일대일 관계다. 따라서 팀 엔티티와 컬렉션인 List members를 추가했다. 그리고 일대다 관계를 매핑하기 위해서 @OneToMany를 추가했다. mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드의 이름 값으로 주면된다. 반대쪽 매핑이 Member.team 이므로 team을 값으로 주었다.

일대다 컬랙센 조회

public void biDirection(){
    Team team = em.find(Team.class, "team1");
    List<members> = team.getMembers(); // 팀 -> 회원 객체 그래프 탐색

    for (Member member : members){
        print(member.getUsername());
    }
}
// 결과 
// 회원1
// 회원2

연관관계의 주인

@OneToMany는 직관적으로 이해가 될것이다. 문제는 mappedBy 속성이다

엄밀히 이야기 하자면 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인것 처럼 보이게 할 뿐이다. 객체 양방향 관계는 다음과 같다

객체 연관관계 
    회원 -> 팀 연관관계 1개 (단방향)
    팀 -> 회원 연관관계 1개 (단방향)

테이블 연관관계
    회원 <-> 팀의 연관관계 1개 (양방향)

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까 ? 이런 차이로 JPA에서 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리 해야하는데 이것을 연관관계의 주인 이라 한다

양방향 매핑의 규칙 : 연관관계의 주인

양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 설정 해야 한다. 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

어떤 연관관계를 주인으로 정할지는 mappedBy 속성으로 사용하면 된다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야한다.

그렇다면 어느 Member.class Team.class 둘 중 어떤 것을 연관관계의 주인으로 정해야 할까?

class Member {
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    prviate Team team;
    ...
    
}
class Team {
    @OneToMany
    private List<Member> members = new ArrayList<Member>();
    ...
}

연관관계의 주인을 정한다는 것은 사실 왜리 키 관리자를 선택하는 것이다. 여기서 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 하는데 외래키가 매핑되어 있는 Member 테이블에 있기 때문에 Member에서 하는것이 좋다

연관관계의 주인은 외래 키가 있는 곳

연관관계의 주인은 테이블에 외래 키가 있는 것으로 정해야한다. 여기서 회원 테이블의 외래 키를 가지고 있음으로 Member.team이 주인이 된다. 주인이 아닌 Team.members에는 mappedBy = "team" 속성을 사용해서 주인이 아님을 설정한다. 그리고 mappedBy 속성의 값으로 연관관계의 주인인 Member 엔티티의 team 필드를 의미하는 team을 주면 된다.

class Team {
    @OneToMany(mappedBy = "team") // MappedBy 속성의 값은 연관관계의 주인인 Member.team(멤버필드)
    private List<Member> member = new ArrayList<>();
}

정리하면 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다, 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지 못한다.

양방향 연관관계 저장

public void testSave(){
    // 팀 1 저장
    Team team1 = new Team("team", "팀1");
    em.persist(team1);

    // 회원 1 저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1); // 연관관계 설정 member1 - > team1
    em.persigt(member1);

    // 회원 2 저장
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1); // 연관관계 설정 member2 - > team1
    em.persigt(member2);
}

팀1을 저장하고 회원1, 회원2에 연관관계의 주인인 Member.team 필드를 통해서 회원과 팀의 여관관계를 설정하고 저장했다. 이 코드는 단방향 연관관계에서 살펴본 회원과 팀을 저장하는 코드와 완전히 동일하다

양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다. 따라서 주인이 아닌 방향은 값을 서정하지 않아도 데이터베이스에 외래 키 값이 정상으로 입력된다.

team.getMembers().add(member1); // 무시 연관관계의 주인이 아님
team.getMembers().add(member2); // 무시 연관관계의 주인이 아님

이런 코드가 추가로 있어야 할 것 같지만 Team.member는 연관관계의 주인이 아니다. 주인이 아닌 곳에서 입력 값은 외래 키에 영향을 주지 않는다. 따라서 이전 코드는 데이터베이스에 저장할 때 무시된다.

member1.setTeam(team); // 연관관계의 주인이라서 연관관계 설정 가능
member2.setTeam(team); // 연관관계의 주인이라서 연관관계 설정 가능

양방향 연관관계의 주의점

양방향 연관관계를 설정하고 가장 흔히 실수는 연관관계의 주인에게는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. 데이터베이스에 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해보자 다시 한번 강조하지만 연관관계의 주인만이 외래 키의 값을 변경 할 수 있다.

순수한 객체까지 고려한 양방향 연관관계

그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 지정하지 않아도 될까? 사실은 객체 관점에서 양방향에 모두 값을 입력해주는 것이 가장 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않은 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

예를 들어 JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정 해보자 ORM은 객체와 관계형 데이터베이스 둘다 중요하다.

public void test순수한객체_양방향(){
    // 팀1
    Team team = new Team("team1", "팀1");
    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");

    member1.setTeam(Team1); // 연관관계 설정 member1 - > team1;
    member2.setTeam(Team1); // 연관관계 설정 member2 - > team1;

    List<Member> members = team.getMembers();

    print(member.sizle) // 0 출력
}

예제 코드는 JPA를 사용하지 않는 순수한 객체다. 코드를 보면 Member.team에만 연관관계를 설정하고 반대 뱡향은 연관관계를 설정하지 않았다. 그러니 당연히 size는 결과가 0이 나온다

member1.setTeam(team1); // 회원 -> 팀

양방향은 양쪽다 관계를 설정해야 한다. 이 처럼 회원 -> 팀을 설정하면 다음 코드처럼 반대방향 팀 -> 회원도 설정해야 한다.

team.getMembers().add(member1); // 팀 -> 회원
// 양방향 모두 관계를 설정
public void test순수한객체_양방향(){
    
    // 팀1
    Team team = new Team("team1", "팀1");
    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");

    member1.setTeam(Team1); // 연관관계 설정 member1 - > team1;
    team1.getMembers().add(members1) //연관관계 설정 team1 -> member1;


    member2.setTeam(Team1); // 연관관계 설정 member2 - > team1;
    team1.getMembers().add(members2) //연관관계 설정 team1 -> member2;

    List<Member> members = team.getMembers();

    print(member.sizle) // 2 출력
}

양쪽 모두 관계를 설정했다. 결과도 기대했던 2가 출력된다.

member1.setTeam(team1); // 연관관계의 주인
team1.getMembers().add(members1); // 연관관계의 주인이 아니다. 저장시 사용되지 않는다.

앞서 이야기한 것처럼 객체까지 고려해서 주인이 아닌 곳에도 값을 입력하자

연관관계 편의 메소드

양방향 연관관계는 결국 양쪽 다 신경 써야 한다. 다음처럼 member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다보면 실수로 둘 중 하나만 호출해도 양뱡이 깨질 수 있다. 양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다. 해당 코드를 리팩토링해보자

class Member {
    private Team team;

    public void setTeam(Team team){
        this.team = team;
        team.getMembers().add(this);
    }
    
}

setTeam() 메소드 하나로 양방향 관계를 모두 설정하도록 변경했다.

// 양방향 리팩토링 전체 코드
public void test순수한객체_양방향(){
    
    // 팀1
    Team team = new Team("team1", "팀1");
    em.persist(team1);

    
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1); // 양방향 설정
    em.persist(member1);

    Member member2 = new Member("member1", "회원1");
    member2.setTeam(team1); // 양방향 설정
    em.persist(member2);
}

이렇게 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라고 한다.

연관관계 편의 메서드 작성 시 주의사항

member1.setTeam(teamA); // 1
member1.setTeam(teamB); // 2
Member findMember = teamA.getMember(); // member1이 여젼히 조회된다.

teamB로 변경할 때 teamA -> member1 관계를 제거하지 않았다. 연관관계를 뱐걍힐 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.

// 기존 관계 제거
public void setTeam(Team team) {
    // 기존 팀과 관계 제거
    if(this.team != null){
        this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers.add(tihs);
}

이 코드는 객체에서 소로 다른 단방향 연관관계 2개를 양뱡행 인것처럼 보이게 하려고 얼마나 많은 고민과 수고가 필요한지 보여준다. 반면에 관계형 데이터베이스는 외래키 하나로 문제를 단순하게 해결한다. 정리 하면 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.

정리

단방향 매핑과 비교해서 양방향 매핑은 복잡하다. 연관관계의 주인도 절해야하고, 두 개 의 단방향 연관관계를 양방향으로 만들기 위해서 로직도 잘 관리해야 한다. 중요한 사실은 연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이라는 점이다. 양방향은 여기서 주인이 아닌 연관관계를 하나 추가 했을 뿐이다. 결국 단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된것 뿐이다.

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 단방향을 양방향으로 만들려면 단방향으로 객체 그래프 탐색 기능이 추가된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양방향모두 관리해야 한다.

양방향 매핑은 복잡하다. 비즈니스 로직에 필요에 따라 다르겠지만 우선 단방향 매핑을 사용하고 반대 방향으로 객체 그래프 탐색 기능이 필요 할때 양방향을 사용하도록 코드를 추가해도 된다.

연관관계 의 주인을 정하는 기준

단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다. 하지만 양방향은 연관관계의 주인 이라는 이름으로 인해 오해가 있을 수 있다. 비즈니스의 로직상 더 즁요하다고 연관관계의 주인으로 선택하면 안된다. 비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여해야 한다. 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.

6장 다양한 연관관계 매핑

엔티티의 연과관계를 매핑할 때는 다음 3가지를 고랴해야 한다.

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

먼저 연관관계가 잏는 두 엔티티가 일대일 관계인지 일대다 관계인지 다중성을 고려해야한다. 다음으로 두 엔티티 중 한쪽만 참조하는 단방향 관계인지 서로 탐조하는 양방향 관계인지 고려해야 한다. 마지막으로 양방향 관계면 연관관계의 주인을 정해야 한다.

다중성

  • 다대일
  • 일대다
  • 일대일
  • 다대다

단방향, 양방향

테이블은 외래 키 하나로 조인을 사용해서 양방향으로 쿼리가 가능하므로 사실상 방향이라는 개념이 없다. 반면에 객체는 참조용 필드를 가직고 있는 객체만 연관된 객체를 조회할 수 있다.객체 관계에서 한 쪽만 참조하는 것을 단방향 관계라 하고, 양쪽이 서로 참조하는 것을 양방향 관계라 한다.

연관관계의 주인

데이터베이스는 외래 키 하나로 두 테이블이 연관관계를맺는다. 따라서 테이블의 연관관계를 관리하는 포인티는 외래 키 하나다. 반면 엔티티를 양방향으로 매핑하면 A -> B, B -> A 2곳에서 서로를 참조 한다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳이다.

외래 키를 가진 테이블과 매핑한 엔티티가 외래키를 관리하는게 효율적이므로 보통 이곳을 연관관계의 주인으로 산택하다. 주인이 아닌 방향은 외래 키를 변경할 수 없고 읽기만 가능하다. 연관관계의 주인 mappedBy 속성을 사용하지 않는다. 연과관계의 주인이 아니면 mappedBy 속성을 사용하고 연관관계의 주인 필드 이름을 이름값으로 입력해야 한다.

  • 다대일: 단방향, 양방향
  • 일대다: 단방향, 양방향
  • 일대일: 주 테이블 단방향, 양방향
  • 일대일: 대상 테이블 단방향, 양방향
  • 다대다: 단방향, 양방향

다대일

다대일 관계의 반대방향은 항상 일대다 관계고 일대다 관계의 반대는 방향은 항상 다대일이다. 데이터베이스 테이블의 일, 다 관계에서는 외래 키는 항상 다쪽에 있다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다. 예를 들어 회원(N)과 팀(1)이 있다면 회원쪽이 연관관계의 주인이다.

다대일 단방향 [N:1]

@Entity
class Member {
    @Id
    @GenratedValue
    @Column(name "MEMBER_ID")
    private Long id;
    
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // getter, setter
}

@Entity
class Team {
    @Id
    @GenratedValue
    @Column(name "TEAM_ID")
    private Long id;

    private String name;
    
    // getter, setter
}

회원은 Member.team으러 팀 엔티티를 참조할 수 있지만 반대로 팀에는 회원을 참조하는 필드는 없다 따라서 회원과 팀은 다대일 단방향 연관관계다. @JoinColumn(name = "TEAM_ID")를 사용해서 Member.team 필드를 TEAM_ID 외래 키와 매핑했다.

다대일 양방향 [N:1, 1:N]

@Entity
class Member {
    @Id
    @GenratedValue
    @Column(name "MEMBER_ID")
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public void setTeam(Team team){
        this.team = team;
        //무한 루프에 빠지지 않도록 체크
        if(!team.getMembers().contains(this)){
            team.getMembers().add(this);
        }
        
    }
}

@Entity
class Team {
    @Id
    @GenratedValue
    @Column(name "TEAM_ID")
    private Long id;
    
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArraList<>();

    public void addMember(Member member) {
        this.members.add(member);
        if(member.getTeam() != this){ //무한 루프에빠지지 않도록 체크
            member.setTeam(this);
        }
    }
}

양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.

일대다와 다대일 연관관계는 항상 다(N)에 외래 키가 있다. 여기서는 다쪽인 MEMBER 테이블이 외래 키를 가지고 있으므로 Member.team이 연관관계의 주인이다. JPA는 외래 키를 관리할 때 연관관계의 주인만 사용한다.

양방향 연관관계는 항상 서로를 참조해야 한다.

양뱡행 연관과계는 항상 서로를 차조해야 한다. 어느 한쪽만 참조하면 양방향 연관관계가 성립하지 않는다. 항상 서로를 참조 하려면 연관관계의 편 메소드를 작성하는 것이 좋은데 회원의 setTeam(), 팀의 addMember() 메서드가 이런 편의 메서드드들이다. 편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는데. 양쪽에 다 작성하면 무한루프에 빠지므로 주의 해야한다. 예제 코드는 편의 메소드를 양쪽에 다 작성해서 둘 중 하나만 호출하면 된다. 또 무한 루프에 빠지지 않도록 검사하는 로직도 있다.

일대다

일다대 관계는 다대일 관계의 반대 방향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있도록 자바 컬랙션인 List, Set, Map 중에 하나를 사용해야 한다.

일대다 단방향: [1:N]

하나의 팀은 여러 회원을 참조 할 수 있는데 이런 관계를 일대다 관계라 한다. 그리고 팀은 회원들은 참조하지만 반대로 회원은 팀을 참조 하지 않으면 둘의 관계는 단방향이다.

@Entity
class Team {
    @Id
    @GenratedValue
    @Column(name "TEAM_ID")
    private Long id;
    
    private String name;

    @OneToMany
    @JoinColumn(name = "TEAMD_ID") // MEMBER 테이블의 TEAM_ID (PK)
    private List<Member> members = new ArraList<>();
}

@Entity
class Member {
    @Id
    @GenratedValue
    @Column(name "MEMBER_ID")
    private Long id;

    private String username;
}

팀 엔티티의 Team.members로 회원 테이블의 TEAM_ID 외래 키를 관리한다. 보통 자신이 매핑한 테이블의 외래 키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래 키를 관리한다. 그럴 수밖에 없는 것이 일다대 관계에서 외래 키는 항상 다쪽 테이블에 있다. 하지만 다 쪽인 Member 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없다. 대신 반대쪽인 Team 엔티티에만 참조 필드인 members가 있다 따라서 반대편 테이블의 외래 키를 관리하는 특이한 모습이 나타난다.

일대다 단방향 매핑의 단점

일다대 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다. 본인 테이블에 외래 키가 있으면 엔티티의 저장된 연관관계처리를 INSERT SQL 한 번에 끝낼 수 있지만 다른 테이블에 외래 키가 있으므면 연관 관계 처리를 위한 UPDATE SQL을 추가로 실행 해야한다.

public void testSave() {
    Member member1 = new Member("member1");
    Member member2 = new Member("member2");

    Team team1 = new Team("team1");
    team.getMembers.add(team1);
    team.getMembers.add(team2);

    em.persist(member1); // INSERT member 1
    em.persist(member2); // INSERT member 2
    em.persist(team); // INSERT team, UPDATe member1 fk, UPDATE member2 fk
}
INSERT INTO MEMBER (MEMBER_ID, username) values (null, ?)
INSERT INTO MEMBER (MEMBER_ID, username) values (null, ?)
INSERT INTO TEAM (TEAM_ID, name) values (null, ?)
update Member SET TEAM_ID ? WHERE MEMBER_ID = ?
update Member SET TEAM_ID ? WHERE MEMBER_ID = ?

Member 엔티티는 Team 엔티티를 모른다. 그리고 얀관관계에 대한 정보는 Team 엔티티의 members가 관리한다. 따라서 member 엔티티를 저장할 때는 MEMBER 테이블의 TEAM_ID 외래 키에 아무값도 저장되지 않는다. 대신 TEAM 엔티티를 저장할 때 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래 키를 업데이트 한다.

일대다 단방향 매핑보다는 다대일 양방향 관계를 매핑을 사용하자

일대다 당방향 매핑을 사용하면 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리해야한다. 이것은 성능의 문제도 있지만 관리도 부담스럽다. 문제를 해겨하는 좋은 방법은 일대다 단방향매핑 대신에 다대일 양방향 매핑을 사용하는 것이다. 다대일 양방향 매핑은 관리해야하는 외래 키가 본인 테이블에 있다. 따라서 일대다 단방행 매핑 같은 문제가 발생하지 않는다. 두 매핑의 에이블 모양은 완전히 같으므로 엔티티만 약간 수정하면 된다. 상황에 따라 다르겠지만 일대다 단방향 매핑보다는 다대일 양방향 매핑을 권장한다

일대다 양방향 [1:N, N:1]

일대다 양방향 매핑은 존재 하지 않는다. 대신 다대일 양향향 매핑을 사용해야한다. 더 정확히 말하자면 양방향 매핑에수 @OneToMany는 연관관계의 주인이될 수없다. 왜냐하면 관계형 데이터베이스 특성상 일대다, 다대일, 관계는 항상 다쪽에 외래 키가 있다. 따라서 @OneToMany, @ManyToOne 둘 중에 연관관계의 주인은 항상 다 쪽인 @ManyToOne을 사용 한곳이다. 이런 이유로 @ManyToOne에는 mappedBy 속성이 없다.

그렇다고 일대다 양방향 매핑이 완전히 불가능 한 것은 아니다. 일대다 단방향 매핑을 읽기 전용으로 하나 추가하면 된다.

일대일 [1:1]

일대일 관계는 양쪽이 서로 하나의 관계만 가진다. 예를들어 회원은 하나의 사물함을 사용하고 사물함도 하나의 회원에 의해서만 사용 된다. 1:1 관계는 다음과 같은 특징이 있다.

  • 일대일 관계는 그 반대도 일대일 관계다
  • 테이블 관곙서 일대다, 다대일은 항상 다(N)쪽이 외래 키를 가진다. 반면에 일대일 관계는 주 테이블이나 대상 테이블 중 어느곳이 외래 키를 가줄 수 있다.

테이블은 주 테이블이든 대상 테이블이든 외래 키 하나만 있으면 양쪽으로 조회할 수 있다. 그리고 일대일 관계는 반대쪽도 일대일 관계다. 따라서 이대일 관계는 주 테이블이나 대상 테이블 중 누가 외래 키를 가질지 선택 해야한다

주 테이블에 외래 키

주 객체에 대상 객체를 참조하는것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조 한다. 외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다. 이 방법은 장점은 주 테이블의 외래키를 가지고 있음으로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.

일대일 관계를 구성할 때 객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. JPA도 주 테이블에 외래 키가 있으면 좀더 편리하게 매핑할 수 있다. 주 테이블에 외래 키가 있는 단방향 관계를 먼저 살펴보고 양방향 관계도 살펴보자

단방향

@Entity
class Member {
    @Id
    @GenratedValue
    @Column(name "MEMBER_ID")
    private Long id;

    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

@Entity
class Locker {
    @Id
    @GenratedValue
    @Column(name "LOCKER_ID")
    private Long id;

    private String name;
}

일대일 관계이므로 객체 매핑에 @OneToOne을 사용 했고 데이터베이스에는 LOCKER_ID 외래 키에 유니크 제약조건을 추가했다.

양방향

@Entity
class Member {
    @Id
    @GenratedValue
    @Column(name "MEMBER_ID")
    private Long id;

    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

@Entity
class Locker {
    @Id
    @GenratedValue
    @Column(name "LOCKER_ID")
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker")
    private Member member;
}

양방향이므로 연관관계의 주인을 정해야 한다. MEMBER 테이블의 외래 키를 가지고 있음으로 Member 엔티티에 있는 Member.locker가 연관관계의 주인이다.

대상 테이블에 외래 키

정통적인 데이터베이스 개발자들은 보통 대상 테이블에 외래 키를 두는 것을 선호한다. 이 방법의 장점은 테이블 연관관계를 일대일에서 일다대로 변경할 때 테이블 구조롤 그대로 유질 할 수 있다.

단방향

일대일 관계중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 그리고 이런 모양으로 매핑할 수 있는 방법도 없다. 이 때는 단방향 관계를 Locker에서 Member 방향으로 수정하거나, 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정 해야한다.

양방향

@Entity
class Member {
    @Id
    @GenratedValue
    @Column(name "MEMBER_ID")
    private Long id;

    private String username;

    @OneToOne(mappedBy = "member")
    private Locker locker;
}

@Entity
class Locker {
    @Id
    @GenratedValue
    @Column(name "LOCKER_ID")
    private Long id;

    private String name;

    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면 이렇게 양방향으로 매핑한다. 주 엔티티 Member 엔티티 대신에 대상 엔티티 Locker를 연관관계의 주인으로 만들어 Locker 테이블에 외래 키를 관리 하도록 했다.

주의

플러시를 사용할 때 외래 키를 직접 관리 하지 않은 일대일 관계는 지연로딩으로 설정해도 즉시로딩이 된다. 이것은 프록시의 한계 때문에 발생하는문제 인데 프록시 대신에 bytecode instrumentaition을 사용하면 해결 할 수 있다.

다대다 [N:N]

관계형 데이터베이스는 졍규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어 내는 연결 테이블을 사용한다.

다대다 : 단방향

@Entity
class Member {
    @Id @GenratedValue @Column(name "MEMBER_ID")
    private Long id;

    private String username;

    @ManyToMany
    @JoinTable(
        name = "MEMBER_PRODUCT",
        joinColums = @JoinColumn(name = "MEMBER_ID"),
        inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"),
    )
}

@Entity
class Product {
    @Id @GenratedValue
    private Long id;
    private String name;
}

회원 엔티티와 상품 엔티티를 @ManyToMany로 매핑했다. 여기서 중요한 점은 @ManyToMany, @JoinTable을 사용해서 연결 테이블을 바로 매핑한 것이다. 따라서 회원과 상품을 연결하는 회원 상품 엔티티 없이 매핑을 완료할 수 있다.

  • @JoinTable.name: 연결 테이블을 지정한다. 여기서는 MEMBER_PRODUCT 테이블을 선택했다
  • @JoinTable.jounColumn: 현재 방향인 회원과 매핑할 조인 칼럼 정보를 지정한다. MEMBER_ID로 지정했다.
  • @JoinTable.inverseJoinColumns: 반대 방향인상품과 매핑할 조인 카럼 정보를 지정한다. PRODUCT_ID로 지정했다

MEMBER_RPDUCIT 테이블은 다대다 관계를 일대다, 다대일 관계로 풀어 내기 위해 필요한 연결 테이블 뿐이다. @ManyToMany로 매핑한 덕분에 다대다 관계를 사용할 때는 이 연결 테이블을 신경쓰지 않아도 된다.

// 다대다 관계 저장 코드
public void save(){
    Product productA = new Product();
    productA.setId("ProductA");
    productA.setNAme("상품A");
    em.persist();

    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    member1.getProducts.add(productA); // 연관관계 지정
    em.persist();
}
// 다대다 관계 저장 SQL
INSERT INTO PRODUCT ...
INSERT INTO MEMBER ...
INSERT INTO MEMBER_PRODUCT ...
// 객체 탐색
public void find(){
    Member member = em.find(Member.class, "member1");
    List<Product> products = member.getProducts(); // 객체 그래프 탐색
    for (Product product : products){
        print(product.getName());
    }
}

@ManyToMany 덕분에 복잡한 다대다 관계를 애플리케이션에서 아주 단순하게 사용할 수 있다.

다대다 : 양방향

역방향도 @ManyToMany를 사용한다. 그리고 양쪽 중 원 하는 곳에 mappedBy로 연관관계의 주인을 지정한다.

// 역방향에 추가
@Entity
class Product {
    @Id @GenratedValue
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "products") // 역방향 추가
    private List<Member> members;
}

다대다의 양방향 연관관계는 다음처럼 설정하면 된다.

member.getProducts().add(product);
product.getMembers().add(member);

양방향 연관관계는 연관관계의 편의 메소드를 추가해서 관리하는 것이 편리하다.

public void addProduct(Product product){
    ...
    product.add(product);
    product.getMembers().add(this);
}

연관관계 편의 메소드를 추가했음으로 다음처럼 간단히 연관관계를 설정하면 된다

member.addProduct(product);

다대다: 매핑의 한계와 극복

@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다. 하지만 이 매핑을 실무에서 사용하기에는 한계가 있다. 예를 들어 회원 상품을 주문하면 연결 테이블에 다순히 주문한 회원 아이디와 상품 아이디만 담고 끝나지 않는다. 보통 연결 테이블에 주문 수량 칼럼이나 주문한 날짜 같은 칼럼이 더 필요하다.

그렇다면 결국 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에서 추가한 카럼들을 매핑해야한다. 그리고 엔티티의 간의 관계도 테이블 관계처럼 다대다에서 일대다. 다대일 관계러 풀어야하다.

@Entity
class Member {
    @Id @GenratedValue @Column(name "MEMBER_ID")
    private Long id;

    private String username;

    // 역방향
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts;
}

@Entity
class Product {
    @Id @GenratedValue
    private Long id;
    private String name;
}

@Entity
@IdClass(MemberProductId.class)
class MemberProduct {
    
    @Id
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member; // MemberProductId.member와 연결

    @Id
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product; // MemberProductId.member와 연결
}

class MemberProductId implemnts Serializable {
    
    private String member; // MemberProduct.member 와 연결
    private String product; // MemberProduct.product 와 연결

    //hashCode and equals
}

회원상품 엔티티를 보면 기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColum을 동시에 사용해서 기본 키 + 외래 키를 한번에 매핑했다. 그리고 @IdClass를 사용해서 복합 키를 매핑했다.

복합 기본 키

회원상품 엔티티는 기본 키가 MEMBER_ID, PRODUCT_ID로 이러우전 복합 기본키(단간히 복합 키라 하겠다). JPA에서 복합 키를 사용하려면 벽도의 식별자 클래스를 만들어야 한다. 그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.

복합 키를 위한 식별자 클래스는 다음과 같은 특징이 있다.

  • 복합 키는 별도의 실별자 클래스로 만들어야 한다.
  • Serializable을 구현해야 한다.
  • equals와 hashCode 메소드를 구현 해야한다
  • 기본 생성자가 있어야 한다
  • 실별자 클래스는 public 이여야 한다
  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용 하는 방법도 있다

식별 관계

회원상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다. 이렇게 부모 테이블의 기본 키를 바인딩 해서 자신의 기본키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다.

종합해보면 회원상품은 회원의 기본 키를 받아서 자신의 기본키로 사용함에 동시에 회원의 관계를 위한 외래 키로 사용한다. 그리고 상품의 기본 키로 받아서 자신의 기본 키로 사용함과 동시에 상품과의 관계를 위한 외래 키로 사용한다. 또한 MembedProductId 식별자 클래스로 두 기본 키를 묶어서 복합 기본 키로 사용한다

// 저장 하는 코드
public void save(){
    // 회원 저장
    Member member1 = new Member();
    member.setId("member1");
    member.setUsername("회원1");
    em.persist(member1);

    // 상품 저장
    Product productA = new Product();
    productA.setId("productA");
    productA.setName("productA");
    em.persist(productA);
    
    // 회원상품 저장
    MemberProduct memberProduct = new MemberProduct();
    memberProduct.setMember(member1); // 주문 회원 - 완관괸계 설정
    memberProduct.setProduct(productA) // 주문 상품 - 연관관계 설정
    memberproduct.setOrderAmount(2); //  주문 수량
    em.persist(memberProduct)
}

// 조회 하는 코드
public void find(){
    
    //기본 키 값 설정
    MemberProductId memberProductId = new MemberProductId();
    memberProductId.setMember(member1);
    memberProductId.setProduct(productA);

    MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
    
    Member member = memberProduct.getMember();
    Product product = memberProduct.getProduct();
}

다대다: 새로운 기본 키 사용

추천하는 기본 키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것이다 이것의 장점은 간편하고 거의 영구히 쓸 수 있으며 비지느스에 의존하지 않는다. 그리고 ORM 매핑시에 복합 키를 만들지 않아도 된다.

@Entity
class Order {
    @Id @GenratedValue @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne
    @JointColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JointColumn(name = "PRODUCT_ID")
    private Product product;
    
    pviate int orderAmount;
}

@Entity
class Member {
    @Id @Column(name = "MEMBER_ID")
    private Long id;
    private String username;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();    
}

@Entity
class Product {
    @Id @Column(name = "PRODUCT_ID")
    private Long id;
}
// 저장 하는 코드
public void save(){    
    // 회원 저장
    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    em.persist(member1);

    // 상품 저장
    Product productA = new Product();
    productA.setId("productA");
    productA.setName("상품");
    em.persist(productA);

    // 주문 저장
    Order order = new Order(); 
    order.setMember(member1); // 주문 회원 - 연관관계 설정
    order.setProduct(productA); // 주문 상품 - 연관관계 설정
    order.setOderAmount(2); // 주문 수량
    em.persist(order);
}

// 조회 하는 코드
public void find(){
    Loing OrderId = 1L;
    Order order = en,find(Order.classm orderId);
    
    Member meber = order.getMember();
    Product product = order.getPRoduct();
}
  • 식별자 클래스를 사용하지 않아서 코드가 한결 단순해졌다. 이 처럼 새로운 기본 키를 사용해서 다대다 관계를 풀어내는 것도 좋은 방법이다.

다대다 연관관계 정리

다대다 관계를 일대다 다대일 관계로 풀어내기 위한 연결 테이블을 만들 때 식별자 를 어떻게 구성할지 선택해야한다.

  • 식별 관계 : 받아온 식별자 기본 키 + 외래 키로 사용한다.
  • 비식별 관계: 받아온 식별자 외래 키로만 사용하고 새로운 식별자를 추가한다.

7장 고급 매핑

  • 상속 관계 매핑 : 객체의 상속 관계를 데이터베이스에 어떻게 매핑하는지를 다룬다.
  • @MappedSuperclass: 등록일, 수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보만 상속 받고 싶으면 기능을 사용 하면된다.
  • 복합 키와 식별 관계 매핑: 데이터베이스의 식별자가 하나 이상일 때 매핑하는 방법을 다룬다. 그리고 데이터베이스 설계에서 이야기하는 식별 관계와 비식별관계에 대해서 다룬다
  • 조인 테이블: 테이블은 외래 키 하나로 연관관계를 맺을 수 있지만 연관관계를 관리하는 연결 테이블을 두는 방법도 있다. 여기서는 연결 테이블을 매핑하는 방법을 다룬다
  • 엔티티 하나에 여러 테이블을 매핑하기: 보통 엔티티 하나에 테이블 하나를 매핑하지만 엔티티 하나에 여러 테이블을 매핑하는 방법도 이다. 여기서는 이 매핑방법을 다룬다.

상속 관계 매핑

관계형 데이터베이스는 객체지향 언에서 다루는 상속이라는 개념이 없다. 대신 슈퍼타입 서브타입 관계 라는 모델링 기법이 객체의 상속 관계와 가장 유사하다. ORM에서 이야기하는 삭송 관계 매핑은 객체상의 구조와 데이터베이스의 슈퍼 타입 서브타입 관계를 매핑하는 것이다.

  • 각각의 테이블로 변환 : 모두 테이블로 만들고 조회할 때 조인을 사용한다. JPA에서는 조인 전략이라 한다.
  • 통합 테이블 변환: 테이블 하나만 사용해서 통합한다. JPA에서는 단일 테이블 전략이라고 한다
  • 서브타입 테이블로 변환: 서브 타입마다 하나의 테이블을 만든다. JPA에서는 구현 클래스마다 테이블 전략이라고 한다.

조인 전략

조인 전략은 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 바당서 기본 키 + 외래 키로 사용하는 전략이다. 따라서 조회할 때는 조인을 자주 사용한다. 이 전략을 사용할 때 주의할 점이 있는대 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 칼럼을 추가 해야하는데 여기서는 DTYPE 칼럼을 구분 칼럼으로 사용 한다.

장점

  • 테이블이 정규화 된다
  • 외래 키 참조 무결성 제약 조건을 활용할 수 있다

단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
  • 조회 쿼리가 복잡하다
  • 데이터 등록할 INSERT SQL을 두 번 실행한다.

특징

  • JPA 표준 명세는 구분 칼럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 칼럼 없이 동작한다

단일 테이블 전략

단일 테이블 전략은 이름 그대로 테이블 하나만 사용한다. 그리고 구분 칼럼으로 어떤 자식 데이터가 저장되어 있는지 구분한다.

장점

  • 조인이 필요 없음으로 일반적으로 조회 성능이 빠르다.
  • 조회 쿼리가 단순하다

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용 해야 한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질수 있다. 그러므로 상황에 따라서 조회 성능이 오히려 느려질 수 있다.

특징

  • 구분 칼럼을 꼭 사용해야 한다.

구현 클레스마다 전략

구현 테이블 전략은 자식 엔티티 마다 테이블을 만든다. 그리고 자식 테이블에 각각 필요한 칼럼이 모두 있다.

장점

  • 서브 타입을 구분해서 처리할 때 효과적이다
  • not null 제약조건을 사용할 수 있다.

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다
  • 자식 테이블을 통합해서 쿼리하기 어렵다

특징

  • 구분 칼럼을 사용하지 않는다.

@MappedSuperclass

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받은 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclas를 사용하면 된다.

복합 키와 식별 관계 매핑

식별 관계

식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래키로 사용 하는 관계이다.

비식별 관계

비식 별관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계이다

필수적 식별 관계

  • 외래 키에 null을 허용하지 않는다. 연관관계를 필수적으로 맺어야한다.

선택적 비식별 관계

  • 외래 키에 null을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.

8장 프로시와 연관관계 관리

프록시와 즉시로딩, 지연로딩

  • 객체는 객체 그래프 연관된 객체를 탐색한다. 그런데 객체가 데이터베이스에저장되어 있음으로 연관된 객체를 마음껏 탐색하기는 어렵다. JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다. 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라. 실제 사용하는 시점에 데이터베이스에서 조회할 수 있다. 하지만 자주 함께 사용하는 객체들은 조인을사용해서 함께 조회하는 것이 효과적이다. JPA는 즉시 로딩과 지연로딩 이라는 방법으로 둘을 모두 지원한다.

영속성 전이와 고아 객체

  • JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체 제거라는 편리한 기능을 제공한다.

프록시

엔티티를 조회할 때 연관된 엔티티들은 항상 사용되는 것은 아니다. 예를 들어 회원 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다.

JPA는 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다.

프록시 기초

JPA에서 식별자로 엔티티 하나를 조회할 때는 EntityManager.find()를 사용한다. 이 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.

Member member = em.find(Member.class, "member1");

이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스를 조회하게 된다. 엔티티를 실제 사용하는 시점까지 데이터베이스를 미루고 싶으면 EntityManager.getReferecne() 메서드를 사용하면 된다.

Member member = em.getReference(Member.class, "member1");

이 메소드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위암한 프록시 객체를 반환한다.

프록시 특징

프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 살제 크래스와 겉 모양이 같다. 따라서 사용하는 입장에서 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용 하면된다.

프록시 객체의 초기화

프록시 객체는 member.getName()처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.

// 프록시 초기화 예쩨
// MemberProxy 변환
Member member = em.getReference(Member.class, "id1");
member.getName(); // 1. getName();

// 프록시 클래스 예상 코드
class MemberProxy extends Member {
    Member target = null;

    public String getName(){
        if(terget == null) {
            // 2. 초기화 요청
            // 3. DB 조회
            // 4. 실제 엔티티 생성 및 참조 보관
            this.target = ...;
        }
        // 5. target.getName();
        return target.getName();
    }
}
  1. 프록시 객체 member.getName()을 호출해서 실제 데이터를 조회한다.
  2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
  3. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
  4. 프록시 객체는 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다
  5. 프록시 객체는 실제 엔티티의 객체의 getName()을 호출해서 결과를 반환한다.

프록시의 특징

  • 프록시 객체는 처음 사용 할 때 한 번만 초기화 된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는것은 아니다. 프록시 객체가 초기화하면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다

즉시 로딩과 지연 로딩

프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용된다.

Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); //팀 엔티티 사용

회원 엔티티를 조호히할때 연관된 팀 엔티티도 함께 데이터베이스에서 조회하는 것이 좋을까? 아니면 회원 엔티티만 조회해두고 팀 엔티티는 실제 사용하는 시점에 데이터베이스안에서 조회하는 것이 좋을까?

JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 다음 두 가지 방법을 제공한다.

즉시 로딩

  • em.find(Member.class, "member1")를 호출 할 때 회원 엔티티와 연관된 팀 엔티티도 함께 조회한다
  • 설정 방법: @ManyToOne(fetch = FetchType.EAGER)

em.find(Member.class, "member1")로 회원을 조회하는 순간 팀도 함께 조회한다. 이때 회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행 할것 처럼 같지만, 대부분 JPA 구현체는 즉시 로딩을 최적화하기 위해서 가능하면 조인 쿼리를 사용한다.

NULL 제약조건과 JPA 조인 전략

외부 조인보다 내부 조인이 성능과 최적화에 더 유리하다. 내부 조인을 사용하려면 외래 키에 NOT NULL 제약 조건을 설정하면 값이 있는 것을 보장한다. 따라서 이 때는 내부 조인만 사용된다. JPA에거 이런 사실을 알려줘야 한다. @JoinColumn에 nullable = false 설정을 해서 이 외래 키는 NUL 값을 허용하지 않는다라고 알려주면 JPA는 외부 조인 대신에 내부 조인을 사용한

  • @JoinColumn(nullable = true): NULL 허용(기본값), 외부 조인 사용
  • @JoinColumn(nullable = false): NULL 허용 하지않음, 내부 조인 사용

지연 로딩

  • member.getTeam().getName() 처럼 조회한 팀 엔티티를 실제 사용하는 시점에 JPA가 SQL을 호출해서 팀 엔티티를 조회한다.
  • 설정 방법: @ManyToOne(fetch = FetchType.LAZY)
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); // 팀 객체 실제 사용

em.find(Member.class, "member1")를 호출하면 회원만조회하고 팀은 조회하지 않는다. 대신에 team 멤버변수에 프록시 객체를 넣어 둔다.

Team team = member.getTeam(); // 프록시 객체

변환된 팀 객체는 프록시 객체다. 이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다. 그래서 지연로딩이라 한다.

team.getName(); // 팀 객체 실제 사용

이 처럼 실제 데이터가 필요한 순간이 되서야 데이터베이스를 조회해서 프록시 객체를 초기화한다.

프록시와 컬렉션 래퍼

지연 로딩으로 설정하면 실제 엔티티 대신에 프록시 객체를 사용한다. 프록시 객체는 실제 자신이 사용될 때 까지 데이터베이스를 조회하지 않는다.

Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println("orders = " + orders.getClass().getName());
// 결과: orders = org.hiberate.collection.interal.PersistenBag

하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라 한다.

엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문 내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다. 컬렉샨 래퍼도 컬렉션에 대한 프록시 역할을 하므로 따로 구분하지 않고 프록시로 부른다.

member.getOrders(); // 호출해도 컬렉션은 초기화되지 않는다.
member.getOrders().get(0); // 처럼 컬렉션에 실제 데이터를 조회할 때 데이터베이스를 조회해서  초기화한다.

JPA 기본 패치 전략

  • @ManyToOne, @OneToOne: 즉시 로딩
  • @OneToMany, @ManyToMany: 지연 로딩

JPA의 기본 패치 전략은 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다. 컬렉션을 로딩하는 것은 비용이 많이 들고 잘못하면 너무 많은 데이터를 로딩할 수 있기 때문이다.

추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다. 그리고 애플리케이션 개발이 어느 정도 완료단계에 왔을 때 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용 하도록 최적화하면 된다.

컬렉션에 FetchType.EAGER 사용 시 주의점

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
    • 컬렉션과 조인한다는 것은 데이터베이스 테이블로 보면 일대다 조인이다. 일대다 조인은 결과 데이터가 다 쪽에 있는 수만큼 증가 하게 된다.
    • 따라서 2개 이상의 컬렉션을 즉시 로딩으로 설정하는 것은 권장하지 않는다.
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
    • 다대일 관계인 회원 테이블과 팀 테이블을 조인할 때 회원 테이블 외래 키에 not null 제약조건을 걸어두면 모든 회원은 팀에 소속 되므로 항상 내부 조인을 사용해도된다.
    • 반대로 팀 테이블에 회원 테이블로 일대다 관계를 조인할 때 한명도 없는 팀을 내부 조인하면 팀까지 조회되자 않는다 따라서 문제가 발생한다.
    • 데이터베이스 제약조건으로 이런상황을 막을 수 없다. 따라서 JPA는 일대다 관계를 즉시 로딩할 때 항상 외부 조인을 사용한다.

FetchType.EAGER 설정과 조인 전략

@ManyToOne, @OneToOne
 - (optional = false): 내부 조인
 - (optional = true): 외부 조인

@OneToMany, @ManyToMany
 - (optional = false): 외부 조인
 - (optional = true): 내부 조인

영속성 전이 : CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 양속 상태로 만들고 싶으면 영속성 전이 기능을 사용 하면된다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 쉡게 말해서 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.

영속성 전이: 저장

@Entity
class Parent {
    @Id
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = Cascade.PERSIST)
    private List<Child> children = new ArrayList<>();
}

@Entity
class Child {
    @Id
    private Long id;

    @ManyToOne
    private Parent parent;
}

부모를 영속활 때 연관된 자식들도 함께 영속화 하라고 cascade = Cascade.PERSIST 옵션을 설정 했다.

// 저장 코드

private static void save(){
    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    
    child1.setParent(parent); // 연관관계 추가
    child2.setParent(parent); // 연관관계 추가
    
    parent.getChildren().add(child1);
    parent.getChildren().add(child1);

    // 부모 저장, 연관된 자식들 저장
    em.persist(parent);
}

영속성 전이: 삭제

방금 지정한 부모와 자식 엔티티를 모두 제거하려면 다음 코드와 같이 각각의 엔티티를 하나씩 제거해야한다.

Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findParent);
em.remove(findChild1);
em.remove(findChild2);

영속성 전이 엔티티를 삭제 할 때도 사용할 수 있다. cascade = Cascade.REMOVE 설정하면 다음 코드처럼 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제 된다.

Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);

코드를 실행하면 DELETE SQL을 3번 싱핼 하고 부모는 물론 연관된 저식도 모두 삭제 한다.삭제 순서는 외래 키 제약조건을 고려해서 자식을 먼저 삭제하고 부모를 삭제한다.

cascade = Cascade.REMOVE를 설정하지 않고 이 코드를 실행하면 부모 엔티티만 삭제된다. 하지만 데이터베이의 부모 로우를 삭제하는 순간 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해 데이터베이스에서 외래키 무결성 예외가 발생한다.

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라 한다. 이 긴능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제 된다.

@Entity
class Parent {
    @Id
    private Long id;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}
Parent parent1 = em.find(Parent.calss, id);
parent1.getChildren().remopve(0); // 자식 엔티티를 컬렉션에서 제거

// 실행 결과 SQL은 다음과 같다
// DELETE FROM CHILD WHERE ID = `?

사용 코드를 보면 컬렉션에서 첫 번째 자식을 제거했다. orphanRemoval = true 옵션으로 인해 컬렉션에 엔티티를 제겅하면 데이터베이스의 데이터도 삭제제거 된다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시 할 때 적용 되므로 플러시 시점에서 DELETE SQL이 실행된다.

parent1.getChildren().clear(); // 모든 자식 엔티티를 제거하려면 clear를 사용하면 된다.

고아 객체를 정리해보자. 고아 객체 제거는 참조자 제거된 엔티티는 다른곳에서 참조 하지 않는 고아 객체로 보고 삭제하는 기능이다. 따라서 이 기능은 참조한 곳이 하나 일때만 사용해야 한다. 쉽게 이야기해서 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용 해야한다. 이런 이유로 orphanRemoval은 @OneToOne, @OneToMany에서만 사용할 수 있다.

정리

  • JPA 구현체들은 객체 그래프를 마음껏 탐색 할 수 있도록 지원하는데 이 때 프록시 기술을 사용한다.
  • 객체를 조회할 때 연관된 객체를 즉시 로딩하는 방법을 즉시 로딩라하 하고, 연관된 객체를 지연해서 로딩하는 방법을 지연 로딩이라 한다.
  • 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수 있는데 이것을 영속성 전이라 한다.
  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용하면 된다.

9장 값타입

JPA의 데이터 타입을 가장 큭 ㅔ분려하면 엔티티 타입과 값 타팁으로 나눌 수 있다. 엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다. 엔티티 타입은 식별자를 통해서 지속해서 추적할 수 있지만, 값 타입은 식별자가 없고 숫자나 문자 같은 속성만 있음으로 추적할 수 없다.

예를들어 회원 엔티티라는 것은 그 회원의 키나 나이 값을 변경해도 같은 회원이다. 심지어 그 회원의 모든 데이터를 변경해도 식별자만 유지하면 같은 회원으로 인식할 수 있다. 반면 숫자 값 100을 200으로 변경하면 완전히 다른 값으로 대체 된다.

값 타입은 까지로 나눌 수 있다

기본 타입

  • 자바 기본 타입
  • 래퍼 클래스
  • String

임베디드 타입

새로운 값 타입을 직접 정의해서 사용할 수 있는데 JPA에서는 이것을 임베디드 타입 이라 한다. 중요한것은 직접 증의한 임베디드 타입도 int, String 같은 값 타입이라는 것이다.

@Entity
class Member {
    @Id @GeneratedValue
    private long id;
    private Stirng name;

    @Embdded Period workPeriod; // 근무 시간
    @Embdded Address homeAddress; // 집 주소
}

@Embeddable
class Period {
    @Temporal(TemporalType.DATE) Date startDate;
    @Temporal(TemporalType.DATE) Date endDate;

    public boolean isWork(Date date){
        //.. 값 타입을 위한 메소드를 정의할 수 있다.
    }
}

@Embeddable
class Address {
    @Column(nane = "citry") // 매핑할 칼럼 정의
    private String citry;
    private String street;
    private String zipcode;
}

새로 정의한 값 타입을 재사용할 수 있고 응집도도 아주 높다. Period.isWork() 처럼 해당 값 타입만 사용하는 의미 있는 메소드로 만들 수 있다.

  • @Embeddable: 값 타입을 정의하는 곳에 표시
  • @Embedded: 값 타입을 사용하는 곳에 표시

임베디드 타입과 테이블 매핑

임베디드 타입은 엔티티 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다. 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블보다 클래스의 수가 더 많다.

@AttributeOverride: 속성 재정의

임베디드 타입에 정의한 매핑정보를 재정의하면 엔티티 @AttributeOverride를 사용하면 된다.

임베디드 타입과 null

임베디드 타입 null이면 매핑할 컬럼 값은 모두 null이 된다.

member.setAddress(null);
em.persist(member);

회원 테이블의 주소와 관련 CITY, STREET, ZIPCODE 칼럼 값은 모두 null이 된다.

컬렉션 값 타입

10장 객체지향 쿼리 언어

  • 객체지향 쿼리 소개
  • JPQL
  • Criteria
  • QueryDSL
  • 네이티브 SQL
  • 객체지향 쿼리 심화

JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 다룬다.

객체지향 쿼리 소개

  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티를 대상으로 하는 객체지향 쿼리다. JPQL은 사용하면 JPA는 이 JPQL을 분석한 당므 적절한 SQL을 만들어 데이터베이스를 조회한다. JPQL은 한마디로 정의하면 객체지향 SQL이다

JPA가 공식으로 지원 하는 기능이다.

  • JPQL
  • Criteria 쿼리: JPQL을 편리하게 작성하도록 도와주는 API, 필더 클래스 모음
  • 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.

다음은 공식으로 지원은 아니지만 아라둘 가치가 있다.

  • QueryDSL: Criteria 쿼리처럼 JPQL을 편리하게 작성하도록 도와주는 빌더 클래스 모움
  • JDBC직접사용, Mynatis 같은 SQL 매퍼 프레임워크 사용

JPQL

  • JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상을 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다
  • JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.

이후 정리

  • 개발에 필요할시 추가 정리할것

11장 웹어플리케이션 제작

  • 추가 정리...

12장 스프링 데이터 JPA

스프링 데이터 JPA는 스프링 프레임워크 JPA를 관리하게 사용할 수 있도록 지원하는 프로젝다.이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복 되는 CRUD 문제를 세련된 방법으로 해결된다. 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료 할 수 있다.

공통 인터페이스 기능

스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리한다.

메소드 설명
save(S) 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
delete(T) 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.
findOne(ID) 엔티티하나를 조회한다. 내부에서 EntityManager.find()를 호출한다.
getOne(ID) 엔티티를 프록시로 조회한다. 내부에서 EntitiyManager.getReference()를 호출한다
findAll(...) 모든 엔티티를 조회한다. 정렬 이나 페이징 조건을 파마미터로 제공할 수 있다.

쿼리 메소드 기능

쿼리 메소드 기능은 스프링 데이터 JPA가 제공하는 기능으로 메소드 이람만으로 쿼를 생성하는 기능이 있다.

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의

13장 웹 애플리케이션과 영속성 관리

스프링링 환경서 JPA를 사용하려면 컨테이너가 트랜잭셩과 영속성 컨텍스트를 관리해주므로 애플리케이션을 솜쉽게 개발할 수 있다. 하지만 컨테이너 환경에서 동작하는 JPA의 내부 동작 방식을 이해하지 못하면 문제가 발생했을 때 해결하기가 쉽지가 않다. JPA 내부 동작 방식, 컨테이너 환경에서 웹 애플리케이션을 개발할 때 발생할 수 있는 다양한 문제점과 해결 방법을 알아보자

스프링 컨테이너 기본 전략

스프링 컨테이너는 트랜잭션 범위의 영속성컨텍스트 전략을 기본으로 사용한다. 이 전략은 이름 그대로 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다. 좀 더 풀어서 이야기하자면 이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.

스프링을 사용하면 보통 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜잭션을 시작한다. 외부에서는 단순히 서비스 계층의 메소드를 호출하는 것처럼 보이지만 이 어노테이션이 있으면 호출한 메소드들 실행하기 직전에 트랜잭션 AOP가 먼저 작동한다.

트랜잭션 범위의 영속성 컨텍스트 예제

@Controller
class HelloController {

    @Autowired HelloService helloService;

    public  void hello(){
        // 반환된 member 엔티티는 중영속 상태다 ...(4)
        Member member = helloService.logic();
    }
}

@Service
class HelloService {
    @PersistenceContext //엔티티 매니저 주입
    private EntityManager em;

    @Autowired Repositry1 repositort1;
    @Autowired Repositry2 repositort2;

    // 트랜잭션 시작 ...(1)
    @Transactional
    public void logic(){
        repository1.hello();
        // member는 영속 상태다 ...(2)
        Member member = repository2.findMember();
        return member;
    }
    // 트랜잭션 종료 ...(3)    
}

@Repository
class Repository1 {
    @PersistenceContext
    EntityManager em;

    public void hello(){
        em.xxx(); // A. 영속성 컨텍스트 접근
    }
}

@Repository
class Repository12{
    @PersistenceContext
    EntityManager em;

    public Member findMember(){}
        return em.find(Member.calss, "id"); // B. 영속성 컨텍스트 접근
    }
}
  1. HelloService.logic() 메서드에 @transactional을 선언해서 메소드를 호출할때 트랜잭션을 먼저 시작한다
  2. Repository2.findMember()를 통해조회한 member 엔티티는 트랜잭션 범위 안에 있음으로 영속성 컨텍스트의 관리를 받는다. 따라서 지금은 영속 상태다.
  3. @Transactional을 선언한 메소드가 정상 종료되면 트랜잭션을 커밋하는데, 이때 영속성 컨텍스트를 종료한다. 영속성 컨텍스트가 자라졌음으로 엔티티는 이제부터 준영속 상태가 된다.
  4. 서비스 메소드가 끝나면 트랜잭션과 영속성 컨텍스트가 종료되었다. 따라서 컨트롤러에 반횐된 member 엔티티는 준영속 상태다

트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다

트랜잭션 범위의 영속성 컨텍스트 전략은 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.

Repository1, Repository2는 같은 트랜젹선 범위에 있다. 따라서 엔티티 매니저는 달라도 같은 영속성 컨텍스트를 사용한다.

트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다

여러 스레드에서 동시 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다. 조금 더 풀어서 이야기하자면 스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당한다. 따라서 같은 엔티티 매니저를 호출해도 접근하는 영속성 컨텍스트가 다르므로 멀티스레드 상황에 안전하다.

스프링이나 J2EE 컨테이너의 가장 큰 장점은 트랜잭션과 복잡한 멀티 스레드 상황을 컨테이너가 처리해준다는 점이다. 따라서 개발자는 싱글 스레드 애플리케이션 처럼 단순하게 개발할 수 고 결과적으로 비즈니스 로직 개발에 집중할 수 있다.

준영속 상태와 지연로딩

스프링은 컨테이너 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 그리고 트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나느 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다. 따라서 조회 엔티티가 서비스와 리포지토리 계층에서는 영속성 컨텍스트에 관리되면서 영속 상태를 유지하지만 컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속 상태가 된다.

@Entity
class Order {
    @Id @GeneraltedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 전략
    private Member member; // 주문 회원
    ...
}

컨테이너 환경의 기본 전략인 트랜잭션 범위의 영속성 컨텍스트 전략을 사용하면 트랜잭션이 없는 프리젠테이션 계층에서 엔티티는 준영속 상태다. 따라서 변경 감지와 지연로딩이 동작하지 앟는다. 컨트롤러에 있는 로직인 지연로딩 시점에 예외가 발생한다.

class OrderController {
    public String view(Long orderId){
        Order oder = orderService.findOne(orderId);
        Member member = order.getMember();
        member.getName(); // 지연 로딩 시 예외 발생
    }
}

준영속 상태와 변경 감지

변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층 까지만 동작하고 영속성 컨텍스트가 종료된 프리젠테이션 계층에서는 동작하지 않는다. 보통 변경 감지 긴능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생한다.

단순히 데이터를 보여주기만 하는 프리젠테이샨 계층에서 데이터를 수정할 일은 거의없다. 오히려 변경 감지 기능이 프리젠테이션 계층에도 동작하면 애플리케이션 계층이 가지는 책임이 모호해지고 무엇보다 데이터를 어디서 어떻게 변경 했는지 프리젠테이이션 계층까지 다 찾아야 하므로 유지보수하기 어렵다.

준영속 상태와 지연 로딩

준영속 상태의 가장 골치 아픈 문제는 지연 로딩 가능이 동작하지 않다는 점이다. 준영속 상태의 지연 로딩 문제를 해결하는 방법은 크게 2가지가 있다.

  • 뷰가 필요한 엔티티를 미리 로딩해느는 방법
  • OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법

뷰가 필요한 엔티티 미리 로딩해두는 방법은 어디서 미리 로딩하느냐에 따라 3가지 방법이 있다.

  • 글로벌 패치 전략 수정
  • JPQL 페치 조인
  • 강제로 초기화

글로벌 패치 전략

@Entity
class Order {
    @Id @GeneraltedValue
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 전략
    private Member member; // 주문 회원
    ...
}

// 프리젠테이션 로직
Order order = orderService.findOne(orderId);
Member member = order.getMember();
memger.getName(); // 이미 로딩된 엔티티

엔티티에 있는 fetch 타입을 변경하면 애플리케이션 전체 이 엔티티를 로딩할 대마다 전략을 사용하므로 글로벌 패치 전략이라 한다.

글로벌 패치 전략 단점

사용하지 않는 엔티티를 로딩한다

글로벌 설정으로 되니 Order를 조회하는 모든 엔티티에서 Member도 함께 조회하게 된다.

N +1 문제가 발생한다

JPA를 사용하면 성능상 가장 조심해야 하는 것이 N+1 문제다. N+1 문제가 어떤것인지 알아보자

Order order = em.find(Member.class , 1L);

// 실행된 SQL
SELECT o.*, m.*
FORM ORder o
LEFT OUTER JOIN Member m on o.MEMBER_ID = m.MEMBER_ID
WHERE o.id = 1 

실행된 SQL을 보면 즉시 로딩으로 설정한 Member 엔티티를 JOIN 쿼리로 함께 조회한다. 여기까지 보면 글로벌 즉시 로딩 전략이 상당히 좋아 보이지만 문제닌 JPQL을 사용할 때 발생한다.

List<Order> orders = em.createQuery("select o from ORder o", Order.class)
    .getResultList(); // 연관된 모든 엔티티를 조회한다.

실행된 SQL은 다음과 같다.

select * from Order // JPQL로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
select * from Member where id = ? // EAGER로 실행된 SQL
...

JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 패치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다. 따라서 즉시 로딩이든 지연 로딩이든 구분하지 않고 JPQL 쿼리 자체에 충실하게 SQL을 만든다.

만약 조회된 order 엔티티가 10개면 member를 조회하는 SQL로 10번 실행된다. 이것을 N+1 문제라고 한다. 이러한 문제는 JPQL 페치 조인으로 해결 할 수 있다

JPQL 페치 조인

글로벌 패치 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에 영향을 주므로 너무 비효율적이다. JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인을 알아보자.

// JPQL
select o
form Order o
join fetch o.member

// SQL
select o.*, m.*
from Order o
join Member m on o.MEMBER_ID = m.MEMBER_ID

사용딘 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회 되므로 N+1 문제는 발생하지 않는다.

JPQL 페치 조인 단점

페치 조인의 현실적인 대안이긴 하지만 무분별하게 사용하면 화면에 맞춘 레포지토리 메소드가 증가할 수 있다. 결국 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침법하는 것이다.

강제로 초기화

강제로 초기화는 영속성 컨텍스트가 살아 있을때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다. 글로벌 패치전략을 모두 지연 로딩이라 가정하겠다.

class OrderService {
    Order order = orderRepository.findOer(id);
    order.getMember().getName(); // 프록시 객체를 강제로 초기화한다.
    return order;
}

글로벌 패치 전략을 지연 로딩으로 설정하면 연관된 엔티티를 실제 엔티티가 아닌 프록시 객체로 조회한다. 프록시 객체는 실제 사용하는 시점에 초기화된다.

order.getMember() 까지만 호출하면 단순히 프록시 객체만 반환하고 아직 초기화 하지 않는다. 프록시 객체는 member.getName() 처럼 실제 값을 사용하는 시점에 초기화 된다.

프록시를 초기화하는 역할을 서비스 계층이 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 한다. 은글 슬쩍 프리젠테이션 계층이 서비스 계칭을 침범하는 상항이다. 서비스 계층은 비즈니스 로직을 담당해야지 이렇게 프리젠테이션 계층을 위한 일까지 하는 것은 좋지 않다. 따라서 비즈니스 계층과 프리젠테이션 계층을 위한 프록시 초기화 역할을 FACADE 계층을 추가 한다

FACADE 계층추가

  • 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리해준다
  • 프리젠테이샨 계층에서 필요한 프록시 객체를 초기환다.
  • 서비스 계층을 호출해 비즈니스 로직을 실행한다.
  • 리포지토리를 직접 호출해서뷰가 요구하는 엔티티를 찾는다.
class OrderFacade {
    @Autuwired OrderService orderSerivce;

    public Order findOrder(id){
        ORder order = orderService.findOder(id);
        // 프리젠테이션 계층이 필요한 프록시 객체를 강제로 초기화한다.
        order.getMember().getName();
        return order;
    }
}
class OrderService {

    public Order findORder(id){
        return orderRepository.findOrder(id);
    }
}

서비스 계층과 프리젠테이샨 계층 간의 논리적 의존관계를 제거 했다.

OSIV

OSIV(Open Session In View)는 영속성 컨텍스트 뷰까지 열어 둔다는 뜻이다. 영속성 컨텍스트가 살아 있으면 엔티티 영속 상태를 유지한다. 따라서 뷰에서도 지연 로딩을 사용할 수 있다.

14장 컬랙션과 부가기능

  • 컬렉션 : 다양한 컬렉션과 특징을 설명한다.
  • 컨버터 : 엔티티의 테이블을 반환해서 데이터베이스에 저장한다.
  • 리스너: 엔티티에 발생한 이벤트를 처리한다.
  • 엔티티 그래프: 엔티티를 조회할 때 연관된 엔티티를 선택해서 함께 조회한다.

컬렉션

JPA는 자바가 기본적으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음 경우에 이 컬렉션을 사용 할 수 있다.

  • OneToMany, @ManyToMany를 사용해서 일대다 다대다 엔티티 관계를 매핑 할 때
  • @ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때

자바 컬렉션 인터페이스의 특징은 다음과 같다

  • Collection: 자바가 제공하는 최상위 컬렉션이다. 항버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정한다.
  • Set: 중복을 허용하지 않는 컬렉션이다. 순서를 보장하지 않는다.
  • List: 순서가 있는 컬렉션이다. 순서를 보장하고 중복을 허용한다.
  • Map: Key, Value 구조로 되어 있는 특수한 컬렉션이다.

JPA와 컬렉션

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으러 감싸서 사용한다.

@Entity
class Team {
    @Id
    private String id;

    @OneToMany
    @JoinColumn
    private Collection<Member> members = new ArrayList<Member>();
}

Team은 members 컬렉션을 필드고 가지고 있다 다음 코드로 Team을 영속 상태로 만들어보자

Team team = new Team();

print("before persist = " + team.getMembers().getClass());
team.getMembers().getClass();
print("after persist = " + team.getMembers().getClass());
before persist = class java.util.ArraList
after persit = class org.hibernate.collection.internal.PErsistendBag

출력 결과를 보면 원래 ArrayList 타입 이었던 컬렉션이 엔티티를 영속 상태로 만든 직후 하이버네이트가 제공하는 PersistentBag 타입으로 변경되었다. 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 내장 컬렉션을 사용하도록 참조를 변경한다. 하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어서 래퍼 컬렉션이라고 부른다. 하이버네이트는 이런 특성 때문에 컬렉션을 사용할 때 다음 처럼 즉시 초기화해서 사용하는 것을 권장한다.

Collection<Member> member = new ArrayList<Member>();
컬렉션 인터페이스 내장 컬렉션 중복 허용 순서 보관
Collection, List PersistenceBag O X
Set PersistenceSet X X
List + @OrderColumn PersistenceList O O

Collection, List

Collection, List 인터페이스는 중복을 허용하는 컬렉션이고 PersistenceBag을 래퍼 컬렉션으로 사용한다. 이 인터페이스는 ArrayList로 초기화 하면된다

@Entity
class Parent {
    @Id @GeneratedValue
    private Long id;

    @OneToMany
    @JoinColumn
    private Collection<CollectionChild> collection = new ArrayList<CollectionChild>();

    @OneToMany
    @JoinColumn
    private List<ListChild> List = new ArrayList<CollectionChild>();
    
}

중복을 허용한다는 가정하므로 객체를 추가하는 add() 메소드는 내부에서 어떤 비교도 하지 않고 항상 true를 반환한다. 같은 엔티티가 있는지 찾거나 삭제하는 equals() 메소드를 사용한다.

Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.

Set

Set은 중복을 허용하지 않는 컬렉션이다 하이버네이트는 PersistentSet을 컬렉션 래퍼로 사용한다. 이 인터페이스는 HashSet으로 초기화 하면된다.

public class Parent {
    @OneToMany
    @JoinColmn
    private Set<SetChild> set = new HashSet<>();
}

HashSet은 중복을 허용하지 않음으로 add() 메소드로 객체를 추가할 때 마다 equals() 메소드로 같은 객체가 있는지 비교 한다. 같은 객체가 없으면 객체를 추가하고 true를 리턴하고 같은 객체가 이미 있어서 추가에 실패하면 false를 반환한다.

Set은 엔티티 추가할 때 중복된 엔티티가 있는지 비교해야 한다. 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.

List + @OrderColumn

List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 툭수한 컬렉션으로 인식한다. 순서가 있다는 의미는 데이터베이스에 순가 값을 저장해서 조회할 때 사용한다는 의미다. 하이버네이트 내부 컬렉션인 PersistenceList를 사용한다.

@Entity
public class Baord {
    @Id @GeneratedValue
    private Long id;

    private String title;
    private String content;

    @OneToMany(mappedBy = "board")
    @ORderColumn(name = "POSTION")
    private List<Comment> comments = new ArrayList<>();
}

@Entity
class Comment {
    @Id @GeneratedValue
    private Long id;
    
    private String Comment

    @ManyToOne
    @JoinColumn(name = "BOARD_ID")
    private Board board;    
}

Board.comments는 순서가 있는 컬렉션으로 인식된다. 자바가 제공하는 List 컬렉션 내부에 위치 값을 가지고 있다. 따라서 다음 코드처럼 List의 위치 값을 활용할 수 있다.

list.add(1, data); // 1번 위치에 data1을 저장하라
list.get(10); // 10번 위치에 있는 값을 조회하라

@OrderBy

@OrderBy는 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다. 그리고 @OrderBy는 모든 컬렉션에 사용할 수 있다.

@Converter

컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다. 예를 들어 회원의 VIP 여부를 자바의 boolean 타입을 사용하고 싶다고 하자 JPA를 사용하면 자바의 boolean 타입은 방언에 따라 다르지만 데이터베이스에 저장 될때 0, 1 숫자로 저장된다. 그런데 데이터베이스에 Y, N 으로 저장 하고싶다면 컨버터를 사용하면 된다.

리스너

모든 엔티티를 대상으러 언제 어떤 사용자가 삭제를 요청했는지 모두 로그를 남겨야 하는 요구사항이 있다고 가정 하자. 이때 애플리케이션 삭제 로직을 하나씩찾아서 로그를 남기는 것은 너무 비효율적이다. JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.

15 고급 주제와 선능 최적화

엔티티비교

영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 있다. 이 1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다. 영속성 컨텍스트를 통해 데이터를 저장하거나 조회하면 1차 캐시에 엔티티가 저장된다. 이 1차 캐시 덕분에 변경 감지 기능도 동작하고, 이름 그대로 1차 캐시로 사용되어서 데티어베이스를 통하지 않고도 데이터를 바로 조회할 수 있다. 영속성 컨텍스트를 좀더 정확히 이해하기 위해서는 1차 캐시의 가장 큰 장점인 애플리케이션 수준의 반복 가능한 읽기를 이해해야한다. 같은 영속성 컨텍스트에서 엔티티를 조회하면 다음 코드와 같이 항상 같은 엔티티 인스턴스를 변환한다. 이것은 동승성 비교 수준이 아니라 정말 주소값이 같은 인스턴스를 반환한다.

Member member1 = em.find(Member.class, "1L");
Member member2 = em.find(Member.class, "1L");

assertTrue(member1 == member2); // 둘은 같은 인스턴스다.

영속성 컨텍스트가 같을 때 엔티티 비교

영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.

  • 동일성: == 비교가 같다
  • 동등성: equals() 비교가 같다
  • 데이터베스 동등성: @Id인 데이터베이스 식별자가 같다.

영속성 컨텍스트가 다를 때 엔티티 비교

영속성 컨텍스트가 다르면 동일성 비교에 실패한다. 영속성 컨텍스트가 다를 때 엔티티 비교는 다음과 같다.

  • 동일성: == 비교가 실패한다
  • 동등성: equals() 비교가 만족한다. 단 equals()를 구현해야한다 보통 비즈니스 키로 구현한다.
  • 데이터베으 동등성: @Id인 데이터베이스가 식별자가 같다.

앞서 보았듯이 같은 영속성 컨텍스트를 보장한다면 동일성 비교만 충분하다. 따라서 OSIV 처럼 요청의 시작부터 끝까지 같은 영속성 컨텍스ㅡ를 사용할 때는 동일성 비교가 성공한다. 하지만 지금 처럼 영소겅 켄텍스트가 달라지면 동일성 비교는 실패한다. 따라서 엔티티의 비교에 다른 방법을 사용해야한다. 동일성 비교 대신에 데이터베이스 동등성을 비교하면 엔티티를 영속화 해야 식별자를 얻을 수 있다는 문제가 있다. 엔티티를 영속화 하기전에 식별자 값이 null 이므로 정확한 비교를 할 수 없다. 물론 식별자 값을 직접 부여하는 방식을 사용할 때는 데이터베이스 식별자 비교도 가능하다. 하지만 항상 식별자를 먼저 부여하는 것을 보장하기는 쉽지 않다.

남은 것은 equals()를 사용한 동등성 비교인데, 엔티티를 비교할 때는 비즈니스 키를 활용한 동등성 비교를 권장한다.. 동드엇ㅇ 비교를 위해 equals()를 오버라이딩할 때는 비즈니스 키가 된느 필드를 선택하면 된다. 비즈니스 키가 되는 필드는 보통 중복되지 않고 거의 변하지 않는 데이터베이스 기본 키 후보들이 좋은 대상이다. 기본 키는 유일성만 보장되면 가끔 있는 변경 정도는 허용한다. 따라서 데이터베이스 기본 키 같은 너무 딱딱하게 정하지 않아도된다. 예를 들어 회원 엔티티에 이름과 연락처가 같은 회원이 없다면 회원의 이름과 연락처 정도만 조합해서 사용해도 된다.

정리하자먄 동일성 비교는 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용할 수 있다. 그렇지 않을 때는 비즈니스 키를 사용한 동등성 비교를 해야한다.

프록시 심화 주제

프로시는 원본 엔티티를 상속받아서 만들어지므로 엔티티를 사용하는 클라어인트는 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있다. 따라서 원본 엔티티를 사용하다가 지연 로딩을 하려고 프록시를 변경해도 클라이언트의 비즈니스 로직을 수정하지 않아도된다. 하지만 프록시를 사용하는데 방식의 기술적인 한계로 인해 예상하지 못한 문제들이 발생하기도 하는데, 어떤 문제가 발생하고 어떻게 해결해야 하는지 알아보자

영속성 컨텍스트와 프록시

영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성을 보장한다. 그리고 프록시로 조회해도 영속성 컨텍스트는 영속성 엔티티의 동일서을 보장한다.

프로시 타입 비교

프록시는 원본 엔티티를 상속 받아서 만들어지므로 프록시로 조회한 엔티티의 타입을 비교할 때는 == 비교를 하면 안되고 대신에 instaneof를 사용해야한다.

프록시 동등성 비교

  • 프록시의 타입 비교는 == 비교 대신에 instanceof를 사용해야 한다.
  • 프록시의 멤버변수에 집접 적근하면 안되고 대신에 접그자 메소드를 사

16장 트랜잭션과 락, 2차 캐시