프로젝트 생성
- Spring boot
- IntelliJ-ultimate 버전이 아니라면 스프링 부트 스타터를 이용 (https://start.spring.io/)
- 사용 기능(라이브러리); 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());
}
}
}