24주차에 한것

이번 주에는 nginx와 certbot을 이용하여 HTTPS를 적용해보았다.

또한 HTTPS 적용 시 스웨거 관련 CORS 오류를 해결하였다.

 

이번 주에 한 일

  • 유저 정보 조회/수정 기능 구현
  • 게시글 작성 기능 구현
  • 배포 서버에 HTTPS 적용
  • HTTPS 적용 시 스웨거에서 발생하는 CORS 오류 해결

결과

저번 팀 프로젝트에서는 Route53로 인증서를 발급하고 AWS ELB로 HTTPS로 리다이렉트 시켰었는데

nginx와 certbot으로 다른 AWS 서비스 없이 EC2만으로 HTTPS를 적용할 수 있었다.

그동안 nginx로 인증서를 발급받고 적용할 수 있다고 듣기만 했었는데 괜히 쫄아서 못했었다.

이번 기회에 nginx로 HTTPS를 적용해보면서 nginx에 대해 어느정도 감을 잡을 수 있어서 좋았다.

저번에 프리티어 끝나고 ELB를 제거해두지 않아서 요금이 꽤 나왔었는데 무료로 할 수 있다는 점도 좋았다.

 

또한 프론트와 협업하면서 API 명세서로 스웨거를 사용하고 있었는데, HTTPS 적용 전에는 잘 되던게

HTTPS를 적용하면서 스웨거에서 갑자기 CORS 오류가 발생하여 이를 수정하였다.

스웨거를 이번에 처음 써보는거라 스웨거 관련 설정에서 어려움을 겪었던것 같다.

 

이번 주에 아쉬웠던 부분은 아래와 같다.

  • 이번 주는 프로젝트에 집중을 잘 하지 못했음.
  • 이미지 저장 기능까지 끝내고 싶었으나 하지 못함.

HTTPS 사용하기

로컬에서는 HTTP 요청으로도 테스트 할만하지만 배포 환경에서 HTTP 만으로는 제한되는 부분이 조금 있다.

특히 쿠키를 사용할 때 크롬 정책으로 인해 쿠키 확인이 너무나도 힘들다.

프론트와 백엔드가 같은 도메인인 경우에는 Strict 쿠키 정책으로 어떻게 HTTP로도 가능하겠지만,

다른 도메인이라면 방법이 없다.

그래서 인증서를 발급받고 SSL을 적용해서 HTTPS 요청을 사용하여 Secure 쿠키 정책을 사용해야 한다.

HTTPS는 인증서, 암호화 이런 단어가 들어있어서 어려울것 같지만 생각보다 간단하고 빠르게 할 수 있다.


사용 기술

  • EC2
  • Nginx
  • certbot

1. SSL 적용

SSL을 적용하기 위해서는 인증서가 필요하다.

전에는 Route53에서 매월 0.5달러인가 주고 했지만 이번에는 certbot으로 무료로 해보려고 한다.

certbot의 공식 문서 를 참고하면 아래와 같은 과정을 거친다.

사실상 그대로 가져왔으니 공식 문서만 봐도 충분히 따라할 수 있다.

# 1. certbot을 snap 명령어로 설치 및 실행하므로 snap을 먼저 설치한다
sudo snap install core
sudo snap refresh core

# 2. 기존에 설치된 certbot을 제거한다
# 공식 가이드에선 certbot명령어를 사용할 때 snap이 사용되게 하기 위함이라고 되어있다.
sudo apt-get remove certbot

# 3. certbot을 설치한다
sudo snap install --classic certbot

# 4. certbot 명령어가 실행될 수 있게 세팅한다
sudo ln -s /snap/bin/certbot /usr/bin/certbot

# 5. 아래 명령어를 입력하면 certbot이 nginx 구성을 자동으로 수정한다.
# nginx가 아닌 apache를 웹서버로 사용할 경우, sudo certbot --apache 가 된다
sudo certbot --nginx -d midcon.store -d www.midcon.store

