JPA는 Java Persistence API 의 약자로, Java의 ORM 기술 표준이다.
ORM이란, 객체 관계 매핑(Object-Relational Mapping)이란 의미로,
객체와 DB를 따로따로 설계를 하고 ORM Framework가 그 둘을 Mapping을 해준다.

JPA는 객체와 RDB간의 Mapping과 영속성 컨텍스트에 대해 이해 하는 것이 가장 중요하다.

영속성 컨텍스트

JPA를 이용하는데에 가장 중요한 용어인 영속성 컨텍스트 이다.
em.persist(entity) 이는 “Entity를 영구 저장하는 환경” 이라는 의미를 가진다.

Entity의 생명주기

image

  • 비영속 : 영속성 컨텍스트와 관계가 없음.
    • Entity가 생성만 된 상태이다.
  • 영속 : 영속성 컨텍스트에 관리되는 상태.
    • Persist를 통해서 Entity가 Context에 영속된 상태이다.
    • 영속이 된다고 해서, DB로 Query가 바로 날아가는 것은 아니다.
    • Transaction에 Commit을 해야만 Query가 그제서야 DB로 날아간다.
  • 준영속 : 영속성 컨텍스트에 저장되었다 분리된 상태.
  • 삭제 : 말 그대로 삭제된 상태.

영속성 컨텍스트의 장점

  • 1차 Cache
    image

    • 영속 컨텍스트에 등록 시, Cache에 Entity를 저장하는 것과 동일한 효과를 낸다.
    • em.find(Member.class, "member")와 같이 조회 시, Cache에서 바로 조회를 한다.
    • 로그를 찍어보면, 조회 Query가 사용되지 않음을 알 수 있다.
  • 동일성(Identity) 보장
    • 서로 다른 객체에 동일 Entity를 조회하면, 둘은 동일함을 보장한다.
  • Transaction을 지원하는 쓰기 지연(Write-Behind)
    • Persist를 하는 것만으로는 DB에 Insert Query를 보내지 않는다.
      • 이 때는, 영속 컨텍스트의 1차 캐시와 쓰기 지연 SQL 저장소 에 담기만 한다.
    • Transaction을 Commit 하는 순간, 한 번에 모든 Query를 보낸다.
  • 변경 감지(Dirty Checking)
    image

    • 내가 DB의 Data를 변경하고 싶다면, 그냥 변경만 해주면 된다.
    • 변경한 뒤, 굳이 다시 Persist하거나 DB에 처리해 줄 필요가 없다는 것이다.
    • 알아서 감지하고 Update Query를 날린다는 것!
Flush?

영속성 컨텍스트의 변경 내용을 DB에 반영하는 것을 Flush 라고 한다.

  • em.flush() 로 직접 호출할 수 있다.
  • Transaction Commit 시에, 자동으로 호출된다.
  • JPQL Query를 날릴 때, 자동으로 호출된다.

Flush가 호출되면, 강제로 모든 Query가 DB에 반영되게 된다.

객체와 Table 매핑

  • @Entity
    • 이 Annotation이 붙은 Class는 그 순간 JPA가 관리하는 엔티티가 된다.
    • JPA를 사용해서 DB Table과 매핑하기 위해서는 반드시 필요하다.
    • @Table Annotation을 통해 직접 Table 이름으로 매핑할 수도 있다.
    • 기본 생성자는 필수이다! (파라미터가 없는 public or protected)
      • Final, Enum, Interface, Inner Class들은 쓸 수 없다.
      • 저장할 Field에도 Final은 쓸 수 없다.
    • name 이란 속성으로 Entity 이름 지정 가능, 기본 값은 Class 이름.
      • 잘 쓰지 않지만, 굳이 사용하는 경우는 다른 Package에 같은 이름의 Class가 존재할 때.

Database Schema 자동 생성

처음에 JPA를 이용하기 위해서, persistence.xml File을 만들었었다.
해당 XML 파일에 특정 Property를 추가하면, Database의 Schema를 자동화 시킬 수 있다.

<property name="hibernate.hbm2ddl.auto" value="create" />

