관리 메뉴

나구리의 개발공부기록

프로젝트 환경설정, 예제 도메인 모델 및 동작확인, 공통 인터페이스 기능, 순수 JPA 기반 리포지토리 만들기, 공통 인터페이스 설정/적용/분석 본문

인프런 - 스프링부트와 JPA실무 로드맵/실전! 스프링 데이터 JPA

프로젝트 환경설정, 예제 도메인 모델 및 동작확인, 공통 인터페이스 기능, 순수 JPA 기반 리포지토리 만들기, 공통 인터페이스 설정/적용/분석

소소한나구리 2024. 10. 25. 23:00

출처 : 인프런 - 실전! 스프링 데이터 JPA (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 프로젝트 환경설정

1) 프로젝트 생성

 

(1) Project

  • Gradle - Groovy
  • Java 17
  • Spring Boot 3.3.5

(2) Metadata

  • Group - study
  • Artifact - data-jpa
  • Packaging - jar

(3) Dependencies

  • Spring Web
  • spring Data JPA
  • H2 database
  • Lombok

2) H2 데이터베이스 설정

  • 스프링부트 3.x 이상 버전이기때문에 H2데이터베이스 버전이 2.2.224버전이므로 해당 버전과 맞춰서 설치를 진행
  • 최초 db파일을 생성해야하므로 jdbc:h2:~/datajpa로 홈경로에 db파일을 생성
  • 그 이후에는jdbc:h2:tcp://localhost/~/datajpa 경로로 접속

** 참고

  • db파일 생성시 원하는 경로에 생성할 경우 ~(home)문법이아닌 ./경로/파일명으로 적어주어야 파일이 생성되며 그이후에 tcp로 접속할때에는 ~/경로/파일명으로 접속해도됨
  • 그리고 db파일을 생성하지 않고 tcp로 접속하면 들어가지긴 하는데, 이후 종료했을때 제대로 종료가 안되기 때문에 db파일을 생성한 후 해당파일의 경로로 tcp 접속을 하는 것을 권장함

3) 스프링 데이터 JPA와 DB설정 및 동작확인

(1) application.yml로 변경

  • 기존 생성된 application.properties를 삭제하고 설정파일을 yml로 작성
spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/h2db/datajpa
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
        highlight_sql: true  # 콘솔에 sql구문의 가독성을 높임, p6spy 라이브러리를 사용하는것을 더 권장함

logging.level:
  org.hibernate.SQL: debug
  org.hibernate.orm.jdbc.bind: trace  # SQL 파라미터 바인딩 로그

 

(2) Test를위한 클래스들 생성

  • entity 패키지에 Member 클래스를, repository 클래스에 MemberJpaRepository클래스를 생성
  • setter를 사용하지 않고 생성자를 통해 회원을 생성
  • 실무에서 권장되는 방법이며, 필드의 값을 변경할 때도 setter가 아니라 수정할 수 있는 메서드를 생성하여 하는 것이 권장됨(계속 강의에서 강조되는 내용)
  • JPA 동작 메커니즘 때문에 기본생성자를 생성, 자세한 내용은 JPA 강의 참조
  • 여기서 생성한 repository는 일반 JPA로 동작하는 repository임
// Member클래스
@Entity
@Getter
public class Member {

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

    protected Member() {}

    public Member(String username) {
        this.username = username;
    }
}

// MemberJpaRepository 클래스
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {

    private final EntityManager em;

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

    public Member find(Long id) {
        return em.find(Member.class, id);
    }
}

 

(3) Test 수행 - MemberJpaRepositoryTest생성

  • JUnit5로 생성한 Member와 MemberJpaRepository의 메소드들을 테스트 진행
  • @Transactional을 테스트에서 적용하면 테스트가 종료하고 Rollback을 하기 때문에 안전하게 테스트를 수행할 수 있음
@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {

    @Autowired MemberJpaRepository memberJpaRepository;

    @Test
    public void testMember() {
        Member member = new Member("memberA");
        Member savedMember = memberJpaRepository.save(member);

        Member findMember = memberJpaRepository.find(savedMember.getId());

        assertThat(findMember.getId()).isEqualTo(member.getId());
        assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
        assertThat(findMember).isEqualTo(member);
    }
}

 

(3) Spring Data JPA로 동작하는 Repository 생성

  • 인터페이스로 repository를 생성하고 JpaRepository를 상속받으면 끝이며 JPA강의에서 직접 생성했었던 기능보다 훨씬 수많은 기능을 공짜로 매우 간편하게 사용할 수 있음
public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

(4) Test 수행

  • Repository에 메서드들을 만들지 않았는데, 기본 메서드들이 모두 제공되고있음
  • 테스트를 수행해보면 위에서 JPA로 동작하는 Repository와 완전 동일하게 동작하는 로그를 확인할 수 있음
@SpringBootTest
@Transactional
//@Rollback(false) // 적용하면 롤백되지않고 DB에 데이터가 반영됨
class MemberRepositoryTest {

    @Autowired MemberRepository memberRepository;

