RunGuard 개발 중 이슈들을 정리해보고자 한다.
1. DTO 변환의 핵심 이유
- DTO : 데이터를 전달하기 위한 객체
- JPA에서 entity 그대로 반환하면 지연 로딩 (LAZY) 설정된 프록시 객체를 직렬화하지 못해서 LazyInitializationException이나, 무한 참조 문제가 발생함
@Entity
public class Member {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders;
}
예를 들어 위와 같은 엔티티가 있을 때, orders는 LAZY임
즉, 멤버를 조회할 때는 실제 orders 데이터가 아직 DB에서 조회되지 않음
그래서 orders 자리에 진짜 List가 아니라, 프록시 객체를 넣어둠 = 가짜 객체
-> Hibernate가 orders는 나중에 필요해지면 그때 DB에서 가져와야겠다고 생각
@GetMapping("/members/{id}")
public Member getMember(@PathVariable Long id) {
return memberRepository.findById(id).get();
}
member 객체를 JSON으로 바꾸려고 함.
이때 orders도 같이 읽으려고 하는데 아직 프록시 상태라서 DB 연결 = 영속성 컨텍스트가 닫혀 있으면 발생하는게
LazyInitializationException 임
=> 그래서 데이터를 화면에 필요한 만큼만 담은 DTO로 변환해야함 - Data Transfer Object
< 정리>
1. LAZY 연관관계는 실제 데이터 대신 프록시로 남아 있음
2. Controller가 entity를 JSON으로 변환하려고 함
3. JSON 변환기가 프록시 객체까지 읽는거야
4. 근데 DB 세션이 닫혀있으니 실제 데이터를 가져올 수 없네
5. 그래서 발생하는게 LazyInitializationException
LAZY 프록시 객체를 직렬화하지 못한다는 말은, Entity 안에 아직 DB에서 실제로 가져오지 않은 가짜 연관 객체가 있는데, 이걸 JSON 응답으로 바꾸려다가 DB 세션이 닫혀 있거나 구조가 복잡해서 오류가 난다는 것
2. 직렬화
- JAVA 객체를 응답 JSON으로 바꾸는 과정
orders가 실제 데이터가 아니라 Hibernate 프록시라서 JSON 변환기가 제대로 처리하지 못할 수 있음
그래서 DTO를 쓰는 것
3. 사용 이유
1) Entity를 직접 노출하지 않기 위함
-> DTO에는 필요한 값만 담음
2) LAZY 로딩 문제 피하기 위함
-> DTO로 변환하면 필요한 필드만 꺼내므로 불필요한 연관관계 건드리지 않음
3) 무한 참조 문제 를 막기 위함
4. DTO 변환 과정
- Request DTO : 클라이언트 -> 서버
- Response DTO : 서버 -> 클라이언트
- Entity를 DTO로 변환
Member member = memberRepository.findById(id).get();
MemberResponseDto dto = new MemberResponseDto(
member.getId(),
member.getEmail(),
member.getName()
);
member.getId() → dto.id
member.getEmail() → dto.email
member.getName() → dto.name
Controller에서 DTO 반환
@GetMapping("/members/{id}")
public MemberResponseDto getMember(@PathVariable Long id) {
Member member = memberRepository.findById(id).get();
return new MemberResponseDto(
member.getId(),
member.getEmail(),
member.getName()
);
}
Q. 그러면 만약 갑자기 LAZY 설정된 프록시 객체가 필요해지면 어떡해?
1. @Transactional 안에서 접근하기
@Transactional(readOnly = true)
public MemberDetailResponseDto getMemberDetail(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다."));
return MemberDetailResponseDto.from(member);
}
@Transactional이 있으면 Service 메서드 안에서는 영속성 컨텍스트가 살아 있음
그래서 members.getOders()하면 그 순간 DB에서 가져올 수 있음
단, 이 방법은 회원이 여러 명일 때 조심해야함. 반복문 안에서 getOders() 호출하면 쿼리 많이 나갈 수 있음
=> 이걸 N+1 문제라고 함. (뒤에서 자세히 다뤄보자)
2. fetch join으로 처음부터 같이 조회하기
- Member 조회할 때 orders까지 한 번에 가져오기
3. EntityGraph로 특정 조회에서만 orders를 같이 가져오게 함
'프로젝트 이슈' 카테고리의 다른 글
| RunGuard 개발 - 캐시 무효화 검증 (0) | 2026.07.10 |
|---|---|
| RunGuard 개발 - k6 & Tomcat thread & HikariCP connection pool (0) | 2026.07.08 |
| RunGuard 개발 로컬 캐시(Caffeine) (0) | 2026.07.08 |
| RunGuard 개발 EXPLAIN (0) | 2026.07.08 |
| RunGuard 개발 예외처리 (0) | 2026.07.08 |