CheerUp_Cheers
스프링 부트 - (3) 스프링부트에서 JPA로 데이터베이스 다뤄보자 본문
스프링 부트 - (3) 스프링부트에서 JPA로 데이터베이스 다뤄보자
meorimori 2020. 3. 13. 05:15#JPA
자바 표준 ORM(객체를 맵핑하는 것), 명세
현대의 웹 애플리케이션에서의 관계형 데이터베이스는 빠질 수가 없다.
- 장점
- 내부쿼리를 직접 작성할 필요가 없음.
- 객체 지향 프로그래밍이 쉬움(부모-자식, 1:N 관계)
- 패러다임 불일치 : 0관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 다르다
->따라서 JPA는 중간에서 패러다임을 일치 시켜주기 위한 기술.
->SQL에 종속적인 개발을 하지 않아도 됨.
#Spring Data JPA
구현체들을 좀더 쉽게 추상화 시킨 모듈.
JPA < Hibernate < Spring Data JPA
- 특징
- 구현체 매핑을 지원, 구현체 교체의 용이
- RDMS 외에 다른 저장소 쉽게 교체, 저장소 교체의 용이
-> RDMS에서 몽고DB로 바꾸고 싶다면 의존성만 교체.
3.2 프로젝트에 Spring Data JPA 적용하기
(1)의존성 추가 ( build.gradle > dependecies )
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('com.h2database:h2')
- spring-boot-starter-data-jpa
스프링 부트용 Spring Dta JPA 추상화 라이브러리.
스프링 부트버전에 맞추어 자동으로 JPA관련 라이브러리 버전 관리.
- H2
인메모리용 관계형 DB, 애플리케이션이 재시작될 때마다 초기화(테스트로 주로 사용)
별도의 설치없이 의존성만으로 관리 가능.
(2)domain패키지 생성 및 post 패키지와 post클래스 생성.
도메인이란 게시글,댓글,회원,정산,결제 등의 요구사항 및 문제 영역.
//Posts
package com.jojoldu.book.springboot.domain.posts;
import lombok.Builder;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@NoArgsConstructor
@Entity
public class Posts {
@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클래스는 실제로 DB테이블과 매칭 될 Entity클래스.
JPA 사용시, DB데이터에 작업을 할 경우, 실제 쿼리를 날리기보다는 이 Entity클래스를 수정함으로 가능.
서비스 초기 구축 단계에는 테이블 설계(여기선 Entity설계)가 빈번하게 됨, 이 때 롬복의 어노테이션은 코드 변경량을 최소화 시켜줌.
- @Entity
테이블과 링크될 클래스임을 나타냄.
- @Id
해당 테이블의 PK필드를 나타냄.
- @GeneratedValue
PK의 생성 규칙을 나타냄.
GenerationType.IDENTITY옵션을 추가해야만 auto_increment됨.
- @Column
테이블 칼럼을 나타냄. 굳이 선언하지 않더라도, 해달 클래스 필드는 모두 컬럼.
사용하는 이유, 기본값 외에 추가로 변경이 될 필요가 있을 경우.
해당 줄은 사이즈가 500에 타입은 TEXT.
- @NoArgsConstructor
기본 생성자 자동 추가
public Posts(){}와 같은 효과
- @Getter
클래스 내 모든 필드의 Getter메소드를 자동 생성
- @Builder
해당 클래스의 빌더 패턴 클래스 생성
생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함.
#Posts클래스에는 왜 Setter가 없을까?
Entity클래스에서는 절대 Setter를 만들지 않음.
-> 필드의 값의 변경이 필요하면, 그에 상응하는 Method 생성.
-> 값을 채울 떄는 생성자를 통해 최종값을 채운 후, DB삽입.
#생성자 대신에 왜 @Builder를 사용하는가?
둘다 기능은 같음, 하지만 지금 채워야할 필드가 무엇인지 명확히 지정 불가.
다음 같이 사용시, 명확하게 인지 가능.
Example.builder()
.a(a)
.b(b)
.build();
(3) JpaRepository 작성.
ibatis나 MyBatis등에서 DAO라 불리는 DB Layer 접근자( = JPA의 Repository)
단순히, 인터페이스를 생성한 다음, JpaRepository<Entity 클래스, PK타입>을 상속하면 기본적인 크루드 생성.
도메인 패키지에서 함께 관리.
-> Entity클래스는 리포지터리 없이는 제대로 역할 못하기 때문에.
[ PostsRepository ]
package com.jojoldu.book.springboot.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
[ PostsRepositoryTest ]
package com.jojoldu.book.springboot.web.domain;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
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 postRepository;
@After
public void cleanup(){
postRepository.deleteAll();
}
@Test
public void 게시판저장_불러오기(){
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postRepository.save(Posts.builder()
.title(title)
.content(content)
.author("wotjd4305@naver.com")
.build());
//when
List<Posts> postsList = postRepository.findAll();
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
- @SpringBootTest
H2 데이터베이스를 자동적으로 실행.
- @After
Junit에서 단위 테스트가 끝날 때마다 수행되는 메소를 지정.
보통은 배포 전 전체 테스트를 수행 할때, 데스트간 데이터 침범을 막기 위해 사용.
->여러 테스트가 동시에 수행되면, H2에 데이터가 그대로 남아 다음 테스트시 실패 가능.
- postsRepository.save
테이블 posts에 insert/update 쿼리를 실행.
id값이 있다면 update, 없으면 insert쿼리.
- postsRepsitory.findAll
테이블 posts에 있는 모든 데이터 조회.
#실제로 실행된 쿼리는 어떤것인지 보고싶다?
src/main/resources > application.properties 생성.
spring.jpa.show_sql=true
#출력되는 쿼리로그를 MYSQL 버전으로 변경.
src/main/resources > application.properties
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
3.4 등록/수정/조회 API 만들기
API를 위한 3개의 클래스
- Request 데이터를 받음 Dto
- API 요청을 받을 Controller
- 트랜잭션,도메인 기능 간의 순서를 보장하는 Service
#Spring 웹 계층
(1)Web Layer
- 컨트롤러와 JSP/FreeMarker등의 뷰 템플릿 영역
- 필터, 인터셉터, 컨트롤 어드바이스 등의 외부 요청과 응답에 대한 전반적인 영억
(2)Service Layer
- @Service에 사용되는 서비스 영역.
- Controller와 Dao의 중간 영역에서 사용.
- @Transactionl이 사용되어야 하는 영역이기도 함.
(3)Repository Layer
- 데이터베이스와 같이 데이터 저장소에 정급하는 영역(=DAO)
(4)Dtos
- Dto는 계층 간에 데이터 교환을 위한 객체를 이야기 함.
- 예를 들면, 뷰 템플릿 영역에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등.
(5)Domains
- 도메인이라는 개발대상은 모든사람이 동일한 관점에서 이해할수 있또록 단순화 시킨것을 도메인 모델이라고함.
- @Entity영역 역시 도메인 모델
- 다만, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는것이 아님( VO도 도메인 영역)
#그래서 비지니스 처리는 누가해?
기존에 서비스로 비지니스 처리하던 방식을 트랜잭션 스크립트라고 함.
- 트랜잭션 스크립트(기존, 옛날)
모든 로직이 클래스 내부에서 처리.
서비스 계층이 무의미하며, 객체란 단순히 데이터 덩어리.
But.
우리는 도메인에서 비지니스를 처리.
서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장.
#등록 기능 만들기
수정, 조회는 거의 동일하니 생략.
PostApiController, PostSaveRequestDto, PostService 이 세가지가 필요.
(1)PostApiController(web 패키지)
(!)오류 (초판 오타), p.111
putMapping -> PostMapping
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;
@RequiredArgsConstructor
@RestController
public class PostApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
(2)PostSaveRequestDto(web.dto 패키지)
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity(){
return Posts.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
여기서 DTO클래스는 Entity클래스와 거의 유사함.
사용하는 이유는 Entity클래스는 데이터 베이스와 맞닿은 핵심 클래스 임.
-> 사소한 변경으로 인해 Entity클래스를 변경하는 것은 너무 큰 변경.
(3)PostService(service 패키지)
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;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
}
#롬복을 쓰는 이유?
클래스의 의존성관계가 변경될때마다 생성자코드를 직접 수정하는 번거로움을 줄여줌.
여기서는 @RequeiredArgsConstructor( final이 선언된 모든 필드를 인자값으로 하는 롬복)
#더티 체킹?
영속성 컨텍스트가 유지된 상태에서 Entity값만 변경하면 되니 Update쿼리를 날릴 필요가 없음.
#영속성 컨텍스트?
엔티티를 영구 저장하는 환경, 일종의 논리적 개념.
#조회기능은 실제 톰캣을 실행하여 확인
[1]application.properites에 다음 코드추가
spring.h2.console.enabled=true
[2]웹에 localhost:8080/h2-console로 접속
다음 처럼 입력후 connect
다음처럼 데이터 삽입.
[3] 브라우저로 API조회
localhost:8080/api/v1/posts/1
3.5 JPA Auditing으로 생성시간/수정시간 자동화하기
언제 만들어지고, 언제 수정되었는지는 차후 유지보수에 아주 중요한 정보.
-> DB에 삽입/갱신전 날짜 데이터의 등록/수정
-> 코드가 복잡해짐
-> JPA Auditing 사용!
#LocalDate 사용.
[1] BaseTimeEntity 선언
[ BaseTimeEntity ]
위치 : domain 패키지
모든 Entity의 상위 클래스가 되어 날짜 자동 관리.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
- @MappedSuperclass
JPA Entity클래스 들이 BaseTimeEntity를 상속할 경우 필드(createdDate, modifedDate)도 칼럼으로 인식.
->코드가 지저분해지는것 막아줌.
- @EntityListeners(AuditingEntityListerner.class)
BaseTimeEntity클래스에 Auditing 기능 포함.
- @CreatedDate
Entity가 생성되어 저장될때 시간이 자동 저장됨.
- @LastModifiedDate
조회한 Entity의 값을 변경할 떄 시간이 자동 저장됨.
[2] Posts클래스가 BaseTimeEntity 상속받도록 변경.
[3] Apllication 클래스에 JPA Auditing 어노테이션 활성화
활성화를 위해 어노테이션 하나 추가.
[4] JPA Audting 테스트 코드 작성
위치 : PostsRepositoryTest클래스
테스트 메소드를 하나더 추가.
@Test
public void BaseTimeEntity_등록(){
//given
LocalDateTime now = LocalDateTime.of(2019,6,4,0,0,0);
postRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
//when
List<Posts> postsList = postRepository.findAll();
//then
Posts posts = postsList.get(0);
System.out.println(">>>>>>>>>>>> createdDate="+posts.getCreatedDate()+ ", modifiedDate="+posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
'서적 공부 > 스프링부트 - [스프링부트와 AWS로 혼자 구현하는 웹서비스]' 카테고리의 다른 글
스프링 부트 - (6) AWS 서버 환경을 만들어 보자 (0) | 2020.04.07 |
---|---|
스프링 부트 - (5) 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (0) | 2020.03.24 |
스프링 부트 - (4) 머스테치로 화면 구성하기 (0) | 2020.03.23 |
스프링 부트 - (2) 스프링 부투에서 테스트 코드를 작성하자. (0) | 2020.03.11 |
스프링 부트 - (1) 인텔리제이로 스프링부트 시작하기 (0) | 2020.03.11 |