WEB/JPA

실전! 스프링 부트와 JPA 활용2 (API 개발기본 , 지연로딩과 조회 성능 최적화)

Tony Lim 2021. 4. 3. 20:53
728x90

 

    @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
    {
        private String name;
    }

REST API spec에 맞는 DTO를 따로 만들어 사용해야한다. Entity를 그대로 사용하게되면 유지보수도 힘들고 api spec이 Entity에 따라 수시로 변하는 일이 발생하게된다. Parameter로 받을떄도 마찬가지이다.

 

철저히 쿼리와 커맨드를 분리해야한다. 또 영속성이 끊어진 객체를 반환해야하는 문제점이 있다.

Validation 같은 검증로직은 DTO에 들어가 있는것이 좋다. 어떤 API에선 @NotEmpty가 필요할수도 있고 안필요할 수도 있기때문이다. 

 

JSON 으로 리턴해줄경우 Array 일때는 

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



{
	"data" :[받아온 정보들]
}

항상 이런식으로 확장성 있게 return 해주어야 한다. 그냥 array를 return 해줘버리면 나중에

{
	"count":"4",
	"data":[]
}

이런식의 설계가 불가능 해져버린다.

 

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

        return new Result(collect,collect.size());
    }

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

    @Data
    @AllArgsConstructor
    static class MemberDto
    {
        private String name;
    }
{
    "data": [
        {
            "name": "tony21"
        },
        {
            "name": "12"
        },
        {
            "name": "123"
        }
    ],
    "count": 3
}

이 member들을 다 불러오는 api는 기존 findMembers를 통하여 엔티티 방식으로 담아오고 후에 MemberDto 로 다 변환해준다.

그리고 최종적으로 위에서 언급한 api 확장성을 위해 Result 클래스에 담아주어 return 해준다.

 

양방향 연관관계 

이런 상황일때 의 조회는 둘 중 한쪽을 꼭 @JsonIgnore 해주어야 무한참조루프 에러가 발생하지 않는다.

그리고Hibernate5Module 라이브러리를 추가해서 사용해야한다. 하지만 이런식으로 엔티티를 직접 API 스펙에 맞추는것은 위험한일이고 할 필요도 없는 일이다.

DTO 로 변환을 해서 반환을 하자 

또한 1+N 문제가 발생하는데

  • order 조회 1번으로
  • order -> member 지연로딩 조회 N번
  • order -> delivery 지연로딩 조회 N번

이떄 db에 직접 조회하는것이 아니라 영속성 켄텍스트를 조회하는 것이다.

이떄 지연로딩을 초기화하는 개념으로 fetch join으로 처음 쿼리를 날릴떄 관련된 객체까지 다 가져온다.

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

원하는 것만 가져오기위해서는 쿼리를 new를 통해 직접 짜주어야한다. 이떄도 DTO를 이용하여한다. fetch join의 경우 모든 select * 처럼 다 가져온다.

하지만 이렇게 짜면 재사용성이 많이 떨어진다. Repository는 엔티티의 순수성이 유지되면서 조회되는 것이 앵간하면 좋다.  

저런 쿼리는 따로 OrderSimpleQueryRepository 처럼아예 따로 관리해주는것이 유지보수성 측면에서 괜찮다.

 

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 법을 선택한다.
  2. 필요하면 fetch join으로 성능을 최적화 한다. -> 대부분의 성능이슈가 해결될것이다.
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

 

 

 

 

 

 

 

 

 

728x90