당니의 개발자 스토리
주문 서비스 개발 본문
주문 서비스 개발
이제 드디어 주문 서비스를 개발해 보겠습니다.

OrderService를 만들구요.

주문 서비스는 사실 제일 중요한게 이 세 가지겠죠.
자 그러면 지금까지 만든 걸 가지고 어떻게 엮는지 보여드릴게요.

서비스니까 우선 @Service annotation 있어야 되고 @Transactional을 거의 자동으로 readOnly를 true로 잡아줬구요. 그 다음에 @RequiredArgsConstructor가 있어야 하고, OrderRepository가 있어야 되죠. 그런데 OrderService는 생각해 보시면 repository가 많이 필요합니다. 일단 하면서 보여드릴게요.
주문 먼저 개발해 보겠습니다.

자 주문은 일단 데이터를 변경하는 것이기 때문에 @Transactional이 있어야 되고요.
그 다음에 주문 메서드는 식별자를 반환하도록 할게요. memberId랑 itemId, 주문하려면 member랑 item이랑 그리고 item 몇 개를 주문할지 있어야 되겠죠.
자 이렇게 만든 다음에 제일 먼저 memberRepository가 있어야 됩니다. 지금 보시면,

이 id를 파라미터로 받았기 때문에

이거 보시면 상품 주문할 때 회원을 선택하잖아요. 이때 회원의 아이디만 넘어오는 거에요. 상품도 상품의 아이디만 넘어오는 거에요. 그리고 주문 수량.

이렇게 세 가지 id, id, 숫자 이렇게 넘어오는 거거든요.

자 그래서 id를 보고 값을 꺼내야 되잖아요. 그러면 결과적으로 뭐가 있어야 되겠죠. 자 그래서 뭐가 있어야 되냐?

MemberRepository가 있어야 됩니다. 이렇게 넣구요.
자 그 다음에 또 하나 필요하죠.

ItemRepository가 필요합니다. OrderService가 이것저것 좀 의존을 많이 하죠.
자 먼저 Member를 찾읍시다.

memberRepository에서 findOne으로 member를 찾고요. 제일 처음에 먼저 엔티티를 조회하는 겁니다.

그 다음에 itemRepository에서 findOne 해서 itemId를 가지고 오면 됩니다.
그 다음에 배송 정보를 생성할 건데요. 배송 정보는 지금 간단한 샘플이기 때문에 그냥 회원에 있는 address를 그대로 넣어 주겠습니다.

delivery를 생성하고 delivery.setAddress를 해서 member의 address를 넣는 겁니다. 그러니까 회원의 주소에 있는 값으로 그냥 배송한다는 거죠. 근데 실제로 그렇지 않죠. 실제로는 배송지 정보를 따로 입력해야 되겠죠. 그런데 여기서는 예제를 간단하게 위해서 그냥 이렇게 했습니다.
그 다음에 주문 상품을 생성해 보겠습니다.

이제 여러분이 만들었던 createOrderItem 생성 메서드를 드디어 쓸 때가 왔습니다.

이렇게 파라미터를 알맞게 넣으면 orderItem이 생성이 됩니다.
그 다음에 주문을 생성을 해보겠습니다. 주문을 생성하는게 제일 중요하겠죠?

Order에서 우리가 만들어둔 createOrder 생성 메서드에다가 ctrl + space 하면 자동으로 파라미터를 넣어줍니다.
그 다음에 이제 저장해야겠죠. 주문을 저장해야 하는데요.

이렇게 하고 Order의 식별자만 반환해줄게요.
자 여러분 보세요.

지금 보면 Delivery를 따로 생성해야 되는 거잖아요.

그리고 OrderItem도 따로 persist 해야 되는거거든요.

원래대로 하면 DeliveryRepository가 있어가지고 DeliveryRepository.save 해가지고 delivery를 넣어주고, OrderItem도 JPA에 넣어준 다음에 createOrder의 파라미터 값으로 세팅해줘야 돼요.

