Search

To Do List - BackEnd (SpringBoot)

To Do List REST API ์„œ๋ฒ„ ๊ตฌํ˜„

1.
To Do List ํ…Œ์ด๋ธ” ์ •์˜
2.
Spring Boot REST API ๊ตฌํ˜„
3.
API ๋ช…์„ธ์„œ ์ž๋™ ์ƒ์„ฑ - OpenAPI(Swagger)
4.
API ํ…Œ์ŠคํŠธ

To Do List ํ…Œ์ด๋ธ” ์ •์˜

todos.sql

DROP TABLE IF EXISTS `todos`; CREATE TABLE `todos` ( `no` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'PK', `id` VARCHAR(64) NOT NULL COMMENT 'UK', `name` TEXT NOT NULL COMMENT 'ํ• ์ผ', `status` BOOLEAN NOT NULL DEFAULT false COMMENT '์ƒํƒœ', `seq` INT NOT NULL DEFAULT 0 COMMENT '์ˆœ์„œ', `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp COMMENT '๋“ฑ๋ก์ผ์ž', `updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp COMMENT '์ˆ˜์ •์ผ์ž' );
SQL
๋ณต์‚ฌ
-- ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ TRUNCATE todos; INSERT INTO `todos` ( id, name, status ) VALUES ( UUID(), 'ํ•  ์ผ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ1', true ), ( UUID(), 'ํ•  ์ผ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ2', false ), ( UUID(), 'ํ•  ์ผ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ3', false ), ( UUID(), 'ํ•  ์ผ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ4', false ), ( UUID(), 'ํ•  ์ผ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ5', true ) ;
SQL
๋ณต์‚ฌ

Spring Boot REST API ๊ตฌํ˜„

์š”์ฒญ ๋ฉ”์†Œ๋“œ
์š”์ฒญ ๊ฒฝ๋กœ
ํ•ญ๋ชฉ
Response
๋น„๊ณ 
GET
/todos
ํ• ์ผ ๋ชฉ๋ก
{ โ€œlistโ€ : [], โ€œpaginationโ€ : }
GET
/todos?page=&size=
ํ• ์ผ ๋ชฉ๋ก ํŽ˜์ด์ง•
{ โ€œlistโ€ : [], โ€œpaginationโ€ : }
GET
/todos/{id}
ํ• ์ผ ์กฐํšŒ
{ }
POST
/todos
ํ• ์ผ ๋“ฑ๋ก
โ€œSUCCESSโ€ 201 โ€FAILโ€ 400
PUT
/todos
ํ• ์ผ ์ˆ˜์ •
โ€œSUCCESSโ€ 200 โ€FAILโ€ 400
DELETE
/todos/{id}
ํ• ์ผ ์‚ญ์ œ
โ€œSUCCESSโ€ 200 โ€FAILโ€ 400
โ€ข
domain
โ—ฆ
Todos.java
โ€ข
Mapper
โ—ฆ
TodoMapper.xml
โ—ฆ
BaseMapper.java
โ—ฆ
TodoMapper.java
โ€ข
Service
โ—ฆ
BaseService.java
โ—ฆ
TodoService.java
โ—ฆ
TodoServiceImpl.java
โ€ข
Controller
โ—ฆ
TodoController.java
โ—ฆ
@CrossOrigin("*")
โ—ฆ
@RestController
โ–ช
sp-crud

API ๋ช…์„ธ์„œ ์ž๋™ ์ƒ์„ฑ - OpenAPI(Swagger)

// Springdoc openapi implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
SQL
๋ณต์‚ฌ

build.gradle

plugins { id 'java' id 'war' id 'org.springframework.boot' version '3.4.1' id 'io.spring.dependency-management' version '1.1.6' } group = 'com.aloha' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // Springdoc openapi implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' } tasks.named('test') { useJUnitPlatform() }
SQL
๋ณต์‚ฌ

SwaggerConfig.java

