간단한 MSA 프로젝트의 일환으로, Online Judge의 서비스 중, 문제관리 서비스를 개발 중인데, 서로 다른 엔티티 간 관계를 아래처럼 표현했다.
Problem.java
package com.chabunsi.problemmanage.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "problem")
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Problem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private String content;
@Column
private int solve_num;
@Column
private int wrong_num;
@OneToMany(mappedBy = "problem")
private List<TestCase> testCaseList = new ArrayList<>();
@Column
private int time_limited;
@Column
private int memory_limited;
}
TestCase
package com.chabunsi.problemmanage.entity;
import com.chabunsi.problemmanage.dto.request.TestCaseBody;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "test_case")
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class TestCase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JoinColumn(name="problem_id")
@ManyToOne
@JsonIgnore
private Problem problem;
@Column
private String input;
@Column
private String output;
public TestCase(TestCaseBody testCaseBody, Problem problem) {
this.problem = problem;
this.input = testCaseBody.getInput();;
this.output = testCaseBody.getOutput();
}
@Override
public String toString() {
return "{" +
"id=" + id +
", input='" + input + '\'' +
", output='" + output + '\'' +
'}';
}
}
Problem이라는 엔티티가 OneToMany로 TestCase에 매핑되어있다. 따라서, 하나의 Problem가 여러 개의 TestCase를 갖고 있다. 그럼 저장을 어떻게 할까.
ProblemController.java
@PostMapping("")
public ResponseEntity<?> addProblem(
@RequestBody @Validated ProblemBody problemBody
) {
Problem problem = problemService.addProblem(problemBody);
testCaseService.addTestCases(problemBody.toTestCaseEntity(problem));
return ResponseEntity.ok(problem);
}
처음에 코드를 작성할 때 Problem을 저장하고 이에 대한 레퍼를 가져와 List<TestCase>를 저장하려고 했다(이 때, 각 서비스들의 역할은 단순 JPARepository.save 혹은 .saveAll을 호출하므로, 코드는 생략하겠다).
물론 이 코드로도 실행은 잘 된다. 오류가 일어난다던지 그런건 없다. 하지만, 이미 Problem
의 멤버변수로 List<TestCase> testCaseList
를 OneToMany로 매핑해서 가져오고 있다. 그럼 Problem
을 저장할 때, List<TestCase> testCaseList
을 이용하여 동시에 TestCase
도 저장하게 만들면 되지 않는가. 이부분을 내가 놓쳤다.
이처럼, Problem - TestCase 관계와 같이, 다른 엔티티의 존재에 따라 의미가 달라지는 경우가 있다. TestCase는 Problem이 사라지면 의미가 없다(관련된 문제가 사라지면 테스트케이스가 존재할 필요가 없기 때문이다).
따라서, Problem이 삭제되면 TestCase도 같이 삭제되게끔 만들어놔야 한다. 이를 Cascade를 통해 엔티티 간 관계를 정립시켜줄 수 있다.
특정 엔티티에 대한 작업을 수행하면, 이 엔티티와 관련된 다른 엔티티에도 작업을 준다고 보면 된다.
Cascade의 type은 다음과 같다. JPA 타입과 Hibernate 타입이 따로 있는데, 필요한 건 JPA쪽이니까 이거만 정리한다.
CasCadeType.ALL
: 모든 작업을 모든 CasCade 타입을 적용한다.
CasCadeType.PERSIST
: 엔티티를 영속화할 때, 관련된 엔티티도 같이 유지한다.
CasCadeType.MERGE
: 엔티티를 병합할 때, 관련된 엔티티도 함께 병합한다.
CasCadeType.REMOVE
: 엔티티를 삭제할 때, 관련된 엔티티를 모두 제거한다.
CasCadeType.REFRESH
: 엔티티를 새로 고침할 때, 관련된 엔티티도 함께 새로고침한다.
CasCadeType.DETACH
: 이건 잘 이해가 안되는데.. 엔티티 간 관계를 떼어낼 때, 관련된 엔티티도 떼어낸다고 한다.
이 중에서 내가 사용해야 되는건 영속화 유지랑, 삭제 이부분이다.
TestCase와 매핑된 멤버변수에 CasCade를 적용시켜주면 된다.
Problem.java
...
@OneToMany(mappedBy = "problem", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<TestCase> testCaseList = new ArrayList<>();
...
ProblemController
@PostMapping("")
public ResponseEntity<?> addProblem(
@RequestBody @Validated ProblemBody problemBody
) {
Problem problem = problemService.addProblem(problemBody);
return ResponseEntity.ok(problem);
}
그리고 다음과 같이 호출하고 결과를 얻을 수 있었다.
Request
Response
실제 Table 내부에도 잘 들어온다.