@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));
}
이게 맞는 방법인지는 모르겠지만 이런 느낌으로 하면 순환참조 문제를 해결할 수 있다.