BACK/JPA

[JPA] JPA 연관관계 매핑

연듀 2024. 3. 31. 13:44

연관관계 매핑

객체의 참조와 테이블의 외래키를 매핑하는게 핵심이다.

 

방향(Direction): 단방향, 양방향
다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)

연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인이 필요

 

단방향 연관관계

 

 

 

회원은 하나의 팀에만 소속될 수 있다.(N : 1)

하나에 팀에 여러 회원이 소속될 수 있다.

 

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeamId(team.getId());
em.persist(member);

Member findMember = em.find(Member.class, member.getId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);

 

외래키인 teamId를 테이블에 맞춰 데이터 중심으로 모델링하면, 위에서 보다시피 객체 지향적으로 개발할 수 없다.

테이블은 외래키로 조인해서 연관된 테이블을 찾고, 객체는 참조로 연관된 객체를 찾는다는 큰 차이가 있다.

 

 

 

 

@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;

    @ManyToOne // (Member : Team = N : 1)
    @JoinColumn(name="TEAM_ID") // join 하는 컬럼. team과 외래키 연관관계 매핑
    private Team team;
...
}
@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.setUsername("member1");
member.setTeam(team); // 단방향 연관관계 설정, 참조 저장
em.persist(member);

// DB에서 select 가져오는 쿼리 보고 싶으면
//em.flush(); // 영속성 컨텍스트에 있는 쿼리를 DB에 날림
//em.clear(); // 영속성 컨텍스트 초기화

Member findMember = em.find(Member.class, member.getId()); // 1차 캐시에서 가져와 쿼리가 안나감
Team findTeam = findMember.getTeam();
System.out.println("findTeam.getName() = " + findTeam.getName());

tx.commit();

 

 

참조를 통해 객체 지향적으로 연관관계를 맺는다.

@ManyToOne : (Member : Team) 다대일 관계

@JoinColumn(name=TEAM_ID): 멤버 테이블의 외래키와 매핑한다.

 

양방향 연관관계와 연관관계의 주인

 

객체 참조와 테이블 외래키의 가장 큰 차이

테이블의 연관관계는 외래키 하나로 양방향을 다 연결 할 수 있다.

그런데 객체에서는 Member에서는 Team을 접근할 수 있는데 그 반대는 안된다.

그래서 Team에 members라는 리스트를 넣어줘야 양쪽으로 갈 수 있다.

 

Team.java에 추가

@OneToMany(mappedBy = "team") // Member의 team 과 매핑이 되어 있다.
private List<Member> members = new ArrayList<>();

 

양방향 매핑(반대방향으로도 객체 그래프 탐색 가능)

Member findMember = em.find(Member.class, member.getId()); 
List<Member> members = findMember.getTeam().getMembers();

for (Member member1 : members) {
    System.out.println("member1 = " + member1.getUsername());
}

 

객체는 서로 다른 단방향 연관관계가 두개가 있어 양방향 연관관계가 되는 것이다.

테이블은 외래키 하나로 양쪽의 연관관계를 가지게 된다. (양쪽으로 조인할 수 있다.)

 

객체에서는 멤버 → 팀 (team) 참조 값 / 팀 → 멤버 참조 (members) 값 이렇게 두개가 있다.

둘 중 어떤 값을 업데이트 했을 때 외래키가 업데이트 되어야 하는지 고민이 생긴다.

예를 들면 멤버를 바꾸고 싶거나 새로운 팀에 들어가고 싶을 때, 멤버의 팀 값을 바꿀지 팀의 members를 바꿀지 고민하는 것이다.

사실 DB입장에서는 member 테이블에 있는 team_id인 외래키 값만 업데이트 되기만 하면 된다.

결론은 둘 중 하나로 주인을 정해 외래 키를 관리해야 한다.

 

 

연관관계의 주인

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해 주인만이 외래키를 관리(등록, 수정)
  • 주인이 아닌 쪽은 읽기(조회)만 가능
  • 주인이 아니면 mappedBy 속성으로 주인을 지정해주어야 함

외래 키가 있는 곳을 주인으로 정한다.

