본문 바로가기
개발/스프링

[HTTP] HTTP 메소드와 사용 예

by 카펀 2022. 7. 19.

인프런에서 '모든 개발자를 위한 HTTP 웹 기본 지식' 강의를 듣고 있습니다.

URI와 리소스, HTTP 메소드에 대한 내용을 공부하였고, 이를 진행 중인 토이 프로젝트에 적용시켜 보고자 하였습니다.

 

1. URI와 리소스

2. 리소스와 HTTP 메소드

3. HTTP 메소드 5가지

4. 실제 코드에 적용시켜 보기

5. 기타

 

1. URI와 리소스

URI란, "Uniform Resource Identifier" 의 줄임말입니다. 즉, "리소스 식별자" 라고 할 수 있습니다.

이 외에 URL, URN이 존재합니다. URL은 "Uniform Resource Locator", URN은 "Uniform Resource Name"의 약자입니다.

URL은 특정 리소스의 위치를 가리키며, URN은 특정 리소스의 이름을 가리킵니다.

 

보통 리소스는 위치를 바탕으로 찾게 됩니다. 주로 계층형 구조로 이루어져 있는 파일 시스템 내에서 원하는 특정 리소스를 찾게 되며, 이름만으로 리소스를 특정하기란 굉장히 어렵습니다.

따라서 특정 리소스를 구분할 수 있는 URI, 또는 해당 리소스의 위치를 가리키는 URL을 사용하게 됩니다.

 

세 개념의 집합 관계는 위 그림으로 나타낼 수 있습니다.

URL을 쓰는 것이 곧 URI를 쓰는 것이지만, URN은 잘 사용되지 않습니다.

 

2. 리소스와 HTTP 메소드

만약, 게시판을 만드는 프로젝트 내에서, 아래와 같은 기능을 개발한다고 가정합시다.

  • 글 목록 조회
  • 글 생성
  • 글 수정
  • 글 삭제
  • 글 조회

위 5개의 기능에 대해 URL을 어떻게 정의하면 될까요?

  • 글 목록 조회: select-posts-list
  • 글 생성: create-post
  • 글 수정: edit-post
  • 글 삭제: delete-post
  • 글 조회: select-post

이 정도면 각자 만들려는 기능을 잘 나타낸다고 할 수 있을까요?

 

아쉽지만 URL로써 위 내용들은 좋은 구성이 아닙니다.

앞에서 말한 바와 같이, URL은 '리소스를 식별'하려는 목적을 가집니다.

여기서 리소스란, '글 생성', '글 수정', '글 삭제' 등이 아니라, '글' 그 자체입니다.

따라서 URL은 이렇게 바꾸어 볼 수 있지요.

  • 글 목록 조회: posts
  • 글 생성: posts/{postId}
  • 글 수정: posts/{postId}
  • 글 삭제: posts/{postId}
  • 글 조회: posts/{postId}

'글' 이 핵심이 되었고, 해당 글을 식별하기 위한 postId가 뒤에 붙는 형태로 작성하였습니다.

이러면 각 URL의 역할을 어떻게 식별할 수 있을까요?

 

HTTP 메서드는 리소스에 대한 동작을 나타냅니다.

즉 '글' 을 가지고 뭘 어떻게 할 것인지에 대한 동작을 HTTP 메서드로 나타낼 수 있습니다.

 

3. HTTP 메소드 5가지

 

HTTP 메서드에는 대표적으로 GET, POST, PUT, PATCH, DELETE 등이 존재합니다.

  • GET: 서버로부터 특정 리소스를 요청하기 위한 메서드
  • POST: 서버의 특정 리소스에 주어진 요청을 처리하는 메서드. 메세지 바디를 통해 들어온 데이터를 처리하는 모든 기능을 수행하는데, 주로 전달받은 데이터를 리소스로 등록하거나 프로세스 처리에 사용한다.
  • PUT: 서버의 특정 리소스를 덮어쓰기 위한 메서드. 클라이언트에서 리소스를 구체적으로 특정함.
  • PATCH: 서버의 특정 리소스를 수정하기 위한 메서드.
  • DELETE: 서버의 특정 리소스를 삭제하기 위한 메서드.

이 외에도 HEAD 등과 같이 몇 가지 메서드가 더 존재하지만, 여기서는 생략하도록 하겠습니다.

 

자, 그럼 앞에서 우리가 정의한 각 기능은 이렇게 연결지을 수 있겠지요?

  • 글 목록 조회: GET 메서드 사용
  • 글 생성: POST 메서드 사용
  • 글 수정: PATCH 메서드 사용
  • 글 삭제: DELETE 메서드 사용
  • 글 조회: GET 메서드 사용

4. 실제 코드에 적용시켜 보기

실제로 제가 개발 중인 'CRUD 웹 게시판 제작하기' 토이 프로젝트의 내용을, 앞에서 다룬 HTTP 메서드를 바탕으로 수정해 보았습니다.

 