위와 같이 property에 추가를 해주면, Application 실행 시점에 자동으로 DB에 내 Entity와 같은 모양으로 Table을 만들어준다.
기존 DB에 이미 존재하는 Table이면, Drop을 한 뒤 생성한다.

  • value = create : 위 설명과 동일하다.
  • value = create-drop : Application 종료 시점에 Table을 Drop하는 것 까지.
  • value = update : 변경 사항만 Table에 ALTER로 추가한다.
    • 다만, 문제 발생을 막기 위해 추가만 가능하며 지울 수는 없다.
  • value = validate : Table과 Entity의 Field가 다를 경우, 오류가 발생한다.

운영 서버에는 절대 Create, Create-drop, Update를 사용해선 안된다!
웬만하면 직접 Script를 짜서 사용할 것!

  • 개발 초기 단계는 Create, Create-drop (로컬 서버에서만 사용)
  • Test 서버는 Update, Validate
  • Staging과 운영 서버는 Validate 또는 사용 안함.

Field와 Column 매핑

  • @Column : Table의 해당 이름 Column으로 Mapping한다.
    • name : Mapping할 Column의 이름이다.
    • updatable, insertable : 등록과 변경의 가능 여부를 설정한다.
      • 기본 값은 True.
    • nullable : 필수로 입력받아야하는 값임을 의미한다.(DDL)
      • 기본 값은 True. NOT NULL 조건을 달아주는 것이다.
    • unique : 해당 Column에 Unique 제약을 걸 수 있다.(DDL)
      • 하지만 이름이 Hash 값으로 나오기 때문에 잘 쓰지 않는다.
      • @Table(uniqueConstraints = )를 주로 사용한다!
    • length : 문자 길이의 제약 조건, String에만 쓰인다.(DDL)
    • columnDefinition : 내가 직접 조건을 Query문 처럼 걸 수 있다.(DDL)
      • ex) columnDefinition = "varchar(100) default EMPTY
    • precision, scale : 아주 큰 수나, 소수의 자릿수 관리에 사용되는 속성이다.(DDL)
  • @Temporal : Date Type의 정보를 Mapping한다.
    • 최신 버전을 사용할 땐, LocalDate, LocalDateTime이 사용 가능하다.
    • 위 두 가지를 사용할 때엔, 굳이 @Temporal을 쓰지 않아도 알아서 매핑한다.
  • @Enumerated : 원래 DB에는 ENUM이 없다. 따라서 이 Annotation을 쓴다.
    • 반드시 EnumType.STRING을 통해 속성을 바꿔주어야 한다!
    • 기본 속성은 EnumType.ORDINAL이다. 이는 굉장히 위험하다!
  • @Lob : BLOB과 CLOB을 Mapping한다.
    • String 타입의 경우, CLOB.
    • 나머지는 알아서 BLOB으로 매핑한다.
      • LOB는 Large Object의 약자이다.
      • BLOB은 이진 대형객체, CLOB은 문자 대형객체를 의미한다.
  • @Transient : 특정 Field를 DB에 반영없이 메모리에서만 쓴다.

기본 키 매핑

기본 Key는, NULL이 아니어야하고, 유일하며, 변하면 안된다.
미래까지 이 조건을 만족하는 자연 Key는 찾기가 어렵다.
따라서 Long Type + 대체키 + 키 생성전략을 사용함을 권장한다.

  • @Id : 기본 키(Primary Key)를 직접 할당할 경우 쓴다.

  • @GeneratedValue : 자동으로 Key 값을 생성해준다.

    • IDENTITY
      • strategy = GenerationType.IDENTITY
      • 기본 키 생성을 DB에 위임한다.
      • MySQL의 Auto_Increment와 같은 문법이 된다.
      • IDENTITY는 특이하게, Persist 즉시 Insert Query가 날아간다.
    • SEQUENCE
      • strategy = GenerationType.SEQUENCE
      • ID가 정수의 형태일 때만 사용이 가능하다.
      • IDENTITY와 동일한 기능을 한다.
      • @SequenceGenerator로 직접 Sequence를 만들 수도 있다.
    • TABLE
      • @TableGenerator로 이용한다.
      • Key 생성 전용 Table을 하나 만들어서 DB Sequence를 흉내낸다.
      • 모든 DB에 적용이 가능하다는 장점이 있지만, 성능이 좋지 않다.