Member에 team_id라는 외래키가 있으므로, Member.team가 주인이 되고 team은 mappedBy 속성을 준다.

DB 입장에서 보면 외래키가 있는 것이 무조건 다(N)다.

따라서 DB의 N쪽이 무조건 연관관계의 주인이 된다.

이렇게 해야 외래키가 있는 곳에서 관리를 할 수 있고, 성능적인 부분에서도 좋다.

 

주의점

양방향 매핑 시 가장 많이 하는 실수 - 연관관계의 주인에 값을 입력하지 않는 실수

현재 member에 있는 team 이 연관관계 주인이다.

Member member = new Member();
member.setUsername("member1");
em.persist(member);

Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member); // 주인이 아닌 방향만 연관관계 설정
em.persist(team);

em.flush(); 
em.clear(); 

tx.commit();

 

그래서 이렇게 했을 때 team의 members에 member를 넣었기 때문에

DB를 보면 Member 테이블의 team_id가 null 인걸 볼 수 있다.

왜냐면 members는 주인이 아니라서 읽기 전용이기 때문이다.

따라서 member.setTeam(team)을 해줘야 한다.

 

 

-> 양방향 매핑 시 연관관계의 주인에 값을 입력해야 한다. 

 

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team); // 연관관계 주인에 값을 넣음
em.persist(member);

 

사실 순수한 객체 관계를 고려하면 양방향 연관관계 시 항상 양쪽 다 값을 입력해야 한다. 

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team); // 연관관계 주인에 값을 넣음
em.persist(member);

// DB에서 select 가져오는 쿼리 보고 싶으면
// em.flush();
// em.clear();
team.getMembers().add(member);
            
Team findTeam = em.find(Team.class, team.getId()); 
List<Member> members = findTeam.getMembers(); //  
System.out.println("==================");
for (Member m : members) {
   System.out.println("m.getUsername() = " + m.getUsername());
}
System.out.println("==================");

 

team.getMembers().add(member);

를 안해줬을 시에는 점선 사이에 아무것도 찍히지 않는데(Team을 1차 캐시에서 가져오므로 members의 값이 없다)

team.getMembers().add(member); 를 추가했을 경우 members를 가져온다.

 

순수하게 1차 캐시에만 넣은 상태면 team의 members를 추가하는 세팅이 없으면 JPA가 못 읽어드리는 것이다.

 

이런 예시의 이유들 때문에 양쪽 다 값을 세팅해 주는 것이 좋다.

이를 위해 연관관계 편의 메소드를 생성하는 것이 좋다.

 

team.getMembers().add(member);

를 지우고, Member에 이 메소드를 추가해보자. 

 

public void setTeam(Team team) { // team을 세팅하는 시점에 팀에도 members를 세팅
    this.team = team;
    team.getMembers().add(this);
}

 

이 때 로직이 들어가는 setter는 따로 이름을 바꿔주도록 하자.

ex) setTeam → changeTeam

 

연관관계 편의 메소드는 1이나 다쪽 둘다 넣어도 되는데, 한 군데만 넣는 것이 좋다.

그리고 양방향 매핑 시에 무한 루프를 조심하자.

 

무한루프 ex) toString(), lombok, JSON 생성 라이브러리

→ lombok에서 toString을 만드는 것을 웬만하면 쓰지 말자

→ 컨트롤러에서는 엔티티를 반환하지 말자. (엔티티를 Json으로 반환해버릴 때 무한루프 발생 가능)

 

 


 

 

정리

  • 단방향 매핑만으로도 연관관계 매핑은 완료된 것이고, 양방향 매핑은 여기서 반대 방향으로 조회하는 기능이 추가된 것 뿐이다.
  • 단방향 매핑을 잘 하고 양방향 매핑은 필요할 때 추가해도 된다. (테이블에 영향을 주지 않음)
  • 연관관계 주인은 외래 키를 가지고 있는 곳으로 정한다.

 

 

 

인프런 자바 ORM 표준 JPA 프로그래밍 - 기본편을 수강하고 정리한 글입니다.

https://www.inflearn.com/course/ORM-JPA-Basic