JPA를 직접 사용하면 EntityManager를 통해 persist(), find(), remove() 등을 직접 호출해야 합니다. Spring Data JPA는 이런 JPA를 한 단계 더 추상화한 모듈로, Repository 인터페이스만 선언하면 CRUD 기능을 바로 사용할 수 있게 해줍니다.
[ 개발자 ]
↓ (인터페이스만 선언)
[ Spring Data JPA - JpaRepository ]
↓ (자동 구현체 생성)
[ SimpleJpaRepository ]
↓ (내부적으로 사용)
[ JPA (EntityManager) ]
↓
[ Hibernate (JPA 구현체) ]
↓
[ DB ]
개발자는 인터페이스만 만들면 구현 클래스는 Spring Data JPA가 자동으로 생성해줌
Spring 서버가 기동될 때 아래의 과정이 일어납니다.
JpaRepository를 상속받은 인터페이스를 자동 스캔SimpleJpaRepository 클래스를 자동 생성따라서 @RepositoryAnnotation을 붙이지 않아도 자동으로 Bean 등록이 됩니다.
public interface MemoRepository extends JpaRepository<엔티티타입, Long> {
}
| 제네릭 파라미터 | 설명 |
|---|---|
| Entity ex)User | @Entity가 붙은 엔티티 클래스 |
| Long | 해당 엔티티의 @Id 필드타입 |
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
}
simpleJpaRepository의 save() 내부에 @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이 적용되어 있습니다.어떤 메서드에 @Transactional을 직접 붙여야 하는지 헷갈릴 수 있습니다..
| 메서드 | @Transactional 직접 선언 필요? | 이유 |
|---|---|---|
save() | ❌ | SimpleJpaRepository 내부에 이미 있음 |
findAll() / findById() | ❌ | 단순 조회 (변경 없음) |
| update (변경 감지) | ✅ | 직접 작성한 서비스 메서드이므로 명시 필요 |
delete() | ❌ | SimpleJpaRepository 내부에 이미 있음 |
💡 기억법: Spring Data JPA가 제공하는 메서드(save, delete 등)는 이미 트랜잭션이 걸려 있고, 개발자가 직접 작성한 변경 로직에만
@Transactional을 붙이면 됩니다. (대표적으로 update 관련)
[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]
public interface MemoRepository extends JpaRepository<Memo, Long> {
// 기본적으로 내용 비어있음
}
Jpa 기본 메서드만 사용시, 인터페이스는 선언만 필요할 뿐, 내부 코드를 별도로 요구하지 않습니다.
말 그대로 빈 인터페이스입니다. 하지만 save(), findAll(), findById(), delete() 등이 전부 동작합니다.
인터페이스만으로 동작하는 원리는 위 설명한 내용과 더불어, 아래와 같습니다.
서버가 기동될 때, 아래 과정이 일어납니다.
1. 컴포넌트 스캔 시 JpaRepository를 상속한 인터페이스 발견
↓
2. "아, MemoRepository는 Memo 엔티티 + Long 타입 ID를 쓰는구나" 파악
↓
3. SimpleJpaRepository<Memo, Long> 구현체를 프록시로 자동 생성
↓
4. 해당 구현체를 Spring Bean으로 등록
↓
5. MemoService에서 생성자 주입 시 이 Bean이 주입됨
즉, MemoService의 생성자에서 받는 memoReposioty는 사실 Spring이 자동으로 만들어준 SimpleJpaRepository의 구현체인 것
메서드 이름만으로 쿼리를 자동 생성해주는 기능입니다. 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 조건 |
|---|---|---|
| findBy | findByUsername(String name) | WHERE username = ? |
| findAllBy | findAllByUsername(String name) | WHERE username = ? (복수) |
| Containing | findByContentsContatining(String kw) | WHERE contents LIKE '%kw%' |
| Between | findByIdBetween(Long a, Ling b) | WHERE id BETWEEN ? AND ? |
| OrderBy | findAllByOrderByCreatedAtDesc() | ORDER BY created_at DESC |
| And / Or | findByUsernameAndContents(...) | WHERE username = ? AND contents = ? |
쿼리 메서드의 이름이 너무 길어지거나, 복잡한 조건이 필요할 때 사용합니다.
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);
}
게시판 CRUD 서비스를 예로 들면 아래와 같은 방식으로 확장됩니다.
public interface PostRepository extends JpaRepository<Post, Long> {
// 특정 유저의 게시글을 최신순으로 조회
List<Post> findAllByUserIdOrderByCreatedAtDesc(Long userId);
// 제목 키워드 검색 + 페이징
Page<Post> findAllByTitleContaining(String keyword, Pageable pageable);
// 특정 카테고리의 게시글 수 카운트
long countByCategoryId(Long categoryId);
}