스프링부트와 AWS로 혼자 구현하는 웹서비스 따라하기

어제는

테스트 코드로 코드를 검증하는 걸 해봤습니다. 앞으로도 계속 테스트 코드를 사용하며 테스트코드로 코드를 검증하는 방식에 익숙해져야합니다.
이번에는 스프링부트에서 데이터베이스를 어떻게 사용하는지 배워보겠습니다.

예! 열심히 하겠습니다!

시작👊

스프링 부트에서 JPA로 데이터베이스를 다뤄보자

iBatis(현재의 MyBatis)와 같은 SQL 매퍼를 이용해서 데이터베이스의 쿼리를 작성했습니다. 그러다 보니 실제 개발하는 시간 보다 SQL을 다루는 시간이 더 많았습니다.

헐 맞아 우리 테이블 연결하는데 훨씬 시간 많이 쓰고 테이블 뭐를 더 만들어야 연결이 제대로 되는지 뭐가 어떻게 연결되어야 하는지 제일 많이 고민했잖아.

이것이 이상하게 느껴졌습니다.

..? 왜지..? 나는 그래서 처음 테이블 세팅이 참 중요하구나 생각했는데...역시 천재는 달라...

분명 객체지향 프로그래밍을 배웠는데 왜 객체지향 프로그래밍을 못하지? 객체 모델링보다는 테이블 모델링에만 집중하고, 객체를 단순히 테이블에 맞추어 데이터 전달 역할만 하는 개발은 분명 기형적인 형태였습니다.

아 뼈맞음. 나는 기형적인 형태의 개발을 하고 있었던 거야... 깁스해야겠다...😔

근데 나는 테이블 연결이 어떻게 이뤄지는지도 이해 못 한 똥댕청이지만, 프로젝트하면서 생각했거든여. 이거를 조금 더 세련된 방법으로 할 수 있지 않을까? 아무튼 함. 이해 못했긴 한데 아무튼 했었음. 믿기 어렵지만 아무튼 함.

문제의 해결책으로 JPA 라는 자바 표준 ORM 기술을 만나게 됩니다.

나 JPA 배웠는데 모름...ㅠ 동댕청이

ORM 은 객체를 매핑하는 것이고 SQL Mapper(MyBatis 등) 는 쿼리를 매핑합니다.
JPA 가 어려운 만큼 JPA 를 사용해 얻는 보상은 큽니다. 일단 CRUD 쿼리를 직접 작성할 필요가 없고 부모자식관계표현, 일대다관계표현, 상태와 행위를 한 곳에서 관리하는 등 객체지향 프로그래밍을 쉽게 할 수 있습니다.

내가 여태 배운 게 다 이런 거였나보다. 대단해 나 자신.

일단 시작해보자

본격적인 개발에 들어가보자

요구사항

우리가 만들 게시판( 웹 어플리케이션 )의 요구 사항

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

궁금한 게 있는데 우리 웹 페이지도 만드나? js 로? 나중에 가면 알겠지?

일단 ㄱ

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

  1. build.gradle 에 dependencies 안에 jpa 의존성을 설치해준다.
// build.gradle 파일
// dependencies

    // jpa
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('com.h2database:h2')

어... build.gradle 을 4.10.2 로 낮췄더니 compile 이 작동하네... 다 compile 로 바꿀까... 일단 해볼까. 바꾸는 게 맞는 거 같은데 귀찮으니까 일단 해보고 뭔가 에러가 나면 바꾸자 ㅎㅎㅎㅎ 아 내가 너무 근본없이 일을 하나요? 깔깔

체크해놓읍시다.

spring-boot-starter-data-jpa : 스프링 부트용 Spring Data Jpa 추상화 라이브러립니다. 스프링부트 버전에 맞춰 자동으로 JPA 관련 라이브러리들의 버전을 관리해줍니다.

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

  1. 일단 그렇게 깔고 gradle 재설정하자.

본격적으로 JPA 기능을 사용해볼까요

  1. springboot 아래에 domain 이라는 패키지를 만듭니다

    기존의 쿼리 매퍼를 사용했다면 dao 패키지를 떠올리겠지만, dao 패키지와는 조금 결이 다르다. 그간 xml 에 쿼리를 담고, 클래스ㅡㄴ 오로지 쿼리의 결과만 담던 일들이 모두 domain 클래스라는 곳에서 해결한다.

  2. domain 안에 posts 라는 패키지와 Posts 라는 클래스를 만든다.

  3. Posts 에 코드를 작성해준다.

// Posts.java

package com.prac.webservice.springboot.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
    private String title;

    @Column
    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 는 JPA의 어노테이션이며 @Getter@NoArgsConstructor는 롬복의 어노테이션입니다. 롬복은 코드를 단순화 시켜주지만 필수는 아닙니다. 그래서 필수 어노테이션인 @Entity 를 클래스 가까이 둔 것입니다. 이렇게 하면 추후 코틀린 등의 새 언어 전환으로 롬복이 더이상 필요 없을 경우 쉽게 삭제할 수 있습니다.

이 저자는 어디까지 내다보고 있는걸까. 경험이 얼마나 쌓여야 이런 내공이 생기는 걸까. 진짜 멋있다.
아자 아자! 화이팅!

