스프링부트 DTO 변경 질문

Entity에서 DTO로 변환하는데 며칠을 쓰고도 해결 못해서 질문 올립니다..

  1. 점프 투 스프링부트가 DTO를 사용하지 않고 Entity만 사용되도록 작성되어서 간단한 DTO를 추가하려고 했습니다.
  2. DTO클래스와 Entity클래스 내부에 각각 toEntity, toDTO 메소드를 작성하였지만, 무한 순환 참조가 일어나 Stack Overflow가 발생하였습니다.
  3. ModelMapper, MapStruct를 사용해보아도 answerList의 타입이 List와 List여서 그런지 스택오버플로우는 해결되었지만, mapping 된 값에 null이 들어가는 다른 문제가 생겼습니다.
  4. 그래서 다시 수동 매핑을 하려고 다른 방법을 생각해보았고, 아래와 같은 코드를 작성하게 되었는데, 결국은 무한 순환 참조를 피하는 코드는 작성할 수 없었습니다.
  5. DTO와 Entity간 무한 순환 참조를 피하도록 구현하려면 어떻게 해야할까요?

쓸모 없는 코드는 다 제거하고 dtoToEntity, entityToDTO와 관련된 코드만 첨부하겠습니다.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class QuestionDTO {
    private Integer id;
    private String subject;
    private String content;
    private LocalDateTime createDateTime;
    private LocalDateTime modifyDateTime;
    private List<AnswerDTO> answerList;
    private SiteUserDTO author;


}

@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class QuestionEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(length = 300)
    private String subject;

    @Column(columnDefinition = "TEXT")
    private String content;

    private LocalDateTime createDateTime;

    private LocalDateTime modifyDateTime;

    @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
    private List<AnswerEntity> answerList;

    @ManyToOne
    private SiteUserEntity author;

}

@Service
@RequiredArgsConstructor
public class QuestionServiceImpl implements QuestionService{
private final QuestionRepository questionRepository;

  public QuestionDTO getQuestion(Integer id){
        QuestionEntity questionEntity = this.questionRepository.findById(id).orElseThrow(() -> new DataNotFoundException("question not found"));
        QuestionDTO questionDTO = this.toDTO(questionEntity);
        return questionDTO;
    }

public QuestionEntity toEntity(QuestionDTO questionDTO){
        return QuestionEntity.builder()
                .id(questionDTO.getId())
                .subject(questionDTO.getSubject())
                .content(questionDTO.getContent())
                .createDateTime(questionDTO.getCreateDateTime())
                .modifyDateTime(questionDTO.getModifyDateTime())
                .answerList(convertAnswerDTOListToAnswerEntityList(questionDTO.getAnswerList()))
                .author(questionDTO.getAuthor().toEntity())
                .build();
    }
    private List<AnswerEntity> convertAnswerDTOListToAnswerEntityList(List<AnswerDTO> answerDTOList) {
        List<AnswerEntity> answerEntityList = new ArrayList<>();
        for (AnswerDTO answerDTO : answerDTOList) {
            AnswerEntity answerEntity = AnswerEntity.builder()
                    .id(answerDTO.getId())
                    .content(answerDTO.getContent())
                    .createDateTime(answerDTO.getCreateDateTime())
                    .modifyDateTime(answerDTO.getModifyDateTime())
                    .question(this.getQuestionEntity(answerDTO.getId()))
                    .build();
            answerEntityList.add(answerEntity);
        }
        return answerEntityList;
    }


    private QuestionEntity getQuestionEntity(Integer id){
        QuestionEntity questionEntity = this.questionRepository.findById(id).orElseThrow(() -> new DataNotFoundException("question not found"));
        return questionEntity;
    }

    public QuestionDTO toDTO(QuestionEntity questionEntity){
        QuestionDTO questionDTO = QuestionDTO.builder()
                .id(questionEntity.getId())
                .subject(questionEntity.getSubject())
                .content(questionEntity.getContent())
                .createDateTime(questionEntity.getCreateDateTime())
                .modifyDateTime(questionEntity.getModifyDateTime())
                .answerList(convertAnswerEntityListToAnswerDTOList(questionEntity.getAnswerList()))
                .author(questionEntity.getAuthor().toDTO())
                .build();
        System.out.println("questionDTO = " + questionDTO);
        return questionDTO;
    }
    private List<AnswerDTO> convertAnswerEntityListToAnswerDTOList(List<AnswerEntity> answerEntityList) {
        List<AnswerDTO> answerDTOList = new ArrayList<>();
        for (AnswerEntity answerEntity : answerEntityList) {
            AnswerDTO answerDTO = AnswerDTO.builder()
                    .id(answerEntity.getId())
                    .content(answerEntity.getContent())
                    .createDateTime(answerEntity.getCreateDateTime())
                    .modifyDateTime(answerEntity.getModifyDateTime())
                    .question(this.getQuestionByAnswer(answerEntity.getId()))
                    .build();
            answerDTOList.add(answerDTO);
        }
        return answerDTOList;
    }

    public QuestionDTO getQuestionByAnswer(Integer id){
        QuestionEntity questionEntity = this.questionRepository.findQuestionWithAnswersById(id);
        QuestionDTO questionDTO = this.toDTO(questionEntity);
        return questionDTO;
    }
}

public interface QuestionRepository extends JpaRepository<QuestionEntity, Integer> {
    @Query("SELECT DISTINCT q FROM QuestionEntity q JOIN FETCH q.answerList a WHERE a.id = :answerId")
    QuestionEntity findQuestionWithAnswersById(@Param("answerId") Integer answerId);

}

jumpToSpringBoot 352

