Spring boot JPA 지연 로딩으로 인한 Type definition error 해결

김민우·2023년 2월 28일
0

잡동사니

목록 보기
11/22

다른 엔티티와 일대다 관계를 가진 엔티티를 조회하는 과정에서 다음 문제가 발생했다.

구글링을 해보니 지연로딩으로 인해 연관관계를 가지고 있는 엔티티가 제때 호출이 안된 것이였다. 조회하려는 엔티티와 이와 연관관계를 맺고 있는 엔티티는 다음과 같다.

StudyCafe (조회하려는 엔티티)

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class StudyCafe {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "study_cafe_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "owner_id")
    private Owner owner;

    private String name;
    private Integer minUsingTime;
    private LocalTime openTime;

    private LocalTime closeTime;
    private String address;
    private String comment;

    @Builder
    public StudyCafe(Long id, Owner owner, String name, Integer minUsingTime, LocalTime openTime, LocalTime closeTime, String address, String comment) {
        this.id = id;
        this.owner = owner;
        this.name = name;
        this.minUsingTime = minUsingTime;
        this.openTime = openTime;
        this.closeTime = closeTime;
        this.address = address;
        this.comment = comment;
    }

    public void update(String name, Integer minUsingTime, LocalTime openTime, LocalTime closeTime, String address, String comment) {
        this.name = name;
        this.minUsingTime = minUsingTime;
        this.openTime = openTime;
        this.closeTime = closeTime;
        this.address = address;
        this.comment = comment;
    }
}
  • Owner 와 일대다 관계를 가지고 있으며 지연 로딩을 설정되어 있다

Owner (위 엔티티와 일대다 연관 관계를 맺고있는 엔티티)

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Owner {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "owner_id")
    private Long id;
    private String email;
    private String password;
    private String nickname;
    private String name;
    private LocalDate birthday;

    @Builder
    public Owner(Long id, String email, String password, String nickname, String name, LocalDate birthday) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.name = name;
        this.birthday = birthday;
    }
}

이 오류가 발생한 이유는 컨트롤러에서 엔티티를 DTO로 변환 시 필드 값이자 엔티티인 Owner 를 건들지 않았기 때문이다. 기존 컨트롤러에서 호출한 메소드는 다음과 같다.

    @GetMapping("/studycafe/{cafeId}")
    public StudyCafeDto studyCafe(@PathVariable Long cafeId, Model model) {

        StudyCafe studyCafe = studyCafeService.findById(cafeId);


        StudyCafeDto studyCafeDto = StudyCafeDto.builder()
                                        .id(studyCafe.getId())
                                        .owner(studyCafe.getOwner())
                                        .name(studyCafe.getName())
                                        .minUsingTime(studyCafe.getMinUsingTime())
                                        .openTime(studyCafe.getOpenTime())
                                        .closeTime(studyCafe.getCloseTime())
                                        .address(studyCafe.getAddress())
                                        .comment(studyCafe.getComment())
                                    .build();

        return studyCafeDto;
    }
}

현재 @Builder 를 통해 엔티티를 DTO로 변환하고 있는데 studyCafe.getOwner() 를 자세히 보자. 이를 사용하면 실제 Owner 객체가 오는 것이 아닌 지연 로딩으로 인해 Owner 의 자식 클래스인 프록시 객체가 대신 들어가게 되며 Owner 가 실제 사용되는 시점에 프록시 객체에서 실제 객체로 변환된다.

즉, 내가 이 상황에서 실제 Owner 객체를 사용하기 위해선 studyCafe.getOwner.XXX 와 같이 Owner 에 대해 어떤 작업을 수행했어야 했다.

따라서, 반환되는 studyCafeDtoOwner 필드는 프록시 객체가 들어가 포스트맨으로 테스트 시 정상적으로 객체가 주입되지 않았던 것이다.

