0. 개요
- 포트폴리오를 개발하는데 매진하여 그동안 블로그를 신경 쓰지 않았다.
- 백엔드 개발이 어느 정도 완성되었다. 그래서 이번 포스팅에서는 그동안의 결과물(OpenAPI)을 소개하고
개발 과정에서 겪었던 문제점과 해결방법에 대해 소개해보려 한다.
1. 프로젝트 구조(Structure)
- 6월부터 SAI를 개발하고 있다.
- 프런트와 연동 작업을 진행하면서 메서드가 추가될 수는 있으나,
전체적인 구조적 측면에서는 변동이 없으므로 현재 시점에서 SAI의 백엔드 구조를 소개한다.
├─ projectSAI/sai-backend/src
├── SaiBackendApplication.java
├── api
│ ├── EventApiController.java
│ ├── FriendApiController.java
│ └── MemberApiController.java
├── domain
│ ├── Event.java
│ ├── Friend.java
│ ├── Member.java
│ ├── Record.java
│ └── enums
│ ├── EventEvaluation.java
│ ├── EventPurpose.java
│ ├── RelationStatus.java
│ └── RelationType.java
├── dto
│ ├── event
│ │ ├── requestDto
│ │ │ ├── AddEventRequest.java
│ │ │ ├── DeleteEventRequest.java
│ │ │ └── UpdateEventRequest.java
│ │ └── responseDto
│ │ ├── EventResultResponse.java
│ │ └── SearchEventResponse.java
│ ├── friend
│ │ ├── requestDto
│ │ │ ├── AddFriendRequest.java
│ │ │ ├── DeleteFriendRequest.java
│ │ │ └── UpdateFriendRequest.java
│ │ └── responseDto
│ │ ├── FindFriendResponse.java
│ │ └── FriendResultResponse.java
│ └── member
│ ├── requestDto
│ │ ├── DeleteMemberRequest.java
│ │ ├── EmailValidationRequest.java
│ │ ├── JoinMemberRequest.java
│ │ ├── LoginMemberRequest.java
│ │ └── UpdateMemberRequest.java
│ └── responseDto
│ ├── JoinMemberResponse.java
│ ├── LoginMemberResponse.java
│ ├── MemberResultResponse.java
│ └── SearchMemberResponse.java
├── repository
│ ├── EventRepository.java
│ ├── FriendRepository.java
│ ├── MemberRepository.java
│ └── RecordRepository.java
├── security
│ ├── config
│ │ ├── CorsConfig.java
│ │ ├── SecurityConfig.java
│ │ └── SwaggerConfig.java
│ ├── filter
│ │ ├── FilterResponse.java
│ │ └── JwtFilter.java
│ └── jwt
│ └── JwtProvider.java
└── service
├── EventService.java
├── FriendService.java
├── JwtCookieService.java
├── MemberService.java
└── RecordService.java
- SAI는 Member, Friend, Event의 3가지 Entity를 중점으로 동작하는 애플리케이션이다.
- Record는 ManyToMany 관계를 형성하는 Event와 Friend를 이어주는 JoinTable의 역할을 수행한다.
- 이에 따라 Member, Friend, Event를 중심으로 RestApiController를 형성한다.
2. OpenApi 완성
a) OpenApi 적용
- RestAPI의 문서화를 위하여 OpenAPI 라이브러리를 적용하였다.
- OpenAPI를 적용하면서 느낀 점은 어노테이션이 증가함에 따라 가시적으로 꽤나 코드가 지저분해진다는 것이다.
- 아래의 사진처럼 말이다.
- 아직 "Open" API는 아니지만, 아래의 사진과 같이 문서화를 완성하였다.
b) OpenAPI 접근 권한에 대한 고민
- 아래의 사진과 같이 SAI의 OpenAPI는 GET, POST, PUT, DELETE 메서드를 지원한다.
- 백 오피스가 없는 상황으로, 해당 OpenAPI에 대한 접근 권한을 설정해야 할지 고민이 많았다.
- 그러나 서버 자체적으로 RestAPI 호출은 로그인 회원만 요청할 수 있도록 Filter를 적용하였고,
각 회원은 본인이 가진 정보만 접근할 수 있도록 JPQL을 작성했기에 OpenAPI 접근 권한은 따로 설정하지 않았다.
- 당연하지만 접근을 제한한다면 "Open" API가 아니라고 생각한다.
- 관리자 만이 운용할 수 있는 백오피스 전용 RestAPI 서버를 만든다면, 그때는 접근 권한에 대한 고민을 필요로 하겠다.
3. 회고록 - 문제 해결 이야기
a) 시작에 앞서...
- 서버를 만들수록 기술에 대한 다양한 고민도 있었으나, 중점적으로 고민한 부분은 방법론에 대한 것이다.
- 특정 레이어의 비즈니스 로직에 대한 기준을 어떻게 설정할지, 특정 비즈니스 로직이 해당 위치에
존재하는 것이 맞는지 등 기술은 구현을 위한 방법이자 도구일 뿐 정책, 규칙, 방법론적인 설정이
핵심이라는 것을 다시 한번 깨닫는 과정이었다.
- 이번 RestAPI 서버를 개발하면서 마주했던 문제와 해결 방법에 대해 소개하려 한다.
b - 1) WebSecurityConfigurerAdapter가 Deprecated 되었다.
- SAI는 JWT를 이용한 사용자의 인증과 인가를 사용한다. 이를 위해 Security에 JWT 필터를 할당해야 했다.
- 이전의 Spring Security는 WebSecurityConfigurerAdapter를 extends 받아 메서드 재정의 방식을 사용했다.
- 현재는 FilterChain을 이용한 방식으로 HttpRequest/Response의 최전선에서 Filter를 이용한
보안 설정 방식을 권장한다.
- 이와 같은 이유로 WebSecurityConfigurerAdapter가 Deprecated 되었다.
- 새로운 FilterChain 방식에 대한 레퍼런스가 부족한 것도 사실이지만, Security에 대한 지식의 부재가 원인이었다.
(TMI - 공식 문서만 읽고 필요한 기술을 적용하는 방법에 대한 연습이 부족하다는 것을 깨달았다.)
- AuthenticationManager를 사용함에 있어서 난관을 겪은 것이 대표적인 문제다.
b - 2) UserDetailsService
- AuthenticationManager를 사용하기 위해서는 UserDetailsService를 설정해야 했다.
- Security는 Security가 제공하는 User객체를 통해 개발자가 작성한 회원 Entity의 인증과 인가를 수행한다.
- UserDetailsService는 Security의 User 객체를 이용한 회원 객체를 관리하는 다양한 기능을 제공하는데,
여기서 Security의 User를 사용하는 부분에서 문제에 봉착하였다.
- 첫 번째로 Spring Security는 특정 속성을 요구한다는 것이다.
- Security의 User 객체를 DTO처럼 이용하여 회원 객체(Member)를 인식 및 사용한다.
- 이를 위해서는 회원 객체(Member)에 반드시 Role이라는 속성이 존재해야 한다.
- 현재 시점에서 SAI 프로젝트의 회원(Member) 객체는 다양한 Role을 필요로 하지 않는다.
- 즉, Spring Security를 사용하기 위해서 Member Entity의 구조가 바뀌어야 했다.
- 두 번째로, Role 속성을 사용하기 위해서는 현재 SAI의 Member 객체의 관리 구조가 바뀌어야 했다.
- Role 속성은 List 타입을 가지므로 각 Member 객체의 Role을 관리하는 테이블을 추가해야 했다.
- 나는 이를 원치 않았다.
b - 3) 분명 다른 방법이 있을 것이다.
- Spring 확장성이 뛰어나기에 분명 다른 방법이 있을 거라 생각했다.
- 내가 선택한 차선책은 JWT 필터를 Security에 적용하는 것이 아니라 각 메서드에서 JWT를 검증하는 것이었다.
- 그렇게 나는 코드의 중복성을 최대치로 끌어올리는 방식을 선택했다.
- 목이 마르다고 바닷물을 마시는 것과 같은 이치였다.
- 즉, 근본적인 해결책이 아니었고 나는 이를 알고 있었다.
b - 4) 질문에서 찾은 해결책
- API의 모든 메서드에서 JWT를 검증하도록 했다.
- 그리고 회원가입, 로그인 메서드에서 JWT 검증과 동시에 토큰을 갱신하도록 설정하였다.
- 체계적인 로직을 구현했다고 생각했다. 그러다 문득 다음 질문이 떠올랐다.
"만약 토큰을 가진 기존 사용자가 특정 페이지를 즐겨찾기 해놓고 특정 페이지를 직접 요청하면
JWT 갱신은 언제 이루어지는가?"
- 여기에 대한 나의 대답은 모든 메서드에서 토큰을 갱신해야 한다는 것이었다.
- 이미 모든 메서드에 JWT 검증 과정을 중복 설정했는데, 이는 최악으로 가는 결말이었다.
- 그래서 Okky에 물어보기로 했다.
- 정말 운이 좋았다. 나의 우문에 현답을 해주신 분이 계셨으니 말이다.
b - 5) Filter는 Security의 종속 기능이 아니다.
- Security에서 FilterChain 방식을 사용하니, 당연히 Filter는 Security의 종속 기능이라고 생각했다. 참으로 안일했다.
- 더불어 잊고 있던 AOP와 Interceptor(처음 들어 보았다)라는 옵션까지 생겼다.(저런 분이 사수였으면 좋겠다...)
- 찾아보니 Filter는 Spring의 최전선에서 request/response를 맞이하는 역할을 한다.
Filter를 통과하면 Dispatcher Servlet이 반겨준다.
- Interceptor는 Dispatcher Servlet을 통과한 후 request/response를 맞이하는 역할을 한다.
Interceptor를 통과하면 Controller로 요청이 전달된다.
- AOP는 쉽게 설명하자면, 정의한 기능이 모든 메서드에서 실행된다.
- 이 3가지 선택지 중에서 나는 Filter를 사용하기로 했다.
- 이유는 간단했다. 잘못된 요청은 애초에 안받는게 서버 자원을 덜 갉아먹을 것이라 생각했기 때문이다.
- 그렇게 JWT 필터를 구현할 수 있었고, JWT가 없거나 만료된 경우에는 어떤 요청도 받지 않도록 설계했다.
- 아, 당연히 회원가입이나 로그인 시도는 접근할 수 있다.
4. 마치며...
- 공부한 지식을 적용하고 응용하는 것이 이렇게 즐거울 줄이야.
- 처음으로 만들어본 RestAPI 서버인데, 그래도 꼴을 갖춘 것 같아 참으로 스스로가 자랑스럽고 보람차다.
- 저의 코드 또는 앞으로의 개발 과정이 궁금하시다면, 아래의 깃헙 링크를 참고해주세요.
- 긴 글을 읽어주셔서 감사합니다.
https://github.com/devraphy/sai-back
'Side Projects > 프로젝트 사이' 카테고리의 다른 글
10. 배포 완료 (0) | 2022.09.14 |
---|---|
9. 개선사항(비즈니스 로직 제거, Exception Handler 적용, DataIntegrityViolationException 처리 방법) (0) | 2022.09.12 |
7. 오류 해결 스토리 - constraint ["PUBLIC.UK_MBMCQELTY0"] (0) | 2022.07.30 |
6. 오류 해결 스토리 - Update/delete queries cannot be typed (0) | 2022.07.30 |
5. 오류 해결 스토리 - Join Table 접근과 영속 상태 (0) | 2022.07.27 |
댓글