IT recording...
[스프링 JPA1] 2. 도메인 개발 본문
[원문 링크]
https://adorable-aspen-d23.notion.site/JPA1-2-147b0c2406c84ffda4944650c61a66e9
김영한님의 [실전!스프링부트와JPA활용1 - 웹 어플리케이션개발] 강의를 듣고 작성한 글입니다.
1. 어플리케이션 아키텍처
- 도메인, 리포지토리, 서비스 개발
- 테스트를 통한 검증
- 컨트롤러 개발
요구사항 분석
- 회원 기능
- 회원 등록
- 회원 조회
- 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
- 상품 기능
- 상품 등록
- 상품 수정
- 상품 조회
- 기타 요구사항
- 상품은 재고 관리가 필요하다.
- 상품 주문시 배송 정보를 입력할 수 있다.
2. 회원 도메인 개발
- 기능
- 회원 등록
- 회원 조회
- 개발 순서
- 리포지토리 → 서비스 → 테스트
2-1. MemberRepository
- save, findOne, findAll, findByName 구현
@Repository //component스캔에 의해 자동으로 spring bean으로 관리
@RequiredArgsConstructor
public class MemberRepository {
//@PersistenceContext //JPA를 사용하므로 JPA표준 어노테이션 사용
private final EntityManager em;//spring이 entityMAnager 만들어서 자동으로 주입해줌
public Long save(Member member){
em.persist(member); //db에 insert쿼리
return member.getId();
}
public Member findOne(Long id){
return em.find(Member.class, id); //단건 조회 (타입,pk)
}
//SQL은 테이블을 대상으로 쿼리를 하지만, JPQL은 엔티티(객체)를 대상으로 쿼리를 한다.
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class) //createQuery(쿼리,반환타입)
.getResultList();
}
public List<Member> findByName(String name){
return em.createQuery("select m from Member m where m.username = :name", Member.class)
.setParameter("name",name) //파라미터바인딩
.getResultList();
}
}
- @Repository
- component 스캔에 의해 자동으로 spring bean 으로 관리된다.
- @RequiredArgsConstructor
- 생성자가 하나인 경우, final변수를 자동으로 injection해준다.
public class MemberRepository{
@PersistenceContext
private final EntityManager em;
}
=====================
@RequiredArgsConstructor
public class MemberRepository{
private final EntityManager em;
}
2-2. MemberService
- 회원가입(join), 회원조회(findMembers, findOne)
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
//회원 가입
@Transactional //쓰기
public Long join(Member member){
//중복회원 검증 로직
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
//Exception
List<Member> findMembers = memberRepository.findByName(member.getUsername());
if(!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
//회원 전체 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
//회원 조회
public Member findOne(Long memberId){
return memberRepository.findOne(memberId);
}
}
- @Transactional(readOnly = true)
- 영속성 컨텍스트를 지정한다.
- 읽기 전용에서 명시하여 성능 향상한다.
- Injection
- 필드 주입
- 테스트 시 바꿔야 할 수 있는데 엑세스할 수 없다.
public class MemberService { @Autowired MemberRepository memberRepository; ... }
- 생성자 주입
- 직접 주입하기 때문에 놓치지 않고 주입 가능하다.
- 변경이 불가능한 안전한 객체 생성이 가능하다.
- 생성자가 하나면, @Autowired 생략이 가능하다.
- final 키워드를 추가하면 컴파일 시점에 memberRepository를 설정하지 않는 오류를 체크할 수 있다.
public class MemberService { private final MemberRepository memberRepository; public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } ... }
- lombok
- 와따다. 이걸 쓰자
- 필드 주입
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
...
}
2-3. MemberServiceTest
- 테스트 요구사항
- 회원가입을 성공해야한다.
- 회원가입 시 중복이름이 존재하면 예외가 발생해야 한다.
@RunWith(SpringRunner.class)
@SpringBootTest //AutoWired하기 위해 필요
@Transactional //테스트가 끝나면 롤백
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Autowired //2. 눈으로 보고싶어
EntityManager em;
@Test
//@Rollback(false) //1. @Transactional은 기본적으로 롤백을 하는데, 롤백안하고 눈으로 다 보고싶어 /But 테스트는 반복해서 해야하기 때문에 롤백되어야 함 -> 2
public void 회원가입() throws Exception{
//given
Member member = new Member();
member.setUsername("kim");
//when
em.flush(); //2. 눈으로 보고싶어
Long savedId = memberService.join(member);
//then
Assertions.assertEquals(member, memberRepository.findOne(savedId));
}
@Test
public void 중복_회원_예외() throws Exception{
//given
Member member1 = new Member();
member1.setUsername("kim");
Member member2 = new Member();
member2.setUsername("kim");
//when
memberService.join(member1);
//then
//Junit4
//fail("예외가 발생해야 한다."); //위에서 예외가 터져서 여기 도달하지 말아야 함
//Junit5
IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertEquals("이미 존재하는 회원입니다.", thrown.getMessage());
}
}
- @RunWith(SpringRunner.class)
- 스프링과 테스트 통합
- @SpringBootTest
- 스프링 부트 띄우고 테스트
- AutoWired가 가능하게 한다.
- @Transactional
- 반복 가능한 테스트 지원
- 각 테스트마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백한다.
- @Rollback(false)
- 데이터를 롤백하지 않고 눈으로 보고싶을 때 사용한다.
- TDD
@Test
public void 테스트() throws Exception{
//given
//when
//then
}
3. 주문 도메인 개발
- 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
- 주문 검색
- 개발 순서
- 엔티티 → 리포지토리 → 서비스 → 테스트
2-1. Order 엔티티
- 도메인 내부에서 해결할 수 있는 비즈니스 로직을 처리한다. (도메인 중심 설계)
- 주문취소 - cancel()
- 전체 주문 가격 조회 - getTotalPrice()
/**
* 주문 취소
*/
public void cancel(){
if(delivery.getStatus() == DeliveryStatus.COMP){ //이미 배송 완료일 경우
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for(OrderItem orderItem:orderItems){
orderItem.cancel();
}
}
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
- 생성 메서드 createXXX
- 엔티티 생성
- 엔티티의 생성 시점을 명확히 하여 유지보수를 용이하게 한다.
//==생성 메서드==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){
Order order = new Order();
//set
order.setMember(member);
order.setDelivery(delivery);
for(OrderItem orderItem:orderItems){
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
2-2. OrderItem 엔티티
@Entity
@Getter @Setter
**@NoArgsConstructor(access = AccessLevel.PROTECTED) //protected 생성자 역할**
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY) //orderItem이 다 관계
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice; //주문 당시 가격
private int count; //주문 당시 수량
// protected OrderItem() { //protected -> 생성자를 사용하지 말아라라고 말해주는 것임
// }
**//==생성 메서드==//
public static OrderItem createOrderItem(Item item, int orderPrice, int count){
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}**
**//==비즈니스 로직==//
public void cancel() {
getItem().addStock(count); //재고수량 원복e
}
public int getTotalPrice() {
return getOrderPrice() * getCount();
}**
}
- @NoArgsConstructor
- 외부에서 생성자를 통해 객체를 생성할 수 없도록 한다.
-> protected OrderItem(){} 의 역할 수행
- 주문과 동시에 재고를 관리해야 한다.
- item의 수량은 item의 비즈니스 로직을 이용하여 처리한다.
2-3) OrderRepository
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
public void save(Order order){
em.persist(order);
}
public Order findOne(Long id){
return em.find(Order.class,id);
}
//동적쿼리 (조건이 어느하나 빠질수도있음 -> 없으면 다 가져와)
//1. JPQL을 string으로 하기
//2. JPA에서 표준으로 동적쿼리를 작성할 수 있게 하는 것 -> But 유지보수가 안됨
//3. 결론) QueryDSL을 쓰자!
public List<Order> findAll(OrderSearch orderSearch) {
....
return query.getResultList();
}
}
- QueryDSL 알아보기
2-4) OrderService
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
/**
* 주문
*/
@Transactional
public Long order(Long memberId, Long itemId, int count){
//엔티티 조회
Member member = memberRepository.findOne(memberId);
Item item = itemRepository.findOne(itemId);
//배송정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
//주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
//주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
//new Order(); -> 생성자에 오류가 남 -> noArgsConstructor를 사용했네? -> create메서드가 있구나
//주문 저장
orderRepository.save(order);
return order.getId();
}
//취소
@Transactional
public void cancelOrder(Long orderId){
//주문 탐색
Order order = orderRepository.findOne(orderId);
//주문 취소
order.cancel(); //도메인 내부에서 비즈니스 로직 수행
}
//검색
public List<Order> findOrders(OrderSearch orderSearch){
return orderRepository.findAllByCriteria(orderSearch);
}
}
- 주문
- 회원id, 상품id, 수량을 전달 받아 주문을 진행한다.
- 각 repository에서 entity를 찾아야 한다.
- cascade
- 각 entity들의 라이프 사이클이 정확하게 동일하게 관리될 때만 사용한다.
- 만약 delivery가 굉장히 중요한 정보라 다른 곳에서 참조해서 사용한다면, cascade를 사용할 때 order를 지울 때 delivery도 함께 지워진다.
- 각 entity들의 라이프 사이클이 정확하게 동일하게 관리될 때만 사용한다.
@OneToMany(mappedBy = "order", **cascade = CascadeType.ALL**)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, **cascade = CascadeType.ALL**)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
→ Order에서 OrderItem과 Delivery를 cascade하게 해두었기 때문에, Order만 저장하면 이 둘은 알아서 각 리포지터리에 저장된다.
- 영속성 객체의 JPA의 자동 update쿼리
- 영속성 객체 : repository에서 꺼내온 객체
- JPA에서는 엔티티(객체) 자체의 데이터만 변경해주면 ‘변경내역감지'가 작동하여 update SQL 쿼리가 알아서 촥촥 날라간다.
@Transactional
public void cancelOrder(Long orderId){
//주문 탐색
Order order = orderRepository.findOne(orderId);
//주문 취소
order.cancel(); //도메인 내부에서 비즈니스 로직 수행
//JPA에서는 엔티티 내부에서 데이터만 바꿔주면 변경내역감지가 작동하여 sql쿼리가 알아서 촥촥 날라간다.
//여기다가 sql쿼리 블라블라 안적어도 됨
}
2-4. 도메인 모델 패턴
- 비즈니스 로직의 대부분이 엔티티에 존재한다.
- 서비스 계층은 단순히 위임의 역할을 수행한다.
2-5. OrderService 테스트
- 테스트 요구사항
- 상품 주문이 성공해야 한다.
- 상품을 주문할 때 재고 수량을 초과하면 안된다.
- 주문 취소가 성공해야 한다.
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
@Autowired EntityManager em;
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception{
//given
Member member = createMember();
Book book = createBook("시골 JPA", 10000, 10);
//when
int orderCount = 2;
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
//then
Order getOrder = orderRepository.findOne(orderId);
assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
assertEquals("주문한 삼품 종류 수가 정확해야 한다.",1,getOrder.getOrderItems().size());
assertEquals("주문 가격은 가격 * 수량이다.",10000 * orderCount,getOrder.getTotalPrice());
assertEquals("주문 수량만큼 재고가 줄어야 한다.",8,book.getStockQuantity());
}
@Test
public void 주문취소() throws Exception{
//given
Member member = createMember();
Book item = createBook("시골 JPA", 10000, 10);
int orderCount = 2;
Long orderId = orderService.order(member.getId(), item.getId(), orderCount);
//when
orderService.cancelOrder(orderId);
//then
Order getOrder = orderRepository.findOne(orderId);
assertEquals("주문 취소시 상태는 CANCEL이다.",OrderStatus.CANCEL,getOrder.getStatus());
assertEquals("주문 취소된 상품은 그만큼 재고가 증가해야 한다.",10,item.getStockQuantity());
}
//통합테스트도 의미가 있지만, Item의 removeStock을 테스트하는 unitTest가 더 중요하다.
@Test(expected = NotEnoughStockException.class)
public void 상품주문_재고수량초과() throws Exception{
//given
Member member = createMember();
Book item = createBook("시골 JPA", 10000, 10);
int orderCount = 11;
//when
orderService.order(member.getId(),item.getId(), orderCount);
//then
Assertions.fail("재고 수량 부족 예외가 발생해야 한다."); //위에서 예외가 터져서 여기를 오면 안됨
}
//Data setting
private Book createBook(String name, int price, int stockQuantity) {
Book book = new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockQuantity);
em.persist(book);
return book;
}
private Member createMember() {
Member member = new Member();
member.setUsername("회원1");
member.setAddress(new Address("서울","강가","123-123"));
em.persist(member);
return member;
}
}
2-6. 주문 검색 기능 개발
- 회원 이름, 상품 이름으로 검색하는 기능이 있다고 하자.
- 이때 모든 정보를 다 입력하지 않아도 검색이 가능하다고 하면 동적으로 쿼리를 작성해야 한다.
- ⇒ QueryDSL 사용
- OrderSearch
- 검색 조건 파라미터 클래스 생성
- 검색에서 쓰이는 파라미터들만 받아오는 data를 작성한다.
- Controller에서 OrderSearch를 사용하여 정보를 받아옴
@GetMapping("/orders")
public String orderList(**@ModelAttribute("orderSearch")OrderSearch orderSearch**, Model model){
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders",orders);
return "order/orderList";
}
public List<Order> findOrders(OrderSearch orderSearch){
return orderRepository.findAllByCriteria(orderSearch);
}
public List<Order> findAll(OrderSearch orderSearch) {
//QueryDSL사용
...
return query.getResultList();
}
'Spring' 카테고리의 다른 글
[Redis] Redis란? - (1) (0) | 2022.04.01 |
---|---|
[스프링 JPA1] 3. 컨트롤러 (0) | 2022.02.15 |
[Spring] 개발 중 마주한 오류들 - 1 (0) | 2022.02.15 |
[스프링 JPA1] 1. 요구사항 분석 및 도메인 셜계 (0) | 2022.02.07 |
[스프링 MVC1] 7. 웹 페이지 만들기 (0) | 2022.02.07 |
Comments