    @Test
    public void testMember() {
        Member member = new Member("memberA");

        // 스프링 데이터 JPA가 제공하는 save와 findById 메서드, findById는 기본 반환타입이 Optional임
        Member saveMember = memberRepository.save(member);
        Member findMember = memberRepository.findById(saveMember.getId()).get();

        assertThat(findMember).isEqualTo(saveMember);
        assertThat(findMember.getId()).isEqualTo(saveMember.getId());
        assertThat(findMember.getUsername()).isEqualTo(saveMember.getUsername());

    }
}

 

** 참고

  • 쿼리 파라미터 로그를 남기기 위해 기본 hibernate설정이 아닌 아래의 외부 라이브러리도 많이 사용함
  • 다만 시스템 자원을 사용하기 때문에 개발 단계에서는 편하게 사용하되 운영 시스템에 적용하려면 꼭 성능 테스트를 하고 사용해야함
  • 스프링 부트 버전마다 적용해야하는 버전이 다르기 때문에 사이트에서 내용을 참고해서 적용하면됨
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'

2. 예제 도메인 모델 - 예제 도메인 모델과 동작 확인

1) 도메인 모델의 다이어그램

 

2) Entity 생성

  • 실무와 비슷하게 하기위해 가급적 Setter를 사용하지 않도록 생성
  • 예제이기 때문에 다시 @Setter를 사용하도록 변경하며 실무에서는 Setter를 권장하지 않음

(1) Member 수정 및 Team 생성

  • @NoArgsConstructor 애노테이션을 적용하여 기본생성자를 롬복으로 적용
  • @ToString 애노테이션으로 객체를 출력해도 해당 필드를 쉽게 출력하도록 설정
  • 해당 매핑에 대해서 설명은 JPA 기본강의 내용을 참고해야함
package study.data_jpa.entity;

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // 기본생성자를 생성하는 롬복 애노테이션, 제어자를 PROTECTED로 설정
@ToString(of = {"id", "username", "age"})           // toString을 적용하는 롬복 애노테이션, 연관관계 필드는 toString적용 제외
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String username) {
        this.username = username;
    }
    
    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    // 연관관계 편의 메서드 - team을 변경할 때 해당 메서드를 호출하면 Member와 Team의 연관필드를 모두 수정
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

package study.data_jpa.entity;

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {

    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

 

(2) 생성한 Entity Test

  • 순수 JPA로 동작을 확인하고 이후 강의에서 Spring Data JPA를 적용
package study.data_jpa.entity;

@SpringBootTest
@Transactional
@Rollback(false)
class MemberTest {

    // JPA로 동작 확인
    @PersistenceContext
    private EntityManager em;

    @Test
    public void testEntity() {
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        em.flush();

        List<Member> members = em.createQuery("select m from Member m", Member.class)
                .getResultList();
        for (Member member : members) {
            System.out.println("member = " + member);
            System.out.println("member.getTeam() = " + member.getTeam());
        }
    }
}

 


3. 순수 JPA 기반 리포지토리 생성

** 순수 JPA 리포지들을 Spring Data JPA로 변경하면서 순수 JPA의 한계를 어떻게 극복하는지 알 수 있음

1) MemberJpaRepository 수정

  • 기본적인 JPA를 사용한 메서드들을 작성
  • 스프링데이터 JPA와 비교하기위해 Optional로 반환하는 단건조회 메서드를 추가
  • update는 JPA의 변경감지기능을 활용하기 때문에 메서드를 만들 필요가 없음
package study.data_jpa.repository;

@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {

    private final EntityManager em;

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

    // 삭제
    public void delete(Member member) {
        em.remove(member);
    }

    // 전체조회
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class).getResultList();
    }

    // 단건조회 - 멤버를반환
    public Member find(Long id) {
        return em.find(Member.class, id);
    }

    // 단건조회 - 옵셔널로반환(스프링데이터JPA의 반환방식)
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    // 개수 반환
    public long count() {
        return em.createQuery("select count(m) from Member m", Long.class).getSingleResult();
    }
}

2) TeamJpaRepository 생성

  • MemberJpaRepository와 타입만 다르고 완전 똑같기 때문에 코드 생략
  • 디테일한 쿼리가 달라질뿐이지 이런 기본적인기능들은 코드 구성이 비슷함

3) 생성한 리포지토리의 CURD 테스트

  • Team은 완전히 동일하기 때문에 MemberJpaRepository만 테스트
  • 기존에 생성했던 MemberJpaRepositoryTest에 basicCRUD 테스트케이스를 추가하여 진행
class MemberJpaRepositoryTest {
    // ... 기존 코드 생략
    
