- 데이터베이스를 쉽게 다루기 위한 ‘데이터 액세스 기술’로 ORM(Object-Relational Mapping) 기법을 사용하여 자바 애플리케이션에서 사용하는 객체와 관계형 데이터베이스 사이의 매핑을 관리하는 ORM 기술에 대한 API 표준 명세서(인터페이스) 의미합니다.
- 이 API를 사용하여 개발자가 직접적인 SQL을 작성하지 않고도 데이터베이스에서 데이터를 저장, 업데이트, 삭제, 조회하는 등의 작업을 수행할 수 있게 해 줍니다. - JPA는 표준화된 API를 제공함으로써, 다양한 ORM 프레임워크(예: Hibernate, EclipseLink, OpenJPA 등)와의 호환성을 보장합니다. 이로 인해 개발자는 특정 ORM 프레임워크에 종속되지 않고 필요에 따라 다른 프레임워크로 쉽게 전환할 수 있습니다.
https://adjh54.tistory.com/422
2) JpaRepository
💡 JpaRepository - Spring Data JPA에서 제공하는 기능 중 하나로 개발자가 데이터베이스와의 CRUD(Create, Read, Update, Delete) 연산을 더욱 쉽게 처리할 수 있도록 도와주는 인터페이스입니다. - JpaRepository 인터페이스를 상속받은 인터페이스를 만들면, Spring Data JPA가 자동으로 해당 인터페이스의 구현체를 만들어서 Bean으로 등록해 줍니다. - 이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 메서드 이름만으로 데이터베이스 연산을 수행할 수 있게 됩니다.
1. ORM 내에서 JpaRepository 구조
💡 ORM 내에서 JpaRepository 구조
- 하단의 ORM 내의 JpaRepository의 구조를 확인해 보면 Repository, CRUDRepository, PagingAndSortingRepository 인터페이스로부터 상속을 받습니다. - 그렇기에 JpaRepository 인터페이스를 사용하면 각각의 상속받은 기능을 모두 이용할 수 있습니다.
https://adjh54.tistory.com/422
2. JpaRepository 활용방안
💡 JpaRepository 활용방안
- JpaReposiory 기반으로 활용할 수 있는 방안으로는 JpaRepository 기본 메서드, Query Method, @Query를 이용하는 방안으로 활용됩니다.
활용 방안
설명
예시
기본 제공 메서드
Spring Data JPA에서 제공하는 기본적인 CRUD(Create, Read, Update, Delete) 연산을 처리하도록 도와주는 메서드
save(), findById(), findAll(), count(), delete()
Query Method
메서드 이름을 통해 SQL 쿼리를 생성하고 실행하는 기능
findByUserNm(String userNm) - UserNm 필드를 기준으로 데이터를 찾는 쿼리를 생성하고 실행
@Query
개발자가 직접 JPQL(Java Persistence Query Language) 또는 SQL 쿼리를 작성할 수 있게 해주는 어노테이션
@Query("SELECT u FROM User u WHERE u.email = ?1") - 개발자가 직접 작성한 JPQL 쿼리를 실행
NamedQuery
JPA에서 미리 정의해둔 쿼리를 이름을 통해 사용할 수 있는 기능. 쿼리를 재사용할 수 있고, 애플리케이션 로딩 시점에 쿼리의 유효성을 검사할 수 있음
@NamedQuery(name="findUserById", query="SELECT u FROM User u WHERE u.id= :id") - 미리 정의된 NamedQuery를 사용하여 쿼리를 실행
1.1. 기본 제공 메서드 활용 예시
💡 기본 제공 메서드 - 'JpaRepository를 상속'받아 구성한 UserJpaRepository를 호출하면 기본 제공 메서드 활용이 가능합니다.
@Repository
public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
}
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
private final UserJpaRepository userJpaRepository;
public UserController(UserJpaRepository userJpaRepository) {
this.userJpaRepository = userJpaRepository;
}
@PostMapping("/users")
public ResponseEntity<List<UserEntity>> selectUserList(@RequestParam String userNm) {
UserEntity userEntity = new UserEntity();
// userJpaRepository 인터페이스의 기본 메서드를 호출하여 사용합니다.
userJpaRepository.findAll();
userJpaRepository.findById((long) 1);
userJpaRepository.count();
userJpaRepository.save(userEntity);
userJpaRepository.delete(userEntity);
}
}
1.2. Query Method 활용 예시
💡 Query Method 활용 예시 - 이후에 나오는 Query Method를 통해서 ‘메서드 명’으로 쿼리문을 수행하는 방법으로 활용이 가능합니다.
@Repository
public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
// 메서드 명을 기준으로 사용자 아이디 기준으로 정렬하여 조회합니다.
List<UserEntity> findByOrderByUserId(long userId);
// 메서드 명을 기준으로 사용자 이름을 기준으로 조회합니다.
List<UserEntity> findByUserNm(String userNm);
}
1.3. @Query 활용 예시
💡 @Query 활용 예시 - 이후에 나오는 @Query 어노테이션을 통해 JPQL을 이용한 엔티티 접근 방식 또는 실제 SQL문을 작성하는 nativeQuery 방법을 통해 구성이 가능합니다.
@Repository
public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
// @Query 활용 + JPQL 활용
@Query(value = "SELECT t1 FROM UserEntity t1 WHERE t1.userId = : userId")
List<UserEntity> findByUserNm(String userNm);
// @Query 활용 + NativeQuery 활용(SQL 쿼리 활용)
@Query(value = "SELECT t1 FROM TB_USER t1 WHERE t1.userId = : userId", nativeQuery = true)
List<UserEntity> selectUserId(@Param("userId") long userId);
}
💡 Query Method - Spring Data JPA에서 제공하는 기능 중 하나로 ‘메서드 이름’을 통해 SQL 쿼리를 생성하고 실행하는 기능을 의미합니다.
- 개발자는 SQL 쿼리를 직접 작성하지 않고도 메서드 이름만으로 간단한 조회 쿼리를 수행할 수 있습니다. - 복잡한 쿼리의 경우에는 @Query 어노테이션을 사용하여 JPQL(Java Persistence Query Language) 또는 SQL을 직접 작성할 수 있습니다
💡 Query Method 종류 - 아래의 표와 같이 Query Method를 통해서 이름을 구성하여 쿼리를 생성할 수 있습니다.
💡Query Method 일반적인 생성 규칙
1. 조회를 수행합니다.
- 조회를 수행하는 ‘findBy[엔티티 필드명]’형태를 통해서 엔티티 필드명의 조건을 포함하여 조회를 수행합니다. - 예를 들어, 'UserEntity' 엔티티가 'UserNm'이라는 필드를 가지고 있다면, 해당 필드를 사용하여 데이터를 찾는 Query Method를 'findByUserNm'이라고 지을 수 있습니다.
2. 조건절을 추가합니다.
- 조회를 구성한 뒤 [엔티티 필드명] 다음에 조건절을 추가할 수 있습니다. - 예를 들어, 특정 이름보다 큰 값을 가진 'UserNm' 필드를 찾으려면 'findByUserNmGreaterThan'이라는 이름을 사용할 수 있습니다.
3. 추가 조건절을 추가합니다.
- 여러 필드에 대한 조건을 동시에 지정하려면, 'And' 또는 'Or'를 사용하여 조건들을 연결할 수 있습니다. - 예를 들어, 'UserNm'과 'UserAge' 필드를 동시에 고려하는 Query Method는 'findByUserNmAndUserAge'라고 지을 수 있습니다.
4. 정렬을 추가합니다.
- 특정 필드를 기준으로 결과를 정렬하려면, 'OrderBy' 키워드를 사용하고, 이어서 정렬하려는 필드의 이름과 'Asc' 또는 'Desc'를 추가합니다. - 예를 들면, 'UserNm' 필드를 기준으로 오름차순으로 정렬하는 Query Method는 'findByOrderByUserNmAsc'라고 지을 수 있습니다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 1. 이름이 특정 값인 사용자를 찾습니다.
List<User> findByName(String name);
// 2. 이름이 특정 값이 아닌 사용자를 찾습니다.
List<User> findByNameNot(String name);
// 3. 이름이 특정 값인 사용자를 무시하고 찾습니다.
List<User> findByNameIgnoreCase(String name);
// 4. 이름이 특정 값으로 시작하는 사용자를 찾습니다.
List<User> findByNameStartingWith(String prefix);
// 5. 이름이 특정 값으로 끝나는 사용자를 찾습니다.
List<User> findByNameEndingWith(String suffix);
// 6. 이름에 특정 값이 포함된 사용자를 찾습니다.
List<User> findByNameContaining(String word);
// 7. 나이가 특정 값보다 적은 사용자를 찾습니다.
List<User> findByAgeLessThan(int age);
// 8. 나이가 특정 값보다 많은 사용자를 찾습니다.
List<User> findByAgeGreaterThan(int age);
// 9. 나이가 특정 범위에 속하는 사용자를 찾습니다.
List<User> findByAgeBetween(int start, int end);
// 10. 특정 날짜 이후에 가입한 사용자를 찾습니다.
List<User> findByJoinDateAfter(Date date);
// 11. 특정 날짜 이전에 가입한 사용자를 찾습니다.
List<User> findByJoinDateBefore(Date date);
// 12. 이름을 오름차순으로 정렬합니다.
List<User> findByNameOrderByAsc();
// 13. 이름을 내림차순으로 정렬합니다.
List<User> findByNameOrderByDesc();
// 14. 가입일을 기준으로 최근 사용자를 찾습니다.
User findFirstByOrderByJoinDateDesc();
// 15. 가입일을 기준으로 가장 오래된 사용자를 찾습니다.
User findFirstByOrderByJoinDateAsc();
}
- [POST] /api/v1/user/users로 호출하였을 시 userJpaRepository.findByUserNm() 메서드가 호출되어 해당 값을 반환합니다.
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
private final UserJpaRepository userJpaRepository;
public UserController(UserJpaRepository userJpaRepository) {
this.userJpaRepository = userJpaRepository;
}
/**
* 사용자 리스트를 조회합니다.
*
* @return
*/
@PostMapping("/users")
public ResponseEntity<List<UserEntity>> selectUserList(@RequestParam String userNm) {
List<UserEntity> userEntityList = userJpaRepository.findByUserNm(userNm);
return new ResponseEntity<>(userEntityList, HttpStatus.OK);
}
}
4. 결과 확인
💡 결과 확인
- 아래와 같은 쿼리가 수행됨을 확인하였습니다.
select
userentity0_.user_id as user_id1_3_,
userentity0_.user_nm as user_nm2_3_
from
tb_user userentity0_
where
userentity0_.user_nm=?;
4) @Query
💡 @Query - 개발자가 직접 JPQL(Java Persistence Query Language) 또는 SQL 쿼리를 작성할 수 있게 해 줍니다. 이는 복잡한 쿼리를 수행해야 하는 경우나, Spring Data JPA에서 제공하는 기본 메서드로 처리할 수 없는 상황에서 유용하게 사용됩니다. - @Query 어노테이션은 JpaRepository를 상속받은 인터페이스의 메서드 위에 선언하며, 괄호 안에 직접 쿼리를 작성합니다. - 쿼리를 작성하는 방법에는 JQPL을 이용하거나 SQL문을 사용하는 방법이 있습니다. - JPQL을 사용하는 경우 엔티티 클래스와 그 멤버 변수를 직접 참조하여 쿼리를 작성하며 SQL문으로 사용하려면 @Query 어노테이션에 nativeQuery 속성을 true로 설정하여 작성합니다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.email = ?1")
User findByEmail(String email);
@Query(value = "SELECT * FROM Users u WHERE u.name = ?1", nativeQuery = true)
User findByName(String name);
}
[ 더 알아보기 ] 💡 JPQL(Java Persistence Query Language)
- SQL을 기반으로 한 객체 모델용 쿼리 언어입니다. SQL과 매우 유사한 형태지만 데이터베이스 ‘테이블과 컬럼’이 아닌 ‘자바 클래��와 변수(객체)’에 작업을 수행한다는 점에서 차이가 있습니다. - 그렇기에 데이터베이스 테이블을 대상으로 쿼리 하는 것이 아닌 엔티티(객체)를 대상으로 쿼리를 수행합니다. 💡JpaRepository 내에서 @Query로 구성을 하였다면 Query Method를 이용한 방식보다 우선순위가 높은가?
- @Query 어노테이션을 사용하면 해당 메서드에 대해 @Query 안에 작성한 JPQL 혹은 SQL 쿼리문이 실행됩니다. - 이는 메서드 이름에 정의된 쿼리 생성 규칙을 무시하고 @Query에 명시된 쿼리를 수행하게 됩니다. 이를 통해 복잡한 쿼리를 수행하거나, Spring Data JPA의 메서드 이름을 통한 쿼리 생성 규칙만으로는 처리할 수 없는 상황에 유연하게 대응할 수 있습니다.
1. @Query 속성 / 속성값
속성
속성 타입
설명
countName
String
NamedQuery의 이름을 반환합니다. 이것은 페이지네이션을 사용할 때 실행할 count 쿼리입니다.
countProjection
String
페이지네이션을 위해 생성된 count 쿼리의 projection 부분을 정의합니다.
countQuery
String
페이지에 대한 총 요소 수를 조회하는 페이지네이션 쿼리에 사용될 특별한 count 쿼리를 정의합니다.
name
String
사용할 명명된 쿼리입니다.
nativeQuery
boolean
주어진 쿼리가 네이티브 쿼리인지 설정합니다.
queryRewriter
Class<? extends QueryRewriter>
쿼리 문자열이 완전히 조립된 후에 쿼리 문자열에 적용해야 하는 QueryRewriter를 정의합니다.
- @Query 어노테이션에서는 JQPL에서 사용하는 파라미터 처리와 동일한 ‘위치 기준 파라미터 바인딩’과 ‘이름 기준 파라미터 바인딩’ 처리 방법을 이용합니다.
2.1. 위치 기준 파라미터 바인딩
💡 위치 기준 파라미터 바인딩 - 쿼리에 ‘?1', ‘?2' 등의 형태로 파라미터를 명시하고, 메서드의 파라미터 순서에 따라 값을 바인딩합니다.
- 예를 들어, 아래와 같이 String email이 첫 번째 위치가 되고 ‘?1’값과 바인딩이 되는 형태입니다. - 두 번째 예제에서 역시 첫 번째 String email이 첫 번째가 되고 두 번째 String userSt가 두 번째 값이 바인딩되어 쿼리가 수행이 됩니다.
@Query("SELECT u FROM User u WHERE u.email = ?1")
User findByEmail(String email);
@Query("SELECT u FROM User u WHERE u.email = ?1 AND u.userSt = ?2")
User findByEmailAndUserSt(String email);
2.2. 이름 기준 파라미터 바인딩
💡 이름 기준 파라미터 바인딩
- 쿼리에 ':파라미터명' 형태로 파라미터를 명시하고, @Param("파라미터명") 어노테이션을 사용하여 메서드 파라미터와 매핑합니다. 이 방식을 사용하면 파라미터의 순서를 신경 쓸 필요가 없습니다.
@Query("SELECT u FROM User u WHERE u.email = :email AND u.name = :name")
User findByEmailAndName(@Param("email") String email, @Param("name") String name);
@Query("SELECT u FROM User u WHERE u.address.city = :city")
List<User> findByCity(@Param("city") String city);
2.3. 객체를 파라미터로 받는 바인딩
💡 객체를 파라미터로 받는 바인딩
- 객체를 바로 파라미터로 받아 해당 객체의 필드를 쿼리에 바인딩할 수 있습니다. - 이때는 객체의 필드명을 :#{#객체명.필드명} 형태로 쿼리에 명시하고 @Param("객체명") 어노테이션을 사용하여 객체를 매핑합니다.
@Query("SELECT u FROM User u WHERE u.email = :#{#user.email} AND u.name = :#{#user.name}")
User findByUser(@Param("user") User user);
[ 더 알아보기] 💡 @param 말고 @requestbody 형태의 JSON 데이터를 받아서 처리는 불가능한가?
- 직접적으로 JpaRepository 내에서는 User findByUser(@RequestBody User user); 형태로는 이용이 불가능합니다. - 그러나 컨트롤러 내에서 @RequestBody를 사용하여 클라이언트로부터 데이터를 받고, 그 데이터를 저장 또는 검색하는데 필요한 변수값을 @Query가 정의된 리포지토리 메서드의 파라미터로 전달하는 것은 가능합니다.
@PostMapping("/users")
public ResponseEntity<UserEntity> selectUserList(@RequestBody UserDto userDto) {
UserEntity selectUser = userJpaRepository.selectUser(userDto);
return new ResponseEntity<>(selectUser, HttpStatus.OK);
}
@Repository
public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
@Query("SELECT u FROM UserEntity u WHERE u.userId = :#{#userDto.userId}")
UserEntity selectUser(UserDto userDto);
}
3. @Query nativeQuery 속성
💡 @Query nativeQuery 속성 - @Query 어노테이션에 nativeQuery 속성을 true로 설정하면, SQL을 직접 사용하여 쿼리를 작성할 수 있는 네이티브 쿼리 사용이 가능합니다. - 해당 방식에서는 JPA가 제공하는 JPQL이 아닌 데이터베이스에 특화된 순수 SQL을 사용해야 할 경우 유용합니다. - 그러나 네이티브 쿼리를 사용하면 데이터베이스 벤더에 따라 SQL 문법이 다르므로, 이식성에 문제가 생길 수 있습니다. 또한, 엔티티가 아닌 테이블을 직접 참조하므로, 엔티티와 테이블 사이의 매핑 정보 변경에 취약하다는 단점이 있습니다.
💡 Query nativeQuery 속성을 사용한 예시
- 네이티브 쿼리 내에서도 동일하게 파라미터를 바인딩하는 방법을 ‘위치’ 혹은 ‘이름’, ‘객체’ 기준으로 파라미터를 바인딩합니다.
// @Query nativeQuery=true, 위치 기반 파라미터 바인딩
@Query(value = "SELECT * FROM Users u WHERE u.name = ?1", nativeQuery = true)
User findByName(String name);
// @Query nativeQuery=true, 이름 기반 파라미터 바인딩
@Query(value = "SELECT t1 FROM TB_USER t1 WHERE t1.userId = : userId", nativeQuery = true)
List<UserEntity> selectUserId(@Param("userId") long userId);
// @Query nativeQuery=true, 객체 기반 파라미터 바인딩
@Query(value = "SELECT t1 FROM TB_USER t1 WHERE t1.userId = :#{#userDto.userId}", nativeQuery = true)
List<UserEntity> selectUserByUserId(UserDto userDto);
5) NamedQuery
💡 NamedQuery
- JPA에서 ‘미리 정해 놓은 쿼리’를 ‘이름’을 통해서 사용할 수 있는 기능을 의미하며 사전에 정의해 두었기에 ‘정적 Query’라고도 합니다.
- 애플리케이션 로딩 시점에 파싱 되어 캐싱되므로, 실행 시점에는 파싱 비용이 없어져서 실행 속도가 빠릅니다. - 이 쿼리는 엔티티 클래스 위에 @NamedQuery 어노테이션을 사용하여 정의할 수 있으며, 여러 개의 Named Query를 정의하려면 @NamedQueries 어노테이션을 사용할 수 있습니다. - 각각의 Named Query는 고유한 이름을 가지며, 이 이름을 통해 Repository에서 메서드를 호출할 수 있습니다.
[ 더 알아보기 ] 💡정적 쿼리와 동적 쿼리에는 뭐가 있고 어떤 차이점이 있을까?
- 정적 쿼리의 경우 애플리케이션 로딩 시점에 쿼리를 정의하고 그 이후에는 변경하지 않는 쿼리를 의미합니다. - 이들은 애플리케이션 로딩 시점에 문법을 체크하고 파싱 하여 캐싱하기 때문에, 실행 시점에는 파싱 비용이 발생하지 않아 성능이 좋습니다. 그러나 정적 쿼리는 애플리케이션 로딩 시점에 정의되므로, 이후에 쿼리를 동적으로 변경할 수 없다는 단점이 있습니다. - 정적 쿼리의 종류에는 JPQL, SQL, NamedQuery 등이 있습니다.
- 동적 쿼리의 경우 실행 시점에 쿼리를 생성하고 변경할 수 있는 쿼리를 의미합니다. 이들은 쿼리를 프로그래밍적으로 생성하고 변경할 수 있어 유연성이 뛰어나지만, 실행 시점에 쿼리를 생성하고 파싱 하므로 정적 쿼리보다 성능이 조금 떨어질 수 있습니다. 그러나 동적 쿼리는 복잡한 조건이나 동적인 쿼리가 필요한 경우에 유용하게 사용됩니다. - 동적 쿼리의 종류에는 Criteria API, QueryDSL 등이 있습니다.
1. @NamedQuery 속성 및 속성값
💡 @NamedQuery 속성 및 속성값 - 엔티티 클래스 상위에 @NamedQuery 어노테이션을 통하여서 쿼리를 지정하는 정적 쿼리로 구성합니다.
속성
속성 타입
필수 여부
설명
name
String
필수
@NamedQuery의 이름을 지정합니다. 이 이름은 JPQL 쿼리를 실행할 때 사용됩니다.
- 엔티티 클래스 내에서 @NamedQueries 어노테이션을 통해서 다건의 NamedQuery를 ‘UserEntity.selectUserList’와 UserEntity.selectUserDetail로 구성하였습니다. - 사전에 정의해두었기에 인터페이스만 호출하면 해당 쿼리가 수행이 됩니다.
@Entity
@Getter
@Table(name = "tb_user")
@NamedQueries({
@NamedQuery(name = "UserEntity.selectUserList", query = "SELECT u FROM UserEntity u WHERE u.userNm = :userNm"),
@NamedQuery(name = "UserEntity.selectUserDetail", query = "SELECT u FROM UserEntity u WHERE u.userId = :userId")
})
public class UserEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private long userId;
@Column(name = "user_nm")
private String userNm;
}