JPA의 연관관계 맵핑
- Spring/JPA
- 2021. 11. 25.
연관관계의 필요성
앞선 글에서는 테이블과 객체를 맵핑하는 것들만 따져보았다. 즉, DB의 테이블과 자바의 객체가 연결되는 과정이었다. 그 과정에서 주목해야 할 부분은 내가 맵핑했던 테이블은 다른 테이블과의 연관관계가 없었다. Member라는 테이블이 있었는데, Member는 누구와도 관계를 맺지 않는 독고다이 Table이었다.
- Team, Member가 있다.
- Member는 하나의 Team에만 소속될 수 있다.
- Member와 Team은 다대일 관계다.
그런데 이런 상황이 추가되었다고 가정해보자. 위의 상황을 표로 도식화해보면 아래 그림과 같다.
테이블 관계만 따져보면 MEMBER와 TEAM Table은 TEAM_ID를 FK로 가지고, 그 값으로 서로 연관관계를 가지는 것을 알 수 있다. 반면 아직까지 그림만 봤을 때, Member 객체과 Team 객체는 어떠한 연관관계도 가지지 않은 것을 알 수 있다. 즉, 테이블과 DB가 동기화 되지 않은 상태라고 볼 수 있다.
Table끼리 FK를 가져서 서로 연관관계를 가진 것처럼 객체에도 동일하게 해줘야한다. 어떻게 해야할까? 현재 상태에서 하는 방법은 객체를 테이블에 맞추어 모델링을 한다고 한다. 즉, 객체 간의 연관관계를 지정해주지 않고(참조를 사용하지 않음), 외래 키(FK)를 그대로 사용한다. 실제로 아래 코드를 살펴보면 객체 지향 프로그래밍과는 거리가 멀다는 것을 볼 수 있다. 이는 객체 간의 연관관계가 없기 때문이다.
Team team = new Team();
team.setName("ABCD");
em.persist(team);
Member member = new Member();
member.setUserName("Member1");
member.setTeamId(team.getId());
em.persist(member);
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String userName;
@Column(name = "TEAM_ID")
private Long teamId;
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
코드를 보면 Team 객체를 생성하고 Member 객체를 생성한다. 그리고 Team 객체 생성 + insert를 칠 때 나오는 PK값을 getTeamId() 메서드로 불러와서 그 값을 Member 객체가 가지고 있는 필드에 직접 set하는 형식이 되어있는 것을 볼 수 있다. 실제로 쿼리가 나가는 것과 조회를 하는 것을 확인해보자.
쿼리는 2회가 나가는 것을 확인했다. 그리고 DB에서 직접 조회가 되는지를 한번 확인해본다.
실제로 DB에 각각 저장된 것이 확인되었고, JOIN으로도 확인이 되는지 JOIN 쿼리로 확인을 했다. 결과, 문제 없이 확인되는 것이 보인다. 근데 자꾸 뭐가 문제라고 하는 걸까?
member.setTeamId(team.getTeamId());
가장 큰 문제는 Member 객체가 setTeamId() 메서드로 직접 외래키를 다루고 있다는 것이다. 예를 들어서 Member를 찾아와서 Team을 바꾼다고 해보자. Member를 찾아와서 Team을 바꿀 때 직접 setTeamId()를 하게 되면, Member가 속한 Team은 바뀌는데 그 값이 TEAM Table에 없다고 하더라도 변경이 된다는 점이다. 그리고 DB에서 보게 되면 연결된 것이 제대로 안 보이는 것을 알 수 있다. 이는 객체 간의 연관관계가 없기 때문이다.
테이블에서 찾아온 멤버가 어떤 팀에 속한지를 찾아보는지도 굉장히 객체지향스럽지 않다.
Member member1 = em.find(Member.class, 2L);
Team team1 = em.find(Team.class, member1.getTeamId());
위처럼 Member Class에 PK를 넣어서 Member를 찾아온다. 찾아온 Member에서 getTeamId() 메서드를 이용해서 PK값을 넣어줘서 다시 한번 Team 객체를 찾아와야한다. 객체지향스럽지 않다. 이는 객체간의 연관관계가 설정되어있지 않기 때문이다. 그렇다면 어떻게 해야할까?
객체 연관관계를 사용한 모델링
앞선 예는 객체를 DB 테이블에 맞추어 데이터 중심으로 모델링한 결과다(Long TeamId). 그런데 이 때, JPA 객체에서 테이블의 FK 값을 직접 바꾸는 것을 보면서 객체지향적이지 않은 것을 확인했다. 이런 현상이 나올 수 있는 이유는 아래와 같다.
- 테이블은 외래 키로 Join을 사용해서 연관된 테이블을 찾는다
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
위에서 볼 수 있듯이 테이블과 객체에는 이런 차이가 있다. 따라서 테이블의 Join과 객체의 참조를 동기화 해줘야한다. 이 간격을 메꿔주기 위해서 JPA는 앞으로 테이블의 데이터 지향 모델링이 아닌 객체 지향 모델링을 해야한다. 객체 지향 모델링은 객체 사이의 연관관계를 Join처럼 사용해주는 것이다.
객체 연관관계를 가지게 모델링하기 (다대일 맵핑)
객체끼리 연관관계를 가지게 하려면 한 객체가 다른 객체를 필드 변수로 가지게 하면 된다. 이전에 Member는 Long TeamId라는 필드변수를 가졌었는데, 객체 연관관계를 가지게 하기 위해 내부적으로 Team이라는 객체를 가질 수 있도록 변수를 선언해주었다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String userName;
private Team team;
}
위 내용을 코드로 작성하게 되면 다음과 같이 표현할 수 있다. 그런데 여기서 끝이 아니다. 왜냐하면 참조를 통해 객체 사이의 연관관계는 가지게 되었으나, 객체와 테이블 사이의 연관관계를 만들어주지는 않았기 때문이다. 즉, 객체와 테이블의 Mapping이 필요하다.
객체의 참조와 테이블의 외래키의 맵핑
이제 객체와 테이블의 맵핑이 필요하다. 객체에게는 두 가지를 알려주면 된다.
- 필드 객체가 다른 객체와 어떤 관계인지를 알려준다 (N:1 / 1:N / 1:1 / N : M)
- 필드 객체가 테이블의 어떤 Column가 Join해야하는지를 알려준다 → 다대일은 FK를 연결시켜주면 됨.
위의 테이블에 대해서 Member 클래스와 Member Table의 맵핑을 완료한 코드는 아래와 같다.
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USER_NAME")
private String userName;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
먼저 team 필드 변수가 Member 기준으로 봤을 때, 다대일 관계임을 @ManyToOne 어노테이션을 사용해서 명시해준다. 이 후, @JoinColumn은 Join에 사용되는 컬럼이 무엇인지를 선택하는 어노테이션이다. Member 객체의 team 필드변수가 MEMBER Table의 어떤 Join Column가 매칭될지를 지정해준다. 다대일인 경우, 주로 다대일 테이블의 FK값과 매칭해주면 된다.
다대일 연관관계 설정 완료 후, 코드 실행1
Team team = new Team();
team.setName("독수리");
em.persist(team); // 영속상태가 되면 무조건 PK값이 셋팅되고 영속화됨.
Member member = new Member();
member.setUserName("membera");
member.setTeam(team); // JPA에서 TEAM PK값을 꺼내서, INSERT할 때 FK값으로 사용한다.
em.persist(member);
tx.commit();
위의 코드를 작성했다. 코드의 내용은 Team을 만든 후, 그 Team의 객체를 Member의 필드 변수가 참조하도록 한 후 DB에 저장하는 코드다. 실행 결과를 확인해보겠다.
이 때 내가 혼동한 것은 TEAM의 Join이 필요하기 때문에 실제 JOIN 하는 시점에 DB TABLE의 두 객체에 대한 값이 다 DB에 저장되어있어야 한다는 점이다.
먼저 em.persist(team)에서 영속화 되는데, 이 때 무조건 PK 값이 셋팅되고 영속상태가 된다. 기존에는 member.setTeamId()로 FK 값을 직접 건드렸는데, 이제는 member.setTeam()으로 FK값을 건드리지 않고 객체를 저장하는 식으로 사용한다. 이 때, JPA에 저장된 TEAM에서 PK를 가져와서 JOIN 시에 FK로 사용한다.
실행 결과 INSERT 쿼리가 나갈 때, TEAM 대신에 TEAM_ID로 나가는 것이 확인되었다. DB에 저장된 값을 확인하면, TEAM_ID가 잘 JOIN되어서 저장된 것을 확인할 수 있다. 앞서 이야기 했던 것처럼 MEMBER 객체에 Team 객체를 참조하게 되면, 이 때 Team의 PK 값을 불러와서 Insert Query를 할 때 Join해서 사용할 수 있도록 쿼리가 나간다.
다대일 연관관계 설정 완료 후, 코드 실행2 (객체 그래프 탐색)
현재 Member에서는 Team으로 탐색이 가능하도록 필드 변수를 가지고 있다. 따라서 Member 객체를 하나 가지고 온다면, 이 Member가 속해있는 Team에 대해서도 탐색이 바로 가능하다.
Member findMember = em.find(Member.class, member.getId());
Team findMemberTeam = findMember.getTeam();
System.out.println("findMemberTeam.getName() = " + findMemberTeam.getName());
위의 코드는 member를 하나 찾아온 후, 그 member가 속한 Team이 어떤 Team인지 바로 확인해보는 코드다. 실제 실행 결과는 아래에서 확인할 수 있다.
select 쿼리는 1회 나간 것을 확인할 수 있고, 이 때 Member 인스턴스를 불러오면서 Team 인스턴스까지 Join해서 함께 가져온 것을 확인할 수 있다. 이 덕분에 바로 객체에 접근하는 방식으로 코드를 작성할 수 있었다.
다대일 연관관계 설정 완료 후, 코드 실행3 (연관관계 수정)
Team team1 = new Team();
team1.setName("아아아아아");
em.persist(team1);
Member findMember = em.find(Member.class, member.getId());
findMember.setTeam(team1);
처음에 저장한 Member의 Team이 바뀔 수도 있다. 이 때 연관관계 맵핑을 잘 해두었으면, 새로운 Team을 넣어주면서 이 값을 잘 수정할 수 있다.
실행 결과를 확인해보면 업데이트 쿼리가 나가는 것을 확인할 수 있다. 그리고 기존의 TEAM이 그대로 유지되고, 새 팀이 추가 된 상태에서 MEMBER의 팀이 성공적으로 바뀐 것을 볼 수 있다.
양방향 연관관계 맵핑 (기본적으로는 단뱡항 연관관계 추천)
위 예제를 보면 MEMBER에서 TEAM으로 탐색하는 것은 가능하다. 그렇지만 TEAM에서 MEMBER로 탐색하는 것은 불가능하다. 그 이유는 TEAM 클래스에는 MEMBER 객체가 없지만, MEMBER 객체는 TEAM 객체를 가지기 때문이다. 이 때, TEAM에서 MEMBER 객체를 탐색하기 위해서는 TEAM 객체에 동일하게 MEMBER 객체를 추가해준 후, 어노테이션을 달아주면 된다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "TEAM_NAME")
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public List<Member> getMembers() {
return members;
}
}
Team Class에는 members로 콜렉션을 하나 추가해준다. 이유는 Team과 Member의 관계는 일대다이기 때문이다. Team을 불러왔을 때, 딸려나오는 member가 여러 개가 있을 수 있기 때문이다. 그리고 어노테이션으로는 Member와 Team의 Table 관계가 일대다라는 것을 명시해주고, mappedBy로 Member 클래스의 team과 맵핑되어있는 것을 알려준다.
왜 이렇게 되는 것일까? 이는 객체와 테이블의 차이가 있기 때문이다.
- 테이블은 FK 하나만 있으면 양방향 연관관계가 다 표현이 된다.
- 객체는 상대방의 객체를 가지고 있는지에 따라 단방향 / 양방향 연관관계가 결정된다.
위와 같은 차이가 있기 때문에 객체는 FK 하나로 자유롭게 이동이 되지 않는 것이다. 사실 양방향 연관관계라고 표현을 했지만, 각 객체가 다른 객체를 가지고 있다는 관점에서 본다면 양방향 연관관계라기 보다는 단방향 연관관계가 2개 있는 것으로 이해할 수 있다.
양방향 연관관계?
양방향 연관관계를 살펴보면 Join해서 값을 가져온다는 관점에서 살펴보면 Team team // List<Member> members는 외래키가 2개 있는 것이나 다름이 없다. 따라서 둘 중에 어떤 것을 외래 키처럼 봐야할지 고민이 필요하다. TABLE의 입장에서 본다면 TEAM_ID 외래키 1개로 자유롭게 JOIN이 가능하다.
다시 말해서 DB 입장에서는 TEAM_ID만 어떻게 하면 원하는 목적을 달성할 수 있다.따라서, 현재 양방향 관계에 주어진 2개의 외래키 중 하나만 관리하는 것으로 바꿀 필요가 있다. 2개의 외래 키를 다 사용할 경우, 둘다 값을 수정할 수 있어서 원하지 않는 동작이 일어날 수 있기 때문이다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SELECT *
FROM MEMBER M
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
즉, JPA도 테이블이 FK로 1개를 관리하는 것처럼 양방향 연관관계에서 외래 키를 한 개만 사용할 수 있도록 하는 작업이 필요하다. 이 때, Team team // List<Member> members 중 기본적으로는 일대다 관계에 있는 객체를 mappedBy 어노테이션을 추가해서 조회만 가능하도록 한다. 이런 것들을 연관관계의 주인 설정으로 이해를 할 수 있다.
위의 테이블을 바라보면 Member 객체와 Team 객체가 모두 MEMBER TABLE의 TEAM_ID로 연관관계 맵핑이 된 것을 볼 수 있다. 이 때, mappedBy를 사용해서 Team 객체와 MEBMER TABLE의 연관관계 매핑을 삭제, 정확하게 말해서는 가짜 맵핑된 것으로 만들어줘야한다.
양방향 연관관계 맵핑 규칙
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정 → 외래 키를 1개로만 관리한다.
- 연관관계의 주인만 외래 키를 관리(등록, 수정)
- 연관관계의 주인이 아닌 쪽은 단순 읽기만 가능.
- 연관관계의 주인은 mappedBy 속성을 사용하지 않는다. mappedBy는 내가 연관관계의 주인이 아니고, 쟤가 주인이야라는 의미로 이해할 수 있다.
- 연관관계의 주인이 아니면 mappedBy 속성으로 사용한다.
양방향 연관관계 맵핑을 하면 앞서 말한 것처럼 관리하는 외래 키를 2개에서 1개로 줄여야한다. 실제 DB 테이블도 외래 키를 1개로 관리하기 때문이다. 이렇게 줄이는 방법은 사용하지 않을 곳에 mappedBy 옵션을 선택해주면 된다.
mappedBy를 설정하게 되면 mappedBy가 없는 쪽은 연관관계의 주인이 되고, 있는 쪽은 연관관계의 주인이 아니게 된다. 연관관계의 주인은 저장, 수정 등이 가능하고, 연관관계의 주인이 아니면 저장, 수정은 되지 않고 조회만 된다. 즉, 연관관계의 주인이 가지고 있는 외래 키만 사용해서 관리하겠다는 뜻이 된다.
이게 무슨 말이냐면, List<Member> members에 또 다른 member를 집어넣더라도 JPA는 상관하지 않는다는 뜻이다. 즉, 쿼리가 나가지 않는다. 반대로 연관관계의 주인인 Member 클래스의 Team 필드 변수에 team을 넣으면 JPA는 이걸 분석해서 쿼리를 날린다.
양방향 연관관계 맵핑 코드 실행 : 연관관계의 주인에서 수정
Team team = new Team();
team.setName("teamA");
em.persist(team);
Team team1 = new Team();
team1.setName("teamB");
em.persist(team1);
Member member = new Member();
member.setUserName("memberA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
Team team2 = em.find(Team.class, team1.getId());
findMember.setTeam(team2);
위 코드는 DB에서 Member를 불러오고, 새로운 Team을 불러온다. 그리고 Member의 Team 객체에 새롭게 불러온 Team을 저장해서 실제 Member의 객체가 변경되었을 때 Update 쿼리가 나가는지를 확인한 것이다.
실제 쿼리를 확인해본다. Member와 Team은 당연히 즉시로딩으로 가져오고, 또 다른 Team1이 영속화된다. 이 후, Member에 Team1을 저장하는 코드를 작성했는데, Member의 필드 객체가 바뀌자 update 쿼리가 나가는 것을 확인할 수 있다. 즉, 연관관계의 주인에 수정을 했을 때 update 쿼리가 자동으로 나가는 것을 확인했다.
양방향 연관관계 맵핑 코드 실행 : 연관관계 주인 아닌 쪽에서 수정
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUserName("memberA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId());
Member member1 = new Member();
member1.setUserName("memberQQQ");
List<Member> memberArrayList = new ArrayList<>();
memberArrayList.add(member1);
System.out.println("memberArrayList.get(1) = " + memberArrayList.get(0).toString());
findTeam.setMembers(memberArrayList);
Team을 불러온 다음에 Team의 MemberList를 새롭게 만들어서 members에 넣어주는 코드를 작성했다. 이 코드를 작성했을 때, Update 쿼리가 나가는지를 확인하고자 한다.
코드 실행 결과는 위와 같다. Team 객체를 찾아오는 Select 쿼리가 나간다. Select 쿼리가 나간 후, 새로 만들어진 memberList에도 Member가 있는 것을 확인했다. 그리고 em.persist(Team)을 했음에도 불구하고 Update 쿼리가 나가지 않은 것을 확인할 수 있다. 즉, 연관관계의 주인이 아닌 곳에서는 수정이 일어나도 저장이 되지 않는다는 것을 알 수 있다.
연관관계의 주인은 누가 되어야 하는가?
- 연관관계의 주인은 외래 키가 있는 곳이 주인이 되어야 한다. 즉, 다대일 관계에서 '다'인 테이블이 연관관계의 주인이 되면 된다.
- 아래 그림에서는 Member.team이 연관관계의 주인이 된다.
여기서 쉬운 용어로 좀 더 전환하면, 연관관계의 주인은 진짜 매핑, 연관관계의 주인이 아닌 쪽은 가짜 매핑이라고 정의할 수 있다. 진짜 매핑에서는 값의 수정, 등록이 가능하며 가짜 매핑에서는 값의 조회만 가능하다.
외래 키가 있는 곳이 연관관계의 주인이 되어야 하는 것은 당연하다. Table 간의 연관관계는 PK가 아닌 FK로 JOIN 되기 때문이다. 따라서, 외래 키가 있는 테이블이 연관관계의 주인이 되어야 한다.
양방향 연관관계 사용 시, 자주하는 실수 → 양쪽에 값을 다 넣어라.
양방향 연관관계 사용 시, 자주하는 실수는 연관관계의 주인에 값을 맵핑하지 않는 일이다. 가짜 매핑에만 값을 넣는 경우가 있다. 예를 들어, 아래 코드를 들 수 있다.
Team team = new Team();
team.setName("teamA");
Member member = new Member();
member.setUserName("memberA");
team.getMembers().add(member);
em.persist(member);
위 코드를 확인하면 연관관계의 주인인 Member Class의 Team에 값이 셋팅되어야 하는데, 가짜 맵핑이 된 Members에 member가 추가되는 것을 볼 수 있다.
코드를 실행해보면 위 결과를 받을 수 있다. Member는 DB에 저장되긴 하지만, team_id에는 아무런 값도 없다. 실제 쿼리도 Member에 대한 것만 나갔다. 왜냐하면 가짜 매핑된 Members만 열심히 건드렸기 때문에 실제로 값에는 아무런 영향을 주지 않는 것이다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUserName("memberA");
team.getMembers().add(member);
member.setTeam(team);
em.persist(member);
위는 객체 관점에서 의미를 충분히 살려서 코드를 다시 바꾼 것이다. 순수하게 객체 관점에서 고려해본다면, 양쪽 객체는 다 서로에 대한 참조를 가지고 있는 것이 맞다. 실제 DB에 영향을 주는 것은 연관관계의 주인 뿐이지만, 그것과는 별개로 각 객체가 연결되어 있다는 의미로 코드를 위처럼 짯다.
이번 실행 결과는 MEMBER TABLE의 TEAM_ID가 정상적으로 잘 등록된 것을 볼 수 있다.
양방향 연관관계를 사용한다면 그냥 양쪽 연관관계에 둘다 값을 넣어주는 것이 있다. 데이터베이스 입장에서 봤을 때는 한 군데만 넣어도 동작을 하지만, 객체지향적인 관점에서 본다면 둘다 넣어주는 것이 맞기 때문이다.
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();
Team team1 = em.find(Team.class, team.getId());
List<Member> members = team1.getMembers();
for (Member member1 : members) {
System.out.println(member1);
}
예를 들어 위 코드를 실행하면 정상적으로 동작된다. 이유는 연관관계의 주인에 정상적으로 team 엔티티가 들어가서, DB에 저장이 되었다. 영속성 컨텍스트가 비워졌고, 영속성 컨텍스트가 비워진 후에는 새로 DB에서 값을 조회해서 오기 때문에 이 때 가져오는 Team은 Members를 가지고 있다.
실제로 코드를 실행해보면, 쿼리가 2번 나가는 것을 알 수 있다. 처음에는 TEAM_ID로 TEAM을 한번 가져왔고, 두번째로 Member의 출력이 필요할 때 지연로딩으로 같은 Team_ID를 가지는 Members가 불러와져 iter가 도는 것을 볼 수 있다. 그런데, 이 때 영속성 컨텍스트를 비우지 않는다면 어떤 일이 일어날까?
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
Team team1 = em.find(Team.class, team.getId());
List<Member> members = team1.getMembers();
for (Member member1 : members) {
System.out.println(member1);
}
위 코드를 실행해보자. 위 코드를 실행해보면 iter가 정상적으로 돌지 않는 것을 볼 수 있다.
왜 이런 결과가 나왔을까? 바로 영속화 된 상태에서 Team의 Members에는 객체 관점에서 봤을 때는 아무런 값도 저장이 되지 않았기 때문이다. Flush()가 이루어지지 않은 상태는 객체 상태인데, 객체 상태의 관계로 봤을 때는 영 효과가 없다. DB에 저장되면 그 때서야 사용이 가능하다. 그렇다면 어떻게 해야할까?
객체 상태를 고려해준다면, 영속화 된 상태에서도 정상적으로 기능할 수 있도록 양쪽에 둘다 값을 저장하는 '양방향 편의 메서드'를 만들어줘야한다. 양방향 편의 메서드를 연관관계의 주인 클래스에 작성하는 것이 좋다. 왜냐하면, 연관관계의 주인만 FK 값을 통제한다는 의미를 살리기 위함이다.
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
위와 같이 양방향 연관관계 편의 메서드를 작성할 수 있다. FK값을 셋팅할 때 발생하기 때문에 연관관계의 주인 엔티티의 FK값을 셋팅하는 메서드 안에 양쪽에 값을 추가하도록 설정을 했다. 굳이 Setter에 넣지 않더라도, 내가 만든 메서드에 이런 로직을 넣어도 괜찮다고 한다.
양방향 연관관계 주의 사항
- 순수한 객체 상태에서의 관계를 고려해서 항상 양쪽에 다 값을 설정하자(누락 방지)
- 메서드를 두번씩 하는 것은 불합리하다. 따라서, 연관관계 편의 메서드를 생성해서 사용하자.
- 연관관계 편의 메서드는 양쪽에 다 있으면 무한루프가 발생할 수 있으니, 한쪽에만 작성한다.
- 양방향 맵핑 시에 무한 루프가 발생할 수 있다. 이를 주의해야한다 (toString(), lombok, JSON 생성 라이브러리)
양방향 맵핑 시에 무한루프가 발생할 수 있다. 양방향 맵핑이라는 것은 단방향 맵핑이 2개 있는 것이고, 결국은 양쪽으로 그래프 탐색이 가능하다는 뜻이다. 가장 대표적인 양방향 맵핑의 무한루프는 toString()이 있을 수 있다.
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", homeAddress=" + homeAddress +
", favoriteFoods=" + favoriteFoods +
", addressHistory=" + addressHistory +
// ", team=" + team + // team.toString()을 호출한다.
'}';
}
@Override
public String toString() {
return "Team{" +
"id=" + id +
", name='" + name + '\'' +
// ", members=" + members + // member Collection. 하나하나 member.toString()한다.
'}';
}
위의 코드를 보면 된다. member에서 toSring()을 하면 team.toString()으로 타고 들어간다. team.toString에서는 iter(member.toString())을 타고 들어가게 된다. 계속 무한루프를 돌 수 밖에 없다. 이런 무한루프가 일어날 수 있기 때문에 toString 같은 메서드를 오버라이드 하게 되면, 추가 객체 탐색을 할 수 없도록 해주어야 한다.
양방향 맵핑 정리하기
- 기본적으로 단방향 맵핑만으로도 이미 DB 테이블의 연관관계는 맵핑이 완료되었다.
- 양방향 맵핑은 반대 방향으로 그래프 탐색하는 기능이 추가된 것이다.
- JPQL에서 역방향으로 탐색할 일이 많다.
- 먼저 단방향 맵핑으로 설계한 후, 필요시에 양방향 연관관계 맵핑을 추가하면 된다(테이블에는 영향을 주지 않는다)
연관관계의 주인을 정하는 기준은?
연관관계의 주인을 정하는 기준은 간단하다. DB 테이블 관점에서 바라봤을 때, 외래키가 있는 쪽을 연관관계의 주인으로 설정하면 된다. 비즈니스 로직을 기준으로 연관관계의 주인을 선택하게 되면, 설계적으로 꼬이는 경우가 있을 수도 있다고 한다.
'Spring > JPA' 카테고리의 다른 글
JPA, 다양한 연관관계 맵핑 (0) | 2021.11.27 |
---|---|
다대일 연관 관계, 외래키는 어디에 있어야할까? (0) | 2021.11.27 |
JPA의 프록시 관련 정리 (0) | 2021.11.22 |
영속성 컨텍스트 관련 정리 (0) | 2021.11.20 |
JPA를 활용해 DB에 저장, 조회, 삭제, 수정해보기 (0) | 2021.11.20 |