2023-05-16-TIL
2023-05-16-TIL
Today I Learned
findAll vs findById
findById에서는 예외가 발생하지 않는데, findById에서만 바로 발생하는 이유?
이 질문은 조회 메서드마다 SQL이 실행되는 시점이 다르다는 데서 나온다. Thorben Janssen의 비교 글을 기준으로 정리하면 다음과 같다.
findById: 즉시 SELECT를 실행하고Optional<T>를 반환한다. 대상이 없으면 예외가 아니라 빈Optional이 오므로, 보통orElseThrow()로 그 자리에서 예외를 던진다.getReferenceById(이전의getOne,getById): SQL을 실행하지 않고 프록시만 돌려준다. 실제 조회는 식별자 외의 속성에 접근할 때 일어나고, 그때 대상이 없으면EntityNotFoundException이 발생한다. 그래서 예외가 호출 지점이 아니라 나중에 터진다.
findAll은 또 다른 문제다. Vlad Mihalcea는 모든 리포지토리가 상속받는 findAll()이 테이블 전체를 메모리에 올린 뒤 Java에서 거르는 코드를 부추긴다며 이를 안티 패턴이라고 부른다. 개발 데이터에서는 문제가 없다가 운영 데이터 규모에서 터진다는 것이 요지이고, 큰 테이블에는 JpaRepository 대신 필요한 메서드만 노출하는 기본 리포지토리를 쓰자고 제안한다. findAllById는 ID 목록으로 IN 조회를 하므로 반복문으로 findById를 부르는 것보다 낫다.
- https://vladmihalcea.com/spring-data-findall-anti-pattern/
- https://javatute.com/jpa/crudrepository-findallbyid-example-using-spring-boot/
- https://github.com/spring-projects/spring-data-redis/issues/2130
Spring Data JPA 성능 함정
Thorben Janssen은 Spring Data JPA에서 흔한 성능 함정 여섯 가지를 꼽는다. 개발 중에 실행되는 SQL을 보지 않는 것, 업데이트마다 saveAndFlush()를 불러 불필요한 flush를 만드는 것, 비효율적인 기본 키 생성 전략(AUTO, TABLE)을 쓰는 것, 즉시 로딩, 어떤 연관관계를 함께 가져올지 지정하지 않아 N+1이 생기는 것, 연관관계를 포함한 복잡한 인터페이스 프로젝션 때문에 결국 엔티티 전체를 조회하게 되는 것이다. 같은 저자의 다른 글은 위의 findById와 getReferenceById 차이를 다룬다.
- https://thorben-janssen.com/6-performance-pitfalls-when-using-spring-data-jpa/
- https://thorben-janssen.com/spring-data-findbyid-getone-getbyid-and-findone/
JPA 엔티티 연관관계
Thorben Janssen의 연관관계 모범 사례 글들이 강조하는 점은 다음과 같다. @ManyToMany에는 List 대신 Set을 써서 요소 하나를 지울 때 Hibernate가 연결 테이블의 행을 전부 지우고 다시 넣는 일을 피한다. CascadeType.REMOVE는 다대다에서 연결된 반대편 엔티티까지 지울 수 있어 위험하다. 양방향 관계에서는 연관관계의 주인(mappedBy가 없는 쪽)만 외래 키를 바꾸므로, 양쪽 객체를 함께 갱신하는 편의 메서드를 둔다. @OneToMany 단방향은 추가 UPDATE나 연결 테이블이 생기므로 @ManyToOne 쪽에서 매핑하는 편이 효율적이다.
- https://thorben-janssen.com/best-practices-for-many-to-many-associations-with-hibernate-and-jpa/
- https://thorben-janssen.com/best-practices-many-one-one-many-associations-mappings/
- https://ocwokocw.tistory.com/138
Spring Data JPA의 조건부 쿼리
조건이 있을 때만 WHERE에 넣는 조회는 메서드 이름 쿼리로 표현하기 어렵다. 레퍼런스 문서는 이를 위해 Specification, Query by Example, Querydsl 지원을 제공한다. 조건 조합이 적으면 JPQL에 (:name is null or m.name = :name) 같은 식을 쓰는 방법도 있지만, 인덱스를 제대로 타지 못하는 실행 계획이 나올 수 있어 조건이 늘어나면 동적 쿼리 도구로 옮기는 편이 낫다.
- https://medium.com/codex/how-to-easily-do-conditional-querying-with-spring-data-jpa-123079aa09a6
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
Spring Data JPA 레퍼런스
리포지토리 인터페이스 정의, 쿼리 메서드 키워드, 프로젝션, 감사(auditing), 엔티티 저장 시 새 엔티티 판단 방식 같은 기본 동작은 레퍼런스 문서와 저장소의 소스에서 확인할 수 있다. 블로그 글보다 먼저 확인할 기준 자료다.
- https://github.com/spring-projects/spring-data-jpa
추가, 수정, 삭제된 항목 구분하기
화면에서 목록 전체를 편집해 한 번에 저장할 때, 서버는 무엇이 추가되고 수정되고 삭제됐는지 가려내야 한다. 일반적인 방법은 식별자를 기준으로 기존 목록과 요청 목록을 비교하는 것이다.
- 요청에만 있는 항목(ID가 없거나 기존에 없는 ID): 추가
- 양쪽에 모두 있는 항목: 값이 달라졌으면 수정
- 기존에만 있는 항목: 삭제
1
2
3
4
5
6
7
Map<Long, Item> existing = repository.findAllByOwnerId(ownerId).stream()
.collect(Collectors.toMap(Item::getId, Function.identity()));
Set<Long> requestedIds = requests.stream()
.map(ItemRequest::id).filter(Objects::nonNull).collect(Collectors.toSet());
List<Item> toDelete = existing.values().stream()
.filter(item -> !requestedIds.contains(item.getId())).toList();
클라이언트가 항목마다 상태(CREATED, UPDATED, DELETED)를 붙여 보내는 방식도 있지만, 클라이언트를 신뢰해야 하고 동시 수정에 약하다. Power Automate 포럼 글은 이벤트 트리거에서 추가, 수정, 삭제를 구분하는 문제를 다룬다.
- https://powerusers.microsoft.com/t5/Building-Flows/When-an-event-is-added-updated-or-deleted-duplicate-action-when/m-p/598148#M78964
Java Record의 Compact Constructor
record의 정규 생성자는 모든 컴포넌트를 받아 필드에 대입한다. JEP 395는 이를 매개변수 목록을 생략한 compact 형태로 선언할 수 있게 한다. compact canonical constructor에서는 매개변수가 암묵적으로 선언되고, 생성자 본문이 끝난 뒤 필드 대입이 자동으로 이루어진다. 그래서 본문에는 검증과 정규화만 쓰면 된다. JEP에 따르면 본문에서 인스턴스 필드에 직접 대입하는 것은 컴파일 오류다. 매개변수 변수에 다시 대입하는 것은 가능하므로, 정규화한 값을 매개변수에 넣으면 그 값이 필드에 들어간다.
1
2
3
4
5
6
7
public record Range(int start, int end) {
public Range {
if (start > end) {
throw new IllegalArgumentException("start must not be greater than end");
}
}
}
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Record.html
- https://docs.oracle.com/en/java/javase/20/language/records.html
- https://openjdk.org/jeps/395
- https://www.digitalocean.com/community/tutorials/java-records-class
- https://www.youtube.com/watch?v=gJ9DYC-jswo
- https://blogs.oracle.com/javamagazine/post/java-record-compact-canonical-constructor
- https://stackoverflow.com/questions/74083123/java-record-compact-constructor-bytecode
- https://mikemybytes.com/2022/02/16/java-records-and-compact-constructors/
- https://blog.hexabrain.net/399
- https://blog.vvauban.com/blog/java-trick-compact-record-constructors
댓글
아직 댓글이 없습니다