새소식

Java

자바 ORM 표준 JPA 기본편 정리 [1]

  • -
728x90
자바 ORM 표준 JPA 기본편 정리 1

📌 JPA란?

JPA(Java Persistent API)는 자바 진영의 ORM 기술 표준이다. ORM(Object Relational Mapping)이란 객체는 객체대로, 관계형 DB는 관계형 DB대로 설계하고 중간에서 ORM 프레임워크가 매핑을 수행하는 것을 의미한다.

JPA는 Application과 JDBC(Java Database Connectivity) 사이에서 동작한다.

 

💡 ORM 이란 ?

- Object-relational mapping, 객체 관계 매핑

- 객체는 객체대로 설계 ,관계형 데이터베이스는 관계형 데이터베이스대로 설계

- 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해주는 것을 의미한다. ORM 프레임워크가 중간에서 매핑

- 대중적인 언어에는 대부분 ORM 기술이 존재

 

JPA를 사용하는 이유

- SQL 중심적인 개발에서 객체 중심으로 개발

- 생산성

- 유지보수

- 패러다임의 불일치 해결

- 성능

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

 

생산성

: CRUD 가 간결해 진다.

 

유지보수

: 기존엔 필드 변경시 모든 SQL 쿼리를 수정해야 했지만, JPA 를 사용하면 변경된 필드에 맞게 JPA가 알아서 처리해준다.

 

✅ 패러다임의 불일치 해결

: 데이터베이스는 데이터 중심으로 구조화 되어 있다. 객체의 상속, 다형성 같은 개념이 없다. 그러다보니 객체와 데이터베이스가 지향하는 점이 다르다. 이것을 객체와 데이터베이스의 패러다임 불일치라고 한다. 자바 언어는 객체지향으로 이루어져 있고 데이터베이스는 데이터 중심으로 구조화되어 있기 때문에 패러다임 불일치 문제를 개발자가 해결해야 한다.

 

객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등의 다양한 장치들을 제공한다. 그리고 이러한 객체를 보관하는 현실적인 대안은 관계형 DB이다. 여기서 다음과 같은 객체와 관계형 DB의 차이로 인해 SQL 매핑이 필요하다.

 

1. 상속

 

객체는 위와 같이 상속 기능을 지니지만 테이블은 상속의 기능이 없다. 데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 유사한 상태로 테이블을 설계할 수는 있다.

위의 사진을 예시로 Item 객체를 상속받는 Album 객체를 DB에 저장한다면, 객체를 테이블에 맞추어 분해하고 각 테이블에 INSERT SQL 을 날려줘야 한다. 그리고 Album 객체를 DB 에서 조회한다면, 각각의 테이블에 따른 JOIN SQL 을 작성하고 객체를 생성 후 합치는 과정을 거쳐야 한다.

 

2. 연관관계

객체의 경우 참조를 사용해서 member.getTeam() 처럼 연관관계를 정하지만, 테이블은 FK(Foreign Key) 를 사용하여 JOIN ON M.TEAM_ID = T.TEAM_ID 처럼 표현한다. 때문에 객체를 개발할 때 테이블에 맞추어 모델링을 하는 경우가 많다. 즉, Team team 필드 대신에 FK인 Long teamId 필드를 두어서 설계한다. 결국 점점 SQL 에 의존적으로 개발을 진행하게 된다.

 

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

class Team {
     Long id; //TEAM_ID PK사용
     String name; //NAME 컬럼 사용
 }

이는 객체를 테이블에 맞추어 모델링하는 경우다. 이렇게 설계하면 객체를 테이블에 저장하거나 조회할 때는 편하다. 하지만 teamId 필드에 문제가 있다. 관계형 데이터베이스는 조인 기능이 있어 외래 키의 값을 그대로 보관해도 된다. 하지만 객체는 연관된 객체의 참조를 보관해야 Team team = member.getTeam() 메서드통해 연관된 객체를 찾을 수 있다.

 

 

