Upload
sungjoon-yoon
View
2.253
Download
4
Embed Size (px)
Citation preview
RESTful API (including Mobile)
with Spring 3.1
윤성준 (@exnis)
소 개
‘ 이음’ 이라는 스타트업에서 일하는 ,올해로 4 년차인 자바 개발자
• RESTful 의 R 도 모르고 API 의 A 도 모르던 한 개발자가 RESTful API 를 만들기 위해 고민하고 삽질한 과정 공유함으로써 ,
• RESTful 이 무엇인지 , 이를 스프링에서 어떻게 구현할 수 있는지 간략하게나마알아볼 수 있는 시간이 되었으면 ..
들어가기 앞서
목 차
• RESTful API with Spring 3.1
• API Exception Handling
• API Security
• API Test
REST?
REST(Representational State Transfer)
REST(Representational State Transfer)
• 표현 (Representational) - REST 리소스는 XML, JSON, 심지어 HTML 을 포함하여 리소스 사용자에게 가장 적합한 , 사실상 거의 모든 형식으로 표현할 수 있다
• 상태 (State) - REST 와 작업할 경우 리소스에 대해 취할 수 있는 액션보다 리소스의 상태에 대해 더 많은 관심을 둔다
• 전달 (Transfer) - REST 는 한 애플리케이션에서 다른 애플리케이션으로 어떤 표현 형식으로 리소스 데이터 전달을 포함한다
- 스프링 인 액션 제 3 판 中 -
REST(Representational State Transfer)
표현 (Representational) - REST 리소스는 XML, JSON, 심지어 HTML 을 포함하여 리소스 사용자에게 가장 적합한 , 사실상 거의 모든 형식으로 표현할 수 있다
REST(Representational State Transfer)
상태 (State) - REST 와 작업할 경우 리소스에 대해 취할 수 있는 액션보다 리소스의 상태에 대해 더 많은 관심을 둔다
REST(Representational State Transfer)
전달 (Transfer) - REST 는 한 애플리케이션에서 다른 애플리케이션으로 어떤 표현 형식으로 리소스 데이터 전달을 포함한다
REST(Representational State Transfer)
• 리소스 지향적이고 ,
• 애플리케이션을 표현하는 객체와 명사를 강조하며 ,
• 가장 적합한 형식이 무엇이든 간에 서버에서 클라이언트로 ( 또는 그 반대로 ) 리소스의 상태를 전달함
REST-ful?
REST is not a standard; it's a style.
http://www.xfront.com/REST-Web-Services.html
RESTfu
l!
API?
API!....OTL
RESTful API?
RESTless : https://api.dropbox.com/getAccountInfo?id=1RESTful : https://api.dropbox.com/1/account/info [GET]
RESTless : https://api.dropbox.com/deleteFile?id=1RESTful : https://api.dropbox.com/1/fileops/delete [POST]
RESTful API!
• 평범한 HTTP URL 을 통해 호출됨
• URL 이 계층적이라 , 왼쪽에서 오른쪽으로 읽다 보면 광범위한 개념에서 정확한 개념으로 이동함
• 쿼리 파라미터를 이용해 리소스를 식별하는 대신에 전체 기본 URL 이 리소스를 식별함
• URL 은 리소스로 무엇을 수행할 지가 아니라 리소스를 식별할 뿐 . 따라서 리소스를 식별하는 URL 은 GET 하거나 PUT 하거나 에 상관없이 모두 동일함
• 리소스로 무엇을 할지는 HTTP 메소드가 결정할 문제임
RESTful API!
RESTful API!
http://localhost:8080/articles [GET] : 글 목록을 가져옴
http://localhost:8080/articles/123 [GET] : id 가 123 인
글을 가져옴
http://localhost:8080/articles/123 [PUT] : id 가 123 인
글을 작성
http://localhost:8080/articles/123 [DELETE] : id 가 123
인 글을 삭제
“ 동일한 URL 인 /articles/123 으로 요청을 처리함”
메소드 설명 안전 ?(safety)
멱등적 ?(idempo-tency)
GET 서버에서 리소스를 조회한다 . 리소스는 요청 URL 에 의해 식별된다 .
O O
POST 요청 URL 을 리스닝하는 프로세서에 의해 처리되도록 서버에 데이터를 전송한다 .
X X
PUT 요청 URL 에 있는 서버에 리소스를 둔다 . X O
DELETE 요청 URL 에 의해 식별되는 서버의 리소스를 삭제한다 . X O• 안전 (safe)? : 메소드가 리소스의 상태를 변경하지 않는 것• 멱등적 (idempotent)? : 반복되는 요청이 첫 번째 요청 이후에 발생할 수 있는
어떠한 부작용도 일으키지 않는다 . ( 상태를 변경할 수도 변경하지 않을 수도 있음 )
RESTful API!
“ 실제로는 GET, POST 만 사용”
PUT 을 사용하지 않은 이유
• 클라이언트가 URI 구조를 미리 알아야 함: 그러기 위해서는 id 를 클라이언트에 알려줘야 하고 , 쓸데없는 정보가 노출됨
http://localhost:8080/articles/123 [PUT]
http://localhost:8080/articles/write
[POST]
DELETE 을 사용하지 않은 이유
• 리소스를 삭제할 일이 없음
RESTful API! - Tip• 동사 대신 명사를 사용하도록 권장 : getDogs (x) dogs (O)
• 단수명사 보다는 복수명사 : /dog (X) /dogs (O)
• 추상적인 명사가 아닌 시나리오에 맞는 구체적인 명사 사용
• : /photos 와 같은 추상적인 명사가 아닌 /profilePhotos 와 같이 명확한 목적을 알 수 있는 명사를 사용
• /resource/identifier/resource : /owners/5678/dogs (5678 번 주인의 dogs): Identifier 는 변경되지 않는 값
• /dogs ( 전체 dog), /dogs/1 (1 번 dog)
• 출력 결과 형식을 지정 : /dogs ( 기본은 json 이며 /dogs.json 과 동일함 ), /dogs.xml 은 xml 형식으로 출력
• 특정 범위의 값을 가져 올 때는 파라미터 사용 : /dogs?limit=25&offset=50
• 결과를 받기 원하는 항목 선택 : /dogs?fields=name,color,location
RESTful API with Spring
3.1?
스프링이 REST 를 지원하는 방법
• 컨트롤러는 REST 의 네 가지 주요 메소드인 GET, PUT, DELETE, POST 를 포함하여 모든 HTTP 메소드에 대한 요청을 처리할 수 있음
• @PathVariable 에너테이션은 컨트롤러가 파라미터화된 URL( 경로의 일부분에 변수 입력이 있는 URL) 에 대한 요청을 처리할 수 있도록 함
• 리소스는 XML, JSON, Atom 그리고 RSS 같은 데이털 모델 랜더링을 위한 새로운 뷰 구현을 포함하여 스프링의 뷰와 뷰 리졸버를 이용해 클라이언트에 가장 적합한 형태로 리소스의 뒤에서 데이터를 표현할 수 있음
• 뷰 기반의 응답의 경우 , ContentNegotiatingViewResolver 는 클라이언트가 원하는 컨텐츠 타입을 만족시키는 몇 가지 뷰 리졸버에서 생성한 최적의 뷰를 선택할 수 있음
• 컨트롤러 핸들러 메소드에 @ResponseBody 애너테이션을 적용하여 뷰 처리를 완전히 무시하고 , 몇 가지 메시지 변환기 중 하나로 변환된 값을 클라이언트에 대한 응답으로 변환
• 마찬가지로 새로운 @RequestBody 애너테이션은 HttpMethodConverter 구현체와 함께 인바운드 HTTP 데이터를 컨트롤러의 핸들러 메소드에 전달하는 자바 객체로 변환할 수 있음
- 스프링 인 액션 제 3 판 中 -
@Controller
@RequestMapping
@PathVariable
@RequestParam
@ResponseBody
사실 , 스프링 MVC 에서 다 쓰던 것들 ..
@RequestMapping
@PathVariable
@RequestMapping(value = "/files", method = Request-Method.GET)@ResponseBodypublic void getFile(@RequestParam(“id") Integer id) {
File file = fileService.getFile(id);}
@RequestMapping(value = "/files/{id}", method = Re-questMethod.GET)@ResponseBodypublic void getFile(@PathVariable(“id") Integer id) {
File file = fileService.getFile(id);}
http://localhost:8080/files?id=1
http://localhost:8080/files/1
“ 예를 하나 들어보겠습니다”
아이디 ( 이메일 ) 과 비밀번호를 입력하고로그인 버튼을 누르면 ,
http://api.i-um.com/authentication/loginemail : [email protected] : ########## “POST 방식으로 호출”
{ "status":"SUCCESS", "result": {"accessToken": “28as9dyhd923!3e2" }, "error": "NULL"
}
“JSON 으로 리턴”
@RequestMapping(value = "/login", method = Request-Method.POST,produces = "application/json")@ResponseBodypublic ApiResult login(
@RequestParam("email") String email, @RequestParam("password") String password
) { … }
@RequestMapping("/authentica-tion")@Controllerpublic class Authentication-ApiController {
}
http://api.i-um.com/authentication/loginemail : [email protected] : ##########mobileType : IPHONE / ANDROID
“POST 방식으로 호출”
예 ) 로그인 - Annotation 사용
@RequestMapping(value = "/login", method = Request-Method.POST, produces = "application/json")@ResponseBodypublic ApiResult login(
@RequestParam("email") String email, @RequestParam("password") String password
) { … }
@RequestMapping("/api/authen-tication")@Controllerpublic class Authentication-ApiController {
}
http://api.i-um.com/authentication/loginemail : [email protected] : ##########mobileType : IPHONE / ANDROID
“POST 방식으로 호출”
예 ) 로그인 - Annotation 사용
@RequestMapping(value = "/login", method = Request-Method.POST, produces = "application/json")@ResponseBodypublic ApiResult login(
@RequestParam("email") String email, @RequestParam("password") String password
) { … }
@RequestMapping("/api/authen-tication")@Controllerpublic class Authentication-ApiController {
}
{ "status": "SUCCESS", "result": { "accessToken": “28as9dyhd923!3e2" }, "error": "NULL"
}
“JSON 으로 리턴”
예 ) 로그인 - Annotation 사용
“JSON 으로 결과값을 리턴하기 위해서는 ?”
1.MappingJacksonHttpMessageConverter 사용
2. MappingJacksonJsonView (JSON 지원 View 이용 ) & ContentNegotiatingViewResolver 사용 (JSON 외 XML 등 다른 format 의 view 제공하고 싶을 때 )
예 ) 로그인 – JSON 으로 리턴
예 ) 로그인 – JSON 으로 리턴
Controller가
리턴하는 오브젝트
클라이언트가 원하는리소스
HttpMes-sageCon-verter
• MappingJacksonHttpMessageConverter : JSON
• MarshallingHttpMessageConverter : XML
• RssChannelHttpMessageConverter : RSS
.
.
.
“MappingJacksonHttpMessageConverter?”
1.Servlet-context.xml 에 bean 선언 추가
<bean class="org.springframework.web.servlet.mvc.annotation.RequestMappingHandlerAdapter"> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"> <property name="supportedMediaTypes"> <value>application/json;charset=UTF-8</beans:value> </property> </bean> </list> </property></bean>
2. JSON 으로 return 하고자 하는 Controller 에 @ResponseBody 를 붙여줌
ResponseBody 를 통해 반환되는 값에 한글이 있을 경우 깨지는 현상을 방지하기 위해 넣어줌
“MappingJacksonHttpMessageConverter 를 사용하려면 ?”
@RequestMapping(value = "/login", method = RequestMethod.-POST)@ResponseBodypublic ApiResult login(
예 ) 로그인 – JSON 으로 리턴
로그인public class ApiResult {
private SuccessFail status;: SuccessFail 은 SUCCESS / FAIL 의 enum type
private Object result;: API 를 호출한 쪽에 전달되어야 할 값 ( 값이 없으면 NULL)
private ExceptionReason error;: error 가 발생하면 error 에 대한 정보 (code) 를 넣어줌
private String msg; (error 가 NULL 이 아닌 경우에만 보이도록 함 )
: error 가 발생했을 때 클라이언트가 뿌려줘야 할 메시지
…} {
"status":"SUCCESS", "result":{"accessToken":"28as9dyhd923!3e2"}, "error":"NULL"
}
“JSON 으로 변환”
“ 다른 예를 들어보겠습니다”
예 ) 배지 – RequestMapping 전략
@RequestMapping(value = "/badges", method = RequestMethod.GET)@ResponseBodypublic ApiResult getBadges(…) { … }
1) 배지 목록 가져오기
@RequestMapping(value = "/badges/{badgeId}", method = RequestMethod-.GET)@ResponseBodypublic ApiResult getBadge(@PathVariable Long badgeId) { … }
예 ) 배지 – RequestMapping 전략
2) badgeId 에 해당하는 배지 가져오기
@RequestMapping(value = "/badges/{badgeId}/photos", method = Request-Method.GET)@ResponseBodypublic ApiResult getBadgePhotos(@PathVariable Long badgeId) { … }
예 ) 배지 – RequestMapping 전략
3) badgeId 에 해당하는 배지의 사진들 가져오기
배지
@RequestMapping(value = "/badges/{badgeId}/photos/{slotNum}", method = RequestMethod.GET)@ResponseBodypublic ApiResult getBadgePhoto(@PathVariable Long badgeId, @PathVariable(“slotNum”) Integer slotNum) { … }
4) badgeId 에 해당하는 배지의 {slotNum} 번째 사진 가져오기
배지
@RequestMapping(value = "/badges/{badgeId}/photos/{slotNum}/upload", method = RequestMethod.POST)public ApiResult uploadBadgePhoto(@PathVariable Long badgeId,@PathVariable(“photoOrder”) Integer photoOrder) { … }
5) badgeId 에 해당하는 배지의 {slotNum} 번째 사진 업로드하기
API ExceptionHandling
예외처리
“Service 에서 Business Logic 이 실행되다가 Exception 이 발생하면 Controller 에서 catch 해서 에러코드와 메시지를 리턴해줌”
{ "status":"FAIL", "result":"NULL", "error":"INVALID_EMAIL_PASSWORD" "message":" 이메일 또는 비밀번호가 틀렸습니다 "
}
Exception
ex-tends
“Exception 을 상속받아 별도의 Exception Class 를 만듦”
A Exception private AExceptionReason exceptionReason;
public enum AExceptionReason {이유 1, 이유 2, 이유 3, 이유 4, …
};
: 예외가 발생한 이유를 enum 형으로 정의함
예외처리
AException (class)AExceptionReason (enum)
CException (class)CExceptionReason (enum)
“ 도메인 or 서비스마다 Exception(class) 과 Exception Reason(enum) 이 생김”
BException (class)BExceptionReason (enum)
DException (class)DExceptionReason (enum)
EException (class)EExceptionReason (enum)
Fexception (class)FExceptionReason (enum)
GException (class)GExceptionReason (enum)
IException (class)IExceptionReason (enum)
HException (class)HExceptionReason (enum)
JException (class)JExceptionReason (enum)
.
.
.
예외처리
try {…} catch (Exception e) { ApiError apiError = null; if (e instanceof AException) { apiResult.setError((((AException)e).getAExceptionReason()); } else if(e instanceof BException) {
apiResult.setError((((BException)e).getBExceptionReason()); } else if(e instanceof CException) {
apiResult.setError((((CException)e).getCExceptionReason()); } else { apiResult.setError(UNKNOWN_EXCEPTION); }
apiResult.setMessage(e.getMessage());}
return apiResult;
코드의 중복이 발생 !
Exception 개수만큼 조건문 늘어남 !!
예외처리
ExceptionReason(interface)
AExceptionReason(enum)
implements
BExceptionReason(enum)
CExceptionReason(enum)
…
Enum 은 extend 가 안되어서 ,interface 만든 후 implements 하도록 처리 !
예외처리 : 개선 후
FrontModuleExcep-tion (abstract
class)
Exception (class)
AException (class)
public abstract ExceptionReason getExceptionRea-son();
ex-tends
ex-tends
BException(class)
…
@Overridepublic ExceptionReason getException-Reason() { return aExceptionReason;}
@Overridepublic ExceptionReason getExceptionReason() { return bExceptionReason;}
private AExceptionReasonaExceptionReason;
private BExceptionReasonbExceptionReason;
예외처리 : 개선 후
try {…} catch (FrontModuleException e) { apiResult.setError(e.getExceptionReason()); apiResult.setMessage(e.getMessage());}
return apiResult;
Exception 개수가 많아도 하나로 처리 !
예외처리 : 개선 후
But 여전히 코드의 중복이 발생 !
예외처리 : 다른 개선방안
ExceptionResolver
ResponseEntity<?>ContentNegotiat-ingViewResolver
http://dev.anyframejava.org/docs/anyframe/plugin/springrest/1.0.2/reference/html/ch10.html
API Security
“3rd Party 나 개발자에게 공개된 API” (pub-
lic)
“ 클라이언트 (앱 ) 와 통신하는 API” ( 반만 public)
“ 회사 내부에서만 사용되는 API”(private)
API Security
아래에 해당할수록 어플리케이션 레벨의 보안에 신경써야 !
적당히
꼼꼼히
API Security
ID(Identity): 누가 API request 를 요청했는지 확인
인증 (Authentication): 주체의 신원을 주체가 주장하는 신원과 대비해 검증하는 과정 (A 가 정말로 A 가 맞는지 확인 )
허가 (Authorization): 인증된 사용자에게 권한들을 승인하는 과정 (A 가 어떤 액션을 하려고 할 때 , 그 액션을 하도록 허용되었는지 확인 )
API 에서 이 3 가지를 모두 요구하지는 않는다 !
ID(Identity) 만 요구 - Google Maps API: API key 만 알면 API 사용 가능
API Security
ID(Identity), 인증 (Authentication) 요구– Twit-ter API: username / password 를 입력해서 인증해야 함
API Security
ID(Identity), 인증 (Authentication), 허가(Authorization) 요구– Facebook API: email / password 입력한 후 , 특정 action 에 대한 허가를 요구함
API Security
“ 이렇게 했습니다”
API Security
• Oauth
• OpenID
• SAML
• HTTP authentication
• WS-Security
• Basic API Key
• 로그인 이후 모든 API 호출 시 , 액세스 토큰 (Access To-ken) 을 파라미터로 같이 넘겨 매번 인증 (Authentication)함
• 액세스 토큰은 DB 에 저장되어 있음 ( 세션에 저장하지 않음 )
• 스프링에서 제공하는 http basic 이나 remember-me au-thenticaiton 을 사용하지 않았음
• 액세스 토큰이 다시 생성되어 업데이트 및 , 클라이언트에게 리턴 되는 경우
1. 로그아웃 후 , 다시 로그인2. 다른 기기에 설치되어 있는 앱으로 로그인
API Security
{ "status": "SUCCESS", "result": { "accessToken": “28as9dyhd923!3e2" }, "error": "NULL"
}
“ 로그인 성공”
ApiResult abc(@RequestParam("accessToken") String accessToken, …) {
accessToken 을 authentication 하는 로직 }
ApiResult def(@RequestParam("accessToken") String accessToken, …) {
accessToken 을 authentication 하는 로직}
ApiResult xyz(@RequestParam("accessToken") String accessToken, …) {
accessToken 을 authentication 하는 로직} 코드의 중복이 발생 !
API Security
public class MobileAuthenticationInterceptor extends HandlerInterceptorAdapter {
@Override public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
accessToken 을 authentication 하는 로직 }}
AccessToken 을 인증하는 로직을 Interceptor 로 분리해서Controller 메소드가 실행되기 전 호출되도록 처리함
API Security
API Security
• 보안 인터셉터 엘리먼트를 통한 메서드 보호
• 포인트컷을 활용한 메서드 보호
• 애너테이션을 활용한 메소드 보호
장점 : 호출 보호하고자 하는 메서드 위에 애너테이션만 붙이면 된다 .단점 : 나중에 어디에 애너테이션을 적용했는지 잊어버려 검색해봐야 한다 .
“ 특정유저 (슈퍼유저 ) 만 호출가능한 메서드를 만들고 싶을 때”
- 스프링 3 레시피 中 -
메서드 호출 보호 (Spring Security)
API Security
@Secured("ADMIN_USER") <!-- 스프링에서 제공 -->@RolesAllowed(“ADMIN_USER”) <!-- JSR-250 -->@PreAuthorize(“hasRole(‘ADMIN_USER’)”) <!-- 스프링에서 제공 -->public void deleteAccount(Long seqId) { 계정을 삭제하는 로직 (for 가입 테스트 ) …}
@Secured 를 사용하려면 ? <global-method-security secured-annotations=“enabled”/>
@RolesAllowed 를 사용하려면 ? <global-method-security jsr250-annotations=“enabled” />
@PreAuthorize 를 사용하려면 ? <global-method-security pre-post-annotations=“enabled” />
• 클라이언트에 정보를 딱 필요한 만큼만 준다 .
• 민감한 정보는 절대 넘겨주지 않는다 .
( 예 : 유저의 seq_id, 주민번호 등 )
API Security - 잊지 말아야 할 것 !
API TEST
API(Analytical Profile Index) TEST?
출처 : http://www.biologyreference.com/Ar-Bi/Bacterial-Genetics.html
클라이언트 (앱 ) 개발자나 기획자가쉽고 편하게 API 를 테스트하게 하려면 ?
API TEST
REST Client 이용 (Firefox, Chrome 확장 플러그인 설치 )?
장점 : 설치 및 사용이 쉽다 . 단점 : 테스트해야 할 API 들이 많은 경우 , 매번 HTTP URL 을
입력하기 번거롭다 . 개발자에게만 친숙한 환경이다 .
“ 이렇게 했습니다”
API TEST
웹 테스트 페이지
http://swagger.wordnik.com/
API TEST
http://twitter.github.com/bootstrap/
Component scan 으로 모든 Controller 클래스를 스캔한 후(@Controller 에너테이션으로 스캔 가능 ), 클래스의 methods() 를 사용해 모든 method 를 가져올 수 있음
API TEST
• @PathVariable 로 들어오는 값과 @RequestParam 으로 들어오는 값을 따로 처리해야 함
• 메소드 추가 , 삭제가 불편함 ( 일괄적으로 처리하기 때문 )• 유연하게 카테고리를 나누기 힘듬
메소드가 생길 때마다 URL, 메소드 타입 , 파라미터는개발자가 직접 입력하자 !
API TEST
public enum ApiTestEnum {
// A. 로딩페이지IntroGate("A. 로딩페이지 ", "/intro", "GET", "gate"),
// B. 로그인Login("B. 로그인 ", "/authentication/login", "POST", "login"),FindPassword("B. 로그인 ", "/authentication/password/find", "POST", "findPassword"),
// C. 가입IsVaildEmail("C. 가입 ", "/authentication/email/check", "POST", "is-ValidEmail"),AuthorizeName("C. 가입 ", "/authentication/name", "POST", "autho-rizeName"),... private String apiCategory; private String apiUrl; private String methodType; private String name;}
카테고리 URL HTTP메소드
메소드명
ApiTestEnum ApiTestCon-troller
ApiTestMan-ager
ApiTestView(swagger, boot-
strap)
References
1. http://apigee.com/about/api-best-practices/all/ebook2. [Book] 웹 개발자를 위한 웹을 지탱하는 기술3. [Book] 스프링 인 액션 제 3 판4. [Book] 스프링 3 레시피5. [Book] 스프링 시큐리티 3
Thank you for Listening!