controller
,web
service
repository
domain
개발 순서
- 서비스, 리포지토리 계층을 개발 → 테스트 케이스 작성, 검증 → 웹 계층 적용
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 주입
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 많이 사용, 생성자가 하나면 생략 가능
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
- 반복 가능한 테스트 지원
- 각각의 테스트를 실행할 때마다 트랜잭션 시작, 테스트 종료 시 트랜잭션 강제 롤백
- 해당 애노테이션이 테스트 케이스에서 사용될 때만 롤백)
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;
}
}
addStock()
removeStock()
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);
}
}
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();
}
}
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);
}
}