[Spring Boot] 03장. 스프링 부트에서 JPA로 데이터베이스 다뤄보자 - JPA
🌱 이번 장의 스터디 범위
JPA / Hibernate / Spring Data Jpa의 관계
Spring Data Jpa를 이용하여 관계형 데이터베이스를 객체지향적으로 관리하는 방법
🌱 JPA 소개
객체를 관계형 데이터베이스에서 관리하는 것이 중요
관계형 데이터베이스가 SQL만 인식할 수 있어 프로젝트의 대부분이 SQL로 채워지며 SQL을 피할 수 없음
반복적인 SQL을 만드는 것을 통해 단순 반복 작업의 문제가 생김
어떻게 데이터를 저장할지에 대해패러다임 불일치 문제가 생김
관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 달라 객체를 데이터베이스에 저장하려고 하니 문제 발생
JPA는 지향하는 바가 다른 객체지향 프로그래밍과 관계형 데이터베이스의 중간에서 패러다임을 일치시켜주기 위한 기술
개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행
개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 더는 SQL에 종속적인 개발을 하지 않아도 됨
🌱Spring Data JPA
인터페이스인 JPA를 사용하기 위해서는 구현체가 필요
구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA 모듈을 이용하여 JPA 기술을 다룸
JPA ← 구현체 Hibernate ← Spring Data JPA
Spring Data JPA가 등장한 이유 1. 구현제 교체의 용이성 : Hibernate 외에 다른 구현체 (Eclipse, Link 등)로 교체 가능
Spring Data JPA가 등장한 이유 2. 저장소 교체의 용이성 : 관계형 데이터베이스 외에 다른 저장소 (MongoDB 등)로 교체 가능
Spring Data의 하위 프로젝트들의 기본적인 CRUD는 같기 때문에 의존성 등만 교체하면 쉽게 교체 가능
🌱 요구사항 분석
3장에서 6장까지 하나의 게시판 제작
게시판 기능 - 게시글 조회, 등록, 수정, 삭제
회원 기능 - 구글/네이버 로그인, 로그인한 사용자 글 작성 권한, 본인 작성 글에 대한 권한 관리
🌱 프로젝트에서 Spring Data JPA 적용하기
build.gradle 의존성 등록
// build.gradle
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.projectlombok:lombok')
/* 스프링 부트용 Spring Data JPA 추상화 라이브러리
스프링 부트 버전에 맞춰 다음으로 JPA 관련 라이브러리 버전 관리 */
compile('org.springframework.boot:spring-boot-starter-data-jpa')
/* 인메모리 관계형 데이터베이스
별도의 설치 없이 프로젝트 의존성만으로 관리 */
compile('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
domain 패키지 생성 : 도메인을 담을 패키지
도메인이란 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역
domain 패키지에 posts 패키지와 Posts 클래스 생성
domain.posts - Posts.java
Posts 클래스 작성
필수 (주요) 어노테이션인 @Entity를 롬복의 어노테이션보다 클래스에 가깝게 작성
JAVA 외의 다른 언어로 전환 시 롬복이 필요 없으므로 쉽게 삭제 가능
Posts 클래스는 실제 DB의 테이블과 매칭될 클래스로 Entity 클래스라고도 함
JPA를 사용하면 DB 데이터에 실제 쿼리를 날리기보다는 Entity 클래스의 수정을 통해 작업
롬복은 서비스 초기 구축 단계에서 테이블 설계가 빈번하게 변경될 때 코드 변경량을 최소화시켜줌
// Posts.java
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Getter // 롬복 어노테이션 - 클래스 내의 모든 필드의 Getter 메소드를 자동생성
@NoArgsConstructor // 롬복 어노테이션 - 기본 생성자 자동 추가, public Posts() {}와 같은 효과
@Entity // JPA 어노테이션 - 테이블과 링크될 클래스임을 나타냄
public class Posts {
@Id // JPA 어노테이션 - 해당 테이블의 PK (기본키) 필드
@GeneratedValue(strategy = GenerationType.IDENTITY) // JPA 어노테이션 - PK의 생성 규칙
private Long id;
@Column(length = 500, nullable = false) // JPA 어노테이션 - 테이블의 칼럼
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String author;
@Builder // 롬복 어노테이션 - 해당 클래스의 빌더 패턴 클래스 생성
public Posts(String title, String content, String author) {
this.title = title;
this.content = content;
this.author = author;
}
}
Entity 클래스에서는 절대 Setter 메소드를 사용하지 않고 대신 목적과 의도를 나타낼 수 있는 메소드를 사용
Setter가 없는 대신 생성자를 통해 DB에 삽입하거나 해당 이벤트에 맞는 public 메소드를 호출하여 변경
이 책에서는 생성자 대신 @Builder를 통해 제공되는 빌더 클래스 사용
채워야 할 필드가 무엇인지 명확히 지정할 수 없는 생성자와 달리 빌더는 어느 필드에 어떤 값을 채워야할지 명확하게 인지 가능
// 잘못된 사용 예 - JPA에서는 Setter 메소드 사용 X
public class Order {
public void setStatus(boolean status) {
this.status = status
}
}
public void 주문서비스의_취소이벤트() {
order.setStatus(false);
}
// 올바른 사용 예 - 목적과 의도를 나타낼 수 있는 cancelOrder 메소드 추가 O
public class Order {
public void cancelOrder(boolean status) {
this.status = status
}
}
public void 주문서비스의_취소이벤트() {
order.cancelOrder(false);
}
JpaRepository인 PostRepository 인터페이스 생성 : Posts 클래스로 Database에 접근하게 해주는 인터페이스
JPA의 Repository는 DB Layer 접근자
Entity 클래스와 기본 Entity Repository는 함께 위치해야 하므로 도메인 패키지에서 함께 관리
// PostsRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
// JpaRepository<Entity 클래스, PK 타입>을 상속 - CRUD 메소드 자동 생성
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
🌱 Spring Data JPA 테스트 코드 작성하기
domain 패키지에 posts 패키지와 PostsRepositoryTest 클래스 생성
domain.posts - PostsRepository.java
save, findAll 기능을 테스트하는 PostsRepositoryTest 클래스
// PostsRepositoryTest.java
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest() // H2 데이터베이스(인모메리 관계형 데이터베이스)를 자동으로 실행
public class PostsRepositoryTest {
@Autowired
PostRepository postRepository;
// Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정, 테스트간 데이터 침범을 막음
@After
public void cleanup() {
// 다음 테스트 실행 시를 위해 데이터 삭제
postRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기() {
// given
String title = "테스트 게시글";
String content = "테스트 본문";
/* 테이블 posts에 insert/updata 쿼리를 실행
id 값이 있다면 update, 없다면 insert 쿼리 실행 */
postRepository.save(Posts.builder()
.title(title)
.content(content)
.author("wn8925@sookmyung.ac.kr")
.build());
// when
// 테이블 posts에 있는 모든 데이터를 조회해오는 메소드
List<Posts> postsList = postRepository.findAll();
// then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
PostsRepositoryTest 테스트 실행 및 통과 확인
application.properties 파일로 실제로 실행된 쿼리인 쿼리 로그를 콘솔에서 확인하기