도메인 개발
- Repository에서 사용되는 어노테이션
- @PersistenceContext : 스프링 컨테이너의 EntityManger 빈을 주입받는 것
- @PersistenceContext 대신 @RequiredArgsConstructor와 @Autowired를 사용해서 final EntityManager를 주입 받는 방법도 있다.
- @PersistenceUnit : 스프링 컨테이너의 EntityMangerFactory 빈을 주입받는 것
- @PersistenceContext : 스프링 컨테이너의 EntityManger 빈을 주입받는 것
- Entity 클래스에서 사용되는 어노테이션
- @GenerateValue : PK 값인 ID 값을 자동으로 생성해주는 역할로 EntityManger에 의해 persist()로 영속화될 때 ID값을 만들어서 넣어준다. (DB 저장 이전에 ID값 생성)
- ID를 만드는 방식은 별도 테이블로 관리 / 시퀀스 사용 ... 등등이 있으며 선택할 수 있다.
- @GenerateValue : PK 값인 ID 값을 자동으로 생성해주는 역할로 EntityManger에 의해 persist()로 영속화될 때 ID값을 만들어서 넣어준다. (DB 저장 이전에 ID값 생성)
- Service에서 사용되는 어노테이션
- @Transactional : JPA에서의 모든 데이터 변경이 트랜잭션 단위로 실행되도록 한다.
- Java에서 제공하는 Jakarta보다 Spring에서 제공하는 @Transactional을 사용하는 것이 더 좋다.
- 클래스 단위로 사용할 수 있고 메소드 단위로 사용할 수 있다.
- 메소드 단위 로 사용 시 @Transactional(readOnly = true) 옵션을 지정하면 조회 메소드에 성능을 최적화할 수 있다.
- 클래스 단위 설정과 메소드 단위 설정이 함께 있으면 메소드 단위 설정이 우선권을 갖는다.
- 클래스 단위로 readOnly = true 옵션을 지정하고 조회 메소드가 아닌 경우 메소드 단위 @Transactional을 따로 지정해주는 것을 권장.
- @Transactional : JPA에서의 모든 데이터 변경이 트랜잭션 단위로 실행되도록 한다.
- 의존성 주입 시 사용되는 어노테이션
- @Autowired : 어노테이션이 붙은 필드를 스프링 컨테이너에서 찾아서 빈을 주입해준다.
- 생성자 주입의 경우 생성자가 1개라면 따로 어노테이션을 사용하지 않아도된다.
- @AllArgsConstructor : 모든 필드를 갖는 생성자를 만들어준다.
- @RequiredArgsConstructor : final로 선언된 필드만 갖는 생성자를 만들어준다.
- 주로 생성자 주입을 위해 사용한다.
- @Autowired : 어노테이션이 붙은 필드를 스프링 컨테이너에서 찾아서 빈을 주입해준다.
- 의존성 주입이 필요한 필드는 final로 선언
- 의존성 주입 필드는 값이 이후에 바뀌지 않으며 초기화 하지 않는 경우 컴파일 오류가 발생하기에 좋다.
- 테스트 케이스 작성 시 쿼리 확인하고 싶을 때
- 테스트 코드는 @Transactional에 의해서 트랜잭션이 DB에 저장되기 전에 롤백된다. 따라서 INSERT 쿼리를 로깅으로 확인할 수 없고 DB에 저장된 내용을 볼 수 없다.
- @Rollback(value = false) 사용 시 롤백이 되지 않고 DB에 저장된다.
- em.flush()를 호출하게 되면 INSERT 쿼리는 확인할 수 있지만 DB에 저장되기 전 롤백된다.
- 테스트 코드는 @Transactional에 의해서 트랜잭션이 DB에 저장되기 전에 롤백된다. 따라서 INSERT 쿼리를 로깅으로 확인할 수 없고 DB에 저장된 내용을 볼 수 없다.
- Assertions.fail() : 해당 메소드에 도달하는 경우 테스트가 실패하도록 한다.
- Assertions.assertThrow() : 사용자가 예측한 오류가 던져질 경우 테스트를 성공으로 처리한다.
- 테스트 시에는 테스트 종료 후 초기화가 되고 테스트를 격리된 환경에서 진행하는 것이 좋다.
- 메모리 DB: 자바를 실행할 때 자바 안에서 DB를 만들어서 띄우는 방법 ( = 테스트를 격리된 환경에서 진행)
- 운영 환경에서는 main 디렉토리에 있는 것들이 우선권을 갖고 테스트 환경에서는 test 디렉토리에 있는 것들이 우선권을 갖는다.
- test 디렉토리에 resources 디렉토리를 만든 후 application.yml을 생성하고 별도로 세팅을 한다면 테스트 시 별도의 환경을 구축할 수 있다.
- 스프링 부트에서 별도의 설정이 없으면 메모리 DB모드로 실행하기 때문에 따로 설정하지 않고 파일만 만들어놓더라도 메모리 DB로 실행시킬 수 있다. (기본값은 create-drop)
- 테스트와 개발 환경을 따로 구축할 수 있기에 따로 구축하는 것을 권장한다.
- test 디렉토리에 resources 디렉토리를 만든 후 application.yml을 생성하고 별도로 세팅을 한다면 테스트 시 별도의 환경을 구축할 수 있다.
@Entity
public abstract class Item {
...
private int stockQuantity;
// ==재고 증가 비지니스 로직== //
public void addStock(int quantity) {
stockQuantity += quantity;
}
// ==재고 감소 비지니스 로직== //
public void removeStock(int quantity) {
int restStock = stockQuantity -= quantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
stockQuantity = restStock;
}
}
- 엔티티 클래스 설계 시 엔티티 자체가 해결할 수 있는 비지니스 로직은 엔티티 클래스에 포함시키는 것이 좋다.
- 객체지향적인 측면에서 봤을 때 해당 데이터를 가지고 있는 쪽에 비지니스 로직이 있는 것이 가장 좋다. (응집력 측면)
ex) Item 엔티티에 stockQuantity(재고) 필드가 있기 때문에 Item에서 재고 관련 비지니스 로직을 가지고 있는 것이 객체지향 측면에서 바람직하다.
- 객체지향적인 측면에서 봤을 때 해당 데이터를 가지고 있는 쪽에 비지니스 로직이 있는 것이 가장 좋다. (응집력 측면)
- 도메인 주도 설계 (DDD) : 도메인이 비지니스 로직의 주도권을 가지고 개발하는 것
- 도메인 주도 설계 시 서비스의 많은 로직이 엔티티로 이동하고 서비스는 엔티티를 호출하는 정도의 얇은 비지니스 로직을 가지게 된다.
- 도메인 주도 설계 시 information expert pattern을 지키면서 개발할 수 있다.
- 정보전문가 패턴 (Information expert pattern) : 디자인 패턴 중 하나이며 객체에게 책임을 할당할 때 가장 기본이 되는 책임 할당 원칙이다.
- 객체가 상태와 행동을 가지는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의 관점에서 표현한 것
- 정보와 행동을 최대한 가깝게 위치시킴으로 캡슐화를 유지할 수 있다.
- 더 응집력있고 이해하기 쉬운 코드를 작성할 수 있다.
- 도메인 주도 설계로 비지니스 로직을 엔티티에 포함(도메인 모델 패턴) vs 서비스에 모든 비지니스 로직 포함(트랜잭션 스크립트 패턴)
- 도메인 모델 패턴 : 엔티티를 객체로 사용하는 것
- 트랜잭션 스크립트 패턴: 엔티티를 자료 구조로 사용하는 방식
- 서로 각 장단점이 있기에 상황에 맞게 사용하는 것이 중요 (클린코드 6장 참고해 볼 것)
- cascade 속성으로 지정해놓는 경우 Repositoy를 따로 만들지 않아도 엔티티가 persist되면 해당 엔티티와 cascade로 묶여있는 엔티티들도 persist된다.
- cascade를 사용해도 되는 조건(모두 만족해야 한다)
- 라이프 사이클이 엔티티끼리 서로 같을 경우
- 해당 엔티티를 제외한 다른 엔티티에서는 참조하지 않을 경우
@NoArgsConstructor(access = AccessLevel.PROTECTED)
- Lombok 어노테이션으로 기본 생성자를 protected로 선언할 수 있다.
// == 생성 메소드 == //
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;
}
- createOrder() 메소드는 생성자와 같은 역할을 하는 생성하는 메소드이다.
- 생성자 대신 생성 메소드를 따로 사용하는 이유는 생성자와 달리 메소드의 이름을 별도로 부여할 수 있기에 메소드의 의도를 명확하게 표현할 수 있다.
동적 쿼리 작성
- 검색 조건 (회원명, 주문 상태)에 따른 주문 정보를 검색할 때 동적 쿼리를 어떻게 작성해야하는가?
public List<Order> findAll(OrderSearch orderSearch) {
//1번 방식 => 조건이 없는 경우에는 사용할 수 없음
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())
.setMaxResults(1000) //최대 1000건
.getResultList();
//2번 방식 => 하나하나 다 적어서 만든 동적 쿼리
String jpql = "select o from Order o join o.member";
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);
if (orderSearch.getOrderStatus() != null) {
query = query.setParameter("status", orderSearch.getOrderStatus());
}
if (StringUtils.hasText(orderSearch.getMemberName())) {
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
//3번 방식 => 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();
}
- 1번 방식은 검색 조건이 없는 경우에는 사용할 수 없다는 단점이 존재한다.
- 2번 방식은 하나하나 다 작성해서 JPQL로 처리하는 방식으로 번거롭고 실수로 인한 버그가 발생할 가능성이 높기에 실무에서 사용하지 않는 방식
- 3번 방식은 JPA Criteria를 사용하는 방식으로 JPA가 제공하는 JPQL을 자바 코드로 작성할 수 있도록 JPA에서 표준으로 제공하는 기능으로 실무에서는 잘 사용하지 않는 방식
- 결국 실무에서 사용해야하는 방식은 QueryDSL 방식이다.
출처: [인프런 김영한 실전 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발]
'Spring > [인프런 김영한 실전 스프링 부트와 JPA 활용 1]' 카테고리의 다른 글
[인프런 김영한 실전 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 웹 계층 개발 (1) | 2024.09.03 |
---|---|
[인프런 김영한 실전 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 도메인 분석 설계 (0) | 2024.08.29 |
[인프런 김영한 실전 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 프로젝트 환경설정 (0) | 2024.08.29 |