Spring Data JPA에 대해 알아보자

StrayCat·2026년 2월 15일

1. Spring Data JPA 개요

JPA vs Spring Data JPA

JPA를 직접 사용하면 EntityManager를 통해 persist(), find(), remove() 등을 직접 호출해야 합니다. Spring Data JPA는 이런 JPA를 한 단계 더 추상화한 모듈로, Repository 인터페이스만 선언하면 CRUD 기능을 바로 사용할 수 있게 해줍니다.

[ 개발자 ](인터페이스만 선언)
[ Spring Data JPA - JpaRepository ](자동 구현체 생성)
[ SimpleJpaRepository ](내부적으로 사용)
[ JPA (EntityManager) ][ Hibernate (JPA 구현체) ][ DB ]

개발자는 인터페이스만 만들면 구현 클래스는 Spring Data JPA가 자동으로 생성해줌


2. 동작원리 - SimpleJpaRepository 자동 생성

Spring 서버가 기동될 때 아래의 과정이 일어납니다.

    1. JpaRepository를 상속받은 인터페이스를 자동 스캔
    1. 해당 인터페이스 정보를 기반으로 SimpleJpaRepository 클래스를 자동 생성
    1. 생성된 구현체를 Spring Bean으로 등록

따라서 @RepositoryAnnotation을 붙이지 않아도 자동으로 Bean 등록이 됩니다.


3. 사용 방법 - JpaRepository 등록

public interface MemoRepository extends JpaRepository<엔티티타입, Long> {
}
제네릭 파라미터설명
Entity ex)User@Entity가 붙은 엔티티 클래스
Long해당 엔티티의 @Id 필드타입

4. CRUD 메서드 정리

4-1. Create - save()

public MemoResponseDto createMemo(MemoRequestDto requestDto) {
    Memo memo = new Memo(requestDto);         // RequestDto → Entity
    Memo saveMemo = memoRepository.save(memo); // DB 저장
    return new MemoResponseDto(saveMemo);       // Entity → ResponseDto
}
  • simpleJpaRepositorysave() 내부에 @Transactional이 이미 적용되어 있음
  • 내부적으로 EntityManager.persist()(신규) 또는 merge()(기존)를 호출
// simpleJpaRepository.java

	@Override
	@Transactional
	public <S extends T> S save(S entity) {

		Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);

		if (entityInformation.isNew(entity)) {
			entityManager.persist(entity);
			return entity;
		} else {
			return entityManager.merge(entity);
		}
	}

4-2. Read - findAll() / findById()
전체 조회 :

public List<MemoResponseDto> getMemos() {
    return memoRepository.findAll().stream()
            .map(MemoResponseDto::new)
            .toList();
}

단건 조회 :

private Memo findMemo(Long id) {
    return memoRepository.findById(id).orElseThrow(() ->
        new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
    );
}
  • findById()의 반환 타입은 Optional<Entity>
  • Optional은 null 안전성을 위해 Java 8부터 도입된 래퍼 클래스
  • orElseThrow()를 사용하면 값이 없을 때 예외를 던질 수 있어 깔끔함

4-3. Update - 변경 감지 (Dirty Checking)

@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
    Memo memo = findMemo(id);    // 영속성 컨텍스트에 올림
    memo.update(requestDto);      // 엔티티 필드 값 변경
    return id;
}

⚠️ SimpleJpaRepository에는 update 메서드가 없습니다.

  • 대신 영속성 컨텍스트의 변경 감지를 활용함
1. findById()로 엔티티 조회 → 영속 상태
2. 엔티티의 setter/update 메서드로 필드 값 변경
3. 트랜잭션 커밋 시점에 JPA가 변경을 감지
4. 자동으로 UPDATE SQL 실행
  • 이 때문에 @Transactional이 반드시 필요함. 트랜잭션 없이는 변경감지도 동작하지 않습니다.

4-4. Delete - delete()

public Long deleteMemo(Long id) {
    Memo memo = findMemo(id);
    memoRepository.delete(memo);
    return id;
}
  • delete() 내부에도 @Transactional이 적용되어 있습니다.
  • 파라미터로 엔티티 객체를 넘깁니다 (id가 아님).

5. @Transactional 정리

어떤 메서드에 @Transactional을 직접 붙여야 하는지 헷갈릴 수 있습니다..

메서드@Transactional 직접 선언 필요?이유
save()SimpleJpaRepository 내부에 이미 있음
findAll() / findById()단순 조회 (변경 없음)
update (변경 감지)직접 작성한 서비스 메서드이므로 명시 필요
delete()SimpleJpaRepository 내부에 이미 있음

💡 기억법: Spring Data JPA가 제공하는 메서드(save, delete 등)는 이미 트랜잭션이 걸려 있고, 개발자가 직접 작성한 변경 로직에만 @Transactional을 붙이면 됩니다. (대표적으로 update 관련)


