Intro


뭐든지 그냥 되는 것은 없다.

여태까지 개발자가 되기 위한 공부를 하면서 머리를 잘 굴려야 한다는 것은 굉장히 중요한 것이라고 생각한다.

개발자의 적성 적합 여부를 떠나서 그저 눈치껏, 흘러가는대로 공부했던 사람이라면 개발자로서 성장할 수 없고 바로 뽀록(?)이 난다.

그 이유는 바로 몰입과 집중에서 나온다고 생각한다. 그리고 몰입과 집중은 멍하니 가만히 있는 것이 아니라 쉴 새없이 생각을 요구하게 되고 결국 머리를 엄청 굴려야 원하는대로 될까말까한 상황이 비일비재하다.

굉장히 단순한 기능이나 모듈을 추가로 개발하거나 적용한다고 하더라도 실제로 개발하기 전에 어떻게 개발하고 적용할 것인지 설계하는 과정과 설계한 것을 토대로 개발하는 과정에서 생각보다 큰 시간과 비용이 들어간다.

다른 많은 것들도 마찬가지겠지만 유독 개발 공부를 하면서 그냥 되는대로 넘어갔던 것은 거의 없는 것 같다. 항상 동작 원리나 개념 자체를 이해하고 넘어가야 다음 단계로 진입할 수 있었고, 이 것을 몰랐다면, 다음 단계로 넘어가더라도 결코 성장할 수 없음을 매번 느낀다.

결국 그냥 되는대로 개발하는 것은 불가능하고 그래서도 안된다. 항상 내가 어떤 것을 개발하려 하는지, 방향성은 맞는지 상기하면서 진행해야 함을 다짐하자.




Day - 71

Docker 이미지 Docker Hub로 푸시해보기

Docker Hub를 사용하면서 이미지를 빌드하고 푸시하는 과정에 대해서 정리해보려 한다.

나의 Docker Hub에 푸시된 이미지들은 위와 같이 실습 차원에서 진행한 4개 정도가 있다. 여기서 특정 이미지를 받아와서 수정하고 Docker Hub(이하 저장소)에 푸시하는 과정을 진행해보려 한다.

0. 사전 준비

필자는 Docker Desktop 환경에서 Docker를 사용하였다.

또한 Docker Hub 저장소에 우리가 만든 이미지를 Push하기 위해서는 말 그대로 저장소가 준비되어야 한다. Docker Hub 계정으로 접속하여 my-nginx-image라는 저장소를 미리 만들어두자.


1. 이미지 찾기

저장소에서 원하는 이미지를 검색한다. 이번 글에서는 공식 Nginx 이미지를 활용하였다.


2. 이미지 다운로드하기

docker pull 명령을 이용해 하여 Nginx 이미지를 다운로드한다. 이미지를 다운로드하기 위해 다음과 같은 명령어를 입력하자.

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
5731adb3a4ab: Pull complete
8785c8f663d3: Pull complete
023b6bd393e4: Pull complete
fd8f86b165b0: Pull complete
8f41e7c12976: Pull complete
3b5338ea7d08: Pull complete
Digest: sha256:6650513efd1d27c1f8a5351cbd33edf85cc7e0d9d0fcb4ffb23d8fa89b601ba8
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

docker image ls 명령어를 통해 받아온 이미지 목록을 확인해보면 nginx 이미지를 잘 받아온 것을 확인할 수 있다.

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
nginx        latest    8a5e3e44915c   11 days ago    135MB

3. 이미지 수정하기

이제 nginx를 나만의 이미지로 변경하여 저장소에 업로드하기 위해 Dockerfile을 작성한다.

FROM nginx COPY index.html /usr/share/nginx/html/

그리고 그냥 nginx 이미지만 이용하면 심심하니 직접 만든 페이지를 적용하여 보자. Dockerfile과 동일한 위치에 index.html 파일을 생성한다.

<!DOCTYPE html>
<html>
<head>
  <title>Welcome to my Nginx image!</title>
</head>
<body>
  <h1>Hello from my Nginx image!</h1>
  <p>This is a custom index page served by my Nginx image.</p>
  <p>This image is called "my-nginx-image".</p>
</body>
</html>

4. 변경한 이미지 빌드하기

우리가 정의한 Dockerfile을 사용하여 이미지를 수정한 것을 적용하기 위해 docker build 명령어를 사용해야 한다.

docker build -t [myusername]/[my-nginx-image] .

여기서 [myusername]를 본인의 Docker Hub 사용자이름으로, [my-nginx-image]를 자신이 지정한 이미지 이름으로 변경하면 된다.

필자의 경우는 아래와 같은 명령어를 입력했다.

$ docker build -t langoustine/my-nginx-image .
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM nginx
 ---> 8a5e3e44915c
Step 2/2 : COPY index.html /usr/share/nginx/html/
 ---> 4f247af805f6
Successfully built 4f247af805f6
Successfully tagged langoustine/my-nginx-image:latest

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

빌드가 잘 성공하였고 이미지 목록을 보니 새로 빌드한 이미지가 생성된 것을 확인할 수 있었다.