근데 지금 보면 이거 하나만 했죠. 왜 그러냐면 Order에 가보시면,

이 Cascade 옵션 때문에 그렇습니다. 저번에 세팅을 할 때 OrderItem은 Cascade로 CascadeType.ALL을 했었습니다. 무슨 말이냐? Order를 persist 하면,

여기 들어와 있는 Collection에 와있는 OrderItem도 다 persist를 강제로 날려줍니다.

그 다음에 Delivery도 지금 Cascade 걸려 있죠. 그래서 Order가 Persist될 때 이 Delivery Entity도 Persist를 해주는 겁니다.

그래서 이렇게 하나만 저장을 해줘도 OrderItem이랑 Delivery가 자동으로 persist가 된 겁니다.
이 cascade의 범위에 대해서는 사람들이 고민을 하세요. 어디까지를 cascade를 해야 되나? 라고 고민을 하시는데요, 보통 명확하게 칼로 자르기는 애매한데 어떤 개념에서 쓰시면 좋냐면 Order 같은 경우에 Order가 Delivery를 관리하고 Order가 OrderItem을 딱 관리하거든요. 이 그림 정도에서만 써야 돼요. 그리고 뭔가 참조하는 게 딱 주인이 private 오너인 경우에만 써야 돼요.
정리를 하자면, 지금 Order 말고는 다른 데서 Delivery를 아무도 안 쓰거든요. OrderItem도 마찬가지예요. OrderItem을 Order만 참조해서 써요.
물론 OrderItem이 다른 걸 참조할 수 있지만, 다른 데서 OrderItem을 참조하는 데가 없어요. 라이프 사이클에 대해서 동일하게 관리를 할 때는 이것들이 의미가 있습니다.
그리고 다른 것이 참조할 수 없는 private 오너인 경우에 이거를 쓰시면 도움을 받을 수 있고요.
그게 아니라면, 예를 들어서 Delivery가 되게 중요해요. 그러면 다른 데서도 Delivery를 참조하고 갖다 쓴단 말이에요. 이럴 때는 이렇게 cascade를 막 쓰시면 안 돼요. 왜냐하면 잘못하면 오토 지울 때 저거 다 지워지고 pesist도 막 다른 게 걸려 있으면 되게 복잡하게 얽혀 돌아가거든요.
OrderItem도 마찬가지예요. 만약 OrderItem이 되게 중요하고 다른 데서 막 갖다 쓴단 말이에요. 그러면 이렇게 cascade를 쓰지 말고 별도의 리포지토리를 생성해서 pesist, pesist를 별도로 하시는 게 낫습니다.

이번 케이스는 딱 Order만 Delivery를 사용하고, Order만 OrderItem을 사용하고 Persist 해야하는 Life Cycle이 완전히 똑같기 때문에 이 두 가지 조건이 다 충족하면서 이렇게 order 메서드를 완성했습니다.
이런 개념에 대해서 머릿속으로 딱 안 들어오시면 그냥 안 쓰시는 게 더 낫습니다. 일단 cascade를 안 쓰시다가 쓰다 보면서, 리팩토리를 하는 거죠. 이렇게 따로 참조하는 게 없고 라이프 사이클 자체를 persist 할 때 같이 persist 해야 되는구나. 이런 두 가지 조건을 만족하는 거에 대해서 어느 정도 감을 잡으시면 그때 코드를 리팩토링 하셔서 cascade를 넣어주시는 식으로 설계를 하는 게 더 나은 방법이 되실 거예요.
그 다음에 주문 취소를 하기 전에 한번 정리해 드릴게요. 자 먼저 Order에 대해서 정리 한번 하고 주문 취소로 들어가겠습니다.

먼저 엔티티를 조회합니다. findOne 으로 Member 엔티티 조회해 오고요. 그 다음에 Item 엔티티를 조회해옵니다.

