Covenant



๐Ÿš€ [๋กœ์ผ“ ํ•™์Šต] ์Šคํ”„๋ง๋ถ€ํŠธ CRUD REST API (JPA, MySQL, Gradle)





๋กœ์ผ“์ฒ˜๋Ÿผ ๋น ๋ฅด๊ฒŒ ๋งŒ๋‚˜๋Š” ์Šคํ”„๋ง๋ถ€ํŠธ!!


0. ์‹œ์ž‘ํ•˜๋ฉฐ


17๋…„ ์ฒ˜์Œ Django๋กœ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฐœ๋ฐœ์„ ์ ‘ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋‹น์‹œ ์žฅ๊ณ ๊ฑธ์ฆˆ ํŠœํ† ๋ฆฌ์–ผ์„ ๋”ฐ๋ผํ•˜๋ฉฐ ๊ณต๋ถ€ํ–ˆ์Šต๋‹ˆ๋‹ค. DB๋„ ๋ชจ๋ฅด๋˜ ์‹œ์ ˆ์ด๋ผ ORM๋„ ์ƒ์†Œํ•˜์˜€๊ณ  ํ…œํ”Œ๋ฆฟ ์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•œ ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋žœ๋”๋ง ๋ชจ๋“ ๊ฒŒ ๋‚ฏ์„ค์—ˆ์Šต๋‹ˆ๋‹ค. ์šฐ์„  ํŠœํ† ๋ฆฌ์–ผ์„ ๋”ฐ๋ผ์„œ ๊ฒŒ์‹œํŒ์„ ๋งŒ๋“ค์–ด๋ณด๊ณ  ๊ถ๊ธˆํ–ˆ๋˜ ๋ถ€๋ถ„, ํ”„๋กœ์ ํŠธ์‹œ ๋ง‰ํžˆ๋˜ ๋ถ€๋ถ„์„ ์ฐพ์•„์„œ ๊ณต๋ถ€ํ•˜๋Š”ํ•˜๋Š” ๋ฐฉ์‹์ด ๋„์›€์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Spring ๊ณต๋ถ€ ์ „๋žต๋„ ์ด์™€ ๋งˆ์ฐฌ๊ฐ€์ง€์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. Spring Boot ๊ณต๋ถ€์— ํ•œ์„ธ์›”, JPA ๊ณต๋ถ€์— ํ•œ์„ธ์›” ๊ทธ๋ฆฌ๊ณ  ๋ญ”๊ฐ€๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ ค๊ณ  ํ•˜๋ฉด ๋‹ค์‹œ ์ƒˆ๋กœ์šด๊ฒŒ ๋‚˜์˜ค๊ณ  ๋‹ค์‹œ ๊ณต๋ถ€ํ•˜๋Š”๋ฐ ํ•œ์„ธ์›”... ์ด๋Ÿฌ๋‹ค๊ฐ€ ํฅ๋ฏธ๋ฅผ ์žƒ์–ด๋ฒ„๋ฆฌ๊ฑฐ๋‚˜ ์ •๋…„ํ‡ด์ง์ด ๋ˆˆ ์•ž์— ๋‹ค๊ฐ€์˜ฌ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


๋„์„œ ์˜ˆ์•ฝ ์‹œ์Šคํ…œ API ๊ฐœ๋ฐœ์„ ํ†ตํ•˜์—ฌ ์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ ๊ธฐ๋ณธ์ ์ธ ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ, ์กฐํšŒ์— ๋Œ€ํ•œ API๋ฅผ ๋งŒ๋“ค๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ค‘๊ฐ„์— ์žˆ๋Š” ์„ค๋ช…์„ ์ฐธ๊ณ ํ•˜์—ฌ ๊ฐ„๋‹จํ•œ API๋ฅผ ๋งŒ๋“ค์–ด๋ณด๊ณ  ๋™์ž‘์„ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•˜๋ฉด์„œ ํ•˜๋‚˜์”ฉ ํŒŒ์•…ํ•ด ๋‚˜๊ฐ€๋Š” Top Down ๊ณต๋ถ€๋ฒ•์œผ๋กœ Spring Boot๋ฅผ ์ •๋ณตํ•ด๋ณด์„ธ์š”.


์ˆ˜๋ก๋œ ์ฝ”๋“œ๋Š” javatodev.com๋ฅผ ๋”ฐ๋ž์ง€๋งŒ ์„ค๋ช…์€ ๊ณต๊ฐ์ด ๋˜์ง€ ์•Š์€ ๋ถ€๋ถ„์ด ์žˆ๊ธฐ์— ์ œ๊ฐ€ ์ƒˆ๋กœ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.


๊ธ€์—์„œ ์‚ฌ์šฉ๋œ ์ „์ฒด ์ฝ”๋“œ๋Š” Github์—์„œ ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.




1. spring initializr๋กœ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘!


spring initializer๋Š” ์ดˆ๊ธฐ ์„ค์ •๋œ ์Šคํ”„๋ง ๋ถ€ํŠธ ํ”„๋กœ์ ํŠธ๋ฅผ ์›ํ•˜๋Š” ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์›น ์‚ฌ์ดํŠธ์ž…๋‹ˆ๋‹ค. start.spring.io ์—์„œ ํ•˜๋‹จ์˜ ์Šคํฌ๋ฆฐ์ƒท์ฒ˜๋Ÿผ ์ž…๋ ฅํ•œ ํ›„ GENERATE๋ฅผ ํด๋ฆญํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.