3. 객체 그래프 탐색과 Entity 신뢰 문제

객체는 자유롭게 객체 그래프를 탐색할 수 있어야 한다. 하지만 DB 와의 연관관계 차이 때문에 신뢰 문제가 발생한다.

SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
...



class MemberService {
    ...
    public void process(String memberId) {
        Member member = memberDAO.find(memberId);
        // 자유로운 객체 그래프 탐색, 그래프 탐색이 가능한가 ?
        member.getTeam();
        // 이게 가능한지 확신할 수 있는가 ?
        member.getOrder().getDelivery(); 
    }
}

위의 코드에서 객체 그래프를 어디까지 탐색할 수 있는 지 알 수 없다. 전적으로 SQL 문에 달려있기 때문이다.

JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL 문을 사용한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 조회할 수 있다. 이 기능은 실제 객체를 사용할 때까지 조회를 미룬다고 하여 Lazy Loading 이라고 한다.

 

class Member {
  private Order order;
  
  public Order getOrder() {
    return order;
  }
}

//처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);

Order order = memeber.getOrder();
order.getOrderDate(); //Order를 사용하는 시점에 SELECT ORDER SQL

 

4. 객체 비교

데이터베이스는 기본 키의 값으로 각 로우를 구분한다. 반면, 객체는 동일성 비교와 동등성 비교라는 두가지 방법이 있다.

동일성 비교는 == 비교다. 객체의 인스턴스 주소 값을 비교한다. 동등성 비교는 equals() 메소드를 통해 객체 내부의 값을 비교한다.

JPA는 동일한 트랜잭션에서 같은 객체가 조회되는 것을 보장한다.

String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
// member1 == member2 ??? 두 객체는 다르다.

public Member getMember(String memberId) {
    String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
    // JDBC API, SQL 실행
    return new Memeber(...);
}

기존의 방식으로 같은 ID를 가진 객체를 조회하려고 하더라도, 매번 새로운 객체를 생성하기 때문에 서로 다른 객체가 반환됩니다.

 

String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2; //같다.

정리해보면, 객체답게 모델링 할수록 SQL과 매핑하는 작업만 늘어나게 되어 SQL에 의존적으로 설계하게 되는 문제가 발생한다. JPA는 객체를 마치 List와 같은 자바 컬렉션에 저장하듯이 DB에 저장해서 문제를 해결한다.

 

✅ 성능 (최적화 기능)

1. 1차 캐시와 동일성 보장

2. 트랜잭션을 지원하는 쓰기 지연 (Transactional Write-behind)

3. 지연 로딩 (Lazy Loading)

 

1. 1차 캐시와 동일성 보장

String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL
Member m2 = jpa.find(Member.class, memberId); //캐시

println(m1 == m2) //true

같은 트랜잭션 안에서는 캐싱을 통해 같은 Entity 를 반환한다. 즉, m1 과 m2 를 조회할 때 한번의 SQL 만 실행한다.

 

2. 트랜잭션을 지원하는 쓰기 지연 (Transactional Write-behind)

transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

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

트랜잭션을 커밋할 때까지 SQL 을 모으다가 JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송한다. 

 

3. 지연로딩

// SELECT * FROM MEMBER
Member member = memberDAO.find(memberId);

Team team = member.getTeam();

// SELECT * FROM TEAM
String teamName = team.getName();

지연 로딩은 객체가 실제로 로딩될 때 로딩이 실행된다. memberDAO.find(memberId), team.getName()

 

/*
    SELECT M.*, T.* 
    FROM MEMBER
    JOIN TEAM …
*/

Member member = memberDAO.find(memberId);
Team team = member.getTeam();
String teamName = team.getName();

즉시 로딩은 JOIN SQL 로 한번에 연관된 객체까지 미리 조회한다.

 


 

📚 JPA 시작

persistence.xml

JPA는 persistence.xml을 사용해서 필요한 설정 정보를 관리한다.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">

    <persistence-unit name="hello">
		<properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
		</properties>
	</persistence-unit>