# certbot은 CLI몇 줄로 SSL을 적용해줄 뿐 아니라 자동 리뉴얼까지 해준다
# 처음 설치할 때부터 이러한 cron job 처리를 위한 내용을 site-available/default 에 자동으로 설정해준다
# 아래 명령어로 자동 리뉴얼이 적용되고 있는지 확인할 수 있다
sudo certbot renew --dry-run

 

5번 커맨드를 입력하면 아래처럼 이메일, 약관 동의 여부 등등을 입력하는 부분이 있다.

당황하지말고 적당히 읽어보고 입력하면 된다.

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): hyukkind@naver.com   # 이메일 입력

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf. You must agree in
order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y  # ACME 약관에 동의하는지 N선택시 진행불가

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y   # 이메일을 통해 Let's Encrypt 프로젝트 정보를 받아볼지
Account registered.
Requesting a certificate for midcon.store and www.midcon.store

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/midcon.store/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/midcon.store/privkey.pem
This certificate expires on 2024-08-15.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for midcon.store to /etc/nginx/sites-enabled/default
Successfully deployed certificate for www.midcon.store to /etc/nginx/sites-enabled/default
Congratulations! You have successfully enabled HTTPS on https://midcon.store and https://www.midcon.store

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

2. 결과 확인

2-1. /etc/nginx/sites-available/default 확인

아래 사진은 /etc/nginx/sites-available/default 로 들어가본 결과이다.

localtion /ws 부분은 본인이 웹소켓 설정하느라 추가한 부분이고 아래 부분부터 보면 되는데

아래처럼 certbot이 nginx 설정까지 자동으로 변경한 알 수 있다.

 

HTTP로 요청하면 HTTPS로 리다이렉트 시키는 설정도 certbot이 자동으로 해두었다.

2-2. HTTP/HTTPS 요청 시

이제 HTTP 로 요청하면 자동으로 HTTPS 로 리다이렉트 되고, HTTPS 요청도 잘 되는걸 확인할 수 있다.


3. 추가

만약 인증서를 삭제하고 싶다면 아래 명령어를 입력하면 된다.

sudo certbot delete

 

그럼 아래와 같은 창을 확인할 수 있고, 여기서 삭제하고 싶은 인증서의 번호를 입력하면 된다.

아쉽지만 nginx 설정까지는 자동으로 삭제해주지는 않는것 같다.

인증서 삭제 후 nginx 변경 부분은 수동으로 삭제하자.

 

 

참고자료

 

10분만에 끝내는 EC2 생성, NGINX 구성, SSL적용

이 포스팅에선 이론적인 내용에 보다는 구성 방법과 흐름에 대해서만 조망합니다. EC2 생성, NGINX 설치, 프록시 설정, 도메인 및 SSL 적용을 해본 적이 없거나 과정에 대해 모호한 부분이 있으시다

creampuffy.tistory.com

 

 

Certbot으로 무료 HTTPS 인증서 발급받기

Let’s Encrypt - Free SSL/TLS CertificatesLet’s Encrypt is a free, automated, and open certificate authority brought to you by the nonprofit Internet Security Research Group (ISRG).Free SSL/TLS Certificates Let's Encrypt라는 비영리 기관을 통해

www.vompressor.com

 

도메인 적용

EC2에 애플리케이션 서비스를 배포까지는 했다.

하지만 아직 도메인을 적용하지 않아서 불편하게 퍼블릭 IP를 입력해서 접속해야한다.

이번 글에서는 이름을 가진 도메인을 붙여볼것이다.

Nginx를 리버스 프록시로 사용하여 도메인으로 들어온 요청을 EC2 8080 포트로 포워딩 해보자.


사용 기술

  • EC2
  • Nginx

1. 도메인 확보

우선 도메인을 붙이려면 도메인을 어떻게든 마련해야 한다.

인터넷에 검색해보면 무료로 구할 수 있는 사이트도 있고, 가비아 같은 호스팅 사이트도 많다.

본인은 가비아에서 1년에 500원짜리 도메인을 샀다.

마이 페이지에서 아래의 도메인 란을 누르면 내 도메인을 확인할 수 있다.


2. DNS 레코드 수정

