본문으로 바로가기

[SpringBoot] SpringBoot에서 JPA 사용하기

category Java/Spring 2023. 8. 5. 16:04

JPA 의 정의 및 간략한 소개는 다음 글에서 확인 할 수 있습니다. 

https://chamggae.tistory.com/205

 

[Spring] Spring 에서 JPA 사용하기 (정의, 장단점, 예제)

💡 JPA 의 정의 Java Persistance API 의 약자 Java 진영에서 ORM (Object-Relation Mapping) 기술 표준으로 사용하는 인터페이스 모음 ORM : 객체는 객체대로, 데이터는 데이터 대로 설계하는 것 자바에서 관계형

chamggae.tistory.com

 

JPA 는 ORM 진영의 자바 명세서 (=인터페이스) 이고 이 JPA 인터페이스를 사용하기 위해 구현체가 필요합니다. 

여기서 대표적으로 흔히 듣던 Hibernate 등이 그 구현체입니다. 

 

하지만 본 글에서 JPA를 사용할 때에는 이 구현체들을 직접 다루지 않습니다. 

구현체들을 더 쉽고 추상화시킨 것이 Spring Data JPA 입니다. (구현체 교체의 용이성, 저장소 교체의 용이성)

 

 

여기서 부터 실습 >>

Springboot 환경에서 Spring JPA 사용하고 테스트 해보기 

 

  • JPA Entity 를 사용하여 데이터 삽입 및 수정 조회
  • JPA Audit 소개 

 

 

 

환경 

  • SpringBoot 2.7.6
  • junit 4.13.2
  • Spring JPA
  • h2 (인메모리 DB)
  • lombok
  • gradle / application.yml

 

build.gradle 정보 

buildscript {
    ext {
        springBootVeresion = '2.7.6.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVeresion}")
    }
}

plugins {
    id 'java'
    id 'eclipse'
    id 'org.springframework.boot' version '2.7.6'
    id 'io.spring.dependency-management' version '1.1.0'
}

group 'org.example'
version '1.0-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'junit:junit:4.13.2'
    implementation('org.springframework.boot:spring-boot-starter-data-jpa') // SpringBoot 용 Spring Data Jpa 라이브러리, Spring에서 Hibernate라는 구현체를 직접 사용하지 않는다고 한다. ,
    implementation('com.h2database:h2') // 인메모리 관계형 데이터베이스, 테스트 용도로 많이 사용

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

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

tasks.named('test') {
    useJUnitPlatform()
}

 

 

1. Entity 생성 

Posts 엔티티

import com.example.springbootAWS.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;


@Getter
@NoArgsConstructor
@Entity // Entity에서는 Setter가 아닌, 명확한 Function사용한다
public class Posts extends BaseTimeEntity {
    
    @Id // PK 필드
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment
    private Long id;
    
    @Column(length = 500, nullable = false) // default = varchar(255)
    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;
    }
}

 

Post 생성 및 업데이트 기능을 추가한 엔티티 입니다. 

 

JPA Audit 이란 ? 

보통 데이터 작업을 할 때 생성 시간 및 업데이트 날짜는 중요한 정보이기에 같이 저장하는 경우가 있습니다. 

다만 이런 반복적인 코드는 JPA Auditing 기능으로 해결할 수 있습니다. 

 

Date 뿐만아니라 @CreatedBy, @ModifiedBy 와 같은 어노테이션도 제공하고 있습니다. 

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;


// 단순 반복적인 audit 코드를 해결할 수 있다
@Getter
@MappedSuperclass   // JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 Date 필드들도 칼럼으로 인식하도록 한다
@EntityListeners(AuditingEntityListener.class)  // JPA Auditing
public abstract class BaseTimeEntity {
    
    @CreatedDate    // Entity 생성 시간
    private LocalDateTime createdDate;
    
    @LastModifiedDate // Entity 변경 시간
    private LocalDateTime modifiedDate;
}

 

단 JPA Auditing 을 사용하기 위해 Application 상단에 @EnableJpaAuditing 어노테이션이 필요합니다. 

 

@SpringBootApplication
@EnableAsync
@EnableJpaAuditing // JPA Auditing 활성화
@Slf4j
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

 

 

 

2. Repository 생성

PostsRepository

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

public interface PostsRepository extends JpaRepository<Posts, Long> {    // <Entity 클래스, PK 타입>
}

DB Layer 접근자 입니다. JPA 에서는 Repository라는 명칭을 쓰며 인터페이스로 사용합니다. 

JpaRepository를 상속하면 기본적으로 CRUD 메소드를 사용할 수 있습니다. 

 

 

3. Service 정보

import com.example.springbootAWS.domain.posts.Posts;
import com.example.springbootAWS.domain.posts.PostsRepository;
import com.example.springbootAWS.web.dto.PostsResponseDto;
import com.example.springbootAWS.web.dto.PostsSaveRequestDto;
import com.example.springbootAWS.web.dto.PostsUpdateRequestDto;
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();
    }
    
    /*
        쿼리를 날리지 않고 가능한 이유는 JPA의 영속성 컨텍스트 때문이다.
        영속성 컨텍스트 : 엔티티를 영구 저장하는 환경 *더티 체킹
     */
    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id= "+id));
        
        posts.update(requestDto.getTitle(), requestDto.getContent());
        
        return id;
    }
    
    public PostsResponseDto findById(Long id) {
        Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id= "+id));
        return new PostsResponseDto(entity);
    }
}

*더티 체킹 : 더티 체킹이란 JPA 측에서 엔티티의 변화가 있는 대상을 모두 update 처리하는 기능

변화의 기준은 최초의 엔티티 상태. 

 

 

JpaRepository 테스트 하기 > 

 

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

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

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