JPA를 처음 접하실 때 @ManyToOne, @OneToMany 같은 연관관계 매핑 어노테이션 때문에 많이 헷갈리실 텐데요. 이 복잡한 개념이 등장한 근본적인 이유부터 살펴보겠슴다.
이유는 '자바 객체'와 '관계형 데이터베이스(RDB) 테이블'의 구조가 태생부터 완전히 다르기 때문입니다. 이를 전문 용어로 '객체와 관계형 데이터베이스의 패러다임 불일치'라고 부릅니다.
1. 객체와 테이블, 어떤 점이 다를까요?
객체 (Object)의 세계: "참조(Reference)"
- 객체는 다른 객체와 관계를 맺을 때 참조를 사용합니다.
- 예를 들어, 댓글(Comment) 객체가 게시글(Post) 객체의 정보를 알기 위해서는 comment.getPost()처럼 참조를 통해 접근해야 합니다.
- 기본적으로 단방향입니다. 게시글 객체 안에 댓글 리스트(List<Comment>)가 존재하지 않는다면, 게시글에서 댓글을 찾아갈 방법이 없습니다. 양쪽에서 서로를 참조하게 하려면 양쪽에 모두 필드를 만들어주어야 합니다.
테이블 (Table)의 세계: "외래 키 (Foreign Key)"
- 테이블은 다른 테이블과 관계를 맺을 때 외래 키(FK) 하나만 있으면 충분합니다.
- 댓글 테이블에 post_id라는 외래 키 하나만 넣어두면, JOIN을 통해 댓글에서 게시글을 찾을 수도 있고, 반대로 게시글에서 댓글을 찾을 수도 있습니다.
- 즉, 데이터베이스는 태생적으로 양방향 접근이 가능합니다.
2. SQL JOIN과는 무슨 차이가 있을까요?
- SQL JOIN: 데이터베이스 내부에서 두 테이블(게시글, 댓글)을 엮어 하나의 결과물로 뽑아내는 '조회 방법'입니다.
- JPA 연관관계 매핑: 자바 객체의 '참조'와 DB 테이블의 '외래 키' 사이의 구조적 차이를 메워주는 '번역기(설명서)' 역할을 합니다.
객체에 @ManyToOne, @JoinColumn을 설정해 주면,
JPA는 "이 객체( @ManyToOne이 붙어 있는 Comment 엔티티 객체)의 참조 필드( Comment 클래스 안에 선언한 private Post post; 변수 )가 DB 테이블(= 해당 엔티티와 연결된 comment 테이블 )의 특정 외래 키(= comment 테이블에 생성되는 post_id 컬럼 )와 매핑되는구나!"라고 이해합니다. 그리고 나중에 DB에서 데이터를 조회할 때 JPA가 알아서 JOIN 쿼리를 작성하여 객체 형태로 알맞게 조립해 줍니다.
3. 그렇다면 왜 JPA 연관관계를 써야 할까요?
과거 MyBatis 같은 기술을 사용할 때는 개발자가 직접 SQL JOIN 쿼리를 작성하고, DB에서 반환된 표 형태의 데이터를 일일이 자바 객체에 세팅(post.setComments(...))하는 번거로운 작업이 필요했습니다.
하지만 JPA의 연관관계 매핑을 활용하면 다음과 같은 장점이 있습니다:
- 생산성 향상: 개발자가 복잡한 JOIN 쿼리를 직접 작성할 필요 없이, 객체 지향적인 코드만 작성하면 JPA가 적절한 쿼리를 생성하여 DB에 전달합니다.
- 객체 중심의 개발: DB 테이블 구조에 자바 코드를 억지로 맞추는 것이 아니라, 진정한 객체 지향 프로그래밍이 가능해집니다. post.getComments() 메서드 하나만 호출하면 해당 게시글에 달린 댓글들을 객체 리스트 형태로 즉시 꺼내어 사용할 수 있습니다.
주로 어디에 쓰이나요? 데이터 간에 '1대 다(1:N)' 관계가 성립하는 모든 곳에 사용됩니다.
- 쇼핑몰: 회원(1)과 주문(N)
- 블로그/게시판: 게시글(1)과 댓글(N)
- 회사 시스템: 부서(1)와 직원(N)
4. @OneToMany, @ManyToOne 초간단 구현 예제 (Post & Comment)
게시판을 만들 때 가장 기본이 되는 관계인 게시글(Post)과 댓글(Comment)을 예시로 코드를 살펴보겠습니다.
① @ManyToOne (다대일) - Comment(댓글) 입장
- 의미: 여러 개의 댓글(N)은 하나의 게시글(1)에 속합니다.
- 특징: DB 테이블에서 외래 키(FK)를 가지는 쪽이며, 연관관계의 주인이 됩니다.
@Entity
public class Comment {
@Id @GeneratedValue
private Long id;
private String content;
// 여러 개의 댓글(Many)이 하나의 게시글(One)에 속함
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id") // DB에 생성될 외래 키 컬럼명 지정
private Post post;
}
② @OneToMany (일대다) - Post(게시글) 입장
- 의미: 하나의 게시글(1)은 여러 개의 댓글(N)을 가집니다.
- 특징: 연관관계의 주인이 아닙니다. DB 테이블 구조에는 영향을 주지 않으며, 오직 객체 상에서 댓글 목록을 조회하기 위한 용도로 존재합니다. 이때 mappedBy 속성을 반드시 지정해야 합니다.
@Entity
public class Post {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
// 하나의 게시글(One)이 여러 개의 댓글(Many)을 가짐
// mappedBy = "post" -> Comment 엔티티에 있는 'post' 필드에 의해 매핑되었음을 의미
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
}
💡 3줄 핵심 요약
- 외래 키(FK)는 무조건 '다(N)' 쪽이 가집니다. (댓글 테이블에 post_id가 있어야 하므로)
- 외래 키를 가진 쪽이 연관관계의 주인입니다. (@JoinColumn은 주인이 사용합니다.)
- 주인이 아닌 쪽은 mappedBy를 사용해 주인을 가리켜야 합니다. ("이 필드는 읽기 전용이며, 저쪽 필드에 의해 관리됩니다"라는 의미)
🔥 실무 꿀팁: @ManyToOne을 사용할 때는 반드시 fetch = FetchType.LAZY (지연 로딩)를 설정하시는 것이 좋습니다. 이를 설정하지 않으면 게시글 하나를 조회할 때 연관된 수많은 댓글을 한 번에 모두 가져오려 시도하여 심각한 성능 저하가 발생할 수 있습니다.
'백엔드 > 🍃 SpringBoot' 카테고리의 다른 글
| boolean 필드 매핑 안될 때 (isTermsAgreed가 false만 나오는 이유) (0) | 2026.03.20 |
|---|---|
| 배포시 spring-boot-devtools 비활성화 (1) | 2025.06.13 |
| Spring Boot | 패키지 구조(계층형 vs 도메인형) (1) | 2025.05.29 |
| findAll()에서 Optional<List<T>>를 쓰지 않아도 되는 이유 (1) | 2025.05.20 |
| SpringSecurity + JWT 에서 nginx health-check 요구시 문제 (0) | 2025.04.01 |