[Spring boot] Spring Data Rest๋ฅผ ์ด์ฉํ REST API ๊ฐ๋ฐ 2
์ง๋ ํฌ์คํธ์์ Spring Data Rest๋ฅผ ์ด์ฉํด Domain๊ณผ Repository Interface๋ง์ ๊ตฌํํ์ฌ HATEOAS์ ์ค์ํ REST API ์๋ฒ๋ฅผ ๊ฐ๋ฐํ์์ต๋๋ค.
๊ทธ๋ฌ๋ REST API๋ฅผ ์ค๊ณํ๋ค๋ณด๋ฉด, HATEOAS์ ๊ตฌ์กฐ๊ฐ ๋ง์์ ๋ค์ง ์์ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค๋ฉด, ๋ฐ์ดํฐ๊ฐ ๋น์ด์์ ๋ ํด๋์ค์ ์ ๋ณด๊ฐ ๋์จ๋ค๊ฑฐ๋, ์ด ์ธ์ ์ ๋ณด๋ฅผ ์ถ๊ฐํ๊ณ ์ถ๋ค๊ฑฐ๋, ๋ง์ฝ User ์ ๋ณด๋ฅผ ๊ฐ์ ธ๋ค ์ค๋ค๋ฉด, ํจ์ค์๋ ์ ๋ณด์ ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ ์จ๊ฒจ์ผํ ๊ฒ์ ๋๋ค.
์ด๋ฒ ํฌ์คํธ์์๋ Spring Data Rest์ Controller + Service ์กฐํฉ์ ๋ฃ์ด์ ๋๋ง์ REST API๋ฅผ ๊ตฌํํ์ฌ API ์๋ฒ๋ฅผ ๋ง๋ค์ด๋ณด๋ ์๊ฐ์ ๊ฐ์ ธ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
RepositroyRestController
์ง๋ ํฌ์คํธ์์ ์ฌ์ฉํ๋ ํ๋ก์ ํธ๋ฅผ ์ด์ด์ ์ฌ์ฉํ์ฌ ์ฌ๊ธฐ์ controller ํจํค์ง๋ฅผ ์ถ๊ฐํ์ฌ, ItemController ๋ผ๋ ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.

