JPA 연관관계 매핑

zwon·2023년 10월 3일
0

JPA

목록 보기
5/9

객체과 테이블 연관관계 차이를 이해를 해야한다.
객체는 참조를 통해 접근한다면 테이블은 FK 외래키를 통해 연관관계를 가진다.
그래서 JPA 연관관계 매핑 글에서는 객체의 참조와 테이블의 외래키를 어떻게 매칭해야하는 것인가에 대해서 정리할 생각이다.

예제 시나리오

  • 회원과 크루가 있다.
  • 회원은 하나의 크루에만 소속될 수 있다.
  • 회원과 크루는 N:1 관계이다.

연관관계 매핑

  • 먼저 객체를 테이블에 맞게 모델링한 코드로 참조 대신 crewId라는 FK를 가지고 있다.
@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @Column(name="crew_Id")
    private Long crewId;
    ...
}
@Entity
public class Crew {
	@Id @GeneratedValue
    private Long id; //crewId
    
    private String name;    
    ...
}
  • 이러한 모델링은 약간 객체지향스럽지 않다
Crew crew = new Crew("crew1");
em.persist(crew);

Member member = new Member("member1");
member.setCrewId(crew.getId());  // 이 부분
em.persist(member);
  • 그리고 크루를 바로 못가져오는 등 조회할 때 번거로움이 있다.
Member findMember = em.find(Member.class, member.getId());
Long crewId = findMember.getCrewId();
Crew findCrew = em.find(Crew.class, crewId);

그래서 이처럼 객체를 테이블에 맞추어 모델링을 하면 객체 간의 협력 관계를 만들 수 없다.
객체는 참조를 통해서 연관된 객체를 찾고, 테이블은 FK를 사용해서 연관된 테이블을 찾는다는 큰 간격이 있다.
우선 이러한 차이가 있다는 것을 알고있자.

이제 객체지향스럽게 모델링을 해보자.


단방향 연관관계

@Entity
public class Crew {
	@Id @GeneratedValue
    private Long id; //crewId
    
    private String name;    
    ...
}
  • 아래처럼 private Crew crew;로 작성하면 JPA한테 Member와 Crew가 무슨 관계인지 알려줘야한다. ex) 1:N인지 N:1 인지 등
  • 그리고 FK랑도 매핑을 해야해서 @JoinColumn을 써야한다.
@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne // Member 입장에서 Crew를 보면 Member:Crew=N:1
    @JoinColumn(name="crew_id") // 이 연관관계에서 join할 컬럼은 뭐야?를 의미함.
    private Crew crew;
    ...
}
  • 그러면 다음과 같이 객체지향스럽게 코드를 작성할 수 있다.
Crew crew = new Crew("crew1");
em.persist(crew);

Member member = new Member("member1");
member.setCrew(crew);  // 이 부분
em.persist(member);
  • 조회할 때도 마찬가지로 객체지향스럽게 조회할 수 있다.
Member findMember = em.find(Member.class, member.getId());
Crew findCrew = findMember.getCrew(); // 이 부분

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

  • 위 예시를 단방향이 아닌 양방향 관계라고 가정하자.
  • 양뱡향이라는 것은 member -> crew로 갈 수 있고 crew -> member로 갈 수 있는 것이다.
  • 객체지향에서는 Crew에는 Member가 Member에는 Crew필드가 있어야 양방향 연관관계가 가능하다. 하지만 테이블은 외래키 하나만으로 양방향 연관관계가 가능하다.
  • 테이블은 Member가 가지고 있는 crew_id를 가지고 Crew테이블과 Join을 하면 크루에 속한 멤버 또는 멤버가 속한 크루에 대해서 알 수 있다.
  • 객체와 테이블의 이러한 차이 역시 알고 있어야한다.
@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne 
    @JoinColumn(name="crew_id")
    private Crew crew;
    ...
}
@Entity
public class Crew {
	@Id @GeneratedValue
    private Long id; //crewId
    
    private String name;    
    
    @OneToMany(mappedBy="crew") // Crew입장에서 Member는 1:N
    private List<Member> members = new ArrayList<>();
    ...
}
  • @OneToMany(mappedBy="crew") 여기서 mappedBy의 역할은 무엇일까?
  • mappedBy는 1:N 매핑에서 나는 뭐랑 연결되어 있는가?를 의미한다.
  • 그래서 members는 Member 클래스의 crew와 연결되어 있다고 알려준 것.
Member findMember = em.find(Member.class, member.getId());
Crew findCrew = findMember.getCrew(); // 이 부분
// 양방향
List<Member> members = findCrew.getMembers(); 
  • 이처럼 crew에서도 member로 접근이 가능해졌다.

이제 mappedBy에 대해서 조금 더 자세히 알아보자.

mappedBy

  • mappedBy를 이해할려면 객체와 테이블이 연관관계를 맺을 때의 차이를 알아야한다.
  • 위 예시를 가지고 설명하자면 테이블은 CrewId 즉 FK로 Join을 하면 양쪽의 연관관계를 다 알 수 있어 양방향 연관관계 1개를 가진다.
  • 객체는 따지고보면 Member -> Crew로 가는 단방향 1개, Crew -> Member로 가는 단방향 1개로 단뱡향2개를 가지고 있는 것이다.
  • 즉 객체는 참조를 2개를 가지고 있는 것인데, Member가 가지고 있는 Crew crew필드가 변경되면 테이블의 crew_id가 변경되어야하는것인지, 아니면 Crew가 가지고 있는 List<Member> members가 변경되면 crew_id가 변경되어야하는 것인지 혼란이 발생한다.
  • 좀 더 쉽게 설명하자면, 내가 크루를 바꾸고싶을 때 Member의 crew 필드 값을 바꿔야하는지, Crew의 List<Member> members의 값을 바꿔야하는지 혼란이 온다는 말이다.