๋ณธ ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•ด์„œ ๋„ค ๊ฐœ์˜ ์Šคํ”„๋ง๋ถ€ํŠธ ์˜์กด์„ฑ์€ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ ์—ญํ™œ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.


  • Spring Web: HTTPํด๋ผ์ด์–ธํŠธ์™€ Spring์˜ ์›๊ฒฉ ์ง€์›์„ ์œ„ํ•œ ์›น ๊ด€๋ จ ๋ถ€๋ถ„์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • Spring Data JPA: JPA๊ธฐ๋ฐ˜ repository๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • MySQL Driver: MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ๋“œ๋ผ์ด๋ฒ„์ž…๋‹ˆ๋‹ค.
  • Lombok: ๋ฐ˜๋ณต์ ์ธ ๊ฐœ๋ฐœ์„ ์ค„์ผ ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์ž๋ฐ” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. (Ref. lombok)

์œ„์˜ ์ด๋ฏธ์ง€์™€ ๊ฐ™์ด ์„ค์ • ํ›„ GENERATE๋ฅผ ํด๋ฆญํ•˜๋ฉด ์••์ถ•ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. ์••์ถ•์„ ํ‘ผ ํ›„ IDE๋กœ ์—ด๊ณ  build.gradle ํŒŒ์ผ์„ ์—ด์–ด์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์˜์กด์„ฑ์ด ์ถ”๊ฐ€๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

2. ๋งŒ๋“ค์–ด๋ณผ API ์‚ดํŽด๋ณด๊ธฐ


๋ณธ ๊ธ€์—์„œ๋Š” CRUD API๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. CRUD๋ž€ Create, Read, Update, Delete์˜ ์•ž ๊ธ€์ž๋ฅผ ๋”ด ์•ฝ์ž์ž…๋‹ˆ๋‹ค. ์›น ์• ํ”Œ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๊ธฐ ์œ„ํ•ด์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ, ์ฝ๊ธฐ์™€ ๊ฐ™์€ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ณธ ์˜ˆ์ œ์—์„œ CRUD API๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ์Šคํ”„๋ง ๋ถ€ํŠธ๋กœ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ผˆ๋Œ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. API URI์™€ ์ƒ์„ธ ๊ธฐ๋Šฅ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.


ใ…คใ…คใ…คใ…คใ…คใ…คใ…คใ…ค URI ใ…คHTTP ๋ฉ”์„œ๋“œใ…ค ์„ค๋ช…
ใ…ค /api/library/book GET ๋„์„œ ์ „์ฒด ์กฐํšŒ
ใ…ค /api/library/book?isbn=1919 ใ…ค GET ๋„์„œ ISBN์œผ๋กœ ๋„์„œ ์กฐํšŒ
ใ…ค /api/library/book/:id GET ๋„์„œID๋กœ ๋„์„œ ์กฐํšŒ
ใ…ค /api/library/book POST ๋„์„œ ๋“ฑ๋ก
ใ…ค /api/library/book/:id DELETE ๋„์„œ ์‚ญ์ œ
ใ…ค /api/library/book/lend POST ๋„์„œ ๋Œ€์ถœ
ใ…ค /api/library/member POST ํšŒ์› ๋“ฑ๋ก
ใ…ค /api/library/member/:id PATCH ํšŒ์› ์ˆ˜์ •




์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค๊ณ ์žํ•˜๋Š” ์Šคํ”„๋ง ๋ถ€ํŠธ ํ”„๋กœ์ ํŠธ์˜ ์•„ํ‚คํ…์ฒ˜๋Š” ์œ„์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.



3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •



3-1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ


MySQL์„ ์„ค์น˜ํ•˜์—ฌ ์ฃผ์‹œ๊ณ  ํ„ฐ๋ฏธ๋„์— ๋‹ค์Œ์˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ MySQL์— ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค.

$ mysql -uroot -p

root ๊ณ„์ •์œผ๋กœ ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค.


mysq> create database covenant;
Query OK, 1 row affected (0.02 sec)

covenant๋ผ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•ด์ค๋‹ˆ๋‹ค.


mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| covenant           |
| db_example         |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| test               |
+--------------------+

MySQL์— ๊ธฐ๋ณธ์ ์œผ๋กœ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ถ”๊ฐ€์ ์œผ๋กœ covenant ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ƒ์„ฑ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



3-2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘์† ์ •๋ณด ์ถ”๊ฐ€


์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ MySQL์— ์ ‘์†ํ•˜๊ธฐ ์œ„ํ•ด์„œ application.properties์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘์† ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/covenant
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
/src/main/resource/application.properties

์ตœ๊ทผ ํ”„๋กœ์ ํŠธ๋Š” ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด์„œ yml ํ˜•์‹์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์œ„์™€ ๋™์ผํ•œ ํ˜•ํƒœ์˜ yml ์„ค์ • ์ •๋ณด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/covenant
    username: root
    password: password
  jpa:
    hibernate:
      ddl-auto: create
/src/main/resource/application.yml

spring initializer ์ƒ์„ฑ์‹œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ํŒŒ์ผ์ผ์ธ application.properties๊ณผ ๋‹ค๋ฅด๊ฒŒ application.yml์€ ์—†์Šต๋‹ˆ๋‹ค. application.properties์™€ ๊ฐ™์€ ๊ฒฝ๋กœ์— application.yml ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.


