[JPA] 프로젝트에 Spring Data JPA 적용하기

Kaite.Kang·2022년 10월 5일
0
post-thumbnail

* 목표

프로젝트 요구사항에 맞게 도메인을 구성하고, JPA 와 연동하여 데이터가 데이터베이스에 저장한다.
마지막으로 테스트 코드를 작성하여 구성한 코드가 올바르게 동작하는지 확인한다.

1. 전체 프로젝트 요구 사항

  1. 게시판 기능
    • 게시글 조회
    • 게시글 등록
    • 게시글 수정
    • 게시글 삭제
  1. 회원 기능
    • 구글 / 네이버 로그인
    • 로그인한 사용자 글 작성 권한
    • 본인 작성 글에 대한 권한 관리

2. 환경

  • Spring boot : 2.6.3
  • gradle : 7.2
  • IntelliJ IDEA : 2021.03
  • Junit : 4.13.2

3. JPA 란?

프로젝트에 들어가기 앞서 JPA가 무엇인지, 어떤 장점이 있는지 알아보자.

  • JPA(Java Persistence API)
    • Java 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용하는 인터페이스 모음
      그 말은 즉, 실제적으로 구현된것이 아니라 구현된 클래스와 매핑을 해주기 위해 사용되는 프레임워크이다.
    • 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다.
    • JPA를 구현한 대표적인 오픈소스로는 Hibernate가 있다.

1) JPA의 필요성

현대의 웹 에플리케이션에서 데이터를 저장하기 위해 관계형 데이터베이스는 필수 요소이다.
관계형 데이터베이스 제품으로 Oracle, MySQL, MSSQL 등이 있다.
객체를 관계형 데이터베이스에서 관리하는 것은 중요하다.

(1) 단순 반복 작업을 줄일 수 있다.
관계형 데이터베이스는 SQL만 인식할 수 있는데, 각 테이블마다 기본적인 CRUD SQL을 생성해야 한다.
그래서 SQL로 프로젝트를 만들다보면 애플리케이션 코드보다 SQL이 더 가득한 프로젝트가 될 수 있다.
이러한 단순 반복작업을 해야하는 면에서 SQL을 직접 사용하는 것보다 JPA를 사용하는 것이 더 효율적이다.

(2) JPA는 객체지향 언어와 패러다임이 일치하여 객체를 데이터로 다루기 편리하다
관계형 데이터베이스와 객체지향 프로그래핑 언어는 패러다임이 불일치하는 문제가 있다.
패러다임이 서로 다른데 객체를 데이터베이스에 저장하려고 하면 여러 문제가 발생할 수 있다.
관계형 데이터베이스는 어떻게 데이터를 저장할지에 초점이 맞춰진 기술이고,
객체지향 프로그래밍 언어는 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술이다.

객체 지향의 특징인 상속, 1:N 등의 다양한 객체 모델링을 관계형 데이터베이스로 표현하기는 어려운데, 그 이유가 서로 패러다임이 다르기 때문이다.

JPA는 이러한 문제를 해결하기 위해 등장하였다.

즉, 개발자는 객체지향적 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행한다. 개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 더는 SQL에 종속적인 개발을 하지 않아도 된다.

이쯤에서 JPA 설명을 마치고, 이후 다른 포스팅에서 JPA에 대해 더 자세히 다루어 보아야겠다.

 

4. 프로젝트에 Spring Data JPA 적용하기

책이 3년 전(2019년)에 집필된 책이라 라이브러리 버전이 많이 업데이트 되었는데 build.gradle에 대한 내용은 책의 저자인 이동욱님의 블로그(https://jojoldu.tistory.com/539?category=717427)를 참고하였다.

1) build.gradle에 의존성 라이브러리 등록한다.

dependencies {
	implementation('org.springframework.boot:spring-boot-starter-data-jpa') //(1)
    implementation("org.mariadb.jdbc:mariadb-java-client") //(2)
    implementation('com.h2database:h2') //(3)
}

(1) pring-boot-starter-data-jpa

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

(2) mariadb-java-client

  • java와 mariadb를 연결하기 위해서는 jdbc 라이브러리가 필요

(3) h2

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

2) Entity 클래스 생성

JPA 기능을 사용해보기 위해 도메인을 생성해보자.

A. 도메인이란

여기서 도메인이란 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역이라고 생각하면 된다.

B. Posts 클래스

