본문 바로가기
Back-end/Spring Boot + REST API

Spring boot - blog application (REST API) : Versioning (버저닝)

by javapp 자바앱 2022. 8. 4.
728x90

 

버저닝(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 구현을 해야겠다는 생각이 들었습니다.

 

 

댓글