- [JPA] 5. 연관관계 매핑 & 주인2025년 02월 21일 19시 25분 16초에 업로드 된 글입니다.작성자: nickhealthy
연관관계가 필요한 이유
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.
- 조영호(객체지향의 사실과 오해 저자)연관관계의 필요성을 알아보기 위해 예제 시나리오를 가져왔다.
다음은 객체와 데이터베이스 테이블 간의 관계를 도식화 한 그림이다.[예제 시나리오 - 참조 대신에 외래키를 필드로 그대로 사용하는 경우]
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
객체와 데이터베이스 테이블 간의 관계 도식화 위 그림은 객체를 테이블에 맞추어 모델링했다. 따라서 `Member` 클래스의 필드 `teamId`가 `MEMBER` 테이블의 외래키 역할을 하게 된다. 그림에도 표현되어 있듯이 객체는 사실상 어떤 연관관계도 가지고 있지 않은 상태다. 데이터를 저장할 수 있는 별개의 필드로만 정의되어 있을 뿐이다.
이를 코드로 표현하자면 다음과 같다.
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; @Column(name = "TEAM_ID") private Long teamId; ... } @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; ... }
이제 이 클래스를 사용할 때 다음과 같은 문제가 발생하게 된다.
... // 팀 저장 Team team = new Team(); team.setName("TeamA"); em.persist(team); // 회원 저장 Member member = new Member(); member.setName("member1"); member.setTeamId(team.getId()); em.persist(member); ...
회원 저장에서 `member.setTeamId(team.getId());` 코드가 문제가 되는데 객체의 연관관계 없이 필드로서(Member.TeamId) 외래키 식별자를 직접 다루다보니 객체지향적인 코드가 나오지 않는다. member는 팀에 속해 있다는 코드로 표현하기 위해서는, 그리고 조금 더 객체지향적인 코드로 만드려면 `member.setTeam(team.getId())`가 조금 더 자연스럽다. 하지만 외래키를 직접 객체에서 다루기 때문에 위와 같은 코드를 작성하게 된다.
참고
`member.setTeamId(team.getId());` 코드에서 바로 `team.getId()`를 사용할 수 있는 이유는 JPA에서 `em.persist(team)`를 호출하게 되면 영속성 컨텍스트에서 ID 값이 자동으로 세팅되기 때문에(DB에 커밋되지 않은 상태임에도) ID 값을 불러올 수 있게 된다.조회할 때 또 다른 문제가 발생하게 된다.
Member findMember = em.find(Member.class, member.getId()); // 특정 멤버의 소속된 팀을 조회하기 위해서 위에서 멤버를 조회한 후 Getter를 통해 TeamId를 한번 더 조회해야 한다. Long findTeamId = findMember.getTeamId(); Team findTeam = em.find(Tema.class, findTeamId);
특정 멤버의 소속된 팀을 조회하기 위해 두번 조회하는 코드가 존재하게 된다. 외래키를 직접 다루고, 객체 간의 연관관계가 없기 때문에 이러한 문제가 발생하게 된다. 이는 객체지향적이지 못하다. 따라서 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
다음과 같은 방법을 통해 모델링을 하는 것이 올바르다.
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
단방향 연관관계
[예제 시나리오 - 객체 지향 모델링, 객체 연관관계 사용]
이번에는 객체지향적인 코드를 작성하기 위해 객체 지향 모델링을 사용해보자.
현재 Member 클래스와 Team 클래스 관계는 N..1 관계이다.
이를 코드로 나타내면 다음과 같다.
// JPA가 관리할 객체 @Entity(name = "MEMBER2") public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; // /** // * 객체를 테이블에 맞춰 모델링 // * - 참조 대신에 외래 키를 그대로 사용 - 객체지향적이지 못함 // */ // @Column(name = "TEAM_ID") // private Long teamId; @ManyToOne // Member와 Team은 N:1 관계 @JoinColumn(name = "TEAM_ID") // Team 엔티티의 TEAM_ID 컬럼과 외래키 관계 private Team team;
`@Entity` 객체에서 어노테이션은 DB와의 관계를 나타내는데 사용하는 어노테이션이다.
`@ManyToOne` 어노테이션이 Team과의 관계에서 수에 관련된 매핑 정보이다. Member 객체는 N이기 때문에 해당 어노테이션을 사용하게 된다.
`@JoinColumn` 어노테이션은 Team과 연관관계를 나타내는 매핑 정보이다.
이제 클래스를 사용할 때 외래키를 직접 다룰 때보다 코드가 훨씬 자연스러워진 것을 확인할 수 있다.
// 객체 지향적인 모델링으로 수정 1 Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setUsername("member1"); member.setTeam(team); // 객체 지향적으로 모델링 수정 em.persist(member); // 객체 지향적인 모델링으로 수정 2 Member findMember = em.find(Member.class, member.getId()); Team findTeam = findMember.getTeam(); // 객체 지향적으로 모델링 수정 System.out.println("findTeam = " + findTeam.getName()); // findTeam = TeamA
- `member.setTeam(team)`으로 코드를 수정했다. 이제 member는 현재 어떤 팀에 소속될 것이라는 것을 표현할 수 있다.
- 특정 멤버의 소속된 팀을 조회했을 때 특정 멤버를 조회한 후 TeamId를 Getter로 조회한 후에야 팀을 조회할 수 있었던 것에 반해, 바뀐 코드는 Member만 조회한 후 해당 멤버가 어떤 팀에 소속되어 있는지 바로 조회할 수 있게 되었다.
양방향 연관관계와 연관관계의 주인
위의 예제에서 멤버에서 팀을 조회하는 것은 가능하다. 하지만 팀에서 멤버를 조회하는 것은 당연히 불가능하다. 왜냐하면 멤버에서만 Team 객체를 참조로 가지고 있기 때문이다. 사실 둘은 관계가 깊기 때문에 Team에서도 조회가 가능하도록 만들어야 할 수도 있다.
다음은 객체와 데이터베이스 테이블 간의 관계를 도식화 한 그림이다.
객체와 데이터베이스 간의 매핑 패러다임 차이
조금 더 자세히 알아보기 전에 데이터베이스와 객체 간의 매핑 패러다임 차이에 대해서 이해하고 갈 필요가 있다.
- 데이터베이스는 객체와 다르게 외래키 하나로 두 테이블의 연관관계를 관리한다.
- 데이터베이스는 사실 한쪽에 외래키만 지정해주면 방향에 대한 개념 없이 조인을 사용해서 양쪽으로 매핑된다.
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
- 객체는 양방향 매핑을 하려면 각 객체마다 참조가 있어야 한다.
- 객체는 양방향 매핑을 설정해주기 위해 객체마다 각각 매핑을 해주어야 한다.
- 즉, 객체에서는 단방향 연관관계를 2개를 만들어야 양방향 매핑이 된다.
테이블과 객체 사이에는 이런 큰 패러다임의 차이가 있다.
양방향 연관관계 매핑
이제 직접 코드를 통해 양방향 연관관계를 사용해보자.
앞의 에제에서 멤버는 팀과 단방향 매핑을 했었다. 이제 팀에서 멤버로 단방향 매핑을 지정해주면 양방향 매핑이 성립된다.
@Entity public class Team { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") // 양방향 연관관계 설정, mappedBy 속성으로 주인을 지정 private List<Member> members = new ArrayList<>(); ... }
이제 매핑을 완료했으니 양방향 매핑이 잘 작동하는지 확인해보자.
`mappedBy`는 조금 이따가 설명한다.
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()); Team findTeam = findMember.getTeam(); // team에서 member를 직접 조회 List<Member> members = team.getMembers(); for (Member m : members) { System.out.println("m.getUsername() = " + m.getUsername()); // m.getUsername() = member1 }
예상대로 팀에서도 멤버를 직접 조회할 수 있게 되었다. 이로써 양방향 매핑이 완성되었다.
연관관계의 주인
하지만 위에서 설명한 '객체와 데이터베이스 간의 매핑 패러다임 차이' 때문에 애매모호한 상황이 발생하게 된다.
어떤 객체를 통해서 외래키를 관리할지에 대한 문제가 발생하게 된다. 왜냐하면 객체 양쪽 모두에서 단방향 매핑 관계를 가지고 있으므로 양방향 매핑 관계는 성립되었지만, JPA 입장에서는 어떤 객체에 변경 상황이 생겼을 때 데이터베이스를 업데이트 해야 하는지 모른다.
또 반대로 데이터베이스 입장에서는 어떤 객체에서 업데이트를 하던 외래키만 업데이트가 잘 되면 어떤 객체가 외래키를 업데이트 하던 아무 상관이 없다. 따라서 JPA에서는 연관관계의 주인(객체)을 정해 하나의 엔티티에서만 업데이트를 지원해야 한다.
JPA 입장에서 애매모호한 연관관계 JPA 입장에서는 이런 애매모호한 연관관계를 해결하기 위해 연관관계의 주인을 지정한다. 특정 엔티티 필드에 연관관계 주인을 지정하면 해당 객체가 업데이트 될 때만 JPA에서 데이터베이스를 업데이트하게 된다. 반대로 말하면 연관관계의 주인으로 지정되지 않은 또 다른 엔티티에서는 아무리 업데이트를 진행해도 데이터베이스에 업데이트 되지 않는다.
따라서 JPA 양방향 매핑에서는 다음과 같은 규칙이 있다.
[양방향 매핑 규칙]
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래키를 관리(등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능
- 주인이 아니면 `mappedBy` 속성으로 주인을 지정
그렇다면 누구를 주인으로 지정할까?
JPA를 많이 사용해 온 유저들의 의해 아래처럼 가이드가 정해져 있다고 생각하면 된다.
- 연관관계의 주인은 데이터베이스 외래키를 관리하는 쪽으로 설정한다. 예를 들어, Member 테이블과 Team 테이블이 있을 때 외래키를 가지고 있는 쪽은 Member 테이블이다. 즉, Member 객체의 필드에서 연관관계의 주인을 지정한다.
- N..1 관계에서 외래키를 가지고 있는 쪽이 N개를 가지게 된다. 양방향 매핑 관계에서 N개를 가지고 있는 쪽을 주인으로 지정한다. 즉, `@ManyToOne`이 주인 관계를 가진다.
- 연관관계의 주인을 지정하려면 `mappedBy` 속성을 사용한다.
다시 연관관계의 주인을 지정한 후에 관계를 표현하면 다음과 같다.
연관관계의 주인을 지정한 후 관계의 모습 이제 Member 엔티티에서 업데이트 할 때만 데이터베이스에 반영되게 된다. 반대로 말하면 Team 엔티티에서 업데이트가 발생해도 데이터베이스에 반영되지 않는다. Team 엔티티는 Member와 관계된 데이터는 읽기만 가능하다.
다음글이 없습니다.이전글이 없습니다.댓글