웹 계층 개발
Validation
Form객체나 DTO 객체에 @NotEmpty 등등 Validation 어노테이션을 적용할 수 있다.
Controller Parameter 앞에는 @Valid를 표기하여 Validation을 사용할 수 있다.
@Valid와 BindingResult를 함께 쓰면 화면으로 Validation 정보를 보낼 수 있다.
Entity와 DTO
- SSR 템플릿 엔진의 경우 컨트롤러에서 Entity를 반환해도 딱히 문제가 없다.
- API를 만들 때는 절대로 컨트롤러에서 Entity를 반환하면 안된다.
- 노출하고싶지 않은 정보를 노출하게 될 수도 있고
- Entity를 변경할 때 API스펙도 함께 바뀌어버리기 때문
기타
- IntelliJ에서 Ctrl+E를 누르면 최근 파일들을 조회할 수 있음.
- IntelliJ에서 Ctrl+Alt+Shift+T를 누르면 Refactoring 옵션이 뜬다.
- IntelliJ에서 Ctrl+Alt+N을 누르면 Inline Variable로 바뀐다.
- Thymeleaf에서 for문은
th:each="member : ${members}"
로 사용할 수 있다. - Thymeleaf에서
th:text="${member.address?.city}"
와 같이 ?를 사용하여 NVL 효과를 볼 수 있다.
변경 감지와 병합(Merge)
결론은 merge는 의도치 않은 변경을 일으킬 수 있기 때문에 실무에서 절대로 쓰지 않는다.
또한 컨트롤러단에서 어설프게 Entity를 만드는것도 유지보수를 어렵게 하는 안티패턴이다. Entity와 Setter를 사용하기보다는 파라미터나 DTO를 활용하여 묶어서 업데이트 하는 것이 좋다.
준영속 엔티티
- 영속성 컨텍스트가 더는 관리하지 않는 엔티티를 말한다.
- 예를 들어 수정화면에서 넘어온 객체는 이미 데이터베이스에 저장되어 식별자를 가지고 있으므로 준영속 엔티티로 볼 수 있다.
- 준영속 엔티티를 수정하려면 "변경 감지(Dirty Checking)"를 사용하거나, "병합(Merge)"을 사용해야 한다.
- 변경 감지(Dirty Checking): 식별자로 영속화된 엔티티를 불러와서 변경
- 병합(Merge): 영속성 컨텍스트에서 식별자로 엔티티를 찾아서 값을 전부 덮어 씀
API 개발 기본
@Controller + @ResponseBody = @RestController
- Entity를 컨트롤러 파라미터로 사용해서는 절대 안된다. 엔티티가 바뀌었을 때 API 스펙에 영향을 주기 때문에 API의 파라미터로는 별도의 DTO를 생성하는것이 좋다.
- PUT은 멱등하다. 예를들어 NAME을 "ABC"로 10번 바꾼 결과와 1번 바꾼 결과가 동일하다. 등록과 수정은 API 스펙이 다르다. 따라서 DTO도 별도로 만들어준다.
- 커맨드와 쿼리를 분리하자. 업데이트를 하고 나서 반환값으로 Entity를 반환하는것은 지양하고 필요 시 ID값만 리턴한다.
- 목록 조회 기능에서 리스트를 그대로 반환 시 JSON이 Array로 생성되고, 이로 인해 API 확장이 불가능해진다. 따라서 목록 조회 시에도 Entity를 그대로 반환하지 않는것은 물론이거니와, 아래 클래스와 같은 데이터를 필드로 가지는 객체형태로 반환해야 한다.
@Data
@AllArgsConstructor
static class Result<T> {
private T data;
}
API 개발 고급
- 지연 로딩과 조회 성능 최적화
- 컬렉션 조회 최적화
- 페이징과 한계 돌파
- OSIV(OpenSessionInView)의 성능 최적화
지연 로딩과 조회 성능 최적화
컨트롤러에서 엔티티를 응답할 경우 양방향 연관관계에서는 반드시 둘 중 하나에 @JsonIgnore 어노테이션을 붙여서 연결을 끊어줘야 한다. 그렇지 않으면 무한 루프를 돌게 된다. 무한 루프를 해결해도 Proxy 관련 클래스(ByteBuddyInterceptor)의 Type Definition 관련 오류가 발생한다.
결국 Hibernate5 모듈 빈 정의를 작성하여 사용해야 하는데 이 모든것을 하는것보다는 Entity를 DTO로 변환하여 작성하는것이 가장 좋은 방법이다.
DTO를 반환해도 N+1 문제가 발생하게 되는데, 그렇다고 fetch 옵션을 EAGER로 바꿔서는 안되고, 모두 LAZY로 유지한 채로 JPQL로 페치조인을 사용하여 쿼리를 개선한다.
쿼리 성능 별 구현방식: V1~V4
// ... 무한루프 발생
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
return all;
}
// ... N+1 문제 발생
@GetMapping("/api/v2/simple-orders")
public Result orderV2() {
List<Order> orders = orderRepository.findAllByCriteria(new OrderSearch());
List<OrderSimpleQueryDto> collect = orders.stream()
.map(OrderSimpleQueryDto::new)
.collect(Collectors.toList());
return new Result(collect);
}
// 가장 권장되는 방식! 필요 시 fetch join을 사용하여 성능을 최적화 한다.
@GetMapping("/api/v3/simple-orders")
public Result orderV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<OrderSimpleQueryDto> collect = orders.stream()
.map(OrderSimpleQueryDto::new)
.collect(Collectors.toList());
return new Result(collect);
}
@GetMapping("/api/v4/simple-orders")
public Result orderV4() {
// 성능면에서는 좋지만 Repository에서 DTO로 직접 받는 것은 재사용성이 조금 떨어진다
// 필요할 경우 아래와 같이 Repository를 분리하여 활용한다
return new Result(orderSimpleQueryRepository.findOrderDtos());
}
페이징과 한계 돌파
application.yml에 default_batch_fetch_size 속성은 LAZY 로딩에서 IN절을 사용하도록 지정하는것이다. 해당 속성이 100이면, IN절 안에 최대 100개의 값을 넣어 쿼리를 반복수행한다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
개별 최적화를 원할 경우 엔티티의 컬렉션 속성 값 @BatchSize 어노테이션을 달아 주면 된다. 컬렉션이 아닌 XToOne 관계는 엔티티 클래스 자체에 지정한다.
이렇게 함으로써 아래와 같은 장점이 생긴다.
- 이 조건으로 쿼리 호출 수가 [1 + N] :arrow_right: [1 + 1]로 줄어든다.
- 중복 데이터가 없으므로 DB 데이터 전송량이 최적화 된다.
- 페이징이 가능해진다.
이렇듯 하이버네이트가 자동으로 처리 해주지만 XToOne 관계는 페이징에 영향을 주지 않기 때문에 fetch join을 적용해주는게 좋다.
데이터베이스 중 IN절의 제한이 있는 경우가 있기 때문에 애플리케이션과 DB의 순간 부하도 고려하여 값은 100~1000 사이로 선택하자. 참고로 JVM 메모리 사용량은 fetch 사이즈와 무관하다.
Repository단에서 DTO를 전달받는 방법도 있다. 이 방법은 필요한 컬럼만 SELECT하기 때문에 전송 데이터 양이 확연히 줄어든다는 장점이 있지만, 그 만큼 코드가 길어진다는 단점이 있다. 따라서 꼭 필요한 경우가 아니라면 fetch join + fetch batch를 사용한 방법이 권장된다.
데이터를 플랫화 시켜서 쿼리 단 한 번만으로 가져오는 방법도 있다. 이 방법 역시 추가작업이 필요하여 코드가 길어지며, 페이징 또한 불가능해진다.
권장 설계 순서
- 엔티티 조회 방식으로 우선 접근
- 페치 조인으로 쿼리 수를 최적화
- 컬렉션 최적화
- 페이징 필요O :arrow_right: hibernate.default_batch_fetch_size or @BatchSize로 최적화
- 페이징 필요X :arrow_right: 페치 조인 사용
- 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
- DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate
Open Session In View(OSIV)
기본적으로 OSIV는 활성화 되어있으며, spring.jpa.open-in-view: false
옵션으로 비활성화 할 수 있다. 참고로 여기서 Session은 하이버네이트의 Session(= JPA의 EntityManager)을 의미한다.
OSIV가 활성화 되어 있을 경우 영속성 컨텍스트의 생존 범위가 넓어 View Template에서 까지도 지연로딩을 사용할 수 있다는 장점이 있다. 하지만 커넥션이 너무 오래 살아있어 커넥션 풀 고갈(=장애)로 이어질 수 있다.
OSIV를 비활성화 하게 되면 영속성 컨텍스트의 생존 범위는 트랜잭션 안으로 제한되며 따라서 모든 지연 로딩을 트랜잭션 안으로 넣어야 된다.
해결 방법
Command와 Query를 서비스 레벨에서 분리한다.
OrderService
- OrderService: 핵심 비즈니스 로직
- OrderQueryService: 화면이나 API에 맞춘 서비스(주로 읽기 전용 트랜잭션 사용)
'Backend' 카테고리의 다른 글
Legacy Application을 Executable Jar로 만들자 (0) | 2021.08.06 |
---|---|
Effective Java (1) - 아이템1, 2 (0) | 2021.05.06 |
JPA (1) - 도메인 개발 (0) | 2021.04.11 |
Spring Cloud 일단 기록하기 (0) | 2021.03.01 |
Spring Core (4) AOP (0) | 2020.07.03 |