인터페이스만 정의했을 뿐인데, 누가 구현체를 만들어서 빈으로 등록해주는 걸까요?

Spring Data JPA를 처음 접하면 마법처럼 느껴집니다. 인터페이스에 메서드 이름만 적으면 쿼리가 자동으로 생성되고, 구현 클래스도 직접 작성할 필요가 없습니다. 이 원리를 이해하면 Spring Data의 강력함을 제대로 활용할 수 있습니다.

개념 정의

Spring Data Repository 는 데이터 접근 계층(DAO)을 인터페이스만으로 정의할 수 있게 해주는 추상화입니다. 개발자가 인터페이스를 선언하면, Spring이 런타임에 구현체를 자동 생성 하여 빈으로 등록합니다.

JAVA
public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByName(String name);
}

이 한 줄의 인터페이스만으로 CRUD 메서드, 페이징, 정렬, 그리고 findByName 쿼리까지 모두 사용할 수 있습니다.

Repository 계층 구조

Spring Data JPA의 Repository는 여러 단계로 나뉘어 있습니다.

PLAINTEXT
Repository (마커 인터페이스)
└── CrudRepository (기본 CRUD)
    └── ListCrudRepository (List 반환)
        └── ListPagingAndSortingRepository (페이징 + 정렬)
            └── JpaRepository (JPA 특화: flush, batch 등)

각 계층이 제공하는 기능은 다음과 같습니다.

  • Repository: 마커 인터페이스로, Spring이 빈으로 인식하는 기준입니다.
  • CrudRepository: save(), findById(), delete(), count() 등 기본 CRUD를 제공합니다.
  • ListCrudRepository: findAll()Iterable 대신 List를 반환합니다.
  • JpaRepository: flush(), saveAllAndFlush(), deleteInBatch() 등 JPA 특화 메서드를 추가합니다.

구현체 자동 생성 원리

동작 과정

  1. ** 컴포넌트 스캔 **: @EnableJpaRepositories(Spring Boot에서는 자동 설정)가 Repository를 상속한 인터페이스를 찾습니다.
  2. ** 프록시 생성 **: 각 인터페이스에 대해 JdkDynamicAopProxy를 사용하여 프록시 객체를 생성합니다.
  3. SimpleJpaRepository: 프록시 내부에서 실제 로직은 SimpleJpaRepository 클래스가 처리합니다.
  4. ** 빈 등록 **: 생성된 프록시를 스프링 빈으로 등록합니다.

SimpleJpaRepository가 실제 CRUD 구현을 담당합니다. findById()는 내부적으로 em.find()를 호출합니다.

JAVA
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

    private final EntityManager em;

    @Override
    public Optional<T> findById(ID id) {
        return Optional.ofNullable(em.find(getDomainClass(), id));
    }

save() 메서드는 엔티티가 새것인지 판단하여 persist()merge()를 자동으로 분기합니다. @Id가 null이면 새 엔티티로 판단합니다.

JAVA
    @Override
    @Transactional
    public <S extends T> S save(S entity) {
        if (entityInformation.isNew(entity)) {
            em.persist(entity);   // 새 엔티티 → INSERT
            return entity;
        } else {
            return em.merge(entity);  // 기존 엔티티 → SELECT + UPDATE
        }
    }
}

메서드 이름 파싱 규칙

Spring Data는 메서드 이름을 분석하여 JPQL 쿼리를 자동 생성합니다.

기본 구조

PLAINTEXT
find + By + 조건필드 + 키워드 + And/Or + 조건필드 + 키워드

주요 키워드

키워드예시SQL 조건
Is, EqualsfindByName(String)= ?
BetweenfindByAgeBetween(int, int)BETWEEN ? AND ?
LessThanfindByAgeLessThan(int)< ?
GreaterThanEqualfindByAgeGreaterThanEqual(int)>= ?
LikefindByNameLike(String)LIKE ?
ContainingfindByNameContaining(String)LIKE %?%
InfindByAgeIn(Collection)IN (?, ?, ...)
OrderByfindByNameOrderByAgeDesc(String)ORDER BY age DESC
NotfindByNameNot(String)<> ?
IsNullfindByEmailIsNull()IS NULL

반환 타입

JAVA
// 단건
Optional<Member> findByEmail(String email);
Member findByName(String name);

// 다건
List<Member> findByAge(int age);
Stream<Member> findByAgeGreaterThan(int age);

// 페이징
Page<Member> findByName(String name, Pageable pageable);
Slice<Member> findByAge(int age, Pageable pageable);

// 존재 여부 / 개수
boolean existsByEmail(String email);
long countByAge(int age);

// 삭제
void deleteByName(String name);
long deleteByAge(int age);

