https://surrealcode.tistory.com/103
앞서서 왜 테이블 설계와 개발 후 왜 연관관계가 필요한지에 알아봤다.
참고하도록 하자.
목표
- 객체와 테이블 연관관계의 차이를 이해
- 객체의 참조와 테이블의 외래 키를 매핑
- 용어 이해
- 방향(Direction) : 단방향, 양방향
- 다중성(Multiplicity) : 다대일, 일대다, 일대일, 다대다 이해
- 연관관계 주인(Owner) : 객체 양방향 연관관계는 관리 주인이 필요
연관관계가 필요한 이유`
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다
예제 시나리오는 다음과 같다
1. 회원과 팀이 있다.
2. 회원은 하나의 팀에만 소속될 수 있다.
3. 회원과 팀은 다대일 관계이다.
팀과 멤버의 관계에선 팀이 1 멤버가 N이 된다.
여러명의 회원이 하나의 팀에 소속될 수 있고, 하나의 팀에 여러명의 회원이 소속될 수 있다.
@Entity
@Table(name = "MEMBERS")
public class Members {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Column(name = "TEAM_ID")
private Long teamId;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Long getTeamId() {
return teamId;
}
public void setTeamId(Long teamId) {
this.teamId = teamId;
}
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
try {
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Members members = new Members();
members.setUsername("member1");
members.setTeamId(team.getId()); //외래키를 직접 다루어야함
em.persist(members);
//조회
Members findMember = em.find(Members.class, members.getId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
SELECT * FROM MEMBER;
MEMBER_ID TEAM_ID USERNAME
1 | 1 | member1 |
select * from team;
TEAM_ID NAME
1 | TeamA |
만약 내가 찾아온 Member가 어느 팀 소속인지 알고 싶다면 코드를 어떻게 짜야할까?
Member findMember = em.find(Member.class, member.getId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);
Member를 찾고, 찾아온 member의 팀 Id를 찾고, 찾아온 팀Id로 팀을 찾는 복잡하고 번거로운 과정을 거쳐야 소속 팀을 찾을 수 있게 된다.
즉 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이런 큰 간격이 있다.
단방향 연관관계
위의 문제를 객체 지향 스럽게 해결하기 위해 단방향 연관관계를 사용해 보았다.
엔티티에선 @(어노테이션)이 중요하다. 데이터베이스에 매핑 되기 때문이다.
Member가 teamId가 아닌 Team의 참조값을 그대로 가져오는 것이다.
@Entity
@Table(name = "MEMBERS")
public class Members {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
@ManyToOne
Member가 하나의 팀에 소속된다.
Member 입장에서는 many, Team 입장에서는 ONE을 알려주는 어노테이션이다.
@JoinColumn(name = "TEAM_ID")는
@ManyToOne관계를 조인할 떄 조인하는 컬럼이 어떤 컬럼인지 알려주는 어노테이션이다.
이렇게 하면 다음과 같은 ORM 매핑이 이루어 지게 된다.
아래의 그림을 보자. Member 엔티티의 Team을 @JoinColumn(어노테이션)이 중요하다. 데이터베이스 Member테이블 TEAM_ID에 연관관계를 매핑하였다.
try {
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Members members = new Members();
members.setUsername("member1");
members.setTeam(team); //JPA가 알아서 팀에 PK값을 꺼내서 FK값을 사용한다.
em.persist(members);
//조회
Members members1 = em.find(Members.class, members.getId());
Team findTeam = members1.getTeam();
System.out.println("findTeam.getName() = " + findTeam.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
연관관계를 매핑하였기 때문에
members1에서 바로 Team을 꺼내올 수 있게 되었다.
findTeam.getName() = TeamA
Hibernate:
/* insert for
hellojpa.Team */insert
into
Team (name, TEAM_ID)
values
(?, ?)
Hibernate:
/* insert for
hellojpa.Members */insert
into
MEMBERS (TEAM_ID, USERNAME, MEMBER_ID)
values
(?, ?, ?)
양방향 연관관계와 연관관계의 주인
객체랑 테이블 두개는 패러다임 차이가 있다.
객체는 연관관계를 찾을 때 참조라는 것을 사용하고, 테이블은 외래키를 가지고 조인을 활용한다.
우리는 이 둘간의 차이에서 오는 것을 이해해야 한다. 그래야 연관관계 주인이라는 개념이 있다는 걸 이해할 수 있다.
앞서 배웠던 코드는 단방향으로밖에 찾을 수 없다. (Member -> Team으로)
하지만 테이블은 사실 JOIN을 활용해서 양쪽으로 왔다갔다 할 수 있다. (멤버에서 팀으로, 팀에서 멤버로 왔다갔다 가능)
테이블의 연관관계는 외래키 하나로 양쪽으로 다 알 수 있는 것이다. 테이블의 연관관계는 외래키 하나로 양방향이 다 있다. 사실 방향이라는 개념 자체가 없다.
이를 양방향 객체 연관관계로 풀어내려면 Team에 Members라는 List형태를 넣어줘야 양쪽으로 참조가 가능하게 된다.
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") //Member엔티티에 team변수명에 걸려 있다.
private List<Member> members = new ArrayList<>();
public List<Member> getMembers() {
return members;
}
public void setMembers(List<Member> members) {
this.members = members;
}
members라는 List를 만들어주었다.
try {
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
System.out.println("m = " + m.getUsername());
}
tx.commit();
위의 코드를 보면 Member에서 Team으로 Team에서 다시 Member로 왔다갔다 하는 것을 볼 수 있다.
반대 방향으로도 객체 그래프를 탐색할 수 있게 되는 것이다.
** 참고 : 객체는 가급적 단방향이 좋다. 나중에 가면 점점 복잡해지기 때문이다. **
연관관계의 주인과 mappedBy
mappedBy의 정체는 대체 뭘까??
연관관계의 주인과 mappedBy를 알려면 객체와 테이블간 연관관계를 맺는 차이를 이해해야 한다.
객체와 테이블이 관계를 맺는 차이
- 객체 연관관계 = 2개(단방향이 2개)
- 회원 -> 팀, 연관관계 1개(단방향)
- 팀 -> 회원, 연관관계 1개(단방향)
- 테이블 연관관계 = 1개
- 회원 <-> 팀, 연관관계 1개(양방향)
객체의 양방향 관계
- 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다. 억지로 두개를 만들어 양방향 연관관계를 맺어버렸다.
- 객체를 양방향으로 참조 하려면, 단방향 연관관계를 2개 만들어야 한다.
class A {
B b; A -> B (a.getB())
}
class B {
A a; B -> A (b.getA())
}
테이블의 양방향 연관관계
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리할 수 있다.
- MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계를 가진다.(양쪽으로 조인할 수 있다.)
1. SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
2. SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
현재 객체는 두 방향으로 만들어 놨다(참조가 두개이다).
어떤 참조값을 사용해서 매핑을 해야 하는지 딜레마가 온다.
만약내가 멤버를 바꾸고 싶거나, 새로운 팀에 들어가고 싶을 때 Member의 Team 값을 바꿔야될지 아니면 Team의 List값을 바꿔야할지 이상해진다.
디비 입장에서는 둘 중 하나 아무나 상관없이 업데이트 치기만 하면 된다
--> 결국 둘 중 하나로 외래 키를 관리 해야 하기 때문에 연관관계의 주인이 생기게 된다.
양방향 매핑 규칙
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정한다.
- 연관관계의 주인만이 외래 키를 관리한다(등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능하다.
- 주인은 mappedBy 속성을 사용할 수 없다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
--> 누구를 주인으로 해야할까?
- ***외래 키가 있는 곳을 주인으로 정해야 한다.***
- 여기서는 Member.team이 연관관계의 주인이다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
+ DB입장에서도 보면 외래키가 있는 곳이 무조건 N이다.
mappedBy는 수정이 불가능하고 조회만 가능하다.
@OneToMany(mappedBy = "team") //Member엔티티에 team변수명에 걸려 있다.
private List<Member> members = new ArrayList<>();
mappedBy에는 값을 넣어도 DB가 없데이트 되지 않는다.
양방향 매핑시 가장 많이 하는 실수
(연관관계의 주인에 값을 입력하지 않음)
Member member = new Member();
member.setUsername("member1");
em.persist(member);
Team team = new Team();
team.setName("TeamA");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(team);
List는 mappedBy이지만 add를 해주었다. 이러면 DB 업데이트가 불가능하다.
SELECT * FROM MEMBER;
MEMBER_ID TEAM_ID USERNAME
1 | null | member1 |
select * from team;
TEAM_ID NAME
1 | TeamA |
그래서 DB TEAM_ID가 Null인 것을 확인할 수 있다. 이 문제를 해결하기 위해선 List가 아닌 연관관계 주인에 값을 셋팅해주어야 한다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
SELECT * FROM MEMBER;
MEMBER_ID TEAM_ID USERNAME
1 | 1 | member1 |
select * from team;
TEAM_ID NAME
1 | TeamA |
운영중에 실수하지 않도록 주의하자.
**양방향 매핑시 그냥 양쪽에 값을 다 넣어주도록 하자.
다음과 같은 코드를 확인해 보자.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
// team.getMembers().add(member);
// em.flush();
// em.clear();
Team findTeam = em.find(Team.class, team.getId()); //1차 캐시
List<Member> members = findTeam.getMembers();
System.out.println("==========");
for (Member m : members) {
System.out.println("m.getUsername() = " + m.getUsername());
}
System.out.println("==========");
tx.commit();
flush를 하지 않은 상태이다. team 객체와 member 객체는 1차 캐시에 저장만 되어있는 상태다.
이 상태에서는 team의 List에 어떠한 값도 들어있지 않기 때문에 m.getUsername() for문을 돌려보아도 아무런 결과가 출력되지 않는다.
(위 코드의 결과값)
==========
==========
이 문제 뿐만 아니라 테스트할때도 문제가 생기기 때문에 양방향 매핑시에는 꼭 양쪽에 값을 다 넣어주도록 하자.
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.
- 연관관계 편의 메소드를 생성하자.
이 문제를 좀 가볍게 하기 위해 "연관관계 편의 메소드"를 사용하여 해결한다.
Member에서 값을 set 할 때, team.getMembers().add(this)를 하여 팀에도 한번에 값을 세팅하는 것이다.
@Entity
public class Member {
...
...
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
또는 Team에서 연관관계 편의 메소드를 사용할수도 있다. 둘 중 한곳을 정해서 만들도록 하자.
@Entity
public class Team {
...
...
public void addMember(Member member) {
member.setTeam(this);
members.add(member);
}
}
- 양방향 매핑시에 무한 루프를 조심하자.
- ex) toString(), lombok, JSON 생성 라이브러리
@Entity
public class Member {
...
...
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", team=" + team + //team의 toString 호출
'}';
}
}
@Entity
public class Team {
...
...
@Override
public String toString() {
return "Team{" +
"id=" + id +
", name='" + name + '\'' +
", members=" + members + //members의 toString호출
'}';
}
}
try {
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
em.persist(member);
team.addMember(member);
Team findTeam = em.find(Team.class, team.getId()); //1차 캐시
List<Member> members = findTeam.getMembers();
System.out.println("==========");
System.out.println("members = " + findTeam);
System.out.println("==========");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
양쪽에서 toString을 호출해대며 문제가 생긴다.
깨알 상식 : 컨트롤러에는 엔티티를 절대 반환하면 안된다. 무한루프 문제, 엔티티 변경순간 API 스펙이 변할 수 있다.
양방향 매핑 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이다.
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
- JPQL에서 역방향으로 탐색할 일이 많다.
- 단방향 매핑을 잘 하고, 양방향은 필요할 때 추가해도 된다.(테이블에 영향을 주지 않는다)
객체 입장에서 볼 때 양방향 맵핑은 이득이 크게 많지 않다. 따라서 단방향으로 설계하고, 어플리케이션 개발 시 양방향이 필요한지 고려해서 만들어도 늦지 않다. 기본적으로 단방향으로 정리하자.
연관관계 주인을 정하는 기준
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.
- 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.
실전 예제 - 2. 연관관계 매핑 시작
테이블 구조는 이전과 같다.
참조를 사용하도록 변경하자.
@Entity
public class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(int stockQuantity) {
this.stockQuantity = stockQuantity;
}
}
@Entity
@Table(name = "test")
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
public void setMember(Member member) {
this.member = member;
}
public Member getMember() {
return member;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public LocalDateTime getOrderDate() {
return orderDate;
}
public void setOrderDate(LocalDateTime orderDate) {
this.orderDate = orderDate;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
}
@Entity
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "ORDER_ID")
private Order order;
@ManyToOne
@JoinColumn(name = "ITEM_ID")
private Item item;
private int orderPrice;
private int count;
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public int getOrderPrice() {
return orderPrice;
}
public void setOrderPrice(int orderPrice) {
this.orderPrice = orderPrice;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
'공부 > JPA' 카테고리의 다른 글
JPA 고급 매핑 (2) | 2024.12.08 |
---|---|
다양한 연관관계 매핑 (0) | 2024.12.03 |
엔티티 매핑 (0) | 2024.11.30 |
JPA 영속성 관리 (1) | 2024.11.28 |
JPA 설정하기 (0) | 2024.11.26 |