회원 & 상품 도메인 개발

유병익·2022년 11월 29일
0
post-thumbnail

1. Application 구현 준비


1.1 Architecture


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

개발 순서

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



2. 회원 도메인 개발


2.1 Plan


  • 구현 기능
    • 회원 등록
    • 회원 목록 조회
  • 개발 순서
    - 회원 엔티티 코드 다시 보기
    - Repository 개발
    - Service 개발
    - 기능 테스트


2.2 Member Repository 개발


  • MemberRepository.java
    package jpabook.jpashop.repository;
    
    import jpabook.jpashop.domain.Member;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Repository;
    
    import javax.persistence.EntityManager;
    import java.util.List;
    
    @Repository
    @RequiredArgsConstructor
    public class MemberRepository {
    
        private final EntityManager em;
    
        public void save(Member member){
            em.persist(member);
        }
    
        public Member findOne(Long id){
            return em.find(Member.class, id);
        }
    
        public List<Member> findAll(){
            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();
        }
    }

기술 설명

  • @Repository
    • 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
  • @PersistenceContext
    • EntityManager 주입
  • @PersistenceUnit
    • EntityManagerFactory 주입



2.3 Member Service 개발


  • MemberService.java
    package jpabook.jpashop.service;
    
    import jpabook.jpashop.domain.Member;
    import jpabook.jpashop.repository.MemberRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    @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) {
            List<Member> findMembers = memberRepository.findByName(member.getName());
            if(!findMembers.isEmpty()){
                throw new IllegalStateException("이미 존재하는 회원입니다.");
            }
        }
    
        /*
         * 전체 회원 조회
         */
        public List<Member> findMembers(){
            return memberRepository.findAll();
        }
    
        public Member findOne(Long memberId){
            return memberRepository.findOne(memberId);
        }
    }

기술 설명

  • @Service
  • @Transactional
    • 트랜잭션, 영속성 컨텍스트
    • readOnly=true
      • 데이터의 변경이 없는 읽기 전용 메서드에 사용
      • 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용)
      • 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
  • @Autowired
    • 생성자 Injection 많이 사용, 생성자가 하나면 생략 가능
📌 멀티 쓰레드 상황을 고려 → 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전



2.4 Member Service Test


  • MemberServiceTest.java
    package jpabook.jpashop.service;
    
    import jpabook.jpashop.domain.Member;
    import jpabook.jpashop.repository.MemberRepository;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.annotation.Rollback;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.transaction.annotation.Transactional;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.fail;
    
    @Transactional  //테스트의 경우 테스트 이후 롤백
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class MemberServiceTest {
    
        @Autowired
        MemberService memberService;
        @Autowired
        MemberRepository memberRepository;
    
        @Test
        @Rollback(value = false)
        public void 회원가입() throws Exception {
            //given
            Member member = new Member();
            member.setName("MemberA");
    
            //when
            Long savedId = memberService.join(member);
            //then
            assertEquals(member, memberRepository.findOne(savedId));
    
        }
    
        @Test(expected = IllegalStateException.class)
        public void 중복회원예외() throws Exception {
            //given
            Member member1 = new Member();
            member1.setName("yoo");
    
            Member member2 = new Member();
            member2.setName("yoo");
    
            //when
            memberService.join(member1);
            memberService.join(member2); // 예외 발생 해야함!
    
            //then
            fail("예외가 발생해야 한다.");
        }
    
    }

기술 설명

  • @RunWith(SpringRunner.class)
    • 스프링과 테스트 통합
  • @SpringBootTest
    • 스프링 부트 띄우고 테스트(이게 없으면 @Autowired 다 실패)
  • @Transactional
    • 반복 가능한 테스트 지원
    • 각각의 테스트를 실행할 때마다 트랜잭션 시작, 테스트 종료 시 트랜잭션 강제 롤백
    • 해당 애노테이션이 테스트 케이스에서 사용될 때만 롤백)



3. 상품 도메인 개발


