개발인생/Backend

[Spring] Spring Data JPA

forri 2025. 3. 19. 16:40

Spring Data JPA란?

- Spring Data JPA는 Spring에서 JPA(Java Persistence API)를 쉽게 사용할 수 있도록 도와주는 라이브러리이다.

- SQL을 직접 작성하지 않아도 객체(Entity)와 데이터베이스를 자동으로 매핑하고, 데이터를 저장/조회/수정/삭제하는 CRUD 기능을 간단하게 구현할 수 있도록 해줌


장점

1️⃣ SQL을 직접 작성하지 않아도 된다

  • @Query 없이도 자동으로 쿼리를 생성해 줌
  • findById(id), save(entity), delete(entity) 같은 메서드만 호출하면 됨

2️⃣ JPA의 기능을 편하게 사용할 수 있다

  • 엔티티(Entity) 기반으로 데이터베이스 테이블과 매핑
  • OneToMany, ManyToOne 같은 관계 설정 가능
  • 자동으로 트랜잭션을 관리해 줌

3️⃣ 페이징 & 정렬 기능이 내장되어 있다

  • findAll(Pageable pageable)을 사용하면 페이징 및 정렬이 가능

기본 사용법

1️⃣ 의존성 추가 (Spring Boot 기준)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>  <!-- 예제용 H2 DB -->
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

 

2️⃣ Entity 클래스 생성 (DB 테이블 매핑)

import jakarta.persistence.*;

@Entity //이 클래스가 DB테이블과 연결됨
@Table(name = "users")
public class User {

    @Id //기본키(PK)
    @GeneratedValue(strategy = GenerationType.IDENTITY) //자동증가값 설정
    private Long id;

    @Column(nullable = false) //null값 허용X
    private String name;

    private int age;

    // 기본 생성자 (필수)
    public User() {}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter & Setter
    public Long getId() { return id; }
    public String getName() { return name; }
    public int getAge() { return age; }
}

 

3️⃣ JPA Repository 인터페이스 생성

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> { //<User엔티티관리, PK타입 Long>
    
    List<User> findByName(String name);
    
    // 추가적인 조회 메서드 정의 가능
}

 

4️⃣ Service 클래스에서 데이터 처리

import org.springframework.stereotype.Service;
import java.util.List;

@Service //Spring이 자동으로 Bean을 생성
public class UserService {
    private final UserRepository userRepository; //private fianl = userRepository 변하지 않도록 안정성 유지

    public UserService(UserRepository userRepository) { //UserRepository 생성자 주입										
        this.userRepository = userRepository;
    }

	//UserService가 UserRepository를 주입받았기 때문에 DB에서 데이터 저장/조회 기능을 수행 가능
    
    public User saveUser(String name, int age) {
        return userRepository.save(new User(name, age)); // User 저장
    }

    public List<User> getUsersByName(String name) {
        return userRepository.findByName(name); // 이름으로 검색
    }
}

 

※ Spring Bean 자동 생성
UserRepository는 @Repository 어노테이션이 있어서 Spring이 자동으로 Bean을 생성
UserService는 @Service 어노테이션이 있어서 Spring이 UserRepository를 자동으로 주입(Injection)
👉 개발자가 new 키워드로 객체를 직접 생성할 필요 없이 Spring이 관리해 줌!

 

※ 스프링 의존성 주입(Dependency Injection, DI)
필드 주입, 생성자 주입, Setter 주입이 있지만 Spring 공식 문서에서 생성자 주입 권장!

생성자 주입 장점>

  • 의존성이 명확하게 드러남 : UserService가 UserRepository를 꼭 필요로 한다는 게 확실함.
  • 불변성(Immutable) 보장 : 한 번 주입된 userRepository를 변경할 수 없음 (final 사용)
  • Spring이 알아서 주입해 줌 (스프링 빈 자동 주입) : @Service와 @Repository를 사용하면 Spring이 자동으로 주입함

 

5️⃣ Controller에서 API로 활용

import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController //JSON데이터를 주고받는 api 생성
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/create")
    public User createUser(@RequestParam String name, @RequestParam int age) {
        return userService.saveUser(name, age);
    }

    @GetMapping("/search")
    public List<User> searchUsers(@RequestParam String name) {
        return userService.getUsersByName(name);
    }
}

📌 GET vs POST 정리

