- 회원기능
- 회원 등록
- 회원 조회 - 상품 기능
- 상품 등록
- 상품 수정
- 상품 조회 - 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
이 순서대로 개발하겠습니다.
다음 기능은 구현하지 않겠습니다.
- 로그인과 권한 관리는 하지 않는다.
- 파라미터 검증과 예외 처리는 하지 않는다.
- 상품은 도서만 사용한다.
- 카테고리는 사용하지 않는다.
- 배송 정보는 사용하지 않는다.
1. 개발 방법
- Controller : MVC 컨트롤러가 모여 있는 곳이다. 컨트롤러는 서비스 계층을 호출하고 결과를 뷰(JSP)에 전달한다.
- Service : 서비스 계층에는 비즈니스 로직이 있고 트랜잭션을 시작한다. 서비스 계층은 데이터 접근 계층인 리포지토리를 호출한다.
- Repository : JPA를 직접 사용하는 곳은 리포지토리 계층이다. 여기서 엔티티 매니저를 사용해서 엔티티를 저장하고 조회한다.
- Domain : 엔티티가 모여 있는 계층이다. 모든 계층에서 사용한다.
개발 순서는 서비스와 리포지토리 계층을 먼저 개발하고 테스트 케이스를 작성해서 검증한다. 검증을 완료하면 컨트롤러와 뷰를 개발하는 순서로 진행해야 한다. 회원 기능부터 하나씩 개발해보자.
2. 회원 기능
- 회원 등록
- 회원 목록 조회
회원 기능 코드
이 회원 엔티티를 저장하고 관리할 리포지토리 코드를 보자.
- 회원 레포지토리 분석
코드를 분석해보자
@Repository
어노테이션이 붙어 있으면 <context : component-scan>에 의해 스프링 빈으로 자동 등록된다. 그리고 이 어노테이션에는 한 가지 기능이 더 있는데 JPA 전용 예외가 발생하면 스프링이 추상화한 예외로 변환해준다. 따라서 서비스 계층은 JPA에 의존적인 예외를 처리하지 않아도 된다.
@PersistenceContext
순수 자바 환경에서는 엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성해서 사용했지만, 스프링이나 J2EE 컨테이너를 사용하면 컨테이너가 엔티티 매니저를 관리하고 제공해준다. 따라서 엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성해서 사용하는 것이 아니라 컨테이너가 제공하는 엔티티 매니저를 사용해야 한다.
@PersistenceContext는 컨테이너가 관리하는 엔티티 매니저를 주입하는 어노테이션이다. 이렇게 엔티티 매니저를 컨테이너로부터 주입 받아서 사용해야 컨테이너가 제공하는 트랜잭션 기능과 연계해서 컨테이너의 다양한 기능들을 사용 할 수 있다.
@PersistenceUnit
@PersistenceContext를 사용해서 컨테이너가 관리하는 엔티티 매니저를 주입 받을 수 있어서 엔티티 매니저 팩토리를 직접 사용할 일은 거의 없겠지만, 엔티티 매니저 팩토리를 주입받으려면 다음처럼 @PersistenceUnit 어노테이션을 사용하면 된다.
다음 코드는 회원 엔티티를 저장(영속화)한다.
다음 코드는 회원 식별자로 회원 엔티티를 조회한다.
다음 코드는 JPQL을 사용해서 이름(name)으로 회원 엔티티들을 조회한다.
- 회원 서비스 분석
@Service : 이 어노테이션이 붙어 있는 클래스는 <context : component-scan>에 의해 스프링 빈으로 등록된다.
@Transactional : 스프링 프레임워크는 이 어노테이션이 붙어 있는 클래스나 메서드에 트랜잭션을 적용한다. 외부에서 이 클래스의 메서드를 호출할 때 트랜잭션을 시작하고 메서드를 종료할 때 트랜잭션을 커밋한다. 만약 예외가 발생하면 트랜잭션을 롤백한다.
@Autowired를 사용하면 스프링 컨테이너가 적절한 스프링 빈을 주입해준다. 여기서는 회원 리포지토리를 주입한다.
- 회원 가입
join() 메서드를 사용한다. 이 메서드는 먼저 validateDuplicateMember()로 같은 이름을 가진 회원이 있는지 검증하고 검증을 완료하면 회원 리포지토리에 회원 저장을 요청한다. 만약 같은 이름을 가진 회원이 존재해서 검증에 실패하면 "이미 존재하는 회원입니다." 라는 메시지를 가지는 예외가 발생한다. 회원가입에 성공하면 생성된 회원 식별자를 반환한다.
회원 기능 테스트
JUnit으로 테스트를 작성해서 검증해보자
- 회원가입을 성공해야 한다.
- 회원가입을 할 때 같은 이름이 있으면 예외가 발생해야 한다.
먼저 잘 동작하는지 테스트해보자.
- 회원가입 테스트
테스트를 스프링 프레임워크와 함께 실행하기 위해 스프링 프레임워크와 JUnit을 통합해야 한다.
- 스프링 프레임워크와 테스트 통합
JUnit으로 작성한 TC를 스프링 프레임워크와 통합하려면 @org.junit.runner.RunWith에 org.springframework.test.context.junit4.SpringJUnit4ClassRunner.class를 지정하면 된다. 이러면 @Autowired 같은 기능들을 사용할 수 있다.
테스트 케이스를 실행할 때 사용할 스프링 설정 정보를 지정한다. 여기서는 설정 정보로 appConfig.xml을 지정했다. 웹과 관련된 정보는 필요하지 않으므로 webAppConfig.xml은 지정하지 않았다.
@Transactional 어노테이션은 보통 비즈니스 로직이 있는 서비스 계층에서 사용한다. 그런데 이 어노테이션을 테스트에서 사용하면 동작 방식이 달라진다. 이때는 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백한다. 따라서 반복 테스트가 가능하다.
- 회원가입() 테스트 케이스
회원 엔티티를 하나 생성하고 join() 메서드로 회원가입을 시도한다. 그리고 실제 회원이 저장되었는지 검증하기 위해 리포지토리에서 회원 id로 회원을 찾아서 저장한 회원과 같은지 assertEquals로 검증한다.
- 중복 회원 예외처리 테스트
이름이 같은 회원은 중복 저장되면 x 예외가 발생해야 함.
@ Test.expected 속성에 예외 클래스를 지정하면 테스트의 결과로 지정한 예외가 발생해야 테스트가 성공한다. 따라서 테스트 결과로 IllegalStateException이 발생해야 테스트가 성공한다. 2번째 회원가입때 검증 로직에서 예외가 발생하므로 fail은 호출되지 않는다. fail이나 IllegalStateException이 발생하지 않으면 테스트는 실패한다.
3. 상품 기능
- 상품 등록
- 상품 목록 조회
- 상품 수정
상품 기능 코드
- 상품 엔티티 분석
밑의 엔티티는 단순히 접근자와 수장자 메서드만 있는 것이 아니라 재고 관련 비즈니스 로직을 처리하는 메서드도 있다.
- addStock() 메서드는 파라미터로 넘어온 수만큼 재고를 늘린다. 이 메서드는 재고가 증가하거나 상품 주문을 취소해서 재고를 다시 늘려야 할 때 사용한다.
- removeStock() 메서드는 파라미터로 넘어온 수만큼 재고를 줄인다. 만약 재고가 부족하면 예외가 발생한다. 주로 상품을 주문할 때 사용한다.
- 상품 리포지토리 분석
상품 리포지토리에선 save() 메서드를 유심히 봐야 하는데 이 메서드 하나로 저장과 수정(병합)을 다 처리한다. 코드를 보면 식별자 값이 없으면 새로운 엔티티로 판단해서 persist()로 영속화하고 만약 식별자 값이 있으면 이미 한 번 영속화 되었던 엔티티로 판단해서 merge()로 수정(병합) 한다. 결국 여기서의 저장(save)이라는 의미는 신규 데이터를 저장하는 것뿐만 아니라 변경된 데이터의 저장이라는 의미도 포함한다.
이렇게 함으로써 이 메서드를 사용하는 클라이언트는 저장과 수정을 구분하지 않아도 되므로 클라이언트의 로직이 단순해진다. 여기서 사용하는 수정(병합)은 준영속 상태의 엔티티를 수정할 때 사용한다. 영속 상태의 엔티티는 변경 감지 기능이 동작해서 트랜잭션을 커밋할 때 자동으로 수정되므로 별도의 수정 메서드를 호출할 필요가 없고 그런 메서드도 없다.
- 상품 서비스 분석
4. 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
주문 기능 코드
- 주문 엔티티
- 생성 메서드(createOrder()) : 주문 엔티티를 생성할 때 사용한다. 주문 회원, 배송 정보, 주문상품의 정보를 받아서 실제 주문 엔티티를 생성한다.
- 주문 취소(cancel()) : 주문 취소 시 사용한다. 주문 상태를 취소로 변경하고 주문 상품에 주문 취소를 알린다. 만약 이미 배송을 완료한 상품이면 주문을 취소하지 못하도록 예외를 발생시킨다.
- 전체 주문 가격 조회 : 주문 시 사용한 전체 주문 가격을 조회한다. 전체 주문 가격을 알려면 각각의 주문상품 가격을 알아야 한다. 로직을 보면 연관된 주문 상품들의 가격을 조회해서 더한 값을 반환한다.
- 주문상품 엔티티
- 생성 메서드(createOrderitem()): 주문 상품, 가격, 수량 정보를 사용해서 주문상품 엔티티를 생성한다. 그리고 item.removeStock(count)를 호출해서 주문한 수량만큼 상품의 재고를 줄인다.
- 주문 취소(cancel()): getItem().addStock(count)를 호출해서 취소한 주문 수량만큼 상품의 재고를 증가시킨다.
- 주문 가격 조회(getTotalPrice()): 주문 가격에 수량을 곱한 값을 반환한다.
- 주문 리포지토리
- 주문 서비스
- 주문 : 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보를 받아서 실제 주문 엔티티를 생성한 후 저장한다.
- 주문 취소 : 주문 식별자를 받아서 주문 엔티티를 조회한 후 주문 엔티티에 주문 취소를 요청한다.
- 주문 검색 : OrderSearch 검색 조건을 가진 객체로 주문 엔티티를 검색한다.
주문 검색 기능
위는 회원1 인 회원을 검색하는 화면이다. 결과를 보면 회원1만 검색된 것을 확인할 수 있다. 이름 검색 옆에 주문상태(주문,취소)를 선택하면 검색 범위를 더 줄일 수 있다.
주문 내역을 검색하는 위의 findAll(OrderSearch orderSearch) 메서드는 검색 조건에 따라 Criteria를 동적으로 생성해서 주문 엔티티를 조회한다.
주문 기능 테스트
- 상품 주문이 성공해야 한다.
- 상품을 주문할 때 재고 수량을 초과하면 안 된다.
- 주문 취소가 성공해야 한다.
- 상품 주문 테스트
상품 주문이 정상 동작하는지 확인하는 테스트다. Given 절에서 테스트를 위한 회원과 상품을 만들고 When 절에서 실제 상품을 주문하고 Then 절에서 주문 가격이 올바른지, 주문 후 재고 수량이 정확히 줄었는지 검증한다.
- 재고 수량 초과 테스트(이때는 NotEnoughStockException 예외가 발생해야 한다)
재고는 10권인데 11개를 주문했다. 주문 초과로 다음 로직에서 예외가 발생한다.
- 주문 취소 테스트
주문을 취소하려면 먼저 주문을 해야 한다. Given 절에서 주문하고 When 절에서 주문을 취소했다. Then 절에서 주문 상태가 주문 취소 상태인지,취소한 만큼 재고가 증가했는지 검증한다.
5. 웹 계층 구현
상품 등록
- 상품 등록 폼
첫 화면에서 상품 등록을 선택하면 /items/new URL을 HTTP GET 방식으로 요청한다. 스프링 MVC는 HTTP 요청 정보와 @RequestMapping의 속성 값을 비교해서 실행할 메서드를 찾는다. 따라서 요청 정보와 매핑되는 createForm() 메서드를 실행한다.
이 메서드는 단순히 items/createItemForm 문자를 반환한다. 스프링 MVC의 뷰 리졸버는 이 정보를 바탕으로 실행할 뷰를 찾는다.
방금 반환한 문자(items/createItemForm)와 뷰 리졸버에 등록한 setPrefix(), setSuffix() 정보를 사용해서 렌더링할 뷰(JSP)를 찾는다.
- 상품 등록
Submit 버튼을 클릭하면 /items/new를 POST 방식으로 요청한다. 그러면 요청 정보와 매핑되는 상품 컨트롤러의 create(Book item) 메서드를 실행한다. 이 메서드는 상품 서비스에 상품 저장을 요청하고 저장이 끝나면 상품 목록 화면으로 리다이렉트한다.
상품 목록
위 jsp를 보면 model에 담아둔 상품 목록인 items를 꺼내서 상품 정보를 출력한다.
상품 수정
수정 버튼을 선택하면 GET 방식으로 요청한다. 결과로 UpdateItemForm() 메서드를 실행하는데 이 메서드는 itemService.fineOne(itemId)를 호출해서 수정할 상품을 조회한다. 그리고 조회 결과를 모델 객체에 담아서 뷰에 전달한다.
Submit 버튼을 클릭하면 /items/{itemId}/ edit URL을 POST 방식으로 요청하고 updateItem() 메서드를 실행한다. 이때 컨트롤러에 파라미터로 넘어온 item 엔티티 인스턴스는 현재 준영속 상태다. 따라서 영속성 컨텍스트의 지원을 받을 수 없고 데이터를 수정해도 변경 감지 기능은 동작하지 않는다.
변경 감지와 병합
- 변경 감지 기능 사용
- 병합 사용
- 변경 감지 기능 사용
영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법이다.
이 코드처럼 트랜잭션 안에서 준영속 엔티티의 식별자로 엔티티를 다시 조회하면 영속상태의 엔티티를 얻을 수 있다. 이렇게 영속 상태인 엔티티의 값을 파라미터로 넘어온 준 영속 상태의 엔티티 값으로 변경하면 된다. 이후 트랜잭션이 커밋될 때 변경감지 기능이 동작해서 DB에 수정사항이 반영된다.
- 병합 사용
위와 거의 비슷하게 동작한다. 파라미터로 넘긴 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다. 그리고 영속 엔티티의 값을 준영속 엔티티의 값으로 채워 넣는다.
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만 병합을 사용 하면 모든 속성이 변경된다.
밑은 상품 리포지토리의 저장 메서드다. 이 메서드는 식별자가 없으면 새로운 엔티티로 판단해서 영속화 하고 식별자가 있으면 병합을 수행한다. 지금처럼 준영속 상태인 상품 엔티티를 수정할 때는 id 값이 있으므로 병합을 수행한다.
상품 주문
상품 주문을 선택하면 /order를 GET방식으로 호출해서 위에 있는 OrderController의 createForm() 메서드를 실행한다. 주문 화면에는 주문할 고객정보와 상품 정보가 필요하므로 model 객체에 담아서 뷰에 넘겨 준다.
다 선택해서 submit 을 누르면 /order URL을 POST방식으로 호출해서 컨트롤러의 order() 메서드를 실행한다. 이 메서드는 고객 식별자, 주문할 상품 식별자, 수량 정보를 받아서 주문 서비스에 주문을 요청한다. 주문이 끝나면 상품 주문 내역이 있는 /orders URL로 리다이렉트 한다.
'Spring > JPA' 카테고리의 다른 글
스프링 데이터 JPA (0) | 2023.07.18 |
---|---|
웹 애플리케이션 제작(도메인 모델과 테이블 설계) (0) | 2023.07.17 |
웹 애플리케이션 제작(프로젝트 환경설정) (0) | 2023.07.17 |
객체지향 쿼리 언어(4) 네이티브 SQL (0) | 2023.07.17 |
객체지향 쿼리 언어(3) QueryDSL (0) | 2023.07.17 |