Programming-[Backend]/JPA

[JPA기본] 5. 기본 키 매핑과 연관관계 매핑 기초

컴퓨터 탐험가 찰리 2021. 9. 27. 21:05
728x90
반응형

 

 

1. 기본 키 매핑

 

@GeneratedValue - 기본 키 생성 전략

기본키를 매핑하는 @Id 어노테이션과 @GeneratedValue 어노테이션에 대해서 알아본다.

 

id값을 직접 매핑한다면 @Id 어노테이션만 있으면 된다. 그런데 수많은 데이터를 처리하므로 보통 기본 키값인 Id값은 자동으로 생성되도록 하는데, 이것을 해주는 것이 @GeneratedValue 어노테이션이다. @GeneratedValue 어노테이션은 자동 생성을 하는 방식을 strategy 속성으로 지정해줄 수 있다. 이 속성의 value는 다음 4가지로 설정이 가능하다.

ex) @GeneratedValue(strategy = GenerationType.IDENTITY)

 

GenerationType.AUTO

GenerationType.IDENTITY

GenerationType.SEQUENCE

GenerationType.TABLE

 

 

AUTO

AUTO는 기본값이며, dialect(방언)에 따라 자동으로 지정된다. 데이터베이스의 dialect 타입에 따라 나머지 IDENTITY, SEQUENCE, TABLE 방식이 지정된다.

 

실습 예제와 같이 H2 Database로 dialect를 지정하면 아래와 같이 sequence ... 으로 표현된다.

 

 

 


 

 

IDENTITY

기본키 생성 방식을 데이터베이스에 위임하는 방식이다. MYSQL의 Auto_increment가 가장 대표적인 예이다. JPA는 앞에서 배운바와 같이 보통 트랜잭션 커밋 시점(tx.commit())에 INSERT SQL이 실행되는데, 이 부분은 예외이다. 왜냐하면 Id값을 자동으로 +1씩 증가시키기 위해서 현재 DB의 Id값이 몇인지 알아야하기 때문이다. 따라서 IDENTITY로 설정하면 em.persist() 시점에 즉시 INSERT SQL이 실행되어 DB에서 식별자 값을 조회한다.

 

아래 사진에서 tx.commit() 전에 INSERT SQL이 나가고, id의 values 값이 특정값인 ?가 아니라 null로 입력되는 것을 확인할 수 있다. 실제 DB에서는 Long Type의 값으로 1, 2, ... 와 같이 지정된다.

 

persist가 일어날때마다 id값을 조회하므로 쓰기 지연 SQL 저장소에서 한번에 SQL을 내보내는 방식을 사용할 수 없다. 그래서 다소의 성능저하가 있을 수 있다.

 


 

SEQUENCE

SEQUENCE 전략은 데이터베이스에서 유일값으로 지정하는 시퀀스를 기반으로 기본키 값을 지정하는 방식이다. 이 방식 역시 persist때 다음 sequence 값을 호출받는다. 다만, 아래 코드와 같이 @SequenceGenerator를 이용하여 한번에 Sequnce값을 할당받으므로 최초 sequence 이후 next sequence까지는 SQL문을 지연했다가 내보낼 수 있다.

 

IDENTITY 전략이 데이터베이스의 id값을 조회하는 것과, SEQUENCE 전략이 데이터베이스의 고유값인 시퀀스를 기반으로 한다는 것은 별다를게 없어보인다. 그러나, @SequenceGenerator의 allocationSize값을 지정한다는 점에서 시퀀스 기반은 성능 최적화를 위한 지원을 해준다.

 

src/main/java/hellojpa/Member.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
@Getter
@Setter
@Table(name="members")
@DynamicInsert
@SequenceGenerator(
    name="MEMBER_SEQ_GENERATOR",
    sequenceName = "MEMBER_SEQ",
    initialValue = 1, allocationSize = 100
)
public class Member {
 
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
  private Long id;
cs

 

persist를 여러번 호출하여 여러 객체를 저장하는 경우를 보자.

 

src/main/java/hellojpa/JpaMain.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
      //코드가 들어갈 부분
      Member memberA = new Member();
      memberA.setUsername("회원A");
 
      Member memberB = new Member();
      memberB.setUsername("회원B");
 
      Member memberC = new Member();
      memberC.setUsername("회원C");
 
      em.persist(memberA);
      em.persist(memberB);
      em.persist(memberC);
      System.out.println("====================================");
 
