Data Rest

Data Rest?

์Šคํ”„๋ง ๊ฐœ๋ฐœ ์ค‘, ๋‹จ์ˆœ ์กฐํšŒ๋ฅผ ์œ„ํ•ด Service, Controller๋ฅผ ๋งŒ๋“ค ๋•Œ ๋งŽ์€ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š” ๋ถˆํŽธํ•จ์„ ๊ฒฝํ—˜ํ•œ ์ ์ด ์žˆ์„ ๊ฒƒ์ด๋‹ค. ์ด๋Ÿฌํ•œ ๋ถˆํŽธํ•จ์„ ํ•ด์†Œํ•˜๊ธฐ ์œ„ํ•ด Spring Data REST๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ž๋™์œผ๋กœ RESTful API๋กœ ๋…ธ์ถœํ•˜์—ฌ ๊ฐœ๋ฐœ์ž์˜ ๋ถ€๋‹ด์„ ์ค„์ธ๋‹ค. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ์ •์˜ํ•˜๋ฉด, Spring Data REST๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ CRUD ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ RESTful ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.

Spring Data REST๋Š” ๋˜ํ•œ HATEOAS ์›์น™์„ ์ค€์ˆ˜ํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ๊ฐ„์˜ ํƒ์ƒ‰์„ฑ์„ ๋†’์ธ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ๋ฃจํŠธ URL์— GET ์š”์ฒญ์„ ๋ณด๋‚ด์–ด, ์„œ๋ฒ„๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ชจ๋“  ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ๋งํฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” JSON ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ๋Š” ๋ณ„๋„์˜ ๋ฌธ์„œํ™” ์—†์ด๋„ API์˜ ํƒ์ƒ‰์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ด์ฒ˜๋Ÿผ Spring Data REST๋Š” ๊ธฐ๋ณธ์ ์ธ CRUD ๊ธฐ๋Šฅ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๋ฉฐ, ๊ฐœ๋ฐœ์ž๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋”์šฑ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ, ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ๋ฐ˜๋ณต์ ์ธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋Œ€์‹ , Spring Data REST๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋”์šฑ ํšจ์œจ์ ์ธ ๊ฐœ๋ฐœ์„ ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

Paging ?

Spring Data REST๋Š” ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์—ฌ ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์…‹์„ ํšจ์œจ์ ์œผ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋ฉด ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ณ , ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์›ํ•˜๋Š” ํฌ๊ธฐ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜๋ˆ ์„œ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Paging

๊ธฐ๋ณธ์ ์œผ๋กœ PagingAndSortingRepository<T, ID>๋ฅผ ์ƒ์†๋ฐ›๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” ์ž๋™์œผ๋กœ ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํŠน์ • URL ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด ํŽ˜์ด์ง€ ํฌ๊ธฐ์™€ ์‹œ์ž‘ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŽ˜์ด์ง•์„ ์ง์ ‘ ์ •์˜ํ•œ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ์— ์ ์šฉํ•˜๋ ค๋ฉด, ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— Pageable ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋ฐ˜ํ™˜ ํƒ€์ž…์„ Page ๋˜๋Š” Slice๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Spring Data REST๊ฐ€ ์ž๋™์œผ๋กœ ํŽ˜์ด์ง• ๋งํฌ๋ฅผ ์‘๋‹ต์— ์ถ”๊ฐ€ํ•˜์—ฌ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋˜๋Š” ์ด์ „ ํŽ˜์ด์ง€๋กœ ์‰ฝ๊ฒŒ ์ด๋™ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ด์ „ ๋ฐ ๋‹ค์Œ ๋งํฌ

๊ฐ ํŽ˜์ด์ง€ ์‘๋‹ต์—๋Š” ํ˜„์žฌ ํŽ˜์ด์ง€์— ๋”ฐ๋ผ ์ด์ „ ๋ฐ ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ๋งํฌ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ฒซ ํŽ˜์ด์ง€์ผ ๊ฒฝ์šฐ prev ๋งํฌ๋Š” ์ƒ์„ฑ๋˜์ง€ ์•Š์œผ๋ฉฐ, ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์ผ ๊ฒฝ์šฐ next ๋งํฌ๋Š” ์ƒ์„ฑ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

curl localhost:8080/people?size=5

์‘๋‹ต์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค:

{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons{&sort,page,size}", 
      "templated" : true
    },
    "next" : {
      "href" : "http://localhost:8080/persons?page=1&size=5{&sort}", 
      "templated" : true
    }
  },
  "_embedded" : {
    โ€ฆ data โ€ฆ
  },
  "page" : { 
    "size" : 5,
    "totalElements" : 50,
    "totalPages" : 10,
    "number" : 0
  }
}

์ด์ฒ˜๋Ÿผ ์‘๋‹ต์—๋Š” ํ˜„์žฌ ํŽ˜์ด์ง€ ์ •๋ณด์™€ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜, ์ด ์—”ํ‹ฐํ‹ฐ ์ˆ˜ ๋“ฑ์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด UI์—์„œ ์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋„๊ตฌ๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Sorting

Spring Data REST๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์˜ ์ •๋ ฌ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด ๊ฒฐ๊ณผ๋ฅผ ์ •๋ ฌํ•  ์ˆ˜ ์žˆ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํŠน์ • ์†์„ฑ์— ๋”ฐ๋ผ ๊ฒฐ๊ณผ๋ฅผ ์ •๋ ฌํ•˜๋ ค๋ฉด sort URL ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

curl -v "http://localhost:8080/people/search/nameStartsWith?name=K&sort=name,desc"

