JPA에서 연관관계 매핑은 총 4가지가 존재합니다.
- @OneToOne
- @ManyToOne
- @OneToMany
- @ManyToMany
이때 방향이 단방향인지, 양방향인지에 따라 또 나뉘게 됩니다. 예를 들어 팀과 멤버가 있다고 가정해 봅시다.
간단하게 생각해 보면 멤버만 팀을 알고 있다면 단방향, 팀 또한 멤버에 대해 알고있다면 양방향이 됩니다. 양방향 연관관계를 코드로 한번 확인해 보겠습니다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@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")
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
위처럼 Member와 Team이 작성되어 있다면, 두 객체에서 모두 연관관계가 맺어진 상대방을 조회할 수 있습니다. 하지만 DB를 생각해 보면 어떨까요? 어떤 테이블이 외래키를 가지고 있을까요?
객체와 DB의 연관관계는 그림과 같은 차이를 가지게 됩니다.
객체의 양방향 관계는 사실 양방향이라기보다는 다른 방향의 단방향 2개라고 볼 수 있습니다. 단방향 2개가 교차하므로 양방향이 되는 것입니다.
일대다 연관관계는 DB에서 외래키 1개로 두 테이블의 연관관계를 관리합니다. MEMBER가 TEAM_ID 외래키를 가지고 있으면서 양방향 연관관계를 가질 수 있습니다. 그래서 우리는 DB에서 다음 쿼리를 통해 양쪽으로 조인해서 조회할 수 있습니다.
SELECT * FROM MEMBER
JOIN TEAM ON MEMBER.TEAM_ID = TEAM.TEAM_ID
SELECT * FROM TEAM
JOIN MEMBER ON TEAM.TEAM_ID = MEMBER.TEAM_ID
우리는 일대다 관계에서 외래키를 다 쪽에서 관리합니다. 이에 따라 JPA에서도 양방향 연관관계 시, 연관관계의 주인을 지정해야 합니다.
양방향 매핑에서는 다음 규칙이 존재합니다.
- 객체의 두 관계 중 하나를 연관관계 주인으로 지정
- 연관관계의 주인이 외래키 관리
- 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 사용 X
- 주인이 아니면 mappedBy 속성으로 주인 지정
결국, 외래키가 있는 쪽을 연관관계의 주인으로 지정해야 합니다. 위 예시에서는 Member의 Team이 연관관계의 주인이 됩니다.
이에 따라 실수하는 경우가 생기기도 합니다. 양방향 매핑 시, 연관관계의 주인에 값을 넣지 않은 경우 의도치 않은 결과가 일어납니다. 위 예시와 같은 Member와 Team 상황에서 다음 코드를 실행시켰다고 가정해 봅시다.
Team team = new Team("철수팀");
em.persist(team);
Member member = new Member("철수");
team.getMembers().add(member);
em.persist(member);
얼핏 보기에 Team을 생성한 후 저장하고, Member를 생성한 후 Team에 Member를 넣고 그 결과를 저장하는 것처럼 보입니다. 하지만 실제 결과는 철수의 TEAM_ID는 null입니다.
위에 언급한 규칙을 보면 연관관계의 주인에 값을 넣어야 함을 알 수 있습니다. 즉 다음과 같이 작성해야 합니다.
Team team = new Team("철수팀");
em.persist(team);
Member member = new Member("철수");
// 순수한 객체 관계를 고려하여 양쪽 다 값을 입력
team.getMembers().add(member);
member.setTeam(team);
em.persist(member);
위 코드를 실행하게 되면 정상적으로 철수의 TEAM_ID에 값이 입력됩니다. 물론 member에 team을 설정하면 DB에 반영되지만, 객체 상태를 고려하여 양쪽 다 값을 설정하는 것이 좋습니다.
이를 위해서 연관관계 편의 메서드를 작성할 수도 있습니다.
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
위 메서드를 이용하여 서비스 내에서 하나의 메서드로 처리할 수도 있습니다.
사실 양방향 매핑은 단방향 매핑에서 반대 방향으로 조회 기능이 추가된 것입니다. 이때 연관관계의 주인은 비즈니스 로직이 기준이 아니라, 외래키의 위치를 기준으로 설정해야 합니다.
'스프링' 카테고리의 다른 글
알림을 비동기 방식으로 보내기 (0) | 2025.02.25 |
---|---|
@Modifying의 동작 알아보기 (0) | 2025.01.23 |
Mybatis vs JPA (0) | 2025.01.08 |
SELECT 작업에 트랜잭션은 필요할까? (0) | 2025.01.08 |
스프링 트랜잭션 (0) | 2025.01.07 |