스프링부트 공부를 하고 처음 마주친 에러의 해결 과정에 대해서 기록하고자 한다. 해당 이슈는 이런 문제가 생길 수 있다~라고 대략적으로 들어보기만 했는데 실제로 맞닥뜨린 것은 처음이었다. Let's Go
문제 상황 : 객체 직렬화 시 연관된 양방향 매핑으로 인한 순환 참조 문제
기존에 내가 원하던 것은 클라이언트의 req와 해당 요청에 대해 내 서버가 어떤 응답을 내렸는지를 로그 파일로 저장해두기 위해 작성했던 코드에서 발생한 에러였다.
@Transactional
public BouquetCustomizingResponseV2 updateBouquet(BouquetCustomizingRequest request, Long memberId, Long bouquetId, List<MultipartFile> multipartFiles) {
String clientIP = getClientIP();
Member member = bouquetCustomizingService.getMemberByMemberId(memberId);
Bouquet existingBouquet = bouquetCustomizingService.getBouquetByMemberIdAndBouquetId(memberId, bouquetId);
try {
String jsonString = objectMapper.writeValueAsString(existingBouquet);
if (!existingBouquet.getMember().equals(member)) {
throw new WhoaException(
NOT_MEMBER_BOUQUET,
"updateBouquet - 실제 bouquet을 수정 요청을 한 memberId와 bouquet의 memberId가 불일치",
clientIP,
"memberId request : " + memberId + "\n" + "bouquet request : " + jsonString
);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new WhoaException(
ExceptionCode.OBJECT_MAPPER_JSON_PROCESSING_ERROR,
"updateBouquet - pathVariable로 받은 bouquetId와 controller memberId로 찾은 bouquet 객체를 json string으로 전환하면서 오류 발생"
);
}
// 생략
위와 같이 내가 설계해둔 코드에서 objectMapper로 bouquet 객체를 string json 형식으로 직렬화하는 과정에서 에러가 터져 catch문을 타는 상황이었다.
이 때 나타난 에러 로그를 보다 구체화 하기 위해 printStackTrace()를 통해 상세화하였더니 아래와 같이 원인을 파악할 수 있었다.
2025-04-05 01:49:21 [http-nio-8080-exec-2] TRACE org.hibernate.orm.jdbc.bind - binding parameter (1:BIGINT) <- [6]
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.whoa.whoaserver.domain.bouquet.domain.Bouquet["member"]
->com.whoa.whoaserver.domain.member.domain.Member["bouquet"]
->org.hibernate.collection.spi.PersistentBag[0]
->com.whoa.whoaserver.domain.bouquet.domain.Bouquet["member"]
->com.whoa.whoaserver.domain.member.domain.Member["bouquet"]
// 생략
문제의 핵심은
Infinite recursion (StackOverflowError)
(through reference chain:
Bouquet["member"] → Member["bouquet"] → List<Bouquet> → Bouquet["member"] → ...)
Bouquet → Member → List<Bouquet> → Bouquet... 으로 전형적인 순환 참조 문제였다.
관련 DB 연관관계 설계는 Member : Bouquet이 일대다였으며 양쪽이 모두 각각 OneToMany로 List<Bouquet>, ManyToOne으로 Member 필드를 가지고 있었다.
그래서 코드 로직 상 Jackson은 existingBouquet을 json으로 변환하려고 할 때
1. Bouquet 직렬화 시작 → Member 필드도 직렬화
2. Member 직렬화 시작 → List<Bouquet> bouquets 필드가 있으니 Bouquet 리스트 직렬화
3. Bouquet 직렬화 → Member 직렬화 → Bouquet 직렬화....
그래서 JsonProcessingException이 계속 발생했던 것이다.
해결 방법 : annotation 적용
내가 선택한 방법은 @JsonManagedReference, @JsonBackReference를 사용하는 방법이었다.
@JsonIgnore 방식도 간편하지만 보다 일대다 연관관계 상 부모-자식 관계를 유지하여 직렬화를 직관적으로 방지하고 싶었다.
지금 이 문제 상황에서 Member는 부모, Bouquet은 자식이라고 볼 수 있다.
그래서 Bouquet entity에서 Member 필드에 JsonManagedReference를 선언하고
vice versa로 Member entity에서 List<Bouquet> 필드에 JsonBackReference를 선언한다.
그러면 Member 객체를 직렬화 하면 JsonBackReference 가 순환참조 방지를 위해 json 직렬화를 제외하기 때문에 bouquets 필드는 json에 포함되지 않는다.
하지만 Bouquet 객체를 json으로 변환하면 JsonManagedReference는 json에 포함이 되므로 member 필드 정보를 확인할 수 있다.
어노테이션 네이밍 그대로 back은 json 빠꾸~를 시키는 것이고 Managed는 관리 의미를 가지니까 json에 포함이 되는 것이라고 직관적으로 이해했다. 그리고 또한 자식 쪽에서 부모쪽 정보는 확인이 가능해야 하므로 부모 필드에 Managed를 붙여야 한다 까지 기억하는 걸로~
부분적으로 Member-Bouquet 1:N 양방향 매핑을 다루었지만
해당 프로젝트에서 Bouquet-BouquetImage 1:N 양방향 매핑도 포함되어있었기 때문에 BouquetImage까지 같이 순환참조 문제가 터졌다. 이 부분도 마찬가지로 Member-Bouquet 순환참조와 구분하기 위해
BouquetImage["bouquet"]
→ Bouquet["images"]
→ PersistentBag[0]
→ BouquetImage["bouquet"]
→ 생략(무한 루프)
// Bouquet.java
@JsonManagedReference("bouquet-image")
@OneToMany(mappedBy = "bouquet", cascade = CascadeType.ALL, orphanRemoval = true)
private List<BouquetImage> images = new ArrayList<>();
// BouquetImage.java
@JsonBackReference("bouquet-image")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bouquet_id")
private Bouquet bouquet;
"bouquet-image" 네이밍을 붙였다.
코드 PR 링크에서 전체 트러블 슈팅 내용을 확인할 수 있다.
이렇게 또 문제 상황 대처 능력을 키워가는 거지
'SpringBoot > 트러블 슈팅' 카테고리의 다른 글
[Spring] JPA와 Redis 충돌 및 @RedisHash 간과한 부분 (0) | 2024.08.13 |
---|---|
[Spring] JPA LazyInitializationException 발생 원인 및 해결 방법 (0) | 2024.08.07 |
[Spring] @RequestBody json DTO null 트러블 슈팅 (0) | 2024.05.22 |
[Spring] @EnableJpaAuditing과 createdAt의 연결고리 (0) | 2024.05.08 |
[Spring] kotlin + security + jwt 구현 과정 트러블 슈팅 (0) | 2024.04.28 |