이번 글에서는 최근 프로젝트를 하면서 실제로 겪은 연관관계 이슈와
공부하면서 정리하게 된 JPA 연관관계의 기초 개념과 주의할 점들을 간단하게 기록한다.
DB 테이블은 서로 연결되어 있고, 이 연결을 관계(연관관계)라고 부른다.
예를 들어, 강의(course)와 학생(student)이 있을 때 "한 명의 학생이 여러 강의를 수강할 수 있다"면 1:N 관계(일대다 관계)인 것이다.
이런 관계를 코드 안에서도 표현해줘야 하는데 JPA에서는 @OneToMany, @ManyToOne, @OneToOne, @ManyToMany 같은 어노테이션을 사용하여 관계를 표현한다.
🌟주의
연관관계를 이해할 때 가장 헷갈리는 부분은 코드에서는 객체끼리 연결되고, DB에서는 테이블끼리 외래키(FK)로 연결된다는 점이다.
JPA는 이 둘을 자동으로 매핑해주지만 항상 “객체는 참조”, “DB는 외래키”라는 관점을 구분해서 바라보아야한다.
한 쪽에서만 다른 객체를 참조하는 방식
ex) CourseTime은 Course만 알고 있고, Course는 CourseTime을 모름
//CourseTime
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
private Course course;
서로 참조하는 구조
ex) CourseTime도 Course를 알고, Course도 CourseTime 목록을 가지고 있음
//Course
@OneToMany(mappedBy = "course")
private List<CourseTime> courseTimes = new ArrayList<>();
Course에서도 CourseTime을 탐색할 수 있어서 단방향보다 더 많은 정보를 가져올 수 있지만, 주의해야할 것들이 있음 (아래에 🔽)
양방향 연관관계를 만들면 JPA가 헷갈릴 수 있기 때문에, 둘 중 한 쪽을 '주인'으로 정해야 한다.
연관관계의 주인은 실제로 외래키를 갖고 있는 객체이다.
mappedBy는 주인이 아닌 쪽에서 사용하는 속성으로, "나는 반대편 필드를 따라갈게"라는 뜻이다.
그래서 mappedBy = "course"는 CourseTime 엔티티 안의 course 필드를 기준으로 관계를 맺는다는 뜻이다.
이걸 정하지 않으면 JPA는 관계를 두 번 맺으려고 하고 쓸데없는 쿼리가 날아가게된다.
추가로 DB 설계 쪽 얘기를 잠깐해보겠다.
우리가 JPA로 연관관계를 맺으면, 결국 DB에도 테이블 간 관계가 생기는데 이걸 식별관계와 비식별관계로 나눌 수 있다.
자식 테이블이 부모 테이블의 기본키(PK)를 포함해서 자기 PK를 만드는 경우
즉, 부모 없이는 자식이 존재할 수 없다. 잘 쓰면 정합성이 좋아지지만 JPA에서는 복잡해져서 실무에서는 많이 쓰지 않는다고 한다.
자식 테이블이 자기만의 PK를 갖고, 부모의 PK는 외래키로만 사용
우리 프로젝트의 CourseTime → Course 관계도 여기에 해당한다.
📘 Course
└─ 🔑 course_id (PK)
📗 CourseTime
├─ 🔑 course_time_id (PK)
└─ 🔗 course_id (FK, 비식별관계)
이렇게 설계하면 구조가 단순하고, 나중에 확장하거나 수정하기도 쉽다.
그래서 JPA에서는 대부분 비식별관계로 설계하는게 기본이라고 한다.
Course와 CourseTime 사이를 양방향으로 설정했다.Enrollment -> Course -> CourseTime 순으로 접근해야한다고 생각했기 때문이다. CourseTime을 단순히 조회하려고 했을 뿐인데 예상치 않게 Course까지 함께 불러오는 쿼리가 나갔다.Course에서 course.getCourseTimes()를 호출하는 경우,Course 쪽에 있던 List<CourseTime>을 제거하고, 필요한 경우에만 JPQL의 fetch join을 통해 명시적으로 조회하도록 구조를 변경했다.💡 단방향으로도 충분할 때가 많다
JPA는 단방향만으로도 기능을 구현할 수 있는 경우가 많다.
화면에서 역방향으로 탐색할 일이 없다면 굳이 양방향으로 만들어 성능이 떨어지는 상항을 만들지 않아야겠다는 생각이 들었다.
연관된 객체를 언제 불러올지도 중요한 설정이다.
join fetch를 써서 한 번에 가져올 수 있다.SELECT ct FROM CourseTime ct JOIN FETCH ct.course이번에 수강신청 현황 조회 기능을 리팩토링하면서 '편하려고 만든 연관관계가 오히려 나중에 발목을 잡을 수 있다'는 걸 확실히 느꼈다. 앞으로는 연관관계를 만들 때 무조건 편한 쪽보다 정확하고 단순한 쪽을 먼저 고려하는 습관을 들여야겠다.