프로젝트 요구사항에 맞게 도메인을 구성하고, JPA 와 연동하여 데이터가 데이터베이스에 저장한다.
마지막으로 테스트 코드를 작성하여 구성한 코드가 올바르게 동작하는지 확인한다.
- 게시판 기능
- 게시글 조회
- 게시글 등록
- 게시글 수정
- 게시글 삭제
- 회원 기능
- 구글 / 네이버 로그인
- 로그인한 사용자 글 작성 권한
- 본인 작성 글에 대한 권한 관리
- Spring boot : 2.6.3
- gradle : 7.2
- IntelliJ IDEA : 2021.03
- Junit : 4.13.2
프로젝트에 들어가기 앞서 JPA가 무엇인지, 어떤 장점이 있는지 알아보자.
현대의 웹 에플리케이션에서 데이터를 저장하기 위해 관계형 데이터베이스는 필수 요소이다.
관계형 데이터베이스 제품으로 Oracle, MySQL, MSSQL 등이 있다.
객체를 관계형 데이터베이스에서 관리하는 것은 중요하다.
(1) 단순 반복 작업을 줄일 수 있다.
관계형 데이터베이스는 SQL만 인식할 수 있는데, 각 테이블마다 기본적인 CRUD SQL을 생성해야 한다.
그래서 SQL로 프로젝트를 만들다보면 애플리케이션 코드보다 SQL이 더 가득한 프로젝트가 될 수 있다.
이러한 단순 반복작업을 해야하는 면에서 SQL을 직접 사용하는 것보다 JPA를 사용하는 것이 더 효율적이다.
(2) JPA는 객체지향 언어와 패러다임이 일치하여 객체를 데이터로 다루기 편리하다
관계형 데이터베이스와 객체지향 프로그래핑 언어는 패러다임이 불일치하는 문제가 있다.
패러다임이 서로 다른데 객체를 데이터베이스에 저장하려고 하면 여러 문제가 발생할 수 있다.
관계형 데이터베이스는 어떻게 데이터를 저장할지에 초점이 맞춰진 기술이고,
객체지향 프로그래밍 언어는 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술이다.
객체 지향의 특징인 상속, 1:N 등의 다양한 객체 모델링을 관계형 데이터베이스로 표현하기는 어려운데, 그 이유가 서로 패러다임이 다르기 때문이다.
JPA는 이러한 문제를 해결하기 위해 등장하였다.
즉, 개발자는 객체지향적 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행한다. 개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 더는 SQL에 종속적인 개발을 하지 않아도 된다.
이쯤에서 JPA 설명을 마치고, 이후 다른 포스팅에서 JPA에 대해 더 자세히 다루어 보아야겠다.
책이 3년 전(2019년)에 집필된 책이라 라이브러리 버전이 많이 업데이트 되었는데 build.gradle에 대한 내용은 책의 저자인 이동욱님의 블로그(https://jojoldu.tistory.com/539?category=717427)를 참고하였다.
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
(2) mariadb-java-client
(3) h2
JPA 기능을 사용해보기 위해 도메인을 생성해보자.
여기서 도메인이란 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역이라고 생각하면 된다.
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;
}
}
Entity의 PK(Primary Key)는 Long 타입의 auto_increment를 추천한다.(MySQL 기준으로 이렇게 하면 bigint 타입이 된다.)
주민등록번호화 같이 비즈니스상 유니크 키나, 여러 키를 조합한 복합키로 PK를 잡을 경우 다음과 같이 난감한 상황이 종종 발생한다.
(1) FK(Foreign Key)를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야하는 상황이 발생한다.
(2) 인덱스에 좋은 영향을 끼치지 못한다.
(3) 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생한다.
→ 따라서 주민등록번호, 복합키 등은 유니크 키로 별로로 추가하시는 것을 추천한다.
서비스 초기 구축 단계에서는 테이블 설계(여기선 Entity 설계)가 빈번하게 변경되는데, 이때 롬복의 어노테이션들은 코드 변경량을 최소화시켜주기 때문에 적극적으로 사용하는 것이 좋다.
(문제점) 그 이유는 getter/setter 를 무작정 생성하면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없어, 차후 변경이 정말 복잡해지기 때문이다.
(해결책) 생성자를 통해 최종값을 채운 후 DB에 삽입하고, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다.
이 프로젝트에서는 생성자 대신에 @Builder 를 통해 제공되는 빌더 클래스를 사용하며, 이 방법을 더 권장한다.
생성자와 빌더의 목적은 같다.
생성자나 빌더나 는 생성 시점에 변수에 값을 채워주는 역할을 한다.
빌더는 채워야 할 필드가 무엇인지 명확히 알 수 있다.
생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다.
예를 들어 아래와 같은 Example() 생성자가 있다면 개발자가 a와 b의 위치를 변경해도 코드를 실행하기 전까지는 문제를 찾기 어렵다.
public Example(String a, String b){
this.a = a;
this.b = b;
}
하지만 빌더를 사용하게 되면 다음과 같이 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.
Example.builder()
.a(a)
.b(b)
.build();
package com.spring.book.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
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);
}
}
@Autowire
, @MockBean
등) 을 사용할 때마다 이 주석이 필요하다.import org.junit.Test;
을 사용해야 한다.import org.junit.jupiter.api.Test;
를 사용한다.(에러문) 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
스프링 부트에서는 application.properties, application.yml 등의 파일에서 코드로 설정할 수 있도록 지원한다.
pring.jpa.show_sql=true
2. 쿼리 로그 확인
PostRepositoryTest 코드 실행시 콘솔에 쿼리 로그가 출력된 것을 확인 할 수 있다.
여기서 create table 쿼리를 보면 id bigint generated by default as identity 라는 옵션으로 생성된다.
그 이유는 H2의 쿼리 문법으로 적용되었기 때문이다.
H2는 MySQL 쿼리를 수행해도 정상적으로 작동하기 때문에 이후 디버깅을 위해서 출려고디는 쿼리 로그를 MySQL 버전으로 변경해보자.
//쿼리 로그 출력
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