본문 바로가기

JPA Entity를 JSON으로 변환할 때 발생할 수 있는 문제점과 해결방법

 이번에 프로젝트를 진행하면서 엔티티를 json으로 변환 후에 view로 전달해주는 과정에서 문제가 발생했었습니다.

일단 결론부터 말씀드리자면 DTO를 따로 만들어서 서비스에서 엔티티 대신 DTO를 리턴 해준 뒤 Json으로 변환 해줬습니다.

 

1. 무한 참조 루프

 Entity를 JSON으로 변환을 했더니 에러가 발생했습니다. 

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
이런 에러 메시지가 나왔는데 무슨 이유 인가 하니 엔티티 간의 관계 설정을 A -> B -> C -> A 이런 식으로 참조 해줬는데 무한루프가 발생해서 생기는 문제였습니다. 해결 방법으로는 
 
@Entity
@Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
public class Trainee extends BusinessObject {

    @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @Column(nullable = true)
    @JsonManagedReference
    private Set<BodyStat> bodyStats;
@Entity
@Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
public class BodyStat extends BusinessObject {

    @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name="trainee_fk")
    @JsonBackReference
    private Trainee trainee;

@JsonBackReference @JsonManagedReference 

두가지 어노테이션을 사용해주면 해당 에러는 해결이 되었습니다. 

 

2. Fetch Lazy까지 조회 ??

하지만 또 하나의 문제가 발생했는데 Json serialize 과정에서 엔티티의 모든 필드들을 맵핑 하려고 하면서 Fetch Lazy 설정한 필드들까지 추가로 조회를 해주는 문제가 발생했습니다. @JsonBackReference @JsonManagedReference는 무한 참조만 안되게 해주기 때문입니다.

해결 방법으로는 해당 필드위에 

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

@JsonIgnore를 붙여주면 됩니다. 그러면 Json serialize 과정에서 조회하지 않고 null로 값을 세팅해줍니다. 

하지만 이렇게하면 엔티티라는 성격의 클래스에 엔티티와 무관한 성격의 어노테이션이 붙고 어노테이션이 늘어나면서 코드가 더러워 보여서 따로 DTO를 만들어 주었습니다.

 

3. 결론은 추가로 DTO 생성

기존의 Shop 엔티티

@Entity
@Table(name = "shop")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(of = "id")
public class Shop {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String type;

    @Column(unique = true, nullable = false)
    private String name;

    @Column
    private String phone;

    @Column(nullable = false)
    private String address;

    @Column
    private Double lat;

    @Column
    private Double lng;

    @Column(nullable = false, columnDefinition = "boolean default true")
    private boolean status;

    @Column
    private Long open;

    @Column
    private Long close;

    @Column
    @Lob
    private String content;

    @Column(nullable = false, columnDefinition = "float default 0")
    private float rating;

    @CreationTimestamp
    @Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    private LocalDateTime regDate;

    @CreationTimestamp
    @Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    protected LocalDateTime updateDate;

    @Column(nullable = false, columnDefinition = "int default 0")
    private int reviewTotal;

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

    @ManyToMany
    @JoinTable(name = "shop_tag",
            joinColumns = @JoinColumn(name = "shop_id"),
            inverseJoinColumns = @JoinColumn(name = "tag_id"))
    private Set<Tag> tags;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "shop_id")
    private Set<ShopImage> shopImages;
}

 

새로 만들어준 ShopDto 필요한 필드들만 넣어준

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ShopDto {
    private Long id;
    private String type;
    private String name;
    private String address;
    private Double lat;
    private Double lng;
    private String phone;
    private Long open;
    private Long close;
    private String content;
    private float rating;
    private int reviewTotal;
    private ShopImage shopImage;
}

 

엔티티를 DTO로 변환해서 리턴해주는 과정.

@Override
@Transactional(readOnly = true)
public Page<ShopDto> getShops(List<String> types, List<String> filters, Pageable pageable) {
     Page<Shop> shops;
     List<ShopDto> shopDtos = new ArrayList<>();
     shops = shopRepository.findByTypesAndTags(types, filters, pageable);

     for (Shop origin : shops) {
     	ShopDto target = new ShopDto();
        BeanUtils.copyProperties(origin, target);
        
        if (pageable != null && origin.getShopImages().iterator().hasNext()) 
            target.setShopImage(origin.getShopImages().iterator().next());
          
        shopDtos.add(target);
      }

      return new PageImpl<ShopDto>(shopDtos, pageable, shops.getTotalElements());
}

DTO를 만들어주면서 Entity의 모든 필드가 아닌 필요한 필드들만 만들었습니다.

Entity -> DTO로 변환해주는 다른 방법으로는 

1. repository에서 처음부터 DTO로 받는 방법

2. Spring에서 제공해주는 Converter 인터페이스를 구현해서 변환

 

이렇게 있는데 1번은 코드가 복잡해져서 나중에 유지보수가 힘들어 보였고 

2번은 저같은 경우는 변환하는 과정이 복잡하지 않아서 BeanUtils.copyProperties를 사용했습니다. 

BeanUtils.copyProperties(A, B) 는 A객체와 B객체에서 이름과 타입이 일치하는 필드들을 A->B로 복사해주는 라이브러리 입니다. 이름 혹은 타입이 일치하지 않는 필드들의 값을 세팅 하고 싶을 때는 직접 set해줘야 합니다.

 

책이나 블로그를 보면 엔티티와는 별개로 DTO를 만들어서 작업하는게 좋다는 이야기를 여러번 봤는데 

그냥 엔티티로 하면 편한데 왜 굳이? 이렇게 생각했는데 이번에 DTO를 사용해야 하는 이유를 하나 더 알게 되었네요..

대부분의 경우 DTO를 따로 만들어서 작업하지만 간단한 경우는 Entity를 그대로 사용한다고 하네요. 

 

 

참조 : https://okky.kr/article/328445

'JPA' 카테고리의 다른 글

JPA - One To Many 단방향의 문제점  (0) 2019.12.28