본문 바로가기
스파르타 내일배움캠프/TIL(Today I learned)

키워드 기능 - 트러블 슈팅

by pandastic 2025. 6. 20.
반응형

 

 

 

목차

     

     

    ✅ 트러블 슈팅

    1. 문제 상황

    • REST API에서 DTO 없이 엔티티(Entity)를 직접 Response로 반환할 경우, 서로의 연관 관계가 계속 참조를 일으켜 무한 순환 참조 문제 발생.
    • 결과적으로 API 응답 시 무한 로딩 현상StackOverflowError가 발생하게 됨.

     

    [ 에러 코드 ]

    더보기
    Caused by: com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: caffeine.nest_dev.domain.keyword.entity.Keyword.profileKeywords: could not initialize proxy - no Session (through reference chain: caffeine.nest_dev.common.dto.CommonResponse["data"]->java.util.ImmutableCollections$ListN[0]->caffeine.nest_dev.domain.profile.dto.response.RecommendedProfileResponseDto["keywords"]->java.util.ArrayList[0]->caffeine.nest_dev.domain.keyword.entity.Keyword["profileKeywords"])
    	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:400)
    	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:359)
    	at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:324)
    	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:765)
    	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:183)
    	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
    	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
    	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
    	at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732)
    	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:760)
    	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:183)
    	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
    	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
    	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
    	at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732)
    	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:760)
    	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:183)
    	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:503)
    	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:342)
    	at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1587)
    	at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1061)
    	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:485)
    	... 141 more

     

    2. 원인 분석

    순환 참조란?

    • 두 엔티티가 서로를 참조하면서 객체 직렬화 과정에서 무한히 서로를 호출하는 상황.
    • 예시로, Profile 엔티티와 연관된 Keyword 엔티티를 직접 반환하면, 다음과 같은 순환 구조가 형성될 수 있다:
    💡 Profile → ProfileKeyword → Keyword → ProfileKeyword → Profile → … (무한 반복)

     

    // Entity를 그대로 반환하는 잘못된 예시
    @Entity
    public class Profile {
        @OneToMany(mappedBy = "profile")
        private List<ProfileKeyword> profileKeywords;
    }
    
    @Entity
    public class ProfileKeyword {
        @ManyToOne
        private Profile profile;
    
        @ManyToOne
        private Keyword keyword;
    }
    
    @Entity
    public class Keyword {
        @OneToMany(mappedBy = "keyword")
        private List<ProfileKeyword> profileKeywords;
    }
    • 위와 같이 서로가 서로를 직접 참조하고 있어, JSON으로 직렬화할 때 순환 참조로 인해 무한 반복이 발생하게 된다.
    • 보안 상으로도 좋지 않음.

    [ 예시 ]

    keywords : [
          id:
          name:
          profileKeywords: [
                id:
                profile: {
             
                },
                keyword: {
     
                }
          ]
    ]

     

     

    3. 해결 방법

    • DTO를 만들어 순환 참조를 끊고 필요한 정보만 전달하여 무한 참조 방지.
    // ProfileResponseDto 예시
    public class ProfileResponseDto {
        private Long id;
        private String title;
        private List<String> keywords;
    
        public static ProfileResponseDto from(Profile profile) {
            ProfileResponseDto dto = new ProfileResponseDto();
            dto.id = profile.getId();
            dto.title = profile.getTitle();
            dto.keywords = profile.getProfileKeywords().stream()
                .map(pk -> pk.getKeyword().getName())
                .collect(Collectors.toList());
            return dto;
        }
    }
    • 이렇게 하면 실제 엔티티 대신 간단한 문자열 리스트나 프리미티브 타입만 담긴 DTO를 반환하여 순환 참조를 막을 수 있음.

     

    REST API에서 데이터를 반환할 때는 항상 DTO를 활용하여 필요한 데이터만 선별하여 반환하자!

     

    반응형