❔연관관계 매핑
데이터베이스에서 엔티티 간 관계를 정의하고, 이를 ORM(JPA) 을 통해 객체지향적으로 표현하는 것을 의미
💠연관관계 매핑이 중요한 이유
현대적인 애플리케이션에서는 여러 개의 엔티티들이 서로 연관되어 있으며, 이 관계를 올바르게 설정하는 것이 중요하다.
- 데이터 정합성(Consistency) 유지
- 중복 데이터 최소화 및 정규화
- 비즈니스 로직의 직관적인 표현 가능
- 객체 간의 관계를 코드에서 직접 표현 ➡️ 가독성과 유지보수성 향상
- SQL JOIN을 활용한 최적화된 데이터 조회 가능
Spring Boot와 JPA를 사용하면 객체 간의 관계를 기반으로 데이터베이스 테이블 간의 관계를 매핑할 수 있다.
⚠️ JPA 연관관계 매핑 시 고려해야 할 점
🔸단방향 vs 양방향
구분 | 설명 |
단방향 | - 한 엔티티에서만 다른 엔티티를 참조 - 외래 키를 포함한 엔티티에서만 연관된 엔티티 조회 가능 - 유지보수 용이 |
양방향 | - 두 엔티티가 서로 참조 - 양쪽에서 엔티티 조회 가능 - 연관관계의 주인(Owner) 개념 필요 - 복잡해질 수 있음 |
실무에서는 가능하면 단방향을 권장하고, 정말 필요할 때만 양방향으로 설정한다.
🔸관계의 종류
1. 일대일(1:1) 관계 (@OneToOne)
한 개의 엔티티가 다른 한 개의 엔티티와 1:1로 매핑되는 관계
@Entity
public class UserInfo {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(fetch = FetchType.LAZY, 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(mappedBy = "profile")
private UserInfo userInfo;
}
- `@OneToOne` 사용
- `@JoinColumn`을 통해 외래키 지정
- 양방향일 경우 `mappedBy`로 연관 관계 주인 설정
- 지연 로딩(FetchType.LAZY) 권장: 필요할 때만 가져오도록 최적화
2. 일대다(1:N) / 다대일(N:1) 관계 (@OneToMany, @ManyToOne)
하나의 엔티티가 여러 개의 엔티티와 연관된 경우
다대일(@ManyToOne) 관계는 외래키가 있는 쪽에서 설정한다.
일대다(@OneToMany) 관계는 단독으로 사용하면 외래키 관리가 어려워지고 성능이 저하될 수 있다.
실무에서는 가능하면 다대일(@ManyToOne)을 사용하고, 필요할 때만 일대다(@OneToMany)를 추가한다.
// ex)Team(One) ↔ Member(Many) (한 팀에는 여러 멤버가 속함)
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id") // 외래 키 설정
private Team team;
}
- `@OneToMany(mappedBy = "team")` ➡️ 외래키 관리 ❌ (연관 관계의 주인이 아님)
- `@ManyToOne(fetch = FetchType.LAZY)` ➡️ ManyToOne 관계에서 외래키 관리 ✅
- N:1 관계에서 외래키를 가진 쪽이 연관 관계의 주인
- `cascade = CascadeType.ALL` 사용 시 Team 삭제 시 Member 도 삭제됨
3. 다대다(N:M) 관계 (@ManyToMany)
JPA에서는 잘 사용하지 않음
중간 테이블을 만들어 1:N, N:1 관계로 풀어서 사용하는 것이 일반적
🔸외래키 관리 주체
연관 관계의 주인은 외래키를 가진 테이블이 된다.
`mappedBy`를 활용하여 어느 쪽이 주인(Owner)이고, 어느 쪽이 읽기 전용인지 설정할 수 있다.
🔵 Cascade와 FetchType 설정
🔹Cascade (영속성 전이)
부모 엔티티에서 자식 엔티티에 대한 영속성(persistence) 전이를 설정할 수 있다.
옵션 | 설명 |
CascadeType.ALL | 모든 변경 전파 (저장, 삭제 등) |
CascadeType.PERSIST | 저장(CREATE) 시 전파 |
CascadeType.MERGE | 병합(UPDATE) 시 전파 |
CascadeType.REMOVE | 삭제(DELETE) 시 전파 |
CascadeType.DETACH | 부모 엔티티가 분리될 때 자식도 분리 |
Cascade.ALL은 신중하게 사용!
1. Cascade 사용 예시
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
저장:
@Transactional
public void saveTeamWithMembers() {
Team team = new Team();
team.setName("Development Team");
Member member1 = new Member();
member1.setName("Alice");
Member member2 = new Member();
member2.setName("Bob");
// 연관관계 설정
team.addMember(member1);
team.addMember(member2);
// 팀을 저장하면 멤버도 자동 저장됨
teamRepository.save(team);
}
-- 내부적으로 실행되는 쿼리
INSERT INTO team (name) VALUES ('Development Team');
INSERT INTO member (name, team_id) VALUES ('Alice', 1);
INSERT INTO member (name, team_id) VALUES ('Bob', 1);
삭제:
@Transactional
public void deleteTeam(Long teamId) {
Team team = teamRepository.findById(teamId).orElseThrow();
teamRepository.delete(team); // -> 연관된 Member 데이터도 삭제됨
}
-- 내부적으로 실행되는 쿼리
DELETE FROM member WHERE team_id = 1;
DELETE FROM team WHERE id = 1;
2. Cascade 권장/비권장 예시
[Cascade 권장 사용 예시]
- @OneToMany 관계에서 자식 엔티티의 생명주기가 부모 엔티티에 종속될 때
- @OneToOne 관계에서 하나의 부모 엔티티가 하나의 자식 엔티티를 완전히 소유하는 경우
[Cascade 비권장 사용 예시]
- @ManyToOne 관계에서 남용할 경우 데이터 손실 위험
- @ManyToMany 관계에서 데이터 삭제 시 복잡한 의도치 않은 삭제가 발생할 가능성이 있음
3. 고아 객체 제거
부모 엔티티에서 리스트를 제거하면, 해당 엔티티도 삭제되도록 설정할 수 있다.
@OneToMany(mappedBy = "team", orphanRemoval = true)
private List<Member> members = new ArrayList<>();
🔹FetchType (즉시 로딩 vs 지연 로딩)
연관된 엔티티를 조회할 때 즉시 로딩(EAGER)과 지연 로딩(LAZY) 방식이 있다.
FetchType.EAGER | 즉시 로딩 (JOIN을 통해 연관된 엔티티 즉시 조회) |
FetchType.LAZY | 지연 로딩 (필요할 때 조회, 프록시 객체로 대체) |
1. 즉시 로딩 (FetchType.EAGER)
- 연관된 엔티티를 즉시 함께 조회
- N+1 문제를 유발할 수 있음
@ManyToOne이나 @OneToOne 기본값이 EAGER이므로 LAZY로 변경하는 것이 일반적이다.
즉시 로딩(EAGER) 예제 - Member엔티티
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
- Member 를 조회하면 즉시 Team 도 함께 조회됨
- 불필요한 데이터 조회로 성능 이슈 발생 가능
2. 지연 로딩 (FetchType.LAZY)
- 연관된 엔티티를 실제로 사용할 때만 조회
- 대부분의 경우 지연 로딩을 기본값으로 설정하는 것이 권장된다.
- @OneToMany는 기본값이 LAZY이므로 특별한 이유가 없다면 그대로 유지해야 한다.
지연 로딩(LAZY) 예제 - Member엔티티
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
- Member 를 조회할 때 Team 은 조회되지 않고, 필요할 때 쿼리가 실행됨
- 권장 방식 (필요한 데이터만 로딩하여 성능 최적화)
🗨️ 정리
JPA 연관관계 매핑은 데이터 정합성 유지, 성능 최적화, 비즈니스 로직 직관성 측면에서 필수적인 요소이다.
단방향 매핑을 우선 적용하되, Cascade, FetchType 설정을 적절히 활용하여 유지보수성과 성능을 고려해야 한다.
'Java Study > Frameworks' 카테고리의 다른 글
git clone 후 build.gradle 빨간 줄❓ 해결 방법‼️ (0) | 2025.02.24 |
---|---|
[Spring] 인증/인가와 Session 방식과 JWT 방식의 차이 (0) | 2025.02.20 |
[Spring] @Transactional을 어디에 붙여야 할까🤔❔ (0) | 2025.02.12 |
[Spring] spring.jpa.hibernate.ddl-auto 설정과 각 옵션 (0) | 2025.02.12 |
[Spring] JPA 영속성 컨텍스트와 JOIN 활용 (0) | 2025.02.12 |