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 참고 (너무 많아서 생략)
3.5 JPA Auditing으로 생성시간/수정시간 자동화하기
보통 엔터티에는 해당 데이터의 생성시간과 수정시간을 포함
그렇다 보니 매번 DB에 삽입하기 전, 갱신하기 전에 날짜 데이터를 등록/수정하는 코드가 여기저기 들어감
이렇게 단순하고 반복적인 코드 -> 해결하고자 JPA Auditing 사용
LocalDate 사용
패키지 접혀서 나와서 해결한 방법
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보다 낮은 버전으로 설정 이라고 되어있었는데 안되서
그냥 이렇게 했다 .....
'GDSC > Spring 입문' 카테고리의 다른 글
[AWS] RDS 30만원 과금 폭탄맞고 환불받은 후기 .. (1) | 2024.01.08 |
---|---|
Chap 04 - 머스테치로 화면 구성하기 (3) | 2023.11.19 |
[Spring 입문] 1주차 레퍼런스 (0) | 2023.11.05 |