$ docker image ls
REPOSITORY                   TAG       IMAGE ID       CREATED              SIZE
langoustine/my-nginx-image   latest    4f247af805f6   About a minute ago   135MB

5. 이미지를 Docker Hub에 Push하기

커스엄한 nginx 이미지가 빌드되었다면 docker push 명령을 사용하여 수정된 이미지를 Docker Hub의 자신의 저장소에 푸시하면 된다. 이 때, doker push 명령을 이용한다.

$ docker push langoustine/my-nginx-image
Using default tag: latest
The push refers to repository [docker.io/langoustine/my-nginx-image]
9cffbaac3860: Pushed
7a99131e1da4: Mounted from library/nginx
c61a83b92ad9: Mounted from library/nginx
0d96feb871c8: Mounted from library/nginx
902b28ccafe7: Mounted from library/nginx
3063fc92629d: Mounted from library/nginx
a49c6ceb5b3a: Mounted from library/nginx
latest: digest: sha256:f1a537c373070a1348b0d49d234489131d58d7619a440733c9d2b47eff9e3fb4 size: 1777

이미지가 Docker Hub에 업로드된 것을 확인하자. Public 접근 권한을 두었기에 다른 이들도 해당 이미지를 사용할 수 있다.


6. Docker 이미지를 컨테이너로 실행하기

마지막으로 업로드한 이미지를 가지고 컨테이너를 실행해볼 수 있다. 컨테이너를 실행할 때는 docker run 명령을 사용하여 이미지를 실행한다.

이 때, 실행할 컨테이너의 이름은 langoustine/my-nginxs-image이고, 호스트의 8080포트를 컨테이너의 80포트로 포워딩해보자.

docker run -d -p 8080:80 langoustine/my-nginx-image

위와 같은 명령어를 입력하면 langoustine/my-nginx-image 이미지로 컨테이너를 실행하고 컨테이너의 웹 서버가 호스트의 포트 8080에서 실행된다.

$ docker ps
CONTAINER ID   IMAGE                        COMMAND                  CREATED         STATUS         PORTS                    NAMES
22a2437344d0   langoustine/my-nginx-image   "/docker-entrypoint.…"   4 seconds ago   Up 3 seconds   0.0.0.0:8080->80/tcp     vigilant_archimedes

7. 웹 브라우저에서 확인하기

커스텀한 nginx 이미지를 컨테이너로 실행하였다. 브라우저에 접속해서 앞서 만든 index.html 화면이 출력되는지 확인해보자.

http://localhost:8080으로 접속하여 컨테이너에서 제공되는 사용자 정의 index.html 페이지를 확인하자.

짜잔. 직접 커스텀한 index.html 화면이 출력되었다. DockerHub를 활용하여 내가 만든 이미지를 다양한 방법으로 관리할 수 있다고 느끼게 된 순간이었다.

실습 이후의 뒷정리는 필수

실습 이후 사용하지 않는 더미 데이터들은 깔끔하게 삭제하거나 종료하는 것이 좋다. 이번 실습에서의 뒷정리는 컨테이너 종료 > Docker 이미지 삭제 순으로 마무리를 하려고 한다.

컨테이너 종료
docker ps 명령을 통해 langoustine/ma-nginx-image 이미지로 실행 중인 컨테이너의 ID를 찾아 docker stop 명령으로 종료한다.

docker stop [컨테이너 ID]

Docker 이미지 삭제
그리고 로컬에 다운로드 한 langoustine/ma-nginx-image 이미지를 docker rmi 명령을 사용하여 삭제하면 실습 뒷정리는 끝이다.

docker rmi langoustine/my-nginx-image

DockerHub라는 이미지 저장소를 활용하여 원하는 이미지를 가져와 내 맘대로 커스텀하여 업로드도 해보고, 커스텀한 이미지를 다운로드하여 컨테이너로 실행까지 해보면서 Docker와 Docker 이미지에 대한 이해도를 어느 정도 높일 수 있었다.



Day - 72

PR(Pull Reqeust) 템플릿 만들기

GitHub 저장소로 소스코드 관리 빈도가 많아지다보니 아직 많이 부족하지만 협업자들과 Pull Request(이하 PR)를 자주 올리게 되었다.

내가 개발한 코드의 리뷰를 받고 피드백을 받아 보완하는 등의 방식을 통해 업데이트를 하는 과정을 하다보니 내가 올리는 PR의 내용이 중구난방이라는 것을 알게 되었다.

PR을 올릴때마다 내용이 통일되지 못하는 부분도 있고, 내용의 전달력도 약하다보니 리뷰어들의 리뷰시간이 길어지거나 직접 물어보러 오는 횟수가 잦아졌다.

그래서 하나의 저장소 내에서는 모두가 동일한 양식으로 PR을 작성할 수 있다면 PR을 더욱 효율적으로 사용할 수 있지 않을까 싶은 마음에 팀원들에게 PR 템플릿 파일을 작성하자고 제안했고 흔쾌히 승인이 떨여저서 PR 템플릿 파일을 적용하였다.

PR에는 어떤 내용이 들어가야 할까?

