Spring Boot [2]

어디든 배우자·2024년 3월 21일

모든 코드는 간단한 예시로 작성했습니다.


	@PostMapping("/api/v1/members")
	public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) {
		Long id = memberService.join(member);
		return new CreateMemberResponse(id);
	}

다른 API에서는 DTO로 받았지만 회원가입시 Body로 Entity를 직접 받았었다.
이유는 모든 인자를 필요로 하는데 굳이? 였다.
하지만 서비스가 커지지 않아도 Entity를 수정하는 일이 많다.
그러니 직접적으로 Entity를 통해 값을 받는 일은 하지 말자.



	@PostMapping("/api/v2/members")
    public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
        Member member = new Member();
        member.setName(request.getName());
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
    }

    @Data
    static class CreateMemberRequest {
        @NotEmpty
        private String name;
    }

객체에 대해서 필수값을 요구하는 API가 있다.
하지만 Entity에 직접적으로 @NotEmpty를 하면 API마다 필수값을 다르게 요구한다면?
따라서 DTO에 @NotEmpty를 사용 권장한다.



	@Transactional
    public void update(Long id, String name) {
        Member member = memberRepository.findOne(id);
        member.setName(name);
    }

@Transactional 이 사용된 이유는 데이터베이스에 여러 작업을 논리적 단위로 묶어서 실행하고, 모두 성공하거나 모두 실패하도록 보장하는 기능을 제공해 줍니다.
해당 메서드는 findOne 또는 setName 메서드 중 하나라도 실패하면,
이전에 수행된 작업들이 모두 롤백되어 데이터베이스 상태가 변화하지 않습니다.

@Transactional 을 사용하면 명시적으로 저장 메서드를 호출하지 않아도 저장이 가능합니다.
JPA의 영속성이 이를 가능하게 합니다.
영속성은 엔티티를 영구 저장하는 환경을 말하며, 트랙잭션 범위 내에서 엔티티의 상태 변화를 추적합니다. @Transactional 로 메서드가 시작될 때 트랙잭션이 시작되고, 영속성이 활성화됩니다. 메서드 내에서 엔티티의 상태가 변경되면, 영속성이 이러한 변경사항을 확인합니다.
트랙잭션이 종료되고 메서드가 반환횔 때, 트랜잭션이 커밋됩니다.
이 때 JPA는 영속성에 추적된 모든 엔티티의 변경사항을 데이터베이스에 반영합니다.
즉, Flush 합니다.



	@GetMapping("/api/v2/members")
    public Result membersV2() {
        List<MemberDto> collect = memberService.findMembers().stream()
                .map(m -> new MemberDto(m.getName()))
                .collect(Collectors.toList());
        return new Result(collect);
    }

    @Data
    @AllArgsConstructor
    static class Result<T> {
        private T data;
    }

응답값을 data로 감싸는거라 생각하면 된다.
나는 원래 Map<String,Object> 형식으로 리턴을 했었고,
API 응답이 OK일때만 데이터를 반환하게 했다.
하지만 이 방법이 좀 더 많이 사용되는 것 같다.



	@Component
	@RequiredArgsConstructor
	public class InitDb {
    
    	private final InitService initService;

    	@PostConstruct
    	public void init() {
        	initService.dbInit1();
    	}
	}

위와 같이 클래스에 @Component 를 붙이면, 이 클래스는 스프링의 컴포넌트 스캔에 의해 자동으로 빈으로 등록됩니다.
@Component 는 싱글톤 범위(Singleton scope)로 빈을 생성합니다. 따라서 스프링 컨테이너에서는 기본적으로 한 개의 인스턴스만 생성되며, 이를 애플리케이션 전반에 걸쳐 공유하여 사용합니다.

@PostConstruct 애노테이션은 Spring Framework에서 사용되며, 해당 메서드가 객체의 생성 후에 실행되어야 함을 나타냅니다. 주로 초기화 작업을 수행하는 데에 활용됩니다.
일반적으로 Spring의 빈(Bean)은 객체 생성 후에 추가적인 초기화 작업을 해야 할 때가 있습니다. 이러한 초기화 작업은 주로 의존성 주입이 완료된 후에 이루어져야 하므로, 생성자보다 나중에 수행되어야 합니다. 이때 @PostConstruct 애노테이션을 사용하면 Spring은 해당 메서드를 빈의 초기화 단계에서 호출합니다.



	@GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName(); //Lazy 강제 초기화
        }
        return all;
    }

