백엔드/스프링
[스프링] 매핑과 엔티티의 관계: 1:N, N:1, N:M, 1:1, FetchType
mike705114
2024. 12. 9. 20:35
엔티티(Entity) 매핑은 데이터베이스 테이블과 자바 객체 간의 관계를 설정하는 과정에서 핵심 개념입니다. 특히 관계형 데이터베이스에서는 테이블 간의 관계를 효과적으로 매핑하기 위해 JPA(Java Persistence API)와 같은 ORM(Object Relational Mapping) 도구를 사용합니다. 이제 엔티티 간의 관계를 정의하는 다양한 방식과 그에 따른 매핑 방법을 구체적으로 알아봅시다!
1. 엔티티 관계의 종류
엔티티 간의 관계는 크게 다음과 같이 나눌 수 있습니다.
- 1:N(One-To-Many)
하나의 엔티티가 여러 엔티티와 관계를 가질 때. - N:1(Many-To-One)
여러 엔티티가 하나의 엔티티와 관계를 가질 때. - N:M(Many-To-Many)
여러 엔티티가 서로 다수의 관계를 가질 때. - 1:1(One-To-One)
하나의 엔티티가 단일 엔티티와 관계를 가질 때.
2. 1:N(One-To-Many) 관계
예제: 하나의 팀은 여러 멤버를 가질 수 있다.
- 테이블 구조:
- Team 테이블
- Member 테이블 (Team의 외래키를 가짐)
단방향 1:N 매핑
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "team_id") // 외래키를 `Member` 테이블에 생성
private List<Member> members = new ArrayList<>();
}
- @OneToMany: 하나의 팀(Team)이 여러 멤버(Member)를 가질 수 있음을 의미.
- @JoinColumn: 외래키를 지정.
단방향 1:N은 Team 엔티티에서 Member 엔티티를 직접 조회할 수 있습니다. 그러나 반대 방향은 불가능합니다.
양방향 1:N 매핑
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
- mappedBy: 관계의 주인(Owner)이 Member 테이블의 team 필드임을 명시.
- 양방향에서는 Team과 Member 양쪽 모두에서 상대 엔티티를 참조할 수 있습니다.
3. N:1(Many-To-One) 관계
예제: 여러 멤버는 하나의 팀에 소속된다.
- 테이블 구조:
- Member 테이블 (team_id 외래키를 가짐)
- Team 테이블
단방향 N:1 매핑
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id") // `team_id`를 외래키로 설정
private Team team;
}
- @ManyToOne: 멤버는 단일팀과 연결됨.
- 단방향이므로 Team에서 Member를 직접 조회할 수 없습니다.
양방향 N:1 매핑
- 위의 1:N 양방향 매핑과 동일한 구조가 적용됩니다.
4. N:M(Many-To-Many) 관계
예제: 학생과 과목의 관계
- 한 학생이 여러 과목을 들을 수 있고, 하나의 과목을 여러 학생이 수강할 수 있다.
단순 N:M 매핑
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "student_subject", // 중간 테이블 이름
joinColumns = @JoinColumn(name = "student_id"), // 현재 엔티티(Student)의 외래키
inverseJoinColumns = @JoinColumn(name = "subject_id")) // 반대 엔티티(Subject)의 외래키
private List<Subject> subjects = new ArrayList<>();
}
@Entity
public class Subject {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "subjects")
private List<Student> students = new ArrayList<>();
}
- @JoinTable: 중간 테이블(student_subject) 생성.
- mappedBy: 반대 방향 매핑.
단순 N:M 매핑은 중간 테이블의 추가적인 속성이 없을 때 사용합니다.
N:M 매핑에서 중간 테이블에 속성이 있는 경우
중간 테이블에 추가 정보(예: 등록 날짜)가 필요한 경우 @ManyToMany 대신 중간 엔티티를 만들어야 합니다.
@Entity
public class StudentSubject {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "subject_id")
private Subject subject;
private LocalDate registeredDate;
}
- 중간 엔티티(StudentSubject)를 통해 Student와 Subject를 연결.
5. 1:1(One-To-One) 관계
예제: 사용자와 프로필의 관계
- 하나의 사용자(User)는 하나의 프로필(Profile)을 가질 수 있음.
단방향 1:1 매핑
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "profile_id")
private Profile profile;
}
- @OneToOne: 1:1 관계를 설정.
- @JoinColumn: 외래키 지정.
양방향 1:1 매핑
@Entity
public class Profile {
@Id @GeneratedValue
private Long id;
private String bio;
@OneToOne(mappedBy = "profile")
private User user;
}
- mappedBy: 관계의 주인이 User 엔티티의 profile 필드임을 명시.
6. 단방향 vs. 양방향 매핑 선택 기준
1. 단방향 매핑을 사용하는 경우
- 간단한 참조만 필요한 경우.
- 예: 특정 엔티티에서 다른 엔티티의 데이터를 조회하기만 하면 되는 경우.
- 예: Order에서 Customer의 이름이나 연락처만 필요할 때.
- 성능 최적화가 중요한 경우.
- 단방향 매핑은 연관 관계를 단일 방향으로만 유지하기 때문에 필요 이상으로 관계를 관리하지 않아도 됩니다.
- 엔티티 간 순환 참조 문제를 피해야 할 때.
- 양방향 매핑은 잘못 설계하면 순환 참조가 발생할 수 있습니다.
2. 양방향 매핑을 사용하는 경우
- 양쪽 엔티티에서 관계를 모두 조회해야 하는 경우.
- 예: Team에서 Member를 조회하거나, Member에서 소속 Team을 조회해야 하는 경우.
- 양쪽 관계를 강하게 연결해야 하는 비즈니스 로직이 있을 때.
- 예: Order와 OrderItem처럼 부모와 자식 엔티티의 관계를 긴밀히 관리해야 할 때.
- 상위 엔티티에서 하위 엔티티를 갱신하거나 추가해야 하는 경우.
- 예: Team에서 Member를 추가하거나 삭제하는 기능을 구현할 때.
7. Lazy Loading과 Eager Loading
FetchType의 종류
JPA에서는 엔티티를 로드할 때 연관된 엔티티를 가져오는 방식을 설정할 수 있습니다. 이 설정은 FetchType을 통해 제어되며, 다음 두 가지 옵션이 있습니다:
- LAZY (지연 로딩)
- 연관된 엔티티를 실제로 사용할 때 데이터베이스에서 로드.
- 연관 관계가 많을수록 초기 로딩 비용을 줄이고, 필요할 때만 데이터를 가져오는 효율적인 방식.
- 기본값:
- @OneToMany, @ManyToMany → LAZY.
- EAGER (즉시 로딩)
- 엔티티를 로드할 때 연관된 엔티티도 함께 로드.
- 연관 엔티티를 자주 사용하는 경우 유리하지만, 데이터가 많을 경우 성능에 악영향을 줄 수 있음.
- 기본값:
- @OneToOne, @ManyToOne → EAGER.
FetchType 설정 예시
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
}
- Team에서 Member는 LAZY로 설정되어 있어 실제로 접근하기 전까지 로드되지 않습니다.
- Member에서 Team은 EAGER로 설정되어 Member를 로드할 때 항상 함께 로드됩니다.
장단점
FetchType장점단점
FetchType | 장점 | 단점 |
LAZY | 초기 로딩 비용 감소, 불필요한 데이터 로드 방지 | 필요한 순간 추가 쿼리가 실행되어 N+1 문제 발생 가능 |
EAGER | 간편한 연관 데이터 로드 | 초기 로딩 비용 증가, 사용하지 않는 데이터도 로드 |
N+1 문제
- 설명: LAZY 로딩 설정 시, 연관된 엔티티를 하나씩 가져오면서 추가 쿼리가 실행되는 문제.
- 해결 방법:
- Fetch Join: JPQL을 사용해 한 번의 쿼리로 데이터를 가져오기.
String query = "SELECT t FROM Team t JOIN FETCH t.members"; List<Team> teams = entityManager.createQuery(query, Team.class).getResultList();
- Batch Fetching: 한 번에 여러 연관 데이터를 로드.
@BatchSize(size = 10)
- Fetch Join: JPQL을 사용해 한 번의 쿼리로 데이터를 가져오기.
- 기본적으로 LAZY를 사용하여 필요할 때만 데이터를 로드하는 것이 성능 최적화에 유리.
- 특정 비즈니스 요구 사항에서 즉시 로딩이 필요한 경우 EAGER를 사용.
- N+1 문제가 발생할 가능성이 높은 경우 Fetch Join 또는 Batch Fetching을 적용.
※ N+1문제는 나중에 더 자세히 다룰 예정입니다!