2장_ JPA 시작
Updated:
1. IntelliJ 프로젝트 세팅
책에서는 이클립스를 사용했지만, 필자는 인텔리제이로 예제를 작성했다.
인텔리제이에서 JPA를 사용하기 위한 세팅을 해보도록 하겠다. 빌드 관리 도구는 Maven이 아닌 Gradle을 사용했다.
build.gradle을 아래와 같이 작성한다.
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
group = 'jpabook.start'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
implementation 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
annotationProcessor 'org.projectlombok:lombok'
}
/*
* queryDSL 설정 추가
*/
def querydslDir = "$buildDir/generated/querydsl"
// JPA 사용 여부와 사용할 경로를 설정
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
// build 시 사용할 sourceSet 추가
sourceSets {
main.java.srcDir querydslDir
}
// querydsl 컴파일시 사용할 옵션 설정
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
// querydsl 이 compileClassPath 를 상속하도록 설정
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
스프링 부트 버전은 2.7.1 버전을 사용했고, 자바는 11 버전을 사용했다.
아직은 사용하지 않지만, 나중에 사용하기 위해 미리 QueryDSL을 추가해뒀고 DB는 h2를 사용했다.
그리고 Spring Boot 2.2부터 JUnit 5 은 기본으로 포함했기 때문에 추가하지 않았으며 롬복도 추가하지 않았다.
annotationProcessor 로만 추가한 이유는 테스트 코드에서 사용하기 위함이다.
다음으로 application.yml을 작성한다. H2 DB 정보를 세팅하는 내용이 담겨있다.
위치는 test/resources 에다가 넣어줬다. (어짜피 테스트로만 실행할거라..)
H2 DB를 임베디드 DB로 사용하면, 부트가 종료 시에 모든 데이터가 날아가는 단점이 있어서 테스트하기 불편하므로
로컬 DB를 설치하여 사용할 예정이다.
spring:
# H2 Setting Info (H2 Console에 접속하기 위한 설정정보 입력)
h2:
console:
enabled: true # H2 Console을 사용할지 여부 (H2 Console은 H2 Database를 UI로 제공해주는 기능)
path: /h2-console # H2 Console의 Path
# Database Setting Info (Database를 H2로 사용하기 위해 H2연결 정보 입력)
datasource:
driver-class-name: org.h2.Driver # Database를 H2로 사용하겠다.
url: jdbc:h2:tcp://localhost/~/test # H2 접속 정보
username: sa # H2 접속 시 입력할 username 정보 (원하는 것으로 입력)
password: # H2 접속 시 입력할 password 정보 (원하는 것으로 입력)
jpa:
properties:
hibernate:
show_sql: true # 하이버네이트가 실행한 SQL을 출력한다.
format_sql: true # 하이버네이트가 실행한 SQL을 출력할 때 보기 쉽게 정렬한다.
use_sql_comments: true # 쿼리를 출력할 때 주석도 함께 출력한다.
id:
new_generator_mappings: true # JPA 표준에 맞춘 새로운 키 생성 전략을 사용한다.
dialect: org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernamte.id.new_generator_mappings 은 값이 ‘FALSE’ 인 경우 Native Generator가 된다.
Native Generator는 다시 하이버네이트에 설정된 방언(Dialect)으로 결정한다. 값이 ‘TRUE’인 경우 SequenceStyleGenerator를 사용하게 되는데
데이터베이스가 Sequence를 지원하는 경우 Sequence Generator를 지원하지 않는다면 Table Generator가 된다.
뭔지는 알 것 같은데 약간 애매모호하다; 일단 이런게 있구나 하고 기억해두고 자세한 내용은 4장에서 다뤄보도록 하자
spring.jpa.properties.hibernamte.dialect는 데이터베이스 방언을 뜻한다.
SQL 표준을 지키지 않거나 특정 데이터베이스만의 고유한 기능을 JPA에서는 방언(Dialect) 이라고 하는데
데이터베이스에 종속되는 기능을 많이 사용하면 나중에 데이터베이스를 교체하기가 어려우므로
하이버네이트를 포함한 대부분의 JPA 구현체들은 이런 문제를 해결하려고 다양한 데이터베이스 방언 클래스를 제공한다.
대표적인 것만 살펴보면 다음과 같다.
- H2: org.hibernate.dialect.H2Dialect
- 오라클 10g: org.hibernate.dialect.Oracle10gDialect
- MySQL: org.hibernate.MySQL5InnoDBDialect
2. H2 데이터베이스 설치
책에서는 http://www.h2database.com에 들어가서 All Platforms 또는 Platform-Independent Zip을 내려받아서 압축을 풀어서 사용하라고 나와있다.
그리고 예제에서 사용한 버전들은 1.4.187이기에 다른 버전을 사용하면 예제가 정상 동작하지 않을 수 있다고 다음 경로를 통해 내려받을 것을 권고하고 있다.
http://www.h2database.com/h2-2015-04-10.zip
하지만 필자는 상남자니까 최신 버전을 받도록 하겠다.
2.1.214 (2022-06-13) 으로 설치했다.
압축을 푼 곳에서 bin/h2.sh를 실행하면 H2 데이터베이스를 서버 모드로 실행한다.
(윈도우는 h2.bat 또는 h2w.bat를 실행하면 된다.)
H2 데이터베이스를 서버 모드로 실행한 후에 웹 브라우저에서 http://localhost:8082를 입력하면 H2 데이터베이스에 접속할 수 있는 화면이 나온다.
맨 위 상단에 저장한 설정을 “Generic H2 (Server)” 로 바꾸고
드라이버 클래스는 “org.h2.Driver”
JDBC URL은 “jdbc:h2:tcp://localhost/~/test”
사용자명은 “sa”
비밀번호는 공란으로 한 후 연결 버튼을 누른다.
그러면 다음과 같은 에러 화면이 보인다.
연결을 누르면 데이터베이스가 자동 생성되어야 하는데, h2 1.4.198 버전 이후부터는 보안 문제로 데이터베이스가 자동으로 생성되지 않는다고 한다.
최신 버전을 괜히 사용한걸까? 란 생각이 잠시 들었지만 방법을 찾아보았다.
그냥 h2가 바라보는 DB 위치에 “test.mv.db” 를 만들어주면 된다고 한다.
메모장에서 다른 이름으로 저장하기 누른 다음, 파일 형식을 모든 파일로 바꾼 후 test.mv.db 입력하여 저장하면 된다. 매우 간단하다.
이제 다시 연결 버튼을 눌러보자. 정상 접속되는 것을 확인할 수 있다.
그리고 테스트를 위해 Member 테이블을 만들어 놓는다.
CREATE TABLE MEMBER(
ID VARCHAR(255) NOT NULL, --아이디(기본키)
NAME VARCHAR(255), --이름
AGE INTEGER NOT NULL, --나이
PRIMARY KEY (ID)
)
3. 예제 작성
위에서 만든 MEMBER 테이블과 매핑 될 객체를 만들어준다.
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Getter
@Setter
@Entity // 이 클래스를 테이블과 매핑한다고 JPA에게 알려준다. 이렇게 @Entity가 사용된 클래스를 엔티티 클래스라 한다.
/**
* 엔티티 클래스에 매핑할 테이블 정보를 알려준다. 여기서는 name 속성을 사용해서 Member 엔티티를 MEMBER 테이블에 매핑했다.
* 이 어노테이션을 생략하면 클래스 이름을 테이블 이름으로 매핑한다 (더 정확히는 엔티티 이름을 사용한다.)
*/
@Table(name = "MEMBER")
public class Member {
/**
* 엔티티 클래스의 필드를 테이블의 기본키(Primary key)에 매핑한다. 여기서는 엔티티의 id 필드를
* ID 기본 키 컬럼에 매핑했다. 이렇게 @Id가 사용된 필드를 식별자 필드라 한다.
*/
@Id
@Column(name = "ID")
private String id;
/**
* 필드를 컬럼에 매핑한다. 여기서는 name 속성을 사용해서 Member 엔티티의 username 필드를
* Member 테이블의 NAME 컬럼에 매핑했다.
*/
@Column(name = "NAME")
private String username;
/**
* age 필드에는 매핑 어노테이션이 없다. 이렇게 매핑 어노테이션을 생략하면 필드명을 사용해서 컬럼명으로 매핑한다.
* 여기서는 필드명이 age이므로 age 컬럼으로 매핑했다. 참고로 책에서는 데이터베이스가 대소문자를 구분하지 않는다고
* 가정했다. 만약 대소문자를 구분하는 데이터베이스를 사용하면 @Column(name="AGE") 처럼 명시적으로 매핑해야 한다.
*/
// 매핑 정보가 없는 필드
private Integer age;
}
이제는 테스트 코드를 만들어서 정상 동작을 확인해보자
import jpabook.start.Member;
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.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest // JPA에 관련된 요소들만 테스트하기 위한 어노테이션으로 JPA 테스트에 관련된 설정들만 적용해준다.
/**
* @DataJpaTest로 테스트하면 @Transactional 어노테이션이 기본으로 적용되어 있어서 무조건 롤백이 되므로,
* 롤백이 안되게 false 처리 해준다.
*/
@Rollback(false)
/**
* AutoConfigureTestDatabase.Replace.ANY는 임베디드 DB를 바라보고,
* AutoConfigureTestDatabase.Replace.NONE은 설정한 값에 따라 DB 소스가 적용된다.
*/
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ExtendWith(SpringExtension.class) // JUnit5 를 사용하기 위해 추가한다.
public class JpaTest {
@Autowired
private TestEntityManager testEntityManager; // 테스트에서 사용되는 EntityManager
@Test
public void 테스트() {
String id = "id1";
String userName = "지한";
int age = 2;
Member member = new Member();
member.setId(id);
member.setUsername(userName);
member.setAge(age);
// 등록
testEntityManager.persist(member);
// 수정
age = 20;
member.setAge(age);
// 한 건 조회
Member findMember = testEntityManager.find(Member.class, id);
/**
* 하나 이상의 회원 목록을 조회
* 여러 데이터를 조회하려면 검색 조건이 포함된 SQL을 사용해아 한다.
* JPA는 JPQL(Java Persistence Query Language) 이라는, SQL을 추상화한 객체지향 쿼리 언어로 이런 문제를 해결한다.
* JPQL은 SQL과 문법이 거의 유사하다.
* JPQL은 엔티티 객체를 대상으로 쿼리한다. 쉽게 이야기해서 클래스와 필드를 대상으로 쿼리한다.
* SQL은 데이터베이스 테이블을 대상으로 쿼리한다.
*/
// testEntityManager는 createQuery() 가 없어서 getEntityManager() 로 EntityManager를 가져온다.
EntityManager entityManager = testEntityManager.getEntityManager();
// 해당 쿼리가 JPQL이다. 참고로 여기서 Member는 엔티티 객체를 말한다. MEMBER 테이블이 아니다. JPQL은 데이터베이스를 전혀 알지 못한다.
TypedQuery<Member> query = entityManager.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
// 조회한 값 검증
assertThat(findMember.getId()).isEqualTo(id); // id 체크
assertThat(findMember.getUsername()).isEqualTo(userName); // userName 체크
assertThat(findMember.getAge()).isEqualTo(age); // age 체크
assertThat(members).hasSize(1); // 조회한 list size 체크
}
@Test
public void 테스트_롤백() {
Member findMember = testEntityManager.find(Member.class, "id1");
testEntityManager.remove(findMember);
}
}
테스트() 코드를 실행하면 “id1” id를 가진 레코드가 생성되고,
테스트_롤백() 코드를 실행하면 “id1” id를 가진 레코드가 삭제되는 것을 확인할 수 있다.
책에는 이클립스를 사용하다보니 EntityManagerFactory와 EntityManager의 생성에 대해 주의하는 글이 있었다.
EntityManagerFactory는 JPA 구현체에 따라서는 데이터베이스 커넥션 풀도 생성하므로 엔티티 매니저 팩토리를 생성하는 비용이 아주 크기 때문에
애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용해야 한다고 한다.
EntityManager는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드간에 공유하거나 재사용하면 안 된다고 한다.
책 예제를 Spring Boot 프로젝트로 작성하다보니, EntityManager를 바로 받아와서 EntityManagerFactory의 생성 이슈는 없었는데
EntityManager는 음… 개인 프로젝트때 스레드간에 공유를 할일이 있었는데…(기본 스레드와 스케줄러 스레드가 같은 EntityManager Bean을 바라봄)
이럴 경우 정확히 어떤 문제가 있다는 건지 이해되지 않는다.
이부분은 좀 더 고민하고 찾아봐야될 부분일 것 같다.
그리고 책에는 EntityManagerFactory와 EntityManager의 종료처리와
EntityTransaction 으로 커밋, 롤백 처리도 중요하게 언급하고 있다.
몇번 코드를 작성해봤지만 에러가 나서 잘 되지 않았다.
다음 장들에서도 계속 다루곤 하니까 다시 한번 적용시켜보도록 하겠다.
마지막으로 @SpringBootTest 와 @DataJpaTest 의 차이점에 대해 궁금하여 찾아봤는데
불러오는 설정들의 차이가 있겠지만 가장 눈에 띄는건 로그인 것 같다.
@SpringBootTest 로 쿼리 테스트를 하면 쿼리를 로그로 확인할 수 있는데
@DataJpaTest 로 쿼리 테스트를 하면 쿼리를 로그로 확인할 수 없다.
이유는 간단했다. @DataJpaTest 는 @Transactional 을 기본적으로 포함해서 롤백이 되다보니 출력하지 않는 것이였다.
하여 위 예제에서는 @Rollback(false) 를 사용하다보니 @DataJpaTest 를 사용하더라도 쿼리를 확인할 수 있다.
Leave a comment