</persistence>

 

이 파일은 /META_INF/persistenc.xml 에 위치해야 한다.

 

JPA 구동 방식

- 먼저 persistence.xml 파일을 조회해서 설정에 맞게 DB를 구성한다.

- DB 에 접근할 때 매번 커넥션을 생성해주는 EntityManagerFactory 를 생성한다. 이는 각 DB 당 하나만 생성해서 Application 전체에서 공유해야 한다.

- 각 커넥션 때마다 EntityManager 가 생성되어 트랜잭션을 처리한 후 소멸된다. 이는 쓰레드 간에 공유하면 안되며 각각의 커넥션마다 생성하고 다 사용했다면 버려야 한다.

 

JPA 동작 확인

public class JpaMain {
    public static void main(String[] args) {
        System.out.println("STARTTTTT");
        // [엔티티 매니저 팩토리] - 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpaStudy");
        
        // [엔티티 매니저] - 생성
        // JPA 의 기능 대부분을 제공, CRUD
        // 내부에 데이터 베이스를 유지하면서 데이터베이스와 통신
        EntityManager em = emf.createEntityManager();
        
        // [트랜잭션] - 획득
        // JPA 를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야 함
        EntityTransaction tx = em.getTransaction();

        try {
            tx.begin();     // [트랜잭션] - 시작
            logic(em);      // 비즈니스 로직 실행
            tx.commit();    // [트랜잭션] - 커밋, 커밋하는 순간 데이터베이스에 INSERT SQL 을 보낸다
        } catch (Exception e) {
            tx.rollback();  // [트랜잭션] - 롤백
        } finally {
            em.close();     // [엔티티 매니저] - 종료
        }
        emf.close();        // [엔티티 매니저 팩토리] - 종료
    }

    public static void logic(EntityManager em) {
        String id = "memberId";
        // 비영속
        Member member = new Member();
        member.setId(id);
        member.setUsername("memberA");
        member.setAge(28);

        // 영속성 컨테스트에 등록, 영속상태
        em.persist(member);

        // 수정
        member.setAge(18);

        // 1건 조회
        Member findMember = em.find(Member.class, id);
        System.out.println("findMember = " + findMember.getUsername() + ", age = " + findMember.getAge());

        // 목록 조회
        List<Member> members = em.createQuery("select m from Member m", Member.class)
                .getResultList();
        System.out.println("member size = " + members.size());

        // 삭제
        em.remove(member);
    }
}

 

자세한 설명은 주석에 모두 첨부해 두었다 :)

 

영속성 관리

JPA 를 사용하면서 영속성, 영속성 Context 라는 단어가 자주 등장한다. 이는 JPA 에서 가장 중요한 2가지 중 하나이다. 

다른 하나는 객체와 관계형 DB 매핑 이다.

 

영속성 Context

Entity 를 영구 저장하는 환경이라는 의미로, 눈에 보이지 않는 논리적인 개념이다.

EntityManager를 통해서 영속성 Context 에 접근한다. 이를 구조도로 표현하면 아래와 같다.

 

 

영속성 생명주기

비영속(new/transient)

: 영속성 Context와 전혀 관계가 없는 새로운 상태를 의미합니다.

 

영속(managed)

: JPA를 통해 객체가 영구 저장(DB에서 관리)된 상태를 의미합니다.

 

준영속(detached)

: 영속 상태였던 객체를 영속성 Context에서 분리한 상태를 의미합니다.

 

삭제(removed)

: 객체를 삭제한 상태를 의미합니다.

// 객체를 생성: 비영속
Member member = new Member();
member.setId("membeA");
member.setName("Joon");

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

// 객체를 저장: 영속
em.persist(member);

// 객체를 분리: 준영속
em.detach(member);

// 객체를 제거: 삭제
em.remove(member);

영속성 Context의 이점

JPA 의 이점들과 유사하다.

 

 