이제 도메인의 DNS 레코드를 설정해서 구매한 도메인으로 접속 시 EC2 서버로 포워딩 해줄것이다.

2-1. 내 도메인의 관리 창으로 이동

2-2. DNS 정보 란의 도메인 연결 설정 창으로 이동

내 도메인 관리 창에서 아래로 내려보면 DNS 정보 란이 보일것이다.

여기서 설정을 누르면 DNS 관리페이지로 넘어간다.

2-3. DNS 관리 페이지에서 DNS 설정

DNS 설정 버튼을 눌러서 DNS 레코드 설정을 해준다.

아래처럼 추가해준다.

호스트는 www, @ 를 넣어준다.

www로 넣으면 www.midcon.store 를 등록하는것이고 @로 넣으면 midcon.store 를 등록함을 의미한다.

값/위치에 해당하는것은 본인의 EC2 인스턴스의 퍼블릭 IP이다.


3. EC2 인스턴스의 인바운드 규칙 설정

우리는 HTTP 혹은 HTTPS 요청으로도메인명/경로 와 같은 형태로 요청을 받을 것이다.

따라서 HTTP  HTTPS에 해당하는 포트 번호를 인바운드 규칙 설정 해줘야한다.

보안그룹으로 이동해서 아래처럼 HTTP, HTTPS에 해당하는 80, 443 포트를 열어준다.


4. Nginx 설정

4-1. Nginx 설치

우선은 EC2에서 Nginx를 설치한다.

Ubuntu 환경의 경우 아래의 명령어를 입력하여 설치한다.

sudo apt-get install nginx -y   // Nginx 설치

 

Nginx가 제대로 설치 되었다면 EC2의 퍼블릭 IP 주소로 접속하면 아래와 같은 화면이 뜰것이다.

4-2. Nginx 설정

HTTP 요청의 기본 포트값은 80이므로 도메인 명:8080 이 아닌 도메인 명 만 입력했을 때 80번 포트로 요청이 들어온다.

따라서 이러한 80번 포트의 요청을 원래 EC2 인스턴스의 8080 포트로 연결시켜야 한다.

이것을 Nginx를 이용해서 해볼 것이다.

 

Nginx의 기본 설정 값은 /etc/nginx/sites-available/default 파일에 설정돼 있다.

이 파일을 수정하여 원하는 설정으로 바꿀것이다.

리눅스 커맨드를 좀 알면 알아서 하겠지만 모른다면 아래 명령어를 입력하면 된다.

sudo vim /etc/nginx/sites-available/default    // 관리자 권한으로 vim으로 뒤 경로 파일 실행

 

위 파일에 들어가면 대략 아래와 같은 창이 뜬다.

 

그럼 i를 눌러서 수정모드로 위 기본 서버 설정 아래에 아래처럼 입력해준다.

아래 설정이 위에서 설명한 80번 포트를 8080번 포트로 연결시켜주는 설정이다.

저장은 Ctrl + c 이후 :wq 입력 후 엔터를 누르면 된다.

# server_name 에 적힌 도메인으로 "/" 이하 경로로 접근 시(사실상 모든 경로) 8080포트로 위임 
server {
    listen 80;
    server_name midcon.store www.midcon.store;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}

 

설정을 변경할 때마다 아래 명령어로 Nginx를 재시작 해줘야 한다.

sudo service nginx restart

5. 결과 확인

아래처럼 도메인으로 접속해도 8080포트로 접속하는것처럼 잘 접속 된다.

 

 

참고자료

 

10분만에 끝내는 EC2 생성, NGINX 구성, SSL적용

이 포스팅에선 이론적인 내용에 보다는 구성 방법과 흐름에 대해서만 조망합니다. EC2 생성, NGINX 설치, 프록시 설정, 도메인 및 SSL 적용을 해본 적이 없거나 과정에 대해 모호한 부분이 있으시다

creampuffy.tistory.com

23주차에 한것

이번 주에는 그간 괜히 쫄려서 못만져보던 Redis를 만져보았다.

어느정도 머리가 컸는지 레디스도 그렇게 어렵지는 않았다.