먼저 기존의 소스 형태입니다.

@RestController
public class PostsController {

    // 게시물 추가
    @PostMapping("/posts/addPost")
    public Long addPost(@RequestBody PostsSaveRequestDto requestDto) {
        // 로그인 한 상태라면, 누구나 글을 쓸 수 있다.
        // 회원 ID, 게시판 ID를 키값으로 한다.
        return postsService.savePost(requestDto);
    }

    // 게시물 수정
    @PutMapping("/posts/editPost/{postId}")
    public Long editPost(@PathVariable Long postId, @RequestBody PostsUpdateRequestDto requestDto) {
        return postsService.update(postId, requestDto);
    }

    // ID값으로 게시물 찾기
    @GetMapping("/posts/findPost/{postId}")
    public PostsResponseDto findPostById (@PathVariable Long postId) {
        return postsService.findById(postId);
    }

    // 게시물 삭제
    public void deletePost() {
    }
}

Mapping 어노테이션에 붙어 있는 URL을 확인해 주세요.

각 기능별로 URL을 따로 만들어 두었습니다.

 

이런 내용을 아래와 같이 수정하였습니다.

 

@RestController
public class PostsController {

    private final PostsService postsService;

    @Autowired
    public PostsController(PostsService postsService) {
        this.postsService = postsService;
    }

    // 게시물 목록 조회
    @GetMapping("/posts")
    public void selectPostsList() {
        // 조회할 때 받는 검색조건, 페이징 등의 정보가 있을 것
    }

    // 게시물 내용 조회
    @GetMapping("/posts/{postId}")
    public void viewPost(@PathVariable String postId) {
        // 게시물의 고유 ID를 키 값으로, 열람 권한이 있다면 열람하도록 한다.
    }

    // 게시물 추가
    @PostMapping("/posts/{postId}")
    public Long addPost(@RequestBody PostsSaveRequestDto requestDto, @PathVariable String postId) {
        // 로그인 한 상태라면, 누구나 글을 쓸 수 있다.
        // 회원 ID, 게시판 ID를 키값으로 한다.
        return postsService.savePost(requestDto);
    }

    // 게시물 수정
    @PatchMapping("/posts/{postId}")
    public Long editPost(@PathVariable Long postId, @RequestBody PostsUpdateRequestDto requestDto) {
        // 1. 본인이 해당 게시물의 생성자이거나, 2. 관리자라면
        // 수정할 수 있다.
        // 해당 권한 체크 로직 구현 필요

        System.out.println(postId);
        return postsService.update(postId, requestDto);
    }

    // ID값으로 게시물 찾기
    @GetMapping("/posts/findPost/{postId}")
    public PostsResponseDto findPostById (@PathVariable Long postId) {
        return postsService.findById(postId);
    }



    // 게시물 삭제
    @DeleteMapping("/posts/{postId}")
    public void deletePost(@PathVariable String postId) {
        // 1. 본인이 해당 게시물의 생성자이거나, 2. 관리자라면
        // 수정할 수 있다.
        //
    }

URL을 전부 "/posts/" 와 같은 형태로 통일하였습니다.

이 컨트롤러 메소드들이 다루는 리소스는 '글 (posts)' 이고, 따라서, URL은 해당 글을 특정하는 용도로 두고, HTTP 메서드를 구분하여 용도를 나누었습니다.

 

그 외에 바뀐 점이 두 가지 있습니다.

먼저, 게시물 수정의 경우, 기존에는 PUT 메서드를 사용하려고 했습니다. 하지만 PUT 메서드의 경우, 수정하려는 항목 값 외에도, 수정하지 않는 값까지 전부 포함해서 요청을 보내야 합니다.

예를 들어, {"title": "hello", "content": "world"} 라는 리소스가 기존에 존재했다고 합시다. 이 때 글 내용을 수정하고 싶어서 {"content": "spring"} 을 PUT 메서드의 파라미터로 넘겨 주면, content 값만 수정되는 것이 아니라, title은 없고 content = spring 인 값으로 덮어씌워집니다.

따라서 저는 수정 기능은 PATCH 메서드가 더 잘 어울린다고 생각했습니다. (물론 이것도 POST 메서드로 할 수 있습니다.)

 

두 번째로, 특정 글을 ID값으로 찾아야 하는 경우에 대한 메서드를 만들었습니다.

하지만 GET 메서드를 사용하는 /posts/는 이미 존재하기 때문에, 부득이하게 URL에 내용을 약간 더 추가하여 구현하였습니다.

 

위 내용을 테스트 코드를 통해 검증하였습니다.

 

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @BeforeEach
    public void setup() {
        restTemplate.getRestTemplate().setRequestFactory(new HttpComponentsClientHttpRequestFactory());
    }

