Spring Data JPA λ‘ λ±λ‘/μ‘°ν/μμ API λ§λ€κΈ°
λ±λ‘/μ‘°ν/μμ API λ§λ€κΈ°
κ° μ½λ μμΈ μ€λͺ μ μ£ΌμμΌλ‘ λ¬μλ€. λ Entity ν΄λμ€μ Repository ν΄λμ€λ π Spring boot μ Spring Data JPA μ μ©νκΈ° μμ μμ±νλ€.
DTO λ§λ€κΈ°
DTOλ λ°μ΄ν°λ₯Ό View Layer μ μ£Όκ³ λ°μ λ(request, response) κ°μΈλ κ°μ²΄μ΄λ€. μΈλΆμ λ ΈμΆλλ κ°μ²΄μ΄λ―λ‘ μμ λΉμ¦λμ€ λ‘μ§μ λ΄μ§ μλλ€. μ¬κΈ°μλ Entity μ μ μ¬νλ, μ΄λ κ² View μ μ£Όκ³ λ°λ λ°μ΄ν°λ€μ μμ£Ό μμ μ΄ μΌμ΄λλ€. Entity λ DBμ λ°μ ν κ°μ²΄κ³ λΉμ¦λμ€ λ‘μ§λ λ΄κ³ μμΌλ―λ‘ μμ£Ό μμ νκ² λλ©΄ κ΄λ ¨λ μ¬λ¬ ν΄λμ€λ€μ μν₯μ λΌμΉκΈ° λλ¬Έμ Entity λ₯Ό dto μ©λλ‘ μ°μ§ μλλ€. μ¦, Entity ν΄λμ€μ Controller μμ μΈ DTO λ λΆλ¦¬ν΄μΌ νλ€.
src/main/java/com.study.springbootaws/web/dto/PostsResponseDto
package com.study.springbootaws.web.dto;
import com.study.springbootaws.domain.posts.Posts;
import lombok.Getter;
@Getter
public class PostsResponseDto {
private Long id;
private String title;
private String content;
private String author;
//repository λ₯Ό ν΅ν΄ μ‘°νν entity λ₯Ό dto λ‘ λ³ν μ©λ
public PostsResponseDto(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.content = entity.getContent();
this.author = entity.getAuthor();
}
}
src/main/java/com.study.springbootaws/web/dto/PostsSaveRequestDto
package com.study.springbootaws.web.dto;
import com.study.springbootaws.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;
}
//resquest dto λ‘ λ°μ Posts κ°μ²΄λ₯Ό entity ννμ¬ μ μ₯νλ μ©λ
public Posts toEntity() {
return Posts.builder().title(title).content(content).author(author).build();
}
}
src/main/java/com.study.springbootaws/web/dto/PostsUpdateRequestDto
package com.study.springbootaws.web.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content) {
this.title = title;
this.content = content;
}
}
Entity μ½λ μμ
Spring Data Jpa λ κΈ°λ³Έμ μΌλ‘ JPA μμμ± μ»¨ν μ€νΈ μ μ§λ₯Ό μ 곡νλ€. μ΄ μνμμ ν΄λΉ λ°μ΄ν°μ κ°μ λ³κ²½νλ©΄ μλμΌλ‘ λ³κ²½μ¬νμ΄ DBμ λ°μνλ€. μ¦, λ³λλ‘ Update 쿼리λ₯Ό λ λ¦¬μ§ μμλ, λ°μ΄ν°λ§ λ³κ²½νλ©΄ μμμ λ³κ²½λλ―λ‘ μμ κΈ°λ₯μ μλμ²λΌ Entity μ update λ©μλλ₯Ό λ§λ€μ΄μ€ ꡬννλ€.
src/main/java/com.study.springbootaws/domain/posts/Posts
...
public class Posts {
...
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
Service λ§λ€κΈ°
보ν΅μ Service μ λΉμ¦λμ€ λ‘μ§μ ꡬννλ€. κ·Έκ²μ νΈλμμ μ€ν¬λ¦½νΈλΌκ³ νλλ°, κ·Έλ¬λ€λ³΄λ μλΉμ€ κ³μΈ΅μ΄ 무μλ―Έν΄μ§λ€. μλΉμ€ κ³μΈ΅μμλ νΈλμμ κ³Ό λλ©μΈ κ° μμλ§μ 보μ₯ν΄μ€μΌ νλ€. λΉμ¦λμ€ λ‘μ§μ λλ©μΈ μμ μ²λ¦¬νλ€.
src/main/java/com.study.springbootaws/service/PostsService
package com.study.springbootaws.service;
import com.study.springbootaws.domain.posts.Posts;
import com.study.springbootaws.domain.posts.PostsRepository;
import com.study.springbootaws.web.dto.PostsResponseDto;
import com.study.springbootaws.web.dto.PostsSaveRequestDto;
import com.study.springbootaws.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@RequiredArgsConstructor //final or NonNull μ΅μ
νλλ₯Ό μ λΆ ν¬ν¨ν μμ±μ λ§λ€μ΄μ€
@Service
public class PostsService {
//@Autowired μ¬μ© μ§μλ¨ -> @RequiredArgsConstructor λ‘ μμ±λλ μμ±μλ‘ μ£Όμ
λ°κΈ° μν΄ final λΆμ.
private final PostsRepository postsRepository;
@Transactional //db νΈλμμ
μλμΌλ‘ commit ν΄μ€
public Long save(PostsSaveRequestDto requestDto) {
//dto λ₯Ό entity ν ν΄μ repository μ save λ©μλλ₯Ό ν΅ν΄ db μ μ μ₯.
//μ μ₯ ν μμ±ν id λ°νν΄μ€
return postsRepository.save(requestDto.toEntity()).getId();
}
@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto) {
Posts posts = postsRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("ν΄λΉ κ²μκΈμ΄ μμ΅λλ€. id="+id));
//JPA μ μμμ± μ»¨ν
μ€νΈ λλΆμ entity κ°μ²΄μ κ°λ§ λ³κ²½νλ©΄ μλμΌλ‘ λ³κ²½μ¬ν λ°μν¨!
//λ°λΌμ repository.update λ₯Ό μ°μ§ μμλ λ¨.
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);
}
}
Controller λ§λ€κΈ°
src/main/java/com.study.springbootaws/web/PostsApiController
package com.study.springbootaws.web;
import com.study.springbootaws.service.PostsService;
import com.study.springbootaws.web.dto.PostsResponseDto;
import com.study.springbootaws.web.dto.PostsSaveRequestDto;
import com.study.springbootaws.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto) {
return postsService.save(requestDto);
}
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) {
return postsService.update(id, requestDto);
}
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById(@PathVariable Long id) {
return postsService.findById(id);
}
}
λ±λ‘/μμ api ν μ€νΈνκΈ°
src/test/java/com.study.springbootaws/web/PostsApiControllerTest
μ΄μ 컨νΈλ‘€λ¬ ν μ€νΈμ λ¬λ¦¬, Jpa λ₯Ό μ¬μ©νκΈ° λλ¬Έμ @SpringbootTest μ TestRestTemplate μ μ¬μ©νλ€.
package com.study.springbootaws.web;
import com.study.springbootaws.domain.posts.Posts;
import com.study.springbootaws.domain.posts.PostsRepository;
import com.study.springbootaws.web.dto.PostsSaveRequestDto;
import com.study.springbootaws.web.dto.PostsUpdateRequestDto;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
//webmvctest λ jpaκ° λμνμ§ μμ.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
@LocalServerPort
private int port;
//mockmvc μ resttemplate λ λ€ μ»¨νΈλ‘€λ¬ ν
μ€νΈ λΌμ΄λΈλ¬λ¦¬μ. testresttemplate μ 컨ν
μ΄λλ₯Ό μ§μ μ€νμν€λ―λ‘ ν΄λΌμ΄μΈνΈ μ
μ₯μμ ν
μ€νΈν λ μ¬μ©ν¨.
//mockmvc λ 컨ν
μ΄λ μ§μ μ€ν μνλ―λ‘ λΉμ¦λμ€ λ‘μ§μ λ¬Έμ μλμ§λ₯Ό νμΈν¨.
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@AfterEach
public void tearDown() throws Exception {
postsRepository.deleteAll();
}
@Test
public void insertPosts() throws Exception {
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";
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
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);
}
@Test
public void updatePosts() throws Exception {
Posts savedPosts = postsRepository.save(Posts.builder().title("title").content("content").author("author").build());
Long updateId = savedPosts.getId();
String expectedTitle = "title2";
String expectedContent = "content2";
PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder().title(expectedTitle).content(expectedContent).build();
String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;
HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
save ν μ€νΈ
update ν μ€νΈ
μ‘°ν ν μ€νΈ
μ‘°ν ν μ€νΈλ μ§μ ν°μΊ£ μλ²λ₯Ό μ€νν΄μ νμΈν κ²μ΄λ€. H2 DBλ₯Ό μ°κΈ° λλ¬Έμ μΉ μ½μμμ μ§μ μ κ·Όν΄μΌ νλ€.
src/main/resources/application.properties
μΉ μ½μ μ΅μ μ νμ±ννλ€.
...
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb
Application ν΄λμ€μ main λ©μλλ₯Ό μ€ννκ³ http://localhost:8080/h2-console μ μ μνλ€. μλμ²λΌ μμ ν΄μ£Όκ³ connect λ₯Ό ν΄λ¦νλ€. JDBC URL μ μ΄μ mysql λ‘ μΏΌλ¦¬ λ³ν properties λ³κ²½ μ μλμ²λΌ μ λ ₯νλ url μ λ°λΌ μ μΌλ©΄ λλ€.
...
spring.datasource.hikari.jdbc-url=jdbc:h2:mem://localhost/~/testdb;MODE=MYSQL
connect λ₯Ό λλ₯΄λ©΄ μλμ²λΌ λ¬λ€. posts κ° μμ±λμ΄μλμ§ νμΈνκ³ insert into posts (author, content, title) values ('author', 'content', 'title'); λ¬Έμ run μν¨λ€.
localhost:8080/api/v1/posts/1 λ‘ λ€μ΄κ° μ‘°νν΄μ λμ€λ©΄ μ±κ³΅!!
JPA Auditing μΌλ‘ μμ±/μμ μκ° μλμΌλ‘ λ£κΈ°
μμ±/μμ μκ°μ μ°¨ν μ μ§λ³΄μμ κ΅μ₯ν μ€μν μ 보λ€. DB μ¬κΈ°μ κΈ°μ λ€μ΄κ°κ² λλλ°, λͺ¨λ Entity μ νλνλ λ£μ΄μ£Όλ μμ μ JPA Auditing μΌλ‘ ν΄κ²°ν μ μλ€.
src/main/java/com.study.springbootaws/domain/BaseTimeEntity
package com.study.springbootaws.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
@MappedSuperclass //λΆλͺ¨ ν΄λμ€(BaseTimeEntity) μ νλλ€λ μ λΆ μ»¬λΌμΌλ‘ μΈμνκ² ν¨.
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate //Entity κ° μμ±λ λ μλμΌλ‘ μμ±/μμ μκ° μ½μ
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
src/main/java/com.study.springbootaws/domain/posts/Post
post Entity ν΄λμ€μ BaseTimeEntity ν΄λμ€λ₯Ό μμμμΌμ€λ€.
...
public class Posts extends BaseTimeEntity {
...
src/main/java/com.study.springbootaws/Application
Application μ½λμ jpa auditingμ νμ±ννλ μ΄λ Έν μ΄μ μ λΆμ¬μ€λ€.
...
@EnableJpaAuditing //jpa auditing νμ±ν
public class Application {
...
ν μ€νΈνκΈ°
src/test/java/com.study.springbootaws/domain/posts/PostsRepositoryTest
PostsRepositoryTest μ BaseTimeEntityTest μ½λλ₯Ό μΆκ°ν΄μ£Όκ³ μ€ννλ©΄ λλ€.
...
@SpringBootTest
public class PostRepositoryTest {
...
@Test
public void insertBaseTileEntity() {
LocalDateTime now = LocalDateTime.now();
postsRepository.save(Posts.builder().title("title").content("content").author("author").build());
List<Posts> postsList = postsRepository.findAll();
Posts posts = postsList.get(0);
System.out.println(">>>>>>>>> createDate = " + posts.getCreatedDate() + ", modifiedDate = " + posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
}
μ±κ³΅!!