이번 글은 회원 로그인 API를 RESTful하게 작성하는 과정을 기록한 글입니다.
코드는 디자인 가이드부터 시작됩니다.
REST API (Representational State Transfer API)란?
REST를 준수해 만든 API
REST API를 통해 (1) 클라이언트와 서버 간 결합도를 낮출 수 있고 (2) 자원 관리가 쉬운 장점을 가짐
- REST란? 웹의 장점을 최대한 활용할 수 있는 네트워크 아키텍처로써
- REST를 준수한 API는 아래와 같은 구성으로 이루어져있음
- REST 구성
(1) 자원 (Resource) - URI
(2) 행위 (Verb) - HTTP Method
(3) 표현 (Representations)
즉 네트워크 자원을 표현하는데에 집중하고, 처리하는 방법에 대한 정의를 HTTP Method로 하는 것이
REST 한 API를 설계하는 방법
REST API 등장 배경
WWW (World Wide Web)의 등장으로 인해 수많은 기기 간 데이터 통신이 많아짐
이로인해 기존 WWW의 문제점이 대두됨
1. 클라이언트와 서버 간 결합도가 높다
아래와 같이 데이터 통신 주체들이 각기 다른 프로토콜을 사용한다면
통신 대상이 누구냐에 따라서 통신 방법이 달라져야함
이는 서버와 클라이언트간의 결합도가 높음을 의미

2. 자원의 표현 및 상태 관리의 어려움
회원 정보 생성 기능에 대한 URI를 표현하는 표준이 부재해서
각기 다른 방식으로 표현함


정리하면 웹은 단순성, 일반성, 관김사의 분리라는 견고한 원칙에 기반헀음에도....
1. 클라이언트와 서버 간 결합도가 높음
2. 자원 관리가 어려움
위의 2가지 단점이 있어 이를 극복하려고 개념이 REST API가 등장
REST란 웹을 위한 제약조건의 집합!

REST의 디자인 가이드 - 제약조건 6가지 준수
이제부터 REST API를 설계 하기 위해 6가지 제약 조건을 준수해보겠습니다.
일단 6가지 조건 중 5가지 조건은 HTTP를 사용한다면 쉽게 준수 할 수 있기 때문에 넘어가겠습니다.
(1~5가지 조건에 대한 자세한 설명은 접은글 참고)
1. 클라이언트 - 서버 구조
클라이언트는 요청을 발생시키고, 서버는 요청에 대해 반응함
서버는 데이터만 전달하면 끝! 클라이언트는 데이터를 받아 원하는 UI로 구성하면 끝!


2. 무상태성
요청은 상태를 가지지 않는다는 제약조건
즉, 각각의 요청은 독립적이고 필요한 모든 정보를 제공해야 함
쉽게말해, 서버는 클라이언트가 이전에 무슨 요청을 보냈는지 모름

3. 캐시가능성
서버는 자원이 캐시 가능한지 명시해야 한다는 제약조건

4. 계층형 시스템
계층형 시스템을 적용해야 한다는 제약 조건
(Ex: MVC 계층)

5. 주문형 코드 (Optional)
클라이언트가 필요에 의해 기능을 확장할 수 있도록 해야한다는 제약조건 (Ex: 플러그인)

