연관관계

이재현·2024년 8월 22일

Spring

목록 보기
7/13

🤍연관관계 매핑

연관관계란 객체와 관계형 데이터베이스 테이블을 서로 매핑하는 것이다.

연관관계 매핑 시에는 생각해야 할 사항이 3가지가 있다.

  • 방향: 단방향, 양방향(객체 간의 참조)

  • 다중성: 일대일, 일대다, 다대일, 다대다

  • 연관관계의 주인: 양방향일 때 연관관계에서 관리의 주체가 되는 곳




🩵 방향

데이터베이스에서는 테이블을 외래 키 하나를 가지고 양쪽 테이블 조인이 가능하다.
따라서 DB에서는 단방향이니 양방향이니 나눌 필요가 없다.

하지만 객체는 참조용 필드가 있는 객체만이 다른 객체를 참조하는 것이 가능하다.
따라서 두 객체 사이에 하나의 참조용 필드만을 가지고 참조하게 되면 단방향, 두 객체 모두가 참조용 필드를 갖고 참조하면 양방향 관계가 된다.

이러한 방향의 선택은 비즈니스 로직에 맞게 개발자가 선택해서 정해야 한다.
member.getReservations() 처럼 예약목록에 대한 참조가 필요하면

  • Member -> Reservation 단방향 참조

reservation.getMembers() 처럼 예약자에 대한 참조가 필요하면

  • Reservation -> Member 단방향 참조

위에서 두 개 다 필요하다면 양방향이 되고, 회원이 예약을 한 개 이상 할 수 있다면 다대일 양방향이 되는 것이다.

💙 양방향의 무분별한 사용 지양

모든 객체 간의 관계를 양방향으로 하면 아무 문제가 없지 않을까?

하지만 회원과 같은 다른 객체와 관계를 정말 많이 맺게 되는 경우, 무분별하게 관계를 맺다 보면 해당 객체가 너무 복잡해지는 문제가 발생한다.

또한 연달아 다른 객체들도 복잡성이 증가한다.

따라서 관계가 필요하다면 일단 단방향으로 설계를 하고, 추후 정말 양방향이 필요하다면 그때 양방향으로 설계를 하는 방식으로 접근하는 것이 좋다.

⭐ Join이 뭔가요?

Join은 관계형 데이터베이스에서 두 개 이상의 테이블을 결합해서 하나의 결과집합을 만들어내는 연산이다.

보통 테이블 간의 관련된 데이터를 조합해서 조회할 때 사용된다.

이때 테이블 간의 관계는 보통 외래 키를 통해서 형성된다.

JPA에서의 Join은 @ManyToOne, @OneToMany, @OneToOne 등의 매핑을 통해서 객체 간의 연관관계를 설정하고, 내부적으로 SQL의 Join을 사용해서 데이터를 조회한다.
(JPA에서는 직접 SQL을 작성하지 않아도 된다는 개꿀사실!)




🩵 다중성

데이터베이스에서 테이블 간의 관계를 기준으로 설정하게 된다.

예를 들어 팀프로젝트 홈페이지 같은 걸 생각해보면 다음과 같은 관계를 설정할 수 있다.

  • 회원 : 프로필 (1:1)

  • 회원 : 회의 예약 (1:N)

  • 회원 : 팀 (N:1)




🩵 연관관계의 주인

연관관계의 주인은 양방향 관계에서 누가 이 관계의 주인으로서 외래 키를 가지고, 테이블을 INSERT, UPDATE 하는 권한을 갖는 것이다.

왜 연관관계의 지정하는가?
연관관계의 주인을 지정하는 것은 JPA에게 누가 주인인지 알려주는 용도이다.

⭐ mappedBy
mappedBy는 양방향 매핑에서 관계 미소유가 객체를 가리키기 위해 사용하는 것이다.
사용방식은 @OnetoMany(mappedBy = “참조하는 엔티티가 있는 변수명”)으로 사용한다.
이를 사용함으로써 현재 자신의 참조가 참조하는 해당 엔티티에 어떤 변수로 지정되었는지 JPA에게 알려줄 수 있다.

⭐ Fetch Type
연관관계를 갖고 있는 객체를 조회하려면 JPA는 해당 객체가 들고 있는 연관관계의 객체들을 모두 조회해서 끌고 온다.

이때 1+N 쿼리 문제가 발생한다.
1+N 쿼리 문제란 주 쿼리 하나와 그 주 쿼리에서 조회된 각 Entity에 대해 N개의 추가 쿼리가 실행되는 상황을 말한다.

