Springboot와 JPA 환경에서 멤버별 결과를 반환하는 코드를 구현하고 있었습니다
아래는 처음 작성했던 코드입니다
// Controller
@GetMapping("/result/{memberId}")
public ResponseEntity<BaseResponse<List<TutorialResult>>> getTutorialResult(
@PathVariable Long memberId
) {
return ResponseEntity.ok(new BaseResponse<>(tutorialService.getTutorialResult(memberId)));
}
// Service
public List<TutorialResult> getTutorialResult(Long memberId) {
return tutorialRepository.findByMember_MemberId(memberId);
}
다음과 같은 코드를 실행했을 때, InvalidDefinitionException 오류가 발생했습니다
// TurotialResult Entity
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long tutorialResultId;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id", referencedColumnName = "companyId", nullable = false)
private Company company;
@Column(nullable = false)
private int startMoney;
@Column(nullable = false)
private int endMoney;
@Column(nullable = false)
private LocalDateTime startDate;
@Column
private LocalDateTime endDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", referencedColumnName = "memberId", nullable = false)
private Member member;
원인은 JPA의 Lazy Loading 이었습니다
@ManyToOne(fetch = FetchType.LAZY) 또는 @OneToOne(fetch = FetchType.LAZY)를 사용하면, 연관된 엔티티는 프록시 객체로 로딩됩니다
프록시 객체는 실제 클래스가 아니기 때문에, Jackson 라이브러리는 이것을 직렬화할 수 없습니다
따라서 컨트롤러에서 엔티티를 직접 반환하는 것을 요구하는 위 코드에서 다음과 같은 에러가 발생했습니다
이런 직렬화 문제를 해결하는 방법은 여러가지가 있지만, 가장 권장되는 방법은 DTO(Data Transfer Object)를 이용하는 것입니다
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
@JsonIgnoreProperties 같은 어노테이션을 사용해서 프록시 객체 직렬화 오류를 무시할 수도 있지만
직렬화 시 어떤 필드가 포함되고 빠질지 예측하기 어렵고, 프로젝트가 커질수록 일관성이 떨어지며, 연관 엔티티가 늘어날수록 유지보수가 어려워집니다
반면 DTO를 이용하면
다음과 같은 장점을 가지기 때문에 DTO 방식이 가장 권장됩니다
// DTO
@Getter
public class TutorialResultResponse {
private Long tutorialResultId;
private Long companyId;
private String companyName;
private int startMoney;
private int endMoney;
private LocalDateTime startDate;
private LocalDateTime endDate;
private Long memberId;
private String memberName;
public TutorialResultResponse(TutorialResult entity) {
this.tutorialResultId = entity.getTutorialResultId();
this.startMoney = entity.getStartMoney();
this.endMoney = entity.getEndMoney();
this.startDate = entity.getStartDate();
this.endDate = entity.getEndDate();
// Lazy 객체에서 필요한 값만 추출
Company company = entity.getCompany();
if (company != null) {
this.companyId = company.getCompanyId();
this.companyName = company.getCompanyName();
}
Member member = entity.getMember();
if (member != null) {
this.memberId = member.getMemberId();
this.memberName = member.getNickname();
}
}
}
다음과 같은 TutorialResultResponse 이라는 새로운 엔티티를 만들어서 문제를 해결했습니다
// Controller
@GetMapping("/result/{memberId}")
public ResponseEntity<BaseResponse<List<TutorialResultResponse>>> getTutorialResult(
@PathVariable Long memberId
) {
return ResponseEntity.ok(new BaseResponse<>(tutorialService.getTutorialResult(memberId)));
}
// Service
public List<TutorialResultResponse> getTutorialResult(Long memberId) {
return tutorialRepository.findByMember_MemberId(memberId)
.stream()
.map(TutorialResultResponse::new)
.collect(Collectors.toList());
}
컨트롤러와 서비스도 다음과 같이 코드를 수정하여 문제를 해결했습니다