또한 yml 파일의 양이 점점 커져서 공통된 부분과 로컬/배포 환경에서 다른 설정을 분리하였다.

그리고 이번 주도 스프링 시큐리티가 적용된 상태에서 컨트롤러 테스트를 하기 위해 시큐리티와 한동안 씨름했다.

언제쯤 익숙해질런지...

 

이번 주에 한 일

  • 레디스에 발급한 인증 번호를 저장하는 로직 구현
  • 회원가입 시 JWT를 발급하는 로직 추가
  • 스프링 시큐리티 내에서 컨트롤러 테스트 환경을 구성하기 위한 설정
  • 로컬, 배포 환경으로 yml 파일 분리

결과

이번 주도 대부분의 시간을 스프링 시큐리티 내에서 컨트롤러 테스트를 하는 방법을 궁리하는데 대부분의 시간을 보냈다.

아무래도 아직 Mocking에 안익숙하기도 하고, JWT 인증 필터가 생각대로 동작하지 않아서 애를 먹었다.

@MockBean 을 써서 어떻게 해보려 했지만 실패했다.

결국 테스트용 SecurityConfig 를 만들어서 테스트에선 JWT 인증 필터를 사용하지 않도록 하였다.

그리고 커스텀 @WithMockUser를 만들어서 인증 필터를 거치지 않고 SecurityContext의 Principal을 쓸 수 있도록 했다.

 

그래도 이번 주엔 레디스도 만져보고, yml 파일 분리하는 법도 익히고, 스프링 시큐리티 내에서 컨트롤러 테스트를 하는 방법도 익힐 수 있어서 제법 알찼다.

문제는 작업량이 적어서 다음 주는 더 열심히 해야한다는 점...

 

이번 주에 아쉬웠던 부분은 아래와 같다.

  • 이번주도 스프링 시큐리티에 시간을 너무 많이 빼앗겨서 작업량이 적었음
  • 아직 Mocking을 하는게 미숙해서 아쉬움

15장 정리

애플리케이션을 설계하다 보면 과거에 경험하고 해결했던 방법을 다시 사용하는 경우가 있다.
이렇게 소프트웨어 설계에서 반복적으로 발생하는 문제에 대해 반복적으로 적용할 수 있는 방법을 디자인 패턴이라고 한다.
디자인 패턴의 목적은 설계를 재사용하기 위함이다.
프레임워크는 설계와 코드를 함께 재사용하기 위한 것이다.
디자인 패턴과 프레임워크 모두 일관성 있는 협력과 관련이 있다.
디자인 패턴은 특정한 변경을 일관성 있게 다루는 협력 템플릿을 제공하고,
프레임워크는 특정한 변경을 일관성 있게 다룰 수 있는 확장 가능한 코드 템플릿을 제공한다.

디자인 패턴과 설계 재사용

소프트웨어 패턴

패턴은 한 컨텍스트에서 유용한 동시에 다른 컨텍스트에서도 유용한 아이디어다.
패턴이 지닌 가장 큰 가치는 경험을 통해 축적된 실무 지식을 효과적으로 요약하고 전달할 수 있다는 점이다.
패턴은 경험의 산물이기 때문에 초보자라고 하더라도 패턴을 익히고 반복적으로 적용하는 과정 속에서 유연하고 품질 높은 소프트웨어를 개발하는 방법을 익힐 수 있다.
패턴은 '이름'을 통해 지식 전달과 커뮤니케이션의 순단으로 사용할 수 있기 때문에 이름 짓기가 중요하다.

패턴과 책임-주도 설계

패턴은 공통으로 사용할 수 있는 역할, 책임, 협력의 템플릿이다.
적절한 패턴을 따르면 특정한 상황에 적용할 수 있는 설계를 쉽고 빠르게 떠올릴 수 있다.
하지만 디자인 패턴을 따른다는건 역할, 책임, 협력의 관점에서 유사성을 공유하는것이지 특정한 구현 방식을 강제하는 것은 아니다.
대부분의 디자인 패턴의 목적은 특정한 변경을 캡슐화함으로써 유연하고 일관성 있는 협력을 설계할 수 있는 경험을 공유하는 것이다.
따라서 디자인 패턴에서 중요한 것은 디자인 패턴의 구현 방법이나 구조가 아니라 어떤 변경을 캡슐화하는지를 이해하는 것이 중요하다.

