당니의 개발자 스토리

주문 검색 기능 개발 본문

스프링/실전! 스프링 부트와 JPA 활용1

주문 검색 기능 개발

clainy 2024. 5. 11. 22:50

주문 검색 기능 개발

이번 시간에는 주문에서 검색 기능을 한번 개발해 보겠습니다.

그러니까 이번 장의 핵심은 JPA에서 동적 쿼리를 어떻게 해결해야 되는가? 이거를 설명드리려고 일부러 이걸 만들었어요.

화면 먼저 보여드리면,

주문 내역을 가보면,

이렇게 여기 회원명과 주문 상태로 검색을 할 수 있게 되어 있어요. 필터링 조건이 들어가는 거죠. 회원명이 userA 이렇게 하면

userA만 되고 이걸 취소하면,

주문 상태가 주문인 거랑 취소인 거랑 이렇게 보이겠죠.

이걸 잘 보시면 결국 이게 동적 쿼리가 만들어져야 되는 상황이에요. 파라미터로 회원명이 있으면 where 문에 회원명이 들어가야 되고, 주문 상태가 선택이 되면 where 문에 주문 상태가 들어가야 되고요.

보통 실무에서는 이런 동적 쿼리를 안 쓸 수가 없거든요. 약간 회피하는 방법들이 있긴 한데, 사실 동적쿼리를 많이 쓰는 게 효율적일 때가 되게 많아요.

자 그래서 이제 JPA 에서 동적 쿼리를 어떻게 해결해야 되는지를 한번 쭉 보여드리겠습니다.

OrderRepository 코드도 가셔서 우리가 이전에 만들어놨던 이걸 완성 시키는 거예요.

자 OrderSearch를 만들어야 돼요.

이렇게 파라미터명을 아까랑 맞추는 겁니다.

그래서 이 파라미터 조건이 있으면 얘가 where 문으로 검색이 되어야 되는 겁니다.

그 다음에 이제 OrderRepository 에서 이제 검색 로직을 구현해야 되겠죠. 그럼 어떻게 만들건가 하는 건데요. 결과적으로는 우선 JPQL을 만들어 볼게요.

우선은 기본은 select가 되는 거죠. Order를 조회하고 그 다음에 Order랑 member를 join을 하는 거에요. 이 join 문은 처음 보시는 분은 JPQL 기본편이나 책을 참고하시면 되구요. JPA에서는 join을 이런식으로 합니다.

여기에 left join을 하시면, 여기 left 적으시면 되구요.

객체이기 때문에 이렇게 뭔가 참조하는 스타일로 조인을 하는 거죠. 이걸 보시고 'Order랑 Order와 연관된 member를 조인해' 이렇게 이해해주시면 됩니다.

테이블로 표현해야 되는 것을 객체로 표현하다 보니까 이게 이제 좋은 선택이어서 이렇게 한 거고요. 실제적으로 이거 돌려보면 우리가 일반적으로 생각하는 SQL join 문으로 번역이 되어서 실행이 됩니다.

자 그런데 만약 OrderSearch의 조건이 다 있다고 하면 이제 이렇게 되겠죠.

자 조건이 다 있다면 여기에다 where o.status, 즉 그 상태(status)를 파라미터 바인딩을 하면 되죠. :status로 하고 and 그 다음에 member(m).name이 like 문으로 해서 여기 :name으로 딱 해주면 돼요. 그러면 사실 이게 완성이란 말이에요.

그 다음에 getResultList() 이렇게 해주면 됩니다.

자 이렇게 하면 그냥 이 결과를 얻어서 얘를 return 해주면 끝나는 거에요.

아 그런데 지금 파라미터 바인딩을 해주는 걸 까먹었죠.

이렇게 해주면 됩니다. 아주 간단하죠?

그런데 만약에 결과를 제한하고 싶어.

그러면 setMaxResults(1000) 해서 1000개로 한계를 잡아주면 돼요. 이러면 최대 1,000개 까지만 조회되는 거에요.

참고로 페이징 하시는 게 궁금하면, setFirstResult 라고 있거든요. 이거는 이제 start 포지션으로 100 으로 하면 100부터 시작을 해서 1,000개 가져온다는 얘기예요. 뭐 이런 식으로 페이징 다 됩니다.

