Home > Troubleshooting > πŸ”[Troubleshooting] πŸš€ Not-Null μ œμ•½ 쑰건 μœ„λ°˜

πŸ”[Troubleshooting] πŸš€ Not-Null μ œμ•½ 쑰건 μœ„λ°˜
Troubleshooting Backend Development Spring Boot

πŸš€ Not-Null μ œμ•½ 쑰건 μœ„λ°˜!

🚨 μ—λŸ¬ λ©”μ‹œμ§€

org.hibernate.PropertyValueException: 
not-null property references a null or transient value: 
com.kobe.schoolmanagement.domain.entity.Student.major

μ΅œμ’… μ˜ˆμ™Έ

DataIntegrityViolationException

πŸ” 핡심 원인

Student μ—”ν‹°ν‹°μ˜ major ν•„λ“œκ°€ null인 μƒνƒœλ‘œ μ €μž₯을 μ‹œλ„ν–ˆκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

μ œμ•½ 쑰건 확인

@Entity
public class Student {
    // ...
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(nullable = false, name = "major_id")  // ❌ null ν—ˆμš© μ•ˆ 함
    private Major major;
}

nullable = false μ„€μ •μœΌλ‘œ 인해 major ν•„λ“œλŠ” λ°˜λ“œμ‹œ 값이 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.


πŸ“Š 문제 λ°œμƒ 흐름

ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­
    ↓
{"name": "κ°•λ―Όμ„±", "admissionYear": 2010, "majorName": "Computer"}
    ↓
StudentRequestDto μˆ˜μ‹ 
    ↓
StudentService.createStudent() 호좜
    ↓
requestDto.toEntity(studentId) μ‹€ν–‰
    ↓
Student μ—”ν‹°ν‹° 생성
    β”œβ”€β”€ name: "κ°•λ―Όμ„±" βœ…
    β”œβ”€β”€ admissionYear: 2010 βœ…
    └── major: null ❌  (Major μ—”ν‹°ν‹°λ₯Ό μ‘°νšŒν•˜μ§€ μ•ŠμŒ!)
    ↓
studentRepository.save(student)
    ↓
Hibernateκ°€ λ°μ΄ν„°λ² μ΄μŠ€μ— μ €μž₯ μ‹œλ„
    ↓
NOT NULL μ œμ•½ 쑰건 μœ„λ°˜ 감지
    ↓
PropertyValueException λ°œμƒ
    ↓
DataIntegrityViolationException

πŸ’‘ 문제 상황 뢄석

λˆ„λ½λœ 둜직

단계 ν˜„μž¬ 상황 ν•„μš”ν•œ μž‘μ—…
1 majorName λ¬Έμžμ—΄ μˆ˜μ‹  βœ… μ™„λ£Œ
2 majorName으둜 Major μ—”ν‹°ν‹° 쑰회 ❌ λˆ„λ½λ¨
3 μ‘°νšŒν•œ Major 객체λ₯Ό Student에 μ„€μ • ❌ λˆ„λ½λ¨
4 Student μ €μž₯ βœ… μ‹œλ„ν–ˆμœΌλ‚˜ μ‹€νŒ¨

κ²°κ³Ό

majorName: "Computer" (String)
    ↓
❌ Major μ—”ν‹°ν‹° 쑰회 둜직 μ—†μŒ
    ↓
major ν•„λ“œ: null
    ↓
μ €μž₯ μ‹€νŒ¨!

βœ… ν•΄κ²° λ°©μ•ˆ

3λ‹¨κ³„λ‘œ 문제λ₯Ό ν•΄κ²°ν•©λ‹ˆλ‹€.


Step 1: MajorRepository λ©”μ„œλ“œ μΆ”κ°€

Major μ—”ν‹°ν‹°λ₯Ό 전곡 μ΄λ¦„μœΌλ‘œ μ‘°νšŒν•  수 μžˆλŠ” λ©”μ„œλ“œλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

package com.kobe.schoolmanagement.repository;

import com.kobe.schoolmanagement.domain.entity.Major;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface MajorRepository extends JpaRepository<Major, Long> {
    
    Optional<Major> findByMajorNumber(String majorNumber);
    
    // βœ… 전곡 μ΄λ¦„μœΌλ‘œ μ‘°νšŒν•˜λŠ” λ©”μ„œλ“œ μΆ”κ°€
    Optional<Major> findByName(String name);
}

