본문 바로가기

GDSC/Spring 입문

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

JPA : 자바 표준 ORM

자사 서비스를 개발하는 곳에서는 스프링부트 & JPA 전사 표준으로 사용 

3.1 JPA 소개

객체를 관계형 데이터베이스에서 관리하는 것 중요

현업 프로젝트 대부분이 애플리케이션 코드보다 SQL 가득 <- 관게형 DB가 SQL만 인식 가능하기 때문

SQL 단순 반복 작업 문제

패러다임 불일치 문제

관계형 DB : 어떻게 데이터를 저장할지에 초점

객체지향 프로그래밍 언어 : 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술

=> 패러다임이 서로 다른데 객체를 데이터베이스에 저장하려고 하니 여러 문제 발생

JPA : 중간에서 패러다임 일치시켜 주기 위한 기술

개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 DB에 맞게 SQL 대신 생성하여 실행

Spring Data JPA

JPA <- Hiberante <- Spring Data JPA

- 구현체 교체의 용이성 : HIbernate 외에 다른 구현체로 쉽게 교체 

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

Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같음

실무에서 JPA

실무에서 JPA를 사용하지 못하는 가장 큰 이유 : 높은 러닝 커브

JPA를 사용해서 얻는 보상

  • CRUD 쿼리를 직접 작성할 필요가 없음
  • 객체지향 프로그래밍 쉽게 가능
  • 잘 활용하면 네이티브 쿼리만큼의 퍼포먼스 낼 수 있음

요구사항 분석

하나의 게시판 만들어보기

게시판 기능

  • 게시글 조회
  • 게시글 등록
  • 게시글 수정
  • 게시글 삭제

회원 기능

  • 구글/네이버 로그인
  • 로그인한 사용자 글 작성 권한
  • 본인 작성 글에 대한 권한 정리

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

의존성 등록

  • spring-boot-starter-data-jpa : 스프링 부트 버전에 맞춰 자동으로 jpa 관련 라이브러리들의 버전 관리
  • h2 : 인메모리 관계형 데이터베이스, 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리 가능
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.projectlombok:lombok'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'junit:junit:4.13.1'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

-> 오타나서 오류났었음 .. 

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

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

//클래스 내 모든 필드의 Getter 메소드 자동생성
@Getter
//기본 생성자 자동 추가
@NoArgsConstructor
@Entity //테이블과 링크될 클래스임을 나타냄
public class Posts {

    @Id //해당 테이블의 pk 필드 나타냄
    //pk의 생성 규칙 나타냄
    @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 클래스에서는 절대 Setter 메소드를 만들지 않음

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

 

기본적 구조 : 생성자를 통해 최종값을 채운 후 db에 삽입, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 함

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

빌더를 사용하면 어느 필드에 어떤 값을 채워야 할지 명확하게 인지 가능

Posts 클래스 생성 후 Posts 클래스로 Database 접근하게 해 줄 JpaRepository 생성

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

주의할 점 !! Entity 클래스와 기본 Entity Repository는 함께 위치해야함

3.3 Spring Data JPA 테스트 코드 작성하기

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

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class PostsRepositoryTest {
    @Autowired
    PostsRepository postsRepository;

    //단위 테스트가 끝날 때마다 수행되는 메소드 지정
    @AfterEach
    public void cleanup() {
        postsRepository.deleteAll();
    }
    @Test
    public void 게시글저장_불러오기() {
        //given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        //테이블 posts에 insert/update 쿼리 실행
        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("jojoldu@gmail.com")
                .build());
        //when
        //테이블 posts에 있는 모든 데이터 조회
        List<Posts> postsList = postsRepository.findAll();

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

 

실제로 실행된 쿼리 어떤 형태일지 확인

application.properties

spring.jpa.show-sql=true
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

안되서 구글링해서 이렇게 적용했더니 돌아감!

 

실행 결과)

Hibernate: create table posts (id bigint not null auto_increment, author varchar(255), content TEXT not null, title varchar(500) not null, primary key (id)) engine=InnoDB

 

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

api 만들기 위해 필요한 3개의 클래스

  • Request 데이터를 받을 Dto
  • api 요청을 받을 Controller
  • 트랜잭션, 도메인 기능 간의 순서를 보장하는 service

 

스프링에서 Bean을 주입받는 방식: @Autowired, setter, 생성자

@RequiredArgsConstructor에서 생성자 주입 해결해줌

 

 

Entity 클래스를 Request/Response 클래스로 사용해서는 안 됨

Entity 클래스 : 데이터베이스와 맞닿은 핵심 클래스, 기준으로 테이블이 생성되고, 스키마가 변경됨

 

영속성 컨텍스트 : 엔티티를 영구 저장하는 환경

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

 

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

이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분 반영 -> Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없음 : 더티 체킹(dirty checking)

 

++ 코드들은 git 참고 (너무 많아서 생략)

 

h2 console 화면

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

보통 엔터티에는 해당 데이터의 생성시간과 수정시간을 포함

그렇다 보니 매번 DB에 삽입하기 전, 갱신하기 전에 날짜 데이터를 등록/수정하는 코드가 여기저기 들어감

이렇게 단순하고 반복적인 코드 -> 해결하고자 JPA Auditing 사용

 

LocalDate 사용

https://khstar.tistory.com/entry/IntelliJ-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Package%EA%B0%80-%EC%A0%91%ED%98%80%EC%84%9C-%EB%82%98%EC%98%A4%EB%8A%94-%EA%B2%BD%EC%9A%B0

 

IntelliJ 프로젝트에서 Package가 접혀서 나오는 경우

IntelliJ에서 프로젝트를 생성하고 Package를 만들면 다음과 같이 접혀서 나오는 경우가 있습니다. controller 밑에 java 파일을 생성하는데는 문제가 없습니다. 하지만 test 밑에 java 파일을 생성해야할

khstar.tistory.com

패키지 접혀서 나와서 해결한 방법

 

package com.jojoldu.book.springboot.domain;

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
//BaseTimeEntity 상속할 경우 필드들도 칼럼으로 인식하도록 함
@MappedSuperclass
//클래스에 Auditing 기능 포함시킴
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    //Entity가 생성되어 저장될 때 시간이 자동 저장됨
    @CreatedDate
    private LocalDateTime createdDate;
    
    //조회한 Entity의 값을 변경할 때 시간이 자동 저장됨
    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

Posts 클래스가 BaseTimeEntity 상속받도록 변경, 

Application 클래스에 @EnableJpaAuditing 추가

JPA Auditing 테스트 코드 작성하기

@Test
    public void BaseTimeEntity_등록() {
        //given
        LocalDateTime now = LocalDateTime.of(2023,11,12,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);

    }

최종 application.properties

spring.jpa.show-sql=true
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
spring.h2.console.enabled=true

올려주신 실습 참고 부분에는 h2의 버전을 1.4.198보다 낮은 버전으로 설정 이라고 되어있었는데 안되서

그냥 이렇게 했다 .....