패턴은 출발점이다.

대부분의 패턴 입문자는 패턴을 적용하는 컨텍스트의 적절성을 무시한 채 맹목적으로 패턴의 구조에만 초점을 맞추기 쉽다.
따라서 부적절한 상황에서 부적절하게 사용된 패턴으로 인해 유지보수 하기 어려운 시스템을 만드는 것을 주의해야 한다.
패턴을 남용하지 않기 위해서는 다양한 트레이드오프 관계 속에서 패턴을 적용하고 사용해 본 경험이 필요하다.
정당한 이유 없이 사용된 모든 패턴은 설계를 복잡하게 만드는 장애물이다.
패턴을 적용할 때는 항상 설계를 좀 더 단순하고 명확하게 만들 수 잇는 방법이 없는지를 고민해야 한다.
또한 코드를 공유하는 모든 사람들이 적용된 패턴을 알고 있어야 한다.
패턴을 적용할 때는 함께 작업하는 사람들이 패턴에 익숙한지 여부를 확인하고, 그렇지 않다면 설계에 대한 지식과 더불어 패턴에 대한 지식도 함께 공유하는 것이 필요하다.
패턴을 가장 효과적으로 적용하는 방법은 패턴을 지향하거나 패턴을 목표로 리팩터링하는 것일 수 있다.

프레임워크와 코드 재사용

프레임워크란 '추상 클래스나 인터페이스를 정의하고 인스턴스 사이의 상호작용을 통해 시스템 전체 혹은 일부를 구현해 놓은 재사용 가능한 설계' 또는 '애플리케이션 개발자가 현재의 요구사항에 맞게 커스터마이징 할 수 있는 애플리케이션의 골격'을 의미한다.

제어 역전 원리

의존성 역전 원리를 따르는 설계는 변경에 유연하게 대처할 수 있다.
이런 의존성 역전 원리는 프레임워크의 가장 기본적인 설계 메커니즘이다.
의존성 역전은 의존성의 방향 뿐만 아니라 제어 흐름의 주체 역시 역전시킨다.
협력을 제어하는 것은 프레임워크이고, 우리는 프레임워크가 적절한 시점에 실해할 것으로 예상되는 코드를 작성한다.


인상깊었던 점

"정당한 이유 없이 사용되는 패턴은 유지보수를 어렵게 만드는 장애물이 된다."
이 부분이 이번 장에서는 인상깊었다.
디자인 패턴을 공부하고서 의욕이 넘쳐서 이리저리 써봤지만 결국 적용하고 보니 과한 설계였던 경험들이 있었다.
이 경험들이 떠오르면서 정당한 이유 없이 사용되는 패턴은 유지보수를 어렵게 만드는 장애물이 된다는 부분이 더 와닿았다.

'공부방 > ' 카테고리의 다른 글

[도서] 함께 자라기 - 애자일로 가는 길을 읽고  (0) 2023.11.02

yml 파일 분리의 필요성

팀 프로젝트를 진행하면서 yml(YAML) 파일의 길이가 꾸준히 늘어나고 있다.

배포하고 도메인까지 연결해보니 로컬에서 테스트할 때와 배포 환경에서 다르게 해줘야할 설정이 점점 많아졌다.

그래서 매번 빌드할때와 테스트할 때 yml 파일을 여러 군데 수정해야하는 불편함까지 생기기 시작했다.

yml 파일를 수정해야 하는 이유를 정리해보면 아래와 같았다.

  • 의존성 추가(Sprind Data Redis, Oauth2 Client 등)
  • DB 설정 분리(로컬은 H2, 배포에선 MySQL)
  • 기능 추가 시 Secret과 같은 환경 변수 설정
  • 쿠키, 스웨거 등의 도메인 설정(로컬에선 localhost, 배포에선 배포 도메인 주소)