์„ค์ • ์ •๋ณด์— ์ €์žฅํ•œ ์†์„ฑ๊ฐ’์˜ ์˜๋ฏธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • spring.datasource.url: MySQL์˜ ํ˜ธ์ŠคํŠธ ์ด๋ฆ„, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ์— ์ ‘์†ํ•˜๊ธฐ์— 127.0.0.1์„, MySQL ์„ค์น˜์‹œ ๊ธฐ๋ณธ ํฌํŠธ ๋ฒˆํ˜ธ์ธ 3306, ๊ทธ๋ฆฌ๊ณ  ์ƒ์„ฑํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ธ covenant๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
  • spring.datasource.username: MySQL username
  • spring.datasource.password: MySQL password
  • spring.jpa.hibernate.ddl-auto: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ์— ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•  ๊ฒƒ์ธ์ง€, ๋ณ€๊ฒฝ์ ๋งŒ ์ˆ˜์ •ํ•  ๊ฒƒ์ธ์ง€ ๋“ฑ๋“ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


3-3. spring.jpa.hibernate.ddl-auto ๋ž€?


spring.jpa.hibernate.ddl-auto์— ์„ค์ •๋œ ๊ฐ’์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.


  • create: Spring Boot๊ฐ€ ์‹คํ–‰๋˜๋ฉด ํ…Œ์ด๋ธ”์„ ์ง€์šฐ๊ณ  ์ƒˆ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๊ธฐ์กด์— ํ…Œ์ด๋ธ”์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋Š” ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.
  • create-drop: Spring Boot๊ฐ€ ์ข…๋ฃŒ๋˜๋ฉด ํ…Œ์ด๋ธ”์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
  • update: Entity ํด๋ž˜์Šค์™€ DB ์Šคํ‚ค๋งˆ๋ฅผ ๋น„๊ตํ•˜์—ฌ DB์— ์ƒ์„ฑ๋˜์ง€ ์•Š์€ ํ…Œ์ด๋ธ”, ์นผ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜๋ฉฐ ์ œ๊ฑฐ๋Š” ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • validate: Entity ํด๋ž˜์Šค์™€ DB ์Šคํ‚ค๋งˆ๋ฅผ ๋น„๊ตํ•˜์—ฌ ๋‹ค๋ฅด๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.
  • none: ์•„๋ฌด๊ฒƒ๋„ ์‹คํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


4. Model Class ์ƒ์„ฑ



4-1. ์ €์ž Model

package com.covenant.springbootmysql.model;

import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "author")
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
}
/model/Author.java

์ €์ž์˜ Model์€ PK(Primary Key)์™€ firstName, lastNameํ•„๋“œ๋ฅผ ๊ฐ–์Šต๋‹ˆ๋‹ค.



4-2. ๋„์„œ Model

package com.covenant.springbootmysql.model;

import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "book")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String isbn;
}
/model/Book.java

๋„์„œ ๋ชจ๋ธ์€ PK ๋„์„œ๋ช…, ISBN๋ฒˆํ˜ธ ํ•„๋“œ๋ฅผ ๊ฐ–์Šต๋‹ˆ๋‹ค.



4-3. ํšŒ์› Model

package com.covenant.springbootmysql.model;

import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;

@Getter
@Setter
@Entity
@Table(name = "member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;

    @Enumerated(EnumType.STRING)
    private MemberStatus status;
}
/model/Member.java

ํšŒ์› ๋ชจ๋ธ์€ PK(Primary Key)์™€ firstName, lastName ๊ทธ๋ฆฌ๊ณ  ํšŒ์› ์ƒํƒœ๋ฅผ ์•Œ๋ ค์ฃผ๋Š” Enum ํ•„๋“œ๋ฅผ ๊ฐ–์Šต๋‹ˆ๋‹ค.


package com.covenant.springbootmysql.model;

public enum MemberStatus {
    ACTIVE, DEACTIVATED
}
/model/MemberStatus.java

Member ๋ชจ๋ธ์˜ status ํ•„๋“œ์— ๋“ค์–ด๊ฐˆ MemberStatus enum ํƒ€์ž…์€ ํšŒ์› ํ™œ์„ฑํšŒ์›, ๋น„ํ™œ์„ฑ ํšŒ์›์œผ๋กœ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.



4-4. ๋Œ€์ถœ Model

package com.covenant.springbootmysql.model;

import java.time.Instant;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "lend")
public class Lend {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Instant startOn;
    private Instant dueOn;
    @Enumerated(EnumType.ORDINAL)
    private LendStatus status;
}
/model/Lend.java

Lend ๋ชจ๋ธ์€ ๋Œ€์ถœ์‹œ์ž‘, ๋Œ€์ถœ์ข…๋ฃŒ ๊ทธ๋ฆฌ๊ณ  ๋Œ€์ถœ์ƒํƒœ ํ•„๋“œ๋ฅผ ๊ฐ–์Šต๋‹ˆ๋‹ค.


package com.covenant.springbootmysql.model;

public enum LendStatus {
    AVAILABLE, BURROWED
}
/model/LendStatus.java

Lend ๋ชจ๋ธ์˜ status ํ•„๋“œ์— ๋“ค์–ด๊ฐˆ LendStatus enum ํƒ€์ž…์€ ๋Œ€์ถœ๊ฐ€๋Šฅ, ๋Œ€์ถœ์ค‘์œผ๋กœ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.




