Web 확장
- 스프링 데이터 프로젝트는 스프링 MVC에서 사용할 수 있는 편리한 기능을 제공
- 설정
스프링 데이터가 제공하는 Web 확장 기능을 활성화하려면 SpringDataWebConfiguration을 스프링 빈으로 등록하면 됨
또는 JavaConfig를 사용하면 EnableSpringDataWebSupport 어노테이션을 사용
설정을 완료하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 HandlerMethodArgumentResolver가 스프링 빈으로 등록됨
// 스프링 빈 등록
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
// JavaConfig 사용
// org.springframework.data.web.config.EnableSpringDataWebSupport 어노테이션 사용
@Configuratrion
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
...
}
// 등록되는 도메인 클래스 컨버터
org.springframework.data.repository.support.DomainClassConverter
- 도메인 클래스 컨버터 기능
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해줌
예) 특정 회원을 수정하는 화면을 보여주려면
도메인 클래스 컨버터 사용 전)
컨트롤러는 HTTP 요청으로 넘어온 회원의 아이디를 사용해서 리포지토리를 통해 회원 엔티티를 조회해야 함
도메인 클래스 컨버터 사용 후)
HTTP 요청으로 넘어온 회원 아이디를 받은 후 도메인 클래스 컨버터가 중간에서 동작해서
아이디를 회원 엔티티 객체로 반환해서 넘겨주기 때문에 컨트롤러를 단순하게 사용
참고로 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾음
// 회원의 아이디로 회원 엔티티 조회
@Controller
public class MemberController {
@Autowired
MemberRepository memberRepository;
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Long id, Model mode) {
// 파라미터로 넘어온 회원 아이디로 회원 엔티티를 찾은 후
Member member = memberRepository.findOne(id); // 회원을 찾음
// 찾아온 회원 엔티티를 model을 사용해서 뷰에 넘겨줌
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
// 도메인 클래스 컨버터 적용
@Controller
public class MemberController {
@Autowired
MemberRepository memberRepository;
@RequestMapping("member/memberUpdateForm")
/* HTTP 요청으로 회원 아이디를 받지만 도메인 클래스 컨버터가 중간에 동작해서
회원 리포지토리를 통해 회원 아이디로 회원 엔티티를 찾아
아이디를 회원 엔티티 객체로 변화해서 넘겨줌
따라서 컨트롤러를 단순하게 사용할 수 있음 */
public String memberUpdateForm(@RequestParam("id") Member member, Model mode) {
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
- 페이징과 정렬 기능
스프링 데이터가 제공하는 페이징과 정렬 기능을
스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공
페이징 기능 : PageHandlerMethodArgumentResolver
정렬 기능 : SortHandlerMethodArgumentResolver
페이징은 파라미터로 Pageable을 받으며, Pageable은 요청 파라미터 정보로 만들어짐
요청 파라미터
page : 현재 페이지, 0부터 시작
size : 한 페이지에 노출할 데이터 건수
sort : 정렬 조건을 정의 예) 정렬 속성 (ASC | DESC), 정렬 방향 등
접두사
사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분
접두사는 스프링 프레임워크가 제공하는 @Qualifier 어노테이션을 사용하며 "{접두사명}_"로 구분
기본값
Pageable의 기본값은 page=0, size=20이며
만약 기본값을 변경하고 싶으면 @PageableDefault 어노테이션을 사용
// 페이징과 정렬 예제
@RequestMapping(value = "/members", method = RequestMethod.GET)
// 요청 파라미터 정보로 만들어진 Pageable을 파라미터로 받음
public String list(Pageable pageable, Model model) {
Page<Member> page = memberService.findMembers(pageable);
model.addAttribute("members", page.getContent());
return "members/memberList";
}
// 요청 파라미터
/members?page=0&size-20&sort=name,desc&sort=address.city
// 접두사
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
예 /members?member_page=0&order_page=1
// 기본값
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "name", direction = Sort.Direction.DESC) Pageable pageable) {
...
}
스프링 데이터 JPA가 사용하는 구현체
- 스프링 데이터 JPA가 제공하는 공통 인터페이스는 SimpleJpaRepository 클래스가 구현
- @Repository 적용
JPA 예외를 스프링이 추상화한 예외로 변환
@Transactional 트랜잭션 적용
JPA의 모든 변경은 트랜잭션 안에서 이루어져야 함
스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경하는 메소드에 @Transactional로 트랜잭션 처리
따라서 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작
물론 서비스 계층에서 트랜잭션을 시작했으면 리포지토리도 해당 트랜잭션을 전파받아서 그래도 사용
@Transactional(readOnly = true)
데이터를 조회하는 메소드에는 readOnly = true 옵션이 적용되어 있음
데이터를 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻음
save() 메소드
저장할 엔티티가 새로운 엔티티면 저장하고 이미 있는 엔티티면 병합
새로운 엔티티를 판단하는 기본 전략은 엔티티의 식별자로 판단하는데 식별자가 객체일 때 null,
자바 기본 타입일 때 숫자 0 값이면 새로운 엔티티로 판단
필요하면 엔티티에 Persistable 인터페이스를 구현해서 판단 로직은 변경할 수 있음
// SimpleJpaRepository
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>, JpaSpecifivationExecutor<T> {
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
}
...
}
// Persistable
public interface Persistable<Id extends Serializable {
ID getId();
boolean isNew();
}
JPA 샵에 적용
- 스프링 프레임워크와 JPA로 개발한 웹 애플리케이션에 스프링 데이터 JPA를 적용
- 환경설정
pom.xml에 spring-data-jpa 라이브러리를 추가한 후 appConfig.xml에 <jpa:repositories>를 추가하고
base-package 속성에 리포지토리 위치를 지정하여 스프링 데이터 JPA를 사용할 준비를 함
// pom.xml
// 스프링 데이터 JPA 라이브러리
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${spring-data-jpa.version}</version>
</dependency>
// appConfig.xml
// XML 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="jpabook.jpashop.repository" />
...
</beans>
- 리포지토리 리팩토링
기존 리포지토리들이 스프링 데이터 JPA를 사용하도록 리팩토링
1) 회원 리포지토리
2) 상품 리포지토리
3) 주문 리포지토리
순으로 리팩토링
// 회원 리포지토리
/* 클래스를 인터페이스로 변경하고 스프링 데이터 JPA가 제공하는 JpaRepository를 상속받음
제네릭 타입을 <Member, Long>으로 지정해서 리포지토리가 관리하는 엔티티 타입과 엔티티의 식별자 타입을 지정 */
public interface MemberRepository extends JpaRepository<Member, Long> {
/* save(), findOne(), findAll() 메소드와 같은 기본 메소드는 상속받은 JpaRepository가 모두 제공하므로 삭제
남겨진 메소드는 findByName()인데 스프링 데이터 JPA가 해당 메소드의 이름을 분석해서 적절한 쿼리를 실행 */
List<Member> findByName(String name);
}
// 상품 리포지토리
public interface ItemRepository extends JpaRepository<Item, Long> {
// 상품 리포지토리가 제공하는 모든 기능은 스프링 데이터 JPA가 제공하는 공통 인터페이스만으로 충분
}
// 주문 리포지토리
/* 주문 리포지토리에는 검색이라는 복잡한 로직이 있는데
스프링 데이터 JPA가 제공하는 명세 기능을 사용해서 검색을 구현하기 위해
코드에 JpaSpecificaionExecutor를 추가로 상속 받음 */
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order>, CustomOrderRepository {
}
- 명세 적용
명세로 검색하는 기능을 사용하기 위해 리포지토리에 JpaSpecificationExecutor를 추가로 상속 받은 후
명세를 작성하기 위한 클래스인 OrderSpec을 추가
그리고 검색조건을 가지고 있는 OrderSearch 객체에 자신이 가진 검색 조건으로 Specification을 생성하도록 추가
마지막으로 기존 코드인 OrderService에서 리포지토리의 검색 코드가 명세를 파라미터로 넘기도록 변경
// OrderSpec 추가
public class OrderSpec {
public static Specification<Order> memberNameLike(final String memberName) {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
if (StringUtils.isEmpty(memberName)) return null;
Join<Order, Member> m = root.join("member", JoinType.INNER); // 회원과 조인
return builder.like(m.<String>get("name"), "%" + memberName + "%");
}
};
}
public static Specification<Order> orderStatusEq(final OrderStatus orderStatus) {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
if (orderStatus == null) return null;
return builder.equal(root.get("status"), orderStatus);
}
};
}
}
// 검색 객체가 Specification 생성하도록 추가
public class OrderSearch {
private String memberName; // 회원 이름
private OrderStatus orderStatus; // 주문 상태
// Getter, Setter
public Specifications<Order> toSpecification() {
return where(memberNameLike(memberName))
.and(orderStatusEq(orderStatus));
}
}
// OrderService.findOrders 리팩토링
@Service
@Transactional
public class OrderService {
...
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch.toSpecification()); // Specification 사용
}
}
스프링 데이터 JPA와 QueryDSL 통합
- 스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원
- QueryDslPredicateExecutor 사용
리포지토리에서 QueryDslPredicateExecutor를 상속받아 QueryDSL을 사용
// QueryDSL 사용 예제
// 장난감이라는 이름을 포함하고 있으면서 가격이 10000~20000원인 상품을 QueryDSL이 생성한 쿼리 타입으로 검색
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll(
item.name.contains("장난감").and(item.price.between(10000, 20000))
);
// QueryDslPredicateExecutor 인터페이스
public interface QueryDslPredicateExecutor<T> {
T findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
long count(Predicate predicate);
}
- QueryDslRepositorySupport 사용
QueryDslPredicateExecutor는 스프링 데이터 JPA에서 편리하게 QueryDSL을 사용할 수 있지만 기능에 한계가 존재
예) join, fetch를 사용할 수 없음
따라서 QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나
스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용
스프링 데이터 JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기 때문에
CustomRepository라는 사용자 정의 리포지토리를 만든 후 QueryDslRepositorySupport를 사용
// CustomOrderRepository 사용자 정의 리포지토리
public interface CustomOrderRepository {
public List<Order> search(OrderSeach orderSearch);
}
// QueryDslRepositorySupport 사용 코드
// 주문 내역 검색 기능을 구현
// 검색 조건에 따라 동적으로 쿼리를 생성
public class OrderRepositoryImpl extends QueryDslRepositorySupport implements CustomOrderRepository {
public OrderRepositoryImpl() {
super(Order.class);
}
@Override
public List<Order> search(OrderSearch orderSearch) {
QOrder order = QOrder.order;
QMember member = QMember.member;
JPQLQuery query = from(order);
if (StringUtils.hasText(orderSearch.getMemberName())) {
query.leftJoin(order.member, member)
.where(member.name.contains(orderSearch.getMemberName()));
}
if (orderSearch.getOrderStatus() != null) {
query.where(order.status.eq(orderSearch.getOrderStatus()));
}
return query.list(order);
}
}
// QueryDslRepositorySupport 코드의 핵심 기능
@Repository
public abstract class QueryDslRepositorySupport {
// 엔티티 매니저 반환
protected EntityManager getEntityManager() {
return this.entityManager;
}
// from 절 반환
protected JPQLQuery from(EntityPath<?>... paths) {
return this.querydsl.createQuery(paths);
}
// QueryDSL delete 절 반환
protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
return new JPADeleteClause(this.entityManager, path);
}
// QueryDSL update 절 반환
protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
return new JPAUpdateClause(this.entityManager, path);
}
// 스프링 데이터 JPA가 제공하는 Querdsl을 편하게 사용하도록 돕는 핼퍼 객체 반환
protected Querydsl getQuerydsl() {
return this.querydsl;
}
}