자바 ORM 표준 JPA 기본편(핵심 내용 정리)

김상운(개발둥이)·2022년 3월 4일
0
post-thumbnail

자바 ORM 표준 JPA 기본편

인프런 김영한님의 '자바 ORM 표준 JPA 기본편' 강의 보러가기
https://www.inflearn.com/course/ORM-JPA-Basic#

1. JPA 소개, 2. JPA 시작

핵심정리를 위한 글이므로 소개는 아래 블로그에서 참고하길 바란다. https://dodeon.gitbook.io/study/kimyounghan-orm-jpa/01-jpa-introduction

3.영속성 관리

영속성 컨텍스트

  • 영속성 컨텍스트란 엔티티를 영구 저장하는 환경이다.
  • 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
  • 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다.

엔티티 생명주기

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

영속성 컨텍스트의 특징

  • 영속성 컨텍스트와 식별자 값
    - 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.
  • 영속성 컨텍스트와 데이터베이스 저장
    - 엔티티를 데이터베이스에 반영하는데 이것을 플러시(flush)라 한다.
  • 영속성 컨텍스트 관리 장점
    - 1차 캐시
    - 동일성 보장
    - 트랜잭션을 지원하는 쓰기 지연
    - 지연 로딩

엔티티 조회

  • 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 한다. 영속성 컨텍스트 내부에 Map이 하나 있는데 키는 @Id로 매핑한 식별자고 값은 엔티티 인스턴스다
  • 엔티티 등록
    - 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 모아둔다. 이를 트랜잭션을 지원하는 쓰기 지연이라 한다.
    - 트랜잭션을 커밋하면 엔티티 매니저는 우선 영속성 컨텍스트를 플러시한다. 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업인데 이때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다.
  • 변경 감지
    1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시가 호출된다.
    2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
    3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
    4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
    5. 데이터베이스 트랜잭션을 커밋한다.

    변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다

플러시

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
2. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.

4. 엔티티 매핑

매핑 어노테이션

  • 객체와 테이블 매핑 : @Entity, @Table
  • 기본 키 매핑 : @Id
  • 필드와 컬럼 매핑 : @Column
  • 연관관계 매핑 : @ManyToOne, @JoinColumn

@Entity

  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다.
  • 기본 생성자는 필수다(파라미터가 없는 public 또는 protected 생성자)
  • final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
  • 저장할 필드에 final을 사용하면 안 된다.

@Table

  • @Table은 엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.

기본 키 매핑

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

필드와 컬럼 매핑


@Column


5. 연관관계 매핑 기초

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 테이블과 객체 사이에는 이런 큰 간격이 있다.

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

  • 참조를 통한 연관관계는 언제나 단방향이다. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 하지만 정확히 이야기하면 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.

단방향 연관관계

@Entity
public 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.team = team;
    }
    //Getter, Setter ...
}

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

    private String name;
    //Getter, Setter ...
}
  • @JoinColumn은 외래 키를 매핑할 때 사용한다.
  • @ManyToOne 어노테이션은 다대일 관계에서 사용한다.

양방향 연관관계

@Entity
public class Member {
    @Id
    @Column (name = ”MEMBER_ID”)
    private String id;
    private String username;
    
    @ManyToOne
    @JoinColumn(name="TEAM_ID H)
    private Team team;
    
    //연관관계 :설정
    public void setTeam(Team team) {
    this.team = team;
    }
    //Getter, Setter ...  
}

@Entity
public class Team {
    @Id
    @Column(name = ”TEAM_ID”)
    private String id;
    
    private String name;
    
    //==추가==//
    @OneToMany (mappedBy = "team")
    private List<Member> members = new ArrayList<Member> () ;
    
    ...
}

연관관계의 주인

@OneToMany만 있으면 되지 mappedBy는 왜 필요할까?

  • 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
  • 연관관계의 주인은 외래 키가 있는 곳

6. 다양한 연관관계 매핑

연관관계 매핑 시 고려사항 3가지

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

다대일(@ManyToOne)

다대일 단방향

  • 가장 많이 사용하는 연관관계
  • 다대일의 반대는 일대다

다대일 양방향

  • 외래 키가 있는 쪽이 연관관계의 주인
  • 양쪽을 서로 참조하도록 개발

일대다(@OneToMany)

일대다 단방향

  • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
  • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)
  • 일대다 단방향 매핑의 단점
  • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
  • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

일대다 양방향

  • 이런 매핑은 공식적으로 존재X
  • @JoinColumn(insertable=false, updatable=false)
  • 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
  • 다대일 양방향을 사용하자

일대일(@OneToOne)

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
  • 다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
  • 반대편은 mappedBy 적용

다대다(@ManyToMany)

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함
  • @JoinTable로 연결 테이블 지정
  • 다대다 매핑: 단방향, 양방향 가능

7. 고급 매핑

  • 상속관계 매핑
  • @MappedSuperclass

상속관계 매핑

  • 관계형 데이터베이스는 상속 관계X
  • 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사
  • 상속관계 매핑: 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑

주요 어노테이션과 상속 전략

  • @Inheritance(strategy=InheritanceType.XXX)
  • @DiscriminatorColumn(name=“DTYPE”)
  • @DiscriminatorValue(“XXX”)
  • JOINED: 조인 전략
  • SINGLE_TABLE: 단일 테이블 전략
  • TABLE_PER_CLASS: 구현 클래스마다 테이블 전략

조인 전략

@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속 매핑은 부모 클래스에 선언해야 한다.
@DiscriminatorColumn(name = "DTYPE") // 부모 클래스에 구분 컬럼을 지정한다.
    public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name; //이름
    private int price; //가격
    ...
}

