제육's 휘발성 코딩
article thumbnail
반응형

1. 프로젝트 환경 설정

1.1. 프로젝트 생성

image

1.2. 라이브러리

  • spring-boot-starter-web
    • 톰캣, 스프링 웹 MVC
  • spring-boot-starter-data-jpa
    • aop, jdbc (HikariCP 커넥션 풀) , 하이버네이트 JPA, 스프링 데이터 JPA
  • spring-boot-starter
    • 스프링 부트 + 스프링 코어 + 로깅
  • spring-boot-starter-test
    • junit5 (기본) , mokito, assertj, spring-test
<code />
plugins { id 'org.springframework.boot' version '2.6.8' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'study' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() }
  • build.gradle은 다음과 같다. ps6spy 는 JPA의 파라미터의 값을 보여주는 설정으로 별도로 추가하였다.

1.3.  

1.4. H2 데이터베이스 설치

H2 데이터베이스 버전은 1.4.200을 사용했다. 맥환경에서는 설치 후 bin 폴더에서 ./h2.sh를 통해 실행

https://sasca37.tistory.com/13?category=1218302 를 참고하자.

  • 최초 접속시 jdbc:h2:~/파일명 후 루트 폴더에 ~/파일명.mv.db 파일 생성 확인
  • 이후 접속시 jdbc:h2:tcp://localhost/~/파일명
<code />
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
  • application.yml 설정은 다음과 같다.

1.5.  

1.6. 전체 동작 확인

<code />
package study.datajpa.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity @Getter public class Member { @Id @GeneratedValue private Long id; private String username; //== 외부 호출 방지 ==// protected Member() {} // 기본 JPA 전략은 public , protected 의 기본 생성자가 반드시 존재해야 함. public Member(String username) { this.username = username; } public void changeUsername(String username) { this.username = username; } }
  • 회원 엔티티 생성 (@Setter를 제외하는 방식 적용)
<code />
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; public Member save(Member member) { em.persist(member); return member; } public Member find(Long id) { return em.find(Member.class, id); } }
  • 기존의 JPA 방식 Repository 생성
<code />
package study.datajpa.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.datajpa.entity.Member; public interface MemberRepository extends JpaRepository<Member, Long> { }
  • Spring Data JPA 방식의 Repository 생성
<code />
package study.datajpa.repository; 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 static org.assertj.core.api.Assertions.*; @SpringBootTest @Transactional @Rollback(value = false) class MemberJpaRepositoryTest { private final MemberJpaRepository memberJpaRepository; @Autowired public MemberJpaRepositoryTest(MemberJpaRepository memberJpaRepository) { this.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); } }
  • 기존 JPA 방식의 테스트 코드
<code />
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 static org.assertj.core.api.Assertions.*; @SpringBootTest @Transactional @Rollback(value = false) public class MemberRepositoryTest { private final MemberRepository memberRepository; @Autowired public MemberRepositoryTest(MemberRepository memberRepository) { this.memberRepository = memberRepository; } @Test public void testMember() { Member member = new Member("memberA"); Member savedMember = memberRepository.save(member); Member findMember = memberRepository.findById(savedMember.getId()).get(); assertThat(findMember.getId()).isEqualTo(member.getId()); assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); assertThat(findMember).isEqualTo(member); } }
  • Spring Data JPA 방식의 테스트 코드

1.7.  

1.8. 예제 도메인 모델 생성

지금까지 Spring Data JPA를 적용하기 위한 프로젝트 설정과 동작 테스트 코드를 작성하였다. 이후 예제에서 다루기 위한 도메인을 생성해보자.

image

  • 다음과 같이 도메인 모델을 설계하고, 구현해보자.

1.8.1.  

1.8.2. 회원 엔티티

<code />
package study.datajpa.entity; import lombok.*; import javax.persistence.*; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "username", "age"}) 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, 0); } public Member(String username, int age) { this(username, age, null); } public Member(String username, int age, Team team) { this.username = username; this.age = age; if (team != null) changeTeam(team); } public void changeTeam(Team team) { this.team = team; team.getMembers().add(this); } }
  • 회원과 팀의 관계는 다대일 즉, 회원이 연관관계의 주인이된다.
    • @ManyToOne으로 설정하고 LAZY 적용 (XToOne은 기본 전략이 EAGER)
    • @NoArgsContructor : 롬복을 사용하여 기본 생성자를 protected로 생성 (JPA 전략 - public or protected 기본 생성자 필수)
    • @ToString(of) : 무한 참조 방지를 위하여 ToString 타입 지정 (Team 제외)
    • @JoinColumn : 연관관계가 이어진 PK를 지정
    • changeTeam(Team team) : 양방향 연관관계에서 양쪽 모두 생성하는 메서드 구현

1.8.3.  

1.8.4. 팀 엔티티

<code />
package study.datajpa.entity; import lombok.*; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @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; } }
  • 팀은 연관관계의 하인이된다.
    • @OneToMany : 하인임을 지정
    • mappedBy : 연관관계의 주인 필드를 설정

1.8.5.  

1.8.6. 동작 테스트

<code />
package study.datajpa.entity; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; @SpringBootTest @Transactional @Rollback(value = false) class MemberTest { @PersistenceContext 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(); em.clear(); // 확인 List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList(); for (Member member : members) { System.out.println("member = " + member); // member 별도 조회 System.out.println("-> member.team = " + member.getTeam()); // 팀 별도 조회 } } }
  • team 출력을 별도로 하여 LAZY로딩이 정상적으로 동작하는 것을 테스트했다.
    • sout(member)를 처음 만나면 member의 모든 정보를 가져와서 영속성 컨텍스트에 저장
    • sout(member.getTeam())을 처음 만나면 member1에 해당하는 teamA 정보를 가져와 영속성 컨텍스트에 저장
    • 이후 멤버 정보는 DB를 거치지 않고 바로 영속성 컨텍스트에서 가져온다.
    • 이후 팀 정보는 member3에서 teamB의 정보를 갖고오기 위해 DB를 한 번 거치게 된다.

본 포스팅은 인프런 - 김영한님의 '실전! 스프링 데이터 JPA' 편을 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^