domain 패키지를 생성하여 도메인을 따로 분류하였다.
domain 패키지에 posts 패키지와 Poists 클래스를 만든다.
Posts 클래스는 실제 DB 테이블과 매칭될 클래스이며 보통 Entity 클래스라고 한다.
JPA를 사용하면 DB 데이터에 작업할 경우 시제 쿼리를 날리기보다는, 이 Entity 클래스의 수정을 통해 작업한다.

package com.spring.book.domain.posts;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@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 content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }

}

A) JPA에서 제공하는 어노테이션

  • @Entity
    • JPA 어노테이션
    • 테이블과 링크될 클래스임을 나타냄
    • 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.
    • ex) SalesManager.java → sales_manager table
  • @Id
    • 해당 테이블의 PK필드를 나타낸다.
  • @GeneratedValue
    • PK의 생성 규칙을 나타낸다.
    • 스프링부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment 가 된다.
  • @Column
    • 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다.
    • (사용 목적) 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
      • 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶거나, 타입을 TEXT로 변경하고 싶을 때의 상황에 사용된다.

Entity의 PK(Primary Key)는 Long 타입의 auto_increment를 추천한다.(MySQL 기준으로 이렇게 하면 bigint 타입이 된다.)
주민등록번호화 같이 비즈니스상 유니크 키나, 여러 키를 조합한 복합키로 PK를 잡을 경우 다음과 같이 난감한 상황이 종종 발생한다.
(1) FK(Foreign Key)를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야하는 상황이 발생한다.
(2) 인덱스에 좋은 영향을 끼치지 못한다.
(3) 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생한다.
→ 따라서 주민등록번호, 복합키 등은 유니크 키로 별로로 추가하시는 것을 추천한다.

B) 롬복 라이브러리의 어노테이션

서비스 초기 구축 단계에서는 테이블 설계(여기선 Entity 설계)가 빈번하게 변경되는데, 이때 롬복의 어노테이션들은 코드 변경량을 최소화시켜주기 때문에 적극적으로 사용하는 것이 좋다.

  • @NoArgsConstructor
    • 기본 생성자 자동 추가
    • public Posts(){}와 같은 효과
  • @Getter
    • 클래스 내 모든 필드의 Getter 메소드를 자동 생성
  • @Builder
    • 해당 클래스의 빌더 패턴 클래스를 생성
    • 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함

C) Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다.

(문제점) 그 이유는 getter/setter 를 무작정 생성하면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없어, 차후 변경이 정말 복잡해지기 때문이다.

(해결책) 생성자를 통해 최종값을 채운 후 DB에 삽입하고, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다.

이 프로젝트에서는 생성자 대신에 @Builder 를 통해 제공되는 빌더 클래스를 사용하며, 이 방법을 더 권장한다.

D) 생성자 대신 빌더 클래스를 사용해야 하는 이유

  • 공통점

생성자와 빌더의 목적은 같다.
생성자나 빌더나 는 생성 시점에 변수에 값을 채워주는 역할을 한다.

  • 차이점

빌더는 채워야 할 필드가 무엇인지 명확히 알 수 있다.

생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다.

예를 들어 아래와 같은 Example() 생성자가 있다면 개발자가 a와 b의 위치를 변경해도 코드를 실행하기 전까지는 문제를 찾기 어렵다.

public Example(String a, String b){
	this.a = a;
	this.b = b;
}

하지만 빌더를 사용하게 되면 다음과 같이 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.

Example.builder()
	.a(a)
	.b(b)
	.build();

 

3) Posts 클래스로 Database를 접근하게 해줄 JpaRepository 생성

package com.spring.book.domain.posts;

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

public interface PostsRepository extends JpaRepository<Posts, Long> {
}
  • Repository 란
    - DB Layer 접근자
    - 보통 ibatis나 MyBatis 등에서 Dao라고 불리며, JPA에서는 Repository라고 부르고 인터페이스로 생성한다.
    - 단순히 인터페이스를 생성 후, JpaRepository<Entity클래스, PK타입>를 상속하면 기본적인 CURD 메소드가 자동으로 생성된다.
    - (주의) Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다.
    둘은 아주 밀접한 관계이고, Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수 가 없다.
    - 나중에 프로젝트 규모가 커져 도메인 별로 프로젝트를 분리해야 한다면 이때 Entity 클래스와 기본 Repository는 함께 움직여야 하므로 도메인 패키지에서 함께 관리해야 한다.
            

4) Spring Data JPA 테스트 코드 작성하기

PostRepositoryTest에서는 save, findAll 기능을 테스트한다.

package com.spring.book.domain.posts;

import org.junit.jupiter.api.AfterEach;
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
public class PostRepositoryTest {
    @Autowired
    PostsRepository postRepository;

