Repository란 무엇인가?
Repository는 데이터베이스와 애플리케이션 간의 데이터 접근을 담당하는 레이어이다.. Spring Data JPA에서는 Repository를 통해 데이터 조작인 CRUD(Create, Read, Update, Delete) 작업을 간단하게 처리할 수 있다.
Repository의 핵심적인 역할과 특징은 다음과 같다.
- 데이터베이스 쿼리를 추상화하여 비즈니스 로직과 데이터 접근 코드를 분리한다.
- 반복적인 CRUD 작업을 간단한 메서드 호출로 처리할 수 있다.
- 비즈니스 로직(Service)과 데이터베이스(Data Access)를 분리하여 유지보수를 쉽게 만든다.
- 객체 지향적으로 데이터를 다룰 수 있으며, JPA를 통해 SQL 대신 메서드 호출로 데이터를 조작할 수 있다.
Repository 구현하기
Spring Data JPA에서는 JpaRepository 인터페이스를 상속받아 Repository를 구현한다.
기본적인 CRUD 메소드를 자동으로 생성하여 지원해주며 복잡한 CRUD 로직은 추가적인 커스텀 메소드를 정의하여 사용한다.
커스템 메소드를 정의할 때는 Custom Repository interface를 따로 생성하여 구현하며, 해당 구현체는 Impl Repository class를 생성하여 구현한다.
각 구현체의 naming은 기본 Repository 이름에 Custom, Impl을 뒤에 postfix 형태로 붙이면 된다.
public interface UserClanRepositoryCustom {
UserClan findUserClanByIds(Long clanId, Long userId);
}
@Repository
@RequiredArgsConstructor
public class UserClanRepositoryImpl implements UserClanRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public UserClan findUserClanByIds(Long clanId, Long userId) {
return queryFactory.selectFrom(userClan)
.where(user.id.eq(userId).and(clan.id.eq(clanId)))
.fetchOne();
}
}
public interface UserClanRepository extends JpaRepository<UserClan, Long>, UserClanRepositoryCustom {
}
위 코드 예시는 User와 Clan 테이블을 N:N 매핑해주는 중계테이블 UserClan entity에 대한 repository이다.
주로 하나의 Entity에 대해 하나의 Repository 개념이 대응된다고 생각하면 된다.
기본 Repository 인터페이스명은 UserClanRepository이며 JpaRepository와 커스텀 메소드 구현을 위한 UserClanRepositoryCustom 인터페이스를 상속받는다.
UserClanRepositoryCustom의 구현체는 UserClanRepositoryImpl class에서 구현한다.
Query 생성 방식
JpaRepository 기본 메소드 사용
Sptring Data JPA 에서는 다양한 방식으로 DB 쿼리를 생성할 수 있다.
먼저 JpaRepository를 상속받으면 기본 CRUD 메소드를 제공하며 method name 기반의 쿼리를 생성할 수 있다.
- find, findByXXX, findAll 등: XXX key를 기준으로 entity를 찾는다. ex) findByName, findById.
- save(T): Entity 객체를 저장하는 Insert 함수.
- delete(T): Entity 객체를 삭제하는 함수.
- exists(T), existsById: Entity 객체가 table 내 존재하는지 검사하는 함수.
List<User> findByActive(boolean active);
User findByEmailAndName(String email, String name);
기본 메소드는 정말 naming 규칙만으로 간단한 CRUD 메소드를 제공해주기 때문에 편리하게 사용할 수 있다.
@Query 어노테이션을 사용한 JPQL
조건문 같은 요소가 포함된 더 복잡한 쿼리는 @Query 어노테이션을 사용하여 JPQL(Java Persistence Query Language)를 작성할 수 있다.
@Query("SELECT u FROM User u WHERE u.active = :active")
List<User> findUsersByActive(@Param("active") boolean active);
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
List<User> findByNameNative(@Param("name") String name);
이런식으로 ":{변수명}"을 JPQL에 포함한 후 실제 method parameter에 @Param으로 매핑하면 해당 파라미터값이 query로 전달된다.
JPQL은 JPA Entity 객체를 대상으로 하는 객체 지향적인 쿼리이다. 따라서 데이터베이스에 종속되지 않고 Entity의 매핑정보나 관계를 그대로 활용한다.
nativeQuery=true로 설정하면 직접 작성한 쿼리를 그대로 실행한다. JAP가 자동으로 SQL을 생성하지 않기 때문에 Entity와 무관하게 작성한 SQL문이 그대로 실행되게 된다. 따라서 예기치 않은 오류나 결과가 발생할 수도 있다.
하지만 Join문이 포함된 쿼리의 경우 Native Query는 JPQL에서 lazy loading 시에 발생할 수 있는 N+1문제가 발생하지 않는다.
만약 User라는 Entity에 Order라는 Entity가 1:N 관계로 종속되는 경우 조회시 다음과 같은 상황이 발생할 수 있다.
@Query("SELECT u FROM User u")
List<User> findAllUsers();
-- JPQL의 실행 쿼리
-- User 조회
SELECT * FROM users;
-- 각 User의 Orders 조회 (N번 발생)
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 2;
...
@Query(value = "SELECT u.*, o.* FROM users u LEFT JOIN orders o ON u.id = o.user_id", nativeQuery = true)
List<Object[]> findUsersWithOrders();
-- Native Query의 실행 쿼리
SELECT u.*, o.* FROM users u LEFT JOIN orders o ON u.id = o.user_id;
비슷해보이지만 다른 두 가지 방식에 대해 차이점을 분명히 인지하고 상황에 맞게 선택하여 쓰는것이 중요하다.
QueryDSL
동적 쿼리가 필요한 경우 QueryDSL을 사용하면 안정성과 가독성을 모두 확보할 수 있다.
먼저 build.gradle에 다음 dependecy를 추가한다.
//QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
QueryDSL 코드는 다음과 같이 이루어진다.
@Autowired
private JPAQueryFactory queryFactory;
public List<User> findUsersByDynamicConditions(Boolean active, String name) {
QUser user = QUser.user;
return queryFactory.selectFrom(user)
.where(
user.active.eq(active),
user.name.containsIgnoreCase(name)
)
.fetch();
}
보시다시피 약간 builder pattern 비슷하게 쿼리를 하나씩 쌓아서 생성할 수 있다.
자세한 설명은 생략하고 이러한 형태로 SQL을 생성하여 실행한다고 생각하면 된다.