1 : N 단방향
ex. 학교 - 학생, 팀 - 멤버
1에 해당하는 엔티티가 연관관계의 주인이 되어 FK를 관리해야하는 요구사항이 있다고 가정하자.
하지만 DB 입장에서는 N에 해당하는 테이블이 FK를 관리한다.
따라서 객체지향과 데이터베이스의 충돌로 인해, 1에 해당하는 엔티티가 N에 해당하는 테이블의 데이터를 수정하게 된다.
어차피 DB 측에서는 1 : N이어도 N : 1처럼 구조가 만들어지고 동작하기 때문에 기능적으로 큰 차이는 없지만,
요구사항에서 튜터를 먼저 생성하고 튜터의 회사를 관리할 것인지, 회사를 먼저 만들고 튜터를 집어넣어줄 것인지에 따라 1 : N을 사용할지, N : 1을 사용할지가 달라진다.
@OneToMany
@JoinColumn(name = "company_id") // 연관 관계 주인이 JoinColumn으로 관계를 정의
private List<Tutor> tutors = new ArrayList<>();
// Company를 통해 Tutor 테이블에 데이터 추가
company.getTutors().add(tutor);
em.persist(company);
만약 @JoinColumn을 생략하면?
JPA가 자체적으로 중간 테이블을 생성하여 관리한다.
불필요한 중간테이블을 생성하고 쿼리를 실행하지 않도록, @JoinColumn을 꼭 붙여줘야 한다.
반대로 둘 다 @JoinColumn을 사용하면?
에러가 나거나 연관관계가 꼬일 위험이 있으므로 반드시 한쪽만 주인으로 정하도록 한다.
1 : N 양방향
1 : N 단방향과 마찬가지로 N:1 양방향을 사용하는 것을 권장한다.
JPA는 양방향 연관관계가 있을 때 DB의 외래키를 주인 엔티티의 필드로 관리한다.
주인이 아닌 엔티티의 필드는 단순히 주인 엔티티가 가지고 있는 값을 매핑받을 뿐이지, DB에 접근해서 값을 생성하거나 수정할 수 없다.
따라서 주인이 아닌 엔티티를 통해 값을 생성하려고 하면, null로 처리한다.
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "team_id") // 주인: 여기서 team_id FK 컬럼이 생김
private Team team;
}
@Entity
public class Team {
@OneToMany(mappedBy = "team") // 단순히 매핑된 것
private List<Member> members = new ArrayList<>();
}
N : 1 or 1 : N 관계에서는 N인 쪽이 FK를 가지고 있어야 하므로 N이 주인이 된다.
만약 요구사항에 의해 1인 쪽이 연관관계의 주인이 되어야 한다면, 아래와 같이 N인 쪽을 읽기 전용으로 만들고 1인 쪽을 @JoinColumn으로 주인으로 직접 설정해준다.
@Entity
public class Tutor {
@ManyToOne
@JoinColumn(name = "company_id", insertable = false, updatable = false) // 읽기 전용
private Company company;
@Column(name = "company_id")
private Long companyId; // 실제로 값을 세팅하는 필드
}
@Entity
public class Company {
@OneToMany
@JoinColumn(name = "company_id") // FK를 여기가 직접 관리
private List<Tutor> tutors = new ArrayList<>();
}
1 : 1 양방향
1 : N 양방향과 마찬가지로 두 엔티티 중 하나를 주인 엔티티로 설정해야 한다.
주인이 아닌 엔티티는 읽기 전용으로 동작하게 된다.
1 : 1 관계는 서로가 하나만을 참조해야 진정한 일대일 관계지만, @OneToOne으로 명시를 하더라도 동작 자체는 @ManyToOne과 비슷하게 동작한다.
따라서 확실하게 일대일 관계를 보장하기 위해 unique=true를 설정해 주어야 한다.
따라서 일대일 연관관계를 다대일로 변경하고 싶으면 unique 설정만 지워주면 된다.
1:1 관계에서는 어떤 엔티티가 주인이 되어야 할까?
- 자주 조회하거나 수정되는 엔티티
- 생명주기 관리 기준 (ex. profile이 항상 user에 종속된다면 user가 주인)
- DB 설계에 따라 (이미 DB 설계가 어느 테이블에 줄지 결정되어 있다면 거기에 맞춰서 결정)
외래 키 위치의 장단점 정리
1. 주 테이블에 외래 키 위치
장점
- JPA로 객체 지향적 개발이 가능
- 주 테이블만 조회해도 대상 테이블 조회 가능
단점
- 대상 테이블에 값이 없다면 null 허용 (무결성 보장 X)
- 대상 테이블의 데이터를 참조하기 때문에, 삭제될 때 외래 키 값을 처리하는 관리 필요
2. 대상 테이블에 외래 키 위치
장점
- 데이터베이스 무결성 보장
- 주 테이블과 대상 테이블 연관관계가 변경되어도 테이블 구조 유지
단점
- 조회 성능 떨어짐
- 연관관계 매칭 설정 복잡
- 지연 로딩으로 설정해도 즉시 로딩된다. (외래키가 대상테이블에 있으면 추후 쿼리로 대상 테이블을 찾아내기 번거롭기 때문)
N : M 단방향
@Entity
@Table(name = "language")
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
@Table(name = "tutor")
public class Tutor {
...
@ManyToMany
@JoinTable( // 중간 테이블 생성
name = "tutor_language", // 중간 테이블 이름 설정
joinColumns = @JoinColumn(name = "tutor_id"),
inverseJoinColumns = @JoinColumn(name = "language_id") // 중간테이블을 연결할 상대 엔티티 명시
)
private List<Language> languages = new ArrayList<>();
...
}
N : M 양방향
@Entity
@Table(name = "language")
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "languages")
private List<Tutor> tutors = new ArrayList<>();
}
N : M 매핑의 문제점
- level, license와 같은 중간테이블에 추가적인 데이터가 필요하다.
- 중간테이블이 내부적으로 생성되기 때문에 생각하지 못한 SQL Query가 실행된다.
- 중간 테이블을 실제 엔티티로 만들어서 @ManyToOne, @OneToMany로 관리한다.
- 두 테이블의 id를 복합키로 PK가 생성된다.
- PK가 종속적이면 사이드 이펙트가 발생하므로, 비즈니스적으로 의미없는 Long값으로 PK를 설정하는 게 유연성에 좋다.
N : M 매핑 - 중간테이블 생성
중간 테이블 엔티티를 직접 생성하여 N : M 매핑의 문제점을 해결한다.
@Entity
@Table(name = "tutor_language")
public class TutorLanguage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "tutor_id")
private Tutor tutor;
@ManyToOne
@JoinColumn(name = "language_id")
private Language language;
private Integer level;
private String license;
}
@Entity
@Table(name = "language")
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "language")
private List<TutorLanguage> tutorLanguages = new ArrayList<>();
}
@Entity
@Table(name = "tutor")
public class Tutor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Company company;
@OneToOne
@JoinColumn(name = "address_id", unique = true)
private Address address;
@OneToMany(mappedBy = "tutor")
private List<TutorLanguage> tutorLanguages = new ArrayList<>();
public Tutor() {
}
public Tutor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
'언어, 프레임워크 > Spring' 카테고리의 다른 글
cascade와 orphanRemoval 비교하기 (1) | 2025.04.17 |
---|---|
JPA 테이블 전략 (2) | 2025.04.17 |
JPA 성능 최적화 (fetch join, batch size) (0) | 2025.04.16 |
Converter, Formatter (0) | 2025.04.15 |
영속성 컨텍스트에서의 insert 시점 실습 (0) | 2025.04.06 |