[백엔드 첫 걸음] API 수정(Update) 기능 실습 (스프링)

khyojun·2022년 8월 15일
1
post-thumbnail

오늘은 수정 기능에 대해서 실습을 해보자.


📂/main/java/web/PostsApiController.java

@RequiredArgsConstructor
@RestController
public class PostApiController {

    private final PostsService postsService;
	~~~
    // 수정
     @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);
    }

}
  • @PathVariable:이 어노테이션을 사용하게 되면은 uri를 추출하여서 해당하는 값을 가져온다.
    • ex) /api/v1/posts/{id} -> @PathVariable을 통하여서 {} 중괄호 안에 있는 id를 가져온다. 그러면 만약 혹시나 하나만 있을땐 이럴 수 있다 치는데 2개 이상일 경우에는? 어떻게 해야 할까?
      방법은 여러가지가 있지만..
      1. @PathVariable 여러 개 사용하기
      2. Map 사용해서 묶어서 사용하기 등등의 방법이 있다.
  • @RequestBody:이 어노테이션이 붙은 파라미터에는 http요청의 본문(body)이 그대로 전달된다.
    일반적인 GET/POST의 요청 파라미터라면 @RequestBody를 사용할 일이 없을 것이다.(@Put일때 사용)
    반면에 xml이나 json기반의 메시지를 사용하는 요청의 경우에 이 방법이 매우 유용하다.
    HTTP 요청의 바디내용을 통째로 자바객체로 변환해서 매핑된 메소드 파라미터로 전달해준다. 

📂/main/java/web/dto/PostsResponseDto.java

package com.khyojun.admin.springboot.web.dto;
import com.khyojun.admin.springboot.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();
  }
}

PostResponseDto는 Entity의 필드 중 일부만 사용하기위하여서 Entity를 인자로 받아와 필드에 값을 넣는다.(굳이 모든 Entity클래스에 있는 필드값을 가져올 필요가 없기 때문이다.) 이래서 Dto로 Entity 받아서 처리.

📂/main/java/web/dto/PostsUpdateRequestDto.java

package com.khyojun.admin.springboot.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;
    }
}

📂/main/java/domain/posts/Posts.java

package com.khyojun.admin.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@NoArgsConstructor
@Getter
@Entity
public class Posts
{

   ~~~~
    public void update(String title, String content){
        this.title=title;
        this.content=content;
    }


}

📂/main/java/service/posts/PostService.java

package com.khyojun.admin.springboot.service.posts;

import com.khyojun.admin.springboot.domain.posts.PostRepository;
import com.khyojun.admin.springboot.domain.posts.Posts;
import com.khyojun.admin.springboot.web.dto.PostsSaveRequestDto;
import com.khyojun.admin.springboot.web.dto.PostsResponseDto;
import com.khyojun.admin.springboot.web.dto.PostsSaveRequestDto;
import com.khyojun.admin.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class PostsService {
    private final PostRepository postRepository;


	~~~
    
    @Transactional
    public Long update(Long id, PostsUpdateRequestDto updateRequestDto){
        Posts posts= postRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id= " + id));
        posts.update(updateRequestDto.getTitle(), updateRequestDto.getContent());
        return id;
    }

    public PostsResponseDto findById(Long id){
        Posts posts= postRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id= " + id));
        return new PostsResponseDto(posts);
    }

}

이곳을 보게 되면 update 기능에서 데이터베이스 쿼리를 날리는 부분이 없다. 이게 가능한 이유가 JPA의 영속성 컨텍스트이기 때문이라고 한다.

영속성 컨텍스트: 엔티티를 영구 저장하는 환경입니다. 일종의 논리적 개념이다.

JPA의 핵심 내용이 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈리게 된다.
JPA의 엔티티 매니저가 활성화된 상태로(Spring Data Jpa를 쓴다면 기본 옵션) 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태이다. 이 상태에서 해당 데이터의 값을 변경을하게 된다면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영을 하게 된다. 즉, Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다는 것이다. 이러한 개념을 더티 체킹이라고 한다.

  • 더티 체킹: Dirty Checking이라고 하는데 여기서 우선 Dirty라고 하는 것은 변화가 생긴 정도로 이해하면 된다. 그러면? Dirty Checking은? 상태 변화 검사이다.
    ex) 트랜잭션이 시작된 순간부터 이제 안쪽에서 여러가지 변화가 생기고 하게 된다면은 결국에는 트랜잭션이 끝나는 시점에서는 변경사항을 다 update해주기 때문에 DB에서는 자동적으로 반영이 된다.

그러면 계속해서 이 코드가 제대로 작동이 되어지는지 테스트 코드를 통해서 알아보도록 하자.

📂/test/java/web/PostApiControllerTest.java


package com.khyojun.admin.springboot.web;


import com.khyojun.admin.springboot.domain.posts.PostRepository;
import com.khyojun.admin.springboot.domain.posts.Posts;
import com.khyojun.admin.springboot.web.dto.PostsSaveRequestDto;
import com.khyojun.admin.springboot.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 PostApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostRepository postRepository;


	~~~

    @Test
    public void Post_업데이트() throws Exception{
        //given
        Posts savedPosts= postRepository.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);

        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postRepository.findAll();

        assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(expectedContent);

    }


}
  • HttpEntity:Spring Framework에서 제공하는 클래스 중 HttpEntity라는 클래스가 존재한다. 이것은 HTTP 요청(Request) 또는 응답(Response)에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스이다.
  • restTemplate.exchange():보다 일반적인 교환 API 로 POST를 수행하는 방법.

참고

  1. 이동욱 저자의 <스프링 부트와 AWS로 혼자 구현하는 웹 서비스>
  2. https://www.baeldung.com/spring-pathvariable
  3. https://cheershennah.tistory.com/179
  4. https://jojoldu.tistory.com/415
  5. https://stackoverflow.com/questions/20186497/what-is-the-resttemplate-exchange-method-for
profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글