김영환님의 강의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 보면서 공부한 내용입니다.
📝 계층형 구조 사용
📝 개발 순서
서비스, 리포지토리 계층을 개발 → 테스트 케이스를 작성 → 검증 웹 계층 적용
📝 개발 순서
@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 생성
resources
폴더를 만든 후 application.yml
을 복사하여 테스트전용 DB를 생성할 수 있다.Cheet Sheet
클릭In-Memory에서 jdbc:h2:mem:test
복사
Test 폴더에 있는 applicaion.yml
의 url
에 복붙
📝 개발 순서
// == 비즈니스 로직 == //
/*
재고 수량 증가
*/
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 ...;
}
}
📝 개발 순서
// ========= 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();
}