로컬에서 테스트 할때와 빌드를 할때 수정할 부분을 최소화 하기 위해 yml 분리 방법을 간단하게 공부하고 정리해보려 한다.

기준은 본인이 스프링을 사용하므로 스프링 기준이다.


yml 파일들을 분리하기 위한 규칙

1. 파일 이름 규칙

yml 파일 이름은 나름의 규칙을 지켜야한다.

application-{profile이름}.yml 의 규칙으로 짓는다.

스프링에서는 {profile이름}으로 yml 파일을 구분하고 활용할 수 있다.

2. application.yml 위치

이건 따로 변경할 수 있다곤 하지만 아래처럼 resources 경로에 둔다.

3. 다른 프로필의 yml 파일 위치

아래처럼 depth를 나눠서 사용할 수도 있지만 본인은 application.yml 과 같은 depth로 사용하려 한다.

project
├── src
│   ├── main
│   ├── java
│   ├── resources
│   │   ├── static
│   │   ├── templates
│   │   ├── yaml
│   │   │   ├── logging
│   │   │   │   └─ application-log.yml
│   │   │   ├── application-dev.yml
│   │   │   └─ application-local.yml
│   │   └─ application.yml

yml 파일 분리 해보기

application.yml

분리하는 기준은 정하기 나름이겠지만 본인은 application.yml 파일에 공통 설정을 담았다.

server:
  port: 8080
spring:
  profiles:
    group:                # yml 파일 여러개를 그룹화 하고싶을 때 사용
      test: local, log   # 이렇게 local, log를 그룹화하고 사용할 때는 active: test
    active: local         # 로컬에서 테스트할 때는 local, 빌드 시엔 prod 로 이 부분만 변경

  data:
    redis:
      host: localhost
      port: 6379
      repositories:
        enabled: false
  main:
    allow-bean-definition-overriding: true

pawland:
  front-test-url: "http://localhost:3000"    # 해당 라인과 아래 라인도 local, prod 로 나누면 하나로 관리할 수 있었을듯
  front-deploy-url: "https://www.pawland.store"
  jwt-key: # JWT 키

gmail:
  host: smtp.gmail.com
  port: 587
  username: hyukkind@gmail.com
  password: # 구글 앱 비밀번호
  smtpProperties:
      auth: true
      starttls-enable: true
      starttls-required: true
      connection-timeout: 5000
      timeout: 5000
      writeTimeout: 5000

aws:
  access-key: # AWS 액세스 키
  secret-key: # AWS 시크릿 키
  s3-image-prefix:  # S3 이미지 프리픽스
  s3-access-point: # S3 액세스 포인트

application-local.yml

로컬에서 사용하는 yml 설정이다.

로컬에서는 인메모리에서 테스트하고, ddl-auto 기능도 마음대로 사용할 수 있다.

spring:
  config:
    active:
      on-profile: local  # 프로필 이름 설정

  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL
  hikari.maximum-pool-size: 20
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: create   # 로컬에선 ddl-auto create로 해도 됨
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        show_sql: true
        dialect: org.hibernate.dialect.H2Dialect
        
pawland:
  back-domain: "localhost"
  back-url: "http://localhost:8080"

application-prod.yml

배포할 때 사용할 yml 설정이다.

배포 환경에서는 ddl-auto 때문에 낭패를 볼 수 있으므로 none으로 설정하고

DB도 새로운 버전으로 배포해도 데이터가 유지되게 MySQL로 바꿔준다.

spring:
  config:
    active:
      on-profile: prod

  datasource:
    url: jdbc:mysql:// # RDS or EC2로 띄운 MySQL 주소
    username: # 사용자
    password: # 비밀번호

  hikari.maximum-pool-size: 20

  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: none   # ddl-auto 는 prod 에선 none
    properties:
      hibernate:
        format_sql: true  # 이 부분도 false 해도 될듯
        show_sql: true    # 이 부분도 false 해도 될듯
        dialect: org.hibernate.dialect.MySQLDialect
        