    @Test
    public void basicCRUD() {   // repository의 CRUD 테스트
        Member memberA = new Member("memberA");
        Member memberB = new Member("memberB");

        memberJpaRepository.save(memberA);
        memberJpaRepository.save(memberB);

        // 단건조회 테스트
        Member findMember1 = memberJpaRepository.findById(memberA.getId()).get();
        Member findMember2 = memberJpaRepository.findById(memberB.getId()).get();
        assertThat(findMember1).isEqualTo(memberA);
        assertThat(findMember2).isEqualTo(memberB);

        findMember1.setUsername("변경하자이름을"); // 수정 테스트
        assertThat(findMember1.getUsername()).isEqualTo("변경하자이름을");

        List<Member> all = memberJpaRepository.findAll();   // 다건 조회 테스트
        assertThat(all.size()).isEqualTo(2);

        long count = memberJpaRepository.count();   // count 테스트
        assertThat(count).isEqualTo(2);

        memberJpaRepository.delete(memberA);    // 삭제 테스트
        memberJpaRepository.delete(memberB);

        long deleteCount = memberJpaRepository.count(); // 다시 count 테스트
        assertThat(deleteCount).isZero();   // 0개가 되어야함
    }
}

4. 공통 인터페이스 기능 - 설정 / 적용 / 분석

 

1) 스프링 데이터 JPA설정

  • 스프링 데이터 JPA를 설정하려면 main메서드가있는 애플리케이션 클래스에 적용할 리포지토리를 @EnableJpaRepositories애노테이션으로 패키지를 포함하여 지정해줘야함

** 참고

  • 해당 설정은 스프링 부트 사용시 생략 가능하기때문에 대부분 수동 설정을 하지않음
@SpringBootApplication
@EnableJpaRepositories(basePackages = "study.data_jpa.repository")
public class DataJpaApplication {
    // main 메서드
}

 

2) 스프링 데이터 JPA 구조

  • 스프링 데이터 JPA는 생성한 Repository 인터페이스를 스프링 데이터 JPA와 관련된 인터페이스를 상속하기만하면 기본적인 메서드들을 구현하지 않아도 사용할 수 있는데, 그 이유는 자동으로 구현체를 만들기 때문임
  • 실제로 스프링 데이터 JPA관련된 인터페이스를 상속받은 Repository 인터페이스를 getClass로 출력해보면 Proxy로 동작하고있는 것을 확인할 수 있음
  • 순수 JPA 리포지토리에서는 @Repository 애노테이션을 붙혀야했지만 스프링 데이터 JPA로 사용할 경우에는 알아서 찾아서 동작하기 때문에 컴포넌트 스캔기능과 스프링을 예외로 변환하는 과정도 모두 자동으로 처리해줌

3) 공통 인터페이스 적용

  • MemberRepository이미 만들어봤기 때문에 적용이 끝났음
  • TeamRepository도 스프링 데이터 JPA로 만들어서 적용
public interface TeamRepository extends JpaRepository<Team, Long> {
}
  • 스프링 데이터 JPA로 만든 MemberRepository의 CRUD를 테스트하기위해 기존에 순수 JPA로 만든 Repository를 테스트했던 basicCRUD 테스트 메서드를 그대로 의존관계만 변경하여 적용해보면 테스트가 정상적으로 동작함
  • 그이유는 JPA의 메서드를 구현했을 때 스프링 데이터 JPA의 메서드이름과 동일하게 구현했기 때문임
  • 즉, 이러한 기본적인 메서드들이 전부 상속받은 스프링 데이터 JPA 관련 인터페이스에 모두 정의 되어있다는 뜻

4) 공통 인터페이스 분석

(1) 상속 관계도

  • JpaRepository를 들어가서 확인해보면 다양한 각각의 Crud, Paging, 상세 Query관련된 스프링 데이터 인터페이스들을 또 상속받고 있음
  • 패키지 구조를 보면 JpaRepository는 jpa.repository에 있지만, 그 상위 인터페이스들은 data.repository에 바로 정의되어있음
  • 그렇기 때문에 JpaRepository를 사용하면 모든 스프링 데이터 JPA의 기능들을 전부 사용할 수 있음
  • JpaRepository인터페이스 -> 스프링 데이터 관련된 Repository 인터페이스들 -> 최상단에 Repository 인터페이스의 구조로 되어있다고 생각하면 되며 스프링 데이터 관련된 Repository의 인터페이스들은 어떤 DB에서 사용하더라도 공통으로 기능들이 적용할 수 있으며, 각 DB마다 상세로 구현되는 인터페이스도 별도로 제공됨
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>
                              , ListPagingAndSortingRepository<T, ID>
                              , QueryByExampleExecutor<T> {

 

(2) 주요 메서드

  • save, delete, findById, getOne(엔터티를 프록시로 조회), findAll .... 등등 수많은 생각하는 기능들이 기본으로 제공되고 있어 메서드의 이름만 알면됨(잘 모르면 엔터페이스에 들어가서 확인해보면됨)
  • findAll 메서드는 정렬이나 페이징조건을 파라미터로 제공할 수 있음

** 참고

  • findOne(id) -> 반환타입이 Optional로 되고 findById(id)로 변경됨
  • exists(id) -> existsById(id)로 변경됨
  • 공통 인터페이스가 아닌 다른 기능을 구현하고자할 때는 쿼리 메소드의 기능이나, 쿼리를 직접 구현하는 애노테이션을 활용하여 커스텀 할 수 있음