package xyz.neonkid.rest.controller; | |
import lombok.RequiredArgsConstructor; | |
import org.springframework.data.domain.Page; | |
import org.springframework.data.domain.Pageable; | |
import org.springframework.data.rest.webmvc.RepositoryRestController; | |
import org.springframework.data.web.PageableDefault; | |
import org.springframework.hateoas.CollectionModel; | |
import org.springframework.hateoas.PagedModel; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; | |
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; | |
import org.springframework.web.bind.annotation.ResponseBody; | |
import xyz.neonkid.rest.domain.Item; | |
import xyz.neonkid.rest.repository.ItemRepository; | |
/** | |
* Created by Neon K.I.D on 5/20/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
@RequestMapping("/items") | |
@RepositoryRestController | |
@RequiredArgsConstructor | |
public class ItemController { | |
private final ItemRepository itemRepository; | |
@GetMapping | |
public @ResponseBody CollectionModel<Item> getItems(@PageableDefault Pageable pageable) { | |
Page<Item> itemList = itemRepository.findAll(pageable); | |
PagedModel.PageMetadata pageMetadata = new PagedModel.PageMetadata(pageable.getPageSize(), | |
itemList.getNumber(), itemList.getTotalElements()); | |
PagedModel<Item> items = PagedModel.of(itemList.getContent(), pageMetadata); | |
items.add(linkTo(methodOn(ItemController.class).getItems(pageable)).withSelfRel()); | |
return items; | |
} | |
} |
Repository์์ ์ฌ์ฉํ๋ @RepositroyRestResource๋ฅผ ์ง์ฐ๊ณ , Controller์์ @RepositoryRestController๋ฅผ ์ ๋ ฅํ๋ฉด, HATEOAS ํํ๊ฐ ๊ฐ์ถฐ์ง ์๋ต ํํ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ์ถฐ์ง๊ฒ ๋ฉ๋๋ค. ๋ค๋ง, ์ด ๋ ์ฌ์ฉํ๋ ์ด๋ ธํ ์ด์ ์์๋ @RestController ์ฒ๋ผ @ResponseBody ์ด๋ ธํ ์ด์ ์ด ์ฝ์ ๋์ด ์์ง ์๊ธฐ ๋๋ฌธ์, ํด๋์ค ์์ชฝ์ด๋ ๋ฉ์๋ ์์ชฝ์ @ResponseBody ์ด๋ ธํ ์ด์ ์ ์ ๋ ฅํด์ค์ผ ํฉ๋๋ค.
ํ ๊ฐ์ง ๋, ์ฃผ์ํด์ผ ํ ์ฌํญ์ด ์๋ค๋ฉด, @RepositoryRestController๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด, ๋งคํํ๋ URI ํ์์ด Spring boot data rest์์ ์ ์ํ๋ REST API ํ์์ ๋ง์์ผ ํฉ๋๋ค. ํด๋น ๋ถ๋ถ์ ๋ํด์๋ ์๋์ ๊ธ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
2020/05/19 - [Programming/Spring] - [Spring boot] REST API์ ๊ธฐ์ด์ ์ค๊ณ
[Spring boot] REST API์ ๊ธฐ์ด์ ์ค๊ณ
Spring boot๊ฐ ๊ธฐ์กด์ Spring์ ๋นํด ๋ค์ํ ์ค์ ๋ค์ ์๋ํ ์์ผ ๊ฐ๋ฐ์๊ฐ ์ค์ ํด์ผ ํ ๋ถ๋ถ์ ์ค์ด๊ณ , ์๋ฒ ๋๋ ํฐ์บฃ์ ํ์ฌํ์ฌ ๋ ์ฌ์ด ๊ฐ๋ฐ๋ค์ด ๊ฐ๋ฅํด์ก๋ค๋ ๊ฒ์ ์์์ต๋๋ค. ์ด๋ฒ ํฌ์คํธ์๏ฟฝ
blog.neonkid.xyz
๊ธฐ์กด์ @RepositoryRestResource๋ฅผ ์ ๋ ฅํ๋ฉด ์ ๊ณตํ๋ URI ํ์๊ณผ ๊ฐ๊ฒ ์ ๊ณตํด์ผ ํด๋น Controller์ ๋ฉ์๋๊ฐ ๊ธฐ์กด์ ๊ธฐ๋ณธ API๋ฅผ Overriding ํ๊ธฐ ๋๋ฌธ์, ๋ง์ฝ ์ผ๋ถ ๋ฉ์๋์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ๋ฉ์๋๋ฅผ ๊ทธ๋๋ก ์ด์ฉํ๊ณ ์ถ๋ค๋ฉด Controller์์ ํด๋น ๋ฉ์๋๋ฅผ ๋ณ๋๋ก ๊ตฌํํ์ง ์์ผ์๋ฉด ๋ฉ๋๋ค.
์ ์ฝ๋๋ ๊ธฐ์กด์ ์๋ต ํํ์์ ๋ง์ URL๋ฅผ ์ ๊ณตํ์๋๋ฐ, ํด๋น URL ์ค์์ ์์ฒญํ URL์ ์ ์ธํ ๋๋จธ์ง URL์ ์ ๊ฑฐํ๊ณ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ตํด์ฃผ๋ ์ฝ๋์ ๋๋ค.
PageMetadata ๊ฐ์ฒด๋ฅผ ์ด์ฉํด ์ ์ฒด ํ์ด์ง ์์ ํ์ฌ ํ์ด์ง ๋ฒํธ, ์ด ์ํ ์ ๋์ ํ์ด์ง ์ ๋ณด๋ฅผ ๋ด๊ณ , PagedModel์ ์ด์ฉํ์ฌ ์ปฌ๋ ์ ํ์ด์ง์ ๋ฆฌ์์ค ์ ๋ณด๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ์ ๊ณตํด์ฃผ๋๋ก ํฉ๋๋ค. ๋ง์ง๋ง์ผ๋ก ํ์ํ ๋งํฌ๋ฅผ linkTo ๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ ์์ฒญํ ๊ฐ๊ฐ์ ์ํ์ ๋งํฌ๋ง์ ๋ํ๋ผ ์ ์๋๋ก selfRel ๋ฉ์๋๋ฅผ ์ด์ฉํ์์ต๋๋ค.

๊ทธ๋ฌ๋ฉด ์ด๋ ๊ฒ, self URL์ ์ ์ธํ ๋๋จธ์ง URL์ ํ์๊ฐ ์๋์ ์ข ๋ ๊น๋ํ ๊ฒฐ๊ณผ๊ฐ์ด ๋์ค๊ฒ ๋๋ ๊ฒ์ด์ฃ . ์ด๋ ๊ฒ ์ฌ๋ฌ๋ถ๋ค์ด ์ํ๋ Result ํด๋์ค๋ฅผ ๋ง๋ค๊ฑฐ๋ ์ํ๋ ์๋ต๊ฐ์ ์ ํ ๋๋ Controller๋ฅผ ๊ตฌํํ์ฌ, REST API URI ํ์์ ๋ง์ถ๋ฉด ์ด๋ฏธ ๊ตฌํ๋์ด ์๋ Data Rest์์ Overriding ๋์ด, ์๋ํ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
JsonIgnore
์ํ ๋ชจ๋ธ๋ง๊ณ , ๋ค๋ฅธ ๋ชจ๋ธ์ ๋ํด์๋ ํ ๋ฒ ๋ค๋ค๋ณด๊ฒ ์ต๋๋ค. ๋ง์ฝ ์ํ ๋ชจ๋ธ์ ๋ง๋ค์๊ณ , ์ํ์ ๊ตฌ์ ํ๊ธฐ ์ํด์๋ ์ฌ์ฉ์๊ฐ ์์ด์ผ ํฉ๋๋ค. ์ฌ์ฉ์์ ๋ํ ๋ชจ๋ธ์ ํ ๋ฒ ๋ง๋ค์ด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
package xyz.neonkid.rest.domain; | |
import lombok.AccessLevel; | |
import lombok.Builder; | |
import lombok.Getter; | |
import lombok.NoArgsConstructor; | |
import javax.persistence.*; | |
/** | |
* Created by Neon K.I.D on 5/20/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
@Getter | |
@Entity | |
@NoArgsConstructor(access = AccessLevel.PUBLIC) | |
@Table | |
public class User { | |
@Id | |
@GeneratedValue(strategy = GenerationType.IDENTITY) | |
@Column | |
private long Id; | |
@Column(unique = true, nullable = false) | |
private String userid; | |
@Column | |
private String password; | |
@Builder | |
public User(String userid, String password) { | |
this.userid = userid; | |
this.password = password; | |
} | |
} |
User Entity๋ฅผ ์ค๊ณํ ๋๋ ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ต๋๋ค. User๋ง๋ค PK๋ฅผ ์ฃผ๊ณ , Unique ์์ฑ์ ๋ฃ์ด, ID์ ์ค๋ณต์ ๋ฐฉ์งํ๋ ๋ฐฉ๋ฒ, ํน์ User ID๋ฅผ PK๋ก ์ค์ ํ๋ ๋ฐฉ๋ฒ ๋ฑ์ด ์๋๋ฐ์. ์ฌ๊ธฐ์๋ User๋ง๋ค PK๋ฅผ ์ฃผ๋ ๋ฐฉ์์ผ๋ก Entity๋ฅผ ๊ตฌ์ฑํ์์ต๋๋ค.
Controller์ Repository๋ Item ํด๋์ค์ ๋๊ฐ์ด ๊ตฌํํ์๋ฉด ๋ฉ๋๋ค.
$ curl -X POST -H "Content-Type: application/json" --data '{ "userid": "neonkid", "password": "1234" }' http://localhost:8080/api/users | json_pp

POST ๋ฉ์๋๋ฅผ ์ฌ์ฉํด ์ฌ์ฉ์ ํ ๊ฐ๋ฅผ ๋ฑ๋กํ๊ฒ ๋๋ฉด, Item API์ฒ๋ผ ๋๊ฐ์ด ๊ฒฐ๊ณผ๊ฐ์ด ๋ชจ๋ธ ํํ์ JSON์ผ๋ก ๋ณํ๋์ด ์ถ๋ ฅ์ด ๋ฉ๋๋ค.

๊ทธ๋ฐ๋ฐ, GET ๋ฉ์๋๋ฅผ ์ด์ฉํด์ ์ฌ์ฉ์๋ฅผ ์กฐํํ์ ๋, ํจ์ค์๋๋ ๊ฐ์ด ์ถ๋ ฅ์ด ๋ฉ๋๋ค. ๋ณดํต ์ฌ์ฉ์์ ๋น๋ฐ๋ฒํธ๋ ์๋ฒ์์ ์ฌ์ฉ์์ ์๋ณ์ ์ํด ์ฌ์ฉ๋์ด์ผ ํ๊ณ , ๊ทธ ์ด์ธ์๋ ์ฌ์ฉํ ์ ์์ด์ผ ํฉ๋๋ค. ๋ฌผ๋ก DB์์ ์ง์ ์กฐํํ๋ ๋ฐฉ๋ฒ๋ ์๊ฒ ์ง๋ง, REST API์ ๊ฒฝ์ฐ, ์ธ๋ถ์์ ์ฌ์ฉํ ์ ์๋ ํฌ์ธํธ ์ง์ ์ด ๋๊ธฐ ๋๋ฌธ์ ์ด๋ฌํ ์ ๋ณด๋ ์ฃผ์ง ์๋ ๊ฒ์ด ์ข๊ฒ ์ฃ ?
JsonIgnore์ ์ฌ์ฉ์ ๊ฐ๋จํฉ๋๋ค. ์๊น ๋ง๋ User ๋๋ฉ์ธ ํด๋ ์ค์์ Password ๋ฉค๋ฒ ๋ณ์์ JsonIgnore๋ง์ ์ถ๊ฐํด์ฃผ๋ฉด ๋์ ๋๋ค.


๊ทธ๋ฌ๋ฉด ์ด๋ ๊ฒ Password ๋ถ๋ถ์ ์ ์ธํ๊ณ , ํด๋ผ์ด์ธํธ์๊ฒ ๋์ ธ์ฃผ๊ณ ์์์ ์ ์ ์์ต๋๋ค.
Event Binding
์ฌ์ฉ์์ ์ถ๊ฐ/์์ ์ ํ ์๋น์ค์ ์์ด, ์ค์ํ ์ฌํญ์ ๋๋ค. ์๋ก์ด ๊ณ ๊ฐ์ด ๋ค์ด์๋ค๋ ๊ฒ์ด ๋ ์๋ ์๊ณ , ๊ณ ๊ฐ์ ์ ๋ณด๋ฅผ ๋ณ๊ฒฝํ๋ค๋ ์ ์ ๊ธฐ๋ก์ ๋จ๊ธธ ์ ์๋ค๋ฉด ์ข๊ฒ ์ฃ .
๊ทธ๋ฌ๋ ํด๋ผ์ด์ธํธ์์ ์ ๊ณตํ๋ ํ์ฌ ์๊ฐ์ด ๋ง์ง ์์ ์๋ ์๊ณ , ๋คํธ์ํฌ ์ง์ฐ์ผ๋ก ์๊ฐ์ด ๋ฌ๋ผ์ง ์๋ ์๊ธฐ ๋๋ฌธ์ ํด๋น ์์ ์ ์๋ฒ์์ ํด์ค๋ค๋ฉด, ์ข ๋ ์ ํํ๊ณ , ํ์คํ๊ฒ ์ ๋ณด ๊ธฐ๋ก์ผ๋ก ๋จ์ ๊ฒ์ ๋๋ค.
Spring boot data rest์์๋ ์ฌ๋ฌ ๋ฉ์๋์ ์ด๋ฒคํธ ๋ฐ์ ์์ ์ ํํนํ์ฌ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฌํ์ฌ, ์ด๋ฅผ ํ์๋ก ๋ง๋ค ์ ์๋ ์ด๋ฒคํธ ์ด๋ ธํ ์ด์ ์ ์ ๊ณตํฉ๋๋ค.
- BeforeCreateEvent
- AfterCreateEvent
- BeforeSaveEvent
- AfterSaveEvent
- BeforeDeleteEvent
- AfterDeleteEvent
- BeforeLinkSaveEvent
- AfterLinkSaveEvent
- BeforeLinkDeleteEvent
- AfterLinkDeleteEvent
๊ฐ๋จํ ์ค๋ช ์ ๋๋ฆฌ๋ฉด, Create๋ ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ์์ฑํ์ ๋์ ์ด๋ฒคํธ, Before์ After๋ ํด๋น ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๊ธฐ ์ ๊ณผ ํ๋ก ๋ฐ์ธ๋ฉ ๋ฉ๋๋ค. Save๋ ๊ธฐ์กด ๋ฐ์ดํฐ์ ์ ๋ฐ์ดํธ, Link์ ๊ฒฝ์ฐ ๊ด๊ณ(1:1, N:N)๋ฅผ ๊ฐ์ง ๋งํฌ๋ฅผ ์์ /์ญ์ ํ์ ๋์ ์ด๋ฒคํธ๋ฅผ ๋งํฉ๋๋ค.
package xyz.neonkid.rest.repository; | |
import lombok.extern.slf4j.Slf4j; | |
import org.springframework.data.rest.core.annotation.HandleAfterCreate; | |
import org.springframework.data.rest.core.annotation.HandleBeforeCreate; | |
import org.springframework.data.rest.core.annotation.RepositoryEventHandler; | |
import xyz.neonkid.rest.domain.User; | |
/** | |
* Created by Neon K.I.D on 5/20/20 | |
* Blog : https://blog.neonkid.xyz | |
* Github : https://github.com/NEONKID | |
*/ | |
@Component | |
@Slf4j | |
@RepositoryEventHandler | |
public class UserEventHandler { | |
@HandleBeforeCreate | |
public void beforeCreateUser(User user) { | |
log.warn("์ฌ์ฉ์ " + user.getUserid() + "๊ฐ ์๋ก์ด ์ ์ ๋ก ํ์ ๊ฐ์ ์ ์๋ ํฉ๋๋ค."); | |
} | |
@HandleAfterCreate | |
public void afterCreateUser(User user) { | |
log.info("์ฌ์ฉ์ " + user.getUserid() + "๊ฐ ์๋ก์ด ์ ์ ๋ก ๋ฑ๋ก๋์์ต๋๋ค."); | |
} | |
} |
๋จผ์ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ํด๋์ค๋ฅผ ํ ๊ฐ ๋ง๋ค์ด์ค๋๋ค. ์ด ๋, ํด๋์ค๋ฅผ ์ด๋ฆ์ ์ด๋ค Entity์ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ธ๋ฉํ ์ง๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ฝ๊ฒ ํ๊ธฐ ์ํด ๋๋ฉ์ธ ๋ชจ๋ธ + EventHandler๋ผ๋ ์ด๋ฆ์ผ๋ก ์ค์ ํด์ค๋๋ค. ์ด์ฐจํผ ๋ฉ์๋์ ์๋ ํ๋ผ๋ฏธํฐ๋ก ๊ตฌ๋ถ์ง๋ ๊ฒ์ด ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ์ง๋ง, ์ฝ๋๋ฅผ ๋ณด๊ธฐ ์ ์ ์ด ์ฝ๋๋ ๋ฌด์์ ํ๋ ์ญํ ์ด๋ค. ๋ผ๊ณ ํํํด์ฃผ๋ ๊ฒ์ด ๊ฐ์ฅ ์ข๊ฒ ์ต๋๋ค.
@RepositoryEventHandler๋ ๊ทธ์ ์ด๋ฒคํธ ํธ๋ค๋ง๋ง ํด์ฃผ๋ ์ด๋ ธํ ์ด์ ์ด๋ฏ๋ก ํด๋น Event Handler๋ฅผ ์ด์ฉํ๊ธฐ ์ํด Spring IoC์ ์์กด์ฑ์ ์ฃผ์ ํด์ค์ผ๋ง ๋์ํ๋ฏ๋ก @Component, @Bean ๋ฑ์ ์ด์ฉํ์ฌ ์์กด์ฑ ์ฃผ์ ์ ํด์ฃผ๋๋ก ํฉ์๋ค.

