프로젝트를 진행하던 도중 Gemini API를 이용하여 상품 내용에 대한 도움을 주는 기능을 구현하고 자신이 추천 받은 내용을 조회할 수 있는 기능을 구현하던 도중 User와 AiDescription 엔티티가 양방향 연관관계를 가지고 있어 무한 루프 문제가 발생했다. 사실 연관관계를 생각 안하고 단순 조회만 생각하고 작성을 했기에 이런 문제가 발생한 거라 앞으론 생각을 하면서 작성을 하려고 기록해두려한다....
1. 무한 참조 문제 발생
- 문제 상황
- Spring Boot + JPA 환경에서 엔티티 간 양방향 연관관계를 설정할 때 JSON 직렬화 과정에서 무한 참조 문제가 발생
- 무한 참조 문제 개념
- 무한 참조는 엔티티 A가 엔티티 B를 참조하고, B가 다시 A를 참조하면서 JSON 직렬화 과정에서 끝없이 순환되는 문제이다.
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String username;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<AiDescription> aiDescriptions = new ArrayList<>();
}
@Entity
@Table(name = "ai_descriptions")
public class AiDescription {
@Id @GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String productName;
private String generatedText;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
위와 같이 User 엔티티는 AiDescription 리스트를 가지고 있고, AiDescription 엔티티는 User를 참조하는 구조이다.
이제 특정 User 데이터를 조회하게 되면
{
"userId": 1,
"username": "OWNER",
"aiDescriptions": [
{
"id": "d013b2a7-ed2c-4829-9066-462ea95260af",
"productName": "로제 떡볶이",
"generatedText": "매콤달콤한 로제 소스...",
"user": {
"userId": 1,
"username": "OWNER",
"aiDescriptions": [
{ ... 무한 반복 ... }
]
}
}
]
}
위 JSON 응답처럼 user-> aiDescriptions -> user ->aiDescriptions 무한 반복이 발생한다!
2. 해결 방안 : @JsonIgnoreProperties 활용
이를 해결하기 위해 @JsonIgnoreProperties 어노테이션을 사용하여 직렬화 과정에서 특정 필드를 제외했다
@Entity
@Table(name = "ai_descriptions")
@JsonIgnoreProperties({"user"}) // user 필드를 JSON 직렬화에서 제외
public class AiDescription {
@Id @GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String productName;
private String generatedText;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
이렇게 하니 이제 AiDescription을 직렬화할 때 User 필드는 포함되지 않기 때문에 무한 루프가 발생하지 않는다.
3. 프록시 직렬화 문제 발생
하지만 무한 참조 문제를 해결하고 User 데이터를 직렬화하는 과정에서 Hibernate 프록시 직렬화 문제가 발생하는 것이 아니겠는가..?
- 프록시 직렬화 문제 개념
Hibernate는 @ManyToOne(fetch = FetchType.Lazy)설정이 되어 있을 경우 프록시 객첼르 사용하여 지연 로딩을 수행한다.
즉, User 객체는 User 인스턴스가 아니라 Hibernate 프록시 객체로 감싸져 있는 상태이다.
//발생 에러 메세지
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor
에러의 원인은 User가 Hibernate의 ByteBuddyInterceptor 프록시 객체로 감싸져 있어 Jackson이 이를 직렬화할 수 없기 때문이다!
4. 해결 방안 : DTO 변환 및 Service에서 데이터 변환
문제를 해결하기 위해 DTO를 도입하여 직접 데이터를 변환한 후 반환하도록 수정했다.
-해결 코드 (AiDescriptionDto 추가)
@Getter
public class AiDescriptionDto {
private UUID id;
private String productName;
private String generatedText;
public AiDescriptionDto(AiDescription aiDescription) {
this.id = aiDescription.getId();
this.productName = aiDescription.getProductName();
this.generatedText = aiDescription.getGeneratedText();
}
}
-해결 코드 (AiDescriptionService 에서 DTO 변환 적용)
@Service
@RequiredArgsConstructor
public class AiDescriptionService {
private final AiDescriptionRepository aiDescriptionRepository;
@Transactional(readOnly = true)
public List<AiDescriptionDto> getUserDescriptions(Long userId) {
List<AiDescription> descriptions = aiDescriptionRepository.findByUser_UserId(userId);
return descriptions.stream()
.map(AiDescriptionDto::new) // DTO로 변환
.collect(Collectors.toList());
}
}
5. 결론 "DTO의 중요성"
1. 엔티티 보호 (Encapsulation)
- 엔티티는 데이터베이스와 직접 연결된 중요한 객체이다.
- 엔티티를 직접 노출하면 외부 요청으로 인해 엔티티가 변경될 위험이 있다.
- DTO를 사용하면 엔티티의 내부 구현을 감추고 필요한 정보만 전달할 수 있다.
2. 무한 참조 & 프록시 직렬화 문제 방지
- 양방향 연관관계가 있는 엔티티는 무한 참조가 발생할 가능성이 높다.
- @JsonIgnoreProperties 같은 어노테이션을 사용해도, Hibernate 프록시 직렬화 문제가 생길 수 있다.
- DTO를 사용하면 엔티티의 연관관계를 제거한 데이터만 전달할 수 있어서 안전하다.
3. API 응답 최적화
- 엔티티에는 불필요한 데이터가 포함될 수 있다.
(예: password, createdBy, updatedBy 등) - DTO를 사용하면 필요한 데이터만 선택적으로 제공하여 API 응답 속도를 최적화할 수 있다.
4. API 변경 유연성 증가
- 엔티티를 직접 사용하면, 엔티티의 필드가 바뀔 때 API도 영향을 받는다.
- DTO를 사용하면, API 응답 형식을 변경할 때 DTO만 수정하면 되므로 유연성이 증가한다.
'Spring > Spring boot' 카테고리의 다른 글
[Spring Boot] 예외 처리 (0) | 2025.02.21 |
---|---|
@PathVariable name 생략시 에러 (0) | 2024.08.28 |
검증 2 (Bean Validation) (0) | 2023.09.11 |
[JDBC] 데이터 접근 기술 - 테스트 (0) | 2023.09.10 |
[JDBC] JdbcTemplate (0) | 2023.09.05 |