    @AfterEach
    public void rollback() {
        postsRepository.deleteAll();
    }

//    @Transactional
    @Test
    @DisplayName("게시물이_등록된다")
    public void postsCreate() throws Exception {

        // given
        LocalDate now = LocalDate.now();

        String category = "질문";
        String title = "안녕하세요 JPA에 대해 질문이 있습니다";
        String createId = "KAKAO00001";
        Date createTime = new Date(System.currentTimeMillis());
        Date lastEditTime = new Date(System.currentTimeMillis());
        String content = "안녕하세요, 질문이 있어서 게시물을 작성합니다. \nJPQL은 이렇게 쓰는게 맞나요?";
        int viewCount = 0;

        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                .category(category)
                .title(title)
                .createId(createId)
                .createTime(createTime)
                .lastEditTime(lastEditTime)
                .content(content)
                .viewCount(viewCount)
                .build();

        String postId = requestDto.getCreateId();

        String url = "http://localhost:" + port + "/posts/" + postId;

        // when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

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

        List<Posts> createdPosts = postsRepository.findAll();
        assertThat(createdPosts.get(0).getCategory()).isEqualTo(category);
        assertThat(createdPosts.get(0).getTitle()).isEqualTo(title);
        assertThat(createdPosts.get(0).getCreateId()).isEqualTo(createId);
//        assertThat(createdPosts.get(0).getCreateTime()).isEqualTo(createTime);
//        assertThat(createdPosts.get(0).getLastEditTime()).isEqualTo(lastEditTime);
        assertThat(createdPosts.get(0).getContent()).isEqualTo(content);
        assertThat(createdPosts.get(0).getViewCount()).isEqualTo(viewCount);
    }

//    @Transactional
    @Test
    @DisplayName("게시물이_수정된다")
    public void postEdit() throws Exception {

        // given
        LocalDate now = LocalDate.now();

        String category = "질문";
        String title = "안녕하세요 JPA에 대해 질문이 있습니다";
        String createId = "KAKAO00001";
        Date createTime = new Date(System.currentTimeMillis());
        Date lastEditTime = new Date(System.currentTimeMillis());
        String content = "안녕하세요, 질문이 있어서 게시물을 작성합니다. \nJPQL은 이렇게 쓰는게 맞나요?";
        int viewCount = 0;

        Posts savedPosts = postsRepository.save(Posts.builder()
                .category(category)
                .title(title)
                .createId(createId)
                .createTime(createTime)
                .lastEditTime(lastEditTime)
                .content(content)
                .viewCount(viewCount)
                .build()
        );

        Long postId = savedPosts.getPostId();
        String title2 = "(완료) 안녕하세요 JPA에 대해 질문이 있습니다";
        String content2 = "괜찮습니다, 질문은 해결했습니다. 감사합니다.";

        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(title2)
                .content(content2)
                .build();

        String url = "http://localhost:" + port + "/posts/" + postId;

        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

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

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

        List<Posts> editedPosts = postsRepository.findAll();
        assertThat(editedPosts.get(0).getTitle()).isEqualTo(title2);
        assertThat(editedPosts.get(0).getContent()).isEqualTo(content2);
    }
}

게시물을 신규로 등록하는 테스트 코드와, 등록한 게시글을 수정하는 테스트 코드입니다.

게시글 등록은 POST 메서드를 사용하였고, 게시물 수정은 PATCH 메서드를 사용하였습니다.

 

다행히 신규로 작성한 테스트 코드와 기존의 코드까지 전부 통과합니다.

 

참고로, 테스트 코드에서 PATCH 메서드를 사용하려고 하면 아래와 같은 에러가 발생하는 경우가 있습니다.

 

Invalid HTTP method: PATCH

에러 내용을 보니, PATCH 메서드가 테스트 코드 실행 시에 정상적인 메서드로 인식되지 않는 듯 하였습니다.

따라서 아래 과정을 진행하여 문제를 해결하였습니다.

1. build.gradle에 새로운 의존성 추가:

implementation 'org.apache.httpcomponents:httpclient:4.5.13'

2. 테스트 코드 클라스 내에 @BeforeEach 추가

@BeforeEach
public void setup() {
    restTemplate.getRestTemplate().setRequestFactory(new HttpComponentsClientHttpRequestFactory());
}

위와 같은 과정을 진행한 후, PATCH 메서드가 정상적으로 동작하는 것을 확인하였습니다.

(출처: Learnote-Dev)

 

5. 기타

일단 만들면서 부딪혀 보자는 생각으로 토이 프로젝트를 진행하고 있는데, 생각보다 배우는 재미가 더 커서 즐겁습니다.

오늘처럼 배운 내용을 코드에 바로 적용시켜 보기도 하고, HTTP에 대한 이해가 높아지며 백엔드 개발자로써 조금 더 성장한 기분입니다.

URL은 리소스만을 나타내기 위해 사용하고, 리소스에 가하는 동작은 HTTP 메소드를 통해 지정합시다!

댓글