스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 3

김진욱·2022년 2월 8일
1
post-thumbnail

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

3.1 JPA 소개

서로 지향하는 바가 다른 2개 영역(객체지향 프로그래밍 언어와 관계형 데이터베이스)을 중간에서 패러다임 일치를 시켜주기 위한 기술이다.
개발자는 객체지향적으로 프로그래밍을 하고 JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행한다.

Spring Data JPA
JPA는 인터페이스로서 자바 표준명세서이다. 인터페이스인 JPA를 사용하기 위해서는 구헨쳐가 필요하다. 하지만 Spring에서는 JPA를 사용할 때 구현체(Hibernate, Eclipse Link 등)들을 직접 다루진 않는다. 구현체들을 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용해 JPA 기술을 다룬다.

  • JPA <- Hiberante <- Spring Data JPA

Spring Data JPA가 등장한 이유는

  • 구현 교체의 용이성
  • 저장소 교체의 용이성

이다.

'구현체 교체의 용이성'이란 Hibernate 외에 다른 구현체로 쉽게 교체하기 위함이다.

'저장소 교체의 용이성'이란 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함입니다.
Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기때문에 의존성 교체만 하면 쉽게 저장소를 교체할 수 있다.

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

build.gradle에 의존성 추가하기

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('org.springframework.boot:spring-boot-starter-mustache')
    implementation('org.projectlombok:lombok')
    implementation('com.h2database:h2')

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    annotationProcessor 'org.projectlombok:lombok'
}

Spring-boot-starter-data-jpa

  • 스프링 부트용 Spring Data Jpa 추상화 라이브러리다.

h2

  • 인메모리 관계형 데이터베이스이다.
  • 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있다.
  • 메모리에서 실행되기 때문에 애플리케이션을 재시작할 떄마다 초기화된다는 점을 이용해 테스트 용도로 많이 사용된다.

Posts 클래스를 생성하자

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

import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {

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

	public void update(String title, String content) {
		this.title = title;
		this.content = content;
	}
}

Posts 클래스는 실제 DB의 테이블과 매칭될 클래스이며 보통 Entity클래스라고 한다. JPA를 사용하면 DB 데이터에 작업할 경우 실제 쿼리를 날리기보다는, 이 Entity 클래스의 수정을 통해 작업을 한다.

@Entity

  • 테이블과 링크될 클래스임을 나타낸다.
  • 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.

@Id

  • 해당 테이블의 PK필드를 나타낸다.

GeneratedValue

  • PK의 생성 규칙을 나타낸다.

@Column

  • 테이블의 컬럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 된다.
  • 사용하는 이유는 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.

참고
Entity의 PK는 Long 타입의 Auto_increment를 추천한다.

Entity 클래스에서는 절대 Setter 메서드를 생성하지 않는다.


PostsRepository 클래스를 생성하자

  • PostsRepository는 Posts클래스로 Database를 접근하게 해준다.
package com.jojoldu.book.springboot.domain.posts;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {

	List<Posts> findAllDesc();
}

JPA에서는 Repoisoty라고 불리는 이 클래스(인터페이스)는 보통 MyBatis나 ibatis에서 Dao라고 불리는 DB Layer 접근자이다.
단순히 JPARepository<Entity 클래스, PK 타입>를 상속하면 기본적인 CRUD 메서드가 자동으로 생성된다.


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

PostsRepositoryTest 클래스를 생성하자

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

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
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;


@ExtendWith(SpringExtension.class)
@SpringBootTest
class PostsRepositoryTest {

	@Autowired
	PostsRepository postsRepository;

	@AfterEach
	public void cleanup() {
		postsRepository.deleteAll();
	}

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

		postsRepository.save(Posts.builder()
			.title(title)
			.content(content)
			.author("jojoldu@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);
	}
}

@AfterEach

