Spring에서 JPA를 사용할 때, 개발자는 JPA의 구현체인 Hibernate, EclipseLink, DataNucleus를 직접 다루어야 했습니다.
이 과정에서 데이터베이스와의 상호작용을 처리하기 위해 "EntityManager"를 직접 사용하고, 복잡한 쿼리를 수동으로 작성하며, 트랜잭션을 직접 관리해야 했습니다. 이러한 방식은 많은 반복적인 코드와 관리 작업을 요구했습니다.
이러한 방식을 해결하기 위해 나온 것이 Spring Data JPA입니다.
Spring Data JPA 란❓
Spring Data JPA는 앞서 언급한 JPA의 직접적인 사용방식(Raw Data)에 비해 복잡성을 줄이고, JPA의 구현체를 추상화하여 더 쉽게 사용할 수 있도록 도와주는 라이브러리입니다.
이를 통해 데이터 액세스 작업을 단순화하고, 자동으로 CRUD 작업을 처리하며, 메서드 이름에 기반한 쿼리 생성 등의 기능을 제공하여 JPA를 훨씬 더 편리하게 사용할 수 있게 해 줍니다.
Spring Data JPA의 SimpleJpaRepository
JPA의 구현체를 추상화하여 더 쉽게 사용할 수 있는 라이브러리인 Spring Data JPA는 어떻게 사용해야 할까요❓
개발자는 "JpaRepository"와 같은 인터페이스를 사용하여 데이터 액세스 작업을 단순화할 수 있습니다.
Spring Data JPA에서는 JpaRepository 인터페이스를 자동으로 구현하는 클래스를 생성해 줍니다.
Spring 서버가 시작될 때, JpaRepository를 상속받은 인터페이스가 자동으로 스캔됩니다.
이 정보를 바탕으로 SimpleJpaRepository 클래스가 자동으로 생성되며, 이 클래스가 Spring의 Bean으로 등록됩니다.
따라서, 개발자는 인터페이스의 구현 클래스를 직접 작성하지 않고도 JpaRepository 인터페이스를 통해 JPA의 기능을 손쉽게 사용할 수 있습니다.
Spring Data JPA를 사용할 때, 일반적으로 SimpleJpaRepository를 직접 사용할 필요는 없습니다.
Spring Data JPA는 SimpleJpaRepository를 자동으로 관리하고, 리포지토리 인터페이스가 JpaRepository를 확장하면 자동으로 이 구현체가 사용됩니다.
Spring Data JPA 등록 방법 (Spring boot)
그럼 JpaRepository는 어떻게 등록을 해야 할까요❓
public interface @Entity클래스 extends JpaRepository<해당 Entity, 해당 Entity IDType> {
}
public interface UserRepository extends JpaRepository<User, Long> {
}
JpaRepository는 제네릭 타입으로 @Entity 클래스와 해당 엔티티의 ID 데이터 타입을 지정하여 상속받는 인터페이스로 선언합니다.
- UserRepository는 JpaRepository를 상속받았기 때문에 Spring Data JPA에 의해 자동으로 Bean으로 등록됩니다.
- 제네릭 타입의 첫 번째 매개변수 인 User는 @Entity 클래스이며, 두 번째 매개변수 인 Long은 @Id 필드의 데이터 타입입니다.
- 이를 통해 UserRepository는 데이터베이스의 user 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 됩니다.
Spring Data JPA 동작
이전에 계속 사용했던 메모장 프로젝트를 Spring Data JPA를 적용해 보겠습니다.
MemoRepository
public interface MemoRepository extends JpaRepository<Memo, Long> {
}
MemoRepository에서는 "memo" 데이터 베이스의 memo 테이블과 연결을 할 것이기 때문에 제네릭 타입의 첫 번째 매개변수에 "memo" Entity를 지정을 합니다.
두 번째 제네릭 매개변수는 Memo 엔티티의 기본 키(primary key) 타입입니다. id는 Long 타입으로 지정되어 있습니다.
MemoService
전체 코드
package com.memo.service;
import com.memo.dto.MemoRequestDto;
import com.memo.dto.MemoResponseDto;
import com.memo.entity.Memo;
import com.memo.repository.MemoRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class MemoService {
private final MemoRepository memoRepository;
//private final JdbcTemplate jdbcTemplate;
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
public List<MemoResponseDto> getMemos() {
// DB 조회
return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// memo 내용 수정
memo.update(requestDto);
return id;
}
public Long deleteMemo(Long id) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// memo 삭제
memoRepository.delete(memo);
return id;
}
// findById()메서드 ➡️ 반환 타입 : Optional ➡️ 값이 존재하지 않을 때 예외 처리 필수
private Memo findMemo(Long id) {
return memoRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
);
}
}
public List<MemoResponseDto> getMemos() {
//DB 조회
return memoRepository.findAll().
stream().
map(MemoResponseDto::new).toList();
}
- memoRepository.findAll()
- 모든 Memo 엔티티를 가져옵니다.
- . stream()
- 리스트 ➡️ 스트림 변환
- . map(MemoResponseDto::new)
- 각 Memo 엔티티를 MemoResponseDto로 변환합니다.
- . collect(Collectors.toList())
- 변환된 MemoResponseDto 객체들을 리스트로 수집합니다.
private Memo findMemo(Long id) {
return memoRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
);
}
Spring Data JPA의 findById() 메서드는 Optional 타입을 반환합니다.
orElseThrow() 메서드
Java의 Optional클래스에서 제공되는 메서드로, Optional객체가 비어 있을 때 예외를 발생시키는 기능을 제공
따라서 값이 존재하지 않을 때 orElseThrow() 메서드를 사용하거나 예외처리를 해주어 예외처리를 해줘야 합니다.
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// memo 내용 수정
memo.update(requestDto);
return id;
}
트랜잭션 환경을 만드는 것이 영속성 컨텍스트를 유지하고 변경 감지(Dirty Checking)를 수행하는 데 필수적입니다.
따라서 @Transactional 어노테이션을 필수로 달아줘야 합니다.
@Transcational ❌
만약 @Transactional 어노테이션을 달아주지 않는다면
"데이터 저장"이라는 데이터가 존재하고 있을 때 해당 데이터를 수정을 해도 다음과 같이 데이터는 UPDATE 되지 않습니다.
'Framework > JPA' 카테고리의 다른 글
[JPA] Query Method란 무엇일까 ❓ (0) | 2024.07.27 |
---|---|
[JPA] JPA Auditing란 무엇일까❓(@EnableJpaAuditing, @MappedSuperclass, @EntityListeners) (0) | 2024.07.27 |
[JPA] 트랜잭션 관리와 영속성 컨텍스트의 관계 (@Transaction, 트랜잭션 전파) (0) | 2024.07.27 |
[JPA] JPA Entity 상태 (비영속, 영속, 준영속, 삭제 상태의 이해) (0) | 2024.07.27 |
[JPA] 영속성 컨텍스트란 무엇일까❓ #2(1차 캐시, 변경 감지, 쓰기지연 감소) (0) | 2024.07.27 |