      tx.commit();
    } catch (Exception e) {
 
...
cs

 

call next value...를 통해 시퀀스 값을 가져오기 위해 데이터베이스와 통신하는 것은 IDENTITY 전략과 다를 바 없다. 그러나 단 한번만 이 과정이 일어나고, persist시마다 일어나지는 않는다. 이것은 allocationSize를 지정해주었기 때문으로, DB에 allocationSize만큼의 공간을 사용할 것이라고 확보해놓고, 실제 메모리에서 persist() 갯수만큼 하나씩 시퀀스를 증가시키며 id값들을 할당한다. 그렇기 때문에 데이터베이스와의 연결 횟수를 최소화하여 성능 향상에 기여할 수 있다.

 

그리고 데이터베이스에서 미리 특정 영역을 확보해놓는 방식이기 때문에, 여러 주체가 동시에 DB에 접근하더라도 동시성에 의한 문제가 발생하지 않는다는 장점이 있다.

 

allocationSize값은 지정하지 않으면 default값이 50이므로 이를 인지하고 주의하여 사용해야 한다. 그리고 만약 100으로 설정한 경우, 트랜잭션이 끝날때마다 100씩 초기값이 증가한다는 것도 인지하고 있도록 하자.

3개씩 persist로 2번 트랜잭션을 날린 경우

 


 

TABLE

Table 전략은 데이터베이스가 아닌 시퀀스값을 저장하는 테이블을 따로 만들어서 이용하는 방식이다. 그러나 최적화된 테이블을 사용하는 방식이 아니므로 성능상의 이슈가 있을 수 있기 때문에 권장하지 않는 방식이라고 한다. 시퀀스 전략과 비슷하게 @TableGenerator이라는 어노테이션을 이용하여 시퀀스 값들을 만든다.

 

 

src/main/java/hellojpa/Member.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
@Getter
@Setter
@Table(name="members")
@DynamicInsert
@TableGenerator(
    name="MEMBER_SEQ_GENERATOR",
    table = "MY_SEQUENCES",
    pkColumnValue = "MEMBER_SEQ",
    initialValue = 1
)
public class Member {
 
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
  private Long id;
cs

 

TABLE 전략을 사용했을 때 실제 만들어진 테이블

 


 

 

2. 연관관계 매핑 기초

 

테이블의 연관관계를 그대로 모방하는 경우 문제점

객체와 테이블은 연관관계가 다른 구조로 되어있다. 위 그림에서 볼 수 있듯이, 테이블의 연관 관계는 외래키를 통해서 연결되어 있다. 그래서 테이블을 기준으로 객체를 설계하였다. 이 상태에서 데이터를 만들어보면 전혀 객체 지향적인 설계가 아니고, 객체간 탐색이 비효율적이게 된다.

 

엔티티 작성

Member와 Team 엔티티를 작성한다. @Column에 대문자 및 snake_case를 적용하여 이름을 바꿔줬는데, 이것은 테이블의 이름 규칙을 그대로 따라간다는 것을 표현한 것이다. 참고로, 스프링과 하이버네이트를 이용하여 객체 이름을 Camel Case로 작성하면, 기본적으로 snake case로 바꿔서 표현해준다.

 

src/main/java/hellojpa/Member.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Getter
@Setter
@DynamicInsert
public class Member {
 
  @Id
  @GeneratedValue
  private Long id;
 
  @Column(name="USERNAME")
  private String username;
 
  @Column(name = "TEAM_ID")
  private Long teamId;
 
}
cs

 

src/main/java/hellojpa/Team.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Getter
@Setter
@DynamicInsert
public class Team {
 
  @Id
  @GeneratedValue
  @Column(name = "TEAM_ID")
  private Long id;
 
  private String name;
 
}
cs

 

엔티티 저장 및 조회

아래 코드 및 주석을 보면, 객체를 저장하거나 조회할 때 외래키 값을 직접 다뤄야하기 때문에 코드가 늘어나고 효율적이지 못하다는 것을 알 수 있다.

 

 

src/main/java/hellojpa/JpaMain.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
... 
 
try {
      Team teamA = new Team();
      teamA.setName("teamA");
      em.persist(teamA);
 
      Member member1 = new Member();
      member1.setUsername("member1");
      member1.setTeamId(teamA.getId()); //외래키 값을 다시 꺼내온다.
      em.persist(member1);
 
      //member1의 team을 조회
      Member foundMember = em.find(Member.class, member1.getId());
      Long foundMemberTeamId = foundMember.getTeamId(); //id 조회 후 다시 Team 객체에 대고 조회
      Team foundMemberTeam = em.find(Team.class, foundMemberTeamId);
 
 
      tx.commit();
    } catch (Exception e) {
 
...
 
cs

 


연관관계 맺기 : 단방향 연관관계

 

가장 기본이 되는 연관관계인 단방향 연관관계를 맺어본다. 위에서 살펴봤듯이, 테이블 중심적인 객체(엔티티) 설계로는 불편한 점들이 많다. 따라서 Member 객체를 아래와 같이 변경한다.

 

@ManyToOne

Member 객체에 Team 객체를 바로 필드값으로 지정한다. 그리고 @ManyToOne 어노테이션을 달아주는데, 이것은 class의 주인인 Member가 Many, 필드값인 Team이 One으로 N:1의 관계를 갖는다는 것을 표시해주는 것이다.

 

@JoinColumn

@JoinColumn은 @ManyToOne과 짝지어지는 어노테이션이라고 할 수 있다. Member 테이블 상에서 Team 객체처럼 사용할 외래키의 이름을 name 속성으로 지정해주면 된다.

 

만약 이 어노테이션을 넣지 않으면, 특정 컬럼을 기반으로 두 객체를 이어주는 것이 아니라 두 객체를 매핑하는 테이블을 하나 새로 생성함으로써 객체를 이어주게 된다. 지금 예제에서 @JoinColumn이 없으면, MEMBER_TEAM이라는 서로의 id값을 기반으로하는 테이블이 생성된다. 이런 방식을 JoinTable 전략방식이라고 하며, @JoinTable 어노테이션으로 해당 table의 name 등 여러 속성을 직접 지정해줄 수도 있다.

 

 

src/main/java/hellojpa/Member.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Getter
@Setter
@DynamicInsert
public class Member {
 
  @Id
  @GeneratedValue
  private Long id;
 
  @Column(name="USERNAME")
  private String username;
 
  @ManyToOne
  @JoinColumn(name = "TEAM_ID")
  private Team team;
 
}
cs

 

 

실행 코드 수정

필드값 입력 시, Team 객체를 setTeam으로 바로 세팅하고, 찾아올때도 getTeam으로 간편하게 찾아오게 되는 것을 확인할 수 있다.

 

src/main/java/hellojpa/JpaMain.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
   try {
      Team teamA = new Team();
      teamA.setName("teamA");
      em.persist(teamA);
 
      Member member1 = new Member();
      member1.setUsername("member1");
      member1.setTeam(teamA);
      em.persist(member1);
 
      //member1의 team을 조회
      Member foundMember = em.find(Member.class, member1.getId());
      Team team = foundMember.getTeam();
...
 
cs

 

유의사항 : 중간 과정에서 DB 저장을 위해 em.flush, em.clear()가 필요하다

사실, 위 코드에서 em.find로 찾아온 member1은 실제 DB에 저장된 member 객체가 아니다. 왜냐하면 em.persist(member)로 영속성 컨텍스트에 member1을 등록했을 뿐, 아직 트랜잭션 커밋이 발생하여 insert 쿼리가 나간게 아니기 때문이다. 의도했던대로 DB에 저장 후, member1의 team을 조회하기 위해서 아래와 같이 코드를 수정한다.

 

src/main/java/hellojpa/JpaMain.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
   try {
      Team teamA = new Team();
      teamA.setName("teamA");
      em.persist(teamA);
 
      Member member1 = new Member();
      member1.setUsername("member1");
      member1.setTeam(teamA);
      em.persist(member1);
 
      em.flush();
      em.clear();
 
      //member1의 team을 조회
      Member foundMember = em.find(Member.class, member1.getId());
      Team team = foundMember.getTeam();
...
cs

 

em.flush()를 적용하여 1차 캐시에 있던 영속성 객체들을 DB에 insert 쿼리를 적용하도록 하고, 1차 캐시에 남아있는 정보를 em.clear()로 지워줘야 한다! 실무에서도 헷갈렸던 내용으로, 영속성 컨텍스트의 개념을 잘 이해하고 있어야 한다는 것을 보여주는 예제이다. 이렇게 하면 em.find로 객체를 찾아올 때, 다시 DB에 select SQL을 날려서 정보를 영속성 컨텍스트에 저장해와서 사용하게 된다.

 

 

 


 

참조

 

 

1. 인프런_자바 ORM 표준 JPA 프로그래밍 - 기본편_김영한 님 강의

https://www.inflearn.com/course/ORM-JPA-Basic/lecture/21735?tab=curriculum

728x90
반응형