Back-end/SpringBoot

[Spring Boot] Rest API ๊ตฌํ˜„ (Feat. spring date rest)

๐Ÿ’ก 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

Spring Data REST builds on top of Spring Data repositories, analyzes your application’s domain model and exposes hypermedia-driven HTTP resources for aggregates contained in the model.

spring.io

๐Ÿ” ์‚ฌ์šฉ๋ฒ• ์˜ˆ์‹œ

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 )

 

[Spring Boot] JPA๋ž€

๐Ÿง JPA๋ž€? ์ž๋ฐ” ๊ฐ์ฒด์™€ DBํ…Œ์ด๋ธ” ๊ฐ„์˜ ๋งคํ•‘์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ORM ํ‘œ์ค€ ๐Ÿ’ก ์š”์†Œ ์—”ํ‹ฐํ‹ฐ DB์—์„œ ์ง€์†์ ์œผ๋กœ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋ฐ” ๊ฐ์ฒด์— ๋งคํ•‘ ๋ฉ”๋ชจ๋ฆฌ ์ƒ์— ์ž๋ฐ” ๊ฐ์ฒด์˜ ์ธ์Šคํ„ด์Šค ํ˜•ํƒœ๋กœ ์กด์žฌํ•˜๋ฉฐ En

withseungryu.tistory.com

 

 

๐Ÿ” 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' ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋ฉ”์†Œ๋“œ

๐Ÿ” ๊ฒฐ๊ณผ

๐Ÿ’ก๋ฐœ์ƒํ•œ ์—๋Ÿฌ

  1. Hateoas์˜ ์—…๋ฐ์ดํŠธ๋กœ ์ธํ•ด ํ•˜์œ„ ํด๋ž˜์Šค ์ค‘ Resources, PageMetadata, PagedResource์˜ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ์—๋Ÿฌ

    ํ•ด๊ฒฐ

    • Resources ๋Š” CollectionModel ๋กœ ๋ณ€๊ฒฝ
    • PageMetadata ๋Š” PagedModel.PageMetadate๋กœ ๋ณ€๊ฒฝ
    • PagedResource ๋Š” PagedModel๋กœ ๋ณ€๊ฒฝ
  2. Unknown column in 'field list' error on MySQL

    ํ•ด๊ฒฐ

    • ํ•„๋“œ ๋ช…์— ์˜คํƒ€๊ฐ€ ์—†๋Š”์ง€ ํ™•์ธ
    • DB์— ํ•ด๋‹น ํ•„๋“œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
    • DB mysql์—์„œ ์ปฌ๋Ÿผ์„ ์ƒ์„ฑ์‹œ Camel case(subTitle, updatedTime)์ด ์•„๋‹Œ Snake case(sub_title, updated_time)์œผ๋กœ ๋ช…๋ช….
  3. ๋ณด์•ˆ ๋ฌธ์ œ (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());

๋ฐ˜์‘ํ˜•