5. Model Class๊ฐ„์˜ ๊ด€๊ณ„ ์ •์˜



5-1. Author์™€ Book (One to Many)


์ €์ž์™€ ๋„์„œ์˜ ๊ด€๊ณ„๋Š” 1:N๊ด€๊ณ„์ž…๋‹ˆ๋‹ค. ํ•œ ์ €์ž๊ฐ€ ์—ฌ๋Ÿฌ ์ฑ…์„ ์“ธ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋ฌผ๋ก  ๊ณต์ €(์—ฌ๋Ÿฌ ์ €์ž๊ฐ€ ํ•˜๋‚˜์˜ ์ฑ…์„ ์ €์ˆ )๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ์œ„ํ•ด์„œ ๊ณต์ €์˜ ๊ฒฝ์šฐ๋Š” ๋ฐฐ์ œํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


@ManyToOne
@JoinColumn(name = "author_id")
@JsonManagedReference
private Author author;
/model/Book.java (์ฝ”๋“œ ์ถ”๊ฐ€)

@JsonBackReference
@OneToMany(mappedBy = "author", 
           fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Book> books;
/model/Author.java (์ฝ”๋“œ ์ถ”๊ฐ€)



5-2. Book์™€ Lend (One to Many)


๊ฐ™์€ ์ฑ…์ด ํ’๋ถ€ํ•œ ๋„์„œ๊ด€์ด์—ฌ์„œ ํ•˜๋‚˜์˜ ์ฑ…์„ ์—ฌ๋Ÿฌ๋ช…์ด ๋Œ€์ถœํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฐ€์ƒ์˜ ์ƒํ™ฉ์„ ๊ฐ€์ •ํ•˜๊ณ˜์Šต๋‹ˆ๋‹ค. ์ฑ…๊ณผ ๋Œ€์ถœ์€ 1:N ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค.

@JsonBackReference
@OneToMany(mappedBy = "book", 
           fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Lend> lends;
/model/Book.java (์ฝ”๋“œ ์ถ”๊ฐ€)

@ManyToOne
@JoinColumn(name = "book_id")
@JsonManagedReference
private Book book;
/model/Lend.java (์ฝ”๋“œ ์ถ”๊ฐ€)


5-3. Member์™€ Lend (One to Many)


ํ•œ ๋ช…์˜ ํšŒ์›์ด ๋„์„œ ์—ฌ๋Ÿฌ๊ถŒ์„ ๋Œ€์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํšŒ์›๊ณผ ๋Œ€์ถœ์€ 1:N๊ด€๊ณ„์ž…๋‹ˆ๋‹ค.

@JsonBackReference
@OneToMany(mappedBy = "member",
          fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Lend> lends;
/model/Member.java (์ฝ”๋“œ ์ถ”๊ฐ€)

@ManyToOne
@JoinColumn(name = "member_id")
@JsonManagedReference
private Member member;
/model/Lend.java (์ฝ”๋“œ ์ถ”๊ฐ€)

์œ„์˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ ์—”ํ‹ฐํ‹ฐ๊ฐ„์˜ ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘์„ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค. ์œ„์—์„œ ์‚ฌ์šฉํ•œ ์–ด๋…ธํ…Œ์ด์…˜๊ณผ ํŒŒ๋ฏธํ„ฐ์˜ ์˜๋ฏธ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


  • @OneToMany: ์ผ๋‹ค๋‹ค ๊ด€๊ณ„ ๋งคํ•‘์ž…๋‹ˆ๋‹ค. mappedBy๋ฅผ ํ†ตํ•ด์„œ ์—ฐ๊ด€๊ด€๊ณ„ ์ฃผ์ธ ํ•„๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
  • @ManyToOne: OneToMany์˜ ๋ฐ˜๋Œ€ ๊ด€๊ณ„์ธ N:1 ๊ด€๊ณ„ ๋งคํ•‘ ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค.
  • @JoinColumn: ์™ธ๋ž˜ํ‚ค๋ฅผ ๋งคํ•‘ํ• ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. name์—๋Š” ์ฐธ์กฐํ•˜๋Š” ํ…Œ์ด๋ธ”์˜ ๊ธฐ๋ณธํ‚ค ์นผ๋Ÿผ๋ช…์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.
  • cascade: JPA์—๋Š” ์˜์†ํ™”๋ž€ ๊ฐœ๋…์ด ์žˆ์Šต๋‹ˆ๋‹ค. CascadeType.ALL์ด๋ฉด ๋ถ€๋ชจ๊ฐ€ ์˜์†ํ™”๊ฐ€ ๋˜๋ฉด ์ž์‹๋„ ์˜์†ํ™”๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ •ํ™•ํ•œ ํ‘œํ˜„์€ ์•„๋‹ˆ์ง€๋งŒ ๋ถ€๋ชจ์™€ ์ž์‹์˜ ์ƒํƒœ๊ฐ€ ๋™์‹œ์— ๋ณ€ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
  • fetch: EAGER, LAZY๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค.
    • EAGER(์ฆ‰์‹œ๋กœ๋”ฉ): ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์„ค์ •๋œ ๋ชจ๋“  ํ…Œ์ด๋ธ”์— ๋Œ€ํ•ด์„œ ์กฐ์ธ์ด ์ด๋ค„์ง‘๋‹ˆ๋‹ค.
    • LAZY(์ง€์—ฐ๋กœ๋”ฉ): ๊ธฐ๋ณธ์ ์œผ๋กœ ์—ฐ๊ด€๊ด€๊ณ„ ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•˜์ง€ ์•Š๊ณ  ์กฐ์ธ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— Join์„ ํ•ฉ๋‹ˆ๋‹ค.



5-4. JPA Cascade Types


์ €์ž๋ฅผ ์‚ญ์ œํ•˜๋ฉด ์ €์ž๊ฐ€ ์“ด ์ฑ…์„ ์‚ญ์ œํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ Cascade Type ์„ค์ •์œผ๋กœ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ํƒ€์ž…์„ ์ดํ•ดํ•˜๋ ค๋ฉด JPA์˜ persist, merge, detach ๋“ฑ์˜ ๊ฐœ๋…์„ ์•Œ์•„์•ผํ•˜๊ธฐ์— ALL๊ณผ REMOVE๋งŒ ์ •๋ฆฌํ•˜๊ณ  ๋„˜์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค.


  • ALL: ๋ชจ๋“  Cascade(PERSIST, MERGE, REMOVE, REFRESH, DETACH)๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • REMOVE: ์‚ญ์ œ์‹œ ์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ ๊ฐ™์ด ์‚ญ์ œ



5-5. JPA ๋ชจ๋ธ ํด๋ž˜์Šค์—์„œ Enum ํ™œ์šฉ


์œ„์—์„œ @Enumerated(EnumType.STRING)๊ณผ @Enumerated(EnumType.ORDINAL) ๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ์˜๋ฏธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.


  • EnumType.ORDINAL: enum ์ˆœ์„œ ๊ฐ’์„ DB์— ์ €์žฅ
  • EnumType.STRING: enum ์ด๋ฆ„์„ DB์— ์ €์žฅ

๋Œ€๋ถ€๋ถ„์˜ ์ƒํ™ฉ์—์„œ STRING ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

public enum LendStatus {
    AVAILABLE, BURROWED
}

์—ฌ๊ธฐ์„œ ์˜ˆ์•ฝ ์ƒํƒœ๊ฐ€ ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ AVAILABLE ์ƒํƒœ๊ฐ€ ์‚ญ์ œ๋˜๋ฉด ์ˆซ์ž ๊ฐ’์„ ์ถ”๊ฐ€ํ•œ ORDINAL์˜ ๊ฒฝ์šฐ ์ž˜๋ชป๋œ ๊ฐ’์ด ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค. STRING์ธ ๊ฒฝ์šฐ ์ค‘๋ณต๋˜๋Š” ์ •๋ณด๊ฐ€ DB์— ์ €์žฅ๋˜์–ด์•ผํ•˜๋ฏ€๋กœ ๋‚ญ๋น„๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Attribute Converter๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ฐธ๊ณ . https://lng1982.tistory.com/279)




5-6. JsonBackReference, JsonManagedReference ์ด๋ž€?


JPA๋กœ ์—ฐ๊ด€๊ด€๊ณ„ ์ž‘์—…์‹œ ์—ฐ๊ด€๊ด€๊ณ„๊ฐ„์— ์„œ๋กœ๋ฅผ ๋ฌดํ•œ์œผ๋กœ ํ˜ธ์ถœํ•˜๋Š” ํ˜„์ƒ์ด ์ƒ๊น๋‹ˆ๋‹ค. ์ˆœํ™˜์ฐธ์กฐ๋ฅผ ๋ฐฉ์–ดํ•˜๊ธฐ ์œ„ํ•œ ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. ๋ถ€๋ชจ ํด๋ž˜์Šค์— @JsonManagedReference, ์ž์‹ ํด๋ž˜์Šค์— @JsonBackReference ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด๋ฉ๋‹ˆ๋‹ค.




6. Repository๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž


JPARepository๋ฅผ ์ด์šฉํ•ด์„œ SQL ์ž‘์„ฑ ์—†์ด ๊ธฐ๋ณธ์ ์ธ CRUD๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@NoRepositoryBean
public interface JpaRepository<T, ID> 
    extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
JpaRepository.java

JpaRepository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ณด๋ฉด CRUD ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ PagingSorting์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. JpaRepository์—์„œ ์ง€์›ํ•˜๋Š” ๊ธฐ๋ณธ์ ์ธ ๋ฉ”์†Œ๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.


  • save(): ๋ ˆ์ฝ”๋“œ ์ €์žฅ
  • findOne(): PK๋กœ ๋ ˆ์ฝ”๋“œ ํ•œ ๊ฑด ์ฐพ๊ธฐ
  • findAll(): ์ „์ฒด ๋ ˆ์ฝ”๋“œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
  • count(): ๋ ˆ์ฝ”๋“œ ๊ฐฏ์ˆ˜
  • delete(): ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ


6-1. AuthorRepository

package com.covenant.springbootmysql.repository;

import com.covenant.springbootmysql.model.Author;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AuthorRepository extends JpaRepository<Author, Long> {
}
/repository/AuthorRepository.java

JpaRepository๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ JpaRepository์— ์—”ํ‹ฐํ‹ฐ์™€ ID๋ฅผ ์ง€์ •ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€ Book, Lend ๊ทธ๋ฆฌ๊ณ  Member๋Š” ์œ„ ์ฝ”๋“œ์˜ ๋ฐ˜๋ณต์ž…๋‹ˆ๋‹ค.



6-2. BookRepository

package com.javatodev.api.repository;

import com.javatodev.api.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface BookRepository extends JpaRepository<Book, Long> {
    Optional<Book> findByIsbn(String isbn);
}
/repository/BookRepository.java

JpaRepository์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ ์ด์™ธ์— ์ถ”๊ฐ€์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์€ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด findBy๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์†Œ๋“œ ์ด๋ฆ„์œผ๋กœ ์ฟผ๋ฆฌ๋ฅผ ์š”์ฒญํ•˜๋Š” ๋ฉ”์†Œ๋“œ์ž„์„ ์ง€์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.



6-3. LendRepository

package com.javatodev.api.repository;

import com.javatodev.api.model.Book;
import com.javatodev.api.model.Lend;
import com.javatodev.api.model.LendStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface LendRepository extends JpaRepository<Lend, Long> {
    Optional<Lend> findByBookAndStatus(Book book, LendStatus status);
}
/repository/LendRepository.java

์—ฌ๋Ÿฌ ํ•„๋“œ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด And๋กœ ์—ฐ๊ฒฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. findByBookAndStatus๋Š” Book๊ณผ Status ํ•„๋“œ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. ์ง€์›ํ•˜๋Š” ๋ฉ”์†Œ๋“œ ์ด๋ฆ„์œผ๋กœ ํ‚ค์›Œ๋“œ ์ง€์ •ํ•˜๋Š” ๋ฐฉ์‹์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ๋‚ด์šฉ์€ docs.spring.io ๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.



6-4. MemberRepository

package com.javatodev.api.repository;

import com.javatodev.api.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
}
/repository/MemberRepository.java



7. Service๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž


๋ณธ ์ฝ”๋“œ์—์„œ๋Š” LibraryService์— ๋งŒ๋“ค๊ณ ์ž ํ•˜๋Š” ๋„์„œ๋Œ€์ถœ API์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ถ”๊ฐ€ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ์„œ๋น„์Šค ๋กœ์ง์ด ์žˆ๋‹ค๋ฉด Service ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.



7-1. LibraryService

package com.covenant.springbootmysql.service;

import com.covenant.springbootmysql.repository.AuthorRepository;
import com.covenant.springbootmysql.repository.BookRepository;
import com.covenant.springbootmysql.repository.LendRepository;
import com.covenant.springbootmysql.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LibraryService {

    private final AuthorRepository authorRepository;
    private final MemberRepository memberRepository;
    private final LendRepository lendRepository;
    private final BookRepository bookRepository;

}
/service/LibraryService.java

@RequiredArgsConstructor๋Š” lombok์ด ์ดˆ๊ธฐํ™” ๋˜์ง€ ์•Š์€ ํ•„๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด์„œ ์˜์กด์„ฑ ์ฃผ์ž…(Dependency Injection)์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



7-2 LibraryService ์ถ”๊ฐ€ ์ฝ”๋“œ

public Book readBook(Long id) {
    Optional<Book> book = bookRepository.findById(id);
    if (book.isPresent()) {
        return book.get();
    }

    throw new EntityNotFoundException(
                "Cant find any book under given ID");
}

public List<Book> readBooks() {
    return bookRepository.findAll();
}

public Book readBook(String isbn) {
    Optional<Book> book = bookRepository.findByIsbn(isbn);
    if (book.isPresent()) {
        return book.get();
    }

    throw new EntityNotFoundException(
                "Cant find any book under given ISBN");
}

public Book createBook(BookCreationRequest book) {
    Optional<Author> author = authorRepository.findById(book.getAuthorId());
    if (!author.isPresent()) {
        throw new EntityNotFoundException(
                    "Author Not Found");
    }

    Book bookToCreate = new Book();
    BeanUtils.copyProperties(book, bookToCreate);
    bookToCreate.setAuthor(author.get());
    return bookRepository.save(bookToCreate);
}

public void deleteBook(Long id) {
    bookRepository.deleteById(id);
}

public Member createMember(MemberCreationRequest request) {
    Member member = new Member();
    BeanUtils.copyProperties(request, member);
    return memberRepository.save(member);
}

public Member updateMember (Long id, MemberCreationRequest request) {
    Optional<Member> optionalMember = memberRepository.findById(id);
    if (!optionalMember.isPresent()) {
        throw new EntityNotFoundException(
                    "Member not present in the database");
    }

    Member member = optionalMember.get();
    member.setLastName(request.getLastName());
    member.setFirstName(request.getFirstName());
    return memberRepository.save(member);
}

public Author createAuthor (AuthorCreationRequest request) {
    Author author = new Author();
    BeanUtils.copyProperties(request, author);
    return authorRepository.save(author);
}

public List<String> lendABook (List<BookLendRequest> list) {
    List<String> booksApprovedToBurrow = new ArrayList<>();
    list.forEach(bookLendRequest -> {
        Optional<Book> bookForId = 
                bookRepository.findById(bookLendRequest.getBookId());
        if (!bookForId.isPresent()) {
            throw new EntityNotFoundException(
                        "Cant find any book under given ID");
        }

        Optional<Member> memberForId = 
                         memberRepository.findById(bookLendRequest.getMemberId());
        if (!memberForId.isPresent()) {
            throw new EntityNotFoundException(
                        "Member not present in the database");
        }

        Member member = memberForId.get();
        if (member.getStatus() != MemberStatus.ACTIVE) {
            throw new RuntimeException(
                        "User is not active to proceed a lending.");
        }

        Optional<Lend> burrowedBook = 
        lendRepository.findByBookAndStatus(
                       bookForId.get(), LendStatus.BURROWED);

        if (!burrowedBook.isPresent()) {
            booksApprovedToBurrow.add(bookForId.get().getName());
            Lend lend = new Lend();
            lend.setMember(memberForId.get());
            lend.setBook(bookForId.get());
            lend.setStatus(LendStatus.BURROWED);
            lend.setStartOn(Instant.now());
            lend.setDueOn(Instant.now().plus(30, ChronoUnit.DAYS));
            lendRepository.save(lend);
        }
    });

    return booksApprovedToBurrow;
}
/service/LibraryService.java ์ฝ”๋“œ ์ถ”๊ฐ€

  • readBookById(String id): id๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋„์„œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  • readBooks(): ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋œ ๋ชจ๋“  ๋„์„œ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค.
  • createBook(BookCreationRequest book): BookCreationRequest๋กœ ๋„์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • deleteBook(String id): id๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋„์„œ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
  • createMember(MemberCreationRequest request): MemberCreationRequest๋กœ ํšŒ์›์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • updateMember (String id, MemberCreationRequest request): id์— ํ•ด๋‹นํ•˜๋Š” ํšŒ์›์„ Member Creation Request๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
  • createAuthor (AuthorCreationRequest request): AuthorCreationRequest๋กœ ์ €์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • lendABook (BookLendRequest request): BookLendRequest๋กœ ๋„์„œ๋ฅผ ๋Œ€์ถœํ•ฉ๋‹ˆ๋‹ค.

Service์—์„œ ์ธ์ž๋กœ ์‚ฌ์šฉํ•˜๋Š” Request ์— ๋Œ€ํ•œ class๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค.




7-3. AuthorCreationRequest


Request ํด๋ž˜์Šค๋Š” lombok์—์„œ ์ง€์›ํ•˜๋Š” @Data ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. @Data๋Š” Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode ์–ด๋…ธํ…Œ์ด์…˜์„ ํฌํ•จํ•œ ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด Request ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ด์ค๋‹ˆ๋‹ค.


package com.covenant.springbootmysql.model.request;

import lombok.Data;

@Data
public class AuthorCreationRequest {
    private String firstName;
    private String lastName;
}
/model/request/AuthorCreationRequest.java


7-4. BookCreationRequest

package com.covenant.springbootmysql.model.request;

import lombok.Data;

@Data
public class BookCreationRequest {
    private String name;
    private String isbn;
    private Long authorId;
}
/model/request/BookCreationRequest.java


7-5. BookLendRequest

package com.covenant.springbootmysql.model.request;

import java.util.List;

import lombok.Data;

@Data
public class BookLendRequest {
    private List<Long> bookIds;
    private Long memberId;
}
/model/request/BookLendRequest.java


7-6. MemberCreationRequest

package com.covenant.springbootmysql.model.request;

import lombok.Data;

@Data
public class MemberCreationRequest {
    private String firstName;
    private String lastName;
}
/model/request/MemberCreationRequest.java



8. Controller๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž

package com.covenant.springbootmysql.controller;

import com.javatodev.api.service.LibraryService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping(value = "/api/library")
@RequiredArgsConstructor
public class LibraryController {
    private final LibraryService libraryService;    
}
/controller/LibraryController.java

RequestMapping์€ URL์„ ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋ฉ”์†Œ๋“œ์™€ ๋งคํ•‘ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์Šคํ”„๋งํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜ ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. /api/library ๊ฐ’์„ ์ฃผ์–ด์„œ ํ˜„์žฌ ์ปจํŠธ๋กค๋Ÿฌ์— ๋ฉ”๋„์Šค์™€ ๋งคํ•‘๋˜๋Š” URL์˜ ๊ณตํ†ต๋„์ƒ์œ„ ๊ฒฝ๋กœ๋ฅผ /api/library๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.



@GetMapping("/book")
public ResponseEntity readBooks(@RequestParam(required = false) String isbn) {
    if (isbn == null) {
        return ResponseEntity.ok(libraryService.readBooks());
    }
    return ResponseEntity.ok(libraryService.readBook(isbn));
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

@GetMapping ์–ด๋…ธํ…Œ์ด์…˜์€ GET์œผ๋กœ ์š”์ฒญ๋œ URL์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์•ž์„œ URL์„ /api/library ๋กœ ์ง€์ •ํ–ˆ์œผ๋ฏ€๋กœ GET /api/library/book์— ๋งคํ•‘๋˜๋Š” ๋ฉ”์†Œ๋“œ์ž…๋‹ˆ๋‹ค.


์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” ์ง์ ‘ Repository๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  Service๋ฅผ ๊ฑฐ์ณ์„œ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์‘๋‹ต๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.


*์ฃผ์˜! ์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ• ๋•Œ๋Š” Repository์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” Entity๋ฅผ ์‘๋‹ต๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค. Entity์˜ ์ŠคํŒฉ์ด ๋ณ€๊ฒฝ๋˜๋ฉด API์˜ ์‘๋‹ต๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ๋˜๋ฉด API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ์‘๋‹ต๊ฐ’์ด ๋ฐ”๋€Œ๊ฒŒ๋˜๋Š” ํ™ฉ๋‹นํ•œ ์ผ์ด ์ƒ๊น๋‹ˆ๋‹ค. ์กฐํšŒํ•œ ๊ฐ์ฒด๋ฅผ API์˜ ์‘๋‹ต๊ฐ’์œผ๋กœ ๋งคํ•‘ํ•˜๋Š” ๋กœ์ง์ด ํ•„์š”ํ•˜์ง€๋งŒ ํ•ด๋‹น ์˜ˆ์ œ์—์„œ๋Š” ์ƒ๋žตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


@GetMapping("/book/{bookId}")
public ResponseEntity<Book> readBook (@PathVariable Long bookId) {
    return ResponseEntity.ok(libraryService.readBook(bookId));
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

REST API์˜ ์„ค๊ณ„ ๊ทœ์น™ ์ค‘ ์ง‘ํ•ฉ(Collection)/{์ง‘ํ•ฉ ๋ฒˆํ˜ธ} ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. (์ฐธ๊ณ : REST๋ž€? REST API ๋””์ž์ธ ๊ฐ€์ด๋“œ)

@PostMapping("/book")
public ResponseEntity<Book> createBook (@RequestBody BookCreationRequest request) {
    return ResponseEntity.ok(libraryService.createBook(request));
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

POST ๋ฉ”์†Œ๋“œ๋Š” ๋„์„œ ์ƒ์„ฑ๊ณผ ๊ฐ™์ด ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ์— ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์†Œ๋“œ์ž…๋‹ˆ๋‹ค. application/json์˜ request body๋ฅผ ๋ฐ›๋„๋ก ์Šคํ”„๋ง๋ถ€ํŠธ์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์„ค์ •๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.


@DeleteMapping("/book/{bookId}")
public ResponseEntity<Void> deleteBook (@PathVariable Long bookId) {
    libraryService.deleteBook(bookId);
    return ResponseEntity.ok().build();
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

DELTE ๋ฉ”์†Œ๋“œ๋Š” ๋„์„œ ์‚ญ์ œ์™€ ๊ฐ™์ด ๋ฆฌ์†Œ์Šค ์‚ญ์ œ ๋ฉ”์†Œ๋“œ์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋Š” ๋„์„œ ID์— ํ•ด๋‹นํ•˜๋Š” ๋„์„œ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. ****


@PostMapping("/member")
public ResponseEntity<Member> createMember (@RequestBody MemberCreationRequest request) {
    return ResponseEntity.ok(libraryService.createMember(request));
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

ํšŒ์› ์ƒ์„ฑ API์ž…๋‹ˆ๋‹ค.


@PatchMapping("/member/{memberId}")
public ResponseEntity<Member> updateMember (@RequestBody MemberCreationRequest request, @PathVariable Long memberId) {
    return ResponseEntity.ok(libraryService.updateMember(memberId, request));
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

PATCH ๋ฉ”์†Œ๋“œ๋Š” ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฆฌ์†Œ์Šค์˜ ๋ถ€๋ถ„ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.


@PostMapping("/book/lend")
public ResponseEntity<List<String>> lendABook(@RequestBody BookLendRequest bookLendRequests) {
    return ResponseEntity.ok(libraryService.lendABook(bookLendRequests));
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

๋„์„œ ๋Œ€์ถœ API์ž…๋‹ˆ๋‹ค.


@PostMapping("/author")
public ResponseEntity<Author> createAuthor (@RequestBody AuthorCreationRequest request) {
    return ResponseEntity.ok(libraryService.createAuthor(request));
}
/controller/LibraryController.java ์ถ”๊ฐ€ ์ฝ”๋“œ

์ €์ž ์ถ”๊ฐ€ API์ž…๋‹ˆ๋‹ค.




9. API ํ…Œ์ŠคํŠธ


Postman์„ ์ด์šฉํ•ด์„œ ์ง€๊ธˆ๊นŒ์ง€ ๊ฐœ๋ฐœํ•œ API๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.



9-1. ์ €์ž ์ถ”๊ฐ€



POST, PUT, PATCH ๋ฉ”์†Œ๋“œ์ธ ๊ฒฝ์šฐ Request Body์— JSON์„ ์„ ํƒํ•ด์„œ body์— ์š”์ฒญ๊ฐ’์„ ๋‹ด์•„์•ผํ•ฉ๋‹ˆ๋‹ค.



9-2. ๋„์„œ ์ถ”๊ฐ€




9-3. ํšŒ์› ์ถ”๊ฐ€




9-4. ๋„์„œ ์กฐํšŒ




9-5. ISBN๋ฒˆํ˜ธ๋กœ ๋„์„œ์กฐํšŒ




9-6. ID๋กœ ๋„์„œ ์กฐํšŒ




9-7. ๋„์„œ ๋Œ€์ถœ





10. References


  • javatodev: javatodev.com
  • ๋ถ€์ŠคํŠธ์ฝ”์Šค Spring: www.boostcourse.org
  • Spring์—์„œ JPA / Hibernate ์ดˆ๊ธฐํ™” ์ „๋žต: pravusid.kr
  • Spring Data JPA: spring.io
  • Building a RESTful Web Service: spring.io
  • @RequestMapping ์–ด๋…ธํ…Œ์ด์…˜์— ๋Œ€ํ•˜์—ฌ: sarc.io