Lined Notebook

[Spring Data JPA] 33. Spring Data JPA의 소개

by ymkim

01. 스프링 데이터 JPA 소개

스프링 데이터 JPA란 스프링 프레임워크와 JPA 기반 위에서 JPA를 더욱 편리하게 사용하기 위한 기술.

01-1. 스프링 데이터 JPA의 장점

  • Repository에 구현 클래스 없이 Interface만으로 개발이 가능
  • 기본적인 CRUD 기능을 JPA가 모두 제공
    • 반복적인 작업을 줄여 핵심 비즈니스 로직에 집중 가능

02. 프로젝트 생성

02-1. 사용 기능

  • version: spring boot 2.7.0
  • groupId: study
  • artifactid: data-jpa
  • lib
    • web
    • jpa
    • h2
    • lombok

02-1. 프로젝트 생성 후 간단 테스트

@RestController
public class TestController {

    @GetMapping("/test")
    public String test() {
        return "test";
    }
}
  • localhost:8080/test로 컨트롤러 요청 확인
  • Build Tool 변경
  • Lombok 셋팅

03. 라이브러리 살펴보기

03-1. gradle 의존관계 확인

/gradlew dependencies --configuration compileClasspath
Welcome to Gradle 7.4.1!

Here are the highlights of this release:
 - Aggregated test and JaCoCo reports
 - Marking additional test source directories as tests in IntelliJ
 - Support for Adoptium JDKs in Java toolchains

For more details see <https://docs.gradle.org/7.4.1/release-notes.html>

> Task :dependencies

