본문 바로가기

스프링부트와 AWS로 구현하는 웹서비스

[스프링부트/AWS] 3장 스프링 부트에서 JPA로 데이터베이스 다뤄보자(1/3) - JPA

스프링을 배울 때는 Mybatis와 같은 SQL매퍼(Mapper)를 이용해서 데이터베이스의 쿼리를 작성했습니다. 그러다 보디 실제로 개발하는 시간보다 SQL을 다루는 시간이 더 많았습니다.

  관계형 데이터베이스를 이용하는 프로젝트에서 객체지향 프로그램을 하기 위한 해결책으로 JPA라는 자바 표준 ORM(Object Relational Mapping)기술이 나왔습니다.

※ Mybatis, iBatis는 ORM이 아닙니다. SQL Mapper입니다. ORM은 객체를 매필하는 것이고, SQL Mapper는 쿼리를 매핑합니다.

- JPA 소개

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

  • 관계형 데이터베이스 : 어떻게 데이터를 저장할지에 초점이 맞춰진 기술
  • 객체지향 프로그래밍 : 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술

- Spring Data JPA

JPA는 인터페이스로서 자바 표준명세서입니다. 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요합니다. 대표적으로 Hibernate, Eclipse Link등이 있습니다. 하지만, Spring에서는 구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA기술을 다룹니다.

  • JPA ← Hibernate ←Spring Data JPA

Hibernate를 쓰는 것과 Spring Data JPA를 쓰는 것 사이에는 큰 차이가 없지만, 이렇게 한 단계 더 감싸놓은 이유는 크게 두 가지가 있다고 합니다.

  1. 구현체 교체의 용이성
    • Hibernate 외의 다른 구현체로 쉽게 교체하기 위함
  2. 저장소 교체의 용이성
    • 관계형 데이터베이스 외에 다른 저장소로 귑게 교체하기 위함

※ 실무에서 JPA를 사용하지 못하는 가장 큰 이유로는 높은 러닝 커브라고 합니다. JPA를 잘 쓰려면 객체지향 프로그래밍과 관계형 데이터베이스를 둘 다 이해해야 합니다.

 

- 요구사항 분석

앞으로 3장에서 6장까지 하나의 게시판(웹 애플리케이션)을 만들어 보고 7장부터 10장까지는 이 서비스를 AWS에 무중단 배포 하는것까지 진행합니다.

이 게시판의 요구사항은 다음과 같습니다.

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

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

build.grade에 jpa, h2 의존성 추가

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.projectlombok:lombok')

    compile('org.springframework.boot:spring-boot-starter-data-jpa') // 1)
    compile('com.h2database:h2') // 2)
    
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

1) spring-boot-starter-data-jpa

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

2) h2

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

 

- JPA 기능 사용

1. domain 패키지 생성

도메인을 담을 패키지 생성

여기서 도메인이란 게시글, 댓글, 회원, 정산 등 소프트웨어에 대한 요구사항 혹은 문제 영역입니다. 기존에 Mybatis와 같은 쿼리 매퍼를 사용했다면 dao 패키지를 떠올리면 됩니다. xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일들이 모두 도메인 클래스라고 불리는 곳에서 해결됩니다.

 

2. posts패키지와 Posts 클래스 생성

 

package com.jordy.books.springboot.domain.posts;

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

import javax.persistence.*;

@Getter // 6)
@NoArgsConstructor  // 5)
@Entity // 1)
public class Posts {
    @Id // 2)
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 3)
    private Long id;

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

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

    private String author;

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

 

이 책의 필자는 어노테이션 순서를 주요 어노테이션을 클래스에 가깝게 두고 있습니다. 정렬하는 기준은 다음과 같습니다.

@Entity는 JPA의 어노테이션이며, @Getter와 @NoArgsConstructor는 롬복의 어노테이션입니다.

롬복은 코드를 단순화시켜 주지만 필수 어노테이션은 아닙니다. 대신 주요 어노테이션인 @Entity를 클래스에 가깝게 두고, 롬복 어노테이션을 위로 두었습니다. 이렇게 하면 이후에 코틀린 등의 새 언어 전환으로 롬복이 필요 없을 경우 쉽게 삭제할 수 있습니다.

Posts클래스는 실제 DB의 테이블과 매칭될 클래스이며 보통 Entity 클래스라고도 합니다. JPA를 사용하면 실제 쿼리를 날리기 보다는, 이 Entity 클래스의 수정을 통해 작업합니다.

Posts의 클래스에는 JPA에서 제공하는 어노테이션들이 몇 개 있습니다.

