JPA 순환참조

Junyoung·2024년 2월 19일

trouble

목록 보기
1/8

JPA는 OSIV를 따로 설정하지 않으면 컨트롤러 request 와 reponse 동안에 DB connection을 가지고 있는다.

JSON은 Getter 와 Setter 를 이용해서 들어온 값과 객체를 매핑한다.

이때 DB 커넥션을 놓아버린 상태에서 프록시 객체의 일정 필드의 값을 Get 해오려고 한다면 DB가 없기 때문에 에러가 발생한다 !

entity에서 양방향 매핑이 진행된다면 ToString 조건도 확인했지만 Json 에러가 계속해서 나왔다.

응답을 Json으로 변환하는 과정에서 JackSon 이 Message 안에 Chat 을 조회해오고 Chat 은 또 Message 를 불러오는 무한 참조가 발생한다 !

Chat 채팅방을 조회해올때
이전 메세지가 존재한다면 이전 메세지를 같이 랜더링을 진행했다

package com.ssafy.soyu.chat.entity;

import com.ssafy.soyu.item.entity.Item;
import com.ssafy.soyu.member.entity.Member;
import com.ssafy.soyu.message.entity.Message;
import jakarta.persistence.*;
import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;

import java.time.LocalDateTime;
import lombok.NoArgsConstructor;
import lombok.ToString;


@Entity
@Table(name = "chat")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(exclude = {"item", "buyer", "seller"})
public class Chat {
    @Id
    @GeneratedValue
    @Column(name = "chat_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "buyer_id")
    private Member buyer;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "seller_id")
    private Member seller;

    @OneToMany(mappedBy = "chat")
    List<Message> message;

    private String lastMessage;

    private LocalDateTime lastDate;

    private Boolean isChecked;
    private LocalDateTime lastChecked;

    public Chat(Item item, Member seller, Member buyer) {
        this.item = item;
        this.seller = seller;
        this.buyer = buyer;
    }

    public void changeLast(String lastMessage) {
        this.lastMessage = lastMessage;
        this.lastDate = LocalDateTime.now();
    }
}
package com.ssafy.soyu.message.entity;

import com.ssafy.soyu.chat.entity.Chat;
import com.ssafy.soyu.profileImage.ProfileImage;
import com.ssafy.soyu.member.entity.Member;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;

import java.time.LocalDateTime;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@Table(name = "message")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(exclude = {"chat", "member", "profileImage"})
public class Message {
    @Id
    @GeneratedValue
    @Column(name = "message_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "chat_id")
    private Chat chat;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "file_id")
    private ProfileImage profileImage;

    private String content;

    LocalDateTime regDate;

    public Message(Chat chat, Member member, String content) {
        this.chat = chat;
        this.member = member;
        this.content = content;
    }
}

ChatResponse 에 Message entity를 담아서 랜더링을 진행했고

package com.ssafy.soyu.chat.dto.response;

import com.ssafy.soyu.image.dto.response.ImageResponse;
import com.ssafy.soyu.message.dto.response.MessageResponse;
import com.ssafy.soyu.profileImage.dto.response.ProfileImageResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

@Schema(description = "채팅방 상세 응답 DTO")
@Data
@AllArgsConstructor
public class ChatResponse {

  @Schema(description = "물품 ID")
  Long itemId;

  @Schema(description = "물품 제목")
  String title;

  @Schema(description = "물품 가격")
  Integer price;

  @Schema(description = "물품 사진")
  List<ImageResponse> imageResponses;

  @Schema(description = "판매자 ID")
  Long buyerId;

  @Schema(description = "판매자 닉네임")
  String buyerNickname;

  @Schema(description = "판매자 프로필")
  ProfileImageResponse sellerProfileImageResponse;

  @Schema(description = "구매자 ID")
  Long sellerId;

  @Schema(description = "구매자 닉네임")
  String sellerNickname;

  @Schema(description = "구매자 프로필")
  ProfileImageResponse buyerProfileImageResponse;

  @Schema(description = "마지막 메세지 내용")
  private String lastMessage;

  @Schema(description = "마지막 메세지 일자/시간")
  @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
  private LocalDateTime lastDate;

  @Schema(description = "확인 여부")
  private Boolean isChecked;

  @Schema(description = "마지막 확인 이자/시간")
  @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
  private LocalDateTime lastChecked;

  @Schema(description = "MessageResponse 리스트")
  private List<Message> messages;
}

이때 순환참조 에러가 발생했다 !
이유는 Message 객체에 있는 Chat, Member, ProfileImage 해당 엔티티들을 get 으로 가져오려고 하기 때문이다 (Json 변환 시점은 이미 DBconnection을 놓아버린 상태)

@Schema(description = "MessageResponse 리스트")
private List messageResponses;
MessageResponse를 하나 만들고 ChatResponse에서 해당 response를 리턴해주므로 해당 에러를 해결한다.

package com.ssafy.soyu.message.dto.response;

import lombok.Data;

@Data
public class MessageResponse {
  Long memberId;
  String content;

  public MessageResponse(Long memberId, String content) {
    this.memberId = memberId;
    this.content = content;
  }
}


에러 없이 데이터가 랜더링 되는것을 확인할수 있다 !

엔티티를 직접 응답하고 요청을 받아 온다면 엔티티의 변경이 있을때마다 API는 계속해서 변경된다 !

따라서 Request Response 를 사용하여 API가 변하지 않는 무결성을 지키고 필요한 데이터만 랜더링 해주는 데이터의 정밀성 또한 증가시킬수 있다.

해당 프로젝트에서는 Request, Response 만 사용했지만 다음 프로젝트에서는 중간 단계의(service 에서 사용하는) Param 의 한단계를 더 거치게 사용하면 좋을거같다

profile
라곰

0개의 댓글