------------------------------------------------------------
Root project 'data-jpa'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.projectlombok:lombok -> 1.18.24
+--- org.springframework.boot:spring-boot-starter-data-jpa -> 2.7.0
|    +--- org.springframework.boot:spring-boot-starter-aop:2.7.0
|    |    +--- org.springframework.boot:spring-boot-starter:2.7.0
|    |    |    +--- org.springframework.boot:spring-boot:2.7.0
|    |    |    |    +--- org.springframework:spring-core:5.3.20
|    |    |    |    |    \\--- org.springframework:spring-jcl:5.3.20
|    |    |    |    \\--- org.springframework:spring-context:5.3.20
|    |    |    |         +--- org.springframework:spring-aop:5.3.20
|    |    |    |         |    +--- org.springframework:spring-beans:5.3.20
|    |    |    |         |    |    \\--- org.springframework:spring-core:5.3.20 (*)
|    |    |    |         |    \\--- org.springframework:spring-core:5.3.20 (*)
|    |    |    |         +--- org.springframework:spring-beans:5.3.20 (*)
|    |    |    |         +--- org.springframework:spring-core:5.3.20 (*)
|    |    |    |         \\--- org.springframework:spring-expression:5.3.20
|    |    |    |              \\--- org.springframework:spring-core:5.3.20 (*)
|    |    |    +--- org.springframework.boot:spring-boot-autoconfigure:2.7.0
|    |    |    |    \\--- org.springframework.boot:spring-boot:2.7.0 (*)
|    |    |    +--- org.springframework.boot:spring-boot-starter-logging:2.7.0
|    |    |    |    +--- ch.qos.logback:logback-classic:1.2.11
|    |    |    |    |    +--- ch.qos.logback:logback-core:1.2.11
|    |    |    |    |    \\--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36
|    |    |    |    +--- org.apache.logging.log4j:log4j-to-slf4j:2.17.2
|    |    |    |    |    +--- org.slf4j:slf4j-api:1.7.35 -> 1.7.36
|    |    |    |    |    \\--- org.apache.logging.log4j:log4j-api:2.17.2
|    |    |    |    \\--- org.slf4j:jul-to-slf4j:1.7.36
|    |    |    |         \\--- org.slf4j:slf4j-api:1.7.36
|    |    |    +--- jakarta.annotation:jakarta.annotation-api:1.3.5
|    |    |    +--- org.springframework:spring-core:5.3.20 (*)
|    |    |    \\--- org.yaml:snakeyaml:1.30
|    |    +--- org.springframework:spring-aop:5.3.20 (*)
|    |    \\--- org.aspectj:aspectjweaver:1.9.7
|    +--- org.springframework.boot:spring-boot-starter-jdbc:2.7.0
|    |    +--- org.springframework.boot:spring-boot-starter:2.7.0 (*)
|    |    +--- com.zaxxer:HikariCP:4.0.3
|    |    |    \\--- org.slf4j:slf4j-api:1.7.30 -> 1.7.36
|    |    \\--- org.springframework:spring-jdbc:5.3.20
|    |         +--- org.springframework:spring-beans:5.3.20 (*)
|    |         +--- org.springframework:spring-core:5.3.20 (*)
|    |         \\--- org.springframework:spring-tx:5.3.20
|    |              +--- org.springframework:spring-beans:5.3.20 (*)
|    |              \\--- org.springframework:spring-core:5.3.20 (*)
|    +--- jakarta.transaction:jakarta.transaction-api:1.3.3
|    +--- jakarta.persistence:jakarta.persistence-api:2.2.3
|    +--- org.hibernate:hibernate-core:5.6.9.Final
|    |    +--- org.jboss.logging:jboss-logging:3.4.3.Final
|    |    +--- net.bytebuddy:byte-buddy:1.12.9 -> 1.12.10
|    |    +--- antlr:antlr:2.7.7
|    |    +--- org.jboss:jandex:2.4.2.Final
|    |    +--- com.fasterxml:classmate:1.5.1
|    |    +--- org.hibernate.common:hibernate-commons-annotations:5.1.2.Final
|    |    |    \\--- org.jboss.logging:jboss-logging:3.3.2.Final -> 3.4.3.Final
|    |    \\--- org.glassfish.jaxb:jaxb-runtime:2.3.1 -> 2.3.6
|    |         +--- jakarta.xml.bind:jakarta.xml.bind-api:2.3.3
|    |         +--- org.glassfish.jaxb:txw2:2.3.6
|    |         \\--- com.sun.istack:istack-commons-runtime:3.0.12
|    +--- org.springframework.data:spring-data-jpa:2.7.0
|    |    +--- org.springframework.data:spring-data-commons:2.7.0
|    |    |    +--- org.springframework:spring-core:5.3.20 (*)
|    |    |    +--- org.springframework:spring-beans:5.3.20 (*)
|    |    |    \\--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36
|    |    +--- org.springframework:spring-orm:5.3.20
|    |    |    +--- org.springframework:spring-beans:5.3.20 (*)
|    |    |    +--- org.springframework:spring-core:5.3.20 (*)
|    |    |    +--- org.springframework:spring-jdbc:5.3.20 (*)
|    |    |    \\--- org.springframework:spring-tx:5.3.20 (*)
|    |    +--- org.springframework:spring-context:5.3.20 (*)
|    |    +--- org.springframework:spring-aop:5.3.20 (*)
|    |    +--- org.springframework:spring-tx:5.3.20 (*)
|    |    +--- org.springframework:spring-beans:5.3.20 (*)
|    |    +--- org.springframework:spring-core:5.3.20 (*)
|    |    \\--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36
|    \\--- org.springframework:spring-aspects:5.3.20
|         \\--- org.aspectj:aspectjweaver:1.9.7
\\--- org.springframework.boot:spring-boot-starter-web -> 2.7.0
     +--- org.springframework.boot:spring-boot-starter:2.7.0 (*)
     +--- org.springframework.boot:spring-boot-starter-json:2.7.0
     |    +--- org.springframework.boot:spring-boot-starter:2.7.0 (*)
     |    +--- org.springframework:spring-web:5.3.20
     |    |    +--- org.springframework:spring-beans:5.3.20 (*)
     |    |    \\--- org.springframework:spring-core:5.3.20 (*)
     |    +--- com.fasterxml.jackson.core:jackson-databind:2.13.3
     |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.13.3
     |    |    |    \\--- com.fasterxml.jackson:jackson-bom:2.13.3
     |    |    |         +--- com.fasterxml.jackson.core:jackson-annotations:2.13.3 (c)
     |    |    |         +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (c)
     |    |    |         +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (c)
     |    |    |         +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.3 (c)
     |    |    |         +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3 (c)
     |    |    |         \\--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.3 (c)
     |    |    +--- com.fasterxml.jackson.core:jackson-core:2.13.3
     |    |    |    \\--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
     |    |    \\--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
     |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.3
     |    |    +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (*)
     |    |    +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (*)
     |    |    \\--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
     |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3
     |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.13.3 (*)
     |    |    +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (*)
     |    |    +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (*)
     |    |    \\--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
     |    \\--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.3
     |         +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (*)
     |         +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (*)
     |         \\--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
     +--- org.springframework.boot:spring-boot-starter-tomcat:2.7.0
     |    +--- jakarta.annotation:jakarta.annotation-api:1.3.5
     |    +--- org.apache.tomcat.embed:tomcat-embed-core:9.0.63
     |    +--- org.apache.tomcat.embed:tomcat-embed-el:9.0.63
     |    \\--- org.apache.tomcat.embed:tomcat-embed-websocket:9.0.63
     |         \\--- org.apache.tomcat.embed:tomcat-embed-core:9.0.63
     +--- org.springframework:spring-web:5.3.20 (*)
     \\--- org.springframework:spring-webmvc:5.3.20
          +--- org.springframework:spring-aop:5.3.20 (*)
          +--- org.springframework:spring-beans:5.3.20 (*)
          +--- org.springframework:spring-context:5.3.20 (*)
          +--- org.springframework:spring-core:5.3.20 (*)
          +--- org.springframework:spring-expression:5.3.20 (*)
          \\--- org.springframework:spring-web:5.3.20 (*)