먼저 PR에는 어떤 내용이 들어가면 좋을까? 생각해보게 되었다. 리뷰어에게 내가 작성한 코드가 어떤 목적인지와 코드 흐름을 빠르게 파악할 수 있도록충분한 정보를 전달해야 한다고 생각이 들었다.

그러나 한편으로는 또 너무 방대한 정보를 담으면 가독성이 떨어진다고 느꼈다. 그래서 다음과 같은 키워드를 중점으로 PR 작성양식을 꾸려보기로 하였다.

  • 코드 변경 목적을 포함했는지?
  • 리뷰어에게 개발 의도가 전달되었는지?
  • 캡처이미지와과 같은 이미지를 적극 활용한다면?
  • 완료사항 및 이후 진행사항을 공지했는지?

위와 같은 이슈들을 고려할 수 있는 내용들을 기준으로 PR이 작성되었으면 좋겠다고 생각하였고, 이 키워드들을 활용해 PR 양식에 포함할 3가지 사항을 정리하였다.

  • Motibation: 코드를 추가하거나 변경하게 된 이유나 목적을 명시한다.
  • Key Changes: 주요 개발 사항을 명시한다.
  • To Reviewers: 리뷰어에게 강조하거나 전달하고자 할 내용을 명시한다.

GitHub에서 PR 템플릿 파일 생성하기

이제 GitHub에서 PR을 올릴 때 자동으로 사전에 정의한 양식을 불러와서 작성할 수 있도록 해보자. PR 템플릿을 생성하고 적용하는 방법은 너무나도 간단하다.

먼저 GitHub에서 PR 템플릿을 저장소로 이동하여 Add file을 선택하고 Create new file하여 새로운 파일을 생성하자.

그리고 pull_request_template.md 라는 이름으로 파일을 생성하자.

일반적으로 숨겨진 디렉토리에 저장하기 위해 .github 디렉토리 하위에 파일을 위치시킨다.

New pull request를 통해 새로운 PR을 작성할 때, 앞서 적용한 PR 템플릿이 적용된 것을 볼 수 있다.



Day - 73

Git fork 전략에 대해서

GitHub를 통해 형상 관리를 하면서 별도의 안전장치 없이 원본 저장소에서 모든 작업을 진행하고 있었는데, 이는 꽤 위험한 행동이라는 것을 알게 되었다. 일단 원본 저장소에서 수정 작업에 대한 대응이 취약하다고 느꼈다.

Git을 제대로 쓰지도 못하는 데다가, 심지어 하나밖에 없는 원본 저장소에서 PR 등 여러 플로우가 꼬여버린다면 상당히 골치가 아파질 수도 있다고 느끼게 되었고 원본 저장소를 안전하게 유지시키면서 협업 관리에는 크게 지장이 없는 방법을 찾다가 fork 전략을 알아보게 되었다.

fork 전략이란?

GitHub에서의 fork 전략은 다른 사람이 소유하는 저장소 원본(Upstream Repository)을 그대로 복사하여 나의 로컬 저장소로 가져온 후 변경하고 개선하는 방법이다.

우리는 fork 전략을 통해 기존 프로젝트의 기능을 확장하거나 개선하거나, 해당 프로젝트와 완전히 다른 방향으로 수정하거나, 오픈 소스 프로젝트에 참여할 수 있는 좋은 기회를 만들 수 있다

fork 전략 절차 살펴보기

Github에서 fork 전략을 구체적으로 어떻게 사용할 수 있는지 알아보자.

1. 원본 프로젝트 복사하기

먼저 원본 저장소(Upstream Repository)를 내 개인 저장소(Local Repository)로 포크해온다. 위 사진처럼 GitHub에서 "Fork" 버튼을 클릭하여 원본 프로젝트를 자신의 GitHub 로컬 저장소로 복사하면 된다.


2. 본인의 로컬 저장소에서 작업 진행하기
보통 일반적으로 fork해온 로컬 저장소에서는 프로젝트 목적 달성을 위해 새로운 브랜치를 만들고 작업을 한 후 변경사항에 대해서 커밋을 진행한다.

이 때, 브랜치 이름은 자신이 작업한 변경사항이 무엇인지 알 수 있도록 작명하는 것이 좋다. 예를 들어 be/feat/xxx 등으로 작성할 수 있다.

fork로 가져온 저장소의 경우 위 사진과 같이 forked 문구가 붙은 것을 확인할 수 있다.


3. 변경사항 여부 확인 후 최신화하기
작업한 수정내역들을 PR하기 전에 원본 저장소에 다른 협업자들이 앞서 반영한 내역들이 있는지 확인한다. 만약 원본 저장소에 변경점이 있었다면, 나의 작업 내역과 겹치는 부분이 있는지 확인하자.

작업내역이 겹친다면 소스코드에서 충돌이 발생할수 있기 때문에 내가 수정한 내역에 원본저장소의 변경점을 반영해 두어야 한다.

물론 원본 저장소에 변경점이 있더라도 내 수정내역과 겹치지 않는다면 별도로 수행할 내용은 없고 PR을 보낼 준비를 하면 된다.


