1. 소개
관계 매핑 어노테이션이란?
DB에서 원래 사용하던 개념을 Java 객체에서도 쉽게 사용할 수 있도록 해주는 도구
사용하는 이유
- SQL 직접 작성 → 자동 변환
- 원래라면 JOIN 같은 SQL을 직접 써야 하는데, JPA에서는 어노테이션만 붙이면 자동으로 처리됨.
- 예를 들어, @OneToMany를 사용하면 내부적으로 JOIN을 사용한 SQL을 실행해 줌.
- 객체 지향적인 개발을 가능하게 함
- DB는 테이블과 컬럼 기반인데, Java는 객체와 필드 기반
- 이를 매끄럽게 연결해주기 위해 관계 매핑 어노테이션이 필요함.
- 생산성 향상
- SQL을 일일이 작성하는 대신, JPA가 알아서 적절한 SQL을 실행해 주니까 개발 속도가 빨라짐.
- 유지보수 용이
- 비즈니스 로직이 변경되더라도, 관계 매핑을 어노테이션으로 관리하면 SQL을 직접 수정할 필요가 줄어듦.
2. 어노테이션 종류
1. @OneToMany
개념
하나가 여러 개를 가지는 관계
한 부모 엔티티가 여러 개의 자식 엔티티를 가질 때 사용
예제
1) 단방향
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "department_id") // 자식 테이블에 외래 키 컬럼 추가
private List<Employee> employees = new ArrayList<>();
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
- @OneToMany는 기본적으로 외래 키를 자식 테이블(Employee)에 두지만, 단방향 매핑을 하려면 @JoinColumn을 사용해야 함.
- 그렇지 않으면 조인 테이블이 자동 생성됨.
2) 양방향
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
}
- mappedBy = "department": 연관 관계의 주인이 아님을 의미. Employee의 department 필드가 연관 관계의 주인.
- CascadeType.ALL: Department 삭제 시 관련된 Employee도 자동 삭제.
- orphanRemoval = true: Department에서 employees 리스트에서 제거되면 DB에서도 삭제됨.
2. @ManyToMany
개념
여러 개의 엔티티가 여러 개의 엔티티와 관계를 가진다.
ex) 학생과 강의, 한 학생은 여러 강의를 들을 수 있고, 강의를 듣는 학생도 여러명이다.
예제
1) 기본 사용법
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
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<>();
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
}
- @JoinTable을 사용하여 연결 테이블(student_course)을 명시적으로 지정.
- joinColumns: 현재 엔티티(Student)의 외래 키.
- inverseJoinColumns: 반대 엔티티(Course)의 외래 키.
- mappedBy = "courses": Course에서는 연결 테이블을 직접 관리하지 않음.
2) 중간 엔티티로 풀어서 매핑
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "student")
private List<StudentCourse> studentCourses = new ArrayList<>();
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@OneToMany(mappedBy = "course")
private List<StudentCourse> studentCourses = new ArrayList<>();
}
@Entity
public class StudentCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
}
- StudentCourse를 통해 다대다 관계를 일대다 & 다대일 관계로 변환.
- 추가적으로 등록 날짜, 성적 같은 컬럼을 넣을 수도 있음.
3. @OneToMany
개념
하나의 엔티티와 하나의 엔티티가 관계를 가진다.
ex) 사용자와 프로필, 주차공간과 차량 같은 경우
예제
1) 단방향
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id") // 외래 키 지정
private Profile profile;
}
@Entity
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
}
- @OneToOne은 기본적으로 즉시 로딩(EAGER) 을 사용하므로, 성능을 고려하여 fetch = FetchType.LAZY로 변경하는 것이 좋음.
- @JoinColumn(name = "profile_id"): User 테이블에 profile_id 외래 키를 둠.
2) 양방향
@Entity
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
@OneToOne(mappedBy = "profile")
private User user;
}
- mappedBy = "profile"을 추가하면 Profile 테이블에는 별도 외래 키 컬럼이 생기지 않음.
4. @ManyToOne
개념
여러 개의 엔티티와 하나의 엔티티가 관계를 가진다.
ex) 여러 명의 직원이 하나의 회사를 다님
예제
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id")
private Company company;
}
@Entity
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
- @ManyToOne(fetch = FetchType.LAZY): 성능을 위해 지연 로딩 설정.
- @JoinColumn(name = "company_id"): Employee 테이블에 company_id 컬럼 생성.
2) 양방향 @OneToMany와 함께 사용
@Entity
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "company")
private List<Employee> employees = new ArrayList<>();
}
- mappedBy = "company": Employee 엔티티의 company 필드가 연관 관계의 주인임을 명시.
3. 추가 개념
1. FetchType (EAGER & LAZY)
기본값
- @OneToOne 및 @ManyToOne → EAGER (즉시 로딩)
- @OneToMany 및 @ManyToMany → LAZY (지연 로딩)
즉시 로딩(EAGER)의 문제점
Employee emp = employeeRepository.findById(1L);
System.out.println(emp.getCompany().getName()); // 회사 정보까지 즉시 로딩됨
필요하지 않은 데이터까지 불필요하게 가져오니까 성능 문제 발생
지연 로딩(LAZY) 적용 방법
@ManyToOne(fetch = FetchType.LAZY)
private Company company;
FetchType.LAZY를 설정하면 emp.getCompany()를 호출하기 전까지 데이터를 가져오지 않음.
2. Cascade (영속성 전이)
부모 엔티티가 저장/삭제될 때 자식 엔티티도 같이 처리되도록 함.
CascadeType 종류:
- ALL: 모든 변경(저장, 수정, 삭제, 병합) 전파.
- PERSIST: 저장 시 전파.
- MERGE: 병합 시 전파.
- REMOVE: 삭제 시 전파.
- REFRESH: 엔티티 새로고침 시 전파.
- DETACH: 엔티티 분리.
사용 예제
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
3. orphanRemoval
부모 엔티티에서 자식 엔티티를 컬렉션에서 제거하면 DB에서도 삭제됨.
사용 예제
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
Company company = companyRepository.findById(1L);
company.getEmployees().remove(0); // 첫 번째 직원 제거
orphanRemoval = true를 설정하면, 컬렉션에서 제거된 Employee는 DB에서도 삭제됨.
4. 고려할 점
- 단방향 vs 양방향
- 단방향: 간단하지만 조회 시 조인 비용이 증가.
- 양방향: 객체 참조가 가능하지만, 무조건 mappedBy를 설정해야 불필요한 중복 테이블 생성 방지.
- 연관 관계의 주인 설정
- mappedBy가 없는 쪽이 연관 관계의 주인.
- 외래 키를 관리하는 테이블이 주인이어야 함.
- 다대다(@ManyToMany) 사용 주의
- @ManyToMany는 연결 테이블을 직접 관리하지 못함.
- 중간 엔티티를 만들어 @OneToMany & @ManyToOne으로 변환하는 것이 좋다.
- 쿼리 최적화 필요
- fetch = FetchType.LAZY 사용 권장. @OneToMany와 @ManyToMany는 기본적으로 LAZY 로딩.
- @EntityGraph 또는 JOIN FETCH를 사용하여 N+1 문제 해결.
5. 결론
결국 이것도 개발자가 SQL을 직접 작성하는 시간을 줄이고, 유지보수를 쉽게 하기 위해 만들어진 것!
'Spring' 카테고리의 다른 글
[Spring] Spring Security (0) | 2025.02.06 |
---|---|
[Spring] N + 1 문제 (0) | 2025.02.06 |
[Spring] 스프링 부트 구조 정리 (0) | 2025.02.04 |
[Spring] 동적으로 HTTP 상태 코드 설정 (0) | 2025.02.04 |
[Spring] Bean (0) | 2025.02.04 |