CheerUp_Cheers

스프링 부트 - (3) 스프링부트에서 JPA로 데이터베이스 다뤄보자 본문

서적 공부/스프링부트 - [스프링부트와 AWS로 혼자 구현하는 웹서비스]

스프링 부트 - (3) 스프링부트에서 JPA로 데이터베이스 다뤄보자

meorimori 2020. 3. 13. 05:15

#JPA

자바 표준 ORM(객체를 맵핑하는 것), 명세

현대의 웹 애플리케이션에서의 관계형 데이터베이스는 빠질 수가 없다.

 

  • 장점

- 내부쿼리를 직접 작성할 필요가 없음.

- 객체 지향 프로그래밍이 쉬움(부모-자식, 1:N 관계)

 

  • 패러다임 불일치 : 0관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 다르다

  ->따라서 JPA는 중간에서 패러다임을 일치 시켜주기 위한 기술.

  ->SQL에 종속적인 개발을 하지 않아도 됨.

 

#Spring Data JPA

구현체들을 좀더 쉽게 추상화 시킨 모듈.

JPA < Hibernate < Spring Data JPA

 

  • 특징

- 구현체 매핑을 지원, 구현체 교체의 용이

- RDMS 외에 다른 저장소 쉽게 교체, 저장소 교체의 용이

  -> RDMS에서 몽고DB로 바꾸고 싶다면 의존성만 교체.


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

(1)의존성 추가 ( build.gradle > dependecies )

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

- spring-boot-starter-data-jpa

  스프링 부트용 Spring Dta JPA 추상화 라이브러리.

  스프링 부트버전에 맞추어 자동으로 JPA관련 라이브러리 버전 관리.

- H2

  인메모리용 관계형 DB, 애플리케이션이 재시작될 때마다 초기화(테스트로 주로 사용)

  별도의 설치없이 의존성만으로 관리 가능.

 

(2)domain패키지 생성 및 post 패키지와 post클래스 생성.

도메인이란 게시글,댓글,회원,정산,결제 등의 요구사항 및 문제 영역.

//Posts
package com.jojoldu.book.springboot.domain.posts;

import lombok.Builder;
import lombok.NoArgsConstructor;

import javax.persistence.*;

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

이 Posts클래스는 실제로 DB테이블과 매칭 될 Entity클래스.

JPA 사용시, DB데이터에 작업을 할 경우, 실제 쿼리를 날리기보다는 이 Entity클래스를 수정함으로 가능.

서비스 초기 구축 단계에는 테이블 설계(여기선 Entity설계)가 빈번하게 됨, 이 때 롬복의 어노테이션은 코드 변경량을 최소화 시켜줌.

 

- @Entity

  테이블과 링크될 클래스임을 나타냄.

- @Id

  해당 테이블의 PK필드를 나타냄.

- @GeneratedValue

  PK의 생성 규칙을 나타냄.

  GenerationType.IDENTITY옵션을 추가해야만 auto_increment됨.

- @Column

  테이블 칼럼을 나타냄. 굳이 선언하지 않더라도, 해달 클래스 필드는 모두 컬럼.

  사용하는 이유, 기본값 외에 추가로 변경이 될 필요가 있을 경우.

  해당 줄은 사이즈가 500에 타입은 TEXT.

- @NoArgsConstructor

  기본 생성자 자동 추가

  public Posts(){}와 같은 효과

- @Getter

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

- @Builder

  해당 클래스의 빌더 패턴 클래스 생성

  생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함.

 

#Posts클래스에는 왜 Setter가 없을까?

Entity클래스에서는 절대 Setter를 만들지 않음.

-> 필드의 값의 변경이 필요하면, 그에 상응하는 Method 생성.

-> 값을 채울 떄는 생성자를 통해 최종값을 채운 후, DB삽입.

 

#생성자 대신에 왜 @Builder를 사용하는가?

둘다 기능은 같음, 하지만 지금 채워야할 필드가 무엇인지 명확히 지정 불가.

다음 같이 사용시, 명확하게 인지 가능.

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

 

(3) JpaRepository 작성.

ibatis나 MyBatis등에서 DAO라 불리는 DB Layer 접근자( = JPA의 Repository)

단순히, 인터페이스를 생성한 다음, JpaRepository<Entity 클래스, PK타입>을 상속하면 기본적인 크루드 생성.

 

도메인 패키지에서 함께 관리.

-> Entity클래스는 리포지터리 없이는 제대로 역할 못하기 때문에.

 

[ PostsRepository ]

package com.jojoldu.book.springboot.domain.posts;

import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {
}

[ PostsRepositoryTest ]

package com.jojoldu.book.springboot.web.domain;

import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
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 postRepository;

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

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

        postRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("wotjd4305@naver.com")
                .build());

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

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

    }

}

- @SpringBootTest

  H2 데이터베이스를 자동적으로 실행.

- @After

  Junit에서 단위 테스트가 끝날 때마다 수행되는 메소를 지정.

  보통은 배포 전 전체 테스트를 수행 할때, 데스트간 데이터 침범을 막기 위해 사용.

  ->여러 테스트가 동시에 수행되면, H2에 데이터가 그대로 남아 다음 테스트시 실패 가능.

- postsRepository.save

  테이블 posts에 insert/update 쿼리를 실행.

  id값이 있다면 update, 없으면 insert쿼리.

- postsRepsitory.findAll

  테이블 posts에 있는 모든 데이터 조회.

 

#실제로 실행된 쿼리는 어떤것인지 보고싶다?

src/main/resources > application.properties 생성.

spring.jpa.show_sql=true

쿼리 로그확인

 

#출력되는 쿼리로그를 MYSQL 버전으로 변경.

