티스토리 뷰
소스 코드는 여기 있습니다. (commit hash: e336ad7)
> git clone https://github.com/lcalmsky/jpa > git checkout e336ad7
Overview
단방향 연관관계를 설명합니다.
JPA를 설명하기 위해 포스팅하는 글이지만 스프링 내에서 사용하는 경우가 많아 스프링 부트 프로젝트로 구성하였습니다.
프로젝트를 구성하는 내용은 다른 포스팅에도 그동안 많이 작성하였기 때문에 생략하였습니다.
객체 지향 모델링
저번 포스팅에서와 다르게 객체지향적으로 모델링한 모습은 아래와 같습니다.
- 객체 연관관계
Member
객체가 Team
객체를 참조하기 위해선 teamId
를 가지는 게 아니라 Team
객체를 참조하고 있어야 합니다.
- 테이블 연관관계
코드로 나타내면 다음과 같습니다.
/src/main/java/com/tistory/jaimenote/jpa/domain/entity/Member.java
package com.tistory.jaimenote.jpa.domain.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne // (1)
@JoinColumn(name = "team_id") // (1)
private Team team;
private Member(String name) { // (2)
this.name = name;
}
public static Member withName(String name) { // (3)
return new Member(name);
}
public void join(Team team) { // (4)
this.team = team;
}
}
(1) 객체를 참조한 부분에 테이블의 FK
해당하는 컬럼(team_id)을 매핑해줍니다.
(2) 외부에서 객체를 임의로 생성할 수 없게 private
생성자를 사용하였습니다.
(3) 외부에서 객체를 사용할 때 호출할 static
생성자 입니다.
(4) 외부에서 팀을 세팅해주기 위한 메서드 입니다.
/src/main/java/com/tistory/jaimenote/jpa/domain/entity/Team.java
package com.tistory.jaimenote.jpa.domain.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
private Team(String name) {
this.name = name;
}
public static Team withName(String name) {
return new Team(name);
}
}
이렇게 객체간 참조를 애너테이션을 통해 매핑해주게되면, Member
객체 입장에서는 너무 쉽게 Team
객체를 참조할 수 있게 됩니다.
테스트
연관관계 설정만으로 우리가 원하는대로 객체 지향적으로 동작하는지 확인해보겠습니다.
/src/test/java/com/tistory/jaimenote/jpa/infra/repository/MemberRepositoryTest.java
package com.tistory.jaimenote.jpa.infra.repository;
import com.tistory.jaimenote.jpa.domain.entity.Member;
import com.tistory.jaimenote.jpa.domain.entity.Team;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
TeamRepository teamRepository;
@BeforeEach
void setup() {
Team team = Team.withName("토트넘");
teamRepository.save(team);
Member member = Member.withName("손흥민");
member.join(team);
memberRepository.save(member);
}
@Test
void findMemberTest() {
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println(member);
}
}
}
저장보단 조회가 목적이므로 테스트 전에 Team
과 Member
를 저장하도록 하였습니다.
테스트가 실행될 때 로그를 확인해보면,
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate:
create table member (
id bigint not null,
name varchar(255),
team_id bigint,
primary key (id)
)
Hibernate:
create table team (
id bigint not null,
name varchar(255),
primary key (id)
)
Hibernate:
alter table member
add constraint FKcjte2jn9pvo9ud2hyfgwcja0k
foreign key (team_id)
references team
이렇게 테이블을 생성하는데 member
테이블에 team_id
가 동일하게 설정된 것을 확인할 수 있고, 마지막에 FK
로 등록하는 것도 확인할 수 있습니다.
그리고나서 @BeforeEach
애노테이션이 붙어있는 setup
메서드가 수행되는데,
Hibernate:
insert
into
team
(name, id)
values
(?, ?)
2022-06-26 05:33:26.259 TRACE 88204 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [토트넘]
2022-06-26 05:33:26.261 TRACE 88204 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [1]
Hibernate:
insert
into
member
(name, team_id, id)
values
(?, ?, ?)
2022-06-26 05:33:26.266 TRACE 88204 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [손흥민]
2022-06-26 05:33:26.266 TRACE 88204 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [1]
2022-06-26 05:33:26.266 TRACE 88204 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [BIGINT] - [2]
team
, member
가 정상적으로 insert
되었습니다.
그리고 마지막으로 조회하는 부분의 로그를 살펴보면,
Hibernate:
select
member0_.id as id1_0_,
member0_.name as name2_0_,
member0_.team_id as team_id3_0_
from
member member0_
Member(id=2, name=손흥민, team=Team(id=1, name=토트넘))
이렇게 정상적으로 조회된 것을 확인할 수 있습니다.
사실 member
의 로그를 출력하는 과정에서 team
객체를 당연히 참조하게 되는데요, 이 때 추가 쿼리가 발생해야 하지만 현재는 EntityManager
에서 영속 상태로 관리하고 있기 때문에 캐싱된 객체를 바로 반환해주었습니다.
정확한 테스트를 위해서는 아래 처럼 수정해야 합니다.
package com.tistory.jaimenote.jpa.infra.repository;
import com.tistory.jaimenote.jpa.domain.entity.Member;
import com.tistory.jaimenote.jpa.domain.entity.Team;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
TeamRepository teamRepository;
@PersistenceContext // (1)
EntityManager entityManager;
@BeforeEach
void setup() {
Team team = Team.withName("토트넘");
teamRepository.save(team);
Member member = Member.withName("손흥민");
member.join(team);
memberRepository.save(member);
entityManager.flush(); // (2)
entityManager.clear(); // (2)
}
@Test
void findMemberTest() {
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println(member); // (3)
}
}
}
(1) 영속성 컨텍스트를 주입합니다.
(2) 영속성 상태를 저장하고 비워줍니다.
(3) member.toString
이 호출되면서 team
을 출력할 때 team
객체를 참조합니다.
다시 테스트를 실행하여 로그를 확인해보면,
Hibernate:
select
member0_.id as id1_0_,
member0_.name as name2_0_,
member0_.team_id as team_id3_0_
from
member member0_
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
2022-06-26 05:43:48.620 TRACE 89017 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
Member(id=2, name=손흥민, team=Team(id=1, name=토트넘))
정상적으로 쿼리가 두 번 발생한 것을 확인할 수 있습니다.
하지만 이렇게 단순한 쿼리를 매번 2회씩 수행할 필요는 없겠죠?
다양한 방법이 존재하지만 이 부분은 차차 다뤄보도록 하겠습니다.
JPA를 다룬 다른 포스팅에 이미 여러 차례 소개되긴 했지만, 기본적인 부분에 대해 추가로 포스팅하고 있어서 앞으로도 진도와 상황에 맞춰 해당 내용을 추가할 예정입니다.
정리
단방향 연관관계를 맺을 때는 Entity
클래스 중 하나의 클래스에만 @ManyToOne
, @JoinColumn
과 같은 애너테이션을 추가하였습니다.
DB
에서는 team
을 먼저 찾고, team_id
를 이용해 해당 team
의 전체 member
를 찾아낼 수 있지만, 객체는 Member
-> Team
으로 단방향 참조만 일어나기 때문에 Team
을 이용해 Member
를 찾는 것은 불가능합니다. (Member
와 Team
을 이용해 Collection
을 만들고 비교해가면서 찾는 상황은 배제하고)
이렇게 한 방향으로만 참조할 수 있는 관계를 단방향 연관관계라고 합니다.
'JPA' 카테고리의 다른 글
[JPA] 연관관계의 주인 (0) | 2022.06.30 |
---|---|
[JPA] 양방향 연관관계 (0) | 2022.06.29 |
스프링 부트 JPA 예약어 컬럼명 사용하기 (0) | 2022.06.27 |
[JPA] 사용시 연관관계가 필요한 이유 (0) | 2022.06.25 |
스프링 데이터 JPA - 새로운 Entity 판별 (3) | 2021.07.14 |
- Total
- Today
- Yesterday
- Spring Boot
- JPA
- 알고리즘
- Spring Boot JPA
- intellij
- QueryDSL
- @ManyToOne
- spring boot app
- 스프링부트
- 스프링 부트 회원 가입
- spring boot application
- 스프링 부트
- r
- 스프링 부트 애플리케이션
- Jackson
- 헥사고날 아키텍처
- Java
- proto3
- 스프링 부트 튜토리얼
- Spring Data JPA
- gRPC
- 함께 자라기 후기
- Linux
- 클린 아키텍처
- spring boot jwt
- 함께 자라기
- leetcode
- Spring Boot Tutorial
- 스프링 데이터 jpa
- JSON
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |