JPA/실전! 스프링 데이터 JPA

1. 프로젝트 환경설정 & 예제 도메인 모델

s_y_130 2023. 9. 20. 23:33

프로젝트 생성


  • Spring boot
  • 사용 기능(라이브러리); web, jpa, h2, lombk
  • SpringBootVersion: 2.7.15
  • groupId: study
  • artifactId: data-jpa

Gradle config code

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.15'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'study'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '11'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	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()
}

 

기본 동작 확인

  • 기본 테스트 케이스 실행
  • 스프링 부트 메인 실행 후 에러페이지로 간단하게 동작 확인(`http://localhost:8080')
  • 테스트 컨트롤러를 만들어서 spring web 동작 확인(http://localhost:8080/hello)
package study.datajpa.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
	@RequestMapping("/hello")
	public String hello() {
		return "hello";
	}
}

 

 

H2 데이터베이스 설치

개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공

 

  • https://www.h2database.com
  • 다운로드 및 설치
  • h2 데이터베이스 버전은 스프링 부트 버전에 맞춘다.
  • 권한 주기: chmod 755 h2.sh
  • 데이터베이스 파일 생성 방법
    • jdbc:h2:~/datajpa (최소 한번)
    • ~/datajpa.mv.db 파일 생성 확인
    • 이후 부터는 jdbc:h2:tcp://localhost/~/datajpa 이렇게 접속

 

 

 

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


Application.yml 작성

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
참고

모든 로그 출력은 가급적 로거를 통해 남겨야 한다.
show_sql : 옵션은 System.out 에 하이버네이트 실행 SQL을 남긴다.
org.hibernate.SQL : 옵션은 logger를 통해 하이버네이트 실행 SQL을 남긴다

 

 

회원 엔티티 작성

@Entity
@Getter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;

    protected Member() {
    }

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

 

회원 JPA 리포지토리 작성

@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 기반 테스트

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

    @Autowired
    MemberJpaRepository memberJpaRepository;

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

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

        //then
        assertThat(findMember.getId()).isEqualTo(member.getId());
        assertThat(findMember.getUsername()).isEqualTo(member.getUsername());

        assertThat(findMember).isEqualTo(member); //JPA 엔티티 동일성 보장
    }
}

참고

Jpa의 모든 데이터 변경은 트랜잭션 안에서 이뤄져야 한다. 

그러므로 Test 시 @Transactional 을 추가해줘야 한다.

+ ) 또한 Test의 @Transactional은 기본적으로 rollback을 취하므로 데이터 확인이 필요하면 @Rollback(value = false) 을 추가해주면 된다.

 

참고

스프링 부트를 통해 복잡한 설정이 다 자동화 되었다. persistence.xml 도 없고, LocalContainerEntityManagerFactoryBean 도 없다.
스프링 부트를 통한 추가 설정은 스프링 부트 메뉴얼을 참고하고, 스프링 부트를 사용하지 않고 순수 스프링과 JPA 설정 방법은 자바 ORM 표준 JPA 프 로그래밍 책을 참고.

 

쿼리 파라미터 로그 남기기

로그에 다음을 추가하기 org.hibernate.type : SQL 실행 파라미터를 로그로 남긴다.

외부 라이브러리 사용 : https://github.com/gavlyukovskiy/spring-boot-data-source-decorator

 

스프링 부트를 사용하면 이 라이브러리만 추가

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7'

// 스프링 부트 3.0
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
※ 참고

쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는 편하게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.

 

 

 

 

예제 도메인 모델


Member 와 Team 두개의 엔티티를 구현하고 연관관계를 맺어줄 것이다.

그리고 해당 관계가 정상적으로 맺어졌는지 테스트까지 진행.

 

 

 

엔티티 클래스

  • Member와 Team 은 다(N) 대 일(1) 관계다.

 

ERD

  • 외래키(FK)는 Member에서 가지고 있는다.

 

 

회원(Member) 엔티티

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @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);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
  • @Setter: 실무에서 가급적 Setter는 사용하지 않기
  • @NoArgsConstructor (AccessLevel.PROTECTED): 기본 생성자 막고 싶은데, JPA 스팩상 PROTECTED로 열어두어야 함
  • @ToString은 가급적 내부 필드만(연관관계 없는 필드만)

changeTeam() 으로 양방향 연관관계 한번에 처리(연관관계 편의 메소드)

 

팀(Team) 엔티티

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

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @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;
    }
}
  • Member와 Team은 양방향 연관관계, Member.team 이 연관관계의 주인
  • Team.members 는 연관관계의 주인이 아님
    • 따라서 Member.team 이 데이터베이스 외래키 값을 변경, 반대편은 읽기만 가능

 

데이터 확인 테스트

@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);
            System.out.println("-> member.team = " + member.getTeam());
        }
    }
}