자 이렇게 하면 되는데 그런데 이건 이제 값이 다 있다는 가정이고,

만약에 값이 없으면 이런 문장이 있으면 안 되겠죠. '상태도 다 가져와, name도 다 가져와' 라고 해서

이렇게만 선택을 해야돼! 라고 하면 동적 쿼리가 되는 거죠.

orderSearch에 name 파라미터가 없으면 'name 파라미터 필터링 조건을 쓰지 말고 다 가져와'

그 다음에 상태 값도 선택되어 있지 않으면,

이게 없으면 Null이겠죠. 그래서 이게 Null이면 '상태 체크 하지 말고 그냥 주문이든, 취소든 다 들고와' 라고 한다면,

쿼리가 이렇게 바뀌어야 되겠죠. 이게 동적 쿼리가 되어야 되거든요. 이제부터 지옥이 시작됩니다. 이 동적 쿼리를 해결하는 게 만만치 않아요. 저도 처음에 JPA 쓰면서 제일 고민됐던 게 이걸 어떻게 해결할지였거든요. 저도 기존에 마이바틱스를 많이 써서 거기에는 동적 쿼리를 쓸 수 있는, 그러니까 xml로 잘 할 수 있는 기능이 많이 있었어요.

자 그래서 일단 첫 번째 무식한 방법. 그냥 jpql 문자를 생자로 해결하는 거예요. 어떻게 하냐?

일단 cmd + option + V 해서 jpql 문을 따로 빼줄게요. 이렇게 한 다음에 이거를 동적으로 막 생성을 해야 돼요. 진짜 힘듭니다. 저는 이런 스타일은 안 쓰기 때문에 시간 관계상 코드를 복붙 할게요.

제가 직접 안 친다는 얘기는 실무에서 이렇게 안 한다는 거예요. 그래서 이 jpql을 동적으로 만들려면,

orderSearch에서 getOrderStatus 상태가 Null이 아니면, 즉 뭔가 값이 있으면 어떻게 합니까?

where나 and 중에 하나를 붙여야 되죠. 처음이 where고 뭔가 또 중간에 조건이 있으면 and를 붙여야 되고요. 그 다음에 어떻게 합니까? o.status는 :status 해서 jpql을 동적으로 빌드 합니다.

지금 이것만 봐도 상당히 복잡하죠. 왜냐면 isFirstCondition이라는 조건은 뭐냐면,

두번째 세번째 나오면 where가 아니고 and가 되어야 되잖아요. 그래서 SQL에 이걸 넣은 거구요. 물론 이거는 완전 처음에는 확정이 되니까 안 넣어도 될 것 같긴 한데, 어쨌든 이렇게 해서 넣고 그 다음에 이제 또 뭘 해야 됩니까?

회원 이름 검색하는 거, 이것도 한번 제가 복붙할게요.

벌써 힘들죠. 위의 orderSearch 파라미터에서

getMemberName이 값이 있으면, 이 hasText가 스프링 프레임워크 제공하는 유틸리티에 있거든요. 그래서 text에 이 값이 있으면 어떻게 합니까?

자, firstCondition이면 where를 넣고, firstCondition이 아니면 and를 넣는 거죠.

그러니까 이게 없으면 isFirstCondition이 처음이니까 where가 들어가야 되죠.

이렇게요. 아무튼 분기하고 또 어떻게 합니까?

마지막에 jpql에 name, like, 이걸 동적으로 넣는 거에요. 아 힘들죠. 그 다음에 이제 jpql이 작성이 어느정도 되겠죠.

자 이제 이 createQuery 에다가 또 해줄게 또 많이 있습니다. return 지우고요.

이렇게까지 세팅을 해놓고, 그 다음에 반환값을 받아야 합니다.

자 얘를 query 라고 해서 받고, 얼마나 복잡한지 설명하는 게 목표이기 때문에 또 복붙하겠습니다.

자 만약에 orderSearch에서 orderStatus가 null이 아니라, 상태가 있으면 어떻게 합니까?

query.setParameter 아까 처음 보여드린 거 있죠. 그거를 하고 memberName도 setParameter를 해줍니다. 이 작업을 다 한 다음에

이걸 해야 되는 거죠.