컨트롤러의 해당 메소드 부분을 다음과 같이 수정했다.

    @GetMapping("/studycafe/{cafeId}")
    public StudyCafeViewDto studyCafe(@PathVariable Long cafeId, Model model) {

        StudyCafe studyCafe = studyCafeService.findById(cafeId);
        StudyCafeViewDto studyCafeViewDto = StudyCafeViewDto.of(studyCafe);

        return studyCafeViewDto;
    }
  • DTO가 어떤 상황에서 쓰이는 지를 더 명확하게 하기위해 이름을 StudyCafeViewDto로 수정
  • StudyCafeViewDto 에는 엔티티 -> DTO(자기 자신) 로 변환해주는 정적 메소드 of가 존재한다.

StudyCafeViewDto

@Getter
@Setter
@ToString
public class StudyCafeViewDto {

    private Long id;
    private OwnerDto owner;
    private String name;
    private Integer minUsingTime;
    private LocalTime openTime;
    private LocalTime closeTime;
    private String address;
    private String comment;

    @Builder
    public StudyCafeViewDto(Long id, OwnerDto owner, String name, Integer minUsingTime, LocalTime openTime, LocalTime closeTime, String address, String comment) {
        this.id = id;
        this.owner = owner;
        this.name = name;
        this.minUsingTime = minUsingTime;
        this.openTime = openTime;
        this.closeTime = closeTime;
        this.address = address;
        this.comment = comment;
    }

    public static StudyCafeViewDto of(StudyCafe studyCafe) {
        return StudyCafeViewDto.builder()
                .id(studyCafe.getId())
                .owner(OwnerDto.of(studyCafe.getOwner()))
                .name(studyCafe.getName())
                .minUsingTime(studyCafe.getMinUsingTime())
                .openTime(studyCafe.getOpenTime())
                .closeTime(studyCafe.getCloseTime())
                .address(studyCafe.getAddress())
                .comment(studyCafe.getComment())
                .build();
    }

    public StudyCafe toEntity() {
        return StudyCafe.builder()
                .id(id)
                .owner(Owner.builder()
                        .id(owner.getId())
                        .email(owner.getEmail())
                        .password(owner.getPassword())
                        .nickname(owner.getNickname())
                        .name(owner.getName())
                        .birthday(owner.getBirthday())
                        .build())
                .name(name)
                .minUsingTime(minUsingTime)
                .openTime(openTime)
                .closeTime(closeTime)
                .address(address)
                .comment(comment)
                .build();
    }
}
  • DTO에서 실제 Owner 엔티티를 필드로 사용하지 않고 별도의 DTO(OwnerDto)를 만들어 이를 사용했다.
  • OwnerDto 에는 엔티티(Owner) -> DTO(자기 자신)으로 바꿔주는 정적 메소드가 존재한다.
    • 바로 이 과정에서 프록시 객체가 실제 객체로 바뀌면서 DTO에 저장되는 것이다.

OwnerDto

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class OwnerDto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "owner_id")
    private Long id;
    private String email;
    private String password;
    private String nickname;
    private String name;
    private LocalDate birthday;

    @Builder
    public OwnerDto(Long id, String email, String password, String nickname, String name, LocalDate birthday) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.name = name;
        this.birthday = birthday;
    }

    public static OwnerDto of(Owner owner) {
          return OwnerDto.builder()
                        .id(owner.getId())
                        .email(owner.getEmail())
                        .password(owner.getPassword())
                        .nickname(owner.getNickname())
                        .name(owner.getName())
                        .birthday(owner.getBirthday())
                        .build();
    }

    @Override
    public String toString() {
        return "Owner{" +
                "id=" + id +
                ", email='" + email + '\'' +
                ", password='" + password + '\'' +
                ", nickname='" + nickname + '\'' +
                ", name='" + name + '\'' +
                ", birthday=" + birthday +
                '}';
    }
}
  • of 메소드를 보면 owner.getXXX() 를 통해 프록시 객체가 실제 객체로 바뀌게 된다.

이제 정상적으로 호출되는지 다시 테스트 해보면 정상적으로 동작하는 것을 알 수 있다.

서로 연관관계를 맺고 있는 엔티티에서 지연 로딩으로 설정되있는 경우 이런 상황을 조심해야 한다. 이러한 문제 때문에 엔티티를 DTO로 변환하는 과정이 꼭 필요하단 것을 다시 한 번 느낄 수 있었다.

0개의 댓글