@Entity
@DiscriminatorValue("A") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.
public class Album extends Item {
    private String artist;
    ...
}

@Entity
@DiscriminatorValue("M")
@PrimaryKeyJoinColumn(name = "MOVIE_ID") // 자식 테이블의 기본 키 컬럼명을 변경 (기본 값은 부모 테이블의 ID 컬럼명)
public class Movie extends Item {
    private String director; //감독
    private String actor; //배우
    ...
}

장점

  • 테이블이 정규화된다.
  • 외래 키 참조 무결성 제약 조건을 활용할 수 있다.
  • 저장공간을 효율적으로 사용한다.

단점

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

단일 테이블 전략

  • 이름 그대로 테이블을 하나만 사용한다. 그리고 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다. 이 전략을 사용할 때 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다

    @Entity
    @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
    @DiscriminatorColumn(name = "DTYPE")
    public abstract class Item {

      @Id @GeneratedValue
      @Column(name = "ITEM_ID")
      private Long id;
    
      private String name; //이름
      private int price; //가격
      ...

    }

    @Entity
    @DiscriminatorValue("A")
    public class Album extends Item {
    ...
    }

    @Entity
    @DiscriminatorValue("M")
    public class Movie extends Item {
    ...
    }

장점

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

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. (성능이 더 안좋아 질 수 있음)

특징

  • 구분 컬럼을 꼭 사용해야 한다. (지정하지 않으면 기본으로 엔티티 이름을 사용한다)

구현 클래스마다 테이블 전략

장점

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

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION을 사용해야 한다)
  • 자식 테이블을 통합해서 쿼리하기 어렵다

8. 프록시와 연관관계 관리

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

프록시

프록시 특징

  • 실제 클래스를 상속 받아서 만들어짐.
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
  • 프록시 초기화
  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초
  • 기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비
    교 실패, 대신 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해
    도 실제 엔티티 반환

지연 로딩

Member를 조회할 때 Team도 함께 조회해야 할까?

지연 로딩 LAZY을 사용해서 프록시로 조회

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "USERNAME")
    private String name;
    @ManyToOne(fetch = FetchType.LAZY) //**
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ..
}
  • 지연로딩을 활용한 프록시 조회

지연 로딩 활용

  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라!
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!

즉시 로딩

Member와 Team을 자주 함께 사용한다면?

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "USERNAME")
    private String name;
    @ManyToOne(fetch = FetchType.EAGER) //**
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ..
}
  • 즉시로딩을 통해 조회

프록시와 즉시로딩 주의

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

영속성 전이: CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때 (예: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.)

CASCADE 의 종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH
영속성 전이 주의
  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함
    을 제공할 뿐

고아 객체

  • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티
    를 자동으로 삭제
  • orphanRemoval = true
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;

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


//자식 엔티티를 컬렉션에서 제거
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);

영속성 전이 + 고아 객체, 생명주기

  • CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 어떻게 될까?
    일반적으로 엔티티는 EntityManager.persist()를 통해 영속화되고 EntityManager.remove()를 통해 제거된다. 이것은 엔티티 스스로 생명주기를 관리한다는 뜻이다. 그런데 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
    영속성 전이는 DDD의 Aggregate Root개념을 구현할 때 사용하면 편리하다

9. 값 타입

  • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
  • 식별자가 없고 값만 있으므로 변경시 추적 불가 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
  • 값 타입은 크게 세가지(기본 값, 임베디드 타입, 값 타입 컬렉션)

기본 값

  • 예): String name, int age
  • 생명주기를 엔티티의 의존 예) 회원을 삭제하면 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면X 예) 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨

임베디드 타입

  • 새로운 값 타입을 직접 정의할 수 있음
  • JPA는 임베디드 타입(embedded type)이라 함
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입
  • @Embeddable: 값 타입을 정의하는 곳에 표시
  • @Embedded: 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수

값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함

값 타입 컬렉션 사용 예

Member member = em.find(Member. class, IL);
// 1. 임베디드값 타입수정
member.setHomeAddress(new Address("새로운도시", "신도시 1", "123456"));

// 2. 기본값 타입컬렉션수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");

// 3. 임베디드값 타입컬렉션수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울", "기존주소", "123-123"));
addressHistory.add(new Address("새로운도시", "새로운 주소", "123-456"));

값 타입 컬렉션의 제약사항

  • JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.
  • 따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
  • 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 데이터베이스 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 있다.

값 타입 복사

  • 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다. 그러나 임베디드 타입처럼 직접 정의한 값 타입은 기본 타입이 아니라 객체 타입이라는 것이다. 자바는 객체에 값을 대입하면 항상 참조값을 전달한다.
  • 객체의 공유 참조는 피할 수 없다. 따라서 근본적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다.

불변 객체

  • 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다. 따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다. 한 번 만들면 절대 변경할 수 없는 객체를 불변 객체라 한다. 불변 객체의 값은 조회할 수 있지만 수정할 수 없다.
  • 불변 객체를 구현하는 다양한 방법이 있지만 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않으면 된다.

10. 객체지향 쿼리 언어

JPQL

  • JPA를 사용하면 엔티티 객체를 중심으로 개발
  • 문제는 검색 쿼리
  • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검
    색 조건이 포함된 SQL이 필요
  • 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존X
  • JPQL을 한마디로 정의하면 객체 지향 SQL

페치조인

  • 페치(Fetch)조인은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.
String jpql = "select m from Member m join fetch m.team";

List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for (Member member : members) {
//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안 함
Systern.out.printin("username = " + member.getUsername () + ”, " +
"teamname = ” + member.getTeam().name());
}

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.(batchSize 사용)



깃허브 주소

https://github.com/issiscv/orm-standard-jpa

profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글