4. 원본 저장소(Upstream Repository)로 PR(Pull Request) 보내기
다음으로 내가 작업한 사항을 원본 저장소에 보내어 리뷰 등을 진행한 후 반영될 수 있도록 Pull Request를 보낸다. 항상 PR를 보내기 전에는 작업한 변경사항을 검토한 후 보내야 한다는 것을 명심하자.

PR(Pull Request)는 내가 수정한 작업내역들을 원본 저장소에 반영(Pull)해줄 것을 요청(Request)하는 작업이다.

또한, PR을 보낸 후 협업자나 동료에게 코드 리뷰를 받았다면 피드백에 대한 부분들을 충분히 보완해야 한다. 최종적으로 PR이 승인되면, 원본 저장소에 작업한 변경사항이 반영될 것이다. 이 때, 로컬 저장소에서 만든 브랜치에서 원본 저장소로 PR을 보내면 된다.


5. 최신화된 원본 저장소와의 동기화 진행하기
이 떄, 원본 저장소에서 변경사항이 있을 경우, 자신의 저장소와 원본 저장소를 동기화해야 한다.

Github UI의 "Synk fork" 버튼을 통해서 간단하게 로컬 저장소를 최신화할 수 있다.

최신화가 되어 원본저장소와 로컬저장소의 차이가 없다면 위 사진과 같이 정상적으로 싱크된 상태임을 알려준다.


6. 로컬 저장소를 clone 했던 프로젝트를 최신화하기
로컬 저장소를 clone 하여 두었던 내 컴퓨터의 프로젝트 디렉토리에 최신화된 로컬 저장소의 내역을 가져온다.

이 때 git pull origin main 명령어를 통해 개인 저장소로 가져오면 된다.


6번까지의 절차를 끝냈다면 fork 전략을 한 번 수행했다고 볼 수 있다. 이처럼 fork 전략에서는 위 6가지 과정을 반복해가며 원본 저장소의 안전성을 유지한 채 PR에 대한 리뷰를 강화할 수 있다는 점에서 큰 메리트를 느껴 앞으로 fork 전략을 잘 활용해야 겠다고 다짐하게 되었다.



Day - 74

API 문서 자동화 도구 Swagger와 Spring REST Docs에 대해서

일반적으로 백엔드 개발자와 프론트엔드 개발자가 협업하는 프로젝트에서는 REST API에 대한 명세서를 작성하고 잘 관리해야 한다. 흔히 사용하는 MS Office 제품이나 Google Docs, 노션 등 다양한 문서 도구들이 있지만 보다 개발자스럽게 API 문서를 작성할 수 있도록 도와주는 도구들을 활용할 수 있다.

Java와 Spring 진영에서의 API 문서 자동화 도구의 종류는?

Java 및 Spring을 사용할 때 가장 대중적인 API 문서화 도구는 Swagger와 Spring REST Docs 두가지가 있다.

이 두가지를 간단하게 비교해보고 상황에 따라 어떤 도구를 사용하는 것이 나은지 살펴보려 한다.

Swagger

Spring에서 Swagger를 사용하기 위해서는 컨트롤러 메소드 상단에 어노테이션을 붙여서 API에 대한 내용을 기술한다.

Swagger UI는 위에서 설명한 OpenAPI specification에 맞춰 생성된 REST API 문서를 화면에서 보기 위한 도구라고 보면 된다.

위 사진은 Swagger를 적용했을 때의 화면이다. 보시다시피 UI가 깔끔하고 API 별로 좋은 가독성을 보여준다고 생각이 들었다.

자세한 내용은 Live Demo에서 확인할 수 있다.

Swagger의 의존성을 추가하고 어노테이션만 작성한다면 자동으로 API 문서를 생성해주기에 문서화 도구를 처음 사용해보는 개발자들에게 편리할 것이다. 이와 더불어 개발한 API를 직접 테스트도 해볼 수도 있으니 말이다.

하지만, 컨트롤러 코드, 즉 소스 코드에 문서화에 대한 어노테이션 코드를 작성해야 한다는 점이 편리할 수도 있지만 한편으로는 가독성을 저하시킬 수도 있다. 또한, 기존 API의 내용이 변경되었을 때, 어노테이션을 함께 수정하지 않으면 API 문서는 수정되지 않는다. 결국 문서와 실제 API 스펙이 일치함을 보장할 수 없게 된다.

Swagger 도입 여부

API 문서화 도구를 사용하는 이유는 말 그대로 내가 개발한 API를 문서로써 확인할 수만 있으면 된다고 생각하는데 Swagger의 경우는 문서화 기능보다는 API 테스트에 특화되어 있다고 느꼈다.

Swagger는 API를 테스트하는 환경에 더욱 특화되어 있음을 알 수 있다. 단순한 소규모 프로젝트에서 빠르게 적용하여 사용하기엔 안성맞춤일 것이다.

다마, 프로젝트의 규모가 커질수록 프로덕션 코드의 유지보수성이 떨어지고, 변경점이 늘어난다. Swagger를 도입하려고 한다면 위에서 언급한 내용들을 잘 고려해야 할 것으로 보인다.


Spring REST Docs

