Search
🌐

[Network] HTTP 메서드 활용

인프런 - 모든 개발자를 위한 HTTP 웹 기본 지식에서 김영한 님이 강의해주는 내용을 바탕으로 정리를 진행했습니다. 강의를 직접 듣고 싶으신 분들은 하단 북마크를 눌러주세요.
*제 포스팅에 나오는 모든 이미지는 김영한 님께서 만든 강의 자료에서 가져왔습니다.
목차 보기

 요약 정리

전체 내용을 보기 쉽게 요약 정리해둔 부분입니다. 자세히 알기 원하는 문장을 선택하시면 관련 섹션으로 이동합니다.

 들어가며

이번 챕터에서는 클라이언트에서 서버로 데이터를 어떤 식으로 전송하는지 예시를 통해서 확인하고, HTTP API를 설계할 때 어떤 식으로 설계하면 좋을지 알아보겠습니다.

 클라이언트에서 서버로 데이터 전송

클라이언트에서 서버로 데이터 전송을 어떻게 할까요? 데이터를 전달하는 방식은 크게 2가지 입니다.
⓵ 쿼리 파라미터를 통한 데이터 전송
쿼리 파라미터를 통한 데이터 전송은 key-value 형식을 사용하는 쿼리를 URI 끝에 넣어서 데이터를 전송하는 방식입니다.
주로 GET 메서드에서 많이 사용하는 데이터 전송 방식이며, 정렬 필터(검색어)같은 곳에서 많이 사용됩니다.
전에도 검색 예시를 보여준 적이 있는데, 그때 q=hello&hl=ko 이런식으로 쿼리를 작성했었습니다. 데이터를 가져와야 하는 상황에서 필터(특정 검색어)를 걸어서 전체 데이터에서 한정적으로 데이터를 가져올 수 있도록 쿼리 파라미터를 보내는겁니다. q=hello&hl=ko 쿼리 파라미터를 받은 서버는 전체 데이터에서 hello에 관련된 데이터를 한국어로 내려줄 수 있게 되겠죠.
⓶ 메시지 바디를 통한 데이터 전송
HTTP 메시지 바디에 데이터를 넣어서 전송하는 방식입니다.
주로 POST, PUT, PATCH 메서드에서 많이 사용하는 데이터 전송 방식입니다.
사용자 정보를 넣어서(POST) 회원 가입을 한다던지, 원하는 상품을 주문(POST) 한다던지, 새로운 리소스를 등록(POST, PUT) 한다던지, 리소스를 변경(PUT, PATCH) 하는 부분에서 사용됩니다. 회원가입을 한다고 했을 때 회원가입을 위해서 사용자의 정보를 보내주어야 합니다. 따라서 메시지 바디를 통해서 등록되길 원하는 사용자의 정보를 보내주는 겁니다.
클라이언트에서 서버로 데이터 전달하는 방식은 크게 2가지가 있지만, 클라이언트에서 서버로 데이터를 전송하는 상황은 총 4가지가 있습니다.
⓵ 정적 데이터 조회
⓶ 동적 데이터 조회
⓷ HTML Form를 통한 데이터 전송
⓸ HTTP API를 통한 데이터 전송
각각의 상황에서 어떤 식으로 데이터를 전송하는지 자세하게 보겠습니다.

정적 데이터 조회

정적 데이터 조회는 가장 간단한 예제입니다. 이미지나 정적 텍스트 문서를 조회하는 상황입니다.
조회이기 때문에 GET 메서드를 사용하고 쿼리 파라미터없이 단순한 리소스 경로만으로 해당 리소스를 조회하는 방식입니다.

상황) GET 메서드를 사용해서 정적 데이터 조회

우리는 /static/star.jpg 라는 경로로 GET 메서드를 사용해서 데이터를 전송합니다. 데이터를 받은 서버가 조회한 이미지 데이터를 응답 데이터로 보내줍니다.
정적 데이터 조회는 데이터를 보내줄 필요가 없고 단순한 리소스 경로로만 리소스를 조회하기 때문에 굉장히 간단합니다. 서버도 해당 리소스 경로에 있는 데이터만 응답 메시지로 전달해주면 됩니다.

동적 데이터 조회

