๐ก Rest API ๋?
์ฐธ๊ณ : https://withseungryu.tistory.com/100
๐ก Spring data rest ๋?
spring-data-rest, a recent addition to the spring-dataproject, is a framework that helps you expose your entities directly as RESTful webservice endpoints. Unlike rails, grails or roo it does not generate any code achieving this goal. spring data-rest supports JPA, MongoDB, JSR-303 validation, HAL and many more. It is really innovative and lets you setup your RESTful webservice within minutes. In this example i’ll give you a short overview of what spring-data-rest is capable of.
a recent addition to the spring-dataproject, is a framework that helps you expose your entities directly as RESTful webservice endpoints.
spring-dataproject์ ์ถ๊ฐ๋์์ผ๋ฉฐ, Restful ์น์๋น์ค ์๋ํฌ์ธํธ๋ก์ ์ง์ ์ ์ผ๋ก ์ํฐํฐ๋ฅผ ๋ํ๋ผ ์ ์๋๋ก ๋์์ฃผ๋ ํ๋ ์์ํฌ์ด๋ค.
It is really innovative and lets you setup your RESTful webservice within minutes
์ ๋ง ํ์ ์ ์ด๊ณ Restful ์น์๋น์ค๋ฅผ ๊ตฌ์ฑํ๋๋ฐ ํฐ ๋์์ ์ค๋ค.
์ฐธ๊ณ : https://spring.io/projects/spring-data-rest
๐ ์ฌ์ฉ๋ฒ ์์
spring:
data:
rest:
base-path : /api
default-page-size : 10
max-page-size : 10
๐ ํ๋กํผํฐ
-
base-path : API์ ๋ชจ๋ ์์ฒญ์ ๊ธฐ๋ณธ ๊ฒฝ๋ก
-
default-page-size : ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ๋ก ํ์ด์ง ํฌ๊ธฐ๋ฅผ ์์ฒญํ์ง ์์ ๋ ์ ์ฉ ํ ๊ธฐ๋ณธ ํ์ด์ง ํฌ๊ธฐ
-
max-page-size : ์ต๋ ํ์ด์ง ์
-
page-param-name : ํ์ด์ง๋ฅผ ์ ํํ๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ช ์ ๋ณ๊ฒฝ
-
limit-param-name : ํ์ด์ง ์์ดํ ์๋ฅผ ๋ํ๋ด๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ช ๋ณ๊ฒฝ
-
sort-param-name : ํ์ด์ง์ ์ ๋ ฌ๊ฐ์ ๋ํ๋ด๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ช ์ ๋ณ๊ฒฝ
-
default-media-type: ๋ฏธ๋์ด ํ์ ์ ์ง์ ์ง ์์์ ๋ ์ฌ์ฉํ ๊ธฐ๋ณธ ๋ฏธ๋์ด ํ์ ์ค์
-
return-body-on-create : ์๋ก์ด ์ํฐํฐ๋ฅผ ์์ฑํ ์ด์ฐํค ์๋ต ๋ฐ๋ ๋ฐํ ์ฌ๋ถ ์ค์
-
enable-enum-translation : 'rest-messages'๋ผ๋ ํ๋กํผํฐ ํ์ผ์ ๋ง๋ค์ด ์ง์ ํ enum๊ฐ์ ์ฌ์ฉ
-
detection-strategy : ๋ ํฌ์งํ ๋ฆฌ ๋ ธ์ถ ์ ๋ต์ ์ค์ ํ๋ ํ๋กํผํฐ ๊ฐ
๐ก REST API ๊ตฌํ
๐ Board(๊ฒ์ํ) ๋ง๋ค๊ธฐ
- DataBase Name : board
- Columns
๐ DB์ REST API ํ๊ฒฝ ์ค์
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/[DB๋ช
]?serverTimezone=Asia/Seoul
username: [DB ID]
password: [DB PASSWORD]
driver-class-name: com.mysql.jdbc.Driver
data:
rest:
base-path: /api
default-page-size: 10
max-page-size: 10
- ์ฌ์ฉํ ์๋ฒ์ ํฌํธ ์ง์
- DB ์ค์ ๊ณผ REST API ํ๊ฒฝ ์ค์
๐ Board ํด๋์ค ์์ฑ ( JPA ์ฌ์ฉ )
JPA ๋?
JPA๋ DB ํ ์ด๋ธ๊ณผ ์๋ฐ ๊ฐ์ฒด ์ฌ์ด์ ๋งคํ์ ์ฒ๋ฆฌํด์ฃผ๋ ORM1์ด๋ ๊ธฐ์ ์ ํ์ค
์ฆ ์๋ฐ์ ํด๋์ค์ DB์ ํ ์ด๋ธ์ ๋งคํํ๋ ๊ธฐ์
package com.example.realboard;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Getter
@NoArgsConstructor
@Entity
@Table
public class Board implements Serializable {
@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;
@Column
private String title;
@Column
private String subTitle;
@Column
private String content;
@Column
private LocalDateTime createdDate;
@Column
private LocalDateTime updatedDate;
@Builder
public Board(String title, String subTitle, String content,
LocalDateTime createdDate, LocalDateTime updatedDate){
this.title = title;
this.subTitle = subTitle;
this.content = content;
this.createdDate = createdDate;
this.updatedDate = updatedDate;
}
public void setCreatedDataNow(){
this.createdDate = LocalDateTime.now();
}
public void setUpdatedDateNow(){
this.updatedDate = LocalDateTime.now();
}
public void update(Board board) {
this.title = board.getTitle();
this.subTitle = board.getSubTitle();
this.content = board.getContent();
this.updatedDate = LocalDateTime.now();
}
}
@Column ๊ณผ @Table์ ๊ฐ์ ์ด๋ ธํ ์ด์ ๋ค์ JPA์ ์ํ ์ ๋ ธํ ์ด์ ์ด๊ธฐ ๋๋ฌธ์
JPA ์ฌ์ฉ๋ฒ์ ์ฐธ๊ณ ํด์ฃผ์ธ์! (์ฐธ๊ณ : https://withseungryu.tistory.com/106 )
๐ BoardRepository ์์ฑ
- JPA์ ๊ธฐ์ ์ ์ฌ์ฉํ๊ธฐ ์ํด JpaRespository๋ฅผ ์์ํ๋ ํด๋์ค๋ฅผ ๋ง๋ค์ด์ค
package com.example.realboard;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface BoardRepository extends JpaRepository<Board, Long> {
}
๐ REST CONTROLLER ์์ฑ
- ์์ฒญ์ ๋ฐ๊ธฐ ์ํ ์ปจํธ๋กค๋ฌ
- @RepositoryRestController ์ด๋ ธํ ์ด์ ์ฌ์ฉ
- HATEOAS ์ ์ฉ (Hypermedia As The Engine Of Application State )
Hateoas : RESTful API๋ฅผ ์ฌ์ฉํ๋ ํด๋ผ์ด์ธํธ๊ฐ ์ ์ ์ผ๋ก ์๋ฒ์ ๋์ ์ธ ์ํธ์์ฉ์ด ๊ฐ๋ฅํ๋๋ก ํ๋ ๊ฒ
- ๋งคํํ๋ URL ํ์์ด spring-data-rest์์ ์ ์ํ๋ Rest API ํ์์ ๋ง์์ผํ๋ค.
- ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ URLํ์๊ณผ ๊ฐ๊ฒ ์ ๊ณตํด์ผ ํ๋ค.
package com.example.realboard;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resources;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@RepositoryRestController
public class BoardRestController {
private BoardRepository boardRepository;
public BoardRestController(BoardRepository boardRepository){
this.boardRepository = boardRepository;
}
@GetMapping("/boards")//api/boards๋ก URLํ์ ์ค๋ฒ๋ผ์ด๋
public @ResponseBody CollectionModel<Board> simpleBoard(@PageableDefault Pageable pageable){
Page<Board> boardList = boardRepository.findAll(pageable);
PagedModel.PageMetadata pageMetadata = new PagedModel.PageMetadata(pageable.getPageSize(), boardList.getNumber(),
boardList.getTotalElements());
//์ ์ฒด ํ์ด์ง ์, ํ์ฌ ํ์ด์ง ๋ฒํธ, ์ด ๊ฒ์ํ ์ ๋ฑ์ ํ์ด์ง ์ ๋ณด๋ฅผ ๋ด๋ pageMetadata
PagedModel<Board> resources = new PagedModel<>(boardList.getContent(), pageMetadata );
//์ปฌ๋ ์
์ ํ์ด์ง ๋ฆฌ์์ค ์ ๋ณด๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ์ ๊ณตํ๋ resources
resources.add(linkTo(methodOn(BoardRestController.class).simpleBoard(pageable)).withSelfRel());
//ํ์ํ ๋งํฌ ์ถ๊ฐ. ์ฌ๊ธฐ์๋ withSelfRel()๋ก 'self'๋ง ์ถ๊ฐ
return resources;
}
@PostMapping //POST ์์ฒญ ๋งคํ
public ResponseEntity<?> postBoard(@RequestBody Board board) {
board.setCreatedDataNow();
boardRepository.save(board);
return new ResponseEntity<>("{}", HttpStatus.CREATED);
}
@PutMapping("/{idx}") //PUT ์์ฒญ ๋งคํ
public ResponseEntity<?> putBoard(@PathVariable("idx")Long idx, @RequestBody Board board){
Board persistBoard = boardRepository.getOne(idx);
persistBoard.update(board);
boardRepository.save(persistBoard);
return new ResponseEntity<>("{}", HttpStatus.OK);
}
@DeleteMapping("/{idx}")//DELETE ์์ฒญ ๋งคํ
public ResponseEntity<?> deleteBoard(@PathVariable("idx")Long idx){
boardRepository.deleteById(idx);
return new ResponseEntity<>("{}", HttpStatus.OK);
}
}
- pageMetadata : ํ์ด์ง ์ ๋ณด๋ฅผ ๋ด๋ ๋ณ์
- resources : Board์ ํ์ด์ง ๋ฆฌ์์ค ์ ๋ณด๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ์ ๊ณตํ๋ ๋ณ์
- resources.add() : ํ์ํ ๋งํฌ๋ฅผ ์ถ๊ฐํด์ฃผ๋ ๋ฉ์๋
- withSelfRel() : 'self' ๋งํฌ๋ฅผ ์ถ๊ฐํด์ฃผ๋ ๋ฉ์๋
๐ ๊ฒฐ๊ณผ
๐ก๋ฐ์ํ ์๋ฌ
-
Hateoas์ ์ ๋ฐ์ดํธ๋ก ์ธํด ํ์ ํด๋์ค ์ค Resources, PageMetadata, PagedResource์ ๋ณ๊ฒฝ์ผ๋ก ์ธํ ์๋ฌ
ํด๊ฒฐ
- Resources ๋ CollectionModel ๋ก ๋ณ๊ฒฝ
- PageMetadata ๋ PagedModel.PageMetadate๋ก ๋ณ๊ฒฝ
- PagedResource ๋ PagedModel๋ก ๋ณ๊ฒฝ
-
Unknown column in 'field list' error on MySQL
ํด๊ฒฐ
- ํ๋ ๋ช ์ ์คํ๊ฐ ์๋์ง ํ์ธ
- DB์ ํด๋น ํ๋๊ฐ ์๋์ง ํ์ธ
- DB mysql์์ ์ปฌ๋ผ์ ์์ฑ์ Camel case(subTitle, updatedTime)์ด ์๋ Snake case(sub_title, updated_time)์ผ๋ก ๋ช ๋ช .
-
๋ณด์ ๋ฌธ์ (Security Problem)
๋ฐ์ ์์ธ
- HTTP์์ฒญ ์ ๋์ผ ์ถ์ฒ ์ ์ฑ ์ ์ฉ์ผ๋ก AJax ์์ฒญ ์คํจ
- ๊ต์ฐจ ์ถ์ฒ HTTP ์์ฒญ์ ๊ฐ๋ฅ์ผ ํด์ฃผ๋ ๋งค์ปค๋์ฆ ๊ต์ฐจ ์ถ์ฒ ์์ ๊ณต์ (CORS) ๊ถํ ๋ถ์ฌ ํ์
ํด๊ฒฐ
CORS ํ์ฉ ๋ฐ ์ํ๋ฆฌํฐ ์ค์
@SpringBootApplication๊ฐ ์๋ ํด๋์ค์ ์ถ๊ฐ
@Configuration @EnableGlobalMethodSecurity(prePostEnabled=true) //๋ฉ์๋ ๊ถํ ์ ํ @EnableWebSecurity //์น ์ํ๋ฆฌํฐ ํ์ฑํ static class SecurityConfiguration extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception{ CorsConfiguration configuration = new CorsConfiguration(); configuration.addAllowedOrigin(CorsConfiguration.ALL); configuration.addAllowedMethod(CorsConfiguration.ALL); configuration.addAllowedHeader(CorsConfiguration.ALL); //CORS์์ orgin,method,header ๋ณ๋ก ํ์ฉํ ๊ฐ์ ์ค์ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); //ํน์ ๊ฒฝ๋ก์ ์ ์ฉ, "/**" -> ๋ชจ๋ ๊ฒฝ๋ก๋ฅผ ๋ปํจ. http.httpBasic().and().authorizeRequests() .anyRequest().permitAll() .and().cors().configurationSource(source)//์์ ์ค์ ํ ๋ด์ฉ ์ํ๋ฆฌํฐ์ ์ ์ฉ .and().csrf().disable(); } }
resources.add(linkTo(methodOn(BoardRestController.class).simpleBoard(pageable)).withSelfRel());