Home > Troubleshooting > πŸ”[Troubleshooting] πŸš€ μ—”ν‹°ν‹° 관계 μ„€μ •

πŸ”[Troubleshooting] πŸš€ μ—”ν‹°ν‹° 관계 μ„€μ •
Troubleshooting Backend Development Spring Boot

πŸš€ μ—”ν‹°ν‹° 관계 μ„€μ •!

🎯 핡심 κ²°λ‘ 

@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;
}

단방ν–₯을 기본으둜 ν•˜κ³ , 정말 ν•„μš”ν•œ κ²½μš°μ—λ§Œ μ–‘λ°©ν–₯ 관계λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.