웹 애플리케이션 만들기 진행 순서
- 프로젝트 환경설정 (프로젝트 구조, 메이븐과 라이브러리 설정, 스프링 프레임워크 설정)
- 도메인 모델과 테이블 설계
- 애플이케이션 기능 구현
프로젝트 환경설정
[JPA 사용을 위한 예외적인 설정]
1. 트랙잭션 관리자를 DatasourceTransactionManager가 아닌 JpaTransactionManager로 등록해야 됨
(JPA + JdbcTeplate + Mybatis 함께 사용가능)
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
2. JPA 예외를 스프링 프레임워크가 추상화한 예외로 변환하는 AOP 적용
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
3. 스프링 프레임워크에서 JPA를 사용하려면 스프링 프레임워크가 제공하는 엔티티 매니저 팩토리를 등록해야 됨
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan" value="jpabook.jpashop.domain"/> <!-- @Entity 탐색 시작 위치 -->
<property name="jpaVendorAdapter">
<!-- 하이버네이트 구현체 사용 -->
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
</property>
<property name="jpaProperties"> <!-- 하이버네이트 상세 설정 -->
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop> <!-- 방언 -->
<prop key="hibernate.show_sql">true</prop> <!-- SQL 보기 -->
<prop key="hibernate.format_sql">true</prop> <!-- SQL 정렬해서 보기 -->
<prop key="hibernate.use_sql_comments">true</prop> <!-- SQL 코멘트 보기 -->
<prop key="hibernate.id.new_generator_mappings">true</prop> <!-- 새 버전의 ID 생성 옵션 -->
<prop key="hibernate.hbm2ddl.auto">create</prop> <!-- DDL 자동 생성 -->
</props>
</property>
</bean>
hibernate.id.new_generator_mappings : 항상 true이길 권장 (레거시 하이버네이트를 쓰는 경우 X)
hibernate.hbm2ddl.auto : create, create-drop, update, validate(다르면 경고, 실행 X)
도메인 모델과 테이블 설계
[도메인 모델 그림]
회원, 주문, 상품의 관계 : 회원 - 주문 (일대다), 주문 - 상품 (다대다)
다대다 관계는 항상 중간에 매핑테이블을 통해 일대다, 다대일 관계로 풀어야 됨
상품 분류 : 도서, 음반, 영화가 상품이라는 공통 속성을 사용함으로 상속 받는 구조
[테이블 설계 그림]
회원 : Address가 임베디드 타입임으로 테이블에 그대로 들어감 (배송도 동일)
아이템 : 자식구조인 도서, 음반, 영화의 엔티티를 다 포함, 타입 구분을 위한 DTYPE이라는 컬럼 추가
연관관계 : 양방향인 경우 연관관계의 주인을 정해야됨, 외래키를 갖고 있는 테이블이 연관관계의 주인이 됨
1. 회원 - 주문 : 주문이 연관관계 주인
2. 주문상품 - 주문 : 주문상품이 연관관계 주인
3. 주문상품 - 상품 : 주문상품이 연관관계 주인
4. 주문 - 배송 : 주문이 연관관계 주인
5. 카테고리 - 상품 : @ManyToMany로 세팅 (실무에선 사용 X, 일부로 설정)
실무에서 다대다 매핑을 안쓰는 이유는 중간 매핑 테이블에 보통 매핑용 정보 말고도 다른 정보들을 제공할 수 있음으로
다대다로 매핑할 경우 그 추가 정보들을 사용할 수 없게 제한하는 것임으로 실무에서 사용하지 않는다.
연관관계의 주인 : 테이블은 외래키 하나로 두개의 테이블의 연관관계를 관리하지만, 엔티티는 단방향으로만 테이블을 참조해서 사용할 수 있다. 즉, 양방향인 경우 서로 단방향으로 참조하는 상태이다. (2개의 방향 화살표 존재)
그러므로 2개의 연관관계 중 하나를 선택하여 외래키 역할을 부여하는걸 연관관계의 주인이라고 할 수 있다.
(연관관계 주인은 외래키 등록, 수정, 삭제 가능 주인이 아닌 쪽은 오직 읽기만 가능)
[연관관계 주인인 객체의 도메인 소스]
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member; //주문 회원
[주인이 아닌 객체의 도메인 소스]
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<Order>();
> 추가적으로 cascade 속성을 세팅할 수 있음 (= 연관관계가 있는 엔티티의 변경을 함께 적용하고 싶은 경우 설정)
ALL - 모두 적용 (persist + remove), PERSIST - 영속, REMOVE - 삭제 등등
연관관계가 있는 컬럼에 Cascade를 안할 경우 부모 클래스는 정상적으로 데이터 변경이 발생하지만, 자식 클래스에서 외래키 무결성 예러가 발생할 수 있음 그러므로 실무에서 최대한 ALL로 설정하길 권장
> fetch 설정 가능 : 연관관계가 있는 엔티티에 대한 조회가 발생할때 참조 객체에 대한 조회가 함께 진행할지는 선택 가능
@ManyToOne, @OneToOne은 기본값이 EAGER임으로 반드시 LAZY로 설정해줘야 됨 (성능적인 이슈)
즉시로딩 : 바로 바로 함께 조회 (fetch = FetchType.EAGER)
지연로딩 : 정말 실제로 그 객체를 필요한 경우에만 조회 (fetch = FetchType.LAZY)
즉시로딩 vs 지연로딩
참고 링크 : https://ict-nroo.tistory.com/132
개발 방법
[계층 의존관계 그림]
컨트롤러 : MVC의 컨트롤러가 모여있는 곳
서비스 : 비즈니스 로직과 트랜젝션 시작점
레포지토리 : JPA 직접 사용하는 곳 엔티티 매니저를 사용해서 엔티티를 저장 및 조회
도메인 : 엔티티가 모여있는 곳
@Repository 어노테이션을 붙여 JPA 예외가 발생하더라도 스프링 추상 예외인 org.springframwork.dao.EmptyResultDataAccessException으로 변환하도록 설정!!
그래야 서비스 계층에서 JPA 예외를 따로 처리하지 않아도 됨.
@PersistenceContext 어노테이션으로 컨테이너가 관리하는 엔티티 매니저를 주입해줘야 됨
(엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성하지 않고 어노테이션으로 주입)
만약, 직접 엔티티 매니터 팩토리를 사용해야 될 경우 @PersistenceUnit 사용
JPQL과 QueryDsl를 사용해서 상세 쿼리를 작성할 수 있음
public List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
@Transactional 어노테이션을 붙여 클래스나 메소드에 트랜젝션 적용, RuntimeException, 언체크 예외가 발생하면 롤백됨.
회원 서비스 클래스내
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
위와 같은 검증 로직이 존재하더라도 멀티 쓰레드 환경을 고려해서 회원명 컬럼에 유니크 제약을 추가할 수 있음 (안정성 up)
테스트 코드 작성 예시
테스트 코드 작성 구조는 given(테스트할 상황 설정), when(테스트 대상 실행), then(테스트 결과 검증)으로 작성한다.
@Test
public void 상품주문() throws Exception {
//Given
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10); //이름, 가격, 재고
int orderCount = 2;
//When
Long orderId = orderService.order(member.getId(), item.getId(), orderCount);
//Then
Order getOrder = orderRepository.findOne(orderId);
assertEquals("상품 주문시 상태는 주문(ORDER)이다.", OrderStatus.ORDER, getOrder.getStatus());
assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
assertEquals("주문 가격은 가격 * 수량이다.", 10000 * 2, getOrder.getTotalPrice());
assertEquals("주문 수량만큼 재고가 줄어야 한다.", 8, item.getStockQuantity());
}
일반적인 상품 주문에 대한 테스트 코드를 작성할 수 있음
@Test(expected = NotEnoughStockException.class)
public void 상품주문_재고수량초과() throws Exception {
//Given
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10); //이름, 가격, 재고
int orderCount = 11; //재고 보다 많은 수량
//When
orderService.order(member.getId(), item.getId(), orderCount);
//Then
fail("재고 수량 부족 예외가 발생해야 한다.");
}
위와 같이 특정 예외에 대한 테스트 코드를 작성할 수 있음
(fail이 호출되거나 정의한 NotEnoughStockException 이 발생 안하면 테스트 실패)
만약 테스트에서 @Transactional 어노테이션을 작성하면 테스트가 끝나면 트랜젝션을 강제로 롤백한다.
주문 도메인 클래스 내
@Enumerated(EnumType.STRING)
private OrderStatus status;//주문상태
@Enumerated JPA내 enum 형태를 어떻게 정의할 것인지를 설정하는 어노테이션임 (기본값이 int형임으로 String 설정하길 권장)
EnumType.ORDINAL : enum 순서 값을 DB에 저장
EnumType.STRING : enum 이름을 DB에 저장
/** 주문 취소 */
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {
throw new RuntimeException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
위와 같이 엔티티내에 필요한 비지니스 로직을 구현함으로써 도메인 모델 패턴을 적용할 수 있다. (서비스는 단순히 엔티티에 요청을 위임)
도메인 모델 패턴 : 비즈니스 로직 대부분을 엔티티 도메인 클래스에 구현, 객체지향의 특성을 적극 활용한 패턴
트랜젝션 스크립트 패턴 : 엔티티 도메인에 비즈니스 로직 거의 없고, 서비스 계층에서 비즈니스 로직을 처리하는 패턴
주문 검색 클래스
package jpabook.jpashop.domain;
/**
* Created by holyeye on 2014. 3. 15..
*/
public class OrderSearch {
private String memberName; //회원 이름
private OrderStatus orderStatus;//주문 상태
//Getter, Setter
public String getMemberName() {
return memberName;
}
public void setMemberName(String memberName) {
this.memberName = memberName;
}
public OrderStatus getOrderStatus() {
return orderStatus;
}
public void setOrderStatus(OrderStatus orderStatus) {
this.orderStatus = orderStatus;
}
}
OrderSearch 객체(getter, setter만 존재)를 통해 검색 조건을 전달, 주문 리포지토리 클래스 내 findAll() 메소드를 통해 주문상품을 검색할 수 있음.
public List<Order> findAll(OrderSearch orderSearch) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> o = cq.from(Order.class);
List<Predicate> criteria = new ArrayList<Predicate>();
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
criteria.add(status);
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인
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); //최대 검색 1000 건으로 제한
return query.getResultList();
}
Criteria VS QueryDSL VS JPQL
criteria랑 queryDsl은 JPQL를 만들어주는 빌더 역할일 뿐 핵심 문법은 JPQL임
criteria랑 queryDsl은 동적 쿼리를 쉽게 작성할 수 있게 도와주지만, criteria는 문법이 복잡하여 queryDsl을 사용하길 권장함
JPQL의 벌크 연산을 통해 대용량 데이터 처리를 할 수 있음
JPA에서도 네이티브 SQL을 통해 직접 쿼리를 작성할 수 있지만 특정 데이터베이스에 종속됨으로 데이터베이스를 변경하기 쉽지 않음으로 최대한 JPQL를 사용하길 권장함
변경 감지 기능 사용
@Transctional
void update (Item item){
Item findItem = em.find(Item.class, item.getId());
findItem.setPrice(item.getPrice());
}
준영속 엔티티의 값으로 영속 상태의 엔티티를 얻은 다음 원하는 값을 수정하면 트랜젝션 커밋때 변경감지 기능을 통해 Update 됨
병합사용
@Transactional
void update(Item item){
Item mergeItem = em.merge(item)
}
변경 감지와 비슷하게 동작하지만 영속 엔티티 값을 준영속 엔티티의 값으로 모두 채워져서 Update 됨
즉 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경 가능하지만, 병합은 모든 속성이 변경된다.
'IT > Spring JPA' 카테고리의 다른 글
JPA 성능 최적화 (0) | 2021.08.13 |
---|---|
JPA 예외처리 및 프록시 심화 익히기 (0) | 2021.08.01 |
JPA 영속성 관리 (0) | 2021.07.15 |
QueryDSL 사용법 (3) | 2020.02.17 |
생성자 제한하기 (0) | 2020.02.16 |