1차 캐시

 

//엔티티를 생성한 상태(비영속) 
Member member = new Member(); 
member.setId("memberA"); 
member.setUsername("회원1");

//엔티티를 영속, 1차 캐시에 저장
em.persist(member);

// 1차 캐시에서 조회 (SELECT query 필요 x)
Member findMember1 = em.find(Member.class, "memberA");

// DB에서 조회 (SELECT query 실행)
Member findMember2 = em.find(Member.class, "memberB");

em.persist(member)로 객체를 영속화하면 각 EntityManager에서 관리하는 1차 캐시에 객체를 저장합니다. 실제 DB에 저장되는 시점은 트랜잭션이 Commit될 때이므로 같은 트랜잭션에서 객체가 변경되는 정보들을 모와서 한번에 쿼리를 날립니다.

 

 

 

엔티티 등록, 트랜잭션을 지원하는 쓰기 지연

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

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

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

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

 

 

하나의 트랜잭션을 기준으로 영속성 Context가 관리하는 객체에 변경된 정보들을 모두 모와서 쓰기 지연 SQL 저장소에 각 SQL을 저장해놓고, Commit되는 순간 DB에 SQL을 보냅니다.

 

엔티티 수정, 변경 감지

 

영속성 Context에서 관리하는 객체에 정보 변경이 생긴 경우, 1차 캐시에 저장되어 있는 스냅샷과 비교를 하고 달라진 부분에 맞게 SQL을 생성해서 쓰기 지연 SQL 저장소에 저장한다. 그리고 Commit되는 순간 Flush한다.

 

여기서 Flush란 영속성 Context의 변경 내용을 DB에 반영하는 것을 의미한다. 종종 용어 때문에 반영 후 1차 캐시를 비우는 것으로 오해하는 경우가 있는데, Flush되더라도 1차 캐시는 EntityManager의 close() 혹은 clear() 등의 메서드가 호출되지 않는 이상 유지된다. em.flush()  호출되거나, 트랜잭션이 Commit되거나, JPQL쿼리가 실행되는 경우 Flush가 동작한다.

 

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(); // [트랜잭션] 커밋

 

💡 플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)

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

 

📚 Entity 매핑

객체와 테이블 매핑

@Entity 가 붙은 클래스는 JPA가 관리하며, 이를 엔티티라 한다. JPA 를 사용해서 테이블과 매핑할 클래스는 @Entity 가 필수이며 저장할 필드에 final 은 존재하면 안된다.

 

@Table 은 Entity 와 매핑할 테이블을 지정한다. (JPA에서 사용할 Entity 이름) name 속성으로 매핑할 테이블 이름을 정할 수 있고, 기본 값은 Class 이름을 그대로 사용한다. 

 

데이터베이스 스키마 자동생성

JPA는 데이터베이스 스키마 자동 생성 옵션이 있다. 해당 옵션에 따라서 DB Dialect를 활용해서  DDL 을 애플리케이션 실행 시점에 자동으로 생성해준다.

 

<!-- 기존 테이블 삭제 후 다시 생성한다. -->
<property name="hibernate.hbm2ddl.auto" value="create" />

<!-- create와 유사하나 종료 시점에 테이블을 삭제한다. -->
<property name="hibernate.hbm2ddl.auto" value="create-drop" />

<!-- 변경분만 반영
	 Field가 추가되면 alter DDL을 보낸다.
	 Field가 삭제되더라도 별도의 DDL을 보내지 않는다. -->
<property name="hibernate.hbm2ddl.auto" value="update" />

<!-- Entity와 테이블이 정상 매핑되었는지만 확인
 	 Entity의 Field와 테이블의 Column이 다르면 Error 발생 -->
<property name="hibernate.hbm2ddl.auto" value="validate" />

 

실제 운영 장비에는 createcreate-dropupdate 옵션을 사용하면 기존 테이블을 삭제하거나 변경하므로 절대 사용하면 안되고 웬만하면 Schema 자동 생성 옵션을 사용하지 않는 것이 좋다.

 

