@RestController 사용 시 컨트롤러 단에서의 엔티티 반환에 대해..

Seongho·2021년 12월 22일

스프링부트

목록 보기
3/3

@RestController를 사용하면 컨트롤러에서 Json 형태로 응답 객체를 직렬화하(Json 형태로 만드는 것)게 된다. 이때 Entity 객체를 반환을 하게 되면 순환 참조 문제가 발생할 수 있다.

예를 들어..

@RestController
@RequiredArgsConstructor
@RequestMapping("/coupon")
public class CouponController {
    private final CouponService couponService;
    private final ResponseService responseService;
    private final JwtTokenProvider jwtTokenProvider;

    @PostMapping
    public Coupon createCoupon(@RequestBody CouponRequest couponRequest) {
        return couponService.createCoupon(couponRequest);
    }

이런 코드가 있을 때, 만약 누군가 쿠폰을 생성하는 Post 요청을 보내면 응답으로 생성한 Coupon 자체를 반환해주게 될 것이다.
여기서 Coupon이 다음과 같이 구성되어 있다고 해 보자.

@Entity
public class Coupon {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "coupon_id")
    private Long couponId;
    private String couponTitle;
    private Integer couponPercentage;
    private Boolean couponUsed = false;
    // 유저
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

쿠폰이 User를 ManyToOne 연관관계로 가지고 있다. 그리고 User가 다음과 같이 구성되어 있다고 해보자.

@Entity
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO) // MySQL은 IDENTITY 사용 가능
    @Column(name = "user_id")
    private Long userId;
    private String username;
    private String password;
    private String address;
    private String phone;

    @OneToMany(mappedBy = "user")
    private List<Coupon> coupons = new ArrayList<Coupon>();

User가 여러 개의 쿠폰을 리스트 형태로 가지게 된다.

이렇게 Coupon이 User를, User가 Coupon을 양방향 N:1 관계로 매핑을 하고 있다면, 맨 처음 createCoupon 메서드에서 Coupon을 Json 형태로 반환할 때 문제가 발생하게 된다.

Coupon을 Json 형태로 만들면 다음과 같을 것이다.

{
  couponId: 5,
  couponTitle: "쿠폰",
  couponPercentage: 5,
  couponUsed: false,
  user: {
          userId: 1,
          username: "admin",
          password: "zxkfq#$4sdkfskldrwr;kweljfk...",
          address: "서울특별시 ...",
          phone: "xxx-xxxx-xxx",
      	  coupons: [{
      		      couponId: 5,
                      couponTitle: "쿠폰",
                      couponPercentage: 5,
                      couponUsed: false,
                      user: {
                              userId: 1,
                              username: "admin",
                              password: "zxkfq#$4sdkfskldrwr;kweljfk...",
                              address: "서울특별시 ...",
                              phone: "xxx-xxxx-xxx",
                              coupons: [{
                              		  ...

이런 식으로, 유저는 쿠폰을 보여주려 하고, 쿠폰은 유저를 보여주려 하면서 Json이 무한히 길어지게 될 것이다.

이를 해결하기 위한 방법으로 가장 간단한건, 순환 참조가 일어나는 부분에 @JsonIgnore 라는 어노테이션을 붙여주는 방법이 있다. 예를 들어,

@Entity
public class Coupon {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "coupon_id")
    private Long couponId;
    private String couponTitle;
    private Integer couponPercentage;
    private Boolean couponUsed = false;
    // 유저
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    @JsonIgnore
    private User user;

이렇게 해주면 Json을 만들 때 user를 제외하게 되면서 순환참조가 끊기게 된다.

혹은 @JsonBackReference와 @JsonManagedReference를 사용할 수도 있다.

@Entity
public class Coupon {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "coupon_id")
    private Long couponId;
    private String couponTitle;
    private Integer couponPercentage;
    private Boolean couponUsed = false;
    // 유저
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    @JsonBackReference
    private User user;
@Entity
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO) // MySQL은 IDENTITY 사용 가능
    @Column(name = "user_id")
    private Long userId;
    private String username;
    private String password;
    private String address;
    private String phone;

    @OneToMany(mappedBy = "user")
    @JsonManagedReference
    private List<Coupon> coupons = new ArrayList<Coupon>();

이런 식으로 하게 되면 coupon의 user는 json에서 제외되고, user의 List<Coupon>은 유지된다.

그런데 이 방법보다는 Response 객체를 직접 만들어서 반환의 형식을 아예 정해주는게 좋지 않을까 생각한다. 예를 들어,

@Getter
@Builder
public class CouponResponse {
    private Long couponId;
    private String couponTitle;
    private Integer couponPercentage;
    private Boolean couponUsed;

    public static CouponResponse of(Coupon coupon) {
        CouponResponse couponResponse = CouponResponse.builder()
                .couponId(coupon.getCouponId())
                .couponTitle(coupon.getCouponTitle())
                .couponPercentage(coupon.getCouponPercentage())
                .couponUsed(coupon.getCouponUsed())
                .build();
                return couponResponse;
    }

이렇게 Response 클래스를 하나 만들어서 Coupon을 반환하는 대신 CouponResponse를 반환해주면, user 정보가 제외되어 순환참조 문제가 애초에 발생하지 않을 것이다.

예를 다시 들어보면..

@RestController
@RequiredArgsConstructor
@RequestMapping("/coupon")
public class CouponController {
    private final CouponService couponService;
    private final ResponseService responseService;
    private final JwtTokenProvider jwtTokenProvider;

    @PostMapping
    public CouponResponse createCoupon(@RequestBody CouponRequest couponRequest) {
        return CouponResponse.of(couponService.createCoupon(couponRequest));
    }

이게 맞는 방법인지는 모르겠지만 이런 느낌으로 하면 순환참조 문제를 해결할 수 있다.

0개의 댓글