비교 GET 요청 POST 요청
사용 목적 조회(Read) 저장(Create), 변경(Update), 삭제(Delete)
데이터 전달 방식 URL 쿼리 파라미터 (?key=value) HTTP Body에 데이터 포함
보안 약함 (URL에 데이터 노출) 강함 (데이터가 숨겨짐)
데이터 크기 제한 있음 (URL 길이 제한) 제한 없음
캐싱 가능 여부 가능 불가능

추가 기능

1️⃣ 페이징 & 정렬 기능

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findAll(Pageable pageable); // 페이징 처리
    List<User> findAll(Sort sort); // 정렬 처리

}

 

  • Repository는 인터페이스로 선언만 하고, 데이터 조회는 Service에서 수행!
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public Page<User> getUsersWithPagination() {
        return userRepository.findAll(PageRequest.of(0, 10));  //첫 페이지에서 10개 조회
    }

    public List<User> getSortedUsers() {
        return userRepository.findAll(Sort.by(Sort.Direction.DESC, "age"));  //나이 내림차순 정렬
    }
}

 

2️⃣ JPQL 사용 (복잡한 쿼리 작성)

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.age > :age")
    //@Query → 직접 JPQL(Query Language) 작성 가능
    //:age → 파라미터를 매핑하여 사용(바인딩을 사용하여 SQL Injection 방지)
    List<User> findUsersOlderThan(@Param("age") int age);
}
@Query
- JPQL 또는 Native SQL을 직접 작성할 수 있도록 해주는 어노테이션
- 기본적인 findBy 메서드 외에도 복잡한 쿼리를 직접 작성(JPA에서는 findBy메서드만 사용가능)
  ex. JOIN, GROUP BY, ORDER BY 등의 복잡한 SQL이 필요하면 @Query 필수

 

 

JPQL은 SQL과 비슷하지만 엔티티 객체를 대상으로 쿼리를 작성하는 언어이다.

👉 테이블이 아니라 엔티티 클래스 기준으로 작성하는 게 특징

 

📌 JPQL과 Native SQL의 차이점 정리

비교 JPQL (JPA Query) Native SQL (Raw SQL)
쿼리 대상 엔티티 객체 (User) DB 테이블 (users)
SQL 변환 여부 JPA가 SQL로 변환해서 실행 변환 없이 그대로 실행
DB 종속성 DB에 독립적 (변환 가능) 특정 DB에 종속
자동 매핑 엔티티 기준으로 데이터 매핑 결과를 직접 매핑해야 할 수도 있음
사용 예시 SELECT u FROM User u SELECT * FROM users
장점 유지보수 쉬움, DB 변경에 유연함 복잡한 SQL 최적화 가능, 성능 튜닝 가능
단점 복잡한 SQL 작성 어려움, 일부 기능 지원 X 유지보수 어려움, DB 종속적

JPA가 SQL로 변환해 실행하면 좋은 점

1️⃣ 데이터베이스 변경에 유연함

만약 데이터베이스가 MySQL에서 PostgreSQL로 바뀐다면?

  • 일반 SQL을 사용하면 모든 쿼리를 다시 작성해야 함.
  • 하지만 JPA는 DB 변경에 맞게 자동 변환되므로 변경할 필요 없음

📌 MySQL과 Oracle의 LIMIT 문법이 다름

-- MySQL (LIMIT 사용)
SELECT * FROM users WHERE age > 25 LIMIT 10;

-- Oracle (ROWNUM 사용)
SELECT * FROM users WHERE age > 25 AND ROWNUM <= 10;

 

2️⃣ 유지보수 & 코드 가독성 향상

  • SQL을 직접 작성하지 않아도 됨 → 코드가 더 직관적
  • 엔티티(Entity) 중심으로 로직을 작성 → 객체지향적으로 관리 가능
  • 비즈니스 로직을 더 쉽게 표현 가능
// SQL 직접 작성 (가독성 떨어짐)
@Query(value = "SELECT * FROM users WHERE age > :age", nativeQuery = true)
List<User> findUsersOlderThan(@Param("age") int age);

// JPQL 사용 (더 직관적)
@Query("SELECT u FROM User u WHERE u.age > :age")
List<User> findUsersOlderThan(@Param("age") int age);

 

3️⃣ SQL Injection 방지

  • JPQL은 파라미터 바인딩(@Param 사용) 을 통해 SQL Injection을 방지

📌 SQL Injection 예제 (위험한 코드)

@Query("SELECT u FROM User u WHERE u.name = '" + name + "'")
List<User> findUsersByName(String name);

 

📌 바인딩을 이용한 안전한 JPQL

@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findUsersByName(@Param("name") String name); //@Param을 사용하면 JPA가 자동으로 SQL Injection을 방지