명세
스프링 데이터 JPA는 책 도메인 주도 설계에 등장하는 명세라는 개념을 사용할 수 있도록 지원한다
-> JPA Criteria로 지원한다
명세(Specification)는 특정 객체가 주어진 조건을 충족하는지 여부를 판단하는 데 사용되는 패턴이다
명세는 참이나 거짓으로 평가되는 술어와 이를 조합하는 연산자로 구성된다
스프링 데이터 JPA에서 술어는 Specifictaion 클래스로 정의되어 있다
Specification은 컴포지트 패턴(composite pattern)으로 구정되어 있어 여러 Specification을 조합할 수 있다
다양한 검색 조건을 조립해 새로운 검색조건을 쉽게 만들 수 있다
명세 기능을 사용하려면 JpaSpecificationExecutor 인터페이스를 상속받으면 된다
public interface OrderRepository extends JPARepository<Order, Long>,
JpaSpecificationExecutor<Order> {
}
JpaSpecificationExecutor의 메소드들은 Specification을 파라미터로 받아서 검색 조건으로 사용한다
public class OrderSpec {
public static Specification<Order> memberName(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.equal(m.get("name"), memberName);
}
};
}
public static Specification<Order> isOrderStatus() {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root,
CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.equal(root.get("status"), OrderStatus.ORDER);
}
};
}
}
명세를 정의하는 코드
Specification 인터페이스를 구현해 명세를 정의한다
편의상 내부 무명 클래스를 사용했다
toPredicate(…) 메소드만 구현하면 되는데 Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어져 이를 활용해 적절한 검색 조건을 반환하면 된다
public List<Order> findOrders(String name) {
List<Order> result = orderRepository.findAll(
where(memberName(name)).and(isOrderStatus())
);
return result
}
명세를 사용하는 코드
Specifications는 명세들을 조립할 수 있도록 도와주는 클래스로 where(), and(), or(), not() 메소드를 제공한다
사용자 정의 리포지토리 구현
스프링 데이터 JPA로 리포지토리를 구현하는 경우 인터페이스만 정의하고 구현체는 만들지 않는다
하지만 메소드를 직접 구현해야 하는 경우가 발생한다
리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 하므로 우회해서 필요한 메소드만 구현할 수 있는 방법을 제공한다
사용자 정의 인터페이스를 작성해야 한다
public interface MomeberRepositoryCustom {
public List<Member> findMemberCustom();
}
사용자 정의 인터페이스를 구현한 클래스를 작성해야 한다
클래스의 이름은 리포지토리 인터페이스 이름 + Impl로 지으면, 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom() {
//사용자 정의 구현
}
}
리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
Web 확장
스프링 데이터 프로젝트가 제공하는 식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버터 기능과 페이징과 정렬 기능을 알아보자
설정
SpringDataWebConfiguration을 스프링 빈으로 등록하면 Web 확장 기능을 사용할 수 있다
@EnableSpringDataWebSupport 어노테이션으로 해당 빈을 등록할 수 있다
도메인 클래스 컨버터 기능
도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩해준다
@Controller
public class MemberController {
@Autowired MemberRepository memberRepository;
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Long id, Model model) {
Member member = memberRepository.findOne(id);
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
@Controller
public class MemberController {
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParan("id") Member member, Model model) {
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
HTTP 요청으로 회원 아이디를 받지만 도메인 클래스 컨버터가 중간에서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다
도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다
(회원 리포지토리를 통해서 회원 아이디로 회원 엔티티를 찾는다)
도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터베이스에는 반영되지 않는다
페이징과 정렬 기능
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공한다
- 페이징 기능: PageableHandlerMethodArgumentResolver
- 정렬 기능: SortHandlerMethodArgumentResolver
@RequestMapping(value="/members", method=RequestMethod.GET)
public String list(Pageable pageable, Model model) {
Page<Member> page = memberService.findMembers(pageable);
model.addAttribute("members", page.getContent());
return "members.memberList";
}
파라미터로 Pageable을 받는다
Pageable은 다음 요청 파라미터 정보로 만들어진다
- page: 현재 페이지, 0부터 시작
- size: 한 페이지에 노출할 데이터 건수
- sort: 정렬 조건을 정의한다 (정렬 속성, 정렬 방향)
사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다
@Qualifier 어노테이션을 사용한다
public String list (
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
)
/members?member_page=0&order_page=1 → 이런 형태로 요청을 보내게 된다
Pageable의 기본값은 page=0, size-20이다
기본값을 변경하고 싶으면 @PageableDefault를 사용하면 된다
@RequestMapping(value="/members_page", method=RequestMethod.GET)
public String list(@PageableDefault(size=12, sort="name",
direction=Sort.Direction.DESC) Pageable pageable) {
...
}
스프링 데이터 JPA가 사용하는 구현체
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
@Override
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
...
}
@Repository 적용
JPA 예외를 스프링이 추상화한 예외로 변환한다
@Transactional 트랜잭션 적용
JPA의 모든 변경은 트랜잭션 내에서 이루어져야 한다
스프링 데이터 JPA는 데이터를 변경(등록, 수정, 삭제)하는 메소드에 트랜잭션 처리를 해준다
서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작한다
만약 서비스 계층에서 트랜잭션을 시작했으면 리포지토리에서 해당 트랜잭션을 전파받아 그대로 사용한다
@Transactional(readOnly = true)
데이터를 조회하는 메소드에는 readOnly = true 옵션을 적용한다
플러시를 생략해 약간의 성능 향상을 얻을 수 있다
save() 메소드
저장할 엔티티가 새로운 엔티티면 저장(persist)하고 이미 있는 엔티티면 병합(merge)한다
엔티티의 식별자가 null이거나 0이면 새로운 엔티티로 판별한다
Persistable 인터페이스를 구현해 판단 로직을 변경할 수 있다
'스터디 > JPA' 카테고리의 다른 글
웹 애플리케이션과 영속성 관리 - (2) (0) | 2025.03.21 |
---|---|
웹 애플리케이션과 영속성 관리 - (1) (0) | 2025.03.20 |
스프링 데이터 JPA - (1) (0) | 2025.02.20 |
객체지향 쿼리 언어 - (7) (1) | 2025.02.19 |
객체지향 쿼리 언어 - (6) (0) | 2025.02.19 |