Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build/
!**/src/test/**/build/

### STS ###
.metadata/
.apt_generated
.classpath
.factorypath
Expand Down
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,83 @@

# JPA 실습 - 순환 참조 문제 해결하기

## 현재 발생하는 문제

### 1. 순환 참조 오류
```json
{
"timestamp": "2025-04-30T15:45:14.678+09:00",
"status": 500,
"error": "Internal Server Error",
"message": "Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]",
"path": "/api/students/1"
}
```

### 2. Hibernate 프록시 직렬화 오류
```
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor
and no properties discovered to create BeanSerializer
```

## 해결해야 할 과제

### 1. 엔티티 수정
- `Student`와 `ClassRoom` 엔티티의 양방향 연관관계 설정
- 순환 참조를 방지하기 위한 Jackson 어노테이션 추가
- 지연 로딩 관련 설정 최적화

### 2. 테스트 케이스 작성
```java
@Test
void testCircularReference() {
// 1. 반 생성
ClassRoom classRoom = new ClassRoom();
classRoom.setName("1반");
classRoom.setCapacity(30);
classRoom.setTeacherName("김선생");
ClassRoom savedClassRoom = classRoomRepository.save(classRoom);

// 2. 학생 생성
Student student = new Student();
student.setName("홍길동");
student.setAge(15);
student.setClassRoom(savedClassRoom);
Student savedStudent = studentRepository.save(student);

// 3. API 호출 테스트
// GET /api/students/{id} 호출 시 순환 참조 오류 발생 확인
// GET /api/classrooms/{id} 호출 시 순환 참조 오류 발생 확인
}
```

### 3. 해결 방안 고민
1. `@JsonIgnore` 사용
2. `@JsonManagedReference`와 `@JsonBackReference` 사용
3. DTO 패턴 적용
4. Jackson 설정 변경

## 학습 목표
1. JPA의 지연 로딩과 프록시 객체 이해
2. Jackson 직렬화 과정에서의 순환 참조 문제 이해
3. 다양한 해결 방안의 장단점 비교
4. REST API에서의 엔티티 직렬화 최적화

## 체크리스트
- [ ] 엔티티 수정
- [ ] 테스트 케이스 작성
- [ ] 해결 방안 구현
- [ ] API 테스트
- [ ] 문서화

## 참고 자료
- [Jackson 직렬화 문서](https://github.com/FasterXML/jackson-docs)
- [Hibernate 프록시 문서](https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#associations)
- [Spring Data JPA 문서](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)

이 브랜치에서 순환 참조 문제를 해결하면서 JPA와 Jackson의 동작 방식을 깊이 이해할 수 있습니다. 다양한 해결 방안을 시도해보고 각각의 장단점을 비교해보세요.
=======
# JPA 실습 스터디 안내

## 스터디 목적
Expand Down
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,15 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5'

// db
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.jpapractice.jpapractice;
package com.example.jpapractice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.example.jpapractice.controller;

import com.example.jpapractice.entity.ClassRoom;
import com.example.jpapractice.service.ClassRoomService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/classrooms")
@RequiredArgsConstructor
public class ClassRoomController {

private final ClassRoomService classRoomService;

/**
* 새로운 반을 생성합니다.
* @param classRoom 생성할 반 정보
* @return 생성된 반 정보
*/
@PostMapping
public ResponseEntity<ClassRoom> createClassRoom(@RequestBody ClassRoom classRoom) {
return ResponseEntity.ok(classRoomService.createClassRoom(classRoom));
}

/**
* 모든 반 정보를 조회합니다.
* @return 반 목록
*/
@GetMapping
public ResponseEntity<List<ClassRoom>> getAllClassRooms() {
return ResponseEntity.ok(classRoomService.getAllClassRooms());
}

/**
* ID로 반 정보를 조회합니다.
* @param id 반 ID
* @return 반 정보
*/
@GetMapping("/{id}")
public ResponseEntity<ClassRoom> getClassRoomById(@PathVariable Long id) {
return ResponseEntity.ok(classRoomService.getClassRoomById(id));
}

/**
* 반 정보를 수정합니다.
* @param id 수정할 반 ID
* @param updatedClassRoom 수정할 반 정보
* @return 수정된 반 정보
*/
@PutMapping("/{id}")
public ResponseEntity<ClassRoom> updateClassRoom(
@PathVariable Long id,
@RequestBody ClassRoom updatedClassRoom) {
return ResponseEntity.ok(classRoomService.updateClassRoom(id, updatedClassRoom));
}

/**
* 반을 삭제합니다.
* @param id 삭제할 반 ID
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteClassRoom(@PathVariable Long id) {
classRoomService.deleteClassRoom(id);
return ResponseEntity.ok().build();
}

/**
* 특정 정원 이상의 반을 조회합니다.
* @param minCapacity 최소 정원
* @return 반 목록
*/
@GetMapping("/large")
public ResponseEntity<List<ClassRoom>> findLargeClassRooms(
@RequestParam Integer minCapacity) {
return ResponseEntity.ok(classRoomService.findLargeClassRooms(minCapacity));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.example.jpapractice.controller;

import com.example.jpapractice.entity.Student;
import com.example.jpapractice.service.StudentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/students")
@RequiredArgsConstructor
public class StudentController {

private final StudentService studentService;

/**
* 새로운 학생을 생성합니다.
* @param student 생성할 학생 정보
* @return 생성된 학생 정보
*/
@PostMapping
public ResponseEntity<Student> createStudent(@RequestBody Student student) {
return ResponseEntity.ok(studentService.createStudent(student));
}

/**
* 모든 학생 정보를 조회합니다.
* @return 학생 목록
*/
@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {
return ResponseEntity.ok(studentService.getAllStudents());
}

/**
* ID로 학생 정보를 조회합니다.
* @param id 학생 ID
* @return 학생 정보
*/
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
return ResponseEntity.ok(studentService.getStudentById(id));
}

/**
* 학생 정보를 수정합니다.
* @param id 수정할 학생 ID
* @param updatedStudent 수정할 학생 정보
* @return 수정된 학생 정보
*/
@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(
@PathVariable Long id,
@RequestBody Student updatedStudent) {
return ResponseEntity.ok(studentService.updateStudent(id, updatedStudent));
}

/**
* 학생을 삭제합니다.
* @param id 삭제할 학생 ID
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteStudent(@PathVariable Long id) {
studentService.deleteStudent(id);
return ResponseEntity.ok().build();
}

/**
* 특정 반의 모든 학생을 조회합니다.
* @param classRoomId 반 ID
* @return 학생 목록
*/
@GetMapping("/classroom/{classRoomId}")
public ResponseEntity<List<Student>> getStudentsByClassRoom(
@PathVariable Long classRoomId) {
return ResponseEntity.ok(studentService.getStudentsByClassRoom(classRoomId));
}

/**
* 특정 나이 이상의 학생을 조회합니다.
* @param minAge 최소 나이
* @return 학생 목록
*/
@GetMapping("/age")
public ResponseEntity<List<Student>> getStudentsByAge(
@RequestParam Integer minAge) {
return ResponseEntity.ok(studentService.getStudentsByAge(minAge));
}
}
Loading