πŸ’‘ Spring Data JPA 쿼리 λ©”μ„œλ“œ

findByName("Computer")
// ↓ μžλ™μœΌλ‘œ λ‹€μŒ 쿼리 생성
// SELECT * FROM major WHERE name = 'Computer'

Step 2: StudentService 둜직 μˆ˜μ •

Major μ—”ν‹°ν‹°λ₯Ό μ‘°νšŒν•˜κ³  Student에 μ„€μ •ν•˜λŠ” λ‘œμ§μ„ μΆ”κ°€ν•©λ‹ˆλ‹€.

Before

@Transactional
public StudentResponseDto createStudent(StudentRequestDto requestDto) {
    int admissionYear = requestDto.getAdmissionYear();
    String majorName = requestDto.getMajorName();
    
    // ❌ Major μ—”ν‹°ν‹° 쑰회 μ—†μŒ
    
    long sequence = studentRepository.countByAdmissionYearAndMajorName(
        admissionYear, majorName) + 1;
    
    String studentId = createStudentNumber.generate(
        admissionYear, majorName, sequence);
    
    // ❌ majorκ°€ null인 μƒνƒœλ‘œ μ—”ν‹°ν‹° 생성
    Student student = requestDto.toEntity(studentId);
    Student savedStudent = studentRepository.save(student);
    
    return StudentResponseDto.fromEntity(savedStudent);
}

After

package com.kobe.schoolmanagement.service;

import com.kobe.schoolmanagement.common.CreateStudentNumber;
import com.kobe.schoolmanagement.domain.entity.Major;
import com.kobe.schoolmanagement.domain.entity.Student;
import com.kobe.schoolmanagement.dto.request.StudentRequestDto;
import com.kobe.schoolmanagement.dto.response.StudentResponseDto;
import com.kobe.schoolmanagement.repository.MajorRepository;
import com.kobe.schoolmanagement.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class StudentService {
    
    private final StudentRepository studentRepository;
    private final MajorRepository majorRepository;  // βœ… μ˜μ‘΄μ„± μ£Όμž…
    private final CreateStudentNumber createStudentNumber;

    @Transactional
    public StudentResponseDto createStudent(StudentRequestDto requestDto) {
        // 1. DTOλ‘œλΆ€ν„° ν•„μš”ν•œ 정보 μΆ”μΆœ
        int admissionYear = requestDto.getAdmissionYear();
        String majorName = requestDto.getMajorName();

        // βœ… 2. Major μ—”ν‹°ν‹° 쑰회 (핡심 μˆ˜μ •)
        Major major = majorRepository.findByName(majorName)
                .orElseThrow(() -> new IllegalArgumentException(
                    "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ „κ³΅μž…λ‹ˆλ‹€: " + majorName));

        // 3. 학생 수 카운트
        long sequence = studentRepository.countByAdmissionYearAndMajorName(
            admissionYear, majorName) + 1;

        // 4. ν•™λ²ˆ 생성
        String studentId = createStudentNumber.generate(
            admissionYear, majorName, sequence);

        // 5. ν•™λ²ˆ 쀑볡 검사
        studentRepository.findByStudentId(studentId).ifPresent(s -> {
            throw new IllegalStateException("ν•™λ²ˆ 생성 좩돌 λ°œμƒ. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”");
        });

        // βœ… 6. major 객체λ₯Ό ν•¨κ»˜ μ „λ‹¬ν•˜μ—¬ μ—”ν‹°ν‹° 생성
        Student student = requestDto.toEntity(studentId, major);
        Student savedStudent = studentRepository.save(student);

        return StudentResponseDto.fromEntity(savedStudent);
    }

    @Transactional(readOnly = true)
    public StudentResponseDto getStudent(String studentId) {
        Student student = studentRepository.findByStudentId(studentId)
            .orElseThrow(() -> new IllegalArgumentException(
                "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν•™μƒμž…λ‹ˆλ‹€."));

        return StudentResponseDto.fromEntity(student);
    }
}

πŸ’‘ μ£Όμš” 변경사항

