Spring/Spring Data JPA

[자바 ORM 표준 JPA 프로그래밍] 12. 스프링 데이터 JPA

noahkim_ 2025. 4. 25. 20:29

김영한 님의 "자바 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가 자동으로 만들어주고 빈으로 등록함

 

구현체 생성 흐름
  1. Spring Data JPA가 해당 인터페이스에 대한 프록시 객체를 생성
  2. 이 프록시 객체는 동작을 SimpleJpaRepository에 위임함
  3. 프록시 객체는 빈으로 자동 등록됨

 

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);
}