M 2023년 5월 18일 11:01 오전

순환참조 오류가 발생하는 오류내역을 보여주세요. - 박응용님, 2023년 5월 18일 8:34 오전 추천 , 대댓글
@박응용님 안녕하세요. 오류 로그는 StackOverflow라고 뜹니다.. getQuestion() -> toDTO() -> convertAnswerEntityListToAnswerDTOList() -> getQuestionByAnswer() -> toDTO() -> 앞의 과정 반복 디버거 찍어보니 이런식으로 진행되어서 무한 순환참조가 이루어집니다. - jumpToSpringBoot님, M 2023년 5월 18일 11:05 오전 추천 , 대댓글
+2 @jumpToSpringBoot님 이미 질문을 알고 있는데, 답변을 통해서 getQuestionByAnswer를 다시 호출할 필요는 없어 보입니다. 이 부분이 순환참조의 주범으로 보이네요. - 박응용님, 2023년 5월 18일 11:45 오전 추천 , 대댓글
@박응용님 덕분에 아래처럼 구현해서 해결했습니다! 감사합니다! - jumpToSpringBoot님, M 2023년 5월 19일 7:31 오전 추천 , 대댓글
목록으로
1개의 답변이 있습니다. 1 / 1 Page

QuestionDTO

@Getter
@Setter
@AllArgsConstructor
@Builder
public class QuestionDTO {
    private Integer id;
    private String subject;
    private String content;
    private LocalDateTime createDateTime;
    private LocalDateTime modifyDateTime;
    private List<Answer> answerList;
    private SiteUserDTO author;


    @Getter
    @Setter
    public static class Answer {
        private Integer id;
        private String content;
        private LocalDateTime createDateTime;
        private LocalDateTime modifyDateTime;
        private SiteUserDTO author;
    }


    public QuestionDTO() {
        this.answerList = new ArrayList<>();
    }

}

QuestionServiceImpl

@Service
@RequiredArgsConstructor
public class QuestionServiceImpl implements QuestionService {
    private final QuestionRepository questionRepository;

    public List<QuestionEntity> getList() {
        return this.questionRepository.findAll();
    }


    public QuestionDTO getQuestion(Integer id) {
        QuestionEntity questionEntity = this.questionRepository.findById(id).orElseThrow(() -> new DataNotFoundException("question not found"));
        QuestionDTO questionDTO = this.toDTO(questionEntity);
        return questionDTO;
    }

    public void create(String subject, String content, SiteUserDTO siteUserDTO) {
        QuestionDTO questionDTO = new QuestionDTO();
        questionDTO.setSubject(subject);
        questionDTO.setContent(content);
        questionDTO.setCreateDateTime(LocalDateTime.now());
        questionDTO.setModifyDateTime(LocalDateTime.now());
        questionDTO.setAuthor(siteUserDTO);
        this.questionRepository.save(this.toEntity(questionDTO));
    }

    public void modify(QuestionDTO questionDTO, String subject, String content) {
        questionDTO.setSubject(subject);
        questionDTO.setContent(content);
        questionDTO.setModifyDateTime(LocalDateTime.now());
        this.questionRepository.save(this.toEntity(questionDTO));
    }

    public void delete(QuestionDTO questionDTO) {
        this.questionRepository.delete(this.toEntity(questionDTO));
    }


    public Page<QuestionEntity> getList(int page) {

        List<Sort.Order> sort = new ArrayList<>();
        sort.add(Sort.Order.desc("createDateTime"));
        Pageable pageable = PageRequest.of(page, 15, Sort.by(sort));
        return this.questionRepository.findAll(pageable);
    }

    public QuestionEntity toEntity(QuestionDTO questionDTO) {
        List<AnswerEntity> answerList = new ArrayList<>();

        for (QuestionDTO.Answer answer : questionDTO.getAnswerList()) {
            AnswerEntity answerEntity = AnswerEntity.builder()
                    .id(answer.getId())
                    .content(answer.getContent())
                    .createDateTime(answer.getCreateDateTime())
                    .modifyDateTime(answer.getModifyDateTime())
                    .author(answer.getAuthor().toEntity())
                    .build();
            answerList.add(answerEntity);
        }


        return QuestionEntity.builder()
                .id(questionDTO.getId())
                .subject(questionDTO.getSubject())
                .content(questionDTO.getContent())
                .createDateTime(questionDTO.getCreateDateTime())
                .modifyDateTime(questionDTO.getModifyDateTime())
                .answerList(answerList)
                .author(questionDTO.getAuthor().toEntity())
                .build();
    }

    public QuestionDTO toDTO(QuestionEntity questionEntity) {
        List<QuestionDTO.Answer> answerList = new ArrayList<>();

        for (AnswerEntity answerEntity : questionEntity.getAnswerList()) {
            QuestionDTO.Answer answer = new QuestionDTO.Answer();
            answer.setId(answerEntity.getId());
            answer.setAuthor(answerEntity.getAuthor().toDTO());
            answer.setContent(answerEntity.getContent());
            answer.setCreateDateTime(answerEntity.getCreateDateTime());
            answer.setModifyDateTime(answerEntity.getModifyDateTime());

            answerList.add(answer);
        }

        return QuestionDTO.builder()
                .id(questionEntity.getId())
                .subject(questionEntity.getSubject())
                .content(questionEntity.getContent())
                .createDateTime(questionEntity.getCreateDateTime())
                .modifyDateTime(questionEntity.getModifyDateTime())
                .answerList(answerList)
                .author(questionEntity.getAuthor().toDTO())
                .build();
    }
}

jumpToSpringBoot

2023년 5월 19일 7:30 오전