회원 로그인 API Java 코드
API는 MVC 계층으로 이루어져 있고, 그중에서 RESTful 하게 작성한 부분을 설명하기 위해 특정 부분만 가져왔다.
API 명세서는 다음 그림과 같다.
//기존 코드 : 회원 로그인 API
// (1) Controller
@PostMapping("/api/user/login")
public ResponseEntity<MemberLoginResponse> login(
@RequestBody final MemberLoginRequest memberLoginRequest,
final HttpServletResponse response) {
MemberLoginResponse loginResult = memberService.login(memberLoginRequest, response);
return ResponseEntity.status(HttpStatus.OK).body(loginResult);
}
// (2) Service
@Transactional
public MemberLoginResponse login(final MemberLoginRequest memberLoginRequest, final HttpServletResponse response) {
String email = memberLoginRequest.getEmail();
String password = memberLoginRequest.getPassword();
Member searchedMember = memberRepository.findByEmail(email).orElseThrow(
() -> new RestApiException(MemberErrorCode.MEMBER_NOT_FOUND));
if(!passwordEncoder.matches(password, searchedMember.getPassword())){
throw new RestApiException(MemberErrorCode.INVALID_PASSWORD);
}
TokenDto tokenDto = jwtUtil.createAllToken(searchedMember.getEmail(), searchedMember.getRole());
Optional<RefreshToken> refreshToken = refreshTokenRepository.findByEmail(email);
if(refreshToken.isPresent()) {
RefreshToken updateToken = refreshToken.get().updateToken(tokenDto.getRefreshToken().substring(7));
refreshTokenRepository.save(updateToken);
} else {
RefreshToken newToken = new RefreshToken(tokenDto.getRefreshToken().substring(7), memberLoginRequest.getEmail());
refreshTokenRepository.save(newToken);
}
response.addHeader(jwtUtil.ACCESS_KEY, tokenDto.getAccessToken());
response.addHeader(jwtUtil.REFRESH_KEY, tokenDto.getRefreshToken());
Optional<Profile> optionalProfile = profileRepository.findByMemberId(searchedMember.getId());
MemberLoginResponse loginResult;
if (searchedMember.getRole() == MemberRoleEnum.ADMIN) {
loginResult = new MemberLoginResponse(searchedMember.getEmail(), searchedMember.getNickname(), optionalProfile.get().getImg(), "ADMIN");
} else {
loginResult = new MemberLoginResponse(searchedMember.getEmail(), searchedMember.getNickname(), optionalProfile.get().getImg(), "USER");
}
return loginResult;
}
// (3) Custom 예외처리
@Getter
@RequiredArgsConstructor
public class RestApiException extends RuntimeException {
private final ErrorCode errorCode;
}
// (4)
@Getter
@RequiredArgsConstructor
public enum MemberErrorCode implements ErrorCode {
INACTIVE_MEMBER(HttpStatus.FORBIDDEN, "본인만 수정/삭제할 수 있습니다."),
DUPLICATED_MEMBER(HttpStatus.BAD_REQUEST, "이미 존재하는 nickname 입니다."),
DUPLICATED_EMAIL(HttpStatus.BAD_REQUEST,"이미 존재하는 email 입니다."),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."),
INVALID_PASSWORD(HttpStatus.NOT_FOUND, "비밀번호가 일치하지 않습니다."),
PASSWORD_ERROR1(HttpStatus.NOT_FOUND, "비밀번호는 4자 이상 8자 이하여야 합니다."),
EMAIL_ERROR1(HttpStatus.NOT_FOUND, "이메일 형식이 올바르지 않습니다."),
ADMIN_ERROR(HttpStatus.NOT_FOUND, "관리자 암호가 틀려 등록이 불가능합니다."),
STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "약국을 찾을 수 없습니다."),
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다."),
;
private final HttpStatus httpStatus;
private final String message;
}
6. 균일한 인터페이스 (가장 중요 But 지키기 어려움)
API 설계 시, 클라이언트와 맞닿아 있는 부분 (Http Request, Http Response 등) 을 쉽고 일반적으로 설계하라는 제약조건
그리고... 균일한 인터페이스는 또 한번 하위의 4가지 제약조건으로 분류됨
(1) 자원의 식별
URI로 자원(리소스)을 표현 할 때는, 고유하게 자원을 식별 할 수 있게 작성해야 한다.
또한 동사보다는 명사를 사용해야한다.
그래서 URI에 "POST /api/user/login" 으로 작성하여 user의 login에 필요한 자원을 서버로 보낼 수 있게 표현하였다.
//기존 코드
// (1) Controller
@PostMapping("/api/user/login")
public ResponseEntity<MemberLoginResponse> login(
@RequestBody final MemberLoginRequest memberLoginRequest,
final HttpServletResponse response) {
MemberLoginResponse loginResult = memberService.login(memberLoginRequest, response);
return ResponseEntity.status(HttpStatus.OK).body(loginResult);
}
(2) 표현을 통한 자원의 조작
표현이란? 메타데이터 (Ex: Content-Type : application/json) + 데이터이면서, 자원의 특정 상태를 의미함
이러한 표현을 통해서 자원을 조작해야 하다.
특히 HTTP POST, PUT 메소드 처럼 Body에 data를 보낼 때, Content-Type이 필요하다.
FE의 axios를 사용해 클라이언트가 서버에서 API 요청 시, Content-Type을 application/json으로 지정한다.
그리고 BE에서는 아래와 같이 Controller 메소드의 매개변수에 MemberLoginRequest Dto를 설정해준다.
//기존 코드
// (1) Controller
@PostMapping("/api/user/login")
public ResponseEntity<MemberLoginResponse> login(
@RequestBody final MemberLoginRequest memberLoginRequest,
final HttpServletResponse response) {
MemberLoginResponse loginResult = memberService.login(memberLoginRequest, response);
return ResponseEntity.status(HttpStatus.OK).body(loginResult);
}
//HTTP Request Body
@Getter
public class MemberLoginRequest {
private String email;
private String password;
}
(3) 자기서술적인 메세지
자원의 표현은 메세지를 처리하기에 충분한 정보를 제공해야 한다.
아래의 API 명세 예시를 봤을 때, 충분하게 정보를 제공하지 못하고 있다.
자기서술적인 메세지의 대표적인 예시는 HTML이다.
아래 메세지에서 <a> 태그를 모른다고 해도, html 명세를 찾아가 알아볼 수 있다.
이렇게 JSON 명세를 정의하고 등록해서 메세지 헤더에 적용하여 자기서술적인 메세지를 만들 수 있다.
(4) HATEOAS
HATEOAS란 Hypermedia As The Engine Of Application State의 약자
클라이언트는 서버와 상호작용하면서 하이퍼링크를 통해 동적으로 모든 다른 리소스에 접근할 수 있어야 한다는 제약조건
더 자세하게 설명을 하자면 HTML의 경우 a 태그를 통해 다른 리소스에 접근이 가능하다.
즉 클라이언트가 어플리케이션의 상태를 동적으로 변경할 수 있으므로 HATEOAS 하다는 것이다.
그러면 JSON의 경우 클라이언트는 다른 상태로 변경할 수 없다.
HATEOAS를 만족하게 하려면
아래와 같이 Location 헤더를 이용해 생성된 자원의 위치를 넣어준다면 특정 URI를 통해 클라이언트의 상태를 동적으로 변경할 수 있으므로 HATEOAS하다 할 수 있다.
두번째로 헤더가 아닌 JSON 메세지 안에 링크를 넘겨주는 방식도 존재한다.
추가적으로 아래 코드처럼 예외처리와 Costom 상태메세지를 통해 클라이언트의 상태를 관리하여 에러를 헨들링하여 API를 더 RESTful하게 할 수 있다.
// (3) Custom 예외처리
@Getter
@RequiredArgsConstructor
public class RestApiException extends RuntimeException {
private final ErrorCode errorCode;
}
// (4) Costom 상태메세지
@Getter
@RequiredArgsConstructor
public enum MemberErrorCode implements ErrorCode {
INACTIVE_MEMBER(HttpStatus.FORBIDDEN, "본인만 수정/삭제할 수 있습니다."),
DUPLICATED_MEMBER(HttpStatus.BAD_REQUEST, "이미 존재하는 nickname 입니다."),
DUPLICATED_EMAIL(HttpStatus.BAD_REQUEST,"이미 존재하는 email 입니다."),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."),
INVALID_PASSWORD(HttpStatus.NOT_FOUND, "비밀번호가 일치하지 않습니다."),
PASSWORD_ERROR1(HttpStatus.NOT_FOUND, "비밀번호는 4자 이상 8자 이하여야 합니다."),
EMAIL_ERROR1(HttpStatus.NOT_FOUND, "이메일 형식이 올바르지 않습니다."),
ADMIN_ERROR(HttpStatus.NOT_FOUND, "관리자 암호가 틀려 등록이 불가능합니다."),
STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "약국을 찾을 수 없습니다."),
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다."),
;
private final HttpStatus httpStatus;
private final String message;
}
그러면 항상 RESTful한게 좋을 걸까요?
실제로는 대부분의 RESTful API라고 불리는 것들은
완전히 REST를 따르지 않는 경우가 많음
엄연히 따지면 HTTP API라고 불러야함
REST의 핵심은 클라이언트와 서버 간의 결합도를 낮춰 독립적인 진화를 할 수 있게 하는 것
여기서 독립적인 진화란 서버와 클라이언트 간의 상호작용에서 변경의 영향을 최소화 하고 유연성을 제공하기 위한 노력을 의미
클라이언트 상태관리, 예외처리, 에러 핸들링
REST API 등
1. REST API 중심 규칙
참고
https://www.youtube.com/watch?v=Tm2mja5_dZs&t=25s
https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_5
Fielding Dissertation: CHAPTER 5: Representational State Transfer (REST)
proxy CERN Proxy, Netscape Proxy, Gauntlet
www.ics.uci.edu
https://meetup.nhncloud.com/posts/92
'웹기술' 카테고리의 다른 글
직렬화와 데이터 형식 (XML, JSON, YAML) (0) | 2023.03.12 |
---|---|
CORS (0) | 2023.03.07 |
API vs REST API vs HTTP API ?! (0) | 2023.03.05 |
브라우저에 URL을 입력하면 어떤 과정이 진행될까? (0) | 2023.03.04 |