연관관계가 지정되어있는 객체를 조회할때 Lazy로 되어있으면 원하는 객체만 조회가 된다.
이 방법은 원하는 객체랑 연관관계로 지정되어있는 객체 또한 조회하고 싶을때 사용하는 방법이다.
해당 코드는 직접적으로 엔티티를 반환하는데 이 방법은 잘못된 방법이다.



	public List<Order> findAllWithMemberDelivery() {
        return em.createQuery(
                        "select o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d", Order.class)
                .getResultList();
    }

N+1을 막기위해 사용했던 패치조인 방법이다. 이걸로 성능을 개선할 수 있다.
이 방법을 많이 사용했고, 여기서 더 성능 개선이 가능한 방법을 찾았다.

	public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
                                " from Order o" +
                                " join o.member m" +
                                " join o.delivery d", OrderSimpleQueryDto.class)
                .getResultList();
    }

리포지토리 DTO에서 직접 조회하는 방법이다.
서로의 장단점이 있으나 확실한 장점은 성능 개선(그렇게 큰 차이는 없다)
확실한 단점은 재사용 불가능 하다는 점이다.

	public List<OrderSimpleQueryDto> findOrderDtos() {
        return queryFactory
                .select(new QOrderSimpleQueryDto(
                        order.id,
                        member.name,
                        order.orderDate,
                        order.status,
                        delivery.address))
                .from(order)
                .join(order.member, member)
                .join(order.delivery, delivery)
                .fetch();
    }

이건 QueryDsl 방식이다.



	@Data
    static class OrderDto {
        private Long orderId;
        private String name;
        private LocalDateTime orderDate; //주문시간
        private OrderStatus orderStatus;
        private Address address;
        private List<OrderItem> orderItems;
		//OrderItem은 엔티티이다.

        public OrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
            orderItems = order.getOrderItems();
        }
    }

의존관계에 의해 DTO 안에 엔티티가 있는 모습이다.
난 이 방법을 많이 사용했었다. 근데 DTO 안에도 엔티티가 없어야한다.
DTO를 사용하려면 엔티티와의 의존을 완전하게 없애야 한다.
OrderItemDTO 를 만들어서 처리해야 한다.



	public List<Order> findAllWithItem() {
        return em.createQuery(
                        "select distinct o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d" +
                                " join fetch o.orderItems oi" +
                                " join fetch oi.item i", Order.class)
                .getResultList();
    }

데이터베이스의 distinct는 한 데이터의 모든 값이 같아야 중복을 제거해준다.
JPA 에서는 자체적으로 고유값이 같으면 중복을 제거해서 컬렉션에 담아 준다.

@OneToMany 는 페치 조인 하나만 가능.

컬렉션을 페치 조인하면 페이징이 불가능하다.
컬렉션을 페치 조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
일다대에서 일(1)을 기준으로 페이징을 하는 것이 목적이다.
그런데 데이터는 다(N)를 기준으로 row가 생 성된다.



	public List<Order> findAllWithItem() {
        return em.createQuery(
                        "select distinct o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d" +
                                " join fetch o.orderItems oi" +
                                " join fetch oi.item i", Order.class)
                .getResultList();
    }

해당 코드에서 보이진 않겠지만 엔티티에 걸려있는 @OneToMany 때문에
페이징 적용이 불가능하다.

spring: 
 jpa:
  properties:
   hibernate:
    default_batch_fetch_size: 1000
//영속성 컨텍스트가 데이터베이스에서 엔티티를 가져올 때 한 번에 가져오는 엔티티의 수를 제한하는 설정입니다.
	public List<Order> findAllWithMemberDelivery(int offset, int limit) {
        return em.createQuery(
                        "select o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d", Order.class)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();
    }

member와 delivery는 ToOne 관계이다
어? @OneToMany 페치 안하면 N+1 이 발생하잖아요?
yml파일 적용 때문에 쿼리 호출 수가 1 + N -> 1 + 1 로 최적화 된다.

ToOne 관계는 페치 조인해도 페이징에 영향을 주지 않는다.
따라서 ToOne 관계는 페치조인으로 쿼리 수 를 줄이고 해결하고, 나머지는 hibernate.default_batch_fetch_size 로 최적화 하자.

조인보다 DB 데이터 전송량이 최적화 된다. (Order와 OrderItem을 조인하면 Order가 OrderItem 만큼 중복해서 조회된다. 이 방법은 각각 조회하므로 전송해야할 중복 데이터가 없다.)
페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
컬렉션 페치 조인은 페이징이 불가능 하지만 이 방법은 페이징이 가능하다.



profile
다 흡수하기.

0개의 댓글