@Query — 직접 쿼리 작성

메서드 이름 파싱으로 표현하기 어려운 쿼리는 @Query로 직접 작성합니다.

JPQL

JAVA
@Query("SELECT m FROM Member m WHERE m.age > :age AND m.team.name = :teamName")
List<Member> findActiveMembers(@Param("age") int age, @Param("teamName") String teamName);

네이티브 SQL

JAVA
@Query(value = "SELECT * FROM member WHERE age > :age", nativeQuery = true)
List<Member> findByAgeNative(@Param("age") int age);

벌크 수정 쿼리

JAVA
@Modifying(clearAutomatically = true)
@Query("UPDATE Member m SET m.age = m.age + 1 WHERE m.team.id = :teamId")
int bulkAgePlus(@Param("teamId") Long teamId);

Projection — 필요한 데이터만 조회

엔티티 전체가 아닌 특정 필드만 조회하고 싶을 때 Projection을 사용합니다.

인터페이스 기반 Projection (Closed)

JAVA
// 인터페이스 정의
public interface MemberNameOnly {
    String getName();
    String getTeamName(); // team.name을 자동 매핑
}

// Repository에서 사용
List<MemberNameOnly> findProjectionsByName(String name);

Closed Projection은 getter에 해당하는 컬럼만 SELECT하므로 성능상 유리합니다.

DTO 기반 Projection

JAVA
// DTO 클래스
public record MemberDto(String name, int age) {}

// JPQL에서 생성자 표현식 사용
@Query("SELECT new com.example.dto.MemberDto(m.name, m.age) FROM Member m")
List<MemberDto> findMemberDtos();

동적 Projection

JAVA
// 제네릭으로 반환 타입을 동적으로 결정
<T> List<T> findByName(String name, Class<T> type);

// 사용
List<MemberNameOnly> names = memberRepository.findByName("심", MemberNameOnly.class);
List<MemberDto> dtos = memberRepository.findByName("심", MemberDto.class);

커스텀 Repository 구현

Spring Data가 자동 생성해줄 수 없는 복잡한 로직이 필요할 때는 커스텀 구현을 추가합니다.

JAVA
// 1. 커스텀 인터페이스 정의
public interface MemberRepositoryCustom {
    List<Member> findMembersByComplexCondition(MemberSearchCondition condition);
}

// 2. 구현 클래스 (이름 규칙: 인터페이스명 + Impl)
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final EntityManager em;

    @Override
    public List<Member> findMembersByComplexCondition(MemberSearchCondition condition) {
        // 복잡한 동적 쿼리 로직
        return em.createQuery(/* ... */).getResultList();
    }
}

// 3. Repository에서 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    // JpaRepository 메서드 + 커스텀 메서드 모두 사용 가능
}

구현 클래스의 이름은 반드시 Impl 접미사 를 붙여야 합니다. Spring Data가 이 규칙으로 구현체를 찾아 연결합니다.

주의할 점

save()가 새 엔티티인지 판단하는 기준을 모르면 merge 함정에 빠진다

SimpleJpaRepository.save()는 엔티티가 새것인지(isNew) 판단하여 persist() 또는 merge()를 호출합니다. @Id가 null이면 새 엔티티로 판단하는데, ID를 직접 할당하는 전략에서는 항상 merge()가 호출되어 ** 불필요한 SELECT**가 발생합니다. 이 경우 Persistable 인터페이스를 구현하여 isNew 로직을 재정의해야 합니다.

커스텀 Repository 구현체의 이름 규칙을 지키지 않으면 동작하지 않는다

커스텀 구현 클래스의 이름은 ** 인터페이스명 + Impl** 접미사를 붙여야 합니다. MemberRepositoryCustom의 구현체는 MemberRepositoryImpl이어야 합니다. 이 규칙을 어기면 Spring Data가 구현체를 찾지 못합니다.

메서드 이름이 길어지면 @Query로 대체해야 한다

findByNameAndAgeGreaterThanAndTeamNameOrderByCreatedDateDesc 같은 메서드명은 가독성이 극도로 나쁩니다. 조건이 3개 이상이면 @Query로 직접 작성하는 것이 유지보수에 유리합니다.

정리

항목설명
구현체 생성JDK Dynamic Proxy로 런타임에 자동 생성
메서드 이름 파싱findBy + 조건필드 + 키워드로 JPQL 자동 생성
@Query복잡한 쿼리를 JPQL 또는 네이티브 SQL로 직접 작성
Projection필요한 컬럼만 SELECT하여 성능 최적화
커스텀 RepositoryImpl 접미사 규칙으로 복잡한 로직 확장
댓글 로딩 중...