본문 바로가기
Error

[Java / Spring / JPA / Error] - 순환참조

by nam_ji 2024. 5. 21.

순환참조 해결하기

순환참조란

  • 스프링 순환 참조 (Circular Reference)란 서로 다른 빈들이 서로 참조를 맞물리게 주입되면서 생기는 현상입니다. beanA에서 beanB를 참조하게 되는데 beanB에서도 beanA를 참조해야 하는 경우 문제가 생기게 됩니다.

순환참조가 발생 시점

1. 문제 엔티티 구조

  • 개인 프로젝트를 진행하면서 마주하게 되었습니다.
  • domain으로 group과 device가 있고, group과 device는 1 : N / device와 group은 N : 1 관계로 설정하면서 발생하게 되었습니다.
    • 엔티티 구성
      더보기
      더보기
      // Group 엔티티
      package com.namji.datacollection.entity;
      
      import jakarta.persistence.*;
      import java.util.ArrayList;
      import java.util.List;
      import lombok.AllArgsConstructor;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      
      @Entity
      @Getter
      @NoArgsConstructor
      @AllArgsConstructor
      @Table(name = "dataGroups")
      public class Group extends TimeStamp {
      
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long groupId;
      
        @Column(nullable = false, unique = true)
        private String groupSerial;
      
        @OneToMany(mappedBy = "group", fetch = FetchType.LAZY)
        private List<Device> devices = new ArrayList<>();
      
        public Group(String groupSerial) {
          this.groupSerial = groupSerial;
        }
      }
      
      // Device 엔티티
      package com.namji.datacollection.entity;
      
      import jakarta.persistence.*;
      import lombok.AllArgsConstructor;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      
      @Entity
      @Getter
      @NoArgsConstructor
      @AllArgsConstructor
      @Table(name = "devices")
      public class Device extends TimeStamp {
      
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long deviceId;
      
        @Column(nullable = false, unique = true)
        private String serialNumber;
      
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "group_id", nullable = false)
        private Group group;
      
      
        public Device(String serialNumber, Group group) {
          this.serialNumber = serialNumber;
          this.group = group;
        }
      }
  • 문제는
    • Group을 로딩하면 Device 리스트를 로딩하려고 시도합니다. 
    • 각 Device를 로딩하면 다시 Group을 로딩하려고 시도합니다.
    • 이렇게 반복되면서 끝없는 순환 참조가 발생하게 되었습니다.

2. 요청 시 응답

  • 위 문제들로 인해 device 등록 요청 시
    {
        "message": "success",
        "data": {
            "deviceId": 1,
            "serialNumber": "WX1",
            "group": {
                "createAt": "2024-05-21T02:19:53.206093",
                "groupId": 1,
                "groupSerial": "SG1",
                "devices": [
                    {
                        "createAt": "2024-05-21T02:19:59.8418857",
                        "deviceId": 1,
                        "serialNumber": "WX1",
                        "group": {
                            "createAt": "2024-05-21T02:19:53.206093",
                            "groupId": 1,
                            "groupSerial": "SG1",
                            "devices": [
                                {
                                    "createAt": "2024-05-21T02:19:59.8418857",
                                    "deviceId": 1,
                                    "serialNumber": "WX1",
                                    "group": {
                                        "createAt": "2024-05-21T02:19:53.206093",
                                        "groupId": 1,
                                        "groupSerial": "SG1",
                                        "devices": [
                                            {
                                                "createAt": "2024-05-21T02:19:59.8418857",
                                                "deviceId": 1,
                                                "serialNumber": "WX1",
                                                "group": {
                                                    "createAt": "2024-05-21T02:19:53.206093",
                                                    "groupId": 1,
  • 이렇게 응답하는 순환 참조 문제가 생겼습니다.

3. 설명

  1. 요청 받은 비즈니스 로직을 처리하고
  2. 응답을 json으로 변환하는 과정에서 jackson이 사용됩니다.
    1. jackson 동작에 의해
    2. device의 Group 객체를 직렬화합니다.
    3. Group의 변수 중 List<Device>가 존재하기 때문에 Device를 직렬화합니다.
    4. 이 과정이 계속 반복되게 됩니다.
  3. 위와 같은 과정에 의해 순환 참조 문제가 발생하게 됐습니다.

해결 방안

1. @JsonIgnore

  • 적용 범위는
    @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})

    이렇게 되어 있습니다.
  • 역할로 직렬화 하는 대상에서 제외하는 역할을 합니다.

 

2. @JsonManagedReference, @ JsonBackReference

  • 두 어노테이션의 역할은 양방향 연결에서 상호 참조를 해결할 수 있습니다.
    • @JsonManagedReference
      정상적으로 직렬화됩니다.
    • @JsonBackReference
      직렬화 되지 않도록 막습니다.
  • 저는 2번째 방법을 이용하여 문제를 해결했습니다.
    • 엔티티 수정
      더보기
      더보기
      // Group 엔티티
      package com.namji.datacollection.entity;
      
      import com.fasterxml.jackson.annotation.JsonIgnore;
      import com.fasterxml.jackson.annotation.JsonManagedReference;
      import jakarta.persistence.*;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.Set;
      import lombok.AllArgsConstructor;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      
      @Entity
      @Getter
      @NoArgsConstructor
      @AllArgsConstructor
      @Table(name = "dataGroups")
      public class Group extends TimeStamp {
      
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long groupId;
      
        @Column(nullable = false, unique = true)
        private String groupSerial;
      
        @OneToMany(mappedBy = "group")
        @JsonManagedReference
        private List<Device> devices = new ArrayList<>();
      
        public Group(String groupSerial) {
          this.groupSerial = groupSerial;
        }
      }
      
      // Device 엔티티
      package com.namji.datacollection.entity;
      
      import com.fasterxml.jackson.annotation.JsonBackReference;
      import jakarta.persistence.*;
      import lombok.AllArgsConstructor;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      
      @Entity
      @Getter
      @NoArgsConstructor
      @AllArgsConstructor
      @Table(name = "devices")
      public class Device extends TimeStamp {
      
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long deviceId;
      
        @Column(nullable = false, unique = true)
        private String serialNumber;
      
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "group_id", nullable = false)
        @JsonBackReference
        private Group group;
      
      
        public Device(String serialNumber, Group group) {
          this.serialNumber = serialNumber;
          this.group = group;
        }
      }
 
  • 수정 후 결과
    {
        "message": "success",
        "data": {
            "deviceId": 1,
            "serialNumber": "WX12345",
            "group": {
                "createAt": "2024-05-22T15:22:33.135076",
                "groupId": 1,
                "groupSerial": "SG1",
                "devices": [
                    {
                        "createAt": "2024-05-22T15:22:35.3658705",
                        "deviceId": 1,
                        "serialNumber": "WX12345"
                    }
                ]
            },
            "createdAt": "2024-05-22T15:22:35.3658705"