자 이것 때문에 위에 의존 관계를 넣었죠. 지금 코딩 해본 거 보시면 알겠지만 롬복이 이 때 큰 도움이 됩니다. 롬복을 안 쓰시면, 이것만 추가하면 되는 게 아니라 막 생성자나 setter 같은 게 있으면 코드를 다 고쳐야 되잖아요.

그래서 이 어노테이션이 RequiredArgs 즉, 필수 값인 arguments들을 Constructor, 생성자로 넣어준다는 뜻으로, 필드에 final이 붙은 애들을 생성자에 넣어서 만들어줍니다.

그 다음에 배송 정보는 이렇게 생성을 했고, OrderItem을 static 생성 메서드를 통해서 생성을 했고요. 주문도 이렇게 static 생성 메서드를 통해서 생성을 했습니다.
그리고 주문을 저장하고 id를 반환을 했죠. 주문을 저장할 때 cascade 옵션이 있기 때문에 OrderItem과 Delivery는 자동으로 함께 persist 되면서 DB 테이블에 insert가 팍팍 됩니다. 물론 트랜잭션이 커밋되는 시점에 flush가 일어나면서 insert가 DB에 날라가겠죠.
그리고 이건 참고로 말씀드리는 건데, 여러분이 createOrderItem을 만들어 놓고 본인 스스로 할 때는 createOrderItem 호출하겠지만,

누군가는 createOrderItem를 만들지 않고 이렇게 개발할 수 있잖아요. orderItem1.set 해가지고 이렇게 값을 다 채우는 식으로 개발할 수도 있어요.
문제는 뭐냐면 여기 로직에서 이렇게 하고, 저기 로직에선 이렇게 하면 나중에 이게 퍼지니까 유지보수하기가 되게 어려워지거든요. 뭔가 생성 로직을 변경할 때 생성 로직에서 어떤 필드를 더 추가한다거나, 뭐 로직을 더 넣는다거나 하면 이게 분산되니까 이거를 못하게 막아야 돼요.

이렇게 생성하는 것 외에 다른 스타일의 생성을 다 막아야 돼요. 어떻게 막느냐? OrderItem에 가셔서 Constructor를 만드실 때 protected로 만드는 거예요.

JPA는 스펙상 Protected까지 기본 생성자를 만들 수 있게 허용해주거든요. 이제 이렇게 해놓으면,

빨간불이 뜨니까 컴파일이 올라가겠죠. 그럼 '아 이거 지금 쓰지 말라는 거구나' JPA 쓰면서 protected 하면 쓰지 말라는 거거든요.

자 그래서 이렇게 제약을 할 수 있게 됩니다

근데 이것도 롬복으로 또 줄일 수 있어요.

이렇게 해주시면 아까랑 똑같은 코드 거든요. 이렇게 해서 저는 이거 많이 씁니다. Order도 마찬가지구요.

Order다가도 똑같이 적어줍시다. 이렇게 하시면, 누군가 바보같이 Order를

이렇게 생성을 하게 되더라도 '왜 빨간불이 떴지?' 하고 가보겠죠.

그럼 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 보고 '아 이거는 직접 생성하면 안 되고 뭔가 다른 스타일로 생성해야 되겠구나' 하고 보니까

'생성 메서드가 있네 그럼 얘를 써야 되겠다' 라고 생각을 하겠죠. 항상 코드를 이런 식으로 여러분 제약하는 스타일로 짜시는 게 더 나아요. 이렇게 해야 항상 좋은 설계와 유지 보수로 끌어갈 수 있어요.
자 그 다음에 주문 취소 해보겠습니다.

주문 취소는 당연히 트랜잭션이 있어야 되겠죠. 주문을 취소할 때 Id만 넘어온단 말이에요. 화면에서 보시면,

주문내역에서 CANCEL 버튼을 누를 때 Id만 넘어오는 거예요. 자 그러면 먼저 찾아야 됩니다.

orderRepository에서 findOne 해가지고 orderId를 찾아요. 그러면 order가 조회가 되겠죠. 그 다음에 뭘 하면 되냐?