Field와 Column 매핑

Entity의 Field와 DB의 Column을 매핑할 때 사용하는 annotation과 속성이 있다.

import javax.persistence.*; 
import java.time.LocalDate; 
import java.time.LocalDateTime; 
import java.util.Date; 

@Entity 
public class Member { 
    @Id 
    private Long id; 

    @Column(name = "name") 
    private String username; 

    private Integer age; 

    @Enumerated(EnumType.STRING) 
    private RoleType roleType; 

    @Temporal(TemporalType.TIMESTAMP) 
    private Date createdDate; 

    @Temporal(TemporalType.TIMESTAMP) 
    private Date lastModifiedDate; 

    @Lob 
    private String description; 
    //Getter, Setter… 
}

 

@Column

속성 설명 기본값
name Field와 매핑할 테이블의 Column 이름 객체의 Field 이름
insertable / updatable 등록 / 변경 가능 여부 TRUE
nullable(DDL) null 값의 허용 어부를 결정 TRUE
unique(DDL) 하나의 Column에 unique 제약 조건을 설정
(@Table의 uniqueConstraints와 역할은 유사)
 
columnDefinition(DDL) DB Column 정보를 직접 설정
(ex. "varchar(100) default 'EMPTY'")
 
length(DDL) String 타입의 문자 길이 제약 조건 설정 255
precision / scale (DDL) BigDecimal 타입에서 표현 정도를 설정 precision=19 / scale=2

 

@Temporal

자바 날짜 타입을 매핑할 때 사용한다. 근래 들어서는 LocalDateLocalDateTime을 타입으로 하면, 최신 하이버네이트가 지원하기 때문에 annotation을 생략할 수 있다.

속성 설명
value - TemporalType.DATE: 날짜 / DB의 date 타입과 매핑 (ex. 2021-01-04)
- TemporalType.TIME: 시간 / DB의 time 타입과 매핑 (ex. 08:55:42)
- TemporalType.TIMESTAMP: 날짜와 시간 / DB의 timestamp 타입과 매핑 (ex. 2021-01-04 08:55:42)

 

@Enumerated

자바 enum 타입을 매핑할 때 사용한다. 다만 추후 요소가 추가될 경우를 대비해 DB 공간을 조금 더 차지하더라도 EnumType.STRING 을 사용해야 한다. ORDINAL 은 사용 X

속성 설명 기본값
value - EnumType.ORDINAL: enum 순서를 DB에 저장
- EnumType.STRING: enum 이름을 DB에 저장
EnumType.ORDINAL

 

@Lob

Large Object 의 줄임말인 어노테이션으로, DB의 BLOB, CLOB 타입과 매핑한다. 이 annotation 에는 별도로 지정할 수 있는 속성이 없다. 매핑하는 필드 타입이 문자면 CLOB, 나머지는 BLOB으로 매핑한다.

필드에 특정 문자열 길이를 지정하지 않는다면 Default로 varchar(255) 까지 저장할 수 있지만, 사진과 같은 것들을 저장하는 칼럼으로 사용할 경우에는 더 많은 자리수를 사용하기 때문에 @Lob 어노테이션으로 Large Object 를 데이터베이스에 적절하게 저장한다.

 

@Transient

주로 메모리상에서만 임시로 어떤 값을 보관하고 싶은 경우처럼, 매핑하지 않은 필드에 사용한다.

즉, 해당 데이터를 테이블의 컬럼과 매핑시키지 않는다. 예를 들면 회원가입 화면에서 비밀번호 재입력과 같은 경우.

 

기본 Key 매핑

Entity 를 식별할 수 있는 Key르 ㄹ매핑할 때 사용할 수 있는 어노테이션(@id, @GeneratedValue) 과 속성(전략)이 있다.

 

@id

직접 할당, 테이블 상의 Primary Key 와 같은 의미를 가진다