예시: 하나의 서점을 조회했는데 해당 객체가 3개의 책을 갖고 있고, 각 책은 2개의 편집자 정보를 갖고 있다.

그렇다면 서점 -> 책 -> 편집자 정보로 이어지는 조회가 발생하는데, 서점 1번 + 책 3번 + 편집자 6번, 총 1+9번의 쿼리가 발생하게 된다.
그렇기에 일대다 방식에서는 Lazy 방식을 지향한다.

  • 즉시로딩(FetchType.EAGER)

    • 연관된 객체를 즉시 가져오는 방식
    • 연관된 객체가 사용되지 않더라도 일단 모두 조회해온다.
  • 지연로딩(FetchType.LAZY)

    • 연관 객체를 프록시 객체로 치환하여 껍데기 객체로 대치하여 추후 직접적인 조회가 발생하면 가져오는 방식
    • 조회를 미루는 방식이다.



🩵 @OneToOne

하나의 객체와 다른 객체간 1:1 연관관계를 맺을 때 사용한다.

하나의 Entity가 오직 다른 하나의 Entity와 연관된 경우이다.

예를 들면 사용자와 사용자 상세정보와 같은 관계가 이에 속한다.

💜 단방향 일대일

@Entity
public class User {
	@Id
	private Long id;
	
	@OneToOne // 일대일 관계 매핑 정보
	@JoinColumn(name = “PROFILE_ID”) // 관계의 주인이 가짐, 외래키를 매핑할 컬럼 명
	private Profile profile;}

@Entity
public class Profile {
	@Id
	private Long Id;}

@OneToOne은 일대일 관계를 매핑하기 위한 어노테이션이다.

@JoinColumn은 외래키를 매핑하기 위한 어노테이션이다.

아까도 말했지만 관계형 데이터베이스에서는 테이블 간의 연관관계를 표현하기 위해서 외래키를 사용한다.

JPA에서 이 외래키를 매핑하기 위해서 @JoinColumn을 사용하고 그 안에 name=”외래 키 컬럼명”을 통해서 외래 키 이름을 명시한다.

@JoinColumn 어노테이션은 관계의 주인이 갖게 된다.

키가 있는 쪽이 주인 아닌가요? 네! 맞습니다!
그렇기에 User가 Profile에 대한 외래키를 갖고 있다고 볼 수 있다.

그렇게 작성하게 되면 위에서 보듯이 UserProfile을 참조하는 상황이 된다.
이러한 관계를 일대일 단방향 관계라고 할 수 있다.

그렇다면 일대일 양방향은 어떻게 표현하는가?

💜 양방향 일대일

@Entity
public class Profile {
	@Id
	private Long Id;

	@OneToOne(mappedBy = “profile”)
	private User user;}

그렇다면 주인이 아닌 객체에서도 똑같이 @OneToOne을 작성해주면 된다.

단! 이때 다른 점은 mappedBy = ”주인 필드명” 속성을 통해 관계의 주인이 상대편임을 명시해줘야 한다.

위에서 보면 User entity의 profile 필드가 주인임을 명시한 것을 볼 수 있다.




💙 @ManyToOne

데이터베이스를 기준으로 다대일의 다중성을 갖는 객체들이다.

N이 다수의 쪽이므로, N측이 외래 키를 관리하는 형태이다. 참고로 DB는 항상 N쪽이 외래키를 갖는다.

💜 다대일 단방향

@Entity @Getter @Setter
public class Book {
	@Id
	private Long id;
	@Column
	String title;
	
	@ManyToOne // 다대일 단방향에서 ‘다’쪽인 책이 갖고 있음
	@JoinColumn(name = “LIBRARY_ID”)
	private Library library;
}

@Entity @Getter @Setter
public class Library {
	@Id
	private Long id;
	@Column
	private String libraryName;
}

아까 일대일 단방향과 형태는 동일하다. 다만 여기서 기억하고 넘어가야 할 점이 있다.

@ManyToOne 어노테이션은 아까랑 다르게 다(Many)쪽에서 사용되는 어노테이션으로 일(One)쪽을 참조하는 어노테이션이다.

일반적으로 다대일 관계에서는 외래키를 관계의 다(Many)쪽에서 소유하게 된다는 점!


💜 다대일 양방향

@Entity @Getter @Setter
public class Library {
	@Id
	private Long id;
	@Column
	private String libraryName;

	// 다대일 중 ‘1’쪽에 달아서 양방향 매핑 성사, 상대 쪽이 관계의 주인임을 명시
	@OneToMany(mappedBy = “library”) 
	Private List<Book> books = new ArrayList<>();
}

양방향은 @OneToMany라는 어노테이션을 일(One) 쪽에 붙이는 것으로 표현한다.




