ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11. 스프링부트와 AWS - 게시글 조회, 수정 API 만들기
    실습/AWS 2021. 5. 7. 04:28
     

    10. 스프링부트와 AWS - 게시글 등록 API 만들기

    지난번 오늘 쓸 내용을 다루다가 내용이 산으로 가서...게시판을 마저 만들도록 하자. 게시물 등록용 API를 만들기 위해선 총 3개의 클래스가 필요하다. - Request 데이터를 받을 dto - API 요청을 받을

    sgcomputer.tistory.com

    이전 글에서 게시글 등록 API를 만들었다.

     

    이번에는 게시글 조회, 수정 API를 만들어보자.

     

    우선 조회 및 수정 API를 위해 전체적으로 할 일은 다음과 같다.

     

    - PostsResponseDto(조회용 Dto) 클래스 생성

    - PostsUpdateDto(업데이트용 Dto) 클래스 생성

    - PostsApiController 클래스 수정

    - PostsService 클래스 수정

    - Posts 클래스 수정

     

    우선 조회용 API부터 만들어보도록 하자.

    조회용 API 만들기

     

    "/src/main/java/com/my/practice00/springboot/controller/dto"에 PostsResponseDto를 생성한다.

     

    생성한 뒤에는 아래와 같이 코드를 입력한다.

     

     

     

    import com.my.practice00.springboot.domain.posts.Posts;
    import com.sun.xml.internal.ws.api.ha.StickyFeature;
    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();
        }
    
    }

     

    여기서 보면 생성자를 하나 만들어서 해당 생성자를 통해 데이터를 전달하는 것을 볼 수 있다.

     

    그냥 간단히 바로 Posts에서 받은 자료를 전달하면 안되나 싶다.

     

    하지만 이전글에서 설명한데로 DB와 관련된 Posts와 Dto는 분리하는 것이 좋다.

     

    아무래도 데이터 자체가 통째로 나가는 것보단 정제된 자료만 전달하는게 맞기 때문이다.

     

     

     

    import com.my.practice00.springboot.controller.dto.PostsResponseDto;
    import com.my.practice00.springboot.controller.dto.PostsSaveRequestDto;
    import com.my.practice00.springboot.controller.dto.PostsUpdateRequestDto;
    import com.my.practice00.springboot.domain.posts.Posts;
    import com.my.practice00.springboot.domain.posts.PostsRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    
    import javax.transaction.Transactional;
    
    @RequiredArgsConstructor
    @Service
    public class PostsService {
    
        private final PostsRepository postsRepository;
    
        @Transactional
        public Long save(PostsSaveRequestDto requestDto){
            return postsRepository.save(requestDto.toEntity()).getId();
        }
    
        @Transactional
        public PostsResponseDto findById(Long id){
            Posts entity = postsRepository.findById(id).orElseThrow(() ->  new IllegalArgumentException("해당 게시물이 없습니다. id="+id));
            return new PostsResponseDto(entity);
        }
    }

     

    다음은 위와 같이 PostService에 findById 메서드를 추가해준다.

     

    해당 메서드는 id를 DB에 전달해서 받은 데이터를 entity에 저장한다.

     

    그리고 PostsResponseDto를 생성할 때 파라미터로 entity로 전달해서 객체를 만들어 반환한다.

     

     

     

    import com.my.practice00.springboot.controller.dto.PostsResponseDto;
    import com.my.practice00.springboot.controller.dto.PostsSaveRequestDto;
    import com.my.practice00.springboot.controller.dto.PostsUpdateRequestDto;
    import com.my.practice00.springboot.domain.posts.PostsRepository;
    import com.my.practice00.springboot.service.posts.PostsService;
    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);
        }
    
        @GetMapping("/api/v1/posts/{id}")
        public PostsResponseDto findById(@PathVariable Long id){
            return postsService.findById(id);
        }
    }

     

    다음으로는 PostsApiController를 PostsResponseDto 메서드를 추가해준다.

     

    해당 메서드는 해당 사용자로부터 게시물 id를 전달받아서 Service에 전달해주는 역할을 한다.

     

    조회용 API 동작 과정을 보면 다음 그림과 같다.

     

     

     

    글로 설명하면 다음과 같다.

     

    - 사용자라 조회하고자 하는 ID를 PostsApiController로 전달

    - ID를 전달받은 PostsApiController는 ID를 PostsService의 findByid메서드로 전달

    - PostsService의 findByid메서드는 전달받은 ID로 데이터베이스에서 자료를 받음

    - 받은 자료를 Posts형식의 Entity에 저장함

    - Entity를 PostsReponseDto 생성자에 제공하여 새로운 PostResponseDto 객체를 생성

    - 해당 객체를 PostsApiController로 반환함

    - PostsApiController는 사용자에게 객체를 반환함

     

    위에서 이미 언급했던 PostsRepository로부터 받은 자료를 바로 반환하지 않는다.

     

    Entity에 넣고 이를 PostsResponseDto에 제공해서 이 객체를 반환하는 형식으로 이뤄진다.

    업데이트용 API 만들기

     

    "/src/main/java/com/my/practice00/springboot/controller/dto"에 PostsUpdateDto를 생성한다.

     

    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;
        }
    
    }

     

    이번에는 다른 Request나 Response에 비해 내용이 간단하다.

     

    Update의 경우는 기본적으로 Author 즉 글쓴이 정보, Id는 업데이트 할 필요가 없다.

     

    글 수정은 제목, 내용의 수정만 필요하므로 생성자를 통해 생성하는 객체는 두 개의 정보만으로 이뤄진다.

     

     

     

    다음은 Posts 클래스에 위와 같이 update 메서드를 추가해준다.

     

     

     

    import com.my.practice00.springboot.controller.dto.PostsResponseDto;
    import com.my.practice00.springboot.controller.dto.PostsSaveRequestDto;
    import com.my.practice00.springboot.controller.dto.PostsUpdateRequestDto;
    import com.my.practice00.springboot.domain.posts.Posts;
    import com.my.practice00.springboot.domain.posts.PostsRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    
    import javax.transaction.Transactional;
    
    @RequiredArgsConstructor
    @Service
    public class PostsService {
    
        private final PostsRepository postsRepository;
    
        @Transactional
        public Long save(PostsSaveRequestDto requestDto){
            return postsRepository.save(requestDto.toEntity()).getId();
        }
    
        @Transactional
        public PostsResponseDto findById(Long id){
            Posts entity = postsRepository.findById(id).orElseThrow(() ->  new IllegalArgumentException("해당 게시물이 없습니다. id="+id));
            return new PostsResponseDto(entity);
        }
    
        @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;
        }
    
    }

     

    다음은 위와 같이 PostsService에 update 메서드를 추가해준다.

     

    이때 메서드의 동작하는 방법을 잘 보도록 하자.

     

    해당 메서드는 다음과 같이 동작한다.

     

    - 사용자로부터 수정 요청 받은 게시물을 통해 id값을 받는다.

    - id값에 맞는 자료를 데이터베이스에서 불러와서 Posts 형식의 객체에 저장한다.

    - 전달받은 수정할 게시물 제목, 내용을 담은 RequestDto 객체의 내용을 update메서드에 전달한다.

    - 최종적으로 수정된 게시물의 id값을 반환한다.

     

    메서드를 잘보면 알겠지만 위의 글쓰기와 달리 postsRepository 객체를 쓰지 않는다.

     

    그냥 객체의 값만 변경하고 메서드를 종료해버린다.

     

    이전에 데이터베이스를 다룰 때는 JPA Repository를 상속받은 PostsRepository를 쓴다고 배웠다.

     

    그런데 이번에는 그렇게 하지 않는다.

     

    그 이유는 바로 JPA의 영속성 컨텍스트 때문이다.

     

    JPA를 쓰게되면 컨텍스트가 생성된다.

     

    그런데 이 컨텍스트는 트랜잭션 안에 있을 때 트랜잭션이 끝나기 전까지 유지가 된다.

     

    그래서 별도의 명령어로 업데이트하는게 아니라 그냥 트랜잭션이 유지된 상태에서 객체의 값만 바꾼다.

     

    그리고나서 트랜잭션이 끝나면 해당 테이블에 객체가 변경된 것을 반영하게 된다.

     

    좀 더 간단히 말하면 트랜잭션란게 유지되는 동안은 JPA를 통해 접근한 값을 계속 유지된다.

     

    그리고 이 트랜잭션이 끝나기 전에 임의로 객체 값을 바꾸고 과정을 끝내면 그대로 값이 저장된다.

     

    이러한 개념을 더티 체킹(Dirty Checking)이라고 이야기한다.

     

    그리고 추가로 Posts에 왜 굳이 update 메서드를 추가하는 이유는 바로 위와 같은 이유다.

     

    일단 JPA에서 기본으로 제공하는 update 메서드는 존재하지 않는다.

     

    그래서 update를 만들어줄 수 밖에 없는 것 같다.

     

    그리고 임의로 만든 update메서드를 써도 더티체킹이 있어서 알아서 저장된다.

     

     

     

    import com.my.practice00.springboot.controller.dto.PostsResponseDto;
    import com.my.practice00.springboot.controller.dto.PostsSaveRequestDto;
    import com.my.practice00.springboot.controller.dto.PostsUpdateRequestDto;
    import com.my.practice00.springboot.domain.posts.PostsRepository;
    import com.my.practice00.springboot.service.posts.PostsService;
    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);
        }
    
        @GetMapping("/api/v1/posts/{id}")
        public PostsResponseDto findById(@PathVariable Long id){
            return postsService.findById(id);
        }
    
        @PutMapping("/api/v1/posts/{id}")
        public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
            return postsService.update(id, requestDto);
        }
    
    }

     

    마지막으로 위와 같이 PostsApiController에 update 메서드를 추가해준다.

     

    해당 코드의 작동을 그림으로 표현하면 아래와 같다.

     

     

     

    글로 좀 더 길게 풀어쓰면 다음과 같다.

     

    - 사용자는 수정하고자 하는 글의 id, 제목, 내용을 PostsApiController에 전달

    - PostsApiController는 id, 제목, 내용을 PostsService의 update 메서드에 전달

    - PostsService의 update 메서드는 전달받은 id로 데이터베이스에서 자료를 전달받음

    - 해당 자료를 Posts 객체에 저장

    - Posts 객체의 update 메서드를 실행하면서 수정할 제목, 내용을 전달하여 객체의 내용 수정

    - 전달받은 id값을 반환함

    - 이와 동시에 트랜잭션이 끝나면서 객체 변경 내용도 저장됨

     

    더티체킹으로 인해서 굳이 별다른 메서드 없이 객체값만 변경해줌으로서 업데이트가 가능하다.

    조회하기, 업데이트하기 테스트

    이제 위에서 추가한 내용을 테스트해보자.

     

    이전에 테스트용으로 작성한 PostsApiControllerTest에 내용을 추가해주면 된다.

     

     

     

    import com.my.practice00.springboot.controller.dto.PostsSaveRequestDto;
    import com.my.practice00.springboot.controller.dto.PostsUpdateRequestDto;
    import com.my.practice00.springboot.domain.posts.Posts;
    import com.my.practice00.springboot.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.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.Java6Assertions.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 = "게시글 제목";
            String 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 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);
        }
    
    }

     

    해당 코드에 대해 간단히 설명하면 다음과 같다.

     

    - 우선 postsRepository의 save 메서드를 통해 글을 저장한다.

    - 해당 글의 id를 얻는다.

    - PostsUpdateRequestDto 형식의 객체를 생성하며 수정할 제목, 글을 객체에 저장한다.

    - 입력된 url에 업데이트 요청을 하면서 위에서 만든 객체, id를 전달한다.

    - 수정된 내용이 기대했던 내용과 맞는지 확인한다.

     

    만약 해당 테스트 클래스 전체를 테스트하면 아래와 같은 결과가 나온다.

     

    이전에 입력한 테스트까지 같이 테스트되기 때문에 2개가 성공한걸로 나온다.

     

     

Designed by Tistory.