진짜 개발자
본문 바로가기

FrameWork/Spring JPA

Spring JPA - JPA를 이용해 Commerce App 만들기 - 7 (장바구니 기능 추가)

728x90
이번 포스팅에서는 장바구니 기능을 추가해보도록 하겠습니다

이번 포스팅에서는 장바구니 기능을 추가해보도록 하겠습니다.

 

전체코드 : https://gitlab.com/galid1/jpa-commerce

 

 

 

1. 장바구니 도메인 계층

1.1 요구사항 분석

장바구니 기능에는 어떠한 기능들이 필요할지 먼저 파악해보겠습니다.

  • 장바구니에 여러 아이템을 추가할 수 있어야한다.
  • 아이템 추가시 이미 장바구니에 존재한다면, 수량을 증가 시켜야 한다.
  • 어떤 아이템을 얼만큼(수량) 담고 있는지 알 수 있어야한다.
  • 담고 있는 아이템을 제거할 수 있어야 한다.
  • 담고 있는 아이템의 수량을 수정할 수 있어야 한다.

 

 

1.2 설계

1. 기능 추출하기

요구사항을 통해 장바구니에 아이템 추가, 장바구니에서 제거, 아이템 주문수량 변경의 기능이 필요함을 알 수 있습니다.

 

 

2. 속성 추출하기

  • 장바구니에 여러 아이템을 추가할 수 있어야한다.
  • 어떤 아이템을 얼만큼(수량) 담고 있는지 알 수 있어야 한다.

위의 요구사항을 통해서, 어떤 아이템을, 얼만큼 담았는지를 표현할 수 있는 CartLine이라는 ValueObject가 필요함을 알 수 있습니다.

 

요구사항을 통해 생성된 CartLine은 위와 같습니다.

 

사용자는 장바구니에 여러 아이템을 담을 수 있기 때문에, CartEntity는 위와 같이 구성될 것 입니다.

 

 

3. 도메인 규칙 추출하기

  • 이미 장바구니에 아이템이 존재한다면, 수량을 증가시켜야 한다.

위의 요구사항으로 아이템을 장바구니에 추가할때에는, 아이템이 장바구니에 존재하는지를 먼저 검사하도록 구현해야 함을 알 수 있습니다.

 

따라서 위와 같이 구현을 할 수 있을것 같습니다.

 

 

값객체 ElementCollection 문제

하지만, 위와 같이 장바구니를 구현하는 경우, 장바구니의 한 줄을 삭제 또는 수정시 효율성 문제가 발생합니다.

 

위 쿼리는, 장바구니에서 한 아이템을 삭제했을때 자동으로 발생하는 쿼리입니다. 자세히 보시면 이상한 점을 알 수 있는데요.

삭제 쿼리시, delete from cart_line where cart_id = ? 가 발생하여, 내가 원하는 item을 제거하는 것이 아닌, 특정 cart에 포함된 데이터를 전체 삭제하는 쿼리가 발생하고, 나머지 item을 다시 insert하는 쿼리가 발생하는 것을 볼 수 있습니다.

 

이는 값 객체에는, Entity처럼 각각을 구분하기 위한 id값이 부여 되지 않기 때문에 JPA 내부적으로 이를 처리하기 위해 특별한 조치? 를 취한것이라고 합니다.

 

 

Map으로 변경하기

비효율적으로 쿼리가 나가는 문제를 해결하기 위해서, List자료형을 Map으로 바꾸어 보도록 하겠습니다. 이렇게 하면 하나의 row를 위한 id가 생겨나기 때문에, where절에 특정 row만을 제거하도록 할 수 있습니다.

 

CartLine을 저장할 자료형을 위와 같이 변경합니다.

 

Cart_Line의 스키마를 확인하면, 위와 같이 Map의 key값을 저장하기 위한 field가 자동으로 추가됨을 볼 수 있습니다.

 

자료형에 맞게 addItemToCart() 메소드 수정

이제 자료형이 바뀌었으니 그에 맞추어 장바구니 추가 로직도 변경이 되어야 합니다. List를 사용할 때보다 훨씬 간결해지고, 한눈에 보아도, Map으로 처리하기 때문에 효율성도 증가되었음을 볼 수 있습니다.

 

 

삭제를 해보면? 위와 같이, cart_key를 이용해 하나의 delete쿼리만 발생하는 것을 확인할 수 있습니다.

 

 

 

1.3 구현

앞서 설명드린것들 중에 별도로 추가된 것은 memberId필드, modifyOrderCount(), removeCartLine() 메소드 외에는 없습니다. 메소드들은 앞서서 설명 드린것을 토대로 구현한 것이며, memberId 필드는, 장바구니 조회시, 장바구니의 소유 회원을 구분하기위해 추가했습니다.

 

 

 

 

2. 장바구니 서비스 계층 구현

2.1 필요 기능

  1. 장바구니 생성
  2. 장바구니 조회
  3. 장바구니 아이템 추가
  4. 장바구니의 아이템 주문 수량 변경
  5. 장바구니의 아이템 삭제

Service계층에서 구현해야할 기능은 위와 같습니다.

 

 

2.2 구현

1. 장바구니 생성 기능

