SpringBoot

Redis에 엔티티 저장중 생긴 순환참조문제

Terror123 2025. 1. 23. 11:22

개요

  • Redis에 Entity 객체를 저장하면서 생긴 순환참조 문제에 대해 이해하고 해결 할 수 있습니다

코드

LolPlayerHistoryEntity


@Getter
@AllArgsConstructor
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Entity
public class LolPlayerHistory implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    private String playerHistoryTitle;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private LolType type;

    @OneToMany(mappedBy = "playerHistory", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<LolPlayer> players = new ArrayList<>();

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    public LolPlayerHistory(User user, String playerHistoryTitle, LolType type) {
        this.user = user;
        this.playerHistoryTitle = playerHistoryTitle;
        this.type = type;
    }

    public static LolPlayerHistory from (LolPlayerHistoryRequestDto dto, User user, LolType type) {
        return new LolPlayerHistory(
                user,
                dto.getPlayerHistoryTitle(),
                type
        );
    }

    public void updatePlayerHistoryTitle(String playerHistoryTitle) {
        this.playerHistoryTitle = playerHistoryTitle;
    }

}

LolPlayerEntity


@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class LolPlayer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private LolTier tier;

    @Column(nullable = false)
    private int mmr;

    @OneToMany(mappedBy = "player", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<LolLines> lines = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "lol_player_history_id")
    private LolPlayerHistory playerHistory;

    public LolPlayer(String name, LolTier tier, int mmr, LolPlayerHistory playerHistory) {
        this.name = name;
        this.tier = tier;
        this.mmr = mmr;
        this.playerHistory = playerHistory;
    }


    public static List<LolPlayer> from(LolPlayerHistoryRequestDto dto, LolPlayerHistory playerHistory) {
        List<LolPlayer> playerList = new ArrayList<>();
        for ( LolPlayerDto riftPlayerRequestDto : dto.getLolPlayerDtos() ) {
            playerList.add(
                    new LolPlayer(
                            riftPlayerRequestDto.getName(),
                            riftPlayerRequestDto.getTier(),
                            riftPlayerRequestDto.getTier().getScore(),
                            playerHistory
                    )
            );
        }
        return playerList;
    }

    public static List<LolPlayerDto> to(List<LolPlayer> players) {
        return players.stream()
                .map(player -> {
                    // 각 플레이어의 라인을 별도로 처리
                    List<LolLinesDto> lolLinesDtos = player.getLines().stream()
                            .map(line -> new LolLinesDto(line.getLine(), line.getLineRole()))
                            .collect(Collectors.toList());

                    // 새로운 LolPlayerDto 생성
                    return new LolPlayerDto(
                            player.getName(),
                            player.getTier(),
                            lolLinesDtos,
                            player.getMmr()
                    );
                })
                .collect(Collectors.toList());
    }

}

현재상황

  • LolPlayerHisotry는 어려개의 LolPlayer를 가질 수 있다
  • LolPlayer는 하나의 LolPlayerHisotry를 가질 수 있다
  • 현재 양방향 관계
  • UserEntity에는 Jpa의 Lazy Loading을 적용하여 정보가 필요하지않을때는 불러오지 않게 설정하였다

문제상황

    public void setPlayerHistoryDetailTeam(LolPlayerHistory lolPlayerHistory){
        redisTemplate.opsForValue().set(REDIS_NAME_PLAYER_HISTORY + lolPlayerHistory.getId(), lolPlayerHistory);
    }
  • Redis에 Entity 객체인 LolPlayerHisotry를 저장시키려고 하는순간 문제가 발생한다
    Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamWriteConstraints.getMaxNestingDepth()`)
  • Jackson이 직렬화 하는 과정에서 그 객체의 깊이가 1000개를 넘어 에러가 발생한것이다
  • 즉 순환참조가 발생됐다고 볼 수 있다

근데 왜 JPA를 활용한 Entity에 저장할때는 순환참조 문제가 생기지 않는것일까? 분명 양방햔 연관관계인데...

  • Redis에 저장할떄는 직렬화후에 저장해야하는 반면

  • Entity를통한 DB에 저장할떄는 필요하지않기 때문이다

  • 다시 본론으로 돌아가보자

    그 이유가 뭘까?

  1. 서로 양방향 연관관계있다
  2. Jackson은 PlayerHisotry를 직렬화한다
  3. PlayerHisotry를 직렬화할떄 Player도 직렬화를 하는데
  4. Player안에서 다시 PlayerHisotry를 참조하고있다
  5. 따라서 스프링은 PlayerHistory,Player Bean중 어떤것을 먼저 만들어야할지 몰라서 에러가 난다고 볼 수 있다
  6. 그래서 계속 참조하다가 객체의 깊이가 초과치까지 깊어지다가 에러가 발생되는것이다

그럼 어떻게 해결가능한가?

  • 간단하다
  • 순환참조를 가지지 않게 하면되기 때문에 DTO로 변환해주면된다
      public void setPlayerHistoryDetailTeam(LolPlayerHistory lolPlayerHistory){
          redisTemplate.opsForValue().set(REDIS_NAME_PLAYER_HISTORY + lolPlayerHistory.getId(), LolPlayerHistoryResponseDetailDto.of(lolPlayerHistory));
      }
  • Entity -> DTO
  • 문제해결!