김영한 님의 "자바 ORM 표준 JPA 프로그래밍" 책을 정리한 포스팅 입니다.
0. Spring Data 프로젝트
- 모든 종류의 데이터 저장소에 대해 추상화 계층을 제공하는 Spring 프로젝트 모음 (RDB, NoSQL, 검색엔진 등)
- 일관된 방식의 데이터 접근을 제공하여 코드 구조를 일관되게 유지할 수 있도록 도와줌
- 인터페이스만 작성해도 DAO 구현체를 자동 생성 및 등록함
공통 구조
- Repository 인터페이스 정의
- Spring Data가 자동으로 구현체(프록시) 생성
- 복잡한 쿼리는 @Query 또는 명시적 정의로 구현
1. Spring Data JPA 프로젝트
- JPA를 기반으로 RDBMS에 접근하는 기능을 제공하는 Spring Data의 하위 프로젝트
- Hibernate와 같은 JPA 구현체 위에 얹어서 사용함
JpaRepository
- 별도의 구현 클래스 없이 인터페이스 상속만으로 데이터 접근 계층을 구성할 수 있도록 도와줌
- 내부적으로는 JPA(EntityManager)를 사용하지만, 구현체는 Spring Data JPA가 자동으로 만들어주고 빈으로 등록함
구현체 생성 흐름
- Spring Data JPA가 해당 인터페이스에 대한 프록시 객체를 생성
- 이 프록시 객체는 동작을 SimpleJpaRepository에 위임함
- 프록시 객체는 빈으로 자동 등록됨
SimpleJpaRepository
- Spring Data JPA의 기본 구현체로, JpaRepository 인터페이스의 대부분의 기능을 구현한 클래스
- 데이터베이스에 접근하는 주요 로직을 담당하며, 기본적인 CRUD 및 기타 다양한 기능들을 제공
기능 | 설명 |
기본 CRUD |
기본적으로 제공하는 CRUD 메서드
- save(), findById(), findAll(), deleteById() 등 |
페이징 |
Pageable 파라미터로 페이징된 결과 조회
- Page<T> findAll(Pageable pageable) |
정렬 |
Sort 객체로 정렬
- List<T> findAll(Sort sort) |
메서드명 기반 쿼리 |
정해진 규칙에 따라 메소드 이름을 지으면 자동으로 쿼리를 생성해줌
- findByUsername, findByEmailAndStatus 등 |
Named Query |
엔티티 클래스에 미리 정의된 쿼리 사용
- @NamedQuery |
JPQL |
직접 작성한 JPQL 실행
- @Query("SELECT m FROM Member m WHERE m.name = :name") |
Native Query |
직접 작성한 SQL 실행
- @Query(value = "SELECT * FROM member WHERE ...", nativeQuery = true) |
파라미터 바인딩 |
@Param으로 JPQL/NativeQuery에 값 바인딩
|
벌크 연산 |
벌크 업데이트
- @Modifying + @Query 조합 |
Projection |
특정 필드만 추출하는 DTO 매핑
|
Specification | 조건 조합이 많은 경우, 동적 쿼리를 객체로 표현 |
예제) 기본 CRUD
더보기
@Test
@Transactional
void crud() {
User newUser = new User("test");
userRepository.save(newUser); // 저장
Optional<User> searched = userRepository.findById(newUser.getId()); // 조회
assertTrue(searched.isPresent());
userRepository.delete(newUser);
assertTrue(userRepository.findById(newUser.getId()).isEmpty());
List<User> users = userRepository.findAll();
assertTrue(!users.isEmpty());
}
예제) 페이징
더보기
@Test
@Transactional
void paging() {
Page<User> page = userRepository.findAll(PageRequest.of(0, 10));
// 페이지 내 데이터 출력
System.out.println("페이지 내 데이터: " + page.getContent());
// 페이지 번호 출력 (0-based)
System.out.println("현재 페이지 번호: " + page.getNumber());
// 페이지 크기 출력
System.out.println("페이지 크기: " + page.getSize());
// 전체 데이터 개수 출력
System.out.println("전체 데이터 개수: " + page.getTotalElements());
// 전체 페이지 수 출력
System.out.println("전체 페이지 수: " + page.getTotalPages());
// 페이지가 첫 페이지인지 확인
System.out.println("첫 페이지 여부: " + page.isFirst());
// 페이지가 마지막 페이지인지 확인
System.out.println("마지막 페이지 여부: " + page.isLast());
}
예제) 정렬
더보기
@Test
@Transactional
void sort() {
List<User> list = userRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
for (User user : list) {
System.out.println(user);
}
}
예제) 메서드명 기반 쿼리
더보기
@Test
@Transactional
void name_based_method() {
User user = userRepository.findByName("Stephen Curry");
assertEquals(user.getName(), "Stephen Curry");
List<User> list = userRepository.findByAge(37);
assertTrue(!list.isEmpty());
}
예제) Named Query
더보기
@NamedQuery(
name = "User.findByTeamId",
query = "SELECT u FROM User u WHERE u.team.id = :team_id"
)
@Entity
public class User { ... }
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Query(name = "User.findByTeamId")
List<User> findByTeamId(@Param("team_id") int teamId);
}
@Test
@Transactional
void named_query() {
List<User> list = userRepository.findByTeamId(1);
System.out.println(list);
}
예제) JPQL
더보기
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Query("SELECT u FROM User u WHERE u.nickname = :nickname")
Optional<User> findByNickname(@Param("nickname") String nickname);
}
@Test
@Transactional
void jpql() {
Optional<User> user = userRepository.findByNickname("chef");
assertTrue(user.isPresent());
System.out.println(user.get());
}
예제) Native Query
더보기
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Query(value = "SELECT auto_increment FROM information_schema.tables WHERE table_name = 'user' and table_schema = database()", nativeQuery = true)
Long getNextId();
}
@Test
@Transactional
void native_sql() {
Long nextId = userRepository.getNextId();
System.out.println(nextId);
}
예제) 벌크 연산
더보기
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Modifying
@Query("UPDATE User u SET u.age = u.age + 1")
int bulkAgePlus();
}
@Test
@Transactional
void bulk_op() {
int updatedCnt = userRepository.bulkAgePlus();
System.out.println(updatedCnt);
}
예제) Projection
더보기
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
List<UserSummary> findByAgeGreaterThan(int age);
}
public interface UserSummary {
String getName();
int getAge();
}
@Test
@Transactional
void projection() {
List<UserSummary> list = userRepository.findByAgeGreaterThan(25);
for (UserSummary userSummary : list) {
System.out.println("[" + userSummary.getAge() + "] " + userSummary.getName());
}
}
예제) Specification
더보기
@Repository
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {
@Test
@Transactional
void specification() {
Specification<User> hasName = hasName("Stephen Curry");
Specification<User> hasAgeGreaterThan = hasAgeGreaterThan(35);
Specification<User> spec = Specification.where(hasName).and(hasAgeGreaterThan);
List<User> all = userRepository.findAll(spec);
for (User user : all) {
System.out.println(user);
}
}
private static Specification<User> hasName(String name) {
return (root, query, cb) -> cb.equal(root.get("name"), name);
}
private static Specification<User> hasAgeGreaterThan(int age) {
return (root, query, cb) -> cb.greaterThan(root.get("age"), age);
}
Web 확장
기능 | 설명 |
도메인 클래스 컨버터 | HTTP 파라미터로 넘어온 엔티티 아이디로 엔티티 객체를 찾아 바인딩하는 기능. |
페이징 + 정렬 | Pageable을 이용해 페이징 처리를 지원. Sort를 이용해 정렬 처리를 지원. |
- 접두사 (Prefix) | 여러 페이징 정보를 구분하기 위해 접두사를 사용하여 구분. @Qualifier 어노테이션을 이용하여 각 페이징 정보를 구분. |
- 기본값 설정 | @PageableDefault 어노테이션을 사용해 Pageable의 기본값을 설정할 수 있음. |
예제) 도메인 클래스 컨버터
더보기
// HTTP 파라미터로 넘어온 엔티티 아이디를 바인딩하여 사용
@RequestMapping(value = "/member/{id}", method = RequestMethod.GET)
public String getMember(@PathVariable Long id) {
Member member = memberService.findMemberById(id);
return "memberDetail";
}
예제) 페이징 + 정렬
더보기
/test/paging?page=0&size=10&sort=name,desc&sort=age,desc
@GetMapping("/test/paging")
public String list(Pageable pageable) {
Page<User> page = userRepository.findAll(pageable);
return "hello spring";
}
접두사
https://localhost:8443/test/paging-prefix?
user_page=0&user_size=5&user_sort=name,asc&team_page=0&
team_size=10&team_sort=name,desc
@GetMapping("/test/paging-prefix")
public String list2(
@Qualifier("user") Pageable userPageable,
@Qualifier("team") Pageable teamPageable) {
Page<User> userPage = userRepository.findAll(userPageable);
Page<Team> teamPage = teamRepository.findAll(teamPageable);
// 페이지 내 데이터 출력
System.out.println("페이지 내 데이터: " + userPage.getContent());
System.out.println("페이지 내 데이터: " + teamPage.getContent());
return "hello spring";
}
기본값 설정
https://localhost:8443/test/paging-default?page=0
@GetMapping("/test/paging-default")
public String list3(@PageableDefault(size = 5, sort = "age", direction = Sort.Direction.DESC) Pageable pageable) {
Page<User> page = userRepository.findAll(pageable);
// 페이지 내 데이터 출력
System.out.println("페이지 내 데이터: " + page.getContent());
return "hello spring";
}
QueryDSL 통합
구분 | 설명 | 한계 및 특징 |
QueryDslPredicateExecutor | QueryDSL을 간단하게 사용하기 위한 인터페이스 제공 |
기능 제약 있음 (join, fetch join 불가 등)
|
QueryDslRepositorySupport | JPAQuery를 직접 사용할 수 있도록 지원 QueryDSL의 모든 기능을 활용 가능 |
번거로움 (클래스 상속 방식 / 직접 구현)
유연한 쿼리 작성 가능 |
예제) QueryDslPredicateExecutor
더보기
@Repository
public interface UserRepository extends JpaRepository<User, Integer>, QuerydslPredicateExecutor<User> { ... }
@Test
@Transactional
void findAll() {
Iterable<User> result = userRepository.findAll(
user.name.contains("S")
.and(user.nickname.eq("chef"))
);
for (User row : result) {
System.out.println(row);
}
}
예제) QueryDslRepositorySupport
더보기
@Getter
@Setter
public class TeamSearch {
private int id;
private String name;
private List<User> members = new ArrayList<>();
public TeamSearch(int id, String name, List<User> members) {
this.id = id;
this.name = name;
this.members = members;
}
}
public interface TeamRepositoryCustom {
List<Team> find(TeamSearch userSearch);
}
public class TeamRepositoryImpl extends QuerydslRepositorySupport implements TeamRepositoryCustom {
public TeamRepositoryImpl() {
super(Team.class);
}
@Override
public List<Team> find(TeamSearch condition) {
QTeam team = QTeam.team;
QUser user = QUser.user;
JPQLQuery<Team> query = from(team);
if (StringUtils.hasText(condition.getName())) query.where(team.name.contains(condition.getName()));
if (condition.getId() > 0) query.where(team.id.eq(condition.getId()));
if (!CollectionUtils.isEmpty(condition.getMembers())) {
List<Integer> userIds = condition.getMembers().stream().map(User::getId).collect(Collectors.toList());
query.leftJoin(team.members, user).where(user.id.in(userIds));
}
return query.fetch();
}
}
@Test
@Transactional
void find() {
List<Team> list = teamRepository.find(new TeamSearch(1, "gsw", List.of(new User(1), new User(2))));
System.out.println(list);
}
'Spring > Spring Data JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 15. 고급 주제와 성능 최적화 (1) | 2025.04.26 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 13. 웹 애플리케이션과 영속성 관리 (1) | 2025.04.26 |
[자바 ORM 표준 JPA 프로그래밍] 10-5. 객체지향 쿼리 언어: 객체지향 쿼리 심화 (0) | 2025.04.24 |
[자바 ORM 표준 JPA 프로그래밍] 10-4. 객체지향 쿼리 언어: Native SQL (0) | 2025.04.24 |
[자바 ORM 표준 JPA 프로그래밍] 10-2. 객체지향 쿼리 언어: Criteria (0) | 2025.04.24 |