BackEnd/Spring

Spring Study 1주차 ~ 2,3장

Bubbles 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