우선, 사용자가 회원가입을 하면, 장바구니가 생성이 되어야 합니다. 장바구니의 소유자를 표시하는 memberId가 지정되어야 하기 때문에, 회원가입시 사용자가 생성될 때, 만들어주어야 합니다.

 

회원 생성시, 장바구니를 같이 생성하도록 할 수 있는데요, 이 방법은 매우 좋지 않은 방법입니다. 왜냐하면, MemberService에서 회원가입시, Member 도메인과는 전혀 관계없는 CartService에 의존하게 되기 때문입니다. 이외에도 여러 단점들이 존재합니다.

 

하지만 현재는 간단히 구축하기 위해 이렇게 구현하고, 추후에 이벤트 소싱을 이용해 이를 리팩토링할 것입니다.

 

CartEntity의 생성자를 보면 알 수 있듯이, 생성될 때 꼭 필요한 값은 장바구니의 소유자(member)의 id값 이 전부입니다.

 

 

2. 장바구니 아이템 추가 기능

장바구니에 아이템을 추가하는 기능은,

  1. 매개변수로 전달된 memberId를 이용해 장바구니를 찾고
  2. 장바구니 아이템 추가 요청 클래스인 AddToCartRequestForm을 이용해, CartLine을 생성하고
  3. CartEntity의 addItemToCart()를 호출합니다.

 

 

 

3. 장바구니 수량 변경, 삭제 기능

수량 변경기능, 삭제 기능 역시, 장바구니를 찾아, CartEntity에 수량변경 기능을 위임하는것이 전부입니다.

 

 

4. 장바구니 조회 기능

장바구니 화면을 구현하기 위해서는, 위 화면에 뿌려져야 할 데이터들을 반환하는 기능이 필요합니다.

 

 

우선, 장바구니에서 보여질 데이터의 형태를 정의하는 CartLineDto클래스를 추가합니다. (itemId : 장바구니 화면에서 하나의 아이템을 클릭했을때, 해당 아이템의 상세페이지를 보여주기위해 필요합니다.)

 

그리고 이 JPQL을 통해 위의 DTO를 반환할 Dao 인터페이스를 정의합니다.

 

위의 "쿼리" 부분에 들어갈 JPQL을 짜야하는데요, 우선 ERD를 살펴보며, 어떻게 JPQL을 작성할지 고민을 해보겠습니다.

 

각각의 화살표는 테이블이 어떠한 column에 의해 연관관계가 있는지를 나타내는 것이며, Foreign Key는 아닙니다. 장바구니 화면에 보여줄 데이터를 JPQL로 조회해오기 위해서는 다음과 같은 조건들이 필요할 것 같습니다.

  • 조회할 Cart의 데이터중 member_id 컬럼은, 현재 로그인 중인 사용자의 id이어야 한다. (where)
  • 조회할 Cart_Line의 데이터중 cart_id 컬럼은 Carts테이블의 cart_id와 같아야 한다.(join)
  • 조회할 items의 데이터중 item_id는 cart_line에 포함된 item_id와 같아야 한다.(join)

 

 

앞선 조건에 의거하여 최종적으로, 만들어낸 JPQL은 위와 같습니다. DTO로 조회하기 위해서, select절에 new Dto()로 기입된것을 볼 수 있습니다.

 

 

 

 

3. 장바구니 표현계층과 View 구현

이 포스팅에서는, 장바구니 화면을 출력하기 위한 표현계층과 View를 구현하는 방법만 알아보도록 하겠습니다. 나머지는 아래의 전체코드를 참고하며 구현해주세요.

 

전체코드 : https://gitlab.com/galid1/jpa-commerce

 

3.1 Controller 구현

우선 장바구니 페이지를 반환하기 위해서 해야할 일은 다음과 같습니다.

 

  1. SpringSecurity의 기능을 이용해, 현재 로그인 중인 사용자의 정보를 얻어와 이를 통해 MemberEntity를 구합니다.

별도로 구현한 AuthenticationConverter에 Spring Security에서 제공하는 Authentication을 넘겨주면, MemberEntity를 반환해줍니다. 이를 통해 memberId를 구할 수 있습니다.

 

  1. cartService의 장바구니 조회 메소드를 이용해, 장바구니 화면에 필요한 데이터를 요청합니다.

 

  1. Model에 데이터를 매핑하고, 장바구니 페이지를 반환합니다.

 

 

 

3.2 View 구현

Cart 페이지 전체코드 : https://gitlab.com/galid1/jpa-commerce/-/blob/master/src/main/resources/templates/carts/cart.html

View를 구현하는 방법들은 이전 포스팅에서도 많이 알아보았으니, 장바구니에서 여러 기능을 처리하기 위한 Jquery부분만을 살펴보고 포스팅을 마치겠습니다.

 

getCartInputList() 함수는, 장바구니의 체크박스를 순회하며, 체크된 것만을 가져와 주문을 위한 OrderLine을 만들기 위한, <input>을 생성합니다.

 

이 작업이 끝나면 최종적으로 주문버튼이 클릭될 때, <form> 태그를 만들고, 앞서 생성한 함수를 이용해 orderLine에 해당하는 Input과 함께, 주문요청을 하게 됩니다.

 

 

 

4. 최종 도메인 모델

장바구니가 추가된 최종 도메인 모델은 위와 같습니다.