[스프링 JPA 활용] WEEK 8

enxnong·2023년 10월 28일
0

김영환님의 강의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 보면서 공부한 내용입니다.

🏊‍♀️ 섹션 3

애플리케이션 아키텍쳐

📝 계층형 구조 사용

  • controller, web : 웹 계층
  • service : 비즈니스 로직, 트랜잭션 처리
  • repository : JPA를 직접 사용하는 계층, 엔티티 매니저 사용
  • domain : 엔티티가 모여 있는 계층, 모든 계층에서 사용

📝 개발 순서
서비스, 리포지토리 계층을 개발 → 테스트 케이스를 작성 → 검증 웹 계층 적용

🏊‍♀️ 섹션 4

회원 도메인 개발

📝 개발 순서

  • 회원 엔티티 코드 검토
  • 회원 리포지토리 개발
  • 회원 서비스 개발
  • 회원 기능 테스트

회원 리포지토리 개발

    @PersistenceContext // jpa가 제공하는 표준 어노테이션
    // JPA의 엔티티 매니저를 스프링이 생성한 엔티티 매니저에 주입해준다
    private EntityManager em;

    // entity manager factory를 직접 주입하고 싶다면?
//    @PersistenceUnit
//    private EntityManagerFactory emf;

    // 회원 저장
    public void save(Member member){
        em.persist(member);
    }

    // 회원 조회(한명)
    public Member findOne(Long id){
        return em.find(Member.class, id);
    }

    // 회원 조회(전체)
    public List<Member> findAll(){
        // from의 대상이 테이블이 아닌 entity 객체
        return em.createQuery("select m from Member m", Member.class).getResultList();
    }

    // 회원 조회(이름)
    public List<Member> findByName(String name){
        return em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }

회원 서비스 개발

@Service
@Transactional(readOnly = true) // jpa의 모든 데이터 변경이나 로직들은 가급적이면 트랜잭션 안에서 처리해야됨
//@AllArgsConstructor // 모든 필드의 셍성자를 만들어줌
@RequiredArgsConstructor // final 있는 필드만 가지고 생성자를 만들어줌
public class MemberService {

    private final MemberRepository memberRepository;

    // 생성자 injection
//    @Autowired 생성자가 1개인경우 자동으로 Autowired 인젝션 해줌
//    public MemberService(MemberRepository memberRepository) {
//        this.memberRepository = memberRepository;
//    }

    // 회원 가입
    @Transactional(readOnly = false)
    public Long join(Member member){

        // 같은 이름인 경우 회원 가입 불가능
        validateDuplicateMember(member); // 중복 회원이면 해당 로직에서 끝
        memberRepository.save(member); // 영속성 컨텍스트에값이 저장되면서 db에 들어간 시점이 아니어도 pk(id값)거 생성됨
        return ...;
    }

    // 중복 회원 검증
    public void validateDuplicateMember(Member member){
        // EXCEPTION ver1
        ...;

		// EXCEPTION ver2
		// ...;
    }

    // 회원 전체 조회
    public List<Member> findAll(){
        return ...;
    }

    // 회원 단건 조회
    public Member findOne(Long id){
        return ...;
    }

}

회원 기능 테스트

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional // 테스트에서 트랜잭션은 기본적으로 rollback이 됨
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Autowired EntityManager em;

    @Test
//    @Rollback(fa/lse)
    public void 회원가입() throws Exception {
        // given
        Member member = new Member();
        member.setName("kim");

        // when
        Long saveId = memberService.join(member);

        // then
        em.flush();
        ...;
    }

//    @Test(excepted = IllegalStateException.class)
    @Test
    public void 중복_회원_예외() throws Exception {
        // given
        Member member1 = new Member();
        member1.setName("kim");

        Member member2 = new Member();
        member2.setName("kim");

        // when
        ...;

        // then
        fail("예외가 발생해야 합니다."); // 코드가 돌다가 오류가 발생하면 fail이 발생함 (원래는 fail까지 오면 안됨!)
    }

}

📝 테스트 전용 DB 생성

  • 테스트는 테스트 파일에 있는 것들이 우선권을 가진다. 그러므로 운영 로직과 동일하게 Test에 resources 폴더를 만든 후 application.yml을 복사하여 테스트전용 DB를 생성할 수 있다.

  • In-Memory에서 jdbc:h2:mem:test 복사

  • Test 폴더에 있는 applicaion.ymlurl에 복붙

🏊‍♀️ 섹션 5

상품 도메인 개발

📝 개발 순서

  • 상품 엔티티 개발(비즈니스 로직 추가)
  • 상품 리포지토리 개발
  • 상품 서비스 개발
  • 상품 기능 테스트

상품 엔티티 개발

    // == 비즈니스 로직 == //
    /*
    재고 수량 증가
     */
    public void addStock(int quantity){
        ...;
    }

    /*
    재고 수량 감소
     */
    public void removeStock(int quantity){
        int restStock = ...;
        if(restStock > 0){
            throw new NotEnoughStockException("need more stock");
        }
        ...;
    }

상품 리포지토리 개발

@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    // 상품 저장
    public void save(Item item){
        if(item.getId() == null){
            em.persist(item);
        } else {
            ...; // 이미 db에 등록된 것을 가져온 것이므로 update 개념으로 생각하면됨
        }
    }

    // 상품 조회(한개)
    public Item find(Long id){
        return ...;
    }

    // 상품 조회(전체)
    public List<Item> findAll(){
        return ...;
    }

}

상품 서비스 개발

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {

// 단순하게 상품 리포지토리를 위임받음

    public final ItemRepository itemrepository;

    @Transactional
    public void saveItem(Item item){
        ...;
    }

    public Item findOne(Long id){
        return ...;
    }

    public List<Item> findAll(){
        return ...;
    }
}

