버저닝(Versioning) REST APIs
4가지 버저닝 방법을 해볼 것인데요
1. URI Path
2. Query parameters
3. Custom headers
4. Content negotiation
버저닝을 해야되는 상황
- request/response format이 바뀌었을 때 (xml, json)
- property name( name, productName) or 타입이 바뀌었을 때
- 요구받은 필드를 추가할 때
- response에 대한 값을 제거할 때
Versioning through
1. URI Path
http://www.example.com/api/v1/empolyees
http://www.example.com/api/v2/products
ex) Twitter, Pay Pal, Google etc
package com.springboot.blog.controller;
@RestController
@RequestMapping()
public class PostController
{
private PostService postService;
@Autowired
private JwtTokenProvider tokenProvider;
// if you are configuring a class as a spring bean and it has only one constructor,
// then we can omit @Autowired annotation
public PostController(PostService postService) {
this.postService = postService;
}
// @GetMapping
// public List<PostDto> getAllPosts() {
// return postService.getAllPosts();
// }
// apply pagenation
@GetMapping("/api/v1/posts")
public PostResponse getAllPosts(
@RequestParam(defaultValue=AppConstants.DEFAULT_PAGE_NUMBER, required=false)int pageNo,
@RequestParam(defaultValue=AppConstants.DEFAULT_PAGE_SIZE, required=false)int pageSize,
@RequestParam(defaultValue=AppConstants.DEFAULT_SORT_BY, required=false)String sortBy,
@RequestParam(defaultValue=AppConstants.DEFAULT_SORT_DIRECTION, required =false) String sortDir,
@RequestHeader("Authorization") String jwtToken
) {
String username= tokenProvider.getUsernameFromJWT( jwtToken.substring(7, jwtToken.length()));
System.out.println(":: "+ username);
return postService.getAllPosts(pageNo,pageSize, sortBy,sortDir);
}
@PostMapping("/api/v1/posts")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<PostDto> createPost(@Valid @RequestBody PostDto postDto){
return new ResponseEntity<>(postService.createPost(postDto),HttpStatus.CREATED);
}
// get post by id
@GetMapping("/api/v1/posts/{id}")
public ResponseEntity<PostDto> getPostByIdV1(@PathVariable(name= "id") long id){
return ResponseEntity.ok(postService.getPostById(id));
}
// get post by id
@GetMapping("/api/v2/posts/{id}")
public ResponseEntity<PostDtoV2> getPostByIdV2(@PathVariable(name= "id") long id){
PostDto postDto = postService.getPostById(id);
PostDtoV2 postDtoV2= new PostDtoV2();
postDtoV2.setId(postDto.getId());
postDtoV2.setTitle(postDto.getTitle());
postDtoV2.setDescription(postDto.getDescription());
postDtoV2.setContent(postDto.getContent());
List<String> tags =new ArrayList<String>();
tags.add("java");
tags.add("SpringBoot");
tags.add("aws");
postDtoV2.setTags(tags);
return ResponseEntity.ok(postDtoV2);
}
// update post by id rest api
@PutMapping("/api/v1/posts/{id}")
public ResponseEntity<PostDto> updatePost(@Valid @RequestBody PostDto postDto, @PathVariable(name="id") long id){
PostDto postResponse = postService.updatePost(postDto, id);
return new ResponseEntity<>(postResponse,HttpStatus.OK);
}
// delete post by id rest api
@DeleteMapping("/api/v1/posts/{id}")
public ResponseEntity<String> deletePost(@PathVariable Long id)
{
postService.deletePostById(id);
return new ResponseEntity<>("삭제 완료",HttpStatus.OK);
}
}
@Data
public class PostDtoV2
{
private long id;
@NotEmpty
@Size(min =2, message="Post title should have at least 2 characters")
private String title;
@NotEmpty
@Size(min= 10, message="Post description should have at 10 characters")
private String description;
@NotEmpty
private String content;
private Set<CommentDto> comments;
private List<String> tags;
}
2. Query parameters
// get post by id
@GetMapping(value= "/api/posts/{id}", params= "version=1")
public ResponseEntity<PostDto> getPostByIdV1(@PathVariable(name= "id") long id){
return ResponseEntity.ok(postService.getPostById(id));
}
// get post by id
@GetMapping(value= "/api/v2/posts/{id}", params="version=2")
public ResponseEntity<PostDtoV2> getPostByIdV2(@PathVariable(name= "id") long id){
PostDto postDto = postService.getPostById(id);
PostDtoV2 postDtoV2= new PostDtoV2();
postDtoV2.setId(postDto.getId());
postDtoV2.setTitle(postDto.getTitle());
postDtoV2.setDescription(postDto.getDescription());
postDtoV2.setContent(postDto.getContent());
List<String> tags =new ArrayList<String>();
tags.add("java");
tags.add("SpringBoot");
tags.add("aws");
postDtoV2.setTags(tags);
return ResponseEntity.ok(postDtoV2);
}
GET, http://localhost:8080/api/posts/1?version=1
GET, http://localhost:8080/api/posts/1?version=2
3. Custom headers
장점 : URI 에 버전 정보가 없어도 된다.
단점 : 사용자 헤더에 버전 정보를 추가해야된다.
// get post by id
@GetMapping(value= "/api/posts/{id}", headers="X-API-VERSION=1")
public ResponseEntity<PostDto> getPostByIdV1(@PathVariable(name= "id") long id){
return ResponseEntity.ok(postService.getPostById(id));
}
헤더를 추가하지 않을 경우 api 를 찾을 수 없게된다. ->
서버에서 버저닝을 추가한다면 클라이언트에서 헤더를 모두 추가해야된다.
4. Content negotiation
커스터마이징 가능한 형태의 헤더 추가
headers[Accept=application/vnd.javapp-v1+jason]
headers[Accept=application/vnd-springcom-v1+json]
ex) 깃허브 : application/vnd.github.v3+json
// get post by id
@GetMapping(value= "/api/posts/{id}", produces="application/vnd.javapp.v1+json")
public ResponseEntity<PostDto> getPostByIdV1(@PathVariable(name= "id") long id){
return ResponseEntity.ok(postService.getPostById(id));
}
// get post by id
@GetMapping(value= "/api/posts/{id}", produces="application/vnd.springcom.v1+json")
public ResponseEntity<PostDtoV2> getPostByIdV2(@PathVariable(name= "id") long id){
PostDto postDto = postService.getPostById(id);
...
}
적용하지 않았을 때에도 호출가능!
url 매핑이 같은 메소드가 있다면 상단 메소드가 호출
headers[Accept=application/vnd.javapp-v1+jason] 호출
headers[Accept=application/vnd-springcom-v1+json] 호출
버저닝 실습을 끝으로 Controller 정리
URL Path 전략으로 버저닝을 유지
ex) api/v1, api/v2 ...
PostController.java
@RestController
@RequestMapping()
public class PostController
{
private PostService postService;
@Autowired
private JwtTokenProvider tokenProvider;
// if you are configuring a class as a spring bean and it has only one constructor,
// then we can omit @Autowired annotation
public PostController(PostService postService) {
this.postService = postService;
}
// @GetMapping
// public List<PostDto> getAllPosts() {
// return postService.getAllPosts();
// }
// apply pagenation
@GetMapping("/api/v1/posts")
public PostResponse getAllPosts(
@RequestParam(defaultValue=AppConstants.DEFAULT_PAGE_NUMBER, required=false)int pageNo,
@RequestParam(defaultValue=AppConstants.DEFAULT_PAGE_SIZE, required=false)int pageSize,
@RequestParam(defaultValue=AppConstants.DEFAULT_SORT_BY, required=false)String sortBy,
@RequestParam(defaultValue=AppConstants.DEFAULT_SORT_DIRECTION, required =false) String sortDir,
@RequestHeader("Authorization") String jwtToken
) {
String username= tokenProvider.getUsernameFromJWT( jwtToken.substring(7, jwtToken.length()));
System.out.println(":: "+ username);
return postService.getAllPosts(pageNo,pageSize, sortBy,sortDir);
}
@PostMapping("/api/v1/posts")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<PostDto> createPost(@Valid @RequestBody PostDto postDto){
return new ResponseEntity<>(postService.createPost(postDto),HttpStatus.CREATED);
}
// get post by id
@GetMapping(value= "/api/v1/posts/{id}")
public ResponseEntity<PostDto> getPostByIdV1(@PathVariable(name= "id") long id){
return ResponseEntity.ok(postService.getPostById(id));
}
// update post by id rest api
@PutMapping("/api/v1/posts/{id}")
public ResponseEntity<PostDto> updatePost(@Valid @RequestBody PostDto postDto, @PathVariable(name="id") long id){
PostDto postResponse = postService.updatePost(postDto, id);
return new ResponseEntity<>(postResponse,HttpStatus.OK);
}
// delete post by id rest api
@DeleteMapping("/api/v1/posts/{id}")
public ResponseEntity<String> deletePost(@PathVariable Long id)
{
postService.deletePostById(id);
return new ResponseEntity<>("삭제 완료",HttpStatus.OK);
}
}
CommentController.java
@RestController
@RequestMapping("/api/v1")
public class CommentController
{
AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController
{
SecurityConfig.java - configure()
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
.exceptionHandling()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.antMatchers(HttpMethod.GET, "/api/v1/**").permitAll()
.antMatchers("/api/v1/auth/**").permitAll()
.anyRequest()
.authenticated();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/api/v1/**, /api/v1/auth/** 에 대해서만 접근 허용
이렇게 4가지 버저닝 방법을 살펴보았는데요.
URI 설계에서 버저닝을 어떤 전략으로 가져갈지 정하고
Controller 구현을 해야겠다는 생각이 들었습니다.
댓글