(c) - dependency constraint
(*) - dependencies omitted (listed previously)

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
  • 위 명령어 입력 시 현재 gradle 의존관계를 확인할 수 있다
  • spring-boot-stater 하나를 받음으로써, 다른 의존 관계를 전부 받아올 수 있다
  • AssertJ Document
  • 핵심 라이브러리
    • Spring MVC
    • Spring ORM
    • JPA, Hibernate
    • Spring Data JPA
  • 기타 라이브러리
    • H2 DB Client
    • 커넥션 풀 : 부트 기본은 HikariCP
    • 로깅 SLF4J & LogBack
    • 테스트(Test)

04. 스프링 데이터 JPA와 DB 설정, 동작 확인

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/datajpa
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        # show_sql: true
        format_sql: true

logging.level:
  org.hibernate.SQL: debug
# org.hibernate.type: trace
  • 기본적으로 DB와의 Connection을 위해 application.yml 파일을 작성
  • 상세한 내용은 생략

04-1. 회원 엔티티 생성

package study.datajpa.entity;

import lombok.*;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
//@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
//@Setter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String userName;

    protected Member() {
    }

    public Member(String userName) {
        this.userName = userName;
    }
}
  • 위와 같이 Member 엔티티를 생성
  • 맴버 변수로는 시퀸스(id), 회원명(userName)을 갖는다
  • 웬만하면 Entity에 Setter 사용은 지양 해야 한다
    • JPA 스펙에 맞춰 protected 생성자 생성
    • 회원명을 초기화 할 수 있는 생성자 생성
    • 참조변수.setUserName(‘username’)과 같은 데이터 변경을 막기 위함

04-2. 회원 레포지토리 생성

다음은 간단한 레포지토리를 생성 해보자 레포지토리 생성 방식은 클래스, 인터페이스 방식으로 진행이 될 예정이다

package study.datajpa.repository;

import org.springframework.stereotype.Repository;
import study.datajpa.entity.Member;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Repository
public class MemberJpaRepository {

    @PersistenceContext
    private EntityManager em; // Injection EntityManager from spring IOC container

    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    public Member find(Long id) {
        return em.find(Member.class, id);
    }
}
  • @PersistenceContext 어노테이션을 통해 EntityManager 객체를 주입 받는다(from IOC Container)
  • 가장 기본이 회원 저장, 아이디 기반 조회 메서드 생성

04-3. 회원 테스트 코드 작성(MemberJpaRepository)

package study.datajpa.repository;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import study.datajpa.entity.Member;

import static org.assertj.core.api.Assertions.*;

@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {

    @Autowired MemberJpaRepository memberJpaRepository;

    @Test
//    @Rollback(false)
    public void testMember() throws Exception {
        //given
        Member member = new Member("memberA");

        //when
        Member savedMember = memberJpaRepository.save(member);
        Member findMember = memberJpaRepository.find(savedMember.getId());

        //then
        assertThat(findMember.getId()).isEqualTo(member.getId());
        assertThat(findMember.getUserName()).isEqualTo(member.getUserName());
        assertThat(findMember).isEqualTo(member); // 동일한 하나의 Transaction 단위에서 JPA는 동일한 객체 반환을 보장 한다, 1차 캐시
    }
}
  • Test의 경우 Junit5를 기반으로 진행 한다
  • MemberJpaRepository를 테스트 하기 위한 간단한 테스트 코드 작성
    • CASE 01: 저장된 회원의 id가 기존 회원의 id와 같은가? (True)
    • CASE 02: 저장된 회원의 이름이 기존 회원의 이름과 같은가? (True)
    • CASE 03: 저장된 회원 객체가 기존 회원 객체와 같은가? (True)
      • JPA는 기본적으로 동일한 하나의 트랜잭션 단위 내에서 동일한 객체 반환을 보장한다
      • 1차캐시, 영속성 컨텍스트
  • @Transactional 어노테이션이 없는 경우 예외가 발생 한다
    • InvalidDataAccessApiUsageException
    • 이러한 이유로 인해, @Transactional 어노테이션을 추가 해주어야 한다

04-4. 회원 레포지토리 생성

이번에는 Spring Data JPA를 활용하기 위한 인터페이스(Interface) 레포지토리를 생성 해보자.