pawland:
  back-domain: "midcon.store"
  back-url: "https://www.midcon.store"

 

참고자료

 

[Spring] 하나의 YAML을 여러 개로 나누어 환경 분리하기

스프링에서는 다양한 환경 변수 및 정보들을 활용하기 위해 yaml 및 properties를 지원한다. 하나의 yaml 파일들을 여러 개의 yaml로 나누는 방법을 익혀보자.

velog.io

 

스프링 시큐리티

나는 스프링 시큐리티가 너무 싫다.

공부하면서 느낀건 편한것도 많지만 간단한 JWT 정도 적용하는데는 굳이 써야하나? 라는 생각이다.

우선 변경되는 부분이 많아서 전에 썼던 프로젝트 코드를 다시 쓰려면 deprecated 된게 한가득이다.

그렇다보니 바꿀 부분이 많아져서 스프링의 장점인 레퍼런스가 많다는 장점이 퇴색된다.

또 스프링 시큐리티라는 컨텍스트에 너무 의존적이라고 생각한다.

그래서 이걸 쓰려면 내부 구현을 속속들이 알아야 의도치 않은 결과를 만나지 않는다.

로그인을 FormData 형식으로 받아서 Json으로 로그인을 하려면 따로 필터를 만들어야 하는것도 불편하다.

복잡한 과정들을 추상화해서 쓰기 편하게 만들었단건 알겠지만 덕분에 공부해야할거도 꽤 있고 복잡하다.

여러데서 쓴다니까 공부는 해둬야지 별 수 있나 싶긴 하지만 그래도 싫은건 싫다.


시큐리티 환경에서 컨트롤러 테스트

무엇보다 내가 싫은건 시큐리티 환경에서 테스트 코드를 작성하기가 너무 번거롭다는 것이다.

아마 지금의 내 JWT 필터 로직이 스프링 시큐리티 컨텍스트와 따로 노는게 문제라고 생각은 하지만,

도무지 시큐리티 컨텍스트 내에 내 JWT 필터 로직을 넣는 방법을 모르겠다.

Argument Resolver로 JWT 기능을 구현했으면 진작 끝냈을것 같다.

이전 JWT 필터 글에서 썼던 대로 기본적으로 모든 요청에 대해 JWT를 검사한다.

따라서 컨트롤러 테스트를 할 때도 이 필터를 거쳐야하기 때문에 JWT를 안넣어주면 필터단에서 걸러진다.

이걸 어떻게 처리해야 하는지 고민하다가 시행착오 끝에 결국 테스트용 SecurityConfig를 만들기로 했다.


구현

TestSecurityConfig

프로덕션 코드에서 검색되지 않도록 테스트 디렉토리에 아래처럼 테스트용 SecurityConfig를 작성한다.

@TestConfiguration
@EnableWebSecurity
@RequiredArgsConstructor
public class TestSecurityConfig {

    private final ObjectMapper objectMapper;
    private final UserDetailsService userDetailsService;
    private final JwtUtils jwtUtils;

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring()
            .requestMatchers("/favicon.ico")
            .requestMatchers("/error")
            .requestMatchers(toH2Console());
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jsonAuthFilter(), UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(e -> {
                e.authenticationEntryPoint(new Http401Handler(objectMapper));
                e.accessDeniedHandler(new Http403Handler(objectMapper));
            })
            .csrf(AbstractHttpConfigurer::disable)
            .build();
    }

    @Bean
    public JsonAuthFilter jsonAuthFilter() {
        JsonAuthFilter filter = new JsonAuthFilter("/api/auth/login", objectMapper);
        filter.setAuthenticationManager(authenticationManager());
        filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(objectMapper, jwtUtils));
        filter.setAuthenticationFailureHandler(new LoginFailHandler(objectMapper));
        return filter;
    }

    @Bean
    public AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new SCryptPasswordEncoder(
            16,
            8,
            1,
            32,
            64
        );
    }
}

SecurityConfig

기존 SecurityConfig에 아래처럼 @Profile 애노테이션을 이용해 특정 프로필에서 사용하지 못하게 설정한다.

