Repository 핵심 인터페이스와 구조
Spring Data JPA의 데이터 액세스 계층은 Repository 인터페이스를 중심으로 추상화되어 있습니다. 이 인터페이스는 도메인 엔티티 클래스와 해당 ID 타입을 제네릭 인자로 받는 마커 인터페이스로 작동합니다. 기본적인 데이터 조작이 필요하다면 CrudRepository를 상속받아 CRUD 기능을 즉시 활용할 수 있으며, 페이징과 정렬 기능이 요구될 경우 PagingAndSortingRepository를 확장하여 사용합니다.
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
// 페이징 및 정렬을 포함한 기본 메서드 자동 제공
}
메서드 이름 기반 쿼리 생성
Spring Data JPA의 강력한 기능 중 하나는 메서드 이름을 파싱하여 JPQL 쿼리를 자동으로 생성하는 것입니다. find...By, read...By, count...By 등의 접두사를 사용하며, And, Or, Between, LessThan 등의 키워드를 조합하여 복잡한 조건도 손쉽게 표현할 수 있습니다.
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
// 다중 조건 및 대소문자 무시
List<Employee> findByDepartmentAndRoleIgnoreCase(String department, String role);
// 중복 제거 및 정렬
List<Employee> findDistinctByStatusOrderByHireDateDesc(String status);
// 결과 개수 제한
Optional<Employee> findTopByDepartmentOrderBySalaryDesc(String department);
}
사용자 정의 Repository 구현 통합
메서드 이름 파싱이나 @Query 애너테이션으로 해결하기 어려운 복잡한 비즈니스 로직은 사용자 정의 인터페이스와 구현체를 통해 추가할 수 있습니다. 구현체 클래스명은 반드시 인터페이스명에 Impl 접미사를 붙여야 Spring Data가 이를 자동으로 감지하여 프록시에 통합합니다.
// 1. 사용자 정의 인터페이스
public interface CustomEmployeeRepository {
void bulkUpdateSalaries(String department, BigDecimal increment);
}
// 2. 구현체 (Impl 접미사 필수)
@Repository
public class CustomEmployeeRepositoryImpl implements CustomEmployeeRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public void bulkUpdateSalaries(String department, BigDecimal increment) {
String jpql = "UPDATE Employee e SET e.salary = e.salary + :inc WHERE e.department = :dept";
entityManager.createQuery(jpql)
.setParameter("inc", increment)
.setParameter("dept", department)
.executeUpdate();
}
}
// 3. 기본 Repository와 통합
public interface EmployeeRepository extends JpaRepository<Employee, Long>, CustomEmployeeRepository {
}
인터페이스 기반 프로젝션 (Projections)
엔티티의 모든 필드 대신 특정 속성만 조회하여 네트워크 및 메모리 오버헤드를 줄이고 싶을 때 프로젝션을 사용합니다. 인터페이스를 정의하고 getter 메서드를 선언하면 Spring Data가 런타임에 동적으로 프록시 객체를 생성하여 매핑합니다. @Value 애너테이션을 사용하면 SpEL을 통해 가상 필드를 계산할 수도 있습니다.
public interface EmployeeSummary {
String getName();
String getDepartment();
// SpEL을 활용한 Open Projection
@Value("#{target.name + ' [' + target.department + ']'}")
String getDisplayName();
}
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<EmployeeSummary> findSummaryByStatus(String status);
}
Specification을 통한 동적 쿼리 조합
검색 조건이 동적으로 변하는 복잡한 화면에서는 JpaSpecificationExecutor를 사용하여 JPA Criteria API를 객체 지향적으로 추상화한 Specification을 활용합니다. 이를 통해 재사용 가능한 predicates를 조합할 수 있습니다.
public class EmployeeSpecifications {
public static Specification<Employee> hasDepartment(String dept) {
return (root, query, cb) ->
(dept == null) ? cb.conjunction() : cb.equal(root.get("department"), dept);
}
public static Specification<Employee> hiredAfter(LocalDate date) {
return (root, query, cb) ->
(date == null) ? cb.conjunction() : cb.greaterThanOrEqualTo(root.get("hireDate"), date);
}
}
// 서비스 계층에서의 활용
List<Employee> result = employeeRepository.findAll(
Specification.where(EmployeeSpecifications.hasDepartment("IT"))
.and(EmployeeSpecifications.hiredAfter(LocalDate.of(2020, 1, 1)))
);
감사(Auditing) 기능 자동화
도메인 객체의 생성 및 수정 시점과 주체를 자동으로 기록하려면 JPA 엔티티 리스너와 Spring Data의 감사 기능을 결합합니다. 엔티티 클래스에 @EntityListeners(AuditingEntityListener.class)를 추가하고, 필드에 @CreatedBy, @CreatedDate 등의 애너테이션을 적용합니다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class BoardArticle {
@Id @GeneratedValue
private Long id;
@CreatedBy
private String author;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
설정 클래스에 @EnableJpaAuditing을 선언하여 기능을 활성화하며, 현재 사용자를 주입하기 위해 AuditorAware 인터페이스를 구현하여 Bean으로 등록해야 합니다.
트랜잭션 및 비관적 락(Pessimistic Lock) 관리
Spring Data JPA의 조회 메서드는 기본적으로 readOnly = true 트랜잭션으로 실행되어 성능을 최적화합니다. 하지만 데이터의 무결성을 위해 동시성을 제어해야 하는 상황에서는 @Lock 애너테이션을 사용하여 비관적 락을 걸 수 있습니다.
public interface AccountRepository extends JpaRepository<Account, Long> {
// 비관적 락을 적용한 조회
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdWithLock(@Param("id") Long id);
// 벌크 수정 연산 (반드시 @Modifying과 @Transactional 필요)
@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Account a SET a.balance = a.balance - :amount WHERE a.id = :id")
int deductBalance(@Param("id") Long id, @Param("amount") BigDecimal amount);
}