@Entity
public class Member {
    @Id
    private Long id;
}

 

@GeneratedValue

자동 생성, Entity 객체 생성 시 자동으로 Key 를 정해주는 방법으로, 각 전략에 따라서 자동으로 생성된다.

 

- IDENTITY

: 기본 Key 생성을 데이터베이스에 위임하는 전략이다. 

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

Member member = new Member();
em.persist(member);
// 원래는 아직은 SQL이 보내지지 않지만, IDENTITY 전략을 사용할 경우 바로 SQL이 실행된다.

tx.commit();

다만 여기서 문제는 해당 Entity를 DB에 저장해야만 기본 Key를 알 수 있다는 점이다. JPA는 트랜잭션을 Commit 하기 전까지 1차 캐시에 Entity들을 보관하는데, IDENTITY 전략으로 Key가 자동 생성된다면 Commit하기 전까지 Key를 알 수 없게 되어, 1차 캐시 활용을 제대로 못하기 때문이다. 따라서 예외적으로 이 전략으로 설정된 Entity의 경우, JPA는 persist() 메서드 호출되면 바로 SQL을 보내서 Key를 받아온다.

 

- SEQUENCE

 데이터베이스의 시퀀스 오브젝트를 사용한다.

@Entity
@SequenceGenerator(
    name = "MEMBER_SEQ_GENERATOR",
    sequenceName = "MEMBER_SEQ",	// 매핑할 DB Sequence 이름
    initialValue = 1, allocationSize = 1)
public class Member {
    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "MEMBER_SEQ_GENERATOR")
    private Long id;
}

 

Key에 알맞는 유일한 값을 순서대로 생성하는 특별한 DB Object인 DB Sequence를 이용해 자동 생성하는 전략이다. @SequenceGenerator에 사용할 수 있는 속성은 다음과 같다.

 

속성 설명 기본값
name 식별자 생성기 이름 필수
sequenceName DB에 등록되어 있는 Sequence 이름 hibernate_sequence
initialValue Sequence DDL을 생성할 때 시작하는 수 지정
(DDL 생성 시에만 사용)
1
allocationSize Sequence 호출 한 번에 증가하는 수
(DB Sequence 값이 하나씩 증가하도록 설정되어 있다면
이 값을 반드시 1로 설정)
50
catalog / schema DB catalog, schema 이름  

 

여기서 allocationSize의 기본값이 50인 이유는 성능 최적화를 위함이다. SEQUENCE 전략도 위와 마찬가지로 DB에 Entity를 저장해야만 Key를 알 수 있는데, 이는 매번 저장할 때마다 SQL을 보내야함을 의미한다. 때문에 성능 문제를 고려하여 JPA는 한 번에 Sequence를 DB로 부터 받아와서 Entity의 Key Field에 할당 가능한 Sequence를 메모리에 보관한다. initialValue가 1인 경우, allocationSize에 크기만큼 Sequence를 받아오고, DB Sequence에는 size만큼 증가시켜놓습니다. Size가 너무 큰 경우, 중간에 Sequence가 낭비될 수 있기 때문에 주로 50 혹은 100으로 정해서 사용한다.

 

- Table

reate table MY_SEQUENCES ( 
     sequence_name varchar(255) not null, 
     next_val bigint, 
     primary key ( sequence_name ) 
)
...