동적 데이터 조회는 이전과 다르게 조회시 데이터를 전달합니다. GET 메서드를 사용하기 때문에 쿼리 파라미터를 사용해서 데이터를 전달합니다.
물론 GET 메서드도 HTTP 메시지 바디를 통해서 데이터를 전송할 수 있긴 하지만 아직 지원하지 않는 서버가 많기 때문에 실무에서는 권장하지 않는 방식이라고 합니다.
동적 데이터 조회 방식은 주로 검색이나 게시판 목록에 정렬, 필터를 걸 때 사용됩니다. 조회 조건을 줄여주는 필터와, 조회 결과를 정렬하는 정렬 조건을 추가 파라미터를 사용해서 보내주는거죠. 물론 그 외에도 사용되는 곳이 많습니다.

상황) GET 메서드를 사용해서 동적 데이터 조회

google.com 검색창에 검색어를 입력한다고 칩시다. 우리는 hello라는 검색어를 넣고 검색 버튼을 눌렀습니다. 이때 /search 경로로 검색어 같은 추가 조건이 들어가야 합니다. 해당 쿼리 파라미터를 서버에서 받아서 key-value 형식으로 꺼내어 검색어를 확인하고 결과를 찾는 일이 발생합니다.
서버는 쿼리 파라미터를 기반으로 정렬 필터를 걸어서 DB에서 결과를 찾아 응답 메시지로 보내줍니다.

HTML Form 데이터 전송

HTML에는 Form 태그라는 것이 존재합니다.
순수하게 HTML 문서만을 가지고 무언가를 만들 때는 Form 태그를 사용해서 회원가입 폼을 만들곤 합니다. 폼에다가 action method 를 넣어서 form submit 버튼을 누르면 웹 브라우저가 form 안에 있는 데이터를 읽어서 HTTP 메시지를 생성해줍니다.
웹 브라우저가 생성한 요청 HTTP 메시지를 보면:
이전에 Form 태그에 있던 <method> <action> <HTTP-version> 순으로 시작 라인이 만들어진 걸 볼 수 있습니다. 헤더에는 낯선 Content-Type 이 적혀있습니다.
application/x-www.form-urlencoded
해당 타입은 모든 전송 데이터를 서버로 보내기 전에 인코딩합니다. 한글이 들어와도 %<숫자> 이런식으로 인코딩해서 전달해줍니다. 해당 타입의 인코딩 데이터는 쿼리 파라미터와 비슷하게 생겼습니다. username=kim&age=20가 쿼리 파라미터 같지 않나요? 데이터도 key-value 형식으로 구성되어 있습니다. 원만한 웹 서버는 해당 데이터를 파싱해서 쓸 수 있도록 되어 있습니다.
만약, 위에 Form 태그에서 method를 get 으로 바꾼다고 생각해봅시다. 메서드를 get 이라는 단어로만 바꾸면 되기 때문에 바꾸는데는 어려움이 없습니다.
웹 브라우저가 get 메서드로 바뀐 Form 태그를 보고 get 메서드는 메시지 바디를 쓰지 않으니 query parameter 형식으로 만들어서 서버로 전달해줍니다. 쿼리 파라미터도 데이터를 전달하는 방식 중 하나이니 username과 age가 제대로 전달되어 DB에 저장될겁니다.
전달은 되겠지만 get 메서드를 이렇게 사용해도 될까요?
우리가 Form에서 사용하는 action은 /save 입니다. 저장을 하는 액션을 나타내는 path네요. 저장에는 get 메서드를 사용해선 안됩니다. 우리는 get 메서드를 조회로만 사용해야 합니다. 리소스 변경이 발생하는 부분에 get을 사용해서는 안됩니다.
HTML Form 데이터 전송 시 크기가 작은 데이터들은 application/x-www.form-urlencoded 타입으로 충분히 데이터 전송이 가능하지만 데이터 크기가 큰 데이터들은 전송이 어려울 수 있습니다.
크기가 크다는 건 아마도 파일, 이미지 전송같은 상황을 말하는거겠죠.
HTML Form에서 파일 전송을 할 때 쓰는 타입은 따로 있습니다. 바로 multipart/form-data 입니다.
multipart/form-data 는 username, age 같은 데이터말고도 byte로 된 파일, 이미지를 전송해야할 때 사용합니다.
여러 개의 Content-Type에 대한 데이터를 보낼 수 있고, 주로 바이너리 데이터 전송에 사용됩니다. String 타입인 username, age 말고도 바이너리 데이터인 file도 함께 보내는걸 보면 알 수 있죠? 그러면 해당 타입의 요청 HTTP 메시지는 어떻게 생겼을까요?
HTTP 요청 메시지 바디가 다른 메시지 바디하고 많이 다르다는 생각이 드셨을겁니다. 일단 Content-Type 부분부터 보고 가자면, Content-Type 옆에 boundary 라는 것이 있습니다.
boundary는 말그대로 경계를 나타냅니다. 경계를 가지고서 데이터들을 구분합니다. 우리가 바운더리를 직접 만드는 것은 아니고 웹 브라우저가 그렇게 만들어줍니다. 위에 Form 태그에서 enctyp="multipart/form-data" 라고 작성해주면 이렇게 만들어줍니다.
boundary를 경계 삼아서 “username”에 kim이라는 데이터가 들어왔다는걸 알 수 있고, “age”에 20이라는 데이터가 들어왔다는걸 알 수 있습니다.
마지막 이미지 데이터는 Content-Type를 png 형식으로 해서 이미지를 보내는걸 알 수 있네요.
예시를 보면 GET, POST 메서드만 사용하고 있습니다.
다른 HTTP 메서드들은 다 어디갔을까요?
HTML Form 전송은 GET, POST 메서드만 지원하기 때문에 나머지 HTTP 메서드는 사용되지 않습니다.