// 1. MajorRepository μ£Όμž… μΆ”κ°€
private final MajorRepository majorRepository;

// 2. Major μ—”ν‹°ν‹° 쑰회
Major major = majorRepository.findByName(majorName)
    .orElseThrow(() -> new IllegalArgumentException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ „κ³΅μž…λ‹ˆλ‹€"));

// 3. toEntity()에 major 전달
Student student = requestDto.toEntity(studentId, major);

Step 3: StudentRequestDto.toEntity() λ©”μ„œλ“œ μˆ˜μ •

toEntity λ©”μ„œλ“œκ°€ Major 객체λ₯Ό λ°›μ•„μ„œ μ„€μ •ν•˜λ„λ‘ μˆ˜μ •ν•©λ‹ˆλ‹€.

Before

public class StudentRequestDto {
    private String name;
    private int admissionYear;
    private String majorName;
    
    public Student toEntity(String studentId) {
        return Student.builder()
                .name(this.name)
                .studentId(studentId)
                .admissionYear(this.admissionYear)
                // ❌ major ν•„λ“œ μ„€μ • μ—†μŒ (null둜 λ‚¨μŒ)
                .build();
    }
}

After

public class StudentRequestDto {
    private String name;
    private int admissionYear;
    private String majorName;
    
    // βœ… Major 객체λ₯Ό νŒŒλΌλ―Έν„°λ‘œ 받도둝 μˆ˜μ •
    public Student toEntity(String studentId, Major major) {
        return Student.builder()
                .name(this.name)
                .studentId(studentId)
                .admissionYear(this.admissionYear)
                .major(major)  // βœ… Major 객체 μ„€μ •
                .build();
    }
}

πŸ”„ μˆ˜μ • ν›„ 데이터 흐름

ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­
    ↓
{"name": "κ°•λ―Όμ„±", "admissionYear": 2010, "majorName": "Computer"}
    ↓
StudentService.createStudent()
    ↓
majorRepository.findByName("Computer")  βœ… 좔가됨
    ↓
Major μ—”ν‹°ν‹° 쑰회 성곡
    └── id: 1
    └── majorNumber: "31513162120518"
    └── name: "Computer"
    ↓
requestDto.toEntity(studentId, major)  βœ… major 전달
    ↓
Student μ—”ν‹°ν‹° 생성
    β”œβ”€β”€ name: "κ°•λ―Όμ„±" βœ…
    β”œβ”€β”€ admissionYear: 2010 βœ…
    └── major: Major 객체 βœ… (null μ•„λ‹˜!)
    ↓
studentRepository.save(student)
    ↓
μ €μž₯ 성곡! βœ…

πŸ“‹ 전체 μˆ˜μ • μš”μ•½

파일 μˆ˜μ • λ‚΄μš© λͺ©μ 
MajorRepository findByName() λ©”μ„œλ“œ μΆ”κ°€ 전곡 μ΄λ¦„μœΌλ‘œ 쑰회
StudentService majorRepository μ£Όμž…
Major 쑰회 둜직 μΆ”κ°€
toEntity()에 major 전달
Major μ—”ν‹°ν‹° 쑰회 및 μ„€μ •
StudentRequestDto toEntity() μ‹œκ·Έλ‹ˆμ²˜ λ³€κ²½
major ν•„λ“œ μ„€μ • μΆ”κ°€
Major 객체λ₯Ό λ°›μ•„μ„œ μ„€μ •

βœ… ν…ŒμŠ€νŠΈ

Postman μš”μ²­

POST http://localhost:8080/api/v1/students

{
    "name": "κ°•λ―Όμ„±",
    "admissionYear": 2010,
    "majorName": "Computer"
}

μ˜ˆμƒ 응닡

{
    "name": "κ°•λ―Όμ„±",
    "admissionYear": 2010,
    "major_info": {
        "id": 1,
        "majorNumber": "31513162120518",
        "name": "Computer"
    }
}

λ°μ΄ν„°λ² μ΄μŠ€ 확인

-- Student ν…Œμ΄λΈ”
SELECT * FROM student;

| id | name | student_id | admission_year | major_id |
|β€”-|β€”β€”|β€”β€”β€”β€”|β€”β€”β€”β€”β€”-|β€”β€”β€”-|
| 1 | κ°•λ―Όμ„± | 1003001 | 2010 | 1 βœ… |


⚠️ μ£Όμ˜μ‚¬ν•­

1. μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 전곡λͺ… 처리

Major major = majorRepository.findByName(majorName)
    .orElseThrow(() -> new IllegalArgumentException(
        "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ „κ³΅μž…λ‹ˆλ‹€: " + majorName));
  • μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” majorName을 λ°›μœΌλ©΄ μ˜ˆμ™Έ λ°œμƒ
  • 사전에 Major 데이터가 DB에 μžˆμ–΄μ•Ό 함

2. Major 데이터 사전 등둝

// Major μ—”ν‹°ν‹°λ₯Ό 미리 μ €μž₯ν•΄μ•Ό 함
Major computerMajor = Major.builder()
    .majorNumber("31513162120518")
    .name("Computer")
    .build();
majorRepository.save(computerMajor);

πŸ“Œ Best Practices

1. DTOμ—μ„œ μ—”ν‹°ν‹° μ°Έμ‘° μ„€μ •

// ❌ λ‚˜μœ 예: DTOμ—μ„œ 직접 쑰회
public Student toEntity(String studentId) {
    Major major = majorRepository.findByName(this.majorName).orElseThrow();
    // DTOκ°€ Repository에 μ˜μ‘΄ν•˜κ²Œ 됨!
}

// βœ… 쒋은 예: νŒŒλΌλ―Έν„°λ‘œ λ°›κΈ°
public Student toEntity(String studentId, Major major) {
    // μ˜μ‘΄μ„±μ΄ λͺ…ν™•ν•˜κ³  ν…ŒμŠ€νŠΈν•˜κΈ° 쉬움
}

2. μ˜ˆμ™Έ λ©”μ‹œμ§€μ— μ»¨ν…μŠ€νŠΈ 포함

// ❌ λ‚˜μœ 예
throw new IllegalArgumentException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ „κ³΅μž…λ‹ˆλ‹€.");

// βœ… 쒋은 예
throw new IllegalArgumentException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ „κ³΅μž…λ‹ˆλ‹€: " + majorName);

3. μ—”ν‹°ν‹° μ œμ•½ 쑰건 확인

@JoinColumn(nullable = false, name = "major_id")
// └─ nullable = false μ„€μ • 확인
//    ν•„μˆ˜ ν•„λ“œλŠ” λ°˜λ“œμ‹œ 값을 μ„€μ •ν•΄μ•Ό 함

πŸ”§ νŠΈλŸ¬λΈ”μŠˆνŒ…

Q1. β€œμ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ „κ³΅μž…λ‹ˆλ‹€β€ μ˜ˆμ™Έκ°€ 계속 λ°œμƒν•΄μš”

원인: Major 데이터가 DB에 μ—†μŒ

ν•΄κ²°:

-- Major 데이터 확인
SELECT * FROM major WHERE name = 'Computer';

-- 데이터가 μ—†λ‹€λ©΄ μ‚½μž…
INSERT INTO major (major_number, name) 
VALUES ('31513162120518', 'Computer');

Q2. major_idκ°€ null둜 μ €μž₯λ˜μ–΄μš”

원인: Student.builder()μ—μ„œ major() λ©”μ„œλ“œ 호좜 λˆ„λ½

ν•΄κ²°:

Student.builder()
    .name(this.name)
    .studentId(studentId)
    .admissionYear(this.admissionYear)
    .major(major)  // βœ… 이 쀄 μΆ”κ°€ 확인
    .build();

Q3. LazyInitializationException이 λ°œμƒν•΄μš”

원인: FetchType.LAZY둜 μ„€μ •λœ majorλ₯Ό νŠΈλžœμž­μ…˜ λ°–μ—μ„œ μ ‘κ·Ό

ν•΄κ²°:

@Transactional(readOnly = true)  // βœ… νŠΈλžœμž­μ…˜ λ²”μœ„ 확인
public StudentResponseDto getStudent(String studentId) {
    Student student = studentRepository.findByStudentId(studentId)
        .orElseThrow();
    return StudentResponseDto.fromEntity(student);  // νŠΈλžœμž­μ…˜ λ‚΄μ—μ„œ λ³€ν™˜
}