Hi yoahn 개발블로그

#3 [Spring Boot] JPA로 데이터베이스 다루기 본문

Framework & Library/springboot

#3 [Spring Boot] JPA로 데이터베이스 다루기

hi._.0seon 2020. 9. 8. 16:08
반응형

3.1 JPA

자바 표준 ORM(Object Relational Mapping) 기술

JPA는 인터페이스로서 자바 표준명세서이다. 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요(Hibernate, Eclipse Link 등)

하지만 Spring 에서 JPA를 사용할 때는 구현체들을 직접 다루지는 않음

 

Spring Data JPA

구현체들을 좀 더 쉽게 사용하기 위해 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA 기술을 다룬다

 

JPA  <--  Hibernate  <--  Spring Data JPA

 

Hibernate를 쓰는 것과 Spring Data JPA를 쓰는 것 사이에는 큰 차이가 없다

 

Spring Data JPA가 등장한 이유

1. 구현체 교체의 용이성

Hibernate 외에 다른 구현체로 쉽게 교체하기 위함 (새로운 JPA구현체가 대세가 될때 그것으로 쉽게 교체 가능)

-> Spring Data JPA 내부에서 구현체 매핑을 지원해주기 때문에

 

2. 저장소 교체의 용이성

관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함

트래픽이 많아져 관계형 데이터베이스로 도저히 감당히 안 되서 MongoDB로 교체가 필요하게 되면 개발자는 Spring Data MongoDB로 의존성만 교체하면 된다.

 

실무에서 JPA

JPA를 잘 쓰려면 객체지향 프로그래밍과 관계형 데이터베이스를 둘 다 이해해야 한다

JPA 장점

1. CRUD 쿼리를 직접 작성할 필요 없음

2. 부모-자식, 1:N 관계 표현, 상태와 행위를 한곳에서 관리 -> 객체지향 프로그래밍을 쉽게 가능

3. 성능 이슈 해결책 존재 -> 네이티브 만큼의 퍼포먼스 가능

 

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

p.86

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')
}

spring-boot-starter-data-jpa

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

스프링 부트 버전에 맞춰 자동으로 JPA관련 라이브러리들의 버전을 관리해준다

 

h2

인메모리 관계형 데이터베이스

별도 설치 필요 없이 프로젝트 의존성만으로 관리할 수 있다

메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용된다

JPA 테스트, 로컬 환경에서의 구동에서 사용

 

springboot.domain 패키지 생성

-> 도메인을 담을 패키지

도메인: 게시글, 댓글, 회원, 정산, 결제 등

 

domain.posts => Posts.java

p.88-89

@Getter
@NoArgsConstructor
@Entity
public class Posts {//실제 DB 테이블과 매칭될 클래스 (=Entity class)
    //JPA를 사용하면 DB 데이터에 작업할 경우 실제 쿼리를 날리기 보다 이 Entity클래스 수정을 통해 작업
    @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 클래스 (Entity 클래스)

- 실제 DB의 테이블과 매칭될 클래스

- DB 데이터에 작업할 경우 이 Entity클래스 수정을 통해 작업

 

@Entity

- 테이블과 링크될 클래스임을 나타낸다.

- 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름 매칭

 

@Id

- 해당 테이블의 PK 필드

 

@GeneratedValue

- PK의 생성 규칙

- 스프링 부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 된다

 

@Column

- 테이블의 컬럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 된다

- 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용

- VARCHAR(255)가 기본값 -> 500으로 바꾸고 싶거나, 타입을 TEXT로 바꾸고 싶거나 하는 경우 사용

 

@Builder

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

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

 

Posts 클래스에 Setter 메소드가 없다

getter/setter를 무작정 만들면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 구분할 수 없어, 차후 기능 변경 시 정말 복잡해진다

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

대신 해당 필드 값 변경이 필요하면 명확히 목적과 의도를 나타낼 수 있는 메소드를 추가해야만 한다

 

setter가 없는 상황에서 어떻게 값을 채워 DB에 삽입?

-> 기본 구조: 생성자를 통해 최종값을 채운 후, DB에 삽입하는 것

값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다

 

생성자 대신에 @Builder 를 통해 제공되는 빌더 클래스를 사용

(디자인 패턴 - 빌더 패턴)

 

domain.posts => PostsRepository.java (Interface)

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

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

DB Layer 접근자, JPA에서는 Repository라고 부르며 인터페이스로 생성한다

인터페이스 생성 후, JpaRepository<Entity 클래스, PK타입> 를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다

 

Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다

( Posts 클래스, PostsRepository 인터페이스가 같은 패키지에 위치 )

Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수가 없음

 

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

3.3 Spring Data JPA 테스트 코드 작성

test 디렉토리

-> domain.posts => PostsRepositoryTest.java

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("jojoludu@gmail.com")
                        .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 에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정

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

- 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2 에 데이터가 그대로 남아 다음 테스트 실행 시 테스트가 실패할 수 있다.

 

postsRepository.save

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

- id 값이 있다면 update 가, 없다면 insert 쿼리가 실행됨

 

postsRepository.findAll

- 테이블 posts에 있는 모든 데이터를 조회해오는 메소드

 

assertThat

- assertj 테스트 검증 라이브러리의 검증 메소드

- 검증할 대상을 인자로 받음

- 메소드 체이닝 지원

 

# 실제로 실행된 쿼리 확인하기

src/main/resources/application.properties 파일 생성

-> spring.jpa.show_sql=true

내용 추가 후 테스트 다시 실행

 

create table 쿼리 -> h2의 쿼리 문법

=> MySQL 문법으로 변경

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

내용 추가

 

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

p.105

총 3개 클래스

1) Request 데이터를 받을 Dto

