본문 바로가기
Side Projects/프로젝트 사이

8. 백엔드 개발 완료(OpenAPI 소개 및 문제해결 회고록)

by devraphy 2022. 9. 6.

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

 

GitHub - devraphy/sai-back: Repository for Project SAI Back-end Development

Repository for Project SAI Back-end Development. Contribute to devraphy/sai-back development by creating an account on GitHub.

github.com

 

댓글