본인은 test 프로필에서 사용하지 못하게 했다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Profile("!test")
public class SecurityConfig {

사용법

1. 테스트용 config를 Import 및 @ActiveProfiles 설정

테스트 클래스에 아래 스크린샷처럼 @import(TestSecurityConfig.class), @ActiveProfiles("test") 를 붙여준다.

2. 인증 필요한 테스트에 커스텀 @MockUser 애노테이션 적용

아래처럼 커스텀@MockUser 애노테이션을 적용한다.

@WithMockUser

스프링 시큐리티를 사용하면 시큐리티 컨텍스트에서 Principal 정보를 꺼내 쓸 수 있다.

따라서 인증 완료된 유저는 컨트롤러에서 @AuthenticationPrincipal 을 사용하여 사용자 정보를 이용하곤 한다.

하지만 컨트롤러 테스트를 할 때 컨트롤러를 호출하면 인증을 하지 않았기 때문에 UserPrincipal 객체가 null일 것이다.

당연히 실제 환경처럼 컨트롤러 호출 시 쿠키에 Jwt를 넣는 등의 인증 과정을 거치면 되겠지만,

매 테스트마다 유효한 Jwt를 만들어서 넣어줘야한다는 문제가 생긴다.

유저 정보를 DB에 넣고, 유저 정보로 Jwt를 만들고, 요청에 넣어주는건 굉장히 번거롭다.

 

이런 번거로움을 해소하기 위해 스프링 시큐리티에서는 @WithMockUser 라는 애노테이션을 제공한다.

이를 이용하면 시큐리티 컨텍스트에 MockUser 정보를 넣어두기 때문에 인증 없이 컨트롤러를 호출해도 문제없이 사용할 수 있다.

하지만 서비스마다 유저 객체 정보가 다르기 때문에 기본적인 @WithMockUser 를 사용하기에는 한계가 있다.

따라서 이번 글에서는 @WithMockUser 를 커스텀해서 사용하여 서비스에 맞는 MockUser 정보를 이용해볼 것이다.


구현

CustomMockSecurityContext

MockSecurityContext를 만든다.

아래처럼 WithSecurityContextFactory를 구현하고, 시큐리티 컨텍스트에 MockUser 정보를 넣어준다.

@RequiredArgsConstructor
public class PawLandMockSecurityContext implements WithSecurityContextFactory<PawLandMockUser> {

    private final UserRepository userRepository;

    @Override
    public SecurityContext createSecurityContext(PawLandMockUser annotation) {
        User user = User.builder()
            .email(annotation.email())
            .nickname(annotation.nickname())
            .password(annotation.password())
            .type(annotation.type())
            .profileImage(annotation.profileImage())
            .introduce(annotation.getIntroduce())
            .build();
        userRepository.save(user);

        UserPrincipal userPrincipal = new UserPrincipal(user);
        SimpleGrantedAuthority role = new SimpleGrantedAuthority(annotation.role());
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userPrincipal,
            user.getPassword(),
            List.of(role));

        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authenticationToken);
        return context;
    }
}

CustomMockUser

이제 위에서 만든 Mock 시큐리티 컨텍스트를 사용할 커스텀 MockUser 애노테이션을 만든다.

아래처럼 원하는 필드를 설정하고, 기본 값을 정해주면 애노테이션을 붙이면 설정한 MockUser 정보를 이용한다.

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = PawLandMockSecurityContext.class)
public @interface PawLandMockUser {

    String email() default "midcondria@naver.com";

    String nickname() default "나는짱";

    String password() default "asd123123";

    String profileImage() default "";

    String getIntroduce() default "";

    LoginType type() default LoginType.NORMAL;

    String role() default "ROLE_USER";

}

사용법

1. 애노테이션만 붙일 때

아래처럼 애노테이션만 붙이면 기본 값으로 설정한 정보를 사용한다.

2. 애노테이션에 원하는 값을 넣을 수 있음

아래처럼 애노테이션에 원하는 값을 넣으면 원하는 설정한 정보를 사용할 수 있다.

+ Recent posts