회원 기능
상품 기능
주문 기능
도메인, 서비스, 리포지토리 -> 테스트 케이스 검증 -> 웹
@PersistenceContext
JPA의 엔티티 매니저를 스프링이 생성한 엔티티 매니저에 주입함
->
스프링 부트의 스프링JPA를 사용하면 @Autowired 로 대체 가능
->
@RequiredArgsConstructor
JPQL
SQL은 테이블을 대상으로, JPQL은 엔티티 객체를 대상으로 쿼리
em.persist(member)
영속성 컨텍스트에 멤버객체를 올린다
이 때, 영속성 컨텍스트는 id값을 key로 가져옴
따라서 persist로 꺼내면 항상 값이 보장됨
@Transactional(readOnly = true)
JPA가 조회 성능을 최적화한다
@RequiredArgsConstructor
롬복 라이브러리
final 필드의 생성자를 만들어줌
@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public MemberRepository(EntityManager em) {
this.em = em;
}
...
}
=
@Repository
public class MemberRepository {
@Autowired // 스프링 부트가 @PersistenceContext 대신 @Autowired도 주입을 지원 해줌
private EntityManager em;
public MemberRepository(EntityManager em) {
this.em = em;
}
...
}
=
@Repository
@RequiredArgsConstructor // 롬복
public class MemberRepository {
private final EntityManager em;
...
}
위의 테스트들은 h2 DB를 실행시키지 않으면 정상적으로 실행되지 않는다
springboot에서 격리된 환경에서의 테스트를 지원한다
src/test/resources/application.yml
spring:
datasource:
url: jdbc:h2:mem:test
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
# show_sql: true # sout에 출력
logging.level:
org.hibernate.SQL: debug # logger에 출력
org.hibernate.type: trace # 쿼리 파라미터 로그 남기기
main 디렉토리처럼 resources 디렉토리를 만들어 application.yml을 생성한다
src/test/resources/application.yml
spring:
# datasource:
# url: jdbc:h2:mem:test
# username: sa
# password:
# driver-class-name: org.h2.Driver
#
# jpa:
# hibernate:
# ddl-auto: create
# properties:
# hibernate:
# format_sql: true
# show_sql: true # sout에 출력
logging.level:
org.hibernate.SQL: debug # logger에 출력
org.hibernate.type: trace # 쿼리 파라미터 로그 남기기
스프링 부트가 yml DB 설정 없이도 동작한다
극단적으로, 전부 주석처리해도 동작한다
객체지향적으로,
데이터를 가지고 있는 쪽에 메서드를 생성하는 것이 좋다
따라서 재고 증가, 감소 로직은 Item 엔티티에 만드는 것이 좋다.
// 비즈니스 로직
/**
* stock 증가
*/
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
/**
* stock 감소소
*/
public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if (restStock < 0){
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
public void save(Item item){
if (item.getId() == null) { // 아이템이 등록되어있지 않으면 새로 등록
em.persist(item);
} else{ // 아이템이 있다면 업데이트
em.merge(item);
}
}
가장 중요한 파트
Transaction 스크립트 패턴 Domain 모델 패턴 중
Domain 모델 패턴에 대해 알아보자
// 생성 매서드
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
생성 메서드에서 주문생성에 대한 비즈니스 로직을 완결시켜 놓음
public void myMethod(String... strings){
// method body
}
varargs, 가변인자
String 객체가 0개부터 n개까지 매개변수로 올수 있다
항상 맨 뒤에 적어야 한다
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
=
public int getTotalPrice(){
return orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
@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);
// 주문 저장
orderRepository.save(order); // CascadeType.All
return order.getId();
}
}
Order 클래스에 CascadeType.ALl을 설정해놓았기 때문에
order 만 persist 해도
orderItem persist
delivery persist 가 작동한다
Cascade의 범위는 어디까지?
Order의 경우 OrderItem과 Delivery를 모두 관리한다
OrderItem과 Delivery는 모두 Order에서만 참조한다
참조의 주인이 명확한 경우에 사용하자
protected OrderItem(){
}
=
@NoArgsConstructor(access = AccessLevel.PROTECTED) //롬복
외부에서 new OrderItem()을 할 수 없게 한다
// 주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
생성 메서드로 생성하게 강제함
주문이 취소되면 상품 재고를 다시 늘려주어야 한다
일반적인 SQL 스타일이라면 상품재고를 조회하고, 변경된 수량만큼 다시 업데이트 하는 쿼리를 작성해야 했겠지만
JPA에서는 dirty check(변경 감지)를 통해 변경점을 찾아서 DB에 업데이트 쿼리를 날려준다
도메인 모델 패턴
주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다
서비스 계층은 엔티티에 필요한 요청을 위임하는 역할을 한다.
엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인모델 패턴이라고 한다.
트랜잭션 스크립트 패턴
반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴 이라고 한다.
좋은 테스트는 단위 테스트
도메인 모델 패턴은 단위 테스트에 적합하다
예제의 테스트는 통합 테스트에 가깝다
회원명으로 주문 내역을 검색하고 싶다.
이때 사용하는 것이 동적 쿼리이다.
JPA에서 동적 쿼리를 어떻게 해결할까?
public List<Order> findAll(OrderSearch orderSearch) {
return em.createQuery("select o from Order o join o.member m" +
" where o.status =:status" +
" and m.name like :name", Order.class)
.setParameter("status", orderSearch.getOrderStatus())
.setParameter("name", orderSearch.getMemberName())
// .setFirstResult(100)// 페이징 시작포인트
.setMaxResults(1000)// 최대 1000건
.getResultList();
}
만약 status, name 파라미터가 없으면 전부 조회해서 가져오고 싶다면
public List<Order> findAll(OrderSearch orderSearch) {
return em.createQuery("select o from Order o join o.member m"
.setMaxResults(1000)// 최대 1000건
.getResultList();
}
이런 코드가 나와야 한다
동적 쿼리가 필요한 시점이다.
public List<Order> findAll(OrderSearch orderSearch) {
//== 동적 쿼리 방법 1 ==//
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 =:status";
}
// 회원 이름 검색
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 (StringUtils.hasText(orderSearch.getMemberName())) {
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
이렇게 복잡한 동적쿼리 생성을 도와주는 도구 : mybatis
public List<Order> findAllByCriteria(OrderSearch orderSearch) {
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();
}
장점 : 동적 jqpl을 자바 코드로 작성할 수 있게 함
단점 : 유지보수성이 제로. 실무 사용 힘듬
출처
김영한 실전! 스프링 부트와 JPA 활용1 (인프런)