그래서 둘 중 하나로 FK를 관리해야한다. 즉 이러한 연관관계의 주인을 정해야한다.

연관관계 주인은 보통 FK를 가진 쪽이 주인이다.

쉽게 얘기하면 연관관계 주인은 다(N)쪽이 주인이다.

  • 연관관계 주인이 아닌 쪽은 read-only
  • 연관관계 주인은 mappedBy속성을 가지지 않고 연관관계 주인이 아니면 mappedBy 속성으로 주인을 지정해야한다.
@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne 
    @JoinColumn(name="crew_id")
    private Crew crew;
    ...
}
  • 그래서 Member.class가 연관관계 주인이다.
@Entity
public class Crew {
	@Id @GeneratedValue
    private Long id; //crewId
    
    private String name;    
    
    @OneToMany(mappedBy="crew") // Crew입장에서 Member는 1:N
    private List<Member> members = new ArrayList<>();
    ...
}

주의

  • 연관관계 주인에 값을 입력하지 않으면 FK에 null값이 들어가기때문에 값을 넣어줘야한다.
  • 결론은 양방향이면 항상 양쪽에 값을 넣어주는 것이 좋고 연관관계 편의 메서드를 사용해서 실수를 방지할 수 있다.
  • 양쪽에 연관관계 편의 메서드가 있으면 무한 루프 등 문제를 일으킬 수 있기때문에 한 쪽에만 생성도록 하자.

[예시] - 단순하게 작성함.

@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne 
    @JoinColumn(name="crew_id")
    private Crew crew;
    
    public void setCrew(Crew crew){
    	this.crew = crew;
        crew.getMembers().add(this); // 이 부분
    } 
    ...
}
  • 연관관계 편의 메서드는 상황에 따라 작성해야 할 객체가 다를 수 있기때문에 상황을 보고 작성해야한다.

연관관계를 매핑시 고려사향으로는 다대다인지, 다대일인지 등과 같은 다중성과 단방향인지 양방향인지, 연관관계의 주인은 누구로 정할건지 등을 고려해야한다.

다대일 N:1 연관관계 @ManyToOne

  • 항상 다(N)쪽에 외래키를 가지고 있어야한다.
  • 위에서 제일 먼저 본 연관관계가 다대일 관계였다.

[다대일 단방향 코드]

@Entity
public class Crew {
	@Id @GeneratedValue
    private Long id; //crewId
    
    private String name;    
    ...
}
@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne // Member 입장에서 Crew를 보면 Member:Crew=N:1
    @JoinColumn(name="crew_id") // 이 연관관계에서 join할 컬럼은 뭐야?를 의미함.
    private Crew crew;
    ...
}
  • 양방향은 생략하겠다.

일대다 1:N 연관관계 @OneToMany

  • 일대다 관계에서는 1이 연관관계 주인이다.
  • DB 테이블입장에서는 다쪽에 FK를 가지고 있고 1이 연관관계 주인이라 아래 코드를 예시로 들어 말하자면 List<Member> members의 값이 바뀌면 FK의 값이 바뀌게 설정되어야 한다.
  • 즉 객체와 테이블의 차이때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조로 권장하지 않음. 일대다보단 다대일 양방향 매핑을 사용하는 것을 권장함.
  • @JoinColumn은 필수

[일대다 단방향]

@Entity
public class Crew {
	@Id @GeneratedValue
    private Long id; //crewId
    
    private String name;
    
    @OneToMany
    @JoinColumn(name="crew_id")
    private List<Member> members = new ArrayList<>();
    ...
}
@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    ...
}

일대일 1:1 연관관계 @OneToOne

  • 일대일 연관관계는 주 테이블 또는 대상 테이블 중에 외래키 선택이 가능하다.
  • FK에 DB 유니크 제약조건이 추가되어야함.
  • FK가 있는 곳이 연관관계의 주인이다.
  • 예시로 Member와 Locker가 일대일 관계라고 하자.

[일대일 단방향 - 주테이블에 외래키가 있는 경우]

@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    @OneToOne
    @JoinColumn(name="locker_id")
    private Locker locker;
    
}
@Entity
public class Locker {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    // 양방향으로 하고싶다면 다음과 같은 코드 추가
    @OneToOne(mappedBy="locker")
    private Member member;
    
}
  • 일대일 단방향 관계에서 대상 테이블에 FK가 있는 관계는 지원하지 않는다.
  • 하지만 일대일 양방향 관계에서 대상 테이블에 FK가 있는 관계는 지원한다.

다대다 N:M 연관관계 @ManyToMany

  • 실무에서 사용하면 안되는 연관관계
  • N:M관계를 중간 테이블을 생성해서 1:N, N:1 관계로 풀어내야한다.
  • 다대다 관계는 추후에 정리하겠다.

자바 ORM 표준 JPA 프로그래밍-기본편을 학습하면서 정리한 블로그입니다.

profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글