JPA

연관관계 매핑, 단방향, 양방향

salmon16 2023. 8. 28. 00:24

연관관계가 필요한 이유

예제 시나리오를 통해 알아보자 

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에 소속될 수 있다.
  • 회원과 팀은 다대일 관계이다.

만약 연관관계가 없이 객체를 테이블에 맞추어 모델링을 하면 아래와 같이 된다.

이렇게 설계를 하게 되면 Member객체에 teamId 필드를 통해 Team을 참조해야 한다.

만약 멤버의 팀을 조회하려면 아래와 같은 코드를 수행해야 한다.

//조회
 Member findMember = em.find(Member.class, member.getId()); 
 Team findTeam = em.find(Team.class, findMember.getTeamId());

이런 코드는 객체 지향적인 방법이 아니다.

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

 

위의 예시를 연관관계를 사용해 보자

 

단방향 연관관계

위 예시를 수정하여 단방향 연관관계를 만들어 보자 Member의 필드에 Team team이라는 필드를 추가하였다.

이제 객체의 참조와 테이블의 외래키를 매핑해주어야 한다.

 @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String name;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

조인할 칼럼을 명시해야 한다 @JoinColumn으로 FK 칼럼의 이름을 name 속성을 통해 지정해 준다.

FK가 어디랑 매핑되어 있는지는 private Team team을 보고 Team이 Entity가 붙은 JPA가 관리해 주는 코드이므로 team과 FK키가 매핑되어 있다고 SQL을 보낸다.

연관관계를 @ManytoOne 애노테이션을 사용하여 지정하자 

Member입장에서 Member N : 1 Team 이므로 ManyToOne으로 설정해 준다.

 

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

Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);

Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();

이제 위 코드가 처럼 객체지향스럽게 팀을 조회를 할 수 있다.

 

위 코드처럼 코드를 작성하면 find 메서드가 실행될 때 member는 영속성 컨텍스트에서 찾아온다 그러므로 select SQL을 보내지 않는데 만약 DB에서 찾아오고 싶으면 find 메서드 전에 em.flush(), em.clear()를 수행해 주면 된다.

양방향 매핑

양방향 매핑에 대 해 알아보자 만약 member에서 team을 조회하는 기능뿐 아니라 team에서 member들의 list를 얻고 싶을 때 양방향으로 매핑을 해야 한다.

 

양방향 매핑에 대해서 알아보자.

DB와 객체 간의 연관관계를 찾는 방법은 다르다

  • DB = 외래 키 하나를 이용한 join
  • 객체는 참조한다 (member.getTeam, Team.getMember)

DB의 테이블에서 연관관계는 단방향이나 양방향이나 동일하다

member에서 Team을 알고 싶으면 TEAM_ID로 JOIN을 

Team에서 member을 알고 싶으면 Team의 PK로 JOIN을 하면 된다.

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

객체에서의 연관관계를 위해서는 Team에 member를 저장하는 List가 필요하다 

객체에서는 양방향을 하기 위해 member에서도 Team을 Team에서도 member을 가지고 있어야 한다.

 

Member Entity는 단방향과 동일하다.

Team Entity를 수정하자

 

@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team") // team 은 Member의 변수 명
    private List<Member> members = new ArrayList<>(); // add 할때 null 안뜨게 하려고

List라는 컬렉션을 추가해 준다 초기에 null이 뜨는 것을 방지하기 위해 new ArrayList를 할당해 준다.

Team 입장에서는 Team 1 : N member 이므로 @oneToMany 애노테이션을 사용해 준다.

그리고 mappedBy로 Member의 team에 의해 매핑되어 있다고 알려주어야 한다.

 

mappedBy

mappedBy에 대해 알아보자

 

객체의 양방향 관계

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다

객체를 양방향으로 참조하려면 단방향 연관관계를 2개를 만들어야 한다.

  • member -> Team(member.getTeam)
  • Team -> member(Team.getMember)

테이블의 양방향 관계

하지만 테이블에서는 사실 양방향 관계라는 것이 없다.

모두 양방향이다.(외래 키 하나로 두 테이블을 연관관계를 관리하고 조인할 수 있다.) 

그래서 member의 Team, Team의 memberList 둘 중 하나로 외래키를 관리해야 한다. 

(둘이 다를 때 기준을 잡기 위해서)

 

DB입장에서 Member도 Team을 가지고, Team도 Member를 가지면 이제 둘 중에 뭘로 연관관계를 매핑해야 하는지 선택해 주어야 한다.