Spring REST Docs는 스프링 프레임워크 자체에서 제공하는 API 문서화 도구이다. Swagger와는 다르게 소스 코드에 문서화 도구를 사용하기 위해 별도로 코드를 추가하지 않아도 된다는 차이점이 있다.

위 사진에서 볼 수 있는 UI가 Spring REST Docs로 작성한 API 문서이다. 확실히 Swagger에 비해 정적이고 문서로써의 역할만 담당하게끔 느껴진다.

Spring REST Docs는 테스트 코드를 작성하는 것으로 API 스펙을 명시하게 된다. 사전에 미리 작성한 테스트를 실행하고 테스트가 성공하면, 스니펫이 생성된다.

Spring REST Docs에서의 스니펫이란?
Spring REST Docs에서 스니펫(snippet)은 요청, 응답 또는 경로 매개변수와 같은 API의 특정 측면을 설명하는 작은 문서라고 볼 수 있다. 일반적으로 개발자가 API를 사용하는 방법을 이해하는 데 도움이 되는 예제 코드 또는 기타 정보가 포함된다.


Spring REST Docs가 테스트를 강제하는 이유

테스트 코드를 작성하지 않으면 스니펫을 얻을 수 없고, 스니펫을 얻지 못하면 결국 API 문서를 포함시킬 수 없기 때문에 API 문서를 제공하기 위해서는 반드시 테스트 코드를 작성해야 한다.

Spring REST Docs는 MockMVC 테스트 프레임워크나 WebFlux WebTestClient 또는 RestAssured를 이용해서 테스트를 작성하고 스니펫을 생성할 수 있다.

이처럼 Spring REST Docs가 이처럼 테스트를 강제하는 이유는 무엇일까? 바로 테스트 코드를 기반으로 API 문서를 작성하기 때문에 서비스의 안정성을 보장받을 수 있다는 메리트가 있기 때문이다.

Spring Rest Docs VS Swagger

-Spring Rest DocsSwagger
장점- 제품코드에 영향이 없다.
- 테스트가 성공해야 문서를 작성할 수 있다.
- 코드 변경점을 바로바로 확인할 수 있다.
- API를 테스트해볼 수 있는 화면을 제공한다.
- Spring REST Docs 보다는 쉽고 편리하게 적용할 수 있다.
단점- 테스트코드를 작성해야 하기 때문에 적용하기 까다로울 수 있다.
- 개발자의 노력 정도에 따라 품질편차가 심하다.
- Swagger 관련 설정 코드를 포함시켜야 한다.
- 기존 코드와의 동기화 여부를 잘 고려해야 한다.

Spring REST Docs를 사용하면 아무래도 테스트코드를 기반으로 문서가 생성되다보니 테스트가 성공하는 올바른 프로덕션 코드에 대해서만 문서를 작성할 수 있다. 테스트가 실패하면, 문서를 생성할 수 없다. 그로 인해서 스웨거와는 다르게 API 스펙과 항상 일치하는 문서를 작성할 수 있다.

API 문서화를 위한 테스트코드를 작성해야 한다는 점이 부담일 수 있으나 테스트 코드 수준 능력이 떨어지는 나에게는 오히려 테스트에 대한 이해도와 실력을 향상시킬 수 있는 기회라고 생각이 들었다. 그래서 이번 글에서는 Spring REST Docs를 적용해보려 한다.


Spring REST Docs를 사용하기 위한 테스트 도구

Spring REST Docs를 사용하기 위한 대표적인 테스트 도구로는 MockMvc와 Rest Assured가 있다.

MockMvc
MockMvc는 @SpringBootTest@WebMvcTest 어노테이션과 함께 사용할 수 있다. @WebMvcTest는 프레젠테이션 계층에 속하는 Bean들을 로드하고 나머지 계층은 Mocking을 하게 된다.

@WebMvcTest 어노테이션을 통해 독립적으로 하나의 계층만을 테스트 하는 기법을 슬라이스 테스트라고 한다.

Mocking이란 테스트 중에 가짜 객체를 생성하여 실제 객체와 비슷하게 동작하도록 하는 것이다. Mocking은 실제 객체가 아직 구현되지 않았거나, 테스트의 범위를 제한하고 의존성을 분리하기 위해 사용된다. 그래서 테스트에서 예상치 못한 부작용이 발생하는 것을 방지하고, 테스트 케이스가 예상한 대로 작동하는지 확인할 수 있다.

Rest Assured
Rest Assured는 별도로 설정하지 않는다면 @SpringBootTest 어ㅗ노테이션을 이용해야 한다. @SpringBootTest 어노테이션은 Spring의 빈들을 컨텍스트에 모두 주입하게 된다.

그래서 애플리케이션 운영 환경과 유사한 환경에서 테스트를 진행할 경우 Rest Assured 를 사용할 수 있다. 그래서 결국 @SprintBootTest 어노테이션을 사용하는 Rest Assured는 속도가 많이 느리고 비용도 많이 들 수 있다.

MockMvc VS Rest Assured

Rest Assured는 보통 @SpringBootTest 어노테이션을 이용해테스트를 수행하게 된다. 그러면 전체 컨텍스트를 로드하여 빈을 주입하기에 속도가 매우 느리다.