import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; @Configuration public class SwaggerConfig { @Bean public GroupedOpenApi publicApi() { return GroupedOpenApi.builder() .group("aloha") // ๊ทธ๋ฃน๋ช… ์„ค์ • .pathsToMatch("/**") // ๊ฒฝ๋กœ ์„ค์ • .build(); } @Bean public OpenAPI springShopOpenAPI() { return new OpenAPI() .info(new Info().title("To do List Proejct API") .description("To do List ํ”„๋กœ์ ํŠธ API ์ž…๋‹ˆ๋‹ค.") .version("v0.0.1")); } }
Java
๋ณต์‚ฌ

๊ธฐ๋ณธ ๊ฒฝ๋กœ

PageHelper ๋ฅผ ์ด์šฉํ•œ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ

builde.gradle

// pagehelper implementation 'com.github.pagehelper:pagehelper-spring-boot-starter:2.1.0'
Java
๋ณต์‚ฌ

application.properites

# PageHelper ์„ค์ • pagehelper.helperDialect=mysql pagehelper.reasonable=true pagehelper.supportMethodsArguments=true pagehelper.params=count=countSql
Java
๋ณต์‚ฌ

Pagination.java

import lombok.Data; /** * [ํŽ˜์ด์ง•] * โœ… ํŽ˜์ด์ง€ ํ•„์ˆ˜ ์ •๋ณด * - ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ : page * - ํŽ˜์ด์ง€๋‹น ๊ฒŒ์‹œ๊ธ€ ์ˆ˜ : size * - ๋…ธ์ถœ ํŽ˜์ด์ง€ ๊ฐœ์ˆ˜ : count * - ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ : total * * โญ ํŽ˜์ด์ง€ ์ˆ˜์‹ ์ •๋ณด * - ์‹œ์ž‘ ๋ฒˆํ˜ธ : start * - ๋ ๋ฒˆํ˜ธ : end * - ์ฒซ ๋ฒˆํ˜ธ : first * - ๋งˆ์ง€๋ง‰ ๋ฒˆํ˜ธ : last * - ์ด์ „ ๋ฒˆํ˜ธ : prev * - ๋‹ค์Œ ๋ฒˆํ˜ธ : next * - ๋ฐ์ดํ„ฐ ์ˆœ์„œ ๋ฒˆํ˜ธ : index */ @Data public class Pagination { // ํŽ˜์ด์ง• ๊ธฐ๋ณธ๊ฐ’ private static final long PAGE_NUM = 1; // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๊ธฐ๋ณธ๊ฐ’ private static final long SIZES = 10; // ํŽ˜์ด์ง€๋‹น ๊ฒŒ์‹œ๊ธ€ ์ˆ˜ ๊ธฐ๋ณธ๊ฐ’ private static final long COUNT = 10; // ๋…ธ์ถœ ํŽ˜์ด์ง€ ๊ฐœ์ˆ˜ ๊ธฐ๋ณธ๊ฐ’ // โœ… ํ•„์ˆ˜ ์ •๋ณด private long page; private long size; private long count; private long total; // โญ ์ˆ˜์‹ ์ •๋ณด private long start; private long end; private long first; private long last; private long prev; private long next; private long index; // ์ƒ์„ฑ์ž public Pagination() { this(0); } // ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ public Pagination(long total) { this(PAGE_NUM, total); } // ํ˜„์žฌ ๋ฒˆํ˜ธ, ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ public Pagination(long page, long total) { this(page, SIZES, COUNT, total); } // ํ•„์ˆ˜ ์ •๋ณด public Pagination(long page, long size, long count, long total) { this.page = page; this.size = size; this.count = count; this.total = total; calc(); } // setter // * ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ์ง€์ • ํ›„, ํŽ˜์ด์ง€ ์ˆ˜์‹ ์žฌ๊ณ„์‚ฐ public void setTotal(long total) { this.total = total; calc(); } // ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ ์ˆ˜์‹ public void calc() { // ์ฒซ ๋ฒˆํ˜ธ this.first = 1; // ๋งˆ์ง€๋ง‰ ๋ฒˆํ˜ธ this.last = (this.total - 1) / size + 1; // ์‹œ์ž‘ ๋ฒˆํ˜ธ this.start = ( (page-1) / count ) * count + 1; // ๋ ๋ฒˆํ˜ธ this.end = ( (page-1) / count + 1 ) * count; if( this.end > this.last ) this.end = this.last; // ์ด์ „ ๋ฒˆํ˜ธ this.prev = this.page - 1; // ๋‹ค์Œ ๋ฒˆํ˜ธ this.next = this.page + 1; // ๋ฐ์ดํ„ฐ ์ˆœ์„œ ๋ฒˆํ˜ธ this.index = (this.page - 1) * this.size; } }
Java
๋ณต์‚ฌ

CORS(Cross-Origin Resource Sharing)

: ๋‹ค๋ฅธ ์ถœ์ฒ˜ (๋‹ค๋ฅธ URL ๋˜๋Š” PORT) ์—์„œ ์‹คํ–‰ํ•œ ์š”์ฒญ์— ๋Œ€ํ•˜์—ฌ ํ—ˆ์šฉํ•ด์ฃผ๋Š” ๋ณด์•ˆ ์ฒ˜๋ฆฌ ๋ฐฉ์‹
์„œ๋กœ ๋‹ค๋ฅธ ์ถœ์ฒ˜์˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐ„ ์ž์› ์š”์ฒญ์„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ œํ•œํ•˜๊ณ , ์„œ๋ฒ„๊ฐ€ ์ด๋ฅผ ํ—ˆ์šฉํ•˜๋„๋ก ์ œ์–ดํ•˜๋Š” ๋ณด์•ˆ ์ •์ฑ…
ํ”„๋ ˆ์ž„์›Œํฌ
ํฌํŠธ
URL
Spring Boot
8080
https://localhost:8080
React
3000, 5173
https://localhost:3000 https://localhost:5173
์„œ๋ฒ„ ํฌํŠธ๊ฐ€ ๋‹ค๋ฅธ ๋ฆฌ์•กํŠธ์—์„œ ์Šคํ”„๋ง ๋ถ€ํŠธ ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด์•ผ ํ•˜๋Š”๋ฐ, ๋ฐฑ์—”๋“œ์ธ ์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ ๋ฆฌ์•กํŠธ์—์„œ ๋ณด๋‚ธ ์š”์ฒญ์„ ํ—ˆ์šฉํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•์ด @CrossOrigin ์–ด๋…ธํ…Œ์ด์…˜์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด๊ณ , ์ด ๊ธฐ์ˆ ์ด CORS ์ž…๋‹ˆ๋‹ค.

@CrossOrigin

๋‹ค๋ฅธ ์ถœ์ฒ˜(Origin)์˜ ์š”์ฒญ์„ ํ—ˆ์šฉํ•˜๋Š” CORS ์„ค์ • ์–ด๋…ธํ…Œ์ด์…˜
์†์„ฑ
์„ค๋ช…
์˜ˆ์‹œ
origins
ํ—ˆ์šฉํ•  ์ถœ์ฒ˜(Origin)
"http://localhost:5173"
originPatterns
ํŒจํ„ด ๊ธฐ๋ฐ˜ ์ถœ์ฒ˜ ํ—ˆ์šฉ (์™€์ผ๋“œ์นด๋“œ ๊ฐ€๋Šฅ)
"http://localhost:*"
methods
ํ—ˆ์šฉํ•  HTTP ๋ฉ”์„œ๋“œ
{GET, POST}
allowedHeaders
ํ—ˆ์šฉํ•  ์š”์ฒญ ํ—ค๋”
"*"
exposedHeaders
ํด๋ผ์ด์–ธํŠธ์—์„œ ์ ‘๊ทผ ํ—ˆ์šฉํ•  ์‘๋‹ต ํ—ค๋”
"Authorization"
allowCredentials
์ฟ ํ‚ค/์ธ์ฆ์ •๋ณด ํ—ˆ์šฉ ์—ฌ๋ถ€
"true"
maxAge
preflight ์บ์‹œ ์‹œ๊ฐ„(์ดˆ)
3600
โ€ข
๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ
@CrossOrigin("*")
Java
๋ณต์‚ฌ
๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•˜์—ฌ ํ—ˆ์šฉ (๋ชจ๋“  URL, ๋ชจ๋“  PORT ํ—ˆ์šฉ)
โ€ข
ํŠน์ • ์š”์ฒญ ํ—ˆ์šฉ (๋‹จ์ผ)
@CrossOrigin( origins = "http://localhost:3000", methods = { RequestMethod.GET, RequestMethod.POST }, allowedHeaders = "*", allowCredentials = "true", maxAge = 3600 )
Java
๋ณต์‚ฌ
ํŠน์ • ์ถœ์ฒ˜(localhost:3000)์—์„œ๋งŒ ์š”์ฒญ์„ ํ—ˆ์šฉํ•˜๋ฉฐ, GET๊ณผ POST ๋ฉ”์„œ๋“œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ํ—ค๋”๋ฅผ ํ—ˆ์šฉํ•˜๊ณ , ์ฟ ํ‚ค/์ธ์ฆ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. Preflight ์š”์ฒญ ๊ฒฐ๊ณผ๋ฅผ 3600์ดˆ(1์‹œ๊ฐ„) ๋™์•ˆ ์บ์‹œํ•˜์—ฌ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.
โ€ข
ํŠน์ • ์š”์ฒญ ํ—ˆ์šฉ (๋‹ค์ˆ˜)
@CrossOrigin( origins = { "http://localhost:3000", "http://localhost:5173" } allowCredentials = "true", maxAge = 3600 )
Java
๋ณต์‚ฌ
์ด ์–ด๋…ธํ…Œ์ด์…˜์€ ์—ฌ๋Ÿฌ ์ถœ์ฒ˜(localhost:3000, localhost:5173)์—์„œ ๋ณด๋‚ด๋Š” ์š”์ฒญ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. allowCredentials = "true"๋Š” ์ฟ ํ‚ค๋‚˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ์„ ํ—ˆ์šฉํ•˜๋ฉฐ, maxAge = 3600์€ Preflight ์š”์ฒญ์˜ ๊ฒฐ๊ณผ๋ฅผ 3600์ดˆ(1์‹œ๊ฐ„) ๋™์•ˆ ์บ์‹œํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฐ˜๋ณต ์š”์ฒญ์„ ์ค„์ด๊ณ  ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์ฃผ๋กœ React ๊ฐœ๋ฐœ ์„œ๋ฒ„(3000๋ฒˆ ํฌํŠธ)์™€ Vite ๊ฐœ๋ฐœ ์„œ๋ฒ„(5173๋ฒˆ ํฌํŠธ) ์–‘์ชฝ์—์„œ ๋ฐฑ์—”๋“œ API์— ์ ‘๊ทผํ•ด์•ผ ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
โ€ข
์˜ˆ์‹œ
@CrossOrigin("*") @Slf4j @RestController @RequestMapping("/todos") @RequiredArgsConstructor public class TodoController { ... }
Java
๋ณต์‚ฌ