JPA의 관계 매핑
JPA는 객체 간의 연관 관계(association)를 데이터베이스의 외래 키 관계로 변환해주는 기능을 제공합니다. 객체지향에서는 필드 참조로 관계를 표현하고, 데이터베이스에서는 외래 키(Foreign Key)로 표현하죠.
이 둘을 매끄럽게 연결해주는 것이 바로 JPA의 관계 매핑(association mapping)입니다. 이 글에서는 1:1, 1:N, N:1, N:N 관계의 표현 방식과 주의사항, 실전 팁까지 모두 알아봅시다.
1. 왜 관계 매핑이 중요한가?
객체 지향 프로그래밍에서는 객체 간의 참조(Reference) 를 사용하지만, 관계형 데이터베이스에서는 외래 키(Foreign Key) 를 사용해 관계를 표현합니다.
즉, 다음과 같은 차이가 존재합니다.
객체 지향 세계 | 관계형 DB 세계 |
A → B (참조) | A 테이블에 B의 외래 키 |
컬렉션 사용 가능 | Join Table 필요 |
JPA는 이러한 차이를 해소하기 위해 다양한 관계 매핑 어노테이션을 제공합니다.
JPA의 관계 매핑 종류
관계 | 설명 |
@OneToOne | 1:1 관계 (예: 사람 - 주민등록증) |
@OneToMany | 1:N 관계 (예: 팀 - 팀원들) |
@ManyToOne | N:1 관계 (예: 여러 주문 - 하나의 고객) |
@ManyToMany | N:N 관계 (예: 학생 - 강의, 서로 다수 참여) |
2. 중심 관계 매핑
a. @ManyToOne (N:1)
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne // Member : Team
@JoinColumn(name = "team_id") // Foreign Key
private Team team;
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
- Member 입장에서 Team은 N:1 관계
- Member 테이블에 @JoinColumn으로 외래 키 명시
- 연관관계의 주인은 Member (즉, 외래 키를 가진 쪽이 주인)
b. @OneToMany (1:N)
i. 단방향 매핑
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany // Team : Member
@JoinColumn(name = "team_id") // Member 테이블에 Foreign Key 생성
private List<Member> members = new ArrayList<>();
}
- 단방향 1:N 관계는 실제 DB에는 외래 키가 Member 테이블에 존재하므로, @JoinColumn을 반드시 명시해야 함
- 잘 사용하지 않음. 대신 양방향 매핑 권장
ii. 양방향 매핑
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // Team : Member, Team 테이블은 연관관계의 주인이 아님
private List<Member> members = new ArrayList<>();
}
- mappedBy는 연관관계 주인이 아님을 표시
- 주인은 외래 키를 가진 Member 쪽
- 주인 쪽만 insert/update SQL을 발생시킴
c. @OneToOne
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne // User : Card
@JoinColumn(name = "card_id")
private Card card;
}
- 드물게 사용되며, 외래 키 위치에 따라 mappedBy를 사용해 양방향 가능
d. @ManyToMany (실무에서는 지양)
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses = new ArrayList<>();
}
- JPA는 중간 테이블을 자동 생성하지만, 실무에서는 중간 엔티티를 따로 만들어 @ManyToOne으로 쪼개는 것이 일반적
지양하는 이유
- 중간 테이블에 데이터를 추가로 넣을 수 없음
- 예: 학생이 어떤 강의를 수강했는지 기록할 때 수강일, 성적, 출결 등 부가 정보를 넣고 싶을 수 있어요.
- @ManyToMany는 중간 테이블에 컬럼 추가가 불가능합니다.
- 그래서 보통 중간 테이블을 별도 엔티티로 만들고, @ManyToOne 관계로 나누는 구조를 사용합니다.
- 연관관계 관리가 어렵고 불명확함
- @ManyToMany는 자동으로 중간 테이블을 관리하지만, 양방향일 경우 어느 쪽이 주인인지 헷갈릴 수 있고, set, add 같은 로직을 명확히 짜기 어려움
- JPA 내부 동작을 정확히 이해하지 않으면 예측 불가한 동작이 나올 수 있음
- 성능 최적화/쿼리 튜닝이 어려움
- 중간 테이블이 숨겨져 있어서 SQL 튜닝, 페이징, 조건 검색 등에 제한이 많습니다.
- 실무에서는 조회 성능, 쿼리 커스터마이징이 매우 중요하기 때문에 명시적인 조인 테이블이 훨씬 유리합니다.
3. 실무에서의 주의사항
- 양방향 관계에서는 연관관계 주인을 명확히 설정해야 함
- 양쪽에 set 해주는 습관 필요
member.setTeam(team);
team.getMembers().add(member);
- 지연 로딩(LAZY)을 기본으로 설정하는 것이 성능상 유리
- @ToString에 연관 필드 포함하면 순환 참조 발생 주의
- CascadeType, orphanRemoval은 라이프사이클 제어 시 유용하지만 주의해서 사용해야 함
4. 정리
관계사용 | 예 | 팁 |
@ManyToOne | 대부분의 관계 (N:1) | FK 가짐, 주인 |
@OneToMany | 팀 → 멤버 (1:N) | mappedBy 권장 |
@OneToOne | 유저 → 여권 | 양방향 가능 |
@ManyToMany | 학생 ↔ 강의 | 실무에서는 중간 엔티티로 대체 |
5. 마무리
JPA의 관계 매핑은 객체 지향적 설계를 데이터베이스와 자연스럽게 연결해주는 핵심 도구입니다. 하지만 잘못 설정하면 쿼리 폭발, 무한 루프, 비효율적인 SQL이 발생할 수 있어 반드시 원리와 실전 팁을 숙지해야 합니다. 관계 매핑을 정확히 이해하고 적용하는 것이 JPA 마스터로 가는 길의 첫걸음입니다.
함께 보면 좋은 자료
블로그 글 :
[JPA] 객체와 데이터베이스를 잇는 다리
JPA(Java Persistence API) JPA(Java Persistence API)는 자바 애플리케이션에서 데이터베이스와의 상호작용을 단순하고 직관적으로 만들어주는 ORM 기술입니다. 복잡한 SQL을 직접 작성하지 않아도 객체 중심
dachaes-devlogs.tistory.com
'프레임워크와 라이브러리 > JPA' 카테고리의 다른 글
[JPA 생명 주기] Cascade와 OrphanRemoval 완전 이해하기 (0) | 2025.04.11 |
---|---|
[JPA] 객체와 데이터베이스를 잇는 다리 (0) | 2025.04.11 |