  • Junit5에서 단위 테스트가 끝날 때마다 수행되는 메서드를 지정
  • 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용한다.

postsRepository.save

  • 테이블 posts에 insert/update 쿼리를 실행한다.
  • id값이 있다면 update가, 없다면 insert 쿼리가 실행된다.

postsRepository.findAll

  • 테이블 posts에 있는 모든 데이터를 조회해오는 메서드이다.

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


추후 JPA의 설정과 h2데이터베이스 연결을 위해 application.properties 파일을 생성하자

위치는 src/main/resources 디렉토리 아래에 만들면 된다.

실제 실행된 쿼리가 어떤 형태인지 보기위해 application.properties에 다음과 같은 코드를 추가하자.

spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb

출력되는 쿼리 로그를 MySQL 버전으로 변경하기 위해 spring.jpa.show_sql=true 아래에 두 줄의 코드를 추가하였다.


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

API를 만들기 위해 총 3개의 클래스가 필요하다.

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

비즈니스 로직을 처리를 담당하는 곳은 Domain이다

PostsApiController 클래스를 생성하자

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;

@RestController
@RequiredArgsConstructor
public class PostsApiController {

	private final PostsService postsService;

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

PostsService 클래스를 생성하자

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;

@Service
@RequiredArgsConstructor
public class PostsService {

	private final PostsRepository postsRepository;

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

Entity 클래스를 절대로 Requeset/Response 클래스로 사용해서는 안된다.


책에 나오는 Test코드는 생략하겠다.

PostsResponseDto는 Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣는다. Entity를 직접 처리하는 방법은 좋지 않기 때문에 Dto로 변환해서 처리한다. 굳이 모든 필드를 가진 생성자가 필요하진 않으므로 Dto는 Entity를 받아 처리한다.

JPA 데이터 update 기능
JPA에서는 update를 할 때 굳이 데이터베이스에 쿼리를 날리지 않아도 된다. 그 이유는 JPA의 영속성 컨텍스트 때문이다.
영속성 컨텍스트란, 엔티티를 영구 저장하는 환경이다.
JPA의 엔티티 매니저(EntityManager)가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태이다.
이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다. 즉 Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다. 이 개념을 더티 체킹(dirty checking) 또는 변경 감지라고 한다.


h2 데이터베이스에 들어가보자
application.properties를 다음과 같이 수정하자.

spring.jpa.show_sql=true
spring.h2.console.enabled=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:testdb;MODE=MYSQL
spring.datasource.hikari.username=sa

수정 후 Application 클래스의 main 메서드를 실행한다. 그리고 웹 브라우저에서 http://localhost:8080/h2-console로 접속한다. JDBC URL에 jdbc:h2:mem:testdb를 입력 후 Connect 버튼을 눌러 관리 페이지로 이동한다.

  • 간단한 쿼리를 실행 해보자.
    select * from posts;


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

BaseTimeEntity 클래스를 생성하자

package com.jojoldu.book.springboot.domain;

import java.time.LocalDateTime;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

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

	@CreatedDate
	private LocalDateTime createdDate;

	@LastModifiedDate
	private LocalDateTime modifiedDate;

}

BaseTimeEntity 클래스는 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할을 한다.

@MappedSuperClass

  • JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createdDate, modifiedDate)도 컬럼으로 인식하도록 한다.

@EntityListeners(AuditingEntityListener.class)

  • BaseTimeEntity 클래스에 AUditing 기능을 포함시킨다.

@CreateDate

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

@LastModifiedDate

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

그리고 JPA Auditing 어노테이션들을 모두 활성화할 수 있도록 Application 클래스에 어노테이션을 추가하자.

package com.jojoldu.book.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

@EnableJpaAuditing

  • JPA Auditing 활성화한다.

추후 추가될 Entity들이 BaseTimeEntity만 상속받게 된다면 더이상 등록일/수정일로 고민할 필요가 없다.

0개의 댓글

관련 채용 정보