2) API 요청 받을 컨트롤러

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

 

Spring 웹 계층

1. Web Layer

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

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

 

2. Service Layer

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

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

- @Transactional이 사용되어야 하는 영역이기도 하다

 

3. Repository Layer

- Database와 같이 데이터 저장소에 접근하는 영역

- 기존 Dao(Data Access Object) 영역

 

4. Dtos

- Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체,

   Dtos는 이들의 영역

- 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 이들을 이야기한다.

 

5. Domain Model

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

(택시 앱 => 배차, 탑승, 요금)

- @Entity 가 사용된 영역 역시 도메인 모델

- 무조건 DB 테이블과 관계가 있어야만 하는 것은 아니다

- VO 처럼 값 객체들도 이 영역에 해당함

=> 비즈니스 처리를 담당하는 곳

 

# Spring 에서 Bean을 주입받는 방식

1. @Autowired

2. setter

3. 생성자   <--- 권장

- 생성자로 Bean 객체를 받도록 하면 @Autowired와 동일한 효과

- @RequiredArgsConstructor  => final 이 선언된 모든 필드를 인자로 하는 생성자를 대신 생성해 준다

 

# Entity 클래스를 Request/Response 클래스로 사용하면 안된다

@Entity => Posts.java

데이터베이스와 맞닿은 핵심 클래스로 이것을 기준으로 테이블 생성, 스키마 변경이 일어난다

많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작한다. Entity 클래스가 변경되면 여러 클래스에 영향

Request/Response용 Dto는 View를 위한 클래스이므로 자주 변경이 필요함

 

=> View 와 DB Layer 의 역할을 철저하게 분리하는 것이 좋다

Controller에서 결괏값으로 여러 테이블을 조인해서 줘야 할 경우가 빈번함

  -> Entity 클래스만으로 표현하기 어려움

 

Entity 클래스와 Controller 에서 쓸 Dto는 분리해서 사용

 

-> Test 코드 web/PostsApiControllerTest.java

p.108-109

ApiController를 테스트하는데 @WebMvcTest를 사용하지 않음

JPA 기능이 작동하지 않기 때문에 JPA 기능까지 한번에 테스트할 때는 @SpringBootTest와 TestRestTemplate을 사용

 

수정/조회 기능 만들기

p.111~

PostsApiController.java 에 추가

web/dto/PostResponseDto.java 추가

-> Entity의 필드 일부만 사용 -> 생성자로 Entity를 받아 필드에 값을 넣음

 

web/dto/PostsUpdateRequestDto.java 추가

Posts.java 수정

 

PostsService.java 수정

-> update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다

= JPA의 영속성 컨텍스트 때문에

-> Entity를 영구 저장하는 환경

JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈린다

 

JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 그 데이터는 영속성 컨텍스트가 유지된 상태이다.

이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영

 = Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다  ( 더티 체킹 : jojoldu.tistory.com/415 )

 

더티 체킹

- 상태 변경 검사

- Dirty: 상태의 변화

JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다 all = 

(JPA에서는 엔티티를 조회하면 조회 상태 그대로 스냅샷 생성 -> 트랜잭션 끝나는 시점에 스냅샷과 비교하여 Update 쿼리를 DB로 전달)

- 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용

 

테스트 코드 작성

web/PostsApiControllerTest.java

수정

 

조회 기능은 실제로 톰캣 실행해서 확인

로컬 환경 - DB: H2

=> 메모리에서 실행, 직접 접근하려면 웹 콘솔을 사용해야 한다

 

웹 콘솔 옵션 활성화

application.properties

내용 추가 => spring.h2.console.enabled=true

 

Application클래스 - main 메소드 실행

http://localhost:8080/h2-console

웹 콘솔 화면 등장

JDBC URL => jdbc:h2:mem:testdb 로 되어있어야 한다

[connect] 버튼 누르기

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

보통 엔티티에는 생성/수정시간이 포함됨

매번 날짜 데이터를 등록/수정하는 코드가 여러곳에 들어가게 된다

-> 반복적인 코드가 모든 테이블과 모든 서비스 메소드에 포함되면 귀찮고 코드 지저분해져서 해결방안으로 JPA Auditing 사용

 

LocalDate 사용

 

domain/BaseTimeEntity.java

-> 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

@MappedSuperclass

- JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드들도 컬럼으로 인식하도록 한다

 

@EntityListeners(AuditingEntityListener.class)

- BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다

 

@CreatedDate

- Entity가 생성되어 저장될 때 시간이 자동 저장된다

 

@LastModifiedDate

- 조회한 Entity의 값을 변경할 때 시간이 자동 저장된다

 

$ 기존의 Posts클래스가 BaseTimeEntity를 상속받도록 변경

$ JPA Auditing 어노테이션들을 모두 활성화할 수 있도록 Application클래스에 활성화 어노테이션 추가

  => @EnabledJpaAuditing

 

# JPA Auditing 테스트 코드 작성

PostsRepositoryTest.java 클래스에 테스트 메소드 추가

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

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

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

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

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }
반응형
Comments