DB에서는 객체가 참조를 어떻게 하든 외래 키값(Member의 TEAM_ID)만 업데이트하면 된다.
양방향 연관관계에서의 외래 키 관리 주인의 기준을 명확하게 하기 위해 연관관계의 주인이라는 개념이 나왔다.


연관관계의 주인(Owner)
양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
  • 연관관계의 주인만(진짜 매핑)이 외래 키를 관리한다.
  • 주인만이 DB에 접근하여 값을 등록, 수정, 변경할 수 있다.
  • 주인이 아닌 객체가 값을 변경하더라도 DB에는 영향이 없다.
  • 주인이 아닌 쪽(가짜 매핑)은 읽기(SELECT)만 가능하다.
  • 주인은 mappedBy 속성을 사용하지 않는다.

누구를 주인으로 해야 할까?

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

DB에는 N에 해당하는 테이블이 FK를 가지고 있다.

주인은 사실 비즈니스 적으로 중요하지 않다. 이렇게 해야 깔끔하게 된다.

 

만약 위의 예시에서 Team을 주인으로 설정한 후  Team의 List를 변경하면 update SQL은 MEMBER 테이블로 날아가게 된다. 즉 다른 테이블에 update SQL이 날아가므로 혼란을 야기할 수 있다.

 

 

연관관계의 주인

양방향 매핑의 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리한다.
  • 주인 아닌 쪽에서는 읽기만 한다.
  • 주인은 mappedBy 속성을 사용하지 않는다. 
  • 주인이 아니면 mappedBy 속성으로 주인을 지정한다.

양방향 매핑 시 가장 많이 하는 실수 

1. 연관관계의 주인에 값을 입력하지 않음

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

이렇게 주인인 member에 setTeam을 하지 않으면 DB member 테이블의 TEAM_ID에는 null이 들어가게 된다.

주인에게만 값을 입력해도 되지만 순수한 객체 관계를 고려하면 양쪽에 다 값을 입력하는 것이 좋다.

 

그리고 연관관계 편의 메소드를 생성해서 사용하자

member의 Team을 바꾸는 예시를 생각해 보자 메소드의 이름은 기본 setter과 다르게 지정해야 한다.

연관관계 편의 메소드를 사용하면 반복되는 코드를 줄일 수 있다.

Member의 메소드

public changeTeam(Team team) {
        
        // Member에 이미 Team이 설정되어 있을 경우
        if(this.team != null) {         
            // team에서 해당 Entity를 제거
            this.team.getMembers().remove(this);
        }        
        // 새로운 팀 설정
        this.team = team;
        
        // 새로운 팀에 멤버 추가
        team.getMembers().add(this);
    }

Team의 메소드

public void addMember(Member memeber) {
	member.setTeam(this)
    members.add(member)
}

양쪽에 둘 다 연관관계 메소드가 있으면 문제(무한 루프 등)가 발생할 수 있기 때문에 한쪽에만 생성해야 한다.

애플리케이션 상황을 봐서 어느 쪽에 생성할지 선택하면 된다.

 

2. 무한 루프를 조심하자

 

toString(), lombok, JSON생성 라이브러리 사용 시 무한 루프를 조심해야 한다

 

예를 들어 member의 lombok toString() 메소드를 사용 시 Team을 출력하게 되는데 이러면 Team의 toString() 메소드가 호출된다. Team의 toString() 메소드 역시 member을 출력해야 하므로 서로가 서로를 계속 호출하는 무한루프에 빠질 수 있다.

 

컨트롤러에서 response에 Entity를 직접 보내게 된다면 member에 Team이 있어 호출하고 또 Team에 members 가 있으므로 member을 호출하게 되므로 무한 루프에 빠질 수 있다.

그러므로 컨트롤러에서 Entity를 반환하지 말고 단순하게 값만 있는 DTO로 변환해서 반환하는 것을 추천한다.

 

단방향 매핑 정리

  • 단방향 매핑만으로 이미 연관관계는 매핑은 완료이다
  • 양방향 매핑은 반대 방향으로 조회 기능이 추가된 것뿐이다.
  • JPQL에서 역방향 탐색할 일이 많다
  • 단방향 매핑으로만 하고 나중에 필요할 시 양방향을 추가하여도 Table이 변경되는 것이 아니기 때문에 충분하다.

 

'JPA' 카테고리의 다른 글

상속관계 매핑  (0) 2023.08.29
다양한 연관관계 매핑  (0) 2023.08.29
엔티티 매핑, 기본 키 매핑  (0) 2023.08.27
JPA 설정 하기  (0) 2023.08.26
영속성 컨텍스트  (0) 2023.08.26