여기서 order.cancel() 하면 끝납니다. '어? 왜 로직이 이거밖에 없지?' 보통 이런 스타일로 코딩을 거의 안 하시거든요.

자 우리 Order에 비즈니스 로직 만들어 놨던 거 기억 나시죠? Order의 cancel()을 호출하면 어떻게 되냐면, 이미 배송 완료가 되어버린 건 취소가 안 되기 때문에 예외를 터뜨리고요.
그 외의 경우에는 어떻게 합니까? 나의 상태를 cancel()로 바꿉니다. 그리고 재고 수량을 원복해야 되기 때문에

OrderItem을 또 cancel() 해줍니다. 그럼 orderItem.cancel()이 재고를 다시 count 만큼 올려놔요.

그래서 이 로직이 이렇게 됩니다.
그런데 이제 재미있는 게 있어요. JPA의 진짜 강점이 바로 여기서 나오거든요.
평상시 같은 경우에는 여러분 어떻게 하셔야 되냐면, 일반적으로 데이터베이스 SQL을 직접 다루는 라이브러리를 MyBatis나 JDBC 템플릿, 아니면 내가 직접 SQL을 날리면,

이 데이터를 지금 변경했잖아요. 변경한 다음에 어떻게 해야됩니까? 바깥에서 내가 리포지토리에다가 update 쿼리를 직접 짜서 날려야 돼요.

이거를 cancel() 하고 나면, 재고가 다시 올라가야 되잖아요. 이 item의 재고를 플러스하는 sql을 내가 직접 짜서 올려야 돼요.

그러니까 이 데이터를 변경한 로직 바깥에서 데이터를 다 끄집어내가지고 쿼리에다가 파라미터 넣어가지고 다 해야 된단 말이에요. 트랜잭셔널 스크립트라고 하죠. SQL을 직접 다루는 스타일로 할 때는 서비스 계층에서 이렇게 비즈니스 로직을 다 쓸 수 밖에 없어요.
그런데 JPA를 활용하면,

이렇게 엔티티 안에 있는 데이터들만 바꿔주면 JPA가 알아서 바뀐 변경 포인트들을 Dirty Checking이라고 변경 내역 감지라고 저는 번역을 하는데, 변경 내역 감지가 일어나면서 변경된 내역들을 찾아서 데이터베이스에 업데이트 쿼리가 쫙쫙 날라갑니다.
이 경우에는 지금 바꾼 데이터가 Order의 상태를 바꿨기 때문에 Order에 대한 업데이트 쿼리가 날라갈 거고,

그 다음 OrderItem은 지금 바꾼 게 없지만, 이 Item에 가보시면,

이 stockQuantity가 바꼈죠. 그렇기 때문에 Item도 업데이트 쿼리가 날아가서 stockQuantity가 원복이 될 겁니다.
이게 JPA를 사용할 때 진짜 엄청 큰 장점이라고 볼 수 있습니다.
이제 주문 취소까지 말씀드렸고, 주문 검색은 나중에 써야 돼서 껍데기만 만들어 놓을게요.

지금 당장 할 게 아니기 때문에 option + cmd + / 해서 주석처리 하고 넘어가겠습니다

자 이렇게 해서 이제 한번 쭉 설명을 드릴게요. 여러분 주문 서비스는 주문, 그리고 주문 취소, 검색과 같은 기능을 제공합니다.

그리고 참고로 예제를 좀 단순화 하기 위해서 제가 주문할 때 주문 상품은 하나만 넘기도록 했어요. 그러면 UI에서 주문할 때,

여러 개를 선택할 수 있게 하면 멀티로 막 select 해서 여러 개 할 수 있으면 되는데 그럼 좀 예제가 복잡해져서 저는 이제 단순화 하려고 일단 이 예제에서는 하나만 넘기도록 제약을 했습니다.