3.1 Plan


  • 구현 기능
    • 상품 등록
    • 상품 목록 조회
    • 상품 수정
  • 개발 순서
    • 상품 엔티티 개발(비즈니스 로직 추가)
    • Repository 개발
    • Service 개발
    • 기능 테스트



3.2 Item Entity


3.2.1 비지니스 로직 추가


  • Item.java
    package jpabook.jpashop.domain.item;
    
    import jpabook.jpashop.domain.Category;
    import jpabook.jpashop.exception.NotEnoughStockException;
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    import java.util.ArrayList;
    import java.util.List;
    
    @Entity
    @Getter
    @Setter
    @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
    @DiscriminatorColumn(name = "dtype")
    public abstract class Item {
    
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
    
        private String name;
        private int price;
        private int stockQuantity;
    
        @ManyToMany(mappedBy = "items")
        private List<Category> categories = new ArrayList<>();
    
        //비지니스 로직
    
        /*
        재고 수량 증가
         */
        public void addStock(int quantity){
            this.stockQuantity += quantity;
        }
    
        /*
        재고 수량 감소
         */
        public void removeStock(int quantity){
            int restStock = this.stockQuantity - quantity;
            if(restStock<0){
                throw new NotEnoughStockException("Need more stock");
            }
            this.stockQuantity = restStock;
        }
    
        public void changeItemInfo(String name, int price, int stockQuantity){
             this.name = name;
             this.price = price;
             this.stockQuantity = stockQuantity;
        }
    }



3.2.2 비지니스 로직 분석


  • addStock()
    • 파라미터로 넘어온 수만큼 재고 증가
    • 재고가 증가 or 상품 주문이 취소되어 재고를 다시 늘려야 할 때 사용
  • removeStock()
    • 파라미터로 넘어온 수만큼 재고 감소
      • 재고가 부족하면 예외 발생
    • 상품을 주문할 때 사용



3.2.3 예외 추가


  • NotEnoughStockException.java
    package jpabook.jpashop.exception;
    
    public class NotEnoughStockException extends RuntimeException {
        public NotEnoughStockException() {
            super();
        }
    
        public NotEnoughStockException(String message) {
            super(message);
        }
    
        public NotEnoughStockException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public NotEnoughStockException(Throwable cause) {
            super(cause);
        }
    
        protected NotEnoughStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    
    }



3.3 Item Repository


  • ItemRepository.java
    package jpabook.jpashop.repository;
    
    import jpabook.jpashop.domain.item.Item;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Repository;
    
    import javax.persistence.EntityManager;
    import java.util.List;
    
    @Repository
    @RequiredArgsConstructor
    public class ItemRepository {
        
        private final EntityManager em;
    
        public void save(Item item) {
            if (item.getId() == null) {
                em.persist(item);
            } else {
                em.merge(item);
            }
        }
    
        public Item findOne(Long id) {
            return em.find(Item.class, id);
        }
    
        public List<Item> findAll() {
            return em.createQuery("select i from Item i", Item.class)
                    .getResultList();
        }
    }



3.4 Item Service


  • ItemService.java
    package jpabook.jpashop.service;
    
    import jpabook.jpashop.domain.item.Item;
    import jpabook.jpashop.repository.ItemRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    @Service
    @Transactional(readOnly = true)
    @RequiredArgsConstructor
    public class ItemService {
    
        private final ItemRepository itemRepository;
    
        @Transactional
        public void save(Item item) {
            itemRepository.save(item);
        }
    
        @Transactional
        public void updateItem(Long itemId, String name, int price, int stockQuantity) {
            Item findItem = itemRepository.findOne(itemId);
            findItem.changeItemInfo(name, price, stockQuantity);
        }
    
        public List<Item> findItems() {
            return itemRepository.findAll();
        }
    
        public Item findOne(Long itemId) {
            return itemRepository.findOne(itemId);
        }
    }
profile
Backend 개발자가 되고 싶은

0개의 댓글