정리 요약
- 관계
- 다대일(N : 1) @ManyToOne
- 일대다( 1 : N) @OneToMany
- 일대일( 1 : 1) @OneToOne
- 다대다(N : N) @ManyToMany
- 방향성
- 테이블
- 외래 키로 양쪽 조인이 가능
- 방향이라는 개념이 없음
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 단방향: 한 쪽만 참조
- 양방향: 양쪽이 단방향으 서로 참조
- 테이블
- 연관관계의 주인
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 찾음
- 객체 양방향 관계는 양쪽이 모두 참조를 하므로 참조가 2군데 이다
- 연관관계의 주인: 외래 키를 관리하는 참조
데이터베이스와 객체의 패러다임 차이
JPA는 객체와 관계형 데이터베이스 간의 매핑을 지원해 주는 기술로, 객체 지향적인 프로그래밍으로 데이터베이스를 조작할 수 있게 해 줍니다. 객체와 관계형 데이터베이스 간의 매핑 시 데이터 매핑(필드와 컬럼) 매핑도 있지만, 객체 연관관계와 테이블 간의 연관관계도 매핑을 처리해주어야 합니다.
기본적으로 객체는 단방향 연관관계를 갖고, 관계형 데이터베이스 테이블은 양방향 연관관계를 갖습니다.
회사와 회사원을 예로 들어보겠습니다.
객체:
public class Employee {
private Long id;
private String name;
private Company company;
}
public class Company {
private Long id;
private String name
}
Employee는 Company를 참조하지만 그 반대인 경우는 없기에 단방향 연관관계입니다. Company 클래스에 필드를 추가하면 양방향이 아니냐는 생각을 할 수도 있지만, 이 경우 양방향 연관관계가 아니라 단방향 연관관계가 2개 될 뿐입니다.
관계형 데이터베이스:
아래와 같이 Employee 테이블은 COMPANY_ID를 외래 키로 Company 테이블은 COMPANY_ID를 기본 키로 테이블 간 양방향 연관관계를 맺을걸 확인할 수 있습니다.
<EMPLOYEE> <COMPANY>
-ID (PK) -COMPANY_ID (PK)
-NAME -NAME
-COMPANY_ID (FK)
객체는 참조로 연관 객체를 찾고 단반향인 반면, 관계형 데이터베이스는 외래 키로 조인해서 찾고 양방향 관계입니다. 객체의 경우 Company이라는 데이터 타입으로 찾지만 관계형 데이터베이스의 경우 COMPANY_ID라는 값을 외래 키로 설정하기 때문에 패러다임 차이가 생깁니다.
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
private Long companyId;
}
위와 같이 구성하게 된다면 관계형 DB 컬럼에 맞췄기 때문에 매핑은 될지 몰라도 객체 지향적이지 않습니다.
(객체 지향적이기 위해선 Company 타입이 담겨야 합니다.)
객체 간의 연관관계와 관계형 데이터베이스 간의 연관관계를 매핑하는 것을 JPA는 어떻게 처리하는지 확인해보도록 하겠습니다.
객체 연관관계 매핑
- @ManyToOne: 다대일(N:1) 관계라는 매핑 정보
- @JoinColumn: 외래 키를 매핑
두 어노테이션을 이용하여 관계형 데이터베이스 연관관계 매핑도 가능하게 하면서 객체 지향적인 코드로 바꿔준 코드입니다. 관계형 데이터베이스처럼 외래 키를 필드로 지정하지 않아도 객체(여기선 Company)를 지정하면 JPA가 알아서 해당 객체 테이블의 pk값을 외래 키로 지정해줍니다. 만약 다른 값으로 외래 키를 지정해주고 싶은 경우 `@JoinColumn`의 referencedColumnName 속성을 통해 해지정해주면 됩니다. `@JoinColumn`의 name 속성은 어떤 이름으로 컬럼명을 설정할 것인지 지정하는 속성입니다.
@Entity
public class Employee {
// ...
@ManyToOne
@JoinColumn(name = "COMPANY_ID")
private Company company;
}
@Entity
public class Company {
@Id
@GeneratedValue
@Column(name = "COMPANY_ID")
private Long id;
// ...
}
양방향 연관관계
데이터베이스 테이블은 외래 키로 서로의 테이블을 양방향으로 조회할 수 있습니다. 하지만 객체는 양쪽에 서로의 객체를 넣어주어야 서로 조회가 가능합니다. 앞서 보여준 예제 코드도 Employee에서만 Company에 접근할 수 있는 단방향 연관관계입니다. 양방향 연관관계도 어려울거 없이 객체 방식대로 양쪽에 서로의 객체를 넣어주고 서로 다른 단방향 연관관계 2개를 만들어주면 됩니다. 이때 단방향 연관관계 2개를 만들어 양방향 연관관계 처럼 보이게 한 것이기 때문에 연관관계 주인을 선정해주어야 합니다.
연관관계 주인이란?
☞ 외래 키 관리자를 선택한다.
양방향 연관관계에서 데이터베이스의 외래 키를 관리하고 조작하는 주체입니다. 객체 지향 관점에서 양방향 연관관계는 두 엔티티가 서로를 참조하기 때문에 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해줍니다. 이를 위해 연관관계의 주인을 지정해주어야 합니다.
@Entity public class Parent { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "parent") private List<Child> children = new ArrayList<>(); // ... }
연관관계 주인 지정:
연관관계 주인이 아닌 엔티티에다가, 어디에 매핑 되었는지에 대한 정보를 표시하는 mappedBy 속성을 사용해주면 됩니다. mappedBy는 읽기 전용으로, 참조만 가능합니다.
-연관관계의 주인은 데이터베이스 연관관계와 매핑됩니다.
-연관관계의 주인은 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다.
-연관관계의 주인이 아닌 경우 읽기만 가능합니다.
parent1.getChildren().add(child1); // 무시 parent1.getChildren().add(child2); // 무시 child1.setParent(parent1); // 연관관계 설정(연관관계의 주인) child2.setParent(parent2); // 연관관계 설정(연관관계의 주인)
연관관계 주인 선정 기준:
- 외래 키를 관리하고 싶은 엔티티를 연관관계 주인으로 지정
- 주로 외래 키를 가지고 있는 쪽이 연관관계 주인 (`Many` 쪽)
- 반대쪽 매핑의 필드 이름을 속성으로 지정하면 됩니다.
연관관계 유형
다대일(@ManyToOne):
여러 엔티티가 한 엔티티를 참조하는 관계입니다. 직원(Employee) 엔티티와 회사(Company) 엔티티의 관계를 예로 들 수 있습니다. 관계형 데이터베이스 설계를 생각해보면 직원('다') 쪽에 외래 키가 존재 해야 합니다. 즉 다대인 관계에서 '다' 쪽에서 연관관계 주인이되어 외래 키를 관리합니다.
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "company_id")
private Company_id company_id;
// ...
}
일대다(@OneToMany):
한 엔티티가 다른 엔티티를 여러 개 가질 수 있는 관계.
회사(Company) 엔티티와 직원(Employee) 엔티티 간의 관계를 예로 들 수 있습니다. 엔티티를 하나 이상 참조할 수 있기 때문에 자바 컬렉션인 List, Set, Map 등을 사용합니다.
공식적으로 존재하지 않는 매핑입니다. 다대일(N:1) 매핑 상태에서 양방향 매핑이 필요한 경우에만 추가해서 사용하고, 단독으로는 사용되지 않습니다.
@Entity
public class Company {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "company")
private List<Employee> employees = new ArrayList<>();
// ...
}
일대일(@OneToOne):
한 엔티티가 다른 한 엔티티와 일대일 관계를 가집니다. 예를 들어 직원(Employee) 엔티티와 이력서(Resume) 엔티티의 관계를 예로 들 수 있습니다.
다대다(@ManyToMany):
여러 엔티티가 다른 엔티티와 서로 다대다 관계를 가지는것을 말합니다. 학생(Student) 엔티티와 과목(Subject) 엔티티 간의 관계를 예로 들 수 있습니다.
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_subject",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "subject_id")
)
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<>();
// ...
}
다대다(@ManyToMany)는 사용하지 않습니다. @ManyToMany 어노테이션을 사용할 때 `@JoinTable` 어노테이션을 같이 사용하여 두 엔티티 간의 관계를 논리적으로 연결하기위해 중간 테이블을 자동으로 생성합니다. 추가 정보를 저장하거나 관계를 연결하고 나타내는 역할을 하지만, 이로인해 테이블의 데이터 중복이 발생할수 있습니다. 또한 중간 테이블에 대한 관리가 어려울 수 있어, 엔티티 간의 관계에 대한 유지보수가 복잡해집니다.
`다대다` 관계를 지양하고 `일대다`,`다대일` 관계로 변환하여 사용하는 것을 권장합니다. @ManyToMany를 사용할 경우 생기는 중간 테이블 대신, 아예 (중간) 엔티티를 생성하여 관계를 유연하게 처리할 수 있습니다.
이때 만든 중간 엔티티는 복합 키를 사용하기 때문에 엔티티에 @IdClass 어노테이션을 시용하여 식별자 클래스로 만들었습니다.
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "student")
private List<StudentSubject> studentSubjects = new ArrayList<>();
// ...
}
@Entity
public class Subject {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "subject")
private List<StudentSubject> studentSubjects = new ArrayList<>();
// ...
}
@Entity
@IdClass(StudentSubjectId.class)
public class StudentSubject {
@Id
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@Id
@ManyToOne
@JoinColumn(name = "subject_id")
private Subject subject;
private int attendance;
// ...
}
public class StudentSubjectId implements Serializable {
private Long student;
private Long subject;
// ... equals method 필수 구현
// ... hashCode method 필수 구현
// ... Constructors
}
@IdClass 말고도 @EmbeddedId를 사용하여 복합 키를 구성하는 방법도 있습니다.
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "student")
private List<StudentSubject> studentSubjects = new ArrayList<>();
// ...
}
@Entity
public class Subject {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "subject")
private List<StudentSubject> studentSubjects = new ArrayList<>();
// ...
}
@Entity
public class StudentSubject {
@EmbeddedId
private StudentSubjectId id;
@ManyToOne
@MapsId("studentId")
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@MapsId("subjectId")
@JoinColumn(name = "subject_id")
private Subject subject;
private int attendance;
// ...
}
@Embeddable
public class StudentSubjectId implements Serializable {
private Long studentId;
private Long subjectId;
// ... equals method 필수 구현
// ... hashCode method 필수 구현
// ... Constructors
}
'Spring Data > JPA' 카테고리의 다른 글
JPA 프록시와 지연로딩: 성능 최적화를 위한 기술(방법) (0) | 2023.08.22 |
---|---|
JPA 상속 관계 매핑 방법과 공통 매핑하는 방법 (0) | 2023.08.21 |
JPA 생성 및 수정 날짜 자동 처리를 위한 공통 엔티티 만들기 @MappedSuperclass, @EnableJpaAuditing (0) | 2023.08.17 |
JPA ddl-auto: 데이터베이스 스키마 자동 생성 전략 (0) | 2023.08.17 |
JPA 엔티티 매핑: 객체와 데이터베이스 테이블의 매핑 (0) | 2023.08.17 |