천천히, 한결같이

[Spring] 프로젝트에 Spring Data JPA 적용하고 테스트 코드 작성하기 본문

Spring

[Spring] 프로젝트에 Spring Data JPA 적용하고 테스트 코드 작성하기

Donghwan Lee 2021. 12. 27. 16:17

이동욱 님의 스프링 부트와 AWS로 혼자 구현하는 웹 서비스책을 공부하며 정리한 내용입니다. 틀린 정보가 있을 수 있으니 주의하시고 댓글로 남겨주시길 바랍니다.

Spring Data JPA, H2 database 추가하기

H2 database 를 사용하기 위해, 우선 build.gradle 에 다음과 같이 의존성을 추가합니다

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.projectlombok:lombok')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('com.h2database:h2')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

   의존성 등록 이후 com.hwanld.book.springboot에다가 domain이라는 새로운 패키지를 만듭니다. 여기서 domain도메인을 담을 패키지입니다. 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역이 여기에 해당합니다.

Entity 클래스 만들기이후 domain패키지에 post패키지과 Posts클래스를 만듭니다. Posts클래스는 다음과 같습니다.

package com.hwanld.book.springboot.domain.post;

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;
    }
}

아래 에노테이션들은 JPA에서 제공해주는 에노테이션입니다.

@Entity : DB의 테이블과 링크될 클래스임을 나타냅니다. 기본값으로 클래스의 가멜케이스 이름을 언더스코어 네이미이으로 테이블 이름을 매칭합니다. (ex: SalesManager.java -> sales_manager table)

@id : 해당 테이블의 PK 필드를 나타냅니다.

@GeneratedValue : PK의 생성 규칙을 나타냅니다. Springboot 2.0 에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 됩니다.

@Column : 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 됩니다. 기본값 외게 추가로 변경이 필요한 옵션이 있으면 사용 합니다. 예를 들어 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶을 때가 이에 해당합니다.

아래 에노테이션들은 lombok에서 제공해주는 에노테이션입니다.

@NoArgsConstructor : 기본 생성자 자동 추가, public Posts() {}와 같은 효과입니다.

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

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

 

​   주요 에노테이션을 클래스에 가깝게 정렬하면 이후 다른 언어로의 전환 등에 있어서 많은 이점을 가질 수 있습니다. 예를 들어 위 코드에서 @Getter, @NoArgsConstructor는 롬복의 에노테이션으로, 개발을 함에 있어 코드를 단순화 해주는 등 많은 이점을 가져다 주지만 필수는 아닙니다. 하지만 @Entity에노테이션은 JPA 에노테이션으로, 없어서는 안되는 에노테이션이기 때문에 클래스와 가장 인접하게 둡니다.

​   Posts 클래스는 실제 DB의 테이블과 매칭될 클래스이며 보통 이를 Entity 클래스라고 합니다. JPA를 사용하게 되면 DB 데이터에 작업을 할 때 실제 쿼리를 날리기 보단 이 Entity 클래스의 수정을 통해 작업을 할 수 있게 됩니다.

​   Entity의 PK는 Long 타입의 (MySQL에선 bigint 타입) Auto_increment를 사용하는 것이 적절합니다. 유니크 키나 여러 키를 조합한 복잡한 키를 PK로 잡을 경우 난감한 상황이 종종 발생합니다. 따라서 주민등록번호, 복합키 등은 유니크 키로 별도로 추가하는게 좋습니다.

​   또한 Posts 클래스에는 Setter 메소드가 없습니다. 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분되지 않아서, 차후 기능 변경 시 정말 복잡해집니다. 그래서 Entity 클래스에는 절때 Setter를 구현하지 않습니다. 이를 대신하기 위해 해당 필드의 값 변경이 필요한 경우 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야만 합니다.

   Setter가 없는 상황에서 DB에 값을 삽입할 때 기본적인 구조는 생성자를 통해 최종값을 채운 후 DB에 삽입하는 것이며, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경합니다. 생성자를 대신해서 @Builder를 통해 제공되는 빌더 클래스를 사용하는 방법도 있습니다. 두 방법의 차이는 아래와 같습니다.

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

//Builder Class
Example.builder()
    .a(a)
    .b(b)
    .build();

​   위와 같이 빌더 패턴을 사용하게 되면 어느 필드에 어떤 값을 채워야 할지 명확하게 알 수 있습니다. 반면 생성자를 사용하게 되면 값이 채워지는 필드의 순서를 정확하게 알고 있지 못하면 직접 실행 전에는 모를 가능성이 높기 때문에 빌더 패턴에 익숙해지면 더욱 좋습니다.

JpaRepository 생성하기

​ 위에서 만들었던 Posts 클래스로 Database를 접근하게 해줄 JpaRepository interface를 domain.posts/PostsRepository에 생성합니다.

package com.hwanld.book.springboot.domain.post;

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

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

​   보통 ibatis나 MyBatis에서 Dao라고 불리는 DB Layer 접근자입니다. JPA에선 Repository라고 부르며, 인터페이스로 생성합니다. 인터페이스를 생성 후 JPARepository <Entity 클래스, PK 타입> 를 상속하면 기본적인 CRUD 메소드가 자동으로 생성됩니다.

​   @Repository를 추가할 필요도 없습니다. 하지만 반드시 Entity 클래스와 기본 Entity Repository는 항상 함께 위치해야 합니다. Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수가 없습니다.

​   나중에 프로젝트 규모가 커져 도메인별로 프로젝트를 분리해야 한다면 이때 Entity 클래스와 기본 Repository는 함께 움직여야 함으로 도메인 패키지에서 함께 관리합니다.

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

​   test디렉토리에 domain.posts 패키지를 생성하고, 테스트 클래스는 PostsRepositoryTest란 이름으로 생성하고 아래 클래스를 추가합니다.

package com.hwanld.book.springboot.domain.post;

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("wkazxf@ajou.ac.kr")
                .build());

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

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

@After : JUnit에서 단위 테스트가 끝날 때 마다 수행되는 메소드를 지정. 하나의 테스트가 끝날 때 마다 데이터베이스에 남아있는 값을 Clean해주는 기능이라고 생각하면 좋습니다.

postsRepository.save : 테이블 posts에 insert/update 쿼리를 실행합니다. id에 값이 있다면 update, 없다면 insert 쿼리가 실행됩니다.

postsRepository.findAll : 테이블 posts에 있는 모든 데이터를 조회해오는 메소드입니다.

실제 실행된 쿼리 볼 수 있도록 설정하기

  ​ 실제 실행된 쿼리를 보기 위해선 여러 가지 방법이 있을 수 있는데, 스프링 부트에서는 application.properties, application.yml 등의 파일로 한 줄의 코드로 설정할 수 있도록 지원하고 권장하고 있습니다.

src/main/resources 디렉토리 아래에 application.properties 파일을 생성하고, 아래 속성을 추가해줍니다.

spring.jpa.show_sql=true

​   현재 프로젝트에서는 H2를 사용해서 H2 쿼리로 쿼리를 확인할 수 있습니다. 하지만 앞으로의 범용성을 위해서, MySQL의 쿼리로 보이도록 하겠습니다. H2에서는 MySQL의 쿼리를 수행해도 정상적으로 작동하기 때문에 이후 디버깅을 위해서 출력되는 쿼리 로그를 MySQL 버전으로 변경하겠습니다.

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
Comments