근데 잘 보시면 알겠지만 제가 말씀드리고 싶은 핵심은 뭐냐면 이렇게 문자를 더하기 해서 하는 거는 보기보다 엄청 힘들어요. 거의 뭐 불가능에 가깝죠. 지금 이 코드도 잘 살펴보시면 버그가 있을 거에요. 버그를 찾기 진짜 힘들 정도죠. 지금 뭐 동적 쿼리 몇 개 하려다가 한 페이지가 도배가 되어버렸죠.

그래서 이렇게 jpql을 문자로 생성하는 것은 정말 번거롭고 실수로 인한 버그가 충분히 발생할 수 있어요. 그래서 MyBatis 쓰는 이유가 이런 동적 쿼리를 생성하는 게 굉장히 편하다는 이점이 있죠. 일단 이 방법은 findAllByString 이라고 이름을 바꾸겠습니다.

그리고 이제 두 번째 방법이 있습니다.

첫 번째 방법은 문자를 더하고 이런 게 되게 복잡하고 지저분하잖아요. 그리고 막 컨디션을 내가 where, and 에 따라서 조립을 해야 되고 직접 수동으로 다 해야 된단 말이에요. 그래서 좀 더 나은 방법이 있습니다.

findAllByCriteria 라고 하는데, Criteria는 이런 동적 쿼리를 빌드 해주는, 그러니까 jpql을 java 코드로 작성할 수 있게 JPA에서 표준으로 제공하는 게 있어요.

이것도 제가 권장하는 방법이 아니기 때문에 여러분 그냥 편하게 들으십시오.

제가 기본편에서도 그냥 소개를 안 했어요. 그냥 간단하게 소개만 하고 넘어갔는데 왜냐면 이거를 저는 실무에서 쓰라고 만든 게 아니라고 생각해요. 실제 이 거를 실무에서 안 써요.

자 먼저 엔티티 매니저에서 CriteriaBuilder를 얻은 다음에 CriteriaBuilder에서 createQuery를 한 다음 얘의 응답 타입을 세팅을 해줍니다. Order.class 하고, 자 그 다음에 여기서부터 이제 시작을 하는 거에요.

from(order) 얘가 아마 시작하는 엔티티라고 보시면 되고, order를 가져와야 되니까 order에 class를 잡고 얘를 alias를 잡는 거에요. 시작이어 가지고 root로 잡습니다. 여러분 모르셔도 편하게 들으시면 됩니다. 그 다음에 조인해야 되죠.

조인은 멤버랑 조인을 해야 되고 조인 타입은 그냥 inner join 할게요. 이렇게 해서 조인한 것은 결과는 멤버니까 m으로 alias를 주었습니다.

그 다음에 어떻게 하느냐?

Predicate 라는 게 있고 동적 Query에 대한 어떤 컨디션 조합을 이걸 가지고 아주 깔끔하게 만들 수 있어요.

이렇게 하면 이 predicate 자체가 이제 조건이 되는 거거든요.

여기서는 되게 복잡하게 했잖아요. 여기서는 이렇게 하나만 넣어 주시면 돼요.

그리고 비슷하게 회원 이름도 이렇게 해주시면 됩니다.

이렇게 하면 끝입니다.

여러분 괴로우실 건데 이걸 보여드린 이유가 있어요. 일단 첫번째 방법보단 낫죠. 첫번째는 아 문자를 내가 겁나 빡세게 조립하는구나 이고, 두 번째는 딱 이렇게 빌드하고 나면 결과적으로 JPQL이 만들어지는 거예요. JPQL을 java 코드로 작성할 수 있게 이 JPA Criteria가 도와주는 거거든요.

특히 이런 동적 쿼리를 작성할 때 아주 메리트가 있습니다.

그런데 얘는 치명적인 단점이 있어요. JPA 스펙 만드시는 분들도 고민을 했단 말이죠. '아 이런 동적 쿼리나 이런 것들을 어떻게 하면 더 깔끔하게 할 수 있을까? jpql을 어떻게 하면 좀 java 코드로 타입 체크 같은 걸 잘할 수 있을까?' 고민을 많이 했는데 내가 볼 때 이건 머리로만 코딩을 한 거예요. 실무를 너무 많이 안 해본 사람이 한 것 같아요.

