-
Spring Study 1주차 ~ 2,3장BackEnd/Spring 2022. 3. 30. 18:46
새롭게 백엔드 프로젝트를 하기 위해서 스프링 스터디를 시작했다. 프로젝트 팀에서 스터디 먼저 몇주 해보고 프로젝트 간단하게 해보기로 했다.
"스프링부트와 AWS로 혼자 구현하는 웹 서비스" 책을 참고해서 공부를 하고 있는데, 이번 주차에는 2~4단원을 공부해오기로 했다. 잠깐 소감을 써보자면 node.js랑 비슷한듯 다른듯... 신기하다.
📌 2장 스프링부트에서 테스트 코드 작성하기
2.1 테스트 코드를 이용한 TDD 개발 : test driven development
항상 실패하는 테스트 먼저 작성 - 테스트가 통과하는 프로덕션 코드 작성 - 테스트 통과 후 프로덕션 코드 리팩토링 3단계로 진행된다. (레드 그린 사이클)
이 레드 그린 사이클의 첫 단계에서 작성하는 단위 테스트 코드를 작성하는 방법을 배운다.
단위테스트의 장점 2가지를 뽑자면, 기능에 대한 테스트를 통해 빠르게 문제를 발견하고 해결할 수 있으며, 시스템 자체의 문서로써 기능한다. 후에 리팩토링, 라이브러리 버전 변경 등에서 기존 기능이 올바르게 작동하는지 테스트할 수 있다.
2.2 HelloController 테스트 코드 작성하기
package firstSpring.practice.practice; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
✅ Application Class : 프로젝트의 메인 클래스
✅ @SpringBootApplication : 스프링 부트의 설정과 bean 읽기, 생성 등을 자동으로 해준다. 이 annotation이 있는 위치부터 설정을 읽으니까 프로젝트의 최상단에 위치해야 함.
✅ SpringApplication.run() : 내장 WAS(web-application service)를 실행하는 코드이다. 내장 WAS를 사용하는 이유는 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있기 때문이다. TOMCAT이 기본이지만, undertow 등 다른 내장 WAS도 사용할 수 있다. 최근에는 docker 등의 클라우드 환경으로 외부 WAS사용하는 일도 있는 것 같다. 이에 대해서는 약간 의견이 갈리는 듯 하다...
- web 패키지 생성 : 컨트롤러 클래스들을 담아둔다.
실습을 위해 HelloController 클래스를 생성한다.
package firstSpring.practice.practice.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } }
✅ @RestController : 컨트롤러를 json을 반환하는 형태로 만들어줌. 더이상 ResponseBody를 각 메소드마다 선언할 필요가 없다.
✅ @GetMapping : Get요청을 받을 수 있는 api를 만들어준다. 현재 코드로는 /hello경로로 들어오는 Get요청에 대해 hello string을 반환해준다.
- HelloControllerTest : HelloController를 test할 클래스를 작성해준다.
package firstSpring.practice.web; import firstSpring.practice.practice.web.HelloController; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @RunWith(SpringRunner.class) // Junit에 내장된 실행자 외에 SpringRunner라는 실행자 사용. SpringBoot-JUnit연결 @WebMvcTest(controllers = HelloController.class) // @Controller, @ControllerAdvice등 사용 가능 public class HelloControllerTest { @Autowired // 스프링이 관리하는 빈을 주입받음 private MockMvc mvc; // 스프링 mvc 테스트의 시작점, 이 클래스를 통해 http메소드 API테스트 가능 @Test public void hello가_리턴된다() throws Exception { String hello = "hello"; mvc.perform(get("/hello")) // MockMvc를 통해 /hello주소로 get요청 .andExpect(status().isOk()) //status 검증 .andExpect(content().string(hello)); // content 검증(hello리턴 맞니?) } }
✅ bean : 자바 IOC 컨트롤러가 관리하는 자바 객체를 의미한다. 즉, 스프링에 의해 생성되고 관리되는 객체
IOC : Inversion Of Control의 약자, 기존의 자바처럼 원하는 객체를 new를 통해 직접 생성하는 것이 아닌, 스프링에 의해 관리당하는 객체를 사용한다. 제어의 역전이 이런 의미이다.
✅ @Autowired : 생성자, setter, 필드 에 사용 가능. @Repository 어노테이션을 통해 해당 repository를 bean으로 등록할 수 있다.
✅ .andExpect() : 이 친구를 통해 결과를 검증할 수 있다. status, 본문 내용 등을 검증할 수 있다.
2.3 롬복 소개 및 설치하기
✅ Lombok?
- getter, setter, tostring 등의 함수들을 컴파일 과정에서 자동으로 생성해주는 라이브러리이다. lombok 어노테이션만 추가되어 있고 생성자 관련 함수들은 작성되어 있지 않기 때문의 코드의 간결성을 높여주는 장점이 있다.
dependencies { implementation('org.projectlombok:lombok') }
의존성을 추가하고 플러그인을 설치해준다. Compiler>Annotation Processor > Enable Annotation Processing 옵션을 체크해주면 사용할 준비가 끝났다.
2.4 Hello Controller 코드를 롬복으로 전환하기
- web 패키지에 dto 패키지를 추가한다. 이 패키지는 모든 응답 dto를 담을 패키지이다.
* dto : 로직을 가지지 않는 데이터 객체이고 getter/setter 메소드만 가진 클래스
package firstSpring.practice.practice.web.dto; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter // 선언된 모든 필드의 get 메소드 생성해줌 @RequiredArgsConstructor // 선언된 모든 final 필드가 포함된 생성자 생성 public class HelloResponseDto { private final String name; private final int amount; }
✅ @Getter : lombok의 어노테이션으로, 이를 선언하면 get메소드를 자동으로 생성해준다. 코드상엔 안 나타나서 좀 더 깔끔해보인다.
이제 HelloResponseDto를 테스트할 코드를 작성해보자.
package firstSpring.practice.web.dto; import firstSpring.practice.practice.web.dto.HelloResponseDto; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class HelloResponseDtoTest { @Test public void lombok_test() { //given String name = "test"; int amount = 1000; //when HelloResponseDto dto = new HelloResponseDto(name, amount); //then // assertThat : assertj(테스트 검증 라이브러리)의 검증 메소드 assertThat(dto.getName()).isEqualTo(name); // 검증하고 싶은 대상을 메소드 인자로 받음, isEqualTo와 비교 assertThat(dto.getAmount()).isEqualTo(amount); } }
✅ 이 때, assertj의 assertThat을 사용을 했다. Junit에도 같은 기능의 메소드가 존재하는데, Junit의 경우 is()와 같은 CoreMatchers라이브러리가 필요하고, assertj가 좀 더 자동완성이 편리해서 assertj를 사용한다고 한다.
✅ 테스트를 진행하면 위처럼 테스트가 성공한 것을 확인할 수 있다. 즉, @Getter로 get메소드가, @RequiredArgsConstructor로 생성자가 자동 생성된 것을 알 수 있다.
- HelloController에 dto 사용할 수 있는 api도 추가해보자.
@GetMapping("/hello/dto") public HelloResponseDto helloResponseDto (@RequestParam("name") String name, @RequestParam("amount") int amount) { return new HelloResponseDto(name, amount); }
✅ @RequestParam : API로 넘긴 파라미터를 가지고 오는 annotation. 좀 더 찾아보니 URL 통해 parameter로 값을 받아오는 경우에 사용하고, @RequestBody의 경우 JSON이나 XML등의 객체 받을 때, 즉 POST방식에서 사용한다.
최종적으로 HelloControllerTest.java코드를 마무리하고, 테스트를 진행한다.
package firstSpring.practice.web; import firstSpring.practice.practice.web.HelloController; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @RunWith(SpringRunner.class) // Junit에 내장된 실행자 외에 SpringRunner라는 실행자 사용. SpringBoot-JUnit연결 @WebMvcTest(controllers = HelloController.class) // @Controller, @ControllerAdvice등 사용 가능 public class HelloControllerTest { @Autowired // 스프링이 관리하는 빈을 주입받음 private MockMvc mvc; // 스프링 mvc 테스트의 시작점, 이 클래스를 통해 http메소드 API테스트 가능 @Test public void hello가_리턴된다() throws Exception { String hello = "hello"; mvc.perform(get("/hello")) // MockMvc를 통해 /hello주소로 get요청 .andExpect(status().isOk()) //status 검증 .andExpect(content().string(hello)); // content 검증(hello리턴 맞니?) } @Test public void helloDto_return() throws Exception { String name = "hello"; int amount = 1000; mvc.perform(get("/hello/dto") .param("name", name) // param : api 테스트 시 사용될 요청 파라미터 설정, 문자열만 가능 .param("amount", String.valueOf(amount))) .andExpect(status().isOk()) .andExpect(jsonPath("$.name", is(name))) // jsonPath : $기준으로 필드명 명시, json 응답값을 필드별로 검증 .andExpect(jsonPath("$.amount", is(amount))); } }
📌 3장 스프링 부트에서 JPA로 데이터베이스 다뤄보기
3.1 JPA 소개
- SQL 종속적인 기존 방식에서 벗어나, 객체지향적으로 프로그래밍 하기 위해 등장. 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행한다.
- 자바 ORM(object relation mapping, 객체와 관계를 매핑 ~ 객체를 통해 간접적으로 Database를 조작)
🔽 JPA 개념이 잘 정리된 블로그
✅ Spring Data JPA, Hibernate, JPA?
❗ JPA : java persistence API - 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스. (라이브러리 아님)
❗ Hibernate : JPA 명세의 구현체이다. 예를 들면 JPA 의 Entnity Manager을 상속받아 Session, Entity Transaction을 상속받아 Transaction으로 사용. 구현체이기에 JPA사용을 위해 반드시 Hibernate 사용할 필요는 X
❗ Spring Data JPA : JPA를 사용하기 편하게끔 만든 모듈로, JPA를 한 단계 추상화시킨(내부적으로는 Entity Manager이용) Repository를 사용.
참고 및 이미지 출처 블로그 : https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/
✅ 책에서는 이렇게 Spring Data JPA가 등장한 이유로 구현체와 저장소(RDBMS외에 다른 저장소. MongoDB등) 교체가 용이하다는 점을 뽑는다.
3.2 프로젝트에 Spring Data JPA 적용하기
- build.gradle파일에 jpa의존성을 등록한다.
dependencies { implementation('org.springframework.boot:spring-boot-starter-web') implementation('org.projectlombok:lombok') implementation('org.springframework.boot:spring-boot-starter-data-jpa') runtimeOnly('com.h2database:h2') testImplementation('org.springframework.boot:spring-boot-starter-test') }
✅ spring-boot-starter-data-jpa : 스프링부트 버전에 맞춰 JPA 라이브러리 관리해준다. Sprign Data JPA 추상화 라이브러리.
✅ h2 : 인메모리 관계형 데이터베이스, 로컬 환경에서 구동하고 jpa 테스트 시 사용한다.
- Domain 패키지 : 소프트웨어의 요구사항, 문제 영역을 담을 패키지
게시판 및 회원 기능을 개발하기 위해 posts 패키지와 Posts 클래스를 먼저 생성한다.
package firstSpring.practice.practice.domain.posts; 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 //위의 두개는 lombok, 얘는 기본 생성자 자동 추가. public Posts() {}와 같은 효과. @Entity // JPA annotation, 테이블과 링크될 클래스 public class Posts extends BaseTimeEntity { // 실제 DB와 매칭될 클래스, 직접 쿼리 날리는 것이 아닌 entity 수정을 통해 작업 @Id // 해당 테이블의 PK, 가능한 pk는 long type, auto_increment : mysql ~ bigint @GeneratedValue(strategy = GenerationType.IDENTITY) // PK 생성 규칙 private Long id; @Column(length = 500, nullable = false) //기본적으로 필드는 다 column, 옵션 주고 싶을 때 사용. 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; } }
✅ 왜 Setter는 없을까?
setter 메소드가 생기면 인스턴스 값들이 언제 어디서 변해야하는지 명확해지지 않는다.
객체의 값을 변경할 때 set메소드를 통해 변경하면, 왜 변경하는지 그 의도가 잘 드러나지 않는다. 변경이 필요할 경우 엔티티 내부에 메서드를 생성하여 자기 자신이 자신을 변경하는 쪽이 좀 더 객체 지향적인다.
✅ @Builder : builder 패턴을 통해 어느 필드에 어떤 값을 채워야 할지 좀 더 명확하게 볼 수 있음.
- PostsRepository : DB layer 접근자, JpaRepository 상속시 기본CRUD메소드 자동 생성됨.
package firstSpring.practice.practice.domain.posts; import org.springframework.data.jpa.repository.JpaRepository; public interface PostsRepository extends JpaRepository<Posts, Long> { }
- @Repository 추가할 필요 X, entity 클래스와 entity repository 함께 위치해야함. -> domain 패키지에서 함께 관리
3.3 Spring Data JPA 테스트 코드 작성하기
package firstSpring.practice.domain.posts; import firstSpring.practice.practice.domain.posts.Posts; import firstSpring.practice.practice.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 // H2 데이터베이스 자동 실행. public class PostsRepositoryTest { @Autowired PostsRepository postsRepository; @After // 단위 테스트가 끝날 때마다 수행하는 메소드 public void cleanup() { postsRepository.deleteAll(); // 테스트 용으로 들어간 데이터들 모두 삭제 } @Test public void getPostList() { String title = "테스트 게시글"; String content = "테스트 본문"; postsRepository.save(Posts.builder()// insert, update 쿼리 실행. id값 있으면 update, 없으면 insert .title(title) .content(content) .author("jojoldu@gmail.com") .build()); List<Posts> postsList = postsRepository.findAll(); // 모든 데이터 조회 Posts posts = postsList.get(0); assertThat(posts.getTitle()).isEqualTo(title); assertThat(posts.getContent()).isEqualTo(content); } }
✅ After : 단위 테스트가 끝날 때마다 수행되는 메소드를 지정, 배포 전 전체 테스트 수행 시 테스트간 데이터 침범을 막기 위해서 사용한다.
✅ postsRepository.save : insert, update쿼리 실행
✅ postsRepository.findAll : 모든 데이터 조회해오는 메소드
테스트를 실행해본다. 이때, 실제 쿼리를 보고 싶다면 resources/application.properties 파일을 생성한 후 아래 코드를 ㅣ입력해준다.
spring.jpa.show_sql=true
이러면 아래 사진처럼 insert문 등 우리가 알고 있는 쿼리문을 발견할 수 있다.
3.4절 등록/수정/조회 API 만들기
* 체크 해야 할 것
✅ Request data를 받을 Dto
✅ API 요청을 받을 Controller
✅ 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
* 스프링 웹 계층의 각 영역
Web Layer @Controller, JSP/Freemaker 등의 뷰 템플릿 영역
외부 요청과 응답에 대한 전반적인 영역을 이야기Service Layer @Service에 사용되는 영역
Controller와 Dao(data access object)의 중간 영역, @Transactionanl이 사용되어야 하는 영역Repository Layer 데이터 저장소에 접근하는 영역(database등) Dtos Dto : 계층 간의 데이터 교환을 위한 객체
뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체등이 이들을 이야기Domain Model 개발 대상을 모든 사람들이 동일한 관점에서 이해하고 공유할 수 있도록 단순화 시킨 것.
@Entity 사용된 영역, VO(값 객체) 등이 포함Domain이 비즈니스 처리를 담당한다.
PostsApiController
package firstSpring.practice.practice.web; import firstSpring.practice.practice.web.dto.PostsResponseDto; import firstSpring.practice.practice.web.dto.PostsSaveRequestDto; import firstSpring.practice.practice.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); } }
PostsService
package firstSpring.practice.practice.service.posts; import firstSpring.practice.practice.domain.posts.Posts; import firstSpring.practice.practice.domain.posts.PostsRepository; import firstSpring.practice.practice.web.dto.PostsListResponseDto; import firstSpring.practice.practice.web.dto.PostsResponseDto; import firstSpring.practice.practice.web.dto.PostsSaveRequestDto import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; @RequiredArgsConstructor @Service public class PostsService { private final PostsRepository postsRepository; @Transactional public Long save(PostsSaveRequestDto requestDto) { return postsRepository.save(requestDto.toEntity()).getId(); }
✅ @Autowired / setter / 생성자
- bean을 주입받는 방식 3가지, 생성자가 가장 권장된다.
- @RequiredArgsConstructor이 final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성
PostsSaveRequestDto
package firstSpring.practice.practice.web.dto; import firstSpring.practice.practice.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(); } }
✅ Entity 클래스와 거의 유사, Dto 클래스를 추가 생성했다. Entity 클래스는 DB와 맞닿은 핵심 클래스이기 때문에 REQUEST/RESPONSE 클래스로 사용하면 안된다.
✅ request/response 용 dto는 view를 위한 클래스이기 때문에 자주 변경이 필요
PostsApiControllerTest
package firstSpring.practice.web; import firstSpring.practice.practice.domain.posts.Posts; import firstSpring.practice.practice.domain.posts.PostsRepository; import firstSpring.practice.practice.web.dto.PostsSaveRequestDto; import firstSpring.practice.practice.web.dto.PostsUpdateRequestDto; 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.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.junit4.SpringRunner; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class PostsApiControllerTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Autowired private PostsRepository postsRepository; @After public void tearDown() throws Exception { postsRepository.deleteAll(); } @Test public void Posts_등록된다() 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); } }
✅ Web-mvc test : jpa 기능 작동 X, 외부 연동과 관련된 부분(Controller등)만 활성화
✅ JPA 기능까지 한번에 테스트할 때에는 @SpringBootTest와 TestRestTemplate을 사용.
그 외 쭉 수정, 조회 기능 실습도 같은 방식으로 진행.
- PostsApiController에 추가
@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); }
- PostsResponseDto
package firstSpring.practice.practice.web.dto; import firstSpring.practice.practice.domain.posts.Posts; import lombok.Getter; @Getter public class PostsResponseDto { private Long id; private String title; private String content; private String author; public PostsResponseDto(Posts entity) { this.id = entity.getId(); this.title = entity.getTitle(); this.content = entity.getContent(); this.author = entity.getAuthor(); } }
✅ PostsResponseDto생성자 부분에서 entity받아서 처리. (entity필드중 일부만 사용하기 때문)
- PostsUpdateRequestDto
package firstSpring.practice.practice.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; } }
- Posts에 추가
public void update(String title, String content) { this.title = title; this.content = content; }
- PostsService에 추가
@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); }
PostsApiControllerTest
@Test public void Posts_수정된다() 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); }
실제로 현재 프로젝트의 H2 데이터베이스를 확인해보자
- application.properties에 추가
spring.h2.console.enabled=true
- http://localhost:8080/h2-console접속, jdbc:h2:mem:testdb로 JDBC URL을 바꿔주고 접속.
- Connect를 누르면 다음처럼 직접 쿼리를 날려볼 수 있다. Posts테이블이 왼쪽에 있어야 함.
아직 데이터가 없으니 Insert문 실행 후 확인해보았다.
⏫ 경로를 위처럼 접속하면 내가 넣은 데이터를 확인할 수 있다!
3.5 JPA Auditing으로 생성시간/수정시간 자동화하기
해당 데이터의 생성시간과 수정시간을 넣는 작업 (created at, last modified)
먼저, BaseTimeEntity클래스 생성. (domain패키지)
package firstSpring.practice.practice.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 // jpa entity 클래스들이 baseTimeEntity 상속할 경우 createdDate, modifiedDate 까지 칼럼으로 인식 @EntityListeners(AuditingEntityListener.class) public abstract class BaseTimeEntity { @CreatedDate // entity 생성, 저장될 때 시간 자동 저장. private LocalDateTime createdDate; @LastModifiedDate // 조회한 entity 값 변경시 시간 자동 저장. private LocalDateTime modifiedDate; }
✅ BaseTimeEntity는 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할.
✅ 이 클래스를 Posts 클래스가 상속, JPA auditing 어노테이션 활성화(application클래스)
public class Posts extends BaseTimeEntity { ... }
@EnableJpaAuditing @SpringBootApplication public class Application { ... }
✅ 마지막으로 PostsRepositoryTest 클래스에 테스트 메소드를 하나 더 만들어보자
@Test public void BaseTimeEntity_등록() { LocalDateTime now = LocalDateTime.of(2022,3,26,0,0,0); postsRepository.save(Posts.builder() .title("title") .content("content") .author("author") .build()); List<Posts> postsList = postsRepository.findAll(); Posts posts = postsList.get(0); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>> createdDate = " + posts.getCreatedDate()+" modified Date = " + posts.getModifiedDate()); assertThat(posts.getCreatedDate()).isAfter(now); assertThat(posts.getModifiedDate()).isAfter(now); }
위 사진처럼 날짜가 담기는 것을 확인할 수 있다!
http://www.yes24.com/Product/Goods/83849117
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24
가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링
www.yes24.com
'BackEnd > Spring' 카테고리의 다른 글
Spring 입문 강의 섹션 0 ~ 섹션 3 (0) 2022.07.29 Spring Study 3주차 8장 (0) 2022.05.01 Spring Study 2주차 ~ 6장, 7장 (0) 2022.04.07 Spring Study 2주차 ~ 5장 (0) 2022.04.06 Spring Study 1주차 ~ 4장 (0) 2022.03.31