보통의 경우 서비스 계층을 테스트할 때 일반적으로 Mocking을 이용해 작성하는 경우가 많다. 왜냐하면 특정 계층만을 대상으로 테스트할 때 속도가 빠르기 때문이다.

그래서 MockMVC는 @WebMvcTest 어노테이션을 이용하여 특정 계층만 테스트할 수 있어 통합테스트가 아닌 API 문서를 위한 테스트에는 더 적격일 수 있다.

Spring REST Docs를 사용하기 위한 문서 도구

스니펫과 함께 API 문서를 작성하는데, 문서 작성 도구의 경우 기본적으로 Asciidoctor를 통해 HTML을 만들게 된다. 이 때, Asciidoctor 대신에 Markdown을 사용할 수도 있다.

Markdown
마크다운(Markdown)은 웹상에서 글을 쓰는 모든 사람들을 위한 글쓰기 도구이다. HTML을 몰라도 손쉽게 텍스트 문자열을 HTML형식으로 변환시켜 주기 때문에 일상에서 많이 사용되고 있다.

Asciidoc
쉽고 간편하게 작성할 수 있는 마크다운에 비해 Asciidoc는 전문적인 문서를 작성할 수 있는 강력한 표현력을 가지고 있다.

Markdown VS Asciidoc

markdown의 경우 비교 대상인 asciidoc와 비교하면 큰 단점이 있는데 바로 Include 기능의 부재이다. asciidoc는 include 기능을 통해 다른 asciidoc 문서를 가져와 포함시킬 수 있는데 비해, 마크다운은 이러한 Include 기능을 제공하지 않는다.

물론 markdown도 Slate를 사용하면 import 기능을 사용할 수 있다고 하지만 다소 불편하여 쓰기가 어려우며, Slate를 사용하려면 Ruby와 Gem을 설치해야하는 등 번거로운 작업이 추가된다고 한다.

markdown이 작성하기가 편하긴 하지만, Asciidoc의 include 기능을 사용하면 문서에 외부 파일이나 코드 조각을 쉽게 포함할 수 있으므로 시간을 절약하고 일관성을 유지할 수 있다.

이와 같은 이유로 Markdown보다는 Asciidoc을 사용하기로 하였다.



Day - 75

프로젝트에 Spring REST Docs 적용하기

전날에 Spring의 문서 자동화 도구들을 사용해보기 위해서 문서화 도구들을 살펴보고, 도입할 문서화 도구를 결정했다면 이번 글에서는 Spring REST Docs를 프로젝트에 적용해보려 한다.

필자는 Spring Boot 3.0.2 버전의 환경에서 진행하였으며 Spring REST Docs를 적용하는 과정에서의 대부분은 Spring REST Docs 공식문서를 가장 많이 참고하여 진행하였음을 알린다.

build.gradle 설정하기

첫 번째로 프로젝트의 build.gradle에 Spring REST Docs를 사용하기 위한 설정을 추가한다.

build.gradle

plugins { // (1)
  id "org.asciidoctor.jvm.convert" version "3.3.2"
}

configurations {
  asciidoctorExt // (2)
}

dependencies {
  asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
  // (3)
  testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
  // (4)
}

ext { // (5)
  snippetsDir = file('build/generated-snippets')
  asciidoctorOutputDir = file('build/docs/asciidoc/')
}

test { // (6)
  outputs.dir snippetsDir
}

asciidoctor { // (7)
  inputs.dir snippetsDir // (8)
  configurations 'asciidoctorExt' // (9)
  dependsOn test // (10)
  baseDirFollowsSourceDir() // (11)
}

각 설정들을 주석 순서대로 살펴보자면 다음과 같다.

  1. Asciidoctor 플러그인을 적용한다.
  2. Asciidoctor를 확장하는 종속성에 대한 asciidoctorExt 구성을 선언한다.
  3. asciidoctorExt 구성에서 spring-restdocs-asciidoctor에 대한 의존성을 추가한다. .adoc 파일에서 build/generated-snippets된 스니펫을 가리키도록 스니펫 속성이 자동으로 구성된다.
  4. testImplementation 구성에서 spring-restdocs-mockmvc에 대한 종속성을 추가한다. MockMvc 대신에 WebTestClient나 REST Assured를 사용하려면 spring-restdocs-webtestclient 또는 spring-restdocs-restassured에 대한 의존성을 추가하면 된다.
  5. 스니펫 파일들이 저장될 경로 snippetsDir 디렉토리를 설정한다.
  6. 스니펫 디렉토리 snippetsDir를 추가하도록 test 작업을 설정한다.
  7. asciidoctor 작업을 설정한다.
  8. 스니펫 디렉터리 snippetsDir를 입력으로 불러오도록 설정한다.
  9. 확장을 위해 asciidoctorExt 구성을 사용하도록 설정한다.
  10. 문서가 만들어지기 전에 테스트가 실행되도록 즉 test 작업 이후에 asciidoctor가 실행되도록 설정한다.
  11. .adoc 파일에서 다른 adoc 파일을 include 하여 사용하는 경우 동일한 경로를 baseDir로 설정한다.

