Spring Data JPA - 공통 인터페이스 기능


Spring Data JPA 공통 인터페이스 기능 만들기

순수 JPA 기반 리포지토리 만들기

Java Persistence API (JPA)는 Java 개발자가 관계형 데이터베이스에서 자바 객체를 저장, 수정, 삭제, 조회할 수 있게 도와주는 인터페이스입니다.
리포지토리는 기본적으로 CRUD(Create, Read, Update, Delete)를 수행할 수 있어야 합니다.

  1. 저장
    JPA를 사용하면 자바 객체를 관계형 데이터베이스에 저장할 수 있습니다. EntityManager의 persist 메소드를 이용하면 간단하게 저장할 수 있습니다.

  2. 변경
    JPA는 변경 감지라는 기능을 제공합니다. 트랜잭션 안에서 엔티티를 조회한 후 데이터를 변경하면, 트랜잭션이 종료되는 시점에 변경 감지 기능이 작동합니다. 변경된 엔티티를 감지하고 UPDATE SQL을 자동으로 실행합니다.

  3. 삭제
    엔티티를 삭제하는 것도 간단합니다. EntityManager의 remove 메소드를 사용하면 됩니다.

  4. 전체 조회
    EntityManager의 createQuery 메소드를 사용하여 JPQL을 이용하면 전체 엔티티를 조회할 수 있습니다.

  5. 단건 조회
    find 메소드를 이용하면 특정 엔티티를 조회할 수 있습니다.

  6. 카운트
    JPQL의 COUNT 함수를 이용하면 저장되어 있는 엔티티의 개수를 세는 것도 가능합니다.

MemberJpaRepository.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Repository
class MemberJpaRepository(
@PersistenceContext private val em: EntityManager
) {

fun save(member: Member): Member {
em.persist(member)
return member
}

fun delete(member: Member) {
em.remove(member)
}

fun findAll(): List<Member> {
return em.createQuery("select m from Member m", Member::class.java)
.resultList
}

fun findById(id: Long): Optional<Member> {
val member = em.find(Member::class.java, id)
return Optional.ofNullable(member)
}

fun count(): Long {
return em.createQuery("select count(m) from Member m", Long::class.javaObjectType)
.singleResult
}

fun find(id: Long?): Member {
return em.find(Member::class.java, id)
}

}
MemberJpaRepositoryTest.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@SpringBootTest
@Transactional
class MemberJpaRepositoryTest(
@Autowired private val memberJpaRepository: MemberJpaRepository,
) {

@Test
fun testMember() {
val member = Member(username = "memberA")
val saveMember = memberJpaRepository.save(member)

val findMember = memberJpaRepository.find(saveMember.id)

findMember.id?.let { assertThat(it == member.id) }
assertThat(findMember.username.equals(member.username))
}

@Test
fun basicCRUD() {
val member1 = Member(username = "member1")
val member2 = Member(username = "member2")
memberJpaRepository.save(member1)
memberJpaRepository.save(member2)

// 단건 조회 검증
val findMember1 = member1.id?.let { memberJpaRepository.findById(it).get() }
val findMember2 = member2.id?.let { memberJpaRepository.findById(it).get() }

assertThat(findMember1).isEqualTo(member1)
assertThat(findMember2).isEqualTo(member2)

// 리스트 조회 검증
val all = memberJpaRepository.findAll()
assertThat(all.size).isEqualTo(2)

// 카운트 검증
val count = memberJpaRepository.count()
assertThat(count).isEqualTo(2)

// 삭제 검증
memberJpaRepository.delete(member1)
memberJpaRepository.delete(member2)

val deletedCount = memberJpaRepository.count()
assertThat(deletedCount).isEqualTo(0)
}

}

공통 인터페이스 설정

JavaConfig 설정- 스프링 부트 사용시 생략 가능

1
2
3
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
class AppConfig {}

@SpringBootApplication 애노테이션 위치를 기준으로 해당 패키지와 하위 패키지를 자동으로 스캔하므로, 따로 위치를 지정하지 않아도 됩니다.
org.springframework.data.repository.Repository 인터페이스를 구현한 클래스를 자동으로 스캔하고 등록하기 때문입니다.

예를 들어, MemberRepository 인터페이스는 구현 클래스 없이도 동작합니다. 인터페이스의 구체적인 클래스를 확인해보면, com.sun.proxy.$ProxyXXX와 같은 프록시 클래스라는 것을 확인할 수 있습니다.

