빠른 봉사인원 모집 및 참여 가능한 캘린더⭐
* 일반 회원가입과 봉사 기관 회원가입을 따로 함
* 기관 회원 가입에서는 사업자 등록증 이미지를 첨부해야 회원 가입 가능
* 로그인 방법은 VONGOLE 서비스에서 제공하는 로그인과 카카오 소셜 로그인 2가지
* 봉사 기관만이 봉사 활동을 모집하는 게시글, 댓글 CRUD가 모두 가능
* 게시물을 작성할 때엔 제목, 내용, 이미지, 주소, 해시태그 등을 작성
* 일반 회원은 게시물 조회와 댓글 CRUD 가능
* 봉사 활동 날짜 별 검색
* 해시태그 별 검색
* 장소 별 검색
* 동적 쿼리 사용해서 여러 조건으로 검색
* 일반 회원은 봉사 활동을 모집하는 게시글에 봉사 활동 신청을 할 수 있음
* 프로필 조회 / 수정
* 봉사 지원한 게시물 목록 조회
* 신청 승인 여부 별로 조회
* 내가 작성한 댓글 목록 조회
* 프로필 조회 / 수정
* 내 게시물 목록 조회
* 봉사 활동 지원자 전체 목록 조회
* 게시물에 해당하는 지원자 목록 조회
* 게시물 / 댓글 좋아요 기능 및 좋아요 수 기능
- 전혜진
- 김성호
- 장석원
- 김경일
- 김성민
- 김원재
- 강진구
- 강창식
FK를 걸어주는 목적에 대한 본질적인 궁금증이 팀 내에서 나오게 되었고, ERD 설계를 할 때 FK를 끊어야 할지 걸어야 할지 토론을 하게 되었다. 보통 생명주기를 함께 하는 테이블들을 관계를 맺어주기 위해 FK를 걸어주곤 한다.
하지만 FK를 걸어 줌으로써 데이터 정합성을 강제하는 측면이 있다는 의견이 있었다. 프로젝트를 예로 들면 한 회원(Member)이 게시물(Board)를 작성한다고 할 때, 게시물 테이블에는 회원 PK값이 FK로 걸려있을 것이다. 이렇게 FK가 걸리게 되면 게시물 테이블을 조회할 때마다 FK로 걸려있는 회원이 존재하는지 정합성을 항상 확인해야만 하고, 회원을 삭제 시에도 이 회원이 작성한 게시글이 있을 경우, 회원만 삭제하면 데이터 정합성이 깨지기 때문에 해당 게시물을 먼저 삭제해 주어야만 한다.
FK를 걸게 될 경우 DB 차원에서 데이터 정합성을 보장해 주는 것이 장점이자 단점으로 작용할 수 있다. 참조 무결성 제약으로 데이터 정합성을 보장해 주는 것은 일관성있는 테이블 관리를 할 수 있고, 테이블 간의 관계를 명확하게 나타내 준다는 장점이 있는 반면, DB의 성능이 느려지고, 유지보수 등의 이슈로 테이블 구조를 변경하게 될 경우, 확장이 어려울 수 있다는 단점이 있다.
하지만 아직까지 우리 팀은 FK를 걸었을 때의 성능 이슈 등을 체감하지 못하는 상황이고, 경우에 따라 슬랙을 예로 들면, 게시물을 삭제하더라도 댓글 데이터는 그대로 남아있게 하는 정책이 존재하지만 우리 프로젝트에서는 게시물과 댓글의 생명주기를 함께 하기로 결정했기 때문에 FK를 걸어두는 것으로 결론이 나게 되었다.
현재 구현하고 있는 스프링 기반 웹 프로젝트에서 사용자 인증 방식으로 Spring Security + JWT 인증 방식을 사용하고 있다. 처음에는 JwtFilter에서 예외를 던져주면 GlobalExceptionHandler에서 처리하면 되지 않을까라고 생각했지만 Filter는 아직 애플리케이션에 들어가지 못했다는 것을 깨달았다. Filter는 Dispatcher Servlet 보다 앞단에 존재하며 Handler Intercepter는 뒷단에 존재하기 때문에 Filter 에서 보낸 예외는 Exception Handler로 처리하지 못한다. jwt 토큰 만료에 대한 예외처리를 하기 위해 현재 필터보다 앞단에 예외 처리를 위한 필터를 하나 더 두고 FilterChain으로 원래의 JWT 유효성 검사를 하던 필터로 요청을 넘겨주는 방법이 있었다. 필터 구성을 이런식으로 해두면 다음 차례 필터의 로직 수행 중 던져진 예외가 앞서 거쳤던 필터로 넘어가서 처리가 가능하게 된다. 즉, 원래는
- 요청 ➡️ JwtAuthenticationFilter
의 형태였다면,
- 요청 ➡️ JwtExceptionFilter ➡️ JwtAuthenticationFilter
로 필터를 구성해서 JwtAuthenticationFilter에서 던진 예외를 JwtExceptionFilter가 처리할 수 있도록 했다.
XSS란 Cross-Site scripting의 약자로, 악의적인 사용자가 공격하려는 사이트에 스크립트를 넣는 기법을 뜻한다. 공격에 성공하면 사이트에 접속한 사용자는 삽입된 코드를 실행하게 되며, 보통 의도치 않은 행동을 수행시키거나 쿠키나 세션 토큰 등의 민감한 정보를 탈취한다.
XSS를 방어하는 방법으로는
- 서버 내에 Filter를 설정해서 XSS 공격 방어
- 스프링 시큐리티가 제공하는 XSS 방어 설정
방어 설정을 하기 이전에 의도적으로 XSS 공격을 하여 어떤 현상이 발생하는지 확인해 보았다. 포스트맨으로 게시물을 작성할 때 내용 인풋값에 <script>alert('XSS 공격')</alert>
을 작성하고 게시물 상세조회를 해 보았다. 정상적으로 XSS 공격이 먹히면 'XSS 공격'문구의 alert 창이 떠야하는데 문자열 그대로 출력이 되고 있었다. XSS 방어 설정을 해주지도 않았는데 XSS 공격이 먹히지 않은 것이다.
XSS 공격이 먹히지 않은 이유에 대해 찾아보니 아래와 같이 헤더에 이미 XSS 방어 설정이 되어있었다.
- X-XSS-Protection : 1; mode=block
이미 방어 설정이 되어있었던 이유는 현재 대부분의 브라우저에서는 X-XSS-Protection이 자동으로 설정되어 브라우저 자체에서 스크립트를 필터링 해준다고 한다. 포스트맨은 크롬의 확장 프로그램이기 때문에 포스트맨으로 XSS 공격을 했을 때, 스크립트가 실행되지 않고 문자열로 출력되었던 것이다.
브라우저 상에서 X-XSS-Protection 설정이 자동적으로 되었던 것과는 별개로 더 확실히 하기 위해서 서버 내에서도 스프링 시큐리티가 제공하는 XSS 방어 설정을 해줌으로써 XSS 공격에 대한 방어를 견고히 헀다.
중간 발표 때 service 단 api들의 for 문으로 이루어진 로직들을 stream으로 바꿔 보라는 피드백을 받았다. 항해를 시작하기 이전부터 stream이 성능이 좋지 않다는 괴소문(?)을 들어왔던 터라 일부러 stream을 사용하지 않아왔었는데 이번 기회에 for-loop와 stream 성능 비교를 해보았다.
'해시태그별 게시물 목록 조회' api를 for-loop와 stream 각각의 로직을 System 클래스의 nanotime() 메서드를 이용하여 성능 비교를 진행하였다. (5회를 실시 한 후 평균을 내는 방식이고 시간은 ms 기준이다.)
횟수 | for-loop | stream |
---|---|---|
테스트1 | 664 | 1139 |
테스트2 | 909 | 806 |
테스트3 | 760 | 1012 |
테스트4 | 1052 | 1099 |
테스트5 | 853 | 833 |
평균 | 847 | 977 |
평균치로 계산해 보았을 때 for-loop가 stream보다 약 1.15배 성능이 좋다. 성능 차이가 생각보다 크게 나지는 않았다. 그렇다면 stream이 성능이 좋지 않다는 괴소문은 어디서부터 비롯된 것일까? 공부를 해보니 stream이 성능이 좋지 않다는 것은 틀린 말은 아니였다. Wrapper 타입의 컬렉션의 비교보다 원시 타입의 배열의 성능 측정에서 성능 차이가 훨씬 더 두드러진다고 한다.
그렇다면 왜 for문이 stream보다 빠를까?
-
for문은 단순 인덱스 기반이다.
stream을 이용하려면 stream 객체를 생성해야 하는데 객체 생성 과정에서 여러 작업들이 이루어지기 때문에 오버헤드가 발생하게 되지만, for문은 단순 인덱스 기반으로 도는 반복문이기 때문에 stream에 비해 빠르다.
-
for문은 컴파일러가 최적화를 시킨다.
for문의 경우 자바라는 언어의 탄생 이래로 함께 해온 문법이기 때문에 JVM에 최적화가 잘되어 있는 반면, stream은 자바8부터 나온 문법이기 때문에 최적화가 덜 되어있다.
이 이외에도 for-loop가 stream에 비해 stack trace가 간단하고 명확하기 때문에 디버깅에 이점이 있기에 for-loop 역시 확실한 장점을 지니고 있다.
그럼에도 불구하고 멘토님께서 stream을 사용해 보라는 피드백을 주신데에는 이유가 있을거라고 생각하고 stream을 사용해야만 하는 이유에 대해 찾아보았다.
-
가독성이 좋아진다.
오늘날의 하드웨어는 충분히 빠르기 때문에 유의미하지 않은 성능 차이를 고려하기 보다는 가독성 및 유지보수를 위해 stream을 사용하곤 한다. for 문의 경우 로직에 따라 indent depth가 깊어져 가독성이 떨어질 수 있는 반면, stream은 보다 간결하게 표현이 가능하다.
-
코드로 작성해야하는 로직을 stream에서 제공해주는 함수로 간단하게 해결 가능하다.
단순 forEach만을 사용하는 로직의 경우 for 문을 사용하는게 오히려 더 이득이 될 수 있지만, 로직이 복잡할 경우, filter, sorted, map 등의 stream에서 제공해주는 함수를 사용할 경우 코드를 더욱 간결하게 작성할 수 있다.
게시물에 해시태그와 같은 기능을 추가하면 어떨까 라는 아이디어가 프로젝트 진행 중에 나오게 되었다. 해시태그와 관련된 이슈 2가지에 대해 이야기해보려고 한다.
처음에는 인스타그램과 같이 해시태그를 커스텀해서 만드는 형태를 생각했었지만 이렇게 커스텀할 경우, 띄어쓰기, 오타, 다양한 단어 사용 등의 이유로 해시태그들이 통일성이 떨어질 것이 우려되었다. 그래서 결국 해시태그 여러개를 지정해 놓고 그 안에서 고르는 형태로 진행하게 되었다.
여러모로 이 방법이 좋다고 생각하는게 앞서 언급했듯이 통일성이 생기기 때문에 한 해시태그에 해당하는 게시글을 검색하기가 수월하고, db에도 지정된 해시태그들만 저장되어 복잡해지지 않을거라 생각했다.
- board 테이블 문자열 컬럼에 저장
- board 테이블 list 컬럼에 저장
- hashtag 테이블 새로 생성해서 board pk값과 함께 저장
1,2 번의 경우 테이블을 따로 만들지 않아도 돼서 데이터를 경제적으로 사용할 수 있다는 장점이 있지만 해시태그 데이터를 개별적으로 조회하기는 매우 어려운 구조이다. 즉, 테이블 정규화 측면에서 좋지 않은 방법이기 때문에 3번 방법을 선택했다.
아직 마무리하지 않은 작업을 스택에 잠시 저장할 수 있도록 하는 명령어이다. 이를 통해 아직 완료하지 않은 일을 commit하지 않고 나중에 다시 꺼내와 마무리할 수 있다.
프로젝트의 git convention으로 api 별로 브랜치를 따로 파서 작업을 한 후 머지하는 식으로 진행했었는데 브랜치를 워낙 많이 바꿔서 작업하다보니 실수가 잦아졌다. git stash는 이럴 때 유용한 명령어이다.
- A브랜치에서 작업하다가 급하게 B브랜치에서 작업해야 할때
- A브랜치에서 작업하다가 B브랜치에서 다른 작업을 작업 해야하는데 깜빡하고 A브랜치에서 작업하다가 나중에 깨달았을 때
git stash 관련된 여러 명령어들이 있지만 두 개만 소개하자면
- git stash - 수정된 작업물을 스택에 임시로 저장해주는 명령어
- git stash pop - 스택에 저장된 작업물을 불러오는 명령어. 다른 브랜치에서도 불러올 수 있다. 불러올 때 주의할 사항은 머지를 할 때처럼 충돌이 날 가능성이 있다.
VONGOLE에서는 회원에 따라 권한을 달리한다. 일반 회원은 게시물 조회만 가능하고 봉사 기관 회원은 게시물 CRUD 모두 가능하다. 이 두 회원의 권한을 달리 하기 위해 테이블 설계를 어떻게 해야할지 팀원들과 고민을 많이 했다.
테이블을 둘로 나누게 될 경우, 각 테이블로부터 불러온 멤버 객체를 어떻게 구분을 지어야 할지 잘 모르는 상황이였고, 테이블을 하나로 통합하기에는 일반 회원과 봉사 기관 멤버 회원 가입 시 받는 입력값이 달라서 이 이슈에 대한 해결책이 필요한 상황이였다.
팀원들과의 회의를 통해 회원가입은 일반 회원, 봉사 기관 회원을 둘로 나누어서 하되, 하나의 테이블로 회원을 관리하고, 각 회원에게서 받는 입력값들을 모두 테이블의 컬럼으로 두고 각 회원에게서 입력받지 않은 값들은 null값으로 처리하도록 했다.
이바울 멘토님의 멘토링을 통해 컬럼에 null값을 저장하기보다는 특정값으로 초기화시켜주는 것이 좋다고 하셔서 이에 대한 공부를 해보고 리펙터링해보려고 한다.
카카오 로그인 처리를 할때 유저에게 JWT토큰을 발급해줘야 하는데 우리 코드에서 토큰을 발급하는 메서드가 스프링 시큐리티 에서 제공해주는 메서드 같았는데 매개변수로 유저 아이디와 비밀번호를 받아서 토큰을 만들어 주는 메서드 였다.
근데 카카오 로그인 유저는 비밀번호가 없어서 uuid값으로 만들어서 넣어줬는데 이렇게 하니까 로그인할때 비밀번호를 통해서 토큰을 만들어 줘야 하는데 비밀번호를 알 수 없어서 토큰을 만들 수 없는 문제가 생겼다.
그래서 카카오유저에게 특정 문자열을 비밀번호로 지정해서 토큰을 만들때 그 문자열을 사용해서 토큰을 발급해 주는 방식으로 해결했다.
이미지를 업로드할 때, dto에 @ModelAttribute라는 어노테이션을 붙여서 요청을 들여온다. ModelAttribute의 경우에는 1대1 매핑을 해주는 RequestParam과는 달리 객체 매핑을 해주기 때문에 코드가 한결 깔끔해지고 순서가 바뀔 위험도 없다.
ModelAttribute를 사용할 때 유의할 점은 객체 매핑이 저절로 되는 것이 아니라 어떤 설정을 해줘야만 한다. @ModelAttribute 설정을 해준 dto를 객체로 바인딩해주기 위해서는 두가지 방법이 있다.
- @AllArgsConstructor를 이용해서 각 필드를 초기화한 객체를 생성한다.
- @NoargsConstructor과 @Setter로 각각의 필드 초기화
객체를 매핑한다는 개념 자체를 아예 모르고 있었고, dto에 기계적으로 @NoArgsConstructor와 @AllArgsConstructor를 붙여 놓았더니 객체 매핑이 되질 않아 NullPointerException 에러가 났고, @NoArgsConstructor 설정을 제거해 줌으로써 에러를 해결할 수 있었다.
코드 한 줄이라도 생각하고 작성해야 겠다는 다짐을 새삼 하게 되었던 이슈였다.
프로필 수정 시, Spring Security에서 제공하는 멤버 객체를 매개변수로 받아 요청받은 데이터로 수정했는데 DB에 적용이 되지 않는 이슈가 있었다.
JPA가 제공하는 멤버 객체가 아니라서 DB에 반영이 안되는 거라고 생각해서 JpaRepository로부터 불러온 멤버 객체를 업데이트하니까 DB에 잘 적용이 되었다.