src/main/resources > application.properties

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

3.4 등록/수정/조회 API 만들기

API를 위한 3개의 클래스

- Request 데이터를 받음 Dto

- API 요청을 받을 Controller

- 트랜잭션,도메인 기능 간의 순서를 보장하는 Service

 

#Spring 웹 계층

Sprint 웹 계층

(1)Web Layer

- 컨트롤러와 JSP/FreeMarker등의 뷰 템플릿 영역

- 필터, 인터셉터, 컨트롤 어드바이스 등의 외부 요청과 응답에 대한 전반적인 영억

(2)Service Layer

- @Service에 사용되는 서비스 영역.

- Controller와 Dao의 중간 영역에서 사용.

- @Transactionl이 사용되어야 하는 영역이기도 함.

(3)Repository Layer

- 데이터베이스와 같이 데이터 저장소에 정급하는 영역(=DAO)

(4)Dtos

- Dto는 계층 간에 데이터 교환을 위한 객체를 이야기 함.

- 예를 들면, 뷰 템플릿 영역에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등.

(5)Domains

- 도메인이라는 개발대상은 모든사람이 동일한 관점에서 이해할수 있또록 단순화 시킨것을 도메인 모델이라고함.

- @Entity영역 역시 도메인 모델

- 다만, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는것이 아님( VO도 도메인 영역)

 

#그래서 비지니스 처리는 누가해?

기존에 서비스로 비지니스 처리하던 방식을 트랜잭션 스크립트라고 함.

 

  • 트랜잭션 스크립트(기존, 옛날)

모든 로직이 클래스 내부에서 처리.

서비스 계층이 무의미하며, 객체란 단순히 데이터 덩어리.

 

But.

우리는 도메인에서 비지니스를 처리.

서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장.

 

#등록 기능 만들기

수정, 조회는 거의 동일하니 생략.

PostApiController, PostSaveRequestDto, PostService 이 세가지가 필요.

 

(1)PostApiController(web 패키지)

(!)오류 (초판 오타), p.111

putMapping -> PostMapping

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.service.posts.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class PostApiController {
    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto){
        return postsService.save(requestDto);
    }
}

(2)PostSaveRequestDto(web.dto 패키지)

package com.jojoldu.book.springboot.web.dto;


import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;

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

    public Posts toEntity(){
        return Posts.builder()
            .title(title)
            .content(content)
            .author(author)
            .build();
    }

}

여기서 DTO클래스는 Entity클래스와 거의 유사함.

사용하는 이유는 Entity클래스는 데이터 베이스와 맞닿은 핵심 클래스 임.

-> 사소한 변경으로 인해 Entity클래스를 변경하는 것은 너무 큰 변경.

(3)PostService(service 패키지)

package com.jojoldu.book.springboot.service.posts;

import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();
    }


}

#롬복을 쓰는 이유?

클래스의 의존성관계가 변경될때마다 생성자코드를 직접 수정하는 번거로움을 줄여줌.

여기서는 @RequeiredArgsConstructor( final이 선언된 모든 필드를 인자값으로 하는 롬복)

 

#더티 체킹?

영속성 컨텍스트가 유지된 상태에서 Entity값만 변경하면 되니 Update쿼리를 날릴 필요가 없음.

 

#영속성 컨텍스트?

엔티티를 영구 저장하는 환경, 일종의 논리적 개념.

 

#조회기능은 실제 톰캣을 실행하여 확인

[1]application.properites에 다음 코드추가

spring.h2.console.enabled=true

[2]웹에 localhost:8080/h2-console로 접속

다음 처럼 입력후 connect

입력(1)

다음처럼 데이터 삽입.

입력(2)

[3] 브라우저로 API조회

localhost:8080/api/v1/posts/1


3.5 JPA Auditing으로 생성시간/수정시간 자동화하기

언제 만들어지고, 언제 수정되었는지는 차후 유지보수에 아주 중요한 정보.

-> DB에 삽입/갱신전 날짜 데이터의 등록/수정

-> 코드가 복잡해짐

-> JPA Auditing 사용!

#LocalDate 사용.

[1] BaseTimeEntity 선언

[ BaseTimeEntity ]

위치 : domain 패키지

모든 Entity의 상위 클래스가 되어 날짜 자동 관리.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    
    @CreatedDate
    private LocalDateTime createdDate;
    
    @LastModifiedDate
    private LocalDateTime modifiedDate;
    
}

- @MappedSuperclass

  JPA Entity클래스 들이 BaseTimeEntity를 상속할 경우 필드(createdDate, modifedDate)도 칼럼으로 인식.

  ->코드가 지저분해지는것 막아줌.

- @EntityListeners(AuditingEntityListerner.class)

  BaseTimeEntity클래스에 Auditing 기능 포함.

- @CreatedDate

  Entity가 생성되어 저장될때 시간이 자동 저장됨.

- @LastModifiedDate

  조회한 Entity의 값을 변경할 떄 시간이 자동 저장됨.

 

[2] Posts클래스가 BaseTimeEntity 상속받도록 변경.

[3] Apllication 클래스에 JPA Auditing 어노테이션 활성화

활성화를 위해 어노테이션 하나 추가.

[4] JPA Audting 테스트 코드 작성

위치 : PostsRepositoryTest클래스

테스트 메소드를 하나더 추가.

   @Test
    public void BaseTimeEntity_등록(){
        //given
        LocalDateTime now = LocalDateTime.of(2019,6,4,0,0,0);
        postRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

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

        //then
        Posts posts = postsList.get(0);

        System.out.println(">>>>>>>>>>>> createdDate="+posts.getCreatedDate()+ ", modifiedDate="+posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);

    }

등록일/수정일 출력