그러니까 이걸 보면 여러분 유지 보수성이 거의 제로에 가까워요.

왜냐면 이걸 가지고 여러분 무슨 쿼리가 생겨날지 머리에 떠오르나요?

이걸 하면 뭔가 오더랑 멤버를 조인을 해가지고 이런 조건이 겹쳐질 것 같다는 쿼리가 되게 눈에 안 떠오르거든요.

그래서 jpa 표준 스펙에 JPA Criteria 가 있는데 저는 이거 안 써요. 제가 운영할 때도 이거를 몇 번 썼다가 도저히 유지 보수가 안되더라고요. 읽는 사람도 완전히 멘붕이거든요. 그래서 안 씁니다. 하지만 이런 식으로 할 수 있다는 걸 여러분들께 보여드리려고 짠 거예요. 어쨌든 JPA 표준 스펙이니까.

자 그러면 도대체 첫 번째 방법, 문자를 내가 직접 다 하는 것도 너무 거지같고 Criteria를 쓰는 것도 코드는 줄어드는 것 같은데 도저히 무슨 SQL이 만들어지는지 머리에 안 떠올라요.

여러분 참고로 Criteria에 대해서 설명이 잘 돼 있는 거는 JPA의 스펙 문서를 보시거나, 아니면 여기 JAVA ORM 표준 JPA 책에 JPA 스펙에 있는 기능을 다 적는게 맞다고 생각해서 적어뒀었거든요. 객체지향 쿼리 언어쪽에 보면 있어요. 그런데 저도 스펙이 있기 때문에 그냥 적은거지 실무에서 못 씁니다 여러분. 복잡해요.

자 그래서 결국 다른 대안이 필요한데요. 많은 개발자들이 이런 고민을 했어요. 어떻게 하면 해결할 수 있을까? 이런 동적 쿼리부터 시작해서 jpql을 문자로 쓰면 오타가 날 수 있잖아요.

어쨌든 그러니까 이런 걸 어떻게 컴파일 시점에 잘 해결할 수 있을까라고 해서 고민해서 나온 라이브러리가 바로 QueryDSL이라는 게 있습니다.

Query DSL은 분량이 제법 돼서 여기서 다루지는 않을 건데 제가 이번 장 제일 마지막에 QueryDSL을 간단하게 소개해드리면서 지금 이 코드를 QueryDSL로 바꾸면 어떻게 되는지 한번 보여드릴 겁니다.

자 QueryDSL로 작성하게 되면 방금 똑같은 코드를 이렇게 작성할 수가 있습니다.

이걸 쓰시면 동적 쿼리가 정말 강력하게 해결이 되고 꼭 동적 쿼리가 아니더라도, 정적 쿼리들도 조금만 복잡해지면 저는 웬만하면 QueryDSL로 다 짜라고 하거든요. 그렇게 해서 얻는 이점이 정말 많습니다.

그리고 이게 다 Java 코드이기 때문에 실수로 오타가 나도 컴파일 시점에 다 잡아줍니다.

자 이렇게 QueryDSL로 이렇게 쫙 바꾸는 거를 마지막에 보여드릴게요.

그러니까 실무 하실 때는 동적 쿼리 때문에도 그렇고, 되게 복잡한 JPQL을 해결하기 위해서도 그렇고 QueryDSL, 개인적으로 제가 프로젝트를 할 때는 스프링 부트, 그 다음에 스프링 데이터 JPA, 그리고 QueryDSL 이렇게는 꼭 함께 가지고 갑니다. 그렇게 해야 실무에서 정말 생산성을 극대화해서 코드도 진짜 아름답게 하면서 뭔가 컴파일 시점에 못 뽑아버리더라도 자동으로 잡아주고 하면서 개발을 깔끔하게 해낼 수 있거든요.

 

자 아무튼 거기까지 설명 드렸고 혹시 기회가 되면 QueryDSL도 제가 좀 강의를 따로 찍던가 이 부분을 한번 고민해 보겠습니다.

'스프링 > 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글

회원 등록  (0) 2024.05.13
홈 화면과 레이아웃  (0) 2024.05.12
주문 기능 테스트  (0) 2024.05.11
주문 서비스 개발  (0) 2024.05.08
주문 리포지토리 개발  (0) 2024.05.04