[LG U+ 유레카 3기] Spring Data JPA find() 메서드 실습

2025. 11. 26. 17:54Java/JPA

Spring Data JPA find() 메서드 실습 - Student 검색 패턴 정리


❶ 상황 설명

이번 오전 실습에서는 Spring Data JPA메서드 이름 규칙 기반 조회(find)를 집중적으로 연습했다.
특히 Student 엔티티를 대상으로 다양한 검색 패턴을 만들어 보는 것이 목표였다.

  • 단일 조건 조회: findByName()
  • 복합 조건 조회: findByEmailAndPhone(), findByEmailOrPhone()
  • 문자열 패턴 조회: StartingWith, EndingWith, Containing, Like
  • 정렬: findAllByOrderByNameDesc()
  • 범위 검색: findByIdBetween()

핵심은 “쿼리를 직접 안 써도, 메서드 이름만으로 조건과 정렬을 표현할 수 있다”를 몸으로 체득하는 것.


❷ Student 엔티티 – 조회 대상 도메인

실습에서 조회의 대상이 되는 엔티티는 Student 테이블과 매핑되는 클래스이다.


package com.mycom.myapp.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = "student")
@Getter
@Setter
@ToString
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(length = 200, nullable = false)
    private String email;

    @Column(length = 13, nullable = false)
    private String phone;

    @Column(length = 255, nullable = false)
    private String name;
}
  • @Entity, @Table : JPA가 관리하는 엔티티 + 테이블 이름 지정
  • @Id + @GeneratedValue(IDENTITY) : PK + AUTO_INCREMENT 전략
  • @Column : 길이와 null 여부 설정

이제 이 Student 엔티티를 기준으로, Spring Data JPA의 findBy... 메서드들을 다양하게 만들어본 것이 이번 실습의 내용이다.


❸ Repository – 메서드 이름만으로 쿼리 만들기

핵심은 Spring Data JPA 메서드 이름 규칙이다.
StudentRepository에서 메서드 이름만 보고도 어떤 SQL이 나갈지 유추할 수 있게 되는 것이 목표였다.


package com.mycom.myapp.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.mycom.myapp.entity.Student;

public interface StudentRepository extends JpaRepository<Student, Integer> {

    // 1) 단일 조건
    List<Student> findByName(String name);

    // 2) 복합 조건 (AND / OR)
    List<Student> findByEmailAndPhone(String email, String phone);
    List<Student> findByEmailOrPhone(String email, String phone);

    // 3) 문자열 패턴
    List<Student> findByNameStartingWith(String name);
    List<Student> findByEmailEndingWith(String email);
    List<Student> findByPhoneContaining(String phone);
    List<Student> findByNameLike(String name);

    // 4) 정렬
    List<Student> findAllByOrderByNameDesc();

    // 5) 범위 검색
    List<Student> findByIdBetween(int from, int to);
}

📌 메서드 이름 → SQL 매핑 요약

메서드 이름 의미 대략적인 JPQL / SQL
findByName 이름이 정확히 일치 where name = ?
findByEmailAndPhone 이메일과 전화번호 둘 다 일치 where email = ? and phone = ?
findByEmailOrPhone 이메일 또는 전화번호 중 하나라도 일치 where email = ? or phone = ?
findByNameStartingWith 이름이 특정 문자열로 시작 where name like '값%'
findByEmailEndingWith 이메일이 특정 문자열로 끝남 where email like '%값'
findByPhoneContaining 전화번호에 특정 문자열이 포함 where phone like '%값%'
findByNameLike 직접 패턴을 넣어서 like 검색 where name like ? (와일드카드는 개발자가 붙임)
findAllByOrderByNameDesc 전체를 name 내림차순으로 정렬 order by name desc
findByIdBetween id가 구간 안에 포함되는 데이터 where id between ? and ? (양끝 포함)

❹ Service & Controller – 레이어드 아키텍처 적용

① Service 인터페이스


package com.mycom.myapp.service;

import java.util.List;
import com.mycom.myapp.entity.Student;

public interface StudentServiceFind {

    List<Student> findByName(String name);
    List<Student> findByEmailAndPhone(String email, String phone);
    List<Student> findByEmailOrPhone(String email, String phone);

    List<Student> findByNameStartingWith(String name);
    List<Student> findByEmailEndingWith(String email);
    List<Student> findByPhoneContaining(String phone);
    List<Student> findByNameLike(String name);

    List<Student> findAllByOrderByNameDesc();
    List<Student> findByIdBetween(int from, int to);
}

② Service 구현체


package com.mycom.myapp.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.repository.StudentRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class StudentServiceFindImpl implements StudentServiceFind {

    private final StudentRepository studentRepository;

    @Override
    public List<Student> findByName(String name) {
        return studentRepository.findByName(name);
    }

    @Override
    public List<Student> findByEmailAndPhone(String email, String phone) {
        return studentRepository.findByEmailAndPhone(email, phone);
    }

    @Override
    public List<Student> findByEmailOrPhone(String email, String phone) {
        return studentRepository.findByEmailOrPhone(email, phone);
    }

    @Override
    public List<Student> findByEmailEndingWith(String email) {
        return studentRepository.findByEmailEndingWith(email);
    }

    @Override
    public List<Student> findByPhoneContaining(String phone) {
        return studentRepository.findByPhoneContaining(phone);
    }

    @Override
    public List<Student> findByNameStartingWith(String name) {
        return studentRepository.findByNameStartingWith(name);
    }

    @Override
    public List<Student> findByNameLike(String name) {
        // Like 패턴은 직접 만들어서 넘겨준다.
        return studentRepository.findByNameLike("%" + name + "%");
    }

    @Override
    public List<Student> findAllByOrderByNameDesc() {
        return studentRepository.findAllByOrderByNameDesc();
    }

    @Override
    public List<Student> findByIdBetween(int from, int to) {
        return studentRepository.findByIdBetween(from, to);
    }
}