HTTP API 데이터 전송

애플리케이션에서 클라이언트에서 서버로 데이터를 바로 전송한다고 할 때, 전에 봤던 HTTP 요청 메시지 형식을 직접 다 만들어서 넘겨주면 됩니다.
클라이언트에서 요청 메시지 형식으로 만들어서 서버에서 데이터를 넘겨주면 됩니다.
HTTP API 데이터 전송은 많은 곳에서 사용됩니다.
서버 to 서버
HTTP API 데이터 전송은 백엔드 서버끼리 통신 시에 많이 사용됩니다. 서버끼리의 통신에서는 HTML 같은게 전혀 없고 기계끼리 통신하는 것이기에 HTTP API를 사용합니다.
앱 클라이언트
웹 클라이언트
HTML에서 Form 전송을 사용하지 않고 대신 javascript를 사용해서 통신한다고 할 때 AJAX을 사용해서 통신하게 됩니다. React, VueJs 같은 웹 클라이언트와 API 통신을 할 때는 HTTP API 데이터 전송을 사용합니다.
이전에 데이터 전달 방식에서도 얘기했듯 HTTP API 데이터 전송은 하단 방식을 통해서 이루어집니다.
POST, PUT, PATCH : HTTP 메시지 바디를 통해서 데이터 전송
GET : 쿼리 파라미터를 통해서 데이터 전송
HTTP API 데이터는 Content-Typeapplication/json를 주로 사용합니다.
예전에는 XML를 많이 사용했는데, XML가 읽기 쉽지 않고 데이터가 너무 복잡하다는 단점때문에 JSON이 많이 사용되고 있습니다. JSON은 심플하고 이해하기 쉽고 XML에 비해서 데이터 크기가 작다는 장점이 있습니다.
클라이언트에서 서버로 데이터를 전송하는 방식도 알았고, 어떠한 상황에서 데이터를 어떤 식으로 보내는지도 알아봤으니, 본격적으로 HTTP API 설계 예시를 보겠습니다.

 HTTP API 설계 예시

이번 섹션에서는 HTTP API를 사용해서 POST 기반, PUT 기반의 API 설계 방법을 알아보고 HTML FORM를 사용해서 API를 설계하는 방법도 알아봅시다.

POST 기반 API 설계