@Entity 
@TableGenerator( 
     name = "MEMBER_SEQ_GENERATOR", 
     table = "MY_SEQUENCES", 
     pkColumnValue = “MEMBER_SEQ", allocationSize = 1) 
public class Member { 
     @Id 
     @GeneratedValue(strategy = GenerationType.TABLE, 
     generator = "MEMBER_SEQ_GENERATOR") 
     private Long id;
     ...

Key 생성 전용 테이블을 하나 만들어서, DB Sequence를 흉내내는 전략이다. 모든 DB에 적용 가능하지만, 성능 문제가 발생할 수 있다. @TableGenerator에 사용할 수 있는 속성은 다음과 같다.

 

속성 설명 기본값
name 식별자 생성기 이름 필수
table Key 생성 테이블 명 hibernate_sequences
pkColumnName Sequence Column 명 sequence_name
valueColumnNa Sequence 값 Column 명 next_val
initialValue 시작하는 수 지정 0
allocationSize Sequence 호출 한 번에 증가하는 수 50
catalog / schema DB catalog / schema  
uniqueConstraints(DDL) Unique 제약 조건 지정  

 

- Auto

기본 전략으로, DB Dialect 에 따라 자동 지정된다.


📚 연관관계 매핑 기초

이 파트에서 무엇보다도 객체와 관계형 DB 의 테이블 연관관계의 차이를 이해하는 것이 중요하다. 먼저 테이블 연관관계에만 중점을 둬서 객체 모델링을 진행한다. 예시로 회원 Entity 와 팀 Enity 가 있고, MEMBER 테이블이 TEAM 테이블을 참조하여 모델링한다. 회원은 하나의 팀에만 소속될 수 있고 회원과 팀은 다대일 관계다.

 

 

객체를 테이블에 맞추어 모델링하면 아래와 같다. 참조 대신 외래 키를 그대로 사용하고 외래 키 식별자를 직접 다룬다.

@Entity
 public class Member { 
     @Id @GeneratedValue
     private Long id;
     @Column(name = "USERNAME")
     private String name;
     @Column(name = "TEAM_ID")
     private Long teamId; 
     … 
 } 
 
 @Entity
 public class Team {
     @Id @GeneratedValue
     private Long id;
     private String name; 
     … 
 }
 
 //팀 저장
 Team team = new Team();
 team.setName("TeamA");
 em.persist(team);
 
 //회원 저장
 Member member = new Member();
 member.setName("member1");
 member.setTeamId(team.getId());
 em.persist(member);
 
 //조회
 Member findMember = em.find(Member.class, member.getId()); 
 //연관관계가 없음
 Team findTeam = em.find(Team.class, team.getId());

 

코드를 보면 알 수 있듯이, FK(Foreign Key) 를 Field로 두어서 객체를 테이블에 맞추어 모델링을 한다. 이렇게 모델링이 되면 비즈니스 로직에서는 객체 간의 연관관계는 없는 상태가 된다. 테이블의 경우 FK로 JOIN해서 연관된 테이블을 찾고, 객체의 경우 참조를 통해 연관된 객체를 찾는, 서로 다른 연관관계 패러다임 때문이다.

따라서 JPA에서는 객체의 연관관계를 사용하여 객체 지향 모델링을 지원한다. 이번 예시는 회원 Entity와 팀 Entity가 N:1 관계로 서로를 양방향 참조하는 걸로 설명하겠다.

 

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

    private String name;

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

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

    private String name;

    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
}

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setName("memberA");
member.setTeam(team);
em.persiste(member);

Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("isEqual = " + (team == findTeam));	// true (1차 캐시에서 가져오기 때문)

List<Member> members = findTeam.getMembers();
for (Member m: members) {
    System.out.println("m = " + m.getName());
}
// 참조를 사용해서 연관관계 조회 (객체 그래프 탐색 가능)

회원 Entity 에는 연관관계를 갖는 Team 객체 Field를 갖고, 팀 Entity 에도 연관관계를 갖는 Member 객체 Field 를 갖는다. 이때 회원과 팀은 N:1 관계이므로 @ManyToOne, @OneToMany annotation으로 설정한다. 여기서 중요한 부분은 두 Entity 연관관계의 주인을 정하는 것이다. 

 

연관관계의 주인이란, 비즈니스 로직 상의 상하관계와는 별도로, 단순히 테이블 구조 상에서 FK를 관리하는 Entity를 의미한다. 위 예시에서 객체 연관관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개인 것으로, 회원 -> 팀 1개와 팀 ->회원 1개로 총 2개이다. 그러나 테이블에서는 FK 하나로 두 테이블의 관계를 관리하여 JOIN 문으로 양쪽 정보를 가질 수 있다. 따라서 두 종류의 Entity 중 하나가 FK를 관리하도록 설정해야 한다. 이게 연관관계의 주인인 것이고, 주인만이 FK를 관리하여 주인이 아닌 쪽은 읽기만 가능하다.

 

그래서 Member Entity 의 Team Field 에는 @JoinColumn 으로 주인임을 나타내고, Team Field 에는 mappedBy 속성으로 주인이 아님을 나타낸다. (주인은 mappedBy 를 사용하지 않는다.) 더불어서 이처럼 연관관계 매핑이 되었을 때 생길 수 있는 문제를 살펴보자.

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setName("memberA");
member1.setTeam(team);
em.persist(member1);

Member member2 = new Member();
member2.setName("memberB");
member2.setTeam(team);
em.persist(member2);

Member member3 = new Member();
member3.setName("memberC");
team.getMembers().add(member3);     // 역방향
em.persist(member3);

Team findTeam = em.find(Team.class, team.getId());

// flush되기 전이므로, 순수 객체상태로 member3만 list에 존재
List<Member> findTeamMembers = findTeam.getMembers();
for (Member m : findTeamMembers) {
	System.out.println("m = " + m.getId() + ": " + m.getName());
}

em.flush();
em.clear();

// flush된 후이고 연관관계 주인은 Member이므로, member1과 member2만 list에  
List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
	System.out.println("m = " + m.getId() + ": " + m.getName());
}