상속관계 매핑

객체엔 상속관계가 존재하지만, RDB에는 사실 상속관계가 없다.
가장 유사한게, 슈퍼타입, 서브타입 관계 라는 기법이 그나마 유사하다.
따라서 객체의 상속과 슈퍼타입 서브타입을 매핑하는 것이 상속관계 매핑이다.

  • 조인 전략
    • 기본적으로 이 방식이 객체 지향에서 가장 정석이라고 생각하자!
    • 테이블을 따로따로 만들어 놓고, 해당 Type의 자식이 등장하면 Join한다.
    • @Inheritance(strategy = JOINED)
    • DTYPE이 필요하므로, @DiscriminatorColumn으로 자동으로 DTYPE Column 생성.
    • 장점
      • Data가 정규화가 되어 있다.
      • 외래 키에 무결성 제약조건을 걸어 활용할 수 있다.
      • 저장 공간을 효율적으로 운용할 수 있다.
    • 단점
      • 조회 시 Join이 많이 쓰인다. 즉, 조회 Query가 복잡하다.
      • Data 저장 시 Insert SQL을 2번 호출한다. (부모, 자식)
  • 단일 테이블 전략
    • 하나의 테이블에 Column으로 다 집어 넣어 버린다.
    • 성능 면에서, 가장 강점을 갖는다.
    • 이 전략은, DTYPE이 Annotation이 따로 없어도 자동으로 생긴다.
    • 장점
      • Join이 필요가 없어서, 성능이 빠르다. 조회 Query 또한 단순하다.
    • 단점
      • 자식 Entity가 Mapping한 Column은 모두 NULL을 허용해야 한다.
      • 테이블의 크기가 매우 커질수도 있어서 오히려 성능을 저하시킬 수도 있다.
  • 구현 클래스마다 테이블 전략
    • 웬만하면, 추천하지 않는 방식이다.
    • 자식 테이블만 만들고, 자식 테이블들이 각각 모두 부모의 정보를 들고 있도록 만든다.
    • 이 경우엔, DTYPE이 별도로 필요하지 않다.
    • 단점
      • 여러 자식 테이블을 함께 조회할 때 성능이 매우 느리다.
      • 자식 테이블을 통합해서 쿼리하기 매우 까다롭다.
      • 변경의 관점에서 봤을 때, 매우 좋지 않다.

기본적으로 JPA는 상속관계에 있는 객체들을 단일 테이블 전략으로 가져간다.
우리는 @Inheritance(strategy = ) Annotation으로 전략을 바꿀 수 있다.
부가적으로 @DiscriminatorColumn Annotation을 통해 DTYPE Column을 자동으로 생성해줄 수도 있다.
해당 상속받은 자식 Class에서 @DiscriminatorValue(name = "?")로 DTYPE의 이름을 Custom할 수도 있다.

단순할 땐 단일 테이블! 복잡할 땐 조인 전략!

Bonus : @MappedSuperclass

우리가 상속 관계를 매핑하기 위해서 @Entity@Inheritance를 썼다면,
상속 관계 매핑이 아닌, 그저 공통 정보를 저장하기 위한 용도로 쓰는 Annotation을 알아보자.

@MappedSuperclass Annotation이 달리게 된, Class의 속성들은
해당 Class를 상속받은 모든 자식들이 공통으로 사용가능한 속성이 된다.

부모 Class를 상속받는 자식 Class에 매핑 정보만 제공한다.
해당 Class Type으로는 조회가 불가능하며, 자식 Class Type으로 조회를 해야 한다.

주로 등록 일자, 수정 일자, 등록자,수정자 같은 전체 Entity에 공통되는 정보를 모을 때 사용한다.

직접 생성해서 사용할 일이 없으므로 추상 클래스 로 구현할 것!
마지막으로, Entity로 지정된 Class 는 같은 Entity Class나 MappedSuperclass만 상속 이 가능하다!