package study.datajpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.Member;

public interface MemberRepository extends JpaRepository<Member, Long> {

}

위에서 만든 MemberJpaRepository 클래스와는 다르게 MemberRepository는 JpaRepository<T, E> 라는 인터페이스 를 상속 받은 상태다. MemberRepository에 save, find와 같은 메서드를 생성 해야 할 것 같지만, 이미 JpaRepository 를 상속 받으면서 모든 메서드를 제공 받을 수 있는 상태다.

JpaRepository 인터페이스

/*
 * Copyright 2008-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      <https://www.apache.org/licenses/LICENSE-2.0>
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.jpa.repository;

import java.util.List;

import javax.persistence.EntityManager;

import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;

/**
 * JPA specific extension of {@link org.springframework.data.repository.Repository}.
 *
 * @author Oliver Gierke
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Sander Krabbenborg
 * @author Jesse Wouters
 * @author Greg Turnquist
 */
@NoRepositoryBean
public interface JpaRepository<t, id=""> extends PagingAndSortingRepository<t, id="">, QueryByExampleExecutor {

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll()
	 */
	@Override
	List findAll();

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
	 */
	@Override
	List findAll(Sort sort);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
	 */
	@Override
	List findAllById(Iterable ids);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
	 */
	@Override
	 List saveAll(Iterable entities);

	/**
	 * Flushes all pending changes to the database.
	 */
	void flush();

	/**
	 * Saves an entity and flushes changes instantly.
	 *
	 * @param entity entity to be saved. Must not be {@literal null}.
	 * @return the saved entity
	 */
	 S saveAndFlush(S entity);

	/**
	 * Saves all entities and flushes changes instantly.
	 *
	 * @param entities entities to be saved. Must not be {@literal null}.
	 * @return the saved entities
	 * @since 2.5
	 */
	 List saveAllAndFlush(Iterable entities);

	/**
	 * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
	 * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
	 * method.
	 *
	 * @param entities entities to be deleted. Must not be {@literal null}.
	 * @deprecated Use {@link #deleteAllInBatch(Iterable)} instead.
	 */
	@Deprecated
	default void deleteInBatch(Iterable entities) {
		deleteAllInBatch(entities);
	}

	/**
	 * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
	 * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
	 * method.
	 *
	 * @param entities entities to be deleted. Must not be {@literal null}.
	 * @since 2.5
	 */
	void deleteAllInBatch(Iterable entities);

	/**
	 * Deletes the entities identified by the given ids using a single query. This kind of operation leaves JPAs first
	 * level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this method.
	 *
	 * @param ids the ids of the entities to be deleted. Must not be {@literal null}.
	 * @since 2.5
	 */
	void deleteAllByIdInBatch(Iterable ids);

	/**
	 * Deletes all entities in a batch call.
	 */
	void deleteAllInBatch();

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
	 */
	@Deprecated
	T getOne(ID id);

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
	 * @since 2.5
	 */
	@Deprecated
	T getById(ID id);

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @since 2.7
	 */
	T getReferenceById(ID id);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
	 */
	@Override
	 List findAll(Example example);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
	 */
	@Override
	 List findAll(Example example, Sort sort);
}
</t,></t,>
  • 위와 같이 인터페이스 간의 상속을 통해 많은 메서드들이 제공이 되는 상태
  • 대표적으로 findAll, saveAll, save, findBy 등의 메서드가 존재 한다

04-5. 회원 테스트코드 작성

위에서 작성한 테스트코드와 동일한 내용이지만, Spring Data JPA의 동작을 확인하기 위해 생성 하였다

package study.datajpa.repository;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import study.datajpa.entity.Member;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository; // AS-IS memberJpaRepository

    @Test
    @Rollback(false)
    public void test() throws Exception {
        //given
        Member member = new Member("memberA");

        //when
        Member savedMember = memberRepository.save(member);
        Member findMember = memberRepository.findById(savedMember.getId()).get();

        //then
        assertThat(findMember.getId()).isEqualTo(member.getId());
        assertThat(findMember.getUserName()).isEqualTo(member.getUserName());
        assertThat(findMember).isEqualTo(member);
    }
}

여기서 짚고 넘어가야 하는 부분은 MemberRepository 인터페이스 안에 개발자가 어떠한 메서드도 생성하지 않았는데, 테스트 코드는 잘 동작 한다는 점이다. 즉, JpaRepository<T, E>를 상속 받음으로써 기존에 개발자가 작성해야 하는 많은 로직을 자동화 시킬 수 있다는 장점이 존재 한다.

참고 자료

블로그의 정보

기록하고, 복기하고

ymkim

활동하기