2022-03-07-TIL
Today I Learned
Convert Entity to DTO
컨트롤러가 JPA 엔티티를 그대로 응답으로 내보내면 몇 가지 문제가 생긴다. 엔티티 필드가 바뀔 때마다 API 응답 형식이 함께 바뀌고, 지연 로딩(lazy loading) 연관 필드가 직렬화되면서 예상치 못한 쿼리나 예외가 생길 수 있으며, 노출하면 안 되는 필드까지 나갈 수 있다. 그래서 응답 전용 DTO로 변환해서 내보낸다.
변환하는 방법은 크게 세 가지다.
- DTO에 정적 팩토리 메서드(
from(entity))나 생성자를 두고 직접 옮긴다. 코드는 늘지만 무엇이 어디로 가는지 명확하다. - ModelMapper, MapStruct 같은 매핑 라이브러리를 쓴다. 반복 코드는 줄지만 필드 이름이 어긋나면 조용히 빠질 수 있어 테스트가 필요하다.
- 처음부터 DTO로 조회한다. Spring Data JPA 프로젝션은 조회할 필드를 담은 DTO 클래스를 반환 타입으로 쓸 수 있게 해 준다. 이때 DTO는 생성자를 하나만 선언해야 Spring Data가 어떤 속성을 채울지 판단할 수 있다.
1
2
3
4
5
public record MemberResponse(Long id, String name) {
public static MemberResponse from(Member member) {
return new MemberResponse(member.getId(), member.getName());
}
}
- https://www.amitph.com/spring-entity-to-dto/
Lombok and JPA
Lombok을 JPA 엔티티에 쓸 때 문제가 되는 애너테이션은 주로 @Data, @EqualsAndHashCode, @ToString이다.
@ToString이 연관 엔티티 필드까지 포함하면, 양방향 연관에서 서로의toString()을 계속 불러StackOverflowError가 나거나, 지연 로딩 필드를 건드려 의도하지 않은 쿼리가 나간다.@EqualsAndHashCode가 모든 필드를 쓰면 같은 문제에 더해 엔티티의 동등성 기준이 흔들린다.
Hibernate 사용자 가이드는 ORM에서 equals()와 hashCode()를 구현할지, 어떻게 구현할지가 의외로 까다로운 문제라고 말한다. 반드시 구현해야 하는 경우는 복합 식별자처럼 식별자 역할을 하는 클래스뿐이고, 그 밖에는 아예 구현하지 않는 것도 고려하라고 한다. Hibernate는 한 세션 안에서는 같은 행에 대해 같은 인스턴스를 돌려주므로 기본 equals로도 충분하다. 문제는 여러 세션에서 가져온 인스턴스를 Set에 섞어 넣을 때이고, 가이드는 식별자 기반으로 구현하더라도 자동 생성 식별자는 저장 전에 null이라서 저장 전후로 hashCode가 달라지는 함정을 예제로 보여 준다.
그래서 엔티티에는 @Getter와 @NoArgsConstructor(access = AccessLevel.PROTECTED) 정도만 쓰고, @Data는 피하며, equals/hashCode가 필요하면 기준을 정해 직접 작성하는 편이 안전하다. JPA 명세가 엔티티에 public 또는 protected 기본 생성자를 요구하므로 protected 기본 생성자를 둔다(Jakarta Persistence 3.1).
- https://dzone.com/articles/lombok-and-jpa-what-may-go-wrong
QueryDSL with Spring Data JPA
Spring Data JPA의 메서드 이름 쿼리와 @Query 문자열만으로는 동적 조건이 많은 검색 쿼리를 다루기 어렵다. QueryDSL은 이 문제를 자바 코드로 쿼리를 작성하는 방식으로 푼다.
Querydsl 레퍼런스는 Querydsl이 HQL 쿼리를 타입 안전하게 유지하려는 필요에서 나왔다고 설명한다. 문자열을 이어 붙여 쿼리를 만들면 읽기 어렵고, 도메인 타입과 속성을 문자열로 참조하니 안전하지 않았다. Querydsl은 애너테이션 프로세서(JPAAnnotationProcessor)로 엔티티마다 QMember 같은 쿼리 타입을 생성하고, 이 타입으로 쿼리를 만든다. 그래서 필드 이름을 바꾸면 쿼리가 컴파일 단계에서 깨지고, IDE 자동 완성도 쓸 수 있다.
1
2
3
4
List<Member> members = queryFactory
.selectFrom(member)
.where(member.name.eq(name), member.age.goe(20))
.fetch();
Spring Data JPA와 함께 쓸 때는 보통 사용자 정의 리포지토리 인터페이스를 만들고, 구현 클래스에서 JPAQueryFactory로 QueryDSL 쿼리를 작성한 뒤 기본 리포지토리가 그 인터페이스를 함께 상속하게 한다.
- https://jaime-note.tistory.com/79
- https://jessyt.tistory.com/14
- https://jessyt.tistory.com/16
- https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/
Entity with Serializable
엔티티에 Serializable을 꼭 구현해야 할까? Jakarta Persistence 명세의 엔티티 클래스 요구 사항을 보면, 엔티티 인스턴스를 원격 인터페이스 등을 통해 분리된(detached) 객체로 값 전달해야 하는 경우에 Serializable을 구현해야 한다고 되어 있다. 즉 필수 조건이 아니라 그런 상황에서만 필요하다.
반면 복합 키를 표현하는 기본 키 클래스는 명세상 반드시 직렬화 가능해야 하고, equals와 hashCode도 정의해야 한다. 엔티티를 HTTP 세션이나 원격 캐시에 그대로 넣는 경우에도 직렬화가 필요해진다. 반대로 엔티티를 API 응답이나 캐시에 직접 쓰지 않고 DTO로 변환한다면 엔티티 자체를 직렬화할 일은 드물다.
- https://www.inflearn.com/questions/17117
댓글
아직 댓글이 없습니다