✅ 양방향 연관관계 (외래키가 있는 테이블이 연관관계의 주인)
양방향 연관관계(객체의 연관관계 , 테이블의 연관관계)는 ****두 개의 객체 간의 상호 참조를 나타내며, 한 객체에서 다른 객체를 참조하는 것뿐만 아니라 그 반대 방향의 참조도 가능한 연관관계를 의미합니다. 이는 객체 지향 모델링의 관점에서 보다 더 현실적이고 완전한 관계를 표현하는 방식입니다.
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개입니다. 객체를 양방향 관계를 참조하기 위해서는 단방향 연관관계를 2개를 생성해야하기 때문입니다.
EX )
A -> B (a.getB())
class A {
B b;
}
======= A <=> B의 양방향 관계를 위해서는 이렇게 두개의 방향이 필요합니다 =======
B -> A(b.getA())
class B {
A a;
}
테이블의 양방향 연관관계란 하나의 외래 키로 데이터베이스 테이블 간에 서로를 참조하는 관계를 의미합니다. 객체 지향 프로그래밍에서의 양방향 연관관계와 유사한 개념으로, 데이터베이스 테이블 간의 상호 관계를 나타내는 것입니다.
일반적으로 데이터베이스 테이블 간의 연관관계는 외래 키(Foreign Key)를 통해 구현됩니다. 양방향 연관관계에서는 두 테이블이 서로를 참조하므로, 각 테이블이 다른 테이블의 외래 키를 가지고 있게 됩니다.
예를 들어, 팀(Team)과 팀원(Member)라는 두 개의 엔티티가 있다고 가정해보겠습니다. 양방향 연관관계에서는 팀이 팀원을 참조하는 것뿐만 아니라 작성자 역시 팀을 참조할 수 있게 됩니다.
TEAM_ID(FK) 해당 외래 키로 TEAM의 PK값과 조인하면 해당 MEMBER의 소속 팀을 알 수 있고, MEMBER의 PK값과 조인하면 TEAM에 소속되어 있는 MEMBER 데이터를 알 수 있습니다.
둘 중 하나로 외래키를 관리해야 합니다. 객체를 살펴보면 참조가 MEMBER → TEAM / TEAM에서 MEMBERS로 가는 2개의 참조값이 생겼는데,
이 때, 어떤 값과 매핑을 해야하는 지를 정하는 것이 중요한 사항입니다. 데이터의 변경이 필요할 때, (예를 들면 새로운 팀으로 or 회원 가입, 탈퇴 등) 어떤 객체를 참조해서 변경을 해야하는 지가 어렵습니다.
이를 정하기 위해 양방향 매핑 규칙이 존재하는데, 아래와 같습니다.
- 객체 간의 참조 설정
- 각 엔티티 클래스에서 상대방 엔티티를 참조하는 필드를 추가합니다.
- 이 필드는 연관된 엔티티를 참조하는 역할을 합니다.
- @ManyToOne, @OneToMany 어노테이션 사용
- 양방향 연관관계의 주인 쪽에 @ManyToOne 어노테이션을 사용하여 다대일 관계를 매핑합니다.
- 연관관계의 반대편에는 @OneToMany 어노테이션을 사용하여 일대다 관계를 매핑합니다.
- @ManyToOne 어노테이션의 mappedBy 속성에는 연관관계의 주인 엔티티 필드명을 지정합니다.
- Cascade 설정
- 양방향 연관관계에서는 연관된 엔티티의 상태 변경을 한 곳에서 관리할 수 있도록 cascade 옵션을 설정하는 것이 좋습니다.
- cascade 옵션을 사용하면 연관된 엔티티의 저장, 수정, 삭제 등의 변경 작업을 한 곳에서 처리할 수 있습니다.
- Fetch 설정
- 양방향 연관관계에서는 엔티티를 조회할 때 지연 로딩(LAZY)을 사용하는 것이 일반적입니다.
- 필요한 경우 특정 상황에서 즉시 로딩(EAGER)을 사용할 수도 있습니다.
- 무한루프 방지를 위한 equals와 hashCode 오버라이딩
- 양방향 연관관계에서는 객체가 서로를 무한히 참조하는 상황을 방지하기 위해 equals와 hashCode메서드를 적절하게 오버라이딩해야 합니다.
- 연관관계의 주인 설정
- 연관관계의 주인은 외래 키 관리를 담당하는 엔티티 쪽입니다.
- 주인 엔티티의 참조 필드에 @JoinColumn 어노테이션을 사용하여 외래 키를 설정합니다.
- 반대편 엔티티 필드에mappedBy 설정
- 양방향 연관관계에서 주인이 아닌 엔티티 쪽에서는 mappedBy 속성을 사용하여 연관관계의 주인 엔티티 필드를 지정합니다.
- 이를 통해 데이터베이스 테이블 간의 외래 키 관계를 설정합니다.
- 주인 엔티티에 연관관계 설정 및 관리 메서드 추가
- 주인 엔티티에 양방향 연관관계를 설정하고 관리하는 메서드를 추가하여 두 엔티티 간의 연관관계를 효과적으로 관리할 수 있습니다.
✍️ 연관관계에서 주인(Owner)
연관관계에서 주인(Owner)이란, 두 엔티티 중 하나에서 외래 키를 관리하고 데이터베이스 연산을 수행하는 역할을 말합니다. 주인 엔티티에서 외래 키를 설정하고 관리하면, 이에 따라 데이터베이스에 적절한 SQL이 생성되어 관련 데이터가 올바르게 저장되고 관리됩니다.
보통 외래 키가 있는곳 주인 엔티티가 외래 키를 관리함으로써 두 엔티티 간의 관계를 유지하고 일관성을 유지할 수 있습니다. 주인이 아닌 쪽은 읽기만 가능하며 , 주인이 아니라면 mappedBy 속성으로 주인을 지정해야 합니다.
외래 키가 있는 곳이 보통 주인이 되며, 1 : N 중에 N쪽이 연관관계의 주인이 됩니다. @ManyToOne 등의 어노테이션이 작성되어야 합니다. DB 테이블의 N쪽이 연관관계의 주인이 되면 프로젝트 구성 시 원할한 실행이 가능합니다.
예를 들어, 팀(Team) 엔티티와 팀원(Member) 엔티티 간의 양방향 연관관계를 가정해보겠습니다. 이 때 팀원이 팀원을 참조하는 @ManyToOne 관계가 있다면, 이 관계에서 팀 엔티티가 주인 역할을 할 수 있습니다. 따라서 팀 엔티티의 외래 키 설정과 관리를 담당하게 됩니다.
💡 Team과 Member ⇒ @OneToMany (Team 기준)
============== Team 클래스 필드 ===================
@OneToMany(mappedBy = "team") //mappedBy : 일대다 매핑에서 어던 객체와 연결되어 있는지 (반대편 방향을 의미) 알려주는 어노테이션
//team으로 매핑이 되어있음을 의미
private List<Member> members = new ArrayList<>();
============== main 메서드 ===================
//OneToMany 연관 관계 매핑 후 Team 저장 코드
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//OneToMany 연관 관계 매핑 후 Team 저장 코드
Member member = new Member();
member.setUsername("회원 1");
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();
============== 쿼리문 출력 결과=======================
Hibernate:
select
m1_0.MEMBER_ID,
t1_0.TEAM_id,
t1_0.name,
m1_0.USERNAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_id=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
Hibernate:
select
m1_0.TEAM_ID,
m1_0.MEMBER_ID,
m1_0.USERNAME
from
Member m1_0
where
m1_0.TEAM_ID=?
m = 회원 1
👉 em.flush()
영속성 컨텍스트에 쌓여 있는 변경 내용(예: 엔티티의 추가, 수정, 삭제)을 데이터베이스에 즉시 동기화합니다. 이 때, 데이터베이스에 대한 쿼리가 실행되어 변경 내용이 반영됩니다.
flush()를 호출하면 데이터베이스와의 실제 동기화가 이루어지며, 트랜잭션이 커밋되기 전에 실행됩니다.
👉 em.clear();:
영속성 컨텍스트를 초기화합니다. 영속성 컨텍스트에 캐시된 엔티티들은 분리(detached) 상태가 되며, 영속성 컨텍스트의 상태가 초기화됩니다.
이는 영속성 컨텍스트의 메모리 누수를 방지하고, 잠재적으로 영속성 컨텍스트에 쌓인 엔티티들을 비워주는 역할을 합니다. 보통은 트랜잭션이 종료될 때나 큰 작업이 끝날 때 호출하여 사용합니다.
💥 양방향 매핑 시 가장 많이 하는 실수
양방향 매핑 시 무한루프
양방향 연관관계에서 무한 루프가 발생하는 문제는 주로 객체 그래프를 JSON으로 변환하거나 로깅 등에서 발생하는 문제입니다. 이는 객체가 서로를 계속 참조하면서 상호 참조에 의해 무한한 루프가 형성되기 때문입니다.
예를 들어, Parent 엔티티와 Child 엔티티가 양방향으로 연관되어 있다고 가정해보겠습니다. 그리고 양쪽 엔티티에는 서로를 참조하는 필드가 있습니다.
@Entity
public class Parent {
@OneToMany(mappedBy = "parent")
private List<Child> children;
}
@Entity
public class Child {
@ManyToOne
private Parent parent;
}
이 상태에서 Parent 엔티티를 JSON으로 변환하려고 하면, Parent 엔티티 안의 children 필드가 Child 엔티티를 가리키고, Child 엔티티 안의 parent 필드가 다시 Parent 엔티티를 가리키게 되어
서로가 계속 참조하는 상황이 발생합니다. 이렇게 서로가 계속 참조하는 상황에서 JSON 변환을 시도하면 무한한 깊이로 객체가 변환되기 때문에 스택 오버플로우나 성능 문제 등이 발생할 수 있습니다.
이를 해결하기 위해서는 다음과 같은 방법들을 사용할 수 있습니다.
@JsonIgnore 어노테이션 사용: Jackson 라이브러리를 사용해 JSON 변환 시 무한 루프가 발생하는 것을 방지할 수 있습니다. @JsonIgnore 어노테이션을 특정 필드에 사용하여 해당 필드가 JSON 변환에서 제외되도록 설정합니다. 하지만 이 방법은 필드 자체가 무시되므로 정보 손실이 발생할 수 있습니다.
@Entity
public class Parent {
// ...
@JsonIgnore
@OneToMany(mappedBy = "parent")
private List<Child> children;
}
- DTO 사용: 엔티티를 그대로 반환하는 대신, DTO(Data Transfer Object)를 사용하여 필요한 정보만 전달하도록 합니다. DTO를 사용하면 필요한 정보를 선택적으로 전달할 수 있고, 무한 루프 문제도 방지할 수 있습니다.
- @JsonManagedReference와 @JsonBackReference 사용 : 이 어노테이션을 사용하면 순환 참조를 해결할 수 있습니다. @JsonManagedReference는 직렬화 시 참조 엔티티를 처리하고, @JsonBackReference는 역방향 연관 엔티티를 처리합니다.
@Entity
public class Parent {
// ...
@JsonManagedReference
@OneToMany(mappedBy = "parent")
private List<Child> children;
}
@Entity
public class Child {
// ...
@JsonBackReference
@ManyToOne
private Parent parent;
}
'[ BACKEND] > Spring' 카테고리의 다른 글
[SPRING] 객체 참조 및 연관 관계 ✍️ (0) | 2023.08.13 |
---|---|
[SPRING] 다양한 연관관계의 종류 (0) | 2023.08.11 |
[SPRING] 연관 관계 매핑 (단방향) (0) | 2023.08.08 |
[SPRING] Entity Mapping (엔티티 매핑) (0) | 2023.08.07 |
[SPRING] Entity와 영속성 (0) | 2023.08.06 |