Asciidoctor는 adoc 파일을 html 등으로 변환해주는 도구라고 보면 된다.


테스트 코드 작성하기

이제 MockMvc 프레임워크를 이용해 컨트롤러 테스트 코드를 작성해야 한다.

MockMvc 테스트 프레임워크를 구성하는 과정에 대해서는 추후 따로 다뤄보기로 하고 이번 글에서는 Spring REST Docs를 적용하는 것과 관련된 테스트 코드 위주로 살펴보려 한다.

MemberControllerTest.java

@AutoConfigureRestDocs
@WebMvcTest(MemberController.class)
public class MemberControllerTest {

    @Autowired
    private MockMvc mockMvc;

	@Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private MemberSerivce memberSerivce;

	@Test
    public void 사용자_회원가입() throws Exception {
    	// ... 생략
		mockMvc.perform(MockMvcRequestBuilders
                        .post("/api/register")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(objectMapper.writeValueAsString(requestDto))
                )
                .andDo(MockMvcResultHandlers.print())
                .andDo(MockMvcRestDocumentation.document("member/register",
                        Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
                        Preprocessors.preprocessResponse(Preprocessors.prettyPrint())))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }	
}

컨트롤러 테스트코드에서 @WebMvcTestMockMvc를 이용해 사용자_회원가입 메소드를 작성하였다.

REST Docs를 위한 테스트 코드 구문은 아래와 같다.


...

mockMvc.perform(MockMvcRequestBuilders
				.post("/api/register") 
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(requestDto))
			)
            .andDo(MockMvcResultHandlers.print())
            .andDo(MockMvcRestDocumentation.document("member/register",
            	Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
                Preprocessors.preprocessResponse(Preprocessors.prettyPrint())))
            .andExpect(MockMvcResultMatchers.status().isOk());
...

위 코드를 static import를 통해 보기 좋게 코드를 줄일 수 있다.

...
mockMvc.perform(post("/api/register")
				.with(csrf()) // (1)
                .contentType(MediaType.APPLICATION_JSON) // (2)
                .content(objectMapper.writeValueAsString(requestDto)) // (3)
		)
        .andDo(print())
        .andDo(document("member/register", // (4)
        		preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint())))
		.andExpect(status().isOk());
...

각 구문들을 자세히 살펴보면 다음과 같다.

  1. POST 방식으로 /api/register 라는 API로 요청을 전송하는 역할을 한다. perform()의 결과로 ResultActions 객체를 받으며, ResultActions 객체는 리턴 값을 검증하고 확인할 수 있는 andExcpect() 메소드를 제공한다.

  2. JSON 형식의 미디어 유형을 나타내는 데 사용되는 속성이다. 일반적으로 클라이언트와 서버간 HTTP 요청/응답의 페이로드가 JSON 형식임을 표시한다.

MediaType 속성을APPLICATION_JSON_UTF8이 아닌 APPLICATION_JSON 속성을 사용한 이유는 Spring Boot 2.2 버전부터 APPLICATION_JSON_UTF8가 Deprecated되었기 때문이다. Deprecated된 이유는 크롬 같은 주요 브라우저가 스펙을 준수하고, 이제 UTF-8 같은 파라미터 값을 넣어주지 않아도 올바르게 해석 되기 때문이다. 그래서 이제는 APPLICATION_JSON_UTF8을 사용하는 것 보다는 그냥 APPLICATION_JSON 을 사용하면 된다.

  1. requestDto라는 Java Object(객체)를 JSON 문자열로 변환시켜서 검증하도록 한다.

  2. /api/member/register 경로에 스니펫을 생성하도록 Spring REST 문서를 구성한다.

위와 같이 Spring REST Docs를 활용하여 컨트롤러 테스트 코드를 작성하였다.

prettyPrint

위 테스트 코드의 Preprocessors.preprocessRequest, Preprocessors.preprocessResponse 구문을 보면 스니펫의 가독성을 향상시켜 출력할수 있는 prettyPrint를 적용하였음을 알 수 있다.

prettyPrint를 이용해 아래와 같이 IDE의 콘솔에서 JSON 출력 포맷을 파악하기 쉽도록 조정할 수 있다.

{
  "type" : "LOCAL",
  "email" : "lango@kakao.com",
  "nickname" : "lango",
  "password" : "test123",
  "role" : "USER",
  "picture" : "test"
}

adoc 스니펫 생성하기

다음으로 작성한 테스트 코드를 실행하게 되면 build/generated-snippets/{document() 지정 경로} 디렉토리를 확인해보자.

아래과 같이 .adoc 확장자를 가지는 여러 스니펫이 생성된 것을 확인할 수 있다.

http-reqeust.adoc 파일을 열어보면 아래와 같은 내용을 볼 수 있다.

POST /register HTTP/1.1
Content-Type: application/json;charset=UTF-8
Content-Length: 144
Host: localhost:8080

{
  "type" : "LOCAL",
  "email" : "lango@kakao.com",
  "nickname" : "lango",
  "password" : "test123",
  "role" : "USER",
  "picture" : "test"
}

