과거 카카오 테크 탬퍼스를 수강할 당시 강사님께서 DTO의 생성자를 통해 DTO 객체를 만들 때 깊은 복사를 해야 View애서 Converter 할 때 Lazy Loading 문제가 안생긴다는 말씀을 해주셨다.
이 때 깊은 복사가 뭐고, 그 반대의 개념은 얕은 복사는 무엇인지 그리고 왜 깊은 복사를 하지 않을 때 Lazy Loading이 발생하는지에 대해서 알아보자.
깊은 복사에 대해서 이야기 하기 전 우리는 먼저 Lazy Loading 방식을 알 필요가 있다.
간단한 아래의 예시를 통해서 이야기 해보자.
Post
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
public static Post from(String title){
return Post.builder()
.title(title)
.build();
}
}
User
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nickname;
}
Post와 User는 N:1, 즉 Many To One 관계이다.
이 때 지연 로딩으로 설정해두었는데, 지연 로딩이란 간단하게 설명을 하면 참조된 엔티티가 실제로 사용될 때 DB로의 쿼리를 통해서 가져오는 것이다.
그렇다면 실제로 사용될 때란 언제인가?
바로 해당 값이 필요할 때라고 이해를 하면 될 것 같다.
더 직관적으로 이야기 하면, getter를 통해서 해당 엔티티의 필드 값을 호출할 때를 의미한다.
예를 들어보자.
@Transactional(readOnly = true)
public PostResDTO getPostById(Long id) throws RuntimeException {
Post post = postRepository.findById(id).orElseThrow(() -> new RuntimeException("post가 존재하지 않습니다."));
return PostResDTO.from(post);
}
위와 같이 간단하게 Id 값으로 Post 엔티티를 조회하는 코드가 있다.
이 때 메서드의 반환 값으로 PostResDTO 객체의 정적 팩터리 메서드를 통해 객체를 생성 후에 리턴해준다.
PostResDTO의 코드를 봐보자.
@Getter
@Builder
public class PostResDTO {
private String title;
private User user;
public static PostResDTO from(Post post) {
return PostResDTO.builder()
.title(post.getTitle())
.user(post.getUser())
.build();
}
}
이 때 Post 엔티티의 필드 값들을 통해서 PostResDTO 객체를 생성 후에 리턴해준다.
좋다.
이러면 아무 문제 없는 코드 아닌가?
만약 이렇게 생각하시는 분이 있으시다면 아직 지연 로딩에 대한 개념이 조금 부족하다 할 수 있다.
위에서 설명한 내용을 기억하는가?
지연 로딩이란 간단하게 설명을 하면 참조된 엔티티가
실제로 사용될 때DB로의 쿼리를 통해서 가져오는 것이다.
위 코드에서는 지연로딩으로 설정된 User 엔티티가 실제로 사용되지 않는다.
위 get() 메서드를 통해서 User 엔티티를 가져왔지만, 이는 프록시 객체이다.
JPA는 지연로딩시 참조된 엔티티의 필드 값이 사용되기 전까지 해당 객체를 프록시 객체로 생성한다.
그렇기에 위 코드를 토대로 클라이언트에 Response를 하게 된다면, 그대로 에러가 나게된다. 그 이유로는 Jackson 라이브러리에서 프록시 객체를 JSON으로 직렬화를 할 수 없기 때문이다.
이러한 코드를 얕은 복사라고 칭한다.
반면 깊은 복사는 실제 엔티티의 필드의 값을 조회하는 코드를 의미한다.
@Getter
@Builder
public class PostResDTO {
private String title;
private User user;
public static PostResDTO from(Post post) {
return PostResDTO.builder()
.title(post.getTitle())
.user(post.getUser().getNickname())
.build();
}
}
위 코드를 통해서 User 엔티티의 필드값을 조회하는 getNickname() 코드가 호출될 때 DB로 쿼리를 날려 값을 가져오게 된다.