1
memberRepository.getClass() // class com.sun.proxy.$ProxyXXX

또한, @Repository 애노테이션을 생략할 수 있습니다. 이는 스프링 데이터 JPA가 자동으로 컴포넌트 스캔을 처리하고, JPA 예외를 스프링 프레임워크의 데이터 액세스 예외로 변환하기 때문입니다.
이렇게 스프링 데이터 JPA는 개발자가 중복된 CRUD 코드를 작성하는 것을 피하게 해주고, 개발 과정을 간소화하며 생산성을 향상시키는 데 크게 기여합니다.

공통 인터페이스 적용

기존에 순수 JPA로 구현했던 MemberJpaRepository를 스프링 데이터 JPA 기반의 MemberRepository로 대체할 예정입니다.
먼저 MemberRepository 인터페이스를 살펴보면, JpaRepository<Member, Long>를 상속받고 있음을 확인할 수 있습니다. 이를 통해 우리는 JPA의 기본적인 CRUD 기능들을 간편하게 사용할 수 있게 됩니다.

MemberRepository.kt
1
2
interface MemberRepository: JpaRepository<Member, Long> {
}
MemberRepositoryTest.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@SpringBootTest
@Transactional
class MemberRepositoryTest(
@Autowired private val memberRepository: MemberRepository,
) {

@Test
fun testMember() {
val member = Member(username = "memberA")
val saveMember = memberRepository.save(member)

val findMember = memberRepository.findById(saveMember.id).get()

findMember.id?.let { assertThat(it == member.id) }
assertThat(findMember.username.equals(member.username))
}

@Test
fun basicCRUD() {
val member1 = Member(username = "member1")
val member2 = Member(username = "member2")
memberRepository.save(member1)
memberRepository.save(member2)

// 단건 조회 검증
val findMember1 = member1.id?.let { memberRepository.findById(it).get() }
val findMember2 = member2.id?.let { memberRepository.findById(it).get() }

assertThat(findMember1).isEqualTo(member1)
assertThat(findMember2).isEqualTo(member2)

// 리스트 조회 검증
val all = memberRepository.findAll()
assertThat(all.size).isEqualTo(2)

// 카운트 검증
val count = memberRepository.count()
assertThat(count).isEqualTo(2)

// 삭제 검증
memberRepository.delete(member1)
memberRepository.delete(member2)

val deletedCount = memberRepository.count()
assertThat(deletedCount).isEqualTo(0)
}

}

기존 순수 JPA 기반 테스트에서 사용했던 코드를 그대로 스프링 데이터 JPA 리포지토리 기반 테스트로 변경해도 동일한 방식으로 동작합니다.

공통 인터페이스 분석

  • JpaRepository 인터페이스: 공통 CRUD 제공 합니다.
  • 제네릭은 <엔티티 타입, 식별자 타입> 설정 합니다.

JpaRepository 공통 기능 인터페이스

JpaRepository.java
1
2
3
4
5
public interface JpaRepository<T, ID extends Serializable>
extends PagingAndSortingRepository<T, ID>
{
...
}

JpaRepository 를 사용하는 인터페이스

MemberRepository.java
1
2
public interface MemberRepository extends JpaRepository<Member, Long> {
}

공통 인터페이스 구성

중요한 변경 사항

  • T findOne(ID)는 이제 Optional<T> findById(ID)로 변경되었습니다.
  • boolean exists(ID)는 이제 boolean existsById(ID)로 변경되었습니다.

제네릭 타입

  • T : 엔티티를 의미합니다.
  • ID : 엔티티의 고유 식별자 타입을 가리킵니다.
  • S : 엔티티와 그의 하위 타입을 의미합니다.

주요 메서드

  • save(S) : 새로운 엔티티는 저장하며, 이미 존재하는 엔티티는 병합합니다.
  • delete(T) : 특정 엔티티를 삭제합니다. 내부적으로는 EntityManager.remove()를 호출합니다.
  • findById(ID) : 특정 엔티티를 조회합니다. 이 과정에서는 EntityManager.find()가 호출됩니다.
  • getOne(ID) : 엔티티를 프록시로 조회합니다. 이 과정에서는 EntityManager.getReference()가 호출됩니다.
  • findAll(…) : 모든 엔티티를 조회합니다. 정렬(Sort)이나 페이징(Pageable) 조건을 매개변수로 제공할 수 있습니다.

소스코드

참조