์ด๋ ๊ฒ POST ๋ฉ์๋๋ก ์ฌ์ฉ์ ๋ฑ๋ก์ ์๋ํ๋ฉด, ์์ ๊ฐ์ด EventHandler ํด๋์ค์์ ๊ตฌํํ๋๋ก, Log๊ฐ ์ฐํ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
๋ง์น๋ฉฐ...
Spring Data Rest๋ฅผ ์ด์ฉํ REST API๋ ๊ฐ๋ฅํ REST API์ ํ์ค ์ค URI ๋ถ๋ถ์ ์ง์ผ์ฃผ๋ฉด์, ์ด์ ๋ํ ๋ฐํ ๊ฐ์ ์ปค์คํฐ๋ง์ด์ง ํ๊ธฐ ์ฉ์ดํ๋ค๋ ๋๋์ ๋ฐ์์ต๋๋ค. ์ฝ๊ฐ์ ๊ฐ์ ์ฑ์ด ๋ถ์ฌ๋์ด ์์ด, ๊ฐ๋ฐํ๋ ๋ฐ ๋ถํธํจ์ ๋๋ ์๋ ์๊ฒ ์ง๋ง ์ด๋ป๊ฒ ๋ณด๋ฉด, ๊ฐ๋ฐ์ ์ ์ฅ์์ ํฐ ๊ท๋ชจ์ ์๋น์ค๋ฅผ ๊ฐ๋ฐ/์ด์ํ ๋ ์ ์ง๋ณด์๋ฅผ ์ข๊ฒ ํ๊ธฐ ์ํ ๋ชฉ์ ์ด๋ผ๊ณ ์๊ฐํ๋ค๋ฉด, ๊ทธ๋ฆฌ ๋์์ง ์์ ์ ํ์ผ ์๋ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
'Programming > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Spring Data] Hibernate, JPA ๊ทธ๋ฆฌ๊ณ Spring Data JPA (0) | 2020.05.23 |
---|---|
[Spring boot] JDBC์ Spring JDBC ๊ทธ๋ฆฌ๊ณ MyBatis (0) | 2020.05.22 |
[Spring boot] Spring Data Rest๋ฅผ ์ด์ฉํ REST API ๊ฐ๋ฐ 1 (0) | 2020.05.20 |
[Spring boot] MVC ํจํด์ ์ด์ฉํ REST API ๊ฐ๋ฐ (3) | 2020.05.19 |
[Spring boot] REST API์ ๊ธฐ์ด์ ์ค๊ณ (0) | 2020.05.19 |