    @AfterEach
    public void cleanup(){
        postRepository.deleteAll();
    }

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

        postRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("kjh@gmail.com")
                .build());

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

        //then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }
}
  • @After
    • Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정한다.
    • 보통은 배포 전 전체 테스트를수행할 때 테스트 간 데이터 침범을 막기 위해 사용한다.
    • 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트가 실패할 수 있다.
  • postRepository.save
    • 테이블 posts에 insert/update 쿼리를 실행한다.
    • id값이 있다면 update가, 없다면 insert 쿼리가 실행된다.
  • postRepository.findAll
    • 테이블 posts에 있는 모든 데이터를 조회해오는 메소드
  • @SpringBootTest
    • 전체 어플리케이션을 로드하여 Spring을 테스트하는 방법으로 애플리케이션에 주입된 전체 Bean을 가져온다. 그 외에 테스트 범위에 따라 다양한 방법을 적용할 수 있다. (참고[1])
    • 별다른 설정없이 @SpringBootTest를 사용할 경우, H2 데이터베이스를 자동으로 실행해준다.
  • @RunWith(SpringRunner.class)
    • Spring Boot 테스트 기능과 JUnit 간의 브리지를 제공하는 데 사용된다. JUnit 테스트에서 Spring Boot 테스트 기능(@Autowire@MockBean등) 을 사용할 때마다 이 주석이 필요하다.
    • SpringBoot 2.1 부터는 @RunWith(SpringRunner.class)이 없어도 잘 동작한다.
      • springBoot 2.1 버전부터는 Junit 버전이 5.x 로 변경되었고 @RunWith 이 @ExtendWith 로 변경되었다. 그리고 해당 어노테이션의 기능은 @SpringBootTest 어노테이션에 포함되도록 변경되었기 때문에 @ExtendWith를 별도로 정의할 필요가 없다.
      • 단, @RunWith(SpringRunner.class)을 사용하려면 Junit4라이브러리인 import org.junit.Test; 을 사용해야 한다.
        Junit5 라이브러리는 import org.junit.jupiter.api.Test; 를 사용한다.
    • (참고) https://findmypiece.tistory.com/173

A. (트러블슈팅) Junit 라이브러리 버전 에러

(에러문) org.junit.runners.model.InvalidTestClassError

(해결) import org.junit.jupiter.api.Test;import org.junit.Test;

(원인) Junit5 라이브러리를 사용하였고, Junit5에서는 @Runwith을 지원하지 않는다.

JUnit4를 사용중이라면 org.junit.jupiter.api.Test이 아니라 import org.junit.Test; 으로 변경해야 한다.

org.junit.jupiter.api.Test는 JUnit5 를 사용한다.

JUnit5는 @RunWith 어노테이션을 지원하지 않는다. 대신 @ExtendWith 어노테이션을 지원한다.

(참고) https://mr-popo.tistory.com/m/40

 

5) JPA를 통해 실행된 쿼리문 확인하기

스프링 부트에서는 application.properties, application.yml 등의 파일에서 코드로 설정할 수 있도록 지원한다.

  1. src/main/resources 디렉토리 아래에 application.properties 파일을 생성하자.
  • application.properties
pring.jpa.show_sql=true
  • 생성 방법

 
2. 쿼리 로그 확인

PostRepositoryTest 코드 실행시 콘솔에 쿼리 로그가 출력된 것을 확인 할 수 있다.

여기서 create table 쿼리를 보면 id bigint generated by default as identity 라는 옵션으로 생성된다.

그 이유는 H2의 쿼리 문법으로 적용되었기 때문이다.

H2는 MySQL 쿼리를 수행해도 정상적으로 작동하기 때문에 이후 디버깅을 위해서 출려고디는 쿼리 로그를 MySQL 버전으로 변경해보자.

  1. MySQL5InnoDBDialect 설정 추가
  • application.properties
//쿼리 로그 출력
spring.jpa.show_sql=true

//MySQL 버전으로 쿼리 로그 출력
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.datasource.hikari.jdbc-url=jdbc:h2:mem://localhost/~/testdb;MODE=MYSQL

스프링부트 버전이 올라가면서 책에서 안내한 MySQL5InnoDBDialect 설정값이 Deprecated되었다.
스프링부트 2.1.10 버전 이후로는 위 설정값을 추가하면된다.

*** 해당 내용은 아래 링크를 참고하였다.
https://github.com/jojoldu/freelec-springboot2-webservice/issues/67

  1. 쿼리 로그 확인
    MySQL 로그로 잘 출력되는 것을 확인 할 수 있다.

참고

도서 - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스

0개의 댓글