반응형
스프링 Data JPA의 구조를 알아보자
📌 서론
JPA를 사용하다보니 대체 어떻게 나는 내 Repository를 생성하고 JpaRepository를 상속받기만 해서 JPA의 기능을 사용할 수 있는 것인지 궁금해졌고 이에 Diagram부터 파고들어 분석을 하기 시작했다.
1. JpaRepository와 상속 관계
Repository<T, ID>
- 마커 인터페이스로서, 실제 구현이 없다.
CrudRepository<T, ID>
- CRUD(Create, Read, Update, Delete) 기능을 제공한다.
PagingAndSortingRepository<T, ID>, ListPagingAndSortingRepository<T, ID>
- 페이징 및 정렬 기능을 추가로 제공한다.
ListCrudRepository<T, ID>
- List 컬렉션에 특화된 CRUD 및 페이징/정렬 기능을 제공한다.
QueryByExampleExecutor<T>
- Example 쿼리를 수행할 수 있는 메서드를 제공한다.
2. JPA에 존재하는 독특한 어노테이션
일반적으로 JPA를 사용하는 개발자라면 Entity 클래스나 Repository 클래스에서 사용되는 어노테이션은 대부분 사용 방법을 알고있을 것이라고 판단하여 잘 사용하지 않거나 독특하다는 생각이 들었던 어노테이션을 2개 정리해 봤다.
@NoRepositoryBean
- @NoRepositoryBean 어노테이션은 Spring Data JPA에서 특별한 용도로 사용된다.
- 이 어노테이션은 인터페이스가 Spring Data JPA에 의해 자동으로 빈으로 등록되는 것을 방지하는 역할을 한다. 이해를 돕기 위해 예시 코드와 그림을 제공한다.
// BaseRepository.java
@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
// 공통으로 사용할 메서드 정의
}
// UserRepository.java
@Repository
public interface UserRepository extends BaseRepository<User, Long> {
// User 도메인에 특화된 메서드 정의
}
코드 설명
- BaseRepository 인터페이스에는 공통으로 사용할 메서드를 정의한다.
- @NoRepositoryBean 어노테이션을 BaseRepository에 붙여, 이 인터페이스가 빈으로 등록되지 않도록 한다.
- UserRepository는 BaseRepository를 상속받아 사용한다.
- UserRepository에는 @Repository 어노테이션이 붙어 있으므로, 이 인터페이스만 Spring 컨테이너에 빈으로 등록된다.
다이어그램 설명
- 이렇게 하면 BaseRepository는 빈으로 등록되지 않고, UserRepository만 빈으로 등록되어 사용된다. 이를 통해 공통 로직을 재사용하면서도, Spring Data JPA의 자동 구성을 유지할 수 있다.
BaseRepository (Not a Bean)
^
|
|
UserRepository (Bean)
주석 및 JavaDoc 예시
- 이렇게 @NoRepositoryBean을 사용하면, 공통 로직을 가진 부모 레포지토리와 실제로 빈으로 등록되어야 하는 자식 레포지토리를 명확하게 구분할 수 있다.
/**
* 공통 레포지토리 인터페이스
* @NoRepositoryBean 어노테이션을 사용하여 빈으로 등록되지 않게 함
*/
@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
// ...
}
@Indexed
- 이 어노테이션은 해당 엔티티가 검색 인덱스에 포함되어야 함을 나타낸다. 주로 검색 성능을 향상시키기 위해 사용되며 이 어노테이션을 사용하면, 해당 엔티티는 검색 인덱스에 추가되어 빠른 검색이 가능하다.
@Entity
@Indexed
public class MyEntity {
// 필드와 메서드 정의
}
3. Spring Data JPA가 사용하는 원칙(추상화, 다형성)
Spring Data JPA는 내부적으로 여러 디자인 패턴과 OOP(Object-Oriented Programming) 원칙을 활용한다.
인터페이스를 통한 추상화
- Spring Data JPA에서는 JpaRepository라는 인터페이스를 제공한다.
- 이 인터페이스에는 데이터베이스 연산을 위한 여러 메서드가 정의되어 있다.
- 예를 들어, save(), delete(), find() 등의 메서드가 있다.
// JpaRepository 인터페이스 예시
public interface JpaRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
Optional<T> findById(ID id);
void delete(T entity);
// ... 기타 메서드
}
다형성: 유연한 구현체 교체
- JpaRepository 인터페이스의 기본 구현체는 SimpleJpaRepository다.
- 이 구현체는 인터페이스에 정의된 메서드를 실제로 구현하여 데이터베이스 연산을 수행한다.
// SimpleJpaRepository 클래스 예시
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
@Override
public <S extends T> S save(S entity) {
// 실제 저장 로직
}
@Override
public Optional<T> findById(ID id) {
// 실제 조회 로직
}
@Override
public void delete(T entity) {
// 실제 삭제 로직
}
// ... 기타 메서드 구현
}
기본 구현체는 주로 한 개
- 대부분의 경우에 SimpleJpaRepository가 충분한 기능을 제공한다. 그러나 특별한 요구사항이 있는 경우, 사용자는 JpaRepository 인터페이스를 구현한 자신만의 구현체를 제공할 수 있다.
// 사용자 정의 JpaRepository 구현체 예시
public class CustomJpaRepository<T, ID> implements JpaRepository<T, ID> {
// 사용자 정의 메서드 구현
}
4. SimpleJpaRepository의 이해와 활용
JpaRepository가 상속하는 인터페이스에 따라 내부 동작이나 지원하는 메서드가 달라질 수 있으므로, 실제 사용하는 버전의 소스 코드나 문서를 꼼꼼히 살펴보는것이 좋다. 구체적인 구현은 종종 SimpleJpaRepository에서 찾을 수 있으니, 이 부분을 꼼꼼히 분석하면 동작 원리를 훨씬 더 잘 이해할 수 있다.
SimpleJpaRepository 설명
- SimpleJpaRepository는 JpaRepository 인터페이스를 구현하며, JPA를 사용하여 데이터베이스와 상호작용하는 로직이 포함되어 있다.
/**
* SimpleJpaRepository 예시
* JpaRepository 인터페이스를 구현하고, JPA를 사용하여 데이터베이스와 상호작용하는 로직이 포함되어 있다.
*/
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
// ... 생략
/**
* 엔터티를 저장한다.
*/
@Override
public T save(T entity) {
// 구현 코드
}
/**
* 주어진 ID에 해당하는 엔터티를 찾는다.
*/
@Override
public Optional<T> findById(ID id) {
// 구현 코드
}
// ... 기타 메소드 구현
}
커스텀 JPA 리포지토리 생성
- SimpleJpaRepository는 대부분의 CRUD 연산을 처리하며, 필요한 경우 이를 상속하여 커스텀 레포지토리를 만들 수 있다.
/**
* 커스텀 레포지토리 예시
* SimpleJpaRepository를 상속하여 커스텀 메서드를 추가할 수 있다.
*/
public class MyCustomJpaRepository<T, ID> extends SimpleJpaRepository<T, ID> {
/**
* 커스텀 메서드
*/
public void myCustomMethod() {
// 구현 코드
}
}
5. Proxy를 활용하는 JPA
Proxy Pattern
- Spring Data JPA는 프록시 패턴을 사용하여 실제 구현을 감춘다. 이를 통해 런타임에 동적으로 쿼리를 생성하고 실행한다.
- JPA에서의 프록시 패턴은 주로 엔터티 객체에 적용된다. 이 패턴은 Lazy Loading을 가능하게 하며, 실제 데이터가 필요한 시점까지 데이터베이스로부터 데이터를 로딩을 미룬다.
예를 들어, User 엔터티가 Post 엔터티와 연관이 있다고 가정해보자
- 여기서 @OneToMany(fetch = FetchType.LAZY) 어노테이션을 보면, FetchType.LAZY로 설정되어 있다. 이는 User 엔티티를 조회할 때, Post 엔티티는 프록시 객체로 초기화된다는 의미다.
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(fetch = FetchType.LAZY)
private List<Post> posts;
}
- 아래의 findUser 메서드를 실행하면, User 엔티티는 DB로부터 가져와지지만, Post엔티티는 Proxy객체로 남아있게 된다. 실제로 Post 데이터가 필요한 시점(get으로 요청)에만 DB로부터 쿼리가 날아가서 로딩된다.
// Lombok 사용
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
public User findUser(Long id) {
User user = userRepository.findById(id).orElse(null);
return user;
}
}
- 이렇게 JPA는 내부적으로 프록시 패턴을 사용하여 Lazy Loading을 구현하고, 성능을 최적화한다.
public void someMethod() {
User user = userService.findUser(1L);
List<Post> posts = user.getPosts(); // 이 시점에서 실제 Post 데이터 로딩
}
6. JpaRepository의 동작 원리와 실제 사용 예시
다음은 UserRepository의 예시 코드이다.
- 이 인터페이스를 선언하면, Spring Data JPA는 내부적으로 SimpleJpaRepository라는 구현체를 생성한다.
public interface UserRepository extends JpaRepository<User, Long> {
}
- 여기서 SimpleJpaRepository는 JpaRepository를 구현한다. 그리고 Spring Data JPA가 제공하는 여러 CRUD 연산 메서드를 실제로 구현한다.
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
// ...
public List<T> findAll() {
// ...
}
public T save(T entity) {
// ...
}
}
반응형
'Spring Data JPA' 카테고리의 다른 글
JPA N+1 문제가 발생하는 상황과 해결방법 (5) | 2023.12.28 |
---|---|
[SpringBoot] 3.x.x 버전에서 P6Spy 적용하기 (3) | 2023.11.17 |
Spring JPA - 데이터 영속화란? (0) | 2023.08.13 |
Spring JPA - 엔티티를 DTO로 바꿔서 사용하는 이유 (0) | 2023.08.13 |
Spring JPA - 연관관계 매핑 (0) | 2023.08.13 |