이전에도 API 설계를 해 본 적이 있습니다. [Network] HTTP 메서드 에서 URI를 설계해보는 일을 했었습니다. 그때 우리는 리소스와 행위를 구분해서 URI가 리소스을 식별할 수 있도록 설계해야한다고 배웠습니다.
따라서, 우리는 URI를 리소스를 식별할 수 있게 설계하고, 행위에 대한 부분은 HTTP 메서드를 통해서 구분할 수 있게끔 할 겁니다.
우리는 POST 기반 API 설계를 이렇게 마쳤습니다.
회원 목록을 가져오는 부분은 GET 메서드를 통해서 /members 경로에서 데이터를 가져올 수 있도록 했습니다. POST 메서드도 같은 경로를 통해서 데이터를 생성합니다. /members 에다가 데이터를 넣으면 새로 등록될 수 있도록 한겁니다. 회원 조회도 GET 메서드를 통해서 특정 id를 가진 특정 회원을 조회할 수 있도록 했습니다. path 는 계층 구조를 가져야하기 때문에 /members/{id}로 만들었네요. 회원 삭제도 같은 경로를 통해서 DELETE 메서드를 사용해서 특정 회원을 삭제하면 됩니다.
조금 고민을 해봐야 하는 부분은 회원 수정 입니다.
회원 수정을 진행하는 부분은 어떤 고민이 필요한 걸까요?
바로 어떤 HTTP 메서드를 사용할 지에 대한 고민입니다.
우리가 PUT 메서드를 사용해서 수정을 만들었다면 이전 데이터를 지우고 새로운 데이터로 덮는다는걸 알고 있어야 합니다. 그렇기 때문에 완전히 이전 데이터를 덮어도 문제가 없는 상황이라면 PUT 메서드를 사용해도 괜찮습니다. 하지만, 클라이언트에서 모든 회원 정보를 매번 보내주어야 합니다. 그래야 깔끔하게 기존 데이터를 날리고 덮을 수 있으니깐요. 만약, 한 개의 데이터라도 빠진 상태로 정보가 보내진다면 데이터가 잘못 설정될겁니다.
PUT 메서드가 수정에는 적합하지 않은 메서드처럼 보일 수 있습니다. 하지만 PUT 메서드가 의미있는 경우들도 있습니다. 바로 게시판 글을 수정하는 경우입니다. 게시글을 수정하는 경우에는 부분 수정을 하지 않기 때문에 전체 데이터를 보내게 됩니다. 따라서, 이전 데이터를 완전히 덮어도 괜찮은겁니다.
그래도 리소스 수정에 더 적합한 메서드가 있습니다. 바로 PATCH 메서드 입니다.
PATCH 메서드는 부분 수정이 가능합니다. 그렇기 때문에 우리가 하고자하는 기본적인 회원 수정같은걸 PATCH 메서드를 통해서 할 수 있게 됩니다.
하지만, 이것도 저것도 애매한 경우도 있을 수 있습니다. 그럴때는 POST 메서드를 써서 해결하면 됩니다.
우리가 위에 설계한 API는 POST 기반으로 설계한 API 였습니다.
POST 메서드를 사용해서 신규 리소스를 등록하는 HTTP API의 특징은:
클라이언트가 등록될 리소스의 URI를 모른다는 겁니다.
만약, 우리가 신규 유저를 등록한다고 칩시다. 아마 /members 라는 경로로 신규 유저 등록 요청 메시지를 보낼거예요. 서버는 요청 메시지를 받아서 DB에 저장합니다. 그리고 해당 리소스에 신규 리소스 식별자를 생성할겁니다. 마지막으로 응답 메시지에 Location: /members/{해당 리소스 식별 id}을 포함해서 클라이언트로 보낼거예요.
즉, 클라이언트가 신규 리소스 식별자를 만들어서 서버로 보내주는 것이 아닌, 신규 리소스를 받은 서버가 신규 리소스 식별자를 만들어주는거죠. 식별자는 클라이언트가 결정할 수 없는 부분인겁니다.
이러한 형식을 우리는 컬렉션(Collection) 이라고 합니다.
서버가 관리하는 리소스 디렉토리이며, 서버가 리소스의 URI를 생성하고 관리합니다. 위에서 우리가 주구장창 봤던 /members가 바로 컬렉션 리소스입니다.

PUT 기반 API 설계