JPA 를 사용하면 DB 데이터에 실제 쿼리를 날리기 보다는, 이 Entity 클래스의 수정을 통해 작업합니다.
@Entity : 테이블과 링크될 클래스임을 나타내며, 클래스의 카멜케이스 이름을 언더스코어 네이밍(스네이크 케이스)으로 테이블이름을 매칭합니다.

@Id : Primary Key

@GeneratedValue : PK의 생성규칙을 나타내며 스프링부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야 auto_increment 가 됩니다.

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

참고

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

  • FK 를 맺을 때 다른 테이블에서 복합키를 전부 갖고 있거나. 중간 테이블을 하나 더 둬야 하는 상황이 발생합니다.
  • 인덱스에 좋은 영향을 끼치지 못하며 유니크한 조건이 변경될 경우 PK 전체를 수정해야하는 일이 발생합니다.

    주민번호, 복합키 등은 유니크 키로 별도로 추가하시는 것을 추천드립니다.

... 이해 못 함 ㅠㅠ 중간테이블을 하나 더 둬야하는 상황이 발생한 적은 있는데 그거랑 무슨 상관이지...? 하 난 댕청이야 ㅠ
화이팅!

@NoArgsConstructor : 기본 생성자 자동 추가( public Post() {} 같은 메서드를 만든 효과)
@Getter : 클래스 내 모든 필드의 Getter 메서드를 생성
@Builder : 해당 클래스의 빌더 패턴 클래스를 생성, 생성자 상단에 선언하면 생성자에 포함된 필드만 빌더에 포함된다.

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

오.. 테이블 설계를 바꿔버리는 구나.. 우리도 바꿀려다가 시간 없어서 못 바꿨는데.. 대단해

이 Posts 클래스에는 Setter 메서드가 없습니다. 자바빈 규약을 생각하면서 Setter/Getter 를 무작정 생성하는 경우가 있는데 이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야하는지 코드상으로 명확하게 구분할 수가 없어, 차후 기능 변경시 정말 복잡해집니다.
그래서 Entity 클래스에서는 절대 Setter 메서드를 만들지 않습니다.
대신 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메서드를 추가해야만 합니다.

이 뒤에 이야기는 이해하지 못했다.

어쨌든 Posts 클래스 생성이 끝났으니

Repository 생성하기

Posts 클래스로 database 를 접근하게 해줄 JpaRepository 를 만들어주자.

  1. domain.posts 패키지에 PostsRepository 를 만들어 주는데, 그냥 만드는게 아니다
    인터페이스로 만들어준다.

  2. 인터페이스에는


package com.prac.webservice.springboot.domain.posts;

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

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

이것만 추가하면 된다.

단순히 인터페이스를 생성후, JpaRepository<Entity 클래스, PK 타입> 를 상속하면 기본적인 CRUD 메서드가 자동으로 생성됩니다. @Repository를 추가할 필요도 없습니다. 단, Entity 클래스와 기본 Entity Repository는 같은 위치에 있어야한다는 점입니다. 추후에 프로젝트 규모가 커져 도메인별로 프로젝트를 분리해야 한다면 이때 Entity 클래스와 기본 Repository는 함께 움직여야 하므로 도메인 패키지에서 함께 관리합니다.

테스트 코드로 검증해보자!

  1. 이전과 같이 test 디렉토리에 domain / posts 를 만들고 PostRepositoryTest.java 를 만들어준다.
  2. 코드를 넣어준다
// PostsRepositoryTest.java

package com.prac.webservice.springboot.domain.posts;

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 PostsRepositoryTeset {

    @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("thovy")
                .build());

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

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

}
  1. 실행해본다.

좋다.
좋아!!

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


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


@postsRepository.findAll : 테이블 posts 에 있는 모든 데이터 조회

별다른 설정 없이 @SpringBootTest를 사용할 경우 H2 데이터베이스를 자동으로 실행해줍니다.

여기서 궁금한 것이 있습니다. 실제로 실행된 쿼리는 어떤 형태일까?

실행된 쿼리를 로그로 볼 수는 없을까요?
java 클래스로 구현할 수도 있으나 스프링 부트에서는 apllication.properties, application.yml 등의 파일로 한 줄의 코드로 설정할 수 있도록 지원하고 권장하니 이를 사용해보겠습니다.

예! 감사합니다.
ㄱㄱ

  1. src / main / resources 디렉토리 아레에 application.properties 파일을 만든다.
spring.jpa.show_sql=true

라고 적어넣는다.

  1. 테스트를 다시 실행해보면

    insert 부터 다다음줄에 select, 마지막 drop 까지 다섯줄이 적혀있는 걸 볼 수 있다.

  2. 그런데 조금 올라가서 create 를 보면

    id bigint generated by default as identity 라고 적혀있다.

출력문을 수정해보자

  1. application.properties 에 코드를 한 줄 추가한다.(한 줄이다)
// application.properties

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

자 다됐다. 본격적으로 API를 만들어보겠습니다!

오늘은 끝

고생하셨습니다!

profile
BEAT A SHOTGUN

0개의 댓글