์—ฌ๋Ÿฌ ์†์„ฑ์„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ•˜๋ ค๋ฉด sort=PROPERTY ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์†์„ฑ ๊ฒฝ๋กœ ํ‘œ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•ด ์ค‘์ฒฉ๋œ ์†์„ฑ์œผ๋กœ๋„ ์ •๋ ฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด ์ˆœ์„œ์— ๋”ฐ๋ผ Pageable์— ์ •๋ ฌ ์ •๋ณด๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.

Spring Data REST๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‚ด๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋•Œ๋กœ๋Š” ๋‹ค์–‘ํ•œ ์ด์œ ๋กœ ํ•ด๋‹น ๋ชจ๋ธ์˜ ๋ทฐ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์„น์…˜์—์„œ๋Š” ๋ฆฌ์†Œ์Šค์˜ ๋ทฐ๋ฅผ ๋‹จ์ˆœํ™”ํ•˜๊ณ  ์ถ•์†Œ๋œ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋กœ์ ์…˜๊ณผ ์—‘์„œํ”„์ธ ๋ฅผ ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Projections?

Projections

ํ•ด๋‹น ๋„๋ฉ”์ธ ๊ฐ์ฒด์— ๋Œ€ํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ์„ฑํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •

interface PersonRepository extends CrudRepository<Person, Long> {}

๊ธฐ๋ณธ์ ์œผ๋กœ Spring Data REST๋Š” ์ด ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ๋ชจ๋“  ์†์„ฑ์„ ํฌํ•จํ•œ ํ˜•ํƒœ๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. firstName๊ณผ lastName์€ ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋กœ ๋‚ด๋ณด๋‚ด์ง€๊ณ , address ์†์„ฑ์— ๋Œ€ํ•ด์„œ๋Š” ๋‘ ๊ฐ€์ง€ ์˜ต์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์˜ต์…˜์€ Address ๊ฐ์ฒด์— ๋Œ€ํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค

interface AddressRepository extends CrudRepository<Address, Long> {}

์ด ๊ฒฝ์šฐ, Person ๋ฆฌ์†Œ์Šค๋Š” address ์†์„ฑ์„ ํ•ด๋‹น ์ฃผ์†Œ ๋ฆฌ์†Œ์Šค๋กœ ์—ฐ๊ฒฐ๋˜๋Š” URI๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‹œ์Šคํ…œ์—์„œ "Frodo"๋ฅผ ์กฐํšŒํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ HAL ๋ฌธ์„œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    },
    "address" : {
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}

๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ Address ๋„๋ฉ”์ธ ๊ฐ์ฒด์— ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ •์˜ํ•˜์ง€ ์•Š์œผ๋ฉด, Spring Data REST๋Š” Person ๋ฆฌ์†Œ์Šค ๋‚ด์— ์ฃผ์†Œ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค:

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : {
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    }
  }
}

ํ•˜์ง€๋งŒ ์ฃผ์†Œ ์ •๋ณด๋ฅผ ์ „ํ˜€ ํฌํ•จํ•˜์ง€ ์•Š๊ณ ์ž ํ•  ๊ฒฝ์šฐ, ๊ธฐ๋ณธ์ ์œผ๋กœ Spring Data REST๋Š” id๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ์†์„ฑ์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์ฃผ์†Œ๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š๋Š” ํ”„๋กœ์ ์…˜์˜ ์˜ˆ์ž…๋‹ˆ๋‹ค:

@Projection(name = "noAddresses", types = { Person.class }) 
interface NoAddresses { 

  String getFirstName(); 

  String getLastName(); 
}

@Projection ์–ด๋…ธํ…Œ์ด์…˜์€ ์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ”„๋กœ์ ์…˜์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. name ์†์„ฑ์€ ํ”„๋กœ์ ์…˜์˜ ์ด๋ฆ„์„ ์ง€์ •ํ•˜๋ฉฐ, types ์†์„ฑ์€ ์ด ํ”„๋กœ์ ์…˜์ด Person ๊ฐ์ฒด์—๋งŒ ์ ์šฉ๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์ ์…˜์€ firstName๊ณผ lastName๋งŒ์„ ๋‚ด๋ณด๋‚ด๊ณ , address ์ •๋ณด๋Š” ์ œ์™ธ๋ฉ๋‹ˆ๋‹ค.

Excerpts

์—‘์„œํ”„์ธ ๋Š” ์ปฌ๋ ‰์…˜ ๋ฆฌ์†Œ์Šค์— ์ž๋™์œผ๋กœ ์ ์šฉ๋˜๋Š” ํ”„๋กœ์ ์…˜์˜ ์ผ์ข…์œผ๋กœ, ๋ฆฌ์†Œ์Šค ์ปฌ๋ ‰์…˜์„ ๋‹จ์ˆœํ™”๋œ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, PersonRepository์— ์—‘์„œํ”„์ธ  ํ”„๋กœ์ ์…˜์„ ์ ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

@RepositoryRestResource(excerptProjection = NoAddresses.class)
interface PersonRepository extends CrudRepository<Person, Long> {}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Person ๋ฆฌ์†Œ์Šค๊ฐ€ ์ปฌ๋ ‰์…˜์— ํฌํ•จ๋  ๋•Œ NoAddresses ํ”„๋กœ์ ์…˜์ด ์ž๋™์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

์—‘์„œํ”„์ธ ๋Š” ๊ฐœ๋ณ„ ๋ฆฌ์†Œ์Šค์—๋Š” ์ž๋™์œผ๋กœ ์ ์šฉ๋˜์ง€ ์•Š์œผ๋ฉฐ, ์ปฌ๋ ‰์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ณธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•˜๋Š” ๋ฐ ์ค‘์ ์„ ๋‘ก๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ”„๋กœ์ ์…˜๊ณผ ์—‘์„œํ”„์ธ ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•„์š”์— ๋”ฐ๋ผ ๋…ธ์ถœ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Last updated

Was this helpful?