공통 인터페이스
org.springframework.data.repository.Repository를 구현한 클래스는 스캔 대상이다.
MemberRepository 인터페이스가 동작한 이유
1. JpaRepository를 상속받은 인터페이스는 구현체가 없음
2. Spring Data JPA가 런타임에 구현체(SimpleJpaRepository)를 만듬
3. 그 구현체를 프록시로 감싸서 스프링 빈으로 등록
* 프록시 객체가 메서드 호출을 가로채서 어떤 전략을 쓸지 결정한 뒤, 내부 구현체(SimpleJpaRepository)가 EntityManager로 JPQL, SQL을 실행하게 한다. 그리고 프록시는 트랜잭션/예외변환/Auditing같은 부가기능도 함
이해 안된부분 정리
구현체를 프록시로 감싼다는게 무슨말인가?
- 진짜 객체를 직접 쓰는게 아니라, 그 앞에 대리 객체를 세워두는 것
프록시가 없다면? -> 호출이 바로 구현체로 가는 구조
프록시가 있다면? -> 프록시 객체를 통해서 구현체로 가는 구조
그렇다면 프록시 객체의 내부 로직은 어떻게 되어있나?
프록시가 메서드 호출을 가로채서 하는일 (예: findByEmailAndPlatform)
1. 메서드 분석
- 쿼리 메서드인가? (쿼리 메서드임)
- @Query가 붙어있나?, 커스텀 구현체가 있나? 단순 CRUD인가? 등등 어떤 전략을 쓸지 결정
2. 트랜잭션 확인
- 이미 트랜잭션이 있는지? 없으면 시작해야 하는지? readOnly인지?
- @Transactional이 여기서 작동
3. 실제 구현체 호출
4. 내부에서 EntityManager 사용 (em.createquery, persist, find ....)
5. 예외 발생시
- 예외 변환을 해야하는가?
(JPA/Hibernate 예외시 PersistenceException, JDBC 예외시 SQLException)
이게 PersistenceExceptionTranslationInterceptor임
즉 프록시가 메서드 이름과 파라미터를 보고 이 요청을 어떤 JPQL, SQL로 변환할지 판단 한 뒤 실제 구현체에게 위임.
왜 프록시를 쓰는건가?
안쓰면?
- 직접 구현체 작성 해야함.
- 트랜잭션 관리, 예외 변환 직접 처리, 메서드 이름 파싱 등 개발자가 할일이 너무 많아짐
쓰면?
- 인터페이스만 선언하면 됨
- 공통기능을 한곳에서 처리
- AOP 확장 가능
* 비즈니스 코드에 관심사를 섞지 않기 위해서
쿼리 메소드
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 repository interface에 쿼리 직접 정의
쿼리 메소드 필터 조건, 쿼리 메소드 기능은 검색해볼것
NamedQuery
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
// 스프링 데이터 JPA로 Named쿼리 호출
public interface MemberRepository
extends JpaRepository<Member, Long> { //** 여기 선언한 Member 도메인 클래스
List<Member> findByUsername(@Param("username") String username);
}
- 선언한 도메인 클래스 + . + 메서드 이름으로 named query를 찾아서 실행
- 만약 없으면 메서드 이름으로 쿼리 생성 전략 사용
(쓸일 잘 없음. @Query 사용하면 된다.)
@Query, 리포지토리 메소드에 쿼리 정의하기
메서드에 JPQL 쿼리 작성
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
- 실행할 메서드에 정적 쿼리를 직접 작성함 (이름없는 named query..)
- Named Query처럼 애플리케이션 실행시점에서 문법 오류를 발견할 수 있다.
- 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 지저분해진다. @Query를 자주 쓰게 될것이다..
@Query 값, DTO 조회
JPA 값 타입 (@Embedded)도 이 방식으로 조회할 수 있다.
// 단순한 단건조회
@Query("select m.username from Member m")
List<String> findUsernameList();
// DTO 직접 조회
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();
- DTO 직접 조회시 new 명령어 사용해야함. 그리고 생성자가 맞는 dto가 필요하다.
파라미터 바인딩
위치 기반과 이름 기반이 있는데, 위치 기반은 그냥 쓰지말아라 (실수할 확률 증가함)
import org.springframework.data.repository.query.Param
// 이름 기반 파라미터 바인딩
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
}
컬렉션 파라미터 바인딩
Collection 타입으로 in절 지원
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
반환 타입
유연한 반환 타입 지원
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
조회 결과가 많거나 없으면?
컬렉션 결과 없음 -> 빈 컬렉션 반환
단건 조회 결과 없음 -> null 반환 (원래는 NoResult가 떠야하지만 스프링 데이터 JPA는 무시하고 null 반환함)
단건조회 2건 이상 -> NonUniqueResultException 발생
스프링 데이터 JPA 페이징과 정렬
- org.springframework.data.domain.Sort : 정렬
- org.springframework.data.domain.Pageable : 페이징 (내부에 Sort 있음)
- org.....Page : 추가 count 쿼리 결과를 포함하는 페이징
- org.....Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit +1 조회)
- List : 추가 count 쿼리 없이 결과만 반환
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용안함
List<Member> findByUsername(String name, Sort sort);
예제코드
- 검색 조건 : 나이 10살
- 정렬 조건 : 이름으로 내림차순
- 페이징 조건 : 첫 번째 페이지, 페이지당 3건 노출
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
두번째 파라미터로 받은 Pageable은 인터페이스다. 실제 사용할때는 실제 해당 인터페이스를 구현한 PageRequest 객체를 사용함
//given 생략
//when
PageRequest pageRequest = PageRequest.of(0, 3,Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//then 생략
pageRequest 생성자의 첫번째는 현재 페이지, 두번째는 조회할 데이터 수 입력.
* page는 0부터 시작한다.
count 쿼리를 분리할 수 있다.
@Query(value = "select m from Member m",
countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
Top, First 사용은 따로 검색해볼것 ( List<Member> findTop3By(); 하면 상위 3건만 조회됨)
페이지를 유지하면서 엔티티 -> DTO 변환
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
- 카운트 쿼리 분리는 복잡한 sql에서 사용하면 됨. 데이터는 left join, 카운트는 left join 안해도 됨
벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- 벌크성 수정, 삭제는 @Modifying 어노테이션 사용해야함 (안하면 예외 발생)
- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화 (@Modifying(clearAutomatically = true)해줘야함.
안해주면 영속성 컨텍스트에 과거 값이 남아서 문제될 수 있다. 다시 조회해야한다면 초기화 할 것
영속성 컨텍스트를 무시하고 실행하기 때문에 영속성 컨텍스트의 엔티티 상태와 DB 엔티티 상대가 달라질 수 있으니
잘 맞춰야함 (영속성 컨텍스트에 엔티티가 없는 상태에서 먼저 벌크 연산 하던지 연산 후 영속성 컨텍스트 초기화 하던지)
EntityGraph
- fetch join의 간편버전
- left outer join 사용
사용 방법은 검색해서 찾아보고, 그다지 쓸모 있어 보이지 않아서 생략...... 명확하게 fetch join 쿼리 작성하는게 좋아보임
JPA Hint
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value =
"true"))
Member findReadOnlyByUsername(String username);
@Test
public void queryHint() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
//when
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //Update Query 실행X
}
- 변경감지 X (스냅샷 안만듬. readOnly)
- 메모리 사용량 감소, 성능 증가
- 조회 전용 API에서 유용하게 사용한다.
사용자 정의 리포지토리 구현
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성한다고 했다.
스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
어떠한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면..
- JPA 직접 사용 (em)
- JDBC template
- MyBatis
- QueryDSL 등등..
// 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
// 사용자 정의 인터페이스 구현 클래스
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
// custom interface 상속
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
규칙 : 리포지토리 인터페이스 이름 + Impl
- 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록한다.
최신 리포지토리 구현 방식
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
사용자 정의 인터페이스명 + Impl도 지원한다. 더욱 직관적이게 바뀌었기 때문에 이 방식을 사용하는것을 권장한다고 함
Auditing
엔티티 생성, 변경시 변경한 사람과 시간을 추적하고 싶을 때 사용
- 등록일, 수정일, 등록자, 수정자
package jpabook.jpashop.domain;
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
- @EntityLIsteners(AuditingEntityListener.class) 을 엔티티에 적용시켜야함
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
}
@EnableJpaAuditing 추가
등록자, 수정자를 처리해주는 AUditorAware 스프링 빈 등록을 해야한다.
(실무에서는 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 받음)
도메인 클래스 컨버터
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
}
도메인 클래스 컨버터 사용 전
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
}
사용 후
- HTTP 요청은 id를 받지만, 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환
- 도메인 클래스 컨버터도 리포지토리를 사용해서 엔티티를 찾는다.
* 조회용으로만 사용해야 한다. 트랜잭션 없는 범위에서 사용했으므로 엔티티 변경 시 DB반영 안됨)
web 확장 - 페이징과 정렬
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
- 파라미터로 Page를 받을 수 있다.
- Pageable은 인터페이스 (위에서 언급했음), 실제로 PageRequest 객체를 생성
예) /members?page=0&size=3&sort=id,desc&sort=username,desc
- 현재 페이지, 0부터 시작
- size : 노출할 데이터 건수
- sort : 정렬 조건
접두사
- 페이징 정보가 둘 이상이면 접두사로 구분
- @Qualifier에 접두사 명 추가
예: /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
Page 내용 DTO 변환
엔티티를 API로 노출하면 안된다. 그래서 꼭 DTO로 변환해서 반환해야한다.
Page는 map을 지원해서 내부 데이터를 다른것으로 변경할 수 있다.
```java
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> pageDto = page.map(MemberDto::new);
return pageDto;
}
// 코드 최적화
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}
Page를 1부터 시작하려면?
- Pageable, Page를 파라미터와 응답값으로 사용하지않고 직접 구현해야한다. 응답값도 직접 구현해야한다.
필요할때 검색해서 써보자
'JPA' 카테고리의 다른 글
| 스프링 부트와 JPA 활용 (1) | 2026.01.26 |
|---|---|
| JPA의 값 타입 (0) | 2025.12.27 |
| 프록시와 연관관계 관리 (0) | 2025.12.26 |
| 고급 매핑 - 상속 관계, Mapped Superclass (0) | 2025.12.25 |
| 엔티티 매핑, 연관관계 (0) | 2025.12.12 |