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, JSO N, 심지어 HTML 을포함하여리소스사용자에게가장적합한, 사실상거의모든형식으로표현할수있다
REST(Representational State Transfer) 상태 (State) - REST 와작업할경우리소스에대해취할수있는액션보다리소스의상태에대해더많은관심을둔다
REST(Representational State Transfer) 전달 (Transfer) - REST 는한애플리케이션에서다른애플리케이션으로어떤표현형식으로리소스데이터전달을포함한다
REST(Representational State Transfer) 리소스지향적이고, 애플리케이션을표현하는객체와명사를강조하며, 가장적합한형식이무엇이든간에서버에서클라이언트로 ( 또는그반대로 ) 리소스의상태를전달함
RESTful?
REST is not a standard; it's a style. http://www.xfront.com/rest-web-services.html
API?
API!...OTL
RESTful API?
RESTful API! RESTless : https://api.dropbox.com/getaccountinfo?id=1 RESTful : https://api.dropbox.com/1/account/info [GET] RESTless : https://api.dropbox.com/deletefile?id=1 RESTful : https://api.dropbox.com/1/fileops/delete [POST]
RESTful API! 평범한 HTTP URL 을통해호출됨 URL 이계층적이라, 왼쪽에서오른쪽으로읽다보면광범위한개념에서정확한개념으로이동함 쿼리파라미터를이용해리소스를식별하는대신에전체기본 UR L 이리소스를식별함 URL 은리소스로무엇을수행할지가아니라리소스를식별할뿐. 따라서리소스를식별하는 URL 은 GET 하거나 PUT 하거나에상관없이모두동일함 리소스로무엇을할지는 HTTP 메소드가결정할문제임
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 으로요청을처리함
RESTful API! 메소드 설명 안전? (safety) GET 서버에서리소스를조회한다. 리소스는요청 URL 에의해식별된다. O 멱등적? (idemp otency) O POST 요청 URL 을리스닝하는프로세서에의해처리되도록서버에데이터를전송한다. X X PUT 요청 URL 에있는서버에리소스를둔다. X O DELET E 요청 URL 에의해식별되는서버의리소스를삭제한다. X O 안전 (safe)? : 메소드가리소스의상태를변경하지않는것 멱등적 (idempotent)? : 반복되는요청이첫번째요청이후에발생할수있는어떠한부작용도일으키지않는다. ( 상태를변경할수도변경하지않을수도있음 )
실제로는 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 @RequestMapping(value = "/files", method = RequestMethod.GET) @ResponseBody public void getfile(@requestparam( id") Integer id) { File file = fileservice.getfile(id); } http://localhost:8080/files?id=1 @PathVariable @RequestMapping(value = "/files/{id}", method = RequestMethod.GET) @ResponseBody public void getfile(@pathvariable( id") Integer id) { File file = fileservice.getfile(id); } http://localhost:8080/files/1
예를하나 들어보겠습니다
아이디 ( 이메일 ) 과비밀번호를입력하고로그인버튼을누르면, http://api.i-um.com/authentication/login email : exnis@naver.com password : ########## POST 방식으로호출 { } JSON으로리턴 "status":"success", "result": {"accesstoken": 28as9dyhd923!3e2" }, "error": "NULL"
예 ) 로그인 - Annotation 사용 http://api.i-um.com/authentication/login email : exnis@naver.com password : ########## mobiletype : IPHONE / ANDROID POST 방식으로호출 @RequestMapping("/authentication") @Controller public class AuthenticationApiController { } @RequestMapping(value = "/login", method = RequestMethod.POST produces = "application/json") @ResponseBody public ApiResult login( @RequestParam("email") String email, @RequestParam("password") String password ) { }
예 ) 로그인 - Annotation 사용 http://api.i-um.com/authentication/login email : exnis@naver.com password : ########## mobiletype : IPHONE / ANDROID POST 방식으로호출 @RequestMapping("/api/authentication") @Controller public class AuthenticationApiController { } @RequestMapping(value = "/login", method = RequestMethod.POST, pro duces = "application/json") @ResponseBody public ApiResult login( @RequestParam("email") String email, @RequestParam("password") String password ) { }
예 ) 로그인 - Annotation 사용 { } "status": "SUCCESS", "result": { "accesstoken": 28as9dyhd923!3e2" }, "error": "NULL" JSON 으로리턴 @RequestMapping("/api/authentication") @Controller public class AuthenticationApiController { } @RequestMapping(value = "/login", method = RequestMethod.POST, pro duces = "application/json") @ResponseBody public ApiResult login( @RequestParam("email") String email, @RequestParam("password") String password ) { }
예 ) 로그인 JSON 으로리턴 JSON 으로결과값을리턴하기위해서는? 1. MappingJacksonHttpMessageConverter 사용 2. MappingJacksonJsonView (JSON 지원 View 이용 ) & ContentNegotiatingViewResolver 사용 (JSON 외 XML 등다른 format 의 view 제공하고싶을때 )
예 ) 로그인 JSON 으로리턴 MappingJacksonHttpMessageConverter? Controller 가리턴하는오브젝트 HttpMessageC onverter 클라이언트가원하는리소스 MappingJacksonHttpMessageConverter : JSON MarshallingHttpMessageConverter : XML RssChannelHttpMessageConverter : RSS...
예 ) 로그인 JSON 으로리턴 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> ResponseBody 를통해반환되는값에한글이있을경우깨지는현상을방지하기위해넣어줌 2. JSON 으로 return 하고자하는 Controller 에 @ResponseBody 를붙여줌 @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public ApiResult login(
로그인 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 가발생했을때클라이언트가뿌려줘야할메시지 } { } JSON 으로변환 "status":"success", "result":{"accesstoken":"28as9dyhd923!3e2"}, "error":"null"
다른예를 들어보겠습니다
예 ) 배지 RequestMapping 전략 1) 배지목록가져오기 @RequestMapping(value = "/badges", method = RequestMethod.GET) @ResponseBody public ApiResult getbadges( ) { }
예 ) 배지 RequestMapping 전략 2) badgeid 에해당하는배지가져오기 @RequestMapping(value = "/badges/{badgeid}", method = RequestMethod.GET) @ResponseBody public ApiResult getbadge(@pathvariable Long badgeid) { }
예 ) 배지 RequestMapping 전략 3) badgeid 에해당하는배지의사진들가져오기 @RequestMapping(value = "/badges/{badgeid}/photos", method = RequestMethod.GET) @ResponseBody public ApiResult getbadgephotos(@pathvariable Long badgeid) { }
배지 4) badgeid 에해당하는배지의 {slotnum} 번째사진가져오기 @RequestMapping(value = "/badges/{badgeid}/photos/{slotnum}", method = RequestMetho d.get) @ResponseBody public ApiResult getbadgephoto( @PathVariable Long badgeid, @PathVariable( slotnum ) Integer slotnum) { }
배지 5) badgeid 에해당하는배지의 {slotnum} 번째사진업로드하기 @RequestMapping(value = "/badges/{badgeid}/photos/{slotnum}/upload", method = Reque stmethod.post) public ApiResult uploadbadgephoto( @PathVariable Long badgeid, @PathVariable( photoorder ) Integer photoorder) { }
API Exception Handling
예외처리 Service 에서 Business Logic 이실행되다가 Exception 이발생하면 Controller 에서 catch 해서에러코드와메시지를리턴해줌 { } "status":"fail", "result":"null", "error":"invalid_email_password" "message":" 이메일또는비밀번호가틀렸습니다 "
예외처리 Exception 을상속받아별도의 Exception Class 를만듦 Exception extends A Exception private AExceptionReason exceptionreason; public enum AExceptionReason { 이유 1, 이유 2, 이유 3, 이유 4, }; : 예외가발생한이유를 enum 형으로정의함
예외처리 도메인 or 서비스마다 Exception(class) 과 Exception Reason(enum) 이생김 AException (class) AExceptionReason (enum) BException (class) BExceptionReason (enum) CException (class) CExceptionReason (enum) DException (class) DExceptionReason (enum) EException (class) EExceptionReason (enum) Fexception (class) FExceptionReason (enum) GException (class) GExceptionReason (enum) HException (class) HExceptionReason (enum) IException (class) IExceptionReason (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); } Exception 개수만큼조건문늘어남!! } apiresult.setmessage(e.getmessage()); return apiresult;
예외처리 : 개선후 Enum 은 extend 가안되어서, interface 만든후 implements 하도록처리! ExceptionReason (interface) implements AExceptionReason (enum) BExceptionReason (enum) CExceptionReason (enum)
예외처리 : 개선후 Exception (class) extends FrontModuleException (a bstract class) public abstract ExceptionReason getexceptionreason(); extends AException (class) private AExceptionReason aexceptionreason; BException (class) private BExceptionReason bexceptionreason; @Override public ExceptionReason getexceptionreason() { return aexceptionreason; } @Override public ExceptionReason getexceptionreason() { return bexceptionreason; }
예외처리 : 개선후 Exception 개수가많아도하나로처리! try { } catch (FrontModuleException e) { apiresult.seterror(e.getexceptionreason()); apiresult.setmessage(e.getmessage()); } return apiresult; But 여전히코드의중복이발생!
예외처리 : 다른개선방안 ExceptionResolver ResponseEntity<?> ContentNegotiatingView Resolver http://dev.anyframejava.org/docs/anyframe/plugin/springrest/1.0.2/reference/html/ch10.html
API Security
API Security 적당히 회사내부에서만사용되는 API (private) 클라이언트 ( 앱 ) 와통신하는 API ( 반만 public) 꼼꼼히 3rd Party 나개발자에게공개된 API (public) 아래에해당할수록어플리케이션레벨의보안에신경써야!
API Security ID(Identity) : 누가 API request 를요청했는지확인 인증 (Authentication) : 주체의신원을주체가주장하는신원과대비해검증하는과정 (A 가정말로 A 가맞는지확인 ) 허가 (Authorization) : 인증된사용자에게권한들을승인하는과정 (A 가어떤액션을하려고할때, 그액션을하도록허용되었는지확인 ) API 에서이 3 가지를모두요구하지는않는다!
API Security ID(Identity) 만요구 - Google Maps API : API key 만알면 API 사용가능
API Security ID(Identity), 인증 (Authentication) 요구 Twitter API : username / password 를입력해서인증해야함
API Security ID(Identity), 인증 (Authentication), 허가 (Authorization) 요구 Facebook API : email / password 입력한후, 특정 action 에대한허가를요구함
이렇게했습니다
API Security Oauth OpenID SAML HTTP authentication WS-Security Basic API Key
API Security 로그인이후모든 API 호출시, 액세스토큰 (Access Token) 을파라미터로같이넘겨매번인증 (Authentication) 함 액세스토큰은 DB 에저장되어있음 ( 세션에저장하지않음 ) 스프링에서제공하는 http basic 이나 remember-me authenticai ton 을사용하지않았음 액세스토큰이다시생성되어업데이트및, 클라이언트에게리턴되는경우 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 AccessToken 을인증하는로직을 Interceptor 로분리해서 Controller 메소드가실행되기전호출되도록처리함 public class MobileAuthenticationInterceptor extends HandlerInterceptorAd apter { } @Override public boolean prehandle(httpservletrequest request, HttpServletResponse response, Object handler) throws Exception { accesstoken 을 authentication 하는로직 }
API Security 특정유저 ( 슈퍼유저 ) 만호출가능한메서드를만들고싶을때 메서드호출보호 (Spring Security) 보안인터셉터엘리먼트를통한메서드보호 포인트컷을활용한메서드보호 애너테이션을활용한메소드보호 - 스프링 3 레시피中 - 장점 : 호출보호하고자하는메서드위에애너테이션만붙이면된다. 단점 : 나중에어디에애너테이션을적용했는지잊어버려검색해봐야한다.
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 />
API Security - 잊지말아야할것! 클라이언트에정보를딱필요한만큼만준다. 민감한정보는절대넘겨주지않는다. ( 예 : 유저의 seq_id, 주민번호등 )
API TEST
출처 : http://www.biologyreference.com/ar-bi/bacterial-genetics.html API(Analytical Profil e Index) TEST?
클라이언트 ( 앱 ) 개발자나기획자가쉽고편하게 API 를테스트하게하려면?
API TEST REST Client 이용 (Firefox, Chrome 확장플러그인설치 )? 장점 : 설치및사용이쉽다. 단점 : 테스트해야할 API 들이많은경우, 매번 HTTP URL 을입력하기번거롭다. 개발자에게만친숙한환경이다.
이렇게했습니다
API TEST 웹테스트페이지
API TEST http://swagger.wordnik.com/ http://twitter.github.com/bootstrap/
API TEST Component scan 으로모든 Controller 클래스를스캔한후 (@Controller 에너테이션으로스캔가능 ), 클래스의 methods() 를사용해모든 method 를가져올수있음 @PathVariable 로들어오는값과 @RequestParam 으로들어오는값을따로처리해야함 메소드추가, 삭제가불편함 ( 일괄적으로처리하기때문 ) 유연하게카테고리를나누기힘듬 메소드가생길때마다 URL, 메소드타입, 파라미터는개발자가직접입력하자!
API TEST ApiTestEnum ApiTestManager ApiTestController ApiTestView (swagger, bootstrap) public enum ApiTestEnum { // A. 로딩페이지 IntroGate("A. 로딩페이지 ", "/intro", "GET", "gate"), 카테고리 URL HTTP 메소드명메소드 // B. 로그인 Login("B. 로그인 ", "/authentication/login", "POST", "login"), FindPassword("B. 로그인 ", "/authentication/password/find", "POST", "findpassword"), // C. 가입 IsVaildEmail("C. 가입 ", "/authentication/email/check", "POST", "isvalidemail"), AuthorizeName("C. 가입 ", "/authentication/name", "POST", "authorizename"),... private String apicategory; private String apiurl; private String methodtype; private String name; }
References 1. http://apigee.com/about/api-best-practices/all/ebook 2. [Book] 웹개발자를위한웹을지탱하는기술 3. [Book] 스프링인액션제 3 판 4. [Book] 스프링 3 레시피 5. [Book] 스프링시큐리티 3
Thank you for Li stening!