③ Controller – REST API 엔드포인트


package com.mycom.myapp.controller;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.service.StudentServiceFind;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/students")
@RequiredArgsConstructor
public class StudentControllerFind {

    private final StudentServiceFind studentServiceFind;

    @GetMapping("/find/name")
    public List<Student> findByName(@RequestParam("name") String name) {
        return studentServiceFind.findByName(name);
    }

    @GetMapping("/find/emailandphone")
    public List<Student> findByEmailAndPhone(@RequestParam("email") String email,
                                                @RequestParam("phone") String phone) {
        return studentServiceFind.findByEmailAndPhone(email, phone);
    }

    @GetMapping("/find/emailorphone")
    public List<Student> findByEmailOrPhone(@RequestParam("email") String email,
                                               @RequestParam("phone") String phone) {
        return studentServiceFind.findByEmailOrPhone(email, phone);
    }

    @GetMapping("/find/namestartingwith")
    public List<Student> findByNameStartingWith(@RequestParam("name") String name) {
        return studentServiceFind.findByNameStartingWith(name);
    }

    @GetMapping("/find/emailendingwith")
    public List<Student> findByEmailEndingWith(@RequestParam("email") String email) {
        return studentServiceFind.findByEmailEndingWith(email);
    }

    @GetMapping("/find/phonecontaining")
    public List<Student> findByPhoneContaining(@RequestParam("phone") String phone) {
        return studentServiceFind.findByPhoneContaining(phone);
    }

    @GetMapping("/find/namelike")
    public List<Student> findByNameLike(@RequestParam("name") String name) {
        return studentServiceFind.findByNameLike(name);
    }

    @GetMapping("/find/orderbynamedesc")
    public List<Student> findAllByOrderByNameDesc() {
        return studentServiceFind.findAllByOrderByNameDesc();
    }

    @GetMapping("/find/idbetween")
    public List<Student> findByIdBetween(@RequestParam("from") int from,
                                            @RequestParam("to") int to) {
        return studentServiceFind.findByIdBetween(from, to);
    }
}

이렇게 해서 Controller → Service → Repository → Entity → DB 흐름을 깔끔하게 나눈 레이어드 아키텍처로 작성했다.


❺ Postman으로 테스트한 주요 예시

1) 이름 정확 일치 검색


GET http://localhost:8080/students/find/name?name=홍길동1

2) 이메일 + 전화번호 AND 검색


GET http://localhost:8080/students/find/emailandphone?email=1@gildong.com&phone=010-0000-0001

3) 이메일 OR 전화번호 검색


GET http://localhost:8080/students/find/emailorphone?email=1@gildong.com&phone=010-0000-0003

4) 이름 시작 문자열 검색 (StartingWith)


GET http://localhost:8080/students/find/namestartingwith?name=홍길동

5) 이메일 끝나는 문자열 검색 (EndingWith)


GET http://localhost:8080/students/find/emailendingwith?email=@gmail.com

6) 전화번호 포함 문자열 검색 (Containing)


GET http://localhost:8080/students/find/phonecontaining?phone=0000

7) Like 검색 (직접 패턴 구성)


GET http://localhost:8080/students/find/namelike?name=홍길동

8) 이름 내림차순 정렬


GET http://localhost:8080/students/find/orderbynamedesc

9) ID 범위 검색 (Between)


GET http://localhost:8080/students/find/idbetween?from=31&to=40

각 요청에 대해 Hibernate가 출력하는 SQL 로그를 보면서, “메서드 이름 → 실제 쿼리”가 머릿속에서 연결되도록 확인했다.


❻ 개념 정리 – 꼭 기억해둘 포인트

  • Spring Data JPA 메서드 규칙을 이해하면, 간단한 조회는 JPQL/SQL을 직접 쓰지 않고도 구현 가능하다.
  • findByXxx : 기본 equal 검색
  • And, Or : 복합 조건 결합
  • StartingWith / EndingWith / Containing : 내부적으로 like를 사용
  • Like : 패턴("%값%")은 개발자가 직접 만들어서 넣는 방식
  • OrderBy필드명Desc/Asc : 정렬 조건을 메서드 이름에 포함
  • Between : 범위 검색이며, 양 끝값도 포함된다.
  • 레이어드 아키텍처: Controller → Service → Repository → Entity → DB 구조를 유지하면, 나중에 기능이 복잡해져도 유지보수가 훨씬 쉽다.

❼ 오늘 실습에서 얻은 교훈 / 느낀 점

  • 간단한 조회 기능 수준에서는, “메서드 이름을 잘 짓는 것 = 쿼리를 잘 짜는 것”과 비슷하다는 감각을 얻었다.
  • findByNameLike("%" + name + "%") 처럼, 결국 뒤에서 어떤 SQL이 나갈지를 상상할 수 있어야 진짜로 JPA를 이해한 것이다.
  • Controller, Service, Repository를 분리해 둔 덕분에, 중간에 검색 조건을 추가하거나 바꿀 때 구조 전체를 바꿀 필요가 없다.
  • 이번 실습은 이후에 배울 페이징(Pageable), 정렬(Sort), 동적 쿼리(JPQL/QueryDSL)을 이해하는 기초가 된다.

오전 실습 덕분에, 앞으로 프로젝트에서 학생/회원/게시글 등의 검색 기능을 만들 때 Spring Data JPA의 메서드 이름 기반 조회를 자신 있게 활용할 수 있는 기반이 만들어졌다.