PUT 기반 API 설계는 파일 관리 시스템을 생각하시면 됩니다. 우리는 원격 파일을 관리하는 API를 만들겁니다.
파일 목록, 파일 조회, 파일 삭제는 이전에 봤는 POST 기반 API 설계와 사용한 HTTP 메서드가 동일한데, 파일 등록은 이전과는 다르게 PUT 메서드를 사용했습니다.
왜 일까요?
우리가 로컬 PC에서 파일을 생성할 때 해당 파일과 동일한 파일이 있다면 기존 파일을 덮어쓰는 걸 본 적이 있을겁니다. 해당 방식과 동일하게 파일 관리 시스템에서도 동일한 파일이 있다면 기존 파일을 지우고 새로운 파일로 덮어쓰는겁니다. 그러한 역할을 해주는 메서드가 바로 PUT 인거구요. PUT은 기존에 있는 데이터를 지우고 새로운 데이터로 덮어씁니다. PUT이 파일 등록에 딱 맞는 역할을 가지고 있기 때문에 해당 시스템에서는 PUT 메서드를 파일 등록에 사용하는 겁니다.
그렇다면 왜 밑에 있는 파일 대량 등록은 POST 메서드를 사용해서 했는지 의문이 들 수도 있습니다.
사실 POST가 해야하는 파일 등록의 역할을 PUT 메서드가 대신 해주고 있기에 POST의 의미를 임의로 정할 수 있는 상태입니다. 따라서, /files를 파일 대량 등록으로 의미를 정한겁니다. 하지만 원한다는 다른 의미로 사용해도 됩니다.
우리가 위에 설계한 API는 PUT 기반으로 설계한 API 였습니다.
PUT 메서드를 사용해서 신규 리소스를 등록하는 HTTP API의 특징은:
클라이언트가 리소스 URI를 알고 있어야 한다는 겁니다.
우리가 PUT 메서드를 가지고 신규 자원을 등록하게 되면, /files/{filename} 이런 식으로 사용해야 합니다. 즉, 클라이언트가 {filename} 에 어떤 값이 들어갈 지 알고 있어야 하는거죠.
PUT 기반 API 설계에서는 클라이언트가 직접 리소스의 URI를 지정하고 생성자 리소스를 다 알고 본인이 관리하고 있어야 합니다.
이 부분에서 위에 있는 POST 기반 API 설계와 차이점이 있다는 걸 알 수 있을겁니다.
POST 메서드를 사용해서 신규 데이터를 등록하면 이는 클라이언트가 서버로 요청을 하는 것이기에 서버가 리소스 URI를 알아서 만들어서 응답 메시지로 넘겨줍니다.
하지만, PUT 메서드 기반으로 한 신규 데이터 등록에서는 클라이언트가 등록된 리소스 URI를 본인이 직접 다 관리하기 때문에 서버는 그냥 클라이언트에서 요청 온대로 해주는 역할을 합니다.
이런 스타일의 관리를 스토어(Store) 라고 합니다.
클라이언트가 관리하는 리소스 저장소이며, 클라이언트가 리소스의 URI를 알고 관리합니다. 위의 예시에서는 /files 가 스토어 리소스입니다.
지금 까지는 우리는 2가지 HTTP API 설계 예시를 보았습니다.
하나는 POST 기반이었고, 하나는 PUT 기반이었습니다. 하지만 PUT 기반 설계는 비중이 거의 없고, 대부분 POST 기반 설계로 진행합니다.

HTML FORM 사용