❤️ 일대다 관계

일대다는 위에서 말한 관계의 주인을 다(N)가 아닌 일(1)에 두는 것을 말한다.

무조건 ‘다’쪽에 외래키를 두고 관리하게 하는데 외래 키 위치와 별개로 ‘1’쪽에서 다 쪽 객체를 관리(등록, 수정)하는 것이다.

@Entity
@Getter @Setter
public class Book {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = “id”, nullable = false)
	private Long id;

	@Column
	private String name;
}

@Entity
@Getter @Setter
public class Library {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = “id”, nullable = false)
	private Long id;

	@Column
	private String libraryName;
	
	@OneToMany
	@JoinColumn(name = “LIBRARY_ID”) // 해당 테이블들이 조인을 하게 됨
	private List<Book> books = new ArrayList<>();
}

하지만 이러한 일대다 형태는 사용을 지양한다.

우선 Entity가 관리하는 외래키가 반대쪽에 있다는 점, 그리고 일(One)쪽을 저장할 때 다(Many)쪽에 대한 update 쿼리가 발생한다는 점 때문이다.

쉽게 말하자면, 현재 ‘1’쪽인 서점이 책들을 갖고 있고 자기가 관리하겠다(관계의 주인이 되겠다)는 상황이다. 따라서 @JoinColumn도 서점이 들고 있게 된다.

하지만 정작 외래키는 데이터베이스 원칙상 다(Many)쪽인 Book이 들고 있게 되므로 관리 객체가 아닌 반대쪽에 외래키가 존재하는 이상한 상황이 발생하게 된다.

또한, 책을 저장할 때는 문제가 없지만 LibraryBook을 넣고, Library를 저장할 때 Library가 보유한 Book에 대해서 update 쿼리가 발생하게 된다.

이처럼 다양한 문제점들이 있기 때문에 사용하지 않는다.




💙 @ManyToMany

다대다 연관관계를 의미하며 JPA스펙상 존재하기는 하지만 실무에서 사용하지는 않는다.

왜? 문제점이 많기 때문이다.

다대다 관계를 조회할 때, 중간 테이블을 통한 조인 연산이 필요하다.
이로 인해 성능 문제가 발생할 수 있으며, N+1 쿼리 문제와 같은 성능 이슈를 초래할 수 있다.

또한, 복잡한 쿼리를 작성하거나, 많은 데이터를 로드할 때 성능 저하가 발생할 수 있다.

그리고 중간 테이블의 데이터 무결성을 보장하기 어려울 수 있다.
특히, 복잡한 다대다 관계에서 데이터가 제대로 동기화되지 않거나 중복이 발생할 수 있다.

그럼 아예 못하는 가?
아니다. 이는 일대다 & 다대일 구조로 풀어서 만드는 방식으로 대처할 수 있다.

그렇다면 이걸 어떻게 풀어서 사용하는가?
중간다리 역할을 하는 테이블을 직접 생성하면 된다.




🩵 @OneToMany & @ManyToOne 중간 테이블

간단한 예시로 학생과 과목의 연관관계를 작성해보려 한다.

💜 Student

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String studentName;

    @OneToMany(mappedBy = "student")
    private List<StudentSubject> studentSubjects = new ArrayList<>();.
}

💜 Subject

@Entity
public class Subject {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String subjectName;

    @OneToMany(mappedBy = "subject")
    private List<StudentSubject> studentSubjects = new ArrayList<>();}

💜 StudentSubject

@Entity
public class StudentSubject {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "student_id") // 외래 키
    private Student student;

    @ManyToOne
    @JoinColumn(name = "subject_id") // 외래 키
    private Subject subject;}

이렇게 두개의 Entity와 한 개의 중간 Entity를 생성함으로써 다대다 연관관계를 설정할 수 있다.

StudentSubject는 각각 @OneToMany를 사용해서 StudentSubject와 연관관계를 설정하고, StudentSubject@ManyToOne을 사용해서 다시 각각의 Studenet, Subject와 연관관계를 설정한다.

그렇다면 테이블 구조는 어떻게 되어있는가?
@JoinColumn을 보면 알겠지만 외래키를 StudentSubject가 갖고 있다.

여기까지 이해가 되었다면 데이터를 어떻게 다뤄야 할지가 문제가 된다.

간단하게 둘 다 갖고 있는 중간테이블에서 다룬다고 생각하면 된다. 여태 다른 Entity들로 데이터를 생성, 조회, 수정, 삭제를 진행했듯이 말이다.

이부분은 추후 필요하다면 작성하도록 하겠다.

0개의 댓글