현재 연관관계 주인은 회원이므로 영속화된 Entity는 FK 를 갖고 있는 회원에 의해 관리되고 팀 Entity 는 갱신된 회원 Entity 에서 가져오는 것만 가능하다. Flush 되기 전, team 이 참조하는 member 로는 순수 객체 상태인 member3 뿐이지만, Flush 가 되어 영속화되면 역방향으로 참조한 member3는 무시되고 member1과 member2 만 연관관계를 갖게 된다. 이 문제를 해결하기 위해, setTeam으로 단방향으로만 Field 값을 주입하는 것 대신에 양쪽 모두 값을 넣어주는 연관관계 편의 메서드를 생성해서 설정해야 한다.

 

@Entity
public class Member {
    ...
    public void addTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setName("memberA");
// member1.setTeam(team);
member1.addTeam(team);
em.persist(member1);

Member member2 = new Member();
member2.setName("memberB");
// member2.setTeam(team);
member2.addTeam(team);
em.persist(member2);

Member member3 = new Member();
member3.setName("memberC");
// team.getMembers().add(member3);     // 역방향
member3.addTeam(team);
em.persist(member3);

Team findTeam = em.find(Team.class, team.getId());

// 양방향 모두 값을 설정해줬기 때문에, member1, member2, member3 모두 list에 존재
List<Member> findTeamMembers = findTeam.getMembers();
for (Member m : findTeamMembers) {
	System.out.println("m = " + m.getId() + ": " + m.getName());
}

em.flush();
em.clear();

// flush된 후에도 양쪽 모두 값 설정이 되어 있기 때문에, member1, member2, member3 모두 list에 존재
List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
	System.out.println("m = " + m.getId() + ": " + m.getName());

 

내용이 길어 다음 글에 추가로 작성하겠다 : )

 

[참고]

https://velog.io/@tmdgh0221/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-%EC%A0%95%EB%A6%AC

728x90

'Java' 카테고리의 다른 글

[스프링 부트와 JPA 활용 2] 정리  (0) 2024.01.12
자바 ORM 표준 JPA 기본편 정리 [2]  (2) 2023.12.04
[스프링 부트와 JPA 활용 1] 정리  (0) 2023.11.13
싱글톤 패턴이란  (0) 2023.10.25
[JAVA] 스프링과 객체지향  (0) 2023.10.15
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.