하지만 실질적으로 기능은 여기서 또 다른 OrderItem을 생성해서 넘기면 여러 개 주문이 됩니다.
한번 주문할 때 여러 개 상품을 선택할 수 있습니다.
이제 주문은 충분히 설명 드린 것 같고, 주문 취소 말씀드렸고, 주문 검색 이거는 뒤에서 한번 해보겠습니다.
여러분 참고로,

지금 주문 서비스의 주문과 주문 취소 메서드를 잘 보시면 비즈니스 로직이 대부분 엔티티에 있어요.

살짝 복잡하긴 하지만 단순한 엔티티를 조회하고 배송 정보 생성하고 주문 상품 생성하고 단순한 엔티티 생성하는 것들을 제외하고는 비즈니스 로직이 createOrder, createOrderItem 여기가 사실 복잡하죠. 무슨 얘기냐면 OrderService의 주문 메서드는 단순히 엔티티 조회하고 연결해주고 호출해주는 정도만 하는 거예요. repositroy를 통해서 Order를 save 해주고, 그런데 주문 취소는 더 단순하죠.

Order를 조회한 다음에 cancel()만 호출하면 끝난단 말이에요. 자 이런 스타일로 엔티티의 핵심 비즈니스 로직이 따로 있고 이걸 호출하는 거를, 엔티티에다가 핵심 비즈니스 로직을 몰아넣는 이 스타일을 뭐라 그러냐면 도메인 모델 패턴이라고 해요.

서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 하죠. 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라고 합니다.

서비스 계층은 진짜 딱 위임만 하고 있죠.
그런데 이것과 반대로 엔티티에는 비즈니스 로직이 거의 없으며 거의 Getter, Setter 밖에 없고, 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라고 해요.
우리가 일반적으로 SQL을 다룰 때 했던, 그런 것들을 대부분 트랜잭션 스크립트 패턴이라고 하죠. 만약 트랜잭션 스크립트 패턴이었으면,

이 로직은

여기 밖에 쭉 나와있고

이 cancel 로직 조차도 바깥에 빠져있겠죠.
일반적으로 SQL을 다룰 때 많이 사용했던 방식이 Transaction Script Pattern이라고 보시면 되고, JPA나 ORM들을 쓰시면 이렇게 Transaction Script Pattern 보다는 Domain Model 패턴, Domain Model 안에 핵심 비즈니스 로직을 다 넣는 것, 이런 식으로 코딩을 많이 하게 됩니다.
Transaction Script Pattern이 꼭 잘됐다 잘못됐다는 아니고요. 여러분이 뭐가 더 유지 보수하기 쉬운지를 고민해보시면 돼요. 때로는 트랜잭션 스크립트 패턴이 더 좋을 때도 있고, 엔티티에 핵심 비즈니스 로직을 다 몰아넣고 데이터들이랑 상태랑 행위를 모아서 좀 더 객체지향 스타일로 코딩하는 게 더 나은 경우도 있습니다. 이거는 항상 뭐가 옳다 그르다 보다는 이게 약간 문맥에 따라서 서로 트레이드 오프가 있거든요 그래서 그걸 맞게 쓰시면 됩니다.
참고로 한 프로젝트 안에서도 이 트랜잭션 스크립트 패턴과 도메인 모델 패턴이 양립을 합니다.
그니까 현재 내 문맥에서 더 뭐가 더 맞는지를 생각해보고 그걸 쓰시면 돼요.
자 그래서 이제 도매인 모델 패턴에 대해서 한번 보여드렸습니다.
다음 시간에는 이렇게 구현한 서비스를 주문 기능 테스트를 통해서 테스트 해보겠습니다.
'스프링 > 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글
| 주문 검색 기능 개발 (0) | 2024.05.11 |
|---|---|
| 주문 기능 테스트 (0) | 2024.05.11 |
| 주문 리포지토리 개발 (0) | 2024.05.04 |
| 주문, 주문상품 엔티티 개발 (0) | 2024.05.03 |
| 상품 서비스 개발 (0) | 2024.04.29 |