JPA 소개

현대 웹 애플리케이션에서 Oracle, MySQL, MSSQL 등의 RDB를 쓰지 않은 경우가 드물다. 따라서, 객체를 관계형 데이터베이스에서 관리하는 것이 중요하다.
하지만, 기존 iBatis, MyBatis 등의 SQL Mapper로 쿼리를 매핑하는 방식은 아래와 같은 문제점이 있었다.
관계형 데이터베이스는 SQL만 인식할 수 있기 때문에, 각 테이블마다 CRUD SQL을 매번 생성하다보면 SQL의 비중이 높아진다. 그에 따라 유지보수의 어려움이 있다.
또한, 관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 서로 다름으로 인한 여러 문제가 발생한다. 상속, 1:N 등 다양한 객체 모델링을 데이터베이스로는 구현할 수 없다.
이러한 문제점을 해결하기 위해 JPA 가 등장하였다.

Spring Data JPA

JPA는 인터페이스로서 자바 표준명세서이다.
따라서, 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요하다. 대표적으로 HIbernate, Eclipse Link 등이 있다.
하지만, Spring에서 JPA를 사용할 때 구현체들을 직접 다루지 않는다.
구현체들을 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA를 다룬다.

JPA <= Hibernate <= Spring Data JPA

 

Spring Data JPA 등장 이유

1. 구현체 교체의 용이성
Hibernate 외에 다른 구현체로 쉬게 교체하기 위하여 Spring Data JPA를 사용한다. 기술의 변화함에 따라 구현체를 교체해야할 때, Spring Data JPA 내부에서 구현체 매핑을 지원해 줌으로 쉽게 교체가 가능하다.
2. 저장소 교체의 용이성
Spring data JPA에서 DB의 의존성만 교체하면 쉽게 DB 교체가 가능하다. Spring Data 하위의 프로젝트들은 기본적인 CRUD 인테페이스(save(), findAll, finOne() 등)가 같이 때문에, 저장소가 교체되어도 기본적인 기능은 변경할 것이 없어 교체가 쉽다.

프로젝트에 Spring Data Jpa 적용하기

build.gradle에 spring-boot-starter-data-jpa, h2 의존성 등록

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.projectlombok:lombok')
    implementation('org.springframework.boot:spring-boot-starter-web') // 추가
    implementation('org.h2database:h2') // 추가
    annotationProcessor('org.projectlombok:lombok') 
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}

spring-boot-starter-data-jpa

- 스프링 부트용 Spring Data Jpa 추상화 라이브러리
- 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리 버전을 관리

h2

- 인메모리 관계형 데이터베이스로 별도의 설치없이 프로젝트 의존성만으로 관리 가능
- 메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용
- JPA의 테스트, 로컬 환경에서의 구동에 사용

DB 테이블과 매칭될 Entity 클래스 작성

기존에 MyBatis같은 쿼리 매퍼의 경우 dao 클래스에서 오로지 xml 쿼리의 결과만 담던일을 Entity 클래스에서 모두 해결

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
@NoArgsConstructor
@Entity
public class Posts {

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

    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder
    public Posts(String title, String Contnet, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

}

@Entity

- 테이블과 링크될 클래스임을 나타낸다.
- 기본값으로 클래스와 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름을 매칭한다.
ex) SalesManager.java -> sales_manager table

@id

- 해당 테이블의 PK 필드를 나타낸다.

@GeneratedValue

- PK의 생성 규칙을 나타낸다.
- 스프링 부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 된다.
!! 웬만하면 Entity의 PK는 Long 타입의 Auto_increment를 추천!!

@Column

- 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다.
- 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
ex) 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶거나, 타입을 TEXT로 변경하고 싶은 경우 등에 사용

@NoArgsConstructor

- 기본 생성자 자동 추가
- public Post(){}와 같은 효과

@Getter

- 클래스 내 모든 필드의 Getter 메소드 자동 생성

@Builder

- 해당 클래스의 빌더 패턴 클래스를 생성
- 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함

DB Layer 접근자 Repository 생성

ibatis나 MyBatis 등에서 Dao라고 불리는 DB Layer 접근자를 JPA에선 Repository라고 부르며 인터페이스로 생성한다.

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {
    
}

!!Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다.!!
단순히 페이스를 생성 후, JpaRepository<Entity 클래스, PK 타입> 를 상속하면 기본적인 CRUD 메소드가 자동으로 생선된다.

위의 Entity 클래스와 기본 Repository는 도메인별로 함께 움직여야 하므로 동일한 도메인 패키지에서 함께 관리한다.

Spring Data JPA 테스트 코드 작성

import com.choee.service.springboot.domain.posts.Posts;
import com.choee.service.springboot.domain.posts.PostsRepository;
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.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @After
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test
    public void 게시글저장_불러오기() {
        //given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        // 빌더를 활용한 빌드 패턴
        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("rhksh99@gmail.com")
                .build());

        //when
        List<Posts> postsList = postsRepository.findAll();

        //then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getAuthor()).isEqualTo(content);
    }
}

@After

- Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정
- 보통은 배포 전 전체 테스트를 수핼할 때 테스트간 데이터 침범을 막기위해 사용된다.
- 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트실행시 테스트가 실패할 수 있다. 따라서 위 코드에서는 cleanup()을 수행하도록 작성되었다.

@postRepository.save

- 테이블 posts에 insert/update 쿼리를 실행
- id 값이 있다면 update가, 있다면 insert 쿼리가 실행된다.

@postRepository.findAll

- 테이블 posts에 있는 모든 데이터를 조회해오는 메소드이다.

@SpringBootTest

- 별다른 설정 없이 H2 데이터베이스를 자동으로 실행해 준다.

 

실행된 쿼리 확인하는 법

스프링 부트에서는 application.properties, application.yml 등의 파일로 한 줄의 코드로 설정할 수 있도록 지원하고 이를 권장한다.
1. src/main/resources 하위에 application.properties 파일 생성 및 옵션 추가

spring.jpa.show-sql=true

2. 콘솔에서 쿼리 로그 확인

+ Recent posts