π μν°ν° κ΄κ³ μ€μ !
π― ν΅μ¬ κ²°λ‘
@ManyToOne
κ΄κ³κ° μ¬λ°λ₯Έ μ€κ³μ
λλ€.
@OneToOne
μ μ΄ μν©μ μ ν©νμ§ μμΌλ©°, μ¬λ¬ νμμ΄ νλμ μ 곡μ μν μ μλ @ManyToOne
κ΄κ³λ₯Ό μ¬μ©ν΄μΌ ν©λλ€.
π κ΄κ³ νμ λΉκ΅
@OneToOne (μΌλμΌ) - β λΆμ μ
νμ ββ μ 곡
(1:1 κ΄κ³)
μμ:
[κ°λ―Όμ±] ββ [μ»΄ν¨ν°κ³΅νκ³Ό]
β
λ¬Έμ : μ»΄ν¨ν°κ³΅νκ³Όμ κ°λ―Όμ± ν λͺ
λ§ μν μ μμ
λ€λ₯Έ νμμ μ»΄ν¨ν°κ³΅νκ³Όλ₯Ό μ νν μ μμ!
νΉμ§
- νλμ νμ β νλμ μ 곡
- νλμ μ 곡 β νλμ νμλ§ κ°λ₯
- νμ€ μΈκ³μ λ§μ§ μμ
@ManyToOne (λ€λμΌ) - β μ μ
μ¬λ¬ νμ β νλμ μ 곡
(N:1 κ΄κ³)
μμ:
[κ°λ―Όμ±] βββ
[κΉμ² μ] βββΌβ [μ»΄ν¨ν°κ³΅νκ³Ό]
[μ΄μν¬] βββ
κ° νμμ νλμ μ 곡μ μν¨
νλμ μ 곡μ μ¬λ¬ νμμ΄ μν μ μμ
νΉμ§
- μ¬λ¬ νμ(
Many
) β νλμ μ 곡(One
) - κ° νμμ νλμ μ κ³΅λ§ λ³΄μ
- νμ€ μΈκ³ λΉμ¦λμ€ λ‘μ§κ³Ό μΌμΉ
π κ΄κ³ λΉκ΅ν
κ΅¬λΆ | @OneToOne | @ManyToOne |
---|---|---|
κ΄κ³ | 1:1 | N:1 |
μ κ³΅λΉ νμ μ | 1λͺ | μ¬λ¬ λͺ |
νμλΉ μ 곡 μ | 1κ° | 1κ° |
νμ€μ± | β λΉνμ€μ | β νμ€μ |
μ¬μ© μμ | μ£Όλ―Όλ±λ‘λ²νΈ, μ¬κΆλ²νΈ | νμ-μ 곡, μ£Όλ¬Έ-κ³ κ° |
π¨ μ¬λ°λ₯Έ ꡬν λ°©λ²
Step 1: Student μν°ν° μμ
κΈ°μ‘΄ String major
νλλ₯Ό Major
μν°ν° μ°Έμ‘°λ‘ λ³κ²½ν©λλ€.
Before
@Entity
public class Student {
// ...
private String major; // β λ¨μ λ¬Έμμ΄
}
After
package com.kobe.schoolmanagement.domain.entity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String studentId;
private int admissionYear;
// β
@ManyToOne κ΄κ³ μ€μ
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "major_id") // DB μΈλν€ μ»¬λΌλͺ
private Major major;
@Builder
public Student(String name, String studentId, int admissionYear, Major major) {
this.name = name;
this.studentId = studentId;
this.admissionYear = admissionYear;
this.major = major;
}
}
π‘ ν΅μ¬ μ΄λ Έν μ΄μ μ€λͺ
@ManyToOne(fetch = FetchType.LAZY)
// ββ FetchType.LAZY: νμν λλ§ Major μ 보λ₯Ό λ‘λ© (μ±λ₯ μ΅μ ν)
// FetchType.EAGER: μ¦μ λ‘λ© (N+1 λ¬Έμ λ°μ κ°λ₯)
@JoinColumn(name = "major_id")
// ββ μΈλν€ μ»¬λΌλͺ
μ "major_id"λ‘ μ§μ
// λ°μ΄ν°λ² μ΄μ€ ν
μ΄λΈμμ major_id 컬λΌμ΄ μμ±λ¨
Step 2: Major μν°ν° (μ°Έκ³ )
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Major {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String majorNumber;
private String name;
// μλ°©ν₯ κ΄κ³κ° νμν κ²½μ° (μ νμ¬ν)
// @OneToMany(mappedBy = "major")
// private List<Student> students = new ArrayList<>();
@Builder
public Major(String majorNumber, String name) {
this.majorNumber = majorNumber;
this.name = name;
}
}
Step 3: DTOλ₯Ό ν΅ν μλ΅ κ΅¬μ±
μν°ν°λ₯Ό API μλ΅μΌλ‘ μ§μ λ ΈμΆνλ κ²μ μννλ―λ‘, DTO λ³ν ν¨ν΄μ μ¬μ©ν©λλ€.
package com.kobe.schoolmanagement.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.kobe.schoolmanagement.domain.entity.Student;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class StudentResponseDto {
private String name;
private int admissionYear;
@JsonProperty("major_info")
private MajorInfoDto majorInfo;
/**
* Student μν°ν°λ₯Ό StudentResponseDtoλ‘ λ³ν
* Major μν°ν°λ MajorInfoDtoλ‘ λ³ννμ¬ μ€μ²© ꡬ쑰 μμ±
*/
public static StudentResponseDto fromEntity(Student student) {
return StudentResponseDto.builder()
.name(student.getName())
.admissionYear(student.getAdmissionYear())
// β
@ManyToOneμΌλ‘ μ€μ λ major νλ νμ©
.majorInfo(MajorInfoDto.fromEntity(student.getMajor()))
.build();
}
}
ποΈ λ°μ΄ν°λ² μ΄μ€ ꡬ쑰
μμ±λλ ν μ΄λΈ ꡬ쑰
Student ν μ΄λΈ
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
student_id VARCHAR(255),
admission_year INT,
major_id BIGINT, -- β
μΈλν€
FOREIGN KEY (major_id) REFERENCES major(id)
);
Major ν μ΄λΈ
CREATE TABLE major (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
major_number VARCHAR(255),
name VARCHAR(255)
);
λ°μ΄ν° μμ
Major ν
μ΄λΈ
| id | major_number | name |
|β-|βββββ|ββ|
| 1 | 31513162120518 | Computer |
| 2 | 31513162120519 | Mathematics |
Student ν
μ΄λΈ
| id | name | student_id | admission_year | major_id |
|β-|ββ|ββββ|βββββ-|βββ-|
| 1 | κ°λ―Όμ± | 1003001 | 2010 | 1 |
| 2 | κΉμ² μ | 1003002 | 2010 | 1 |
| 3 | μ΄μν¬ | 1003003 | 2011 | 2 |
β major_idκ° 1μΈ νμμ΄ μ¬λ¬ λͺ (N:1 κ΄κ³ ꡬν)
β μμ κ²°κ³Ό
API μλ΅ μμ
{
"name": "κ°λ―Όμ±",
"admissionYear": 2010,
"major_info": {
"id": 1,
"majorNumber": "31513162120518",
"name": "Computer"
}
}
β οΈ μ£Όμμ¬ν
1. μν°ν° μ§μ λ ΈμΆ κΈμ§
// β λμ μμ
@GetMapping("/{id}")
public ResponseEntity<Student> getStudent(@PathVariable Long id) {
return ResponseEntity.ok(studentService.getStudent(id));
}
// β
μ’μ μμ
@GetMapping("/{id}")
public ResponseEntity<StudentResponseDto> getStudent(@PathVariable Long id) {
Student student = studentService.getStudent(id);
return ResponseEntity.ok(StudentResponseDto.fromEntity(student));
}
μν°ν° μ§μ λ ΈμΆμ λ¬Έμ μ
- μν μ°Έμ‘°: μλ°©ν₯ κ΄κ³ μ 무ν 루ν λ°μ
- 보μ: λ―Όκ°ν μ 보 λ ΈμΆ μν
- μ±λ₯: λΆνμν μ 보κΉμ§ λͺ¨λ μ μ‘
- μ μ°μ±: API μ€ν λ³κ²½μ΄ μν°ν° λ³κ²½μΌλ‘ μ΄μ΄μ§
2. FetchType μ ν
// β
κΆμ₯: LAZY (μ§μ° λ‘λ©)
@ManyToOne(fetch = FetchType.LAZY)
private Major major;
// νμν μμ μλ§ λ‘λ©
Student student = studentRepository.findById(1L);
// μ΄ μμ μλ major μ λ³΄κ° λ‘λ©λμ§ μμ (Proxy κ°μ²΄)
String majorName = student.getMajor().getName();
// μ΄ μμ μ major μ 보λ₯Ό DBμμ κ°μ Έμ΄
// β οΈ μ£Όμ: EAGER (μ¦μ λ‘λ©)
@ManyToOne(fetch = FetchType.EAGER)
private Major major;
// N+1 λ¬Έμ λ°μ κ°λ₯
// νμ 100λͺ
μ‘°ν μ β Major μ‘°ν 쿼리 100λ² μΆκ° μ€ν
π Best Practices
1. κ΄κ³ μ€μ μμΉ
| κ΄κ³ | μ¬μ© μκΈ° | μμ |
|ββ|βββ-|ββ|
| @OneToOne
| μ λ§ 1:1 κ΄κ³μΌ λλ§ | νμ-νμμμΈμ 보 |
| @ManyToOne
| N:1 κ΄κ³ (κ°μ₯ νν¨) | νμ-μ 곡, μ£Όλ¬Έ-κ³ κ° |
| @OneToMany
| 1:N κ΄κ³ (μλ°©ν₯ νμ μ) | μ 곡-νμ λͺ©λ‘ |
| @ManyToMany
| M:N κ΄κ³ (μ€κ° ν
μ΄λΈ νμ) | νμ-μκ°κ³Όλͺ© |
2. DTO λ³ν λ μ΄μ΄
Controller Layer
β (DTO)
Service Layer
β (Entity)
Repository Layer
β (DB)
3. λ¨λ°©ν₯ vs μλ°©ν₯
// λ¨λ°©ν₯ (κΆμ₯)
// Student β Major μ°Έμ‘°λ§ μ‘΄μ¬
@Entity
public class Student {
@ManyToOne
private Major major;
}
// μλ°©ν₯ (νμμμλ§)
// Student β Major μμͺ½ μ°Έμ‘°
@Entity
public class Major {
@OneToMany(mappedBy = "major")
private List<Student> students;
}
λ¨λ°©ν₯μ κΈ°λ³ΈμΌλ‘ νκ³ , μ λ§ νμν κ²½μ°μλ§ μλ°©ν₯ κ΄κ³λ₯Ό μΆκ°νμΈμ.