ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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 개념이 잘 정리된 블로그

    https://velog.io/@adam2/JPA%EB%8A%94-%EB%8F%84%EB%8D%B0%EC%B2%B4-%EB%AD%98%EA%B9%8C-orm-%EC%98%81%EC%86%8D%EC%84%B1-hibernate-spring-data-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메소드를 통해 변경하면, 왜 변경하는지 그 의도가 잘 드러나지 않는다. 변경이 필요할 경우 엔티티 내부에 메서드를 생성하여 자기 자신이 자신을 변경하는 쪽이 좀 더 객체 지향적인다. 

    참고 : https://velog.io/@aidenshin/%EB%82%B4%EA%B0%80-%EC%83%9D%EA%B0%81%ED%95%98%EB%8A%94-JPA-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%9E%91%EC%84%B1-%EC%9B%90%EC%B9%99

     

    @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
Designed by Tistory.