🏊‍♀️ 섹션 6

주문 도메인 개발

📝 개발 순서

  • 주문 엔티티, 주문상품 엔티티 개발
  • 주문 리포지토리 개발
  • 주문 서비스 개발
  • 주문 검색 기능 개발
  • 주문 기능 테스트

주문, 주문상품 엔티티 개발


// ========= Order.class ========== //

 // == 생성 메서드 == //
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
        Order order = new Order();
        ...;
        return order;
    }

    // == 비즈니스 로직 == //
    /*
    주문 취소
     */
    public void cancel() {
        if (delivery.getStatus() == DeliveryStatus.COMP) { // 배송 완료
             ...;
        }
        this.setStatus(OrderStatus.CANCEL);
        for (OrderItem od : orderItems) {
             ...;
        }
    }

    // == 조회 로직 == //
    /*
    전체 주문 가격 조회
     */
    public int getTotalPrice() {
         ...;
    }

// ========= OrderItem.class ========== //

    // == 생성 메서드 == //
    public static OrderItem createOrderItem(Item item, int orderPrice, int count){
        OrderItem orderItem = new OrderItem();
        ...;
        return orderItem;
    }

    // == 비즈니스 로직 == //
    /*
    주문 취소
     */
    public void cancel() {
       ...; // 재고 수량 원복
    }

    // == 조회 로직 == //
    /*
    주문 상품 전체 가격 조회
     */
    public int getTotalPrice() {
        return ...;
    }
    
    // ========= Item.class ========== //
    // == 비즈니스 로직 == //
    /*
    재고 수량 증가
     */
    public void addStock(int quantity){
        ...;
    }

    /*
    재고 수량 감소
     */
    public void removeStock(int quantity){
        int restStock = ...;
        if(restStock < 0){
            ...;
        }
        this.stockQuantity = restStock;
    }

주문 서비스 개발

@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 = ...;
        Item item = ...;

        // 배송정보 생성
        Delivery delivery = new Delivery();
        ...;

        // 주문 상품 생성
        OrderItem orderItem = ...;

        // 주문 생성
        Order order = ...;

        // 주문 저장
        ...;
        return order.getId();
    }

    // 취소
    @Transactional
    public void cancelOrder(Long orderId){
        // 주문 엔티티 조회
        Order order = ...;
        // 주문 취소
        ...;
    }

    // 검색
//    public List<Order> findOrders(OrderSearch orderSearch){
//        return ...;
//    }
}

주문 기능 테스트

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
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);

        int orderCount = 2;
        // when
        Long orderId = ...;

        // then
        Order getOrder = orderRepository.findOne(orderId);

        assertEquals...;
    }


    @Test
    public void 상품주문_재고수량초과() throws Exception {
        // given
        Member member = createMember();
        Book book = createBook("시골JPA",10000,10);

        int orderCount = 11;

        // when
//        ...;
        try{
            ...;// 예외가 발생해야 된다!!
        } catch (NotEnoughStockException exception){
            return;
        }

        // then
        fail("재고 수량 부족 예외가 발생해야 한다.");
    }

    @Test
    public void 주문취소() throws Exception {
        // given
        Member member = createMember();
        Book book = createBook("시골JPA",10000,10);

        int orderCount = 2;
        ...;

        // when
        ...;

        // then
        Order getOrder = orderRepository.findOne(orderId);

        assertEquals...;
    }

    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.setName("회원1");
        member.setAddress(new Address("서울", "강가", "123-123"));
        em.persist(member);
        return member;
    }

}

주문 검색 기능 개발

💡 실무에서 사용 안함!

    // 주문 검색 (방법 1)
    public List<Order> findAllByString(OrderSearch orderSearch){

        String jpql = "select o from Order o join o.member m";
        boolean isFirstCondition = true;

        // 주문 상태 검색
        if(orderSearch.getOrderStatus() != null){
            if(isFirstCondition){
                jpql += " where";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " o.status = :staus";
        }

        // 회원 이름 검색
        if(StringUtils.hasText(orderSearch.getMemberName())){
            if(isFirstCondition){
                jpql += " where";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " m.name like :name";
        }

        TypedQuery<Order> query = em.createQuery(jpql, Order.class)
                .setMaxResults(1000); // 최대 1000건

        if(orderSearch.getOrderStatus() != null){
            query = query.setParameter("status", orderSearch.getOrderStatus());
        }

        if(orderSearch.getMemberName() != null){
            query = query.setParameter("name", orderSearch.getMemberName());
        }

        return query.getResultList();
    }

    // 주문 검색 (방법 2)
    public List<Order> finaAllByCriteria(OrderSearch orderSearch){
        // jpa를 자바 코드로 작성할 수 있도록 표준으로 제공하는 방법
        // JPA Criteria
        // 단점 :

        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Order> cq = cb.createQuery(Order.class);
        Root<Order> o = cq.from(Order.class);
        Join<Object, Object> m = o.join("member", JoinType.INNER);

        List<Predicate> criteria = new ArrayList<>();

        // 상품 주문 검색
        if(orderSearch.getOrderStatus() != null){
            Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
            criteria.add(status);
        }

        // 회원 이름 검색
        if(StringUtils.hasText(orderSearch.getMemberName())){
            Predicate name =
                    cb.like(m.<String>get("name"), "%" + orderSearch.getMemberName() + "%");
            criteria.add(name);
        }

        cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
        TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000);
        return query.getResultList();
    }
profile
높은 곳을 향해서

0개의 댓글