1) @Entity

  • 테이블과 링크될 클래스임을 나타냅니다.
  • 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭합니다.
  • ex. JordyBook.java -> jordy_book table

2) @Id

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

3) @GeneratedValue

  • PK의 생성규칙을 나타냅니다.
  • 스프링 부트 2.0 에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 됩니다.

4) @Column

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

※ 참고

웬만하면 Entity의 PK는 Long타입의 Auto_increment를 추천한다고 합니다. 주민등록번호와 같이 비즈니스상 유니크 키나, 여러 키를 조합한 복합키로 PK를 잡을 경우 난감한 상황이 종종 발생합니다.

1) FK를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야 하는 상황이 발생합니다.

2) 인덱스에 좋은 영향을 끼치지 못합니다.

3) 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생합니다.

결론, 주민등록번호, 복합키 등은 유니크 키로 별도로 추가하는 것을 추천한다고 합니다.

 

5) ~ 7)은 롬복 라이브러리의 어노테이션들입니다.

5) @NoArgsConstructor

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

6) @Getter

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

7) Builder

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

Posts클래스에는 Setter 메소드가 없습니다. 자바빈 규약을 생각하면 getter/setter를 무작정 생성하는 경우가 있습니다. 이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어, 차후 기능 변경 시 복잡해집니다.

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

  • 잘못된 사용 예
public class Order{
        public void setStatus(boolean status){
            this.status = status
        }
        
        public void 주문서비스의_취소이벤트(){
            order.setStatus(false);
        }
    }
  • 올바른 사용 예
public class Order{
        public void setStatus(boolean status){
            this.status = status
        }
        
        public void 주문서비스의_취소이벤트(){
            order.cancelOrder();
        }
    }

 

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

 

@Build를 통해 제공되는 빌더 클래스를 사용하면 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있습니다.

//기존 생성자
public Example(String a, String b){
	this.a = a;
    this.b = b;
}

//@Builder 빌더 클래스 사용
Example.builder()
	.a(a)
    .b(b)
   	.build();

 - Posts클래스로 Database 접근가능하게 해 줄 JpaRepository생성

패키지 및 클래스 경로

package com.jordy.books.springboot.domain.posts;

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 없이는 제대로 역할을 할 수가 없습니다.

 

- Spring Data JPA 테스트 코드 작성(save, findAll 기능)

패키지 및 클래스 경로

package com.jordy.books.springboot.domain.posts;

import org.assertj.core.api.Assertions;
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;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
    @Autowired
    PostsRepository postsRepository;

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

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

        postsRepository.save(Posts.builder() // 2)
                .title(title)
                .content(content)
                .author("jordy@gmail.com")
                .build());
        //when
        List<Posts> postsList = postsRepository.findAll();// 3)

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

1) @After

  • JUnit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정
  • 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용합니다.
  • 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트가 실패할 수 있습니다. 그래서 deleteAll메소드를 실행시켜줍니다.

2) postsRepository.save

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

3) postsRepository.findAll

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

@SpringBootTest를 사용할 경우 H2 데이터베이스를 자동으로 실행해 줍니다. 테스트 실행 시 정상적으로 통과되는 것을 확인할 수 있습니다.

정상적으로 테스트 통과

- 실행된 쿼리 로그 확인

Java 클래스로 구현할 수 있으나, 스프링 부트에서는 application.properties, application.yml 등의 파일로 한 줄의 코드로 설정할 수 있도록 지원하고 권장합니다.

 

패키지 및 파일 위치

spring.jpa.show-sql=true

위와 같은 옵션 추가 후 다시 테스트를 수행하면 다음과 같은 쿼리 로그를 확인할 수 있습니다.

쿼리 로그 확인

여기서 create 쿼리를 보면 id bigint generated by default as identity라는 옵션으로 생성됩니다. 이는 H2의 쿼리 문법이 적용되었기 때문입니다. H2는 MySQL의 쿼리를 수행해도 정상적으로 작동하기 때문에 이후 디버깅을 위해서 출력되는 쿼리 로그를 MySQl 버전으로 변경해 보겠습니다.

application.properties에서 다음 코드를 추가합니다.

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

추가 후 다시 테스트 코드를 수행해 봅니다.

MySQL 쿼리 로그 확인

 

출처 : 

jojoldu.tistory.com/463

 

[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 출간 후기

(출판사: 프리렉, 쪽수: 416, 정가: 22,000원) 서적 링크 오프라인 서점에는 2019.12.04 (수) 부터 올라갈 예정입니다. 강남 교보문고나 광화문 교보문고는 주말에도 올라올 순 있겠지만, 혹시 모르니

jojoldu.tistory.com