HTML FORM은 위의 HTTP API 와는 다르게 GET, POST 메서드만을 지원합니다. 물론 AJAX 같은 기술을 사용해서 이 부분을 해결할 수 있겠지만, 순수한 HTML, HTML FORM만을 사용한다고 가정합시다.
그렇다면, 이렇게 HTML FORM를 설계할겁니다.
우리가 회원 목록 부분에서 회원 등록 버튼을 누르게 되면, /members/new 경로를 통해서 회원 등록 폼을 보게 될 겁니다. 우리가 회원 등록 폼에다가 회원 정보를 입력합니다. 그리고 submit 버튼을 누르면 POST 메서드를 사용해서 /members/new 혹은 /members 경로로 요청 메서드가 보내지게 됩니다.
둘 중 어떤 경로를 선택해야하는가에 대한 답은 없다고 합니다.
김영한님은 /members/new 경로를 선호하시는데, 그 이유가 POST 이후에 문제가 있어서 결과로 등록 폼을 다시 보내주는 경우가 있다고 합니다. 등록 폼을 응답 메시지로 보내줬을 때, /members/newPOST 메서드 경로를 맞춰두면 이후에 경로를 바꾸지 않아도 깔끔하게 해결되기 때문에 /members/new를 선호하신다고 하셨습니다. 아닌 경우에는 경로를 바꿔야해서 조금 애매하다고 합니다.
아무튼 이후에 회원 한명을 선택해서 /members/{id}를 통해서 회원의 데이터를 가져오고 수정하기 버튼을 눌러서 회원 수정 폼으로 이동합니다. 수정 폼 내부에서 회원 정보를 수정한 후에 수정 버튼을 누릅니다. 수정된 정보가 POST 메서드를 사용해서 보내집니다. 회원 삭제 시에도 /members/{id}/delete 경로를 통해서 POST 메서드를 통해서 요청이 보내집니다.
우리가 위에 설계한 API는 HTML FORM 기반으로 설계한 API 였습니다.
HTML FORM 기반 설계의 특징은:
GET, POST만 사용하기 때문에 제약이 존재한다는 겁니다.
두 메서드로는 나타내지 못하는 부분을 나타낼 수 있는 방법이 필요합니다. 이 부분을 해결해줄 수 있는 방법이 바로 Control URI 입니다.
Control URIGET, POST로 인한 제약을 해결하기 위해서 동사로 된 리소스 경로를 사용하는 겁니다.
위에서 우리가 /new, /edit, /delete 같은 부분을 붙인 이유가 바로 그겁니다. HTML FORM에서는 PUT, DELETE 같은 메서드를 사용할 수 없기 때문에 GET, POST 만으로는 그 의미를 나타내어줄 수가 없습니다. 따라서 HTTP 메서드로 그 의미를 모두 나타낼 수 없기 때문에 Control URI를 사용합니다.
실무에서도 많이 사용한다고 합니다.
이상적으로는 HTTP 메서드를 사용해서 해결하면 좋겠지만 되게 이상적인 것이라고 합니다. 그것만으로 해결하기 애매한 부분들이 정말 많기 때문에 Control URI를 써서 해결한다고 합니다.
하지만, Control URI를 무식하게 써서는 안됩니다.
최대한 리소스라는 개념을 가지고 URI를 설계해야 합니다. 그마저도 안될 때 Control URI를 대체제로 사용해야 하는거죠. HTTP 메서드로는 안 맞아 떨어지는 상황에서도 Control URI를 사용할 수 있습니다.
그리고 무엇보다도 Control URI는 동사로 써야 한다는 점 잊지마세요.

참고하면 좋은 URI 설계 개념

URI 설계 시에 정리하기 어려운 부분들이 존재합니다. 하지만 이 부분을 잘 해결해낸 괜찮은 Practice를 여러 보아서 체계를 만들었습니다. https://restfulapi.net/resource-naming 를 통해서 더 자세하게 보실 수 있습니다.
그 중에서도 4가지 개념을 소개해보려고 합니다.
⓵ 문서(document)
단일 개념입니다. 파일 하나, 객체 인스턴스, 데이터베이스 row 같은 부분을 말합니다.
문서는 /members/100, /files/star.jpg 이런식으로 하나의 데이터를 딱 집어낸 겁니다.
⓶ 컬렉션(collection)
이전에 말했지만 서버가 관리하는 리소스 디렉터리, 서버가 리소스의 URI를 생성하고 관리하는 걸 말합니다.
클라이언트는 그저 요청만 하고 서버가 해당 데이터를 등록하고 새로운 리소스 식별자를 만들어줍니다.
컬렉션은 /members 로 나타내어집니다.
⓷ 스토어(store)
스토어도 이전 PUT 기반 방식에서 얘기했지만 클라이언트가 관리하는 자원 저장소 입니다. 클라이언트가 리소스의 URI를 알고 관리합니다.
스토어는 /files 로 나타내어집니다.
⓸ Controller, 컨트롤 URI
문서, 컬렉션, 스토어로는 아무리해도 해결 안되는 부분이 존재할 수 있습니다. 모든 URI가 깔끔하게 떨어지지는 않기 때문이죠. 그래서 Control URI가 꼭 있어야 합니다.
Control URI를 사용하는 부분은 데이터를 조작하고 변경하는 일이기 때문에 동사를 직접 사용해야 합니다.
만약, 회원의 주문 상태를 다음 상태로 진행한다 라는 의미를 가진 API를 만드려고 하면:
POST /orders/{주문 번호}/delivery 이런식으로 만들어야 합니다. 해당 의미를 가진 HTTP 메서드를 찾아서 깔끔하게 만들어주기가 쉽지 않습니다.