Spring Boot - API Versioning

๐Ÿท๏ธ Tags: spring-boot, api, versioning, java

API๋ฅผ ๊ฐœ๋ฐœํ•˜๋‹ค๋ณด๋ฉด API ๋ฒ„์ „์— ๋Œ€ํ•œ ์ƒ๊ฐ์„ ํ•˜๊ฒŒ ๋œ๋‹ค.

๋ฒ„์ €๋‹(Versioning)์„ ํ•˜์ง€ ์•Š์œผ๋ฉด input/output์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์ด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋˜ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ œ๋Œ€๋กœ ์„œ๋น„์Šคํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ด๋Ÿฌํ•œ API Versioning์„ ํ•˜๊ธฐ ์œ„ํ•ด ์ง€๊ธˆ๊นŒ์ง€ ๋‹จ์ˆœํžˆ URI์— version์„ ๋ช…์‹œํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ๋งŒ ์ง„ํ–‰ํ•ด์™”๋Š”๋ฐ, ๋” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด ์ด๋ฅผ ์ •๋ฆฌํ•ด๋†“๊ณ ์ž ํ•œ๋‹ค.



URI Versioning / URI ์ด์šฉํ•˜๊ธฐ #

// v1
@RestController
@RequestMapping("/api/uri/v1")
public class UriVersionControllerV1 {

    @GetMapping("/versions")
    public ResponseEntity<List<String>> getVersions() {
        return ResponseEntity.ok(List.of("v1.1", "v1.2", "v1.3"));
    }

}

// v2
@RestController
@RequestMapping("/api/uri/v2")
public class UriVersionControllerV2 {

    @GetMapping("/versions")
    public ResponseEntity<List<String>> getVersions() {
        return ResponseEntity.ok(List.of("v2.1", "v2.2", "v2.3"));
    }

}

์•ž์„œ ์ด์•ผ๊ธฐํ•œ ๊ฒƒ์ฒ˜๋Ÿผ URI์— ํŠน์ • ๋ฒ„์ „์„ ๋ช…์‹œํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

๊ตฌํ˜„ํ•˜๊ธฐ ์‰ฝ๊ณ , URI๋ฅผ ํ†ตํ•ด ๋ฐ”๋กœ ๋ฒ„์ „์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ง๊ด€์ ์ด๋‹ค.

ํ•˜์ง€๋งŒ URI ์ž์ฒด๊ฐ€ ๊ธธ์–ด์ง€๊ณ , URI๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ๋Š” ์–ด๋ ต๋‹ค.



Request parameter Versioning #

@RestController
@RequestMapping("/api/parameter")
public class ParameterVersionController {

    @GetMapping(value = "/versions", params = "version=1")
    public ResponseEntity<List<String>> getVersionsV1() {
        return ResponseEntity.ok(List.of("v1.1", "v1.2", "v1.3"));
    }

    @GetMapping(value = "/versions", params = "version=2")
    public ResponseEntity<List<String>> getVersionsV2() {
        return ResponseEntity.ok(List.of("v2.1", "v2.2", "v2.3"));
    }

}

ํŠน์ • ๊ฐ’์˜ Request parameter๋ฅผ ํ†ตํ•ด์„œ ๋ฒ„์ „์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

URI Versioning์— ๋น„ํ•ด URI๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, Client ์ธก์—์„œ ์ ์ ˆํ•œ version parameter๋ฅผ ๊ผญ ๊ณ ๋ คํ•ด์•ผ๋งŒ ํ•œ๋‹ค.



Header Versioning #

@RestController
@RequestMapping("/api/header")
public class HeaderVersionController {

    @GetMapping(value = "/versions", headers = "X-API-Version=1")
    public ResponseEntity<List<String>> getVersionsV1() {
        return ResponseEntity.ok(List.of("v1.1", "v1.2", "v1.3"));
    }

    @GetMapping(value = "/versions", headers = "X-API-Version=2")
    public ResponseEntity<List<String>> getVersionsV2() {
        return ResponseEntity.ok(List.of("v2.1", "v2.2", "v2.3"));
    }

}

Custom header๋ฅผ ํ†ตํ•ด ๋ฒ„์ „์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค. Request parameter ๋ฐฉ์‹๊ณผ ์žฅ๋‹จ์ ์€ ์œ ์‚ฌํ•˜๋‹ค.

๋งŒ์•ฝ ์—ฌ๊ธฐ์— ๋”๋ถˆ์–ด gateway ๋˜๋Š” reverse proxy๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด header์— ๋Œ€ํ•œ ์ œํ•œ์ด ์—†๋Š”์ง€ ํ™•์ธํ•ด๋ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค.



Content negotiation Versioning #

@RestController
@RequestMapping("/api/header/accept")
public class AcceptHeaderVersionController {

    @GetMapping(value = "/versions", produces = "application/double.b.api.v1+json")
    public ResponseEntity<List<String>> getVersionsV1() {
        return ResponseEntity.ok(List.of("v1.1", "v1.2", "v1.3"));
    }

    @GetMapping(value = "/versions", produces = "application/double.b.api.v2+json")
    public ResponseEntity<List<String>> getVersionsV2() {
        return ResponseEntity.ok(List.of("v2.1", "v2.2", "v2.3"));
    }

}

HTTP Content negotiation ๋ฐฉ์‹์„ ์ด์šฉํ•œ Versioning ๋ฐฉ๋ฒ•์ด๋‹ค.

HTTP์˜ ํŠน์„ฑ์— ๋”ฐ๋ผ custom header/request parameter ์—†์ด๋„ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด ๋ถ€๋ถ„๋„ Client ์ธก์—์„œ Accept header๋ฅผ ์ž˜๋ชจ๋ฅด๋ฉด ์œ„ ๋ฐฉ์‹์ด ์ดํ•ด๊ฐ€์ง€ ์•Š์„ ๊ฒƒ ๊ฐ™์•„ Request ์˜ˆ์‹œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์•˜๋‹ค.

### accept header -> version 1
GET localhost:8080/api/header/accept/versions
Accept: application/double.b.api.v1+json

### accept header(content negotiation) -> version 1 - order
GET localhost:8080/api/header/accept/versions
Accept: application/double.b.api.v1+json, application/double.b.api.v2+json,

### accept header -> version 2
GET localhost:8080/api/header/accept/versions
Accept: application/double.b.api.v2+json

### accept header(content negotiation) -> version 2 - order
GET localhost:8080/api/header/accept/versions
Accept: application/double.b.api.v2+json, application/double.b.api.v1+json,

### accept header(content negotiation) -> version 1 - q factor
GET localhost:8080/api/header/accept/versions
Accept: application/double.b.api.v1+json;q=0.3, application/double.b.api.v2+json;q=0.1

### accept header(content negotiation) -> version 2 - q factor
GET localhost:8080/api/header/accept/versions
Accept: application/double.b.api.v1+json;q=0.7, application/double.b.api.v2+json;q=0.9


๊ฒฐ๋ก  #

์ด๋ ‡๋“ฏ 4๊ฐ€์ง€ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์•˜๋‹ค.

๋‹น์—ฐํžˆ ์ •๋‹ต์€ ์—†๋‹ค. ํŒ€/์กฐ์ง์—์„œ ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์„ ํ™œ์šฉํ•ด ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

๋Œ€์‹  ์ค‘์š”ํ•œ ๊ฒƒ์€ โ€˜์ผ๊ด€๋˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๊ฐ€โ€™ ์ธ ๊ฒƒ ๊ฐ™๋‹ค.