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

프로젝트 환경 설정

프로젝트 생성

image

라이브러리

  • 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
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의 파라미터의 값을 보여주는 설정으로 별도로 추가하였다.

 

H2 데이터베이스 설치

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

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

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

 

전체 동작 확인

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를 제외하는 방식 적용)
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 생성
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 생성
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 방식의 테스트 코드
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 방식의 테스트 코드

 

예제 도메인 모델 생성

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

image

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

 

회원 엔티티

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) : 양방향 연관관계에서 양쪽 모두 생성하는 메서드 구현

 

팀 엔티티

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 : 연관관계의 주인 필드를 설정

 

동작 테스트

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

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