▪️JPA
JPA는 서로 지향하는 바가 다른 2개 영역을 중간에서 패러다임 일치를 시켜주기 위한 기술이다.
- 관계형 데이터베이스는 어떻게 데이터를 저장할지에 초점이 맞춰진 기술
- 객체지향 프로그래밍 언어는 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술
개발자가 객체지향적으로 프로그래밍을 하면, JPA가 이를 관계형 DB에 맞게 SQL을 대신 생성해서 실행한다. 이로써 개발자는 더는 SQL에 종속적인 개발을 하지 않아도 된다.
▪️Spring Data JPA
Spring Data JPA는 구현체들을 좀 더 쉽게 사용하고자 추상화시킨 모듈이다.
JPA ← Hibernate ← Spring Data JPA
구현체 교체의 용이성: Hibernate 외에 다른 구현체로 쉽게 교체할 수 있다.
저장소 교체의 용이성: 관계형 DB외에 다른 저장소로 쉽게 교체할 수 있다.
▪️등록/수정/조회 API 만들기
스프링에서 Bean 주입받는 방식
- @Autowired
- setter
- 생성자 (@RequiredArgsConstructor)
- final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복 @이 대신 생성
PostSaveRequestDTO.java
@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();
}
}
Entity와 DTO가 유사하지만, 절대로 Entity 클래스를 Request/Response 클래스로 사용해서는 안된다!
Entity 클래스는 DB와 맞닿은 핵심 클래스다. View Layer,DB Layer의 역할 분리를 철저하게 해야 한다.
따라서 Entity 클래스와 Controller 에서 쓸 Dto는 분리해서 사용해야 한다.
이제 PostApiControllerTest를 통해 post를 Test해본다.
PostApiControllerTest.java
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiContollerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@BeforeEach
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@AfterEach
public void tearDown() throws Exception {
postsRepository.deleteAll();
}
// 인증된 모의(가짜) 사용자를 만들어서 사용한다.
// roles에 권한을 추가할 수 있다.
// 즉, 이 어노테이션으로 인해 ROLE_USER 권한을 가진 사용자가 API를 요청하는 것과 동일한 효과를 가지게 된다.
@DisplayName("Posts 등록")
@Test
@WithMockUser(roles = "USER")
public void Posts_등록() throws Exception {
// given
String title = "title";
String content = "content";
PostsSaveRequestDTO requestDTO = PostsSaveRequestDTO.builder()
.title(title)
.content(content)
.author("author")
.build();
String url = "http://localhost:" + port + "/api/v1/posts";
// when
// ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDTO, Long.class);
// mvc.perform : 생성된 MockMvc를 통해 API를 테스트 한다.
// 본문(Body) 영역은 문자열로 표현하기 위해 ObjectMapper를 통해 문자열 JSON으로 변환한다.
// MediaType.APPLICATION_JSON_UTF8이 deprecated가 되어 MediaType.APPLICATION_JSON으로 바꿔주었다.
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(requestDTO)))
.andExpect(status().isOk());
// then
// assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
// assertThat(responseEntity.getBody()).isGreaterThan(0L);
//
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
}
다음으로 조회,수정 API도 작성한다. (자세한 코드는 깃을 확인)
@Transactional
public Long update(Long id, PostsSaveRequestDTO requestDTO) {
// IllegalArgumentException이란?
// 적합하지 않거나(Illegal), 적절하지 못한 인자(매개 값)을 메서드에 넘겨주었을 때 발생
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
posts.update(requestDTO.getTitle(), requestDTO.getContent());
return id;
}
//Posts.class
public void update(String title, String content) {
this.title = title;
this.content = content;
}
이때 update 기능은 DB에 쿼리를 날리는 부분이 없다. 이는 JPA 영속성 컨텍스트 때문이다.
영속성 컨텍스트
- 엔티티를 영구 저장하는 환경 (일종의 논리적 개념)
- 트랜잭션 안에서 DB에서 데이터를 가져오면 영속성 컨텍스트가 유지된 상태
- 이 상태에서 해당 데이터 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영
- 즉, Entity 객체 값만 변경하면 별도로 update 쿼리를 날릴 필요 없음. => 더티 체킹
수정 기능 테스트 코드는 다음과 같다.
@DisplayName("Posts 수정")
@Test
@WithMockUser(roles = "USER")
public void Posts_수정() throws Exception {
// given
Posts savedPosts = postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updateId = savedPosts.getId();
String expectedTitle = "title2";
String expectedContent = "contents";
PostsUpdateRequestDTO requestDTO = PostsUpdateRequestDTO.builder()
.title(expectedTitle)
.content(expectedContent)
.build();
String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;
HttpEntity<PostsUpdateRequestDTO> requestEntity = new HttpEntity<>(requestDTO);
// when
// ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);
mvc.perform(put(url)
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(requestDTO)))
.andExpect(status().isOk());
// then
// assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
// assertThat(responseEntity.getBody()).isGreaterThan(0L);
// responseEntity의 출력 결과
// <
// 200 OK OK,
// 1,
// [Content-Type:"application/json",
// Transfer-Encoding:"chunked",
// Date:"Fri, 14 Apr 2023 00:36:36 GMT",
// Keep-Alive:"timeout=60",
// Connection:"keep-alive"]
// >
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
▪️JPA Auditing으로 생성시간/수정시간 자동화하기
Java8의 LocalDateTime은 기본 날짜 타입인 Date의 문제점을 제대로 고친 타입이다.
Date와 Calendar 클래스는 다음 문제가 있었다.
1. 불변 객체가 아니다.
- 멀티스레드 환경에서 언제든 문제 발생 가능
2. Calendar는 월(month) 값 설계가 잘못되었다.
- 10월을 나타내는 숫자값이 '9'임.
BaseTimeEntity.class
package springboot.springbootwebservice.domain;
import jakarta.persistence.EntityListeners;
import jakarta.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;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass // JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드들(createdDate, modifyDate)도 컬럼으로 인식하도록 한다.
@EntityListeners(AuditingEntityListener.class) // BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다. Audit은 감시하다, 감사하다라는 뜻으로 Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능
public abstract class BaseTimeEntity { // 추상 클래스는 구체적이지 않은 추상적인(abstract) 데이터를 담고 있는 클래스이다.
@CreatedDate // Entity가 생성되어 저장될 때 시간이 자동 저장된다.
private LocalDateTime createdDate;
@LastModifiedDate // 조회한 Entity의 값을 변경할 때 시간이 자동 저장된다.
private LocalDateTime modifiedDate;
}
그리고 Posts 클래스가 BaseTimeEntity를 상속받도록 변경한다.
public class Posts extends BaseTimeEntity {...
마지막으로 JPA Auditing 어노테이션들을 모두 활성화 하도록 Main클래스에 활성화해준다.
@EnableJpaAuditing // JPA Auditing 활성화 설정
@SpringBootApplication
public class SpringbootWebserviceApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebserviceApplication.class, args);
}
}
JPA Auditing 테스트 코드 작성
PostsRepositoryTest 클래스에 아래 테스트 메소드를 추가한다.이를 수행해보면 실제 시간이 잘 저장됨을 확인할 수 있다.
@Test
@DisplayName("BaseEntity 등록")
public void BaseEntity_등록() throws Exception {
// given
LocalDateTime now = LocalDateTime.of(2023, 4, 15, 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() + ", modifyDate=" + posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
// 출력
// >>>>>>>>>>>> createDate=2023-04-15T13:24:03.496347, modifyDate=2023-04-15T13:24:03.496347
// BaseTimeEntity를 통해 JAP Auditing 기능을 설정해주어 create, modify date를 개발자가 일일이 지정해 줄 필요 없게 되었다.
// JAP Auditing가 자동으로 create, modify date를 자동 생성해주게 된다.성
}
▪️정리
이번 장에서는 다음을 배웠다.
- JPA / Hibernate / Spring Data JPA 의 관계
- Spring Data JPA를 이용하여 관계형 데이터베이스를 객체 지향적으로 관리하는 방법
- JPA의 더티 체킹을 이용하면 Update 쿼리 없이 테이블 수정이 가능하다는 것
- JPA Auditing을 이용하여 등록/수정 시간을 자동화 하는 방법
출처
스프링 부트와 AWS로 혼자 구현하는 웹 서비스
'개발프로젝트' 카테고리의 다른 글
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 06- AWS 서버 환경을 만들어 보자 -AWS EC2 (2) | 2024.01.04 |
---|---|
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 04~05- 스프링 시큐리티와 OAuth2로 로그인 기능 구현하기 (2) | 2024.01.03 |
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 02- 테스트 (0) | 2023.12.20 |
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 01 (0) | 2023.12.19 |
utc4 (2) | 2023.11.15 |