테스트 종료 후 생성된 adoc 스니펫들을 가지고 하나의 API 명세서를 만들면 된다.

src/docs/asciidoc 디렉토리를 구성하여 .adoc 파일을 직접 생성하고 asciidoc 문서를 작성하자.

member.adoc

= Member 관련 API 명세서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== 사용자 회원가입

=== Request
include::{snippets}/member/register/http-request.adoc[]

=== Response
include::{snippets}/member/register/http-response.adoc[]

src/docs/asciidoc 디렉토리에 member.adoc 이라는 파일을 생성하였다.

이 때, include::{snippets}/member/register/http-request.adoc[]와 같은 include 기능을 통해 자동으로 만들어준 스니펫 .adoc 파일을 이용할 수 있음을 알 수 있었다. asciidoc을 작성하는 문법에 대한 구체적인 내용은 다음에 알아보기로 하자.


스니펫으로 HTML 파일 생성하기

이제 다시 빌드를 실행하면 asciidoctor 작업을 실행되고 앞서 구성한 스니펫들을 통해 html 문서를 생성할 수 있을 것이다.

이 때, 만들어지는 API HTML 문서는 src/docs/sciidoc 디렉토리에 생성한 .adoc 파일, 여기서는 member.adoc 파일을 기반으로 생성된다.

build.gradle 빌드 진행
이제 빌드를 진행하여 HTML 파일이 생성되는지 확인하자. 인텔리제이에서 빌드를 직접 실행하거나 터미널에서 아래 명령어를 입력하면 된다.

./gradlew build

빌드 내용을 보면 asciidoctor 작업이 잘 실행된 것을 볼 수 있다.

> Task :asciidoctor

빌드하게 되면 build/docs/asciidoc/ 디렉토리에 우리가 작성한 adoc 파일과 이름이 동일한 html 파일이 생성된 것을 알 수 있다.

member.html 파일을 열어보니 아래와 같이 원하던 API 명세서가 출력되었다.


API 명세서 출력하기

마지막으로 스프링 애플리케이션을 실행한 상태에서 직접 작성한 API 문서로 접근할 수 있어야 한다.

그런데 여태까지 html을 만들지 않았는데, asciidoctor가 html을 만들어주도록 설정해야 한다. 그래서 build.gradle에 다음과 같은 내용을 추가한다.

build.gradle

task copyDocument(type: Copy) { // 1
	dependsOn asciidoctor
	from file("build/docs/asciidoc/")
	into file("src/main/resources/static/docs")
}

build {
	dependsOn copyDocument
}

bootJar { // 2
	dependsOn copyDocument
	from ("${asciidoctor.outputDir}") {
		into 'src/main/resources/static/docs'
	}
}

추가한 내용을 살펴보면 다음과 같다.

  1. copyDocument 작업이 실행될 때 먼저 asciidoctor 작업을 진행시킨다. 그리고 빌드를 통해 만들어진 build/docs/asciidoc/ 경로의 html 파일들을 src/main/resources/static/docs 옮겨 놓는다.

  2. bootJar 작업이 실행될 때 먼저 copyDocument 작업을 진행시켜서 미리 API 문서가 될 HTML 파일을 생성한다. 그리고 앞서 생성한 API 문서 HTML 파일을 static/docs 디렉토리에 넣는다.

모든 설정을 마무리하였다. 이제 스프링 어플리케이션을 실행하고 브라우저에서 접속하여 API 문서를 출력하는 것을 확인하면 된다.

http://localhost/docs/member.html 으로 접속하니 앞서 만든 API 문서가 정상적으로 출력되는 것을 알 수 있었다.






Final..

카카오에서 지원하는 클라우드 특강을 듣는 한주가 금새 지나갔다. 실무자 세 분을 통해 클라우드 생태계에서 개발자로 어떻게 살아남아야 하는지 알 수 있었고 그와 더불어 냉혹한 현실감도 느낄 수 있었다.

Spring 공부만 집중해서 했던 것 같은데, 이제는 알고리즘 문제도 열심히 풀어야 하고 클라우드 CS 공부도 많이 해야한다.

더군다나 이번 주 부터는 파이널 프로젝트를 시작하게 된다. 교육이 마무리가 되고 교육을 통한 압박감은 조금 나아지겠지만, 더 큰 산이 남아있는 것 같아 불안하고 초조하다. 그리고 프로젝트 인프라 구축부터 애플리케이션 설계 및 개발 그리고 진행과정에 따른 문서화 정리를 잘 해야한다고 생각하고 있지만 사실 어떻게, 어디서부터 시작할지 조금 막막한 감이 없지 않다.

그럼에도 불구하고, 프로젝트를 통해 배웠던 것, 공부했던 것을 온전히 녹여내기 위해 노력할 것이다. 단지 취업을 위한 프로젝트가 아닌 좋은 개발자로서 성공하기 위해 어떤 프로젝트를 해야할지에 초점을 두기로 다짐하였다.



혹여 잘못된 내용이 있다면 지적해주시면 정정하도록 하겠습니다.

게시물과 관련된 소스코드는 Github Repository에서 확인할 수 있습니다.

참고자료 출처

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글