6. 전체 데이터 흐름 (Service 레이어 기준)

[Client 요청]
     ↓
[Controller] → MemoRequestDto 수신
     ↓
[Service]
  ├─ Create: RequestDto → Entity → save() → ResponseDto
  ├─ Read:   findAll()/findById() → Stream → ResponseDto
  ├─ Update: findById() → entity.update() → 변경 감지 → 자동 UPDATE
  └─ Delete: findById() → delete(entity)
     ↓
[Repository] → SimpleJpaRepository (자동 생성)
     ↓
[JPA → Hibernate → DB]

7. Repository 인터페이스 상세

public interface MemoRepository extends JpaRepository<Memo, Long> {
    // 기본적으로 내용 비어있음
}

Jpa 기본 메서드만 사용시, 인터페이스는 선언만 필요할 뿐, 내부 코드를 별도로 요구하지 않습니다.

말 그대로 빈 인터페이스입니다. 하지만 save(), findAll(), findById(), delete() 등이 전부 동작합니다.

인터페이스만으로 동작하는 원리는 위 설명한 내용과 더불어, 아래와 같습니다.

Spring이 내부적으로 하는 일

서버가 기동될 때, 아래 과정이 일어납니다.

1. 컴포넌트 스캔 시 JpaRepository를 상속한 인터페이스 발견
        ↓
2. "아, MemoRepository는 Memo 엔티티 + Long 타입 ID를 쓰는구나" 파악
        ↓
3. SimpleJpaRepository<Memo, Long> 구현체를 프록시로 자동 생성
        ↓
4. 해당 구현체를 Spring Bean으로 등록
        ↓
5. MemoService에서 생성자 주입 시 이 Bean이 주입됨

즉, MemoService의 생성자에서 받는 memoReposioty는 사실 Spring이 자동으로 만들어준 SimpleJpaRepository의 구현체인 것


8. Repository 인터페이스에 코드를 작성하는 경우

1. 쿼리 메서드 (Query Method)

메서드 이름만으로 쿼리를 자동 생성해주는 기능입니다. Spring Data JPA가 메서드명을 파싱해서 SQL을 만들어줍니다.

public interface MemoRepository extends JpaRepository<Memo, Long> {

    // username으로 메모 목록 조회
    List<Memo> findAllByUsername(String username);

    // 제목에 특정 키워드가 포함된 메모 조회
    List<Memo> findAllByContentsContaining(String keyword);

    // 특정 날짜 이후에 작성된 메모 조회
    List<Memo> findAllByCreatedAtAfter(LocalDateTime date);

    // username으로 메모 존재 여부 확인
    boolean existsByUsername(String username);

    // username으로 삭제
    void deleteAllByUsername(String username);
}

메서드 이름의 네이밍 규칙이 정해져 있습니다 :

키워드예시생성되는 SQL 조건
findByfindByUsername(String name)WHERE username = ?
findAllByfindAllByUsername(String name)WHERE username = ? (복수)
ContainingfindByContentsContatining(String kw)WHERE contents LIKE '%kw%'
BetweenfindByIdBetween(Long a, Ling b)WHERE id BETWEEN ? AND ?
OrderByfindAllByOrderByCreatedAtDesc()ORDER BY created_at DESC
And / OrfindByUsernameAndContents(...)WHERE username = ? AND contents = ?

2. @Query - 직접 JPQL / SQL 작성

쿼리 메서드의 이름이 너무 길어지거나, 복잡한 조건이 필요할 때 사용합니다.

public interface MemoRepository extends JpaRepository<Memo, Long> {

    // JPQL 사용
    @Query("SELECT m FROM Memo m WHERE m.username = :username ORDER BY m.createdAt DESC")
    List<Memo> findMemosByUsername(@Param("username") String username);

    // 네이티브 SQL 사용 (DB에 종속적인 쿼리가 필요할 때)
    @Query(value = "SELECT * FROM memo WHERE username = :username", nativeQuery = true)
    List<Memo> findMemosByUsernameNative(@Param("username") String username);
}

3. 자주 사용되는 패턴 예시

게시판 CRUD 서비스를 예로 들면 아래와 같은 방식으로 확장됩니다.

public interface PostRepository extends JpaRepository<Post, Long> {

    // 특정 유저의 게시글을 최신순으로 조회
    List<Post> findAllByUserIdOrderByCreatedAtDesc(Long userId);

    // 제목 키워드 검색 + 페이징
    Page<Post> findAllByTitleContaining(String keyword, Pageable pageable);

    // 특정 카테고리의 게시글 수 카운트
    long countByCategoryId(Long categoryId);
}
  • 핵심은 조건과 상황에 맞는 적절한 응용
profile
알면 좋은 것보단 잊어버리기 싫은 것들을 기록합니다.

0개의 댓글