๐ [๋ก์ผ ํ์ต] ์คํ๋ง๋ถํธ 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
์ต๊ทผ ํ๋ก์ ํธ๋ ๊ฐ๋ ์ฑ์ ์ํด์ yml ํ์์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. ์์ ๋์ผํ ํํ์ yml ์ค์ ์ ๋ณด๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/covenant
username: root
password: password
jpa:
hibernate:
ddl-auto: create
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์ 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;
}
๋์ ๋ชจ๋ธ์ 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;
}
ํ์ ๋ชจ๋ธ์ PK(Primary Key)์ firstName, lastName ๊ทธ๋ฆฌ๊ณ ํ์ ์ํ๋ฅผ ์๋ ค์ฃผ๋ Enum ํ๋๋ฅผ ๊ฐ์ต๋๋ค.
package com.covenant.springbootmysql.model;
public enum MemberStatus {
ACTIVE, DEACTIVATED
}
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;
}
Lend ๋ชจ๋ธ์ ๋์ถ์์, ๋์ถ์ข ๋ฃ ๊ทธ๋ฆฌ๊ณ ๋์ถ์ํ ํ๋๋ฅผ ๊ฐ์ต๋๋ค.
package com.covenant.springbootmysql.model;
public enum LendStatus {
AVAILABLE, BURROWED
}
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;
@JsonBackReference
@OneToMany(mappedBy = "author",
fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Book> books;
5-2. Book์ Lend (One to Many)
๊ฐ์ ์ฑ ์ด ํ๋ถํ ๋์๊ด์ด์ฌ์ ํ๋์ ์ฑ ์ ์ฌ๋ฌ๋ช ์ด ๋์ถํ ์ ์๋ค๋ ๊ฐ์์ ์ํฉ์ ๊ฐ์ ํ๊ณ์ต๋๋ค. ์ฑ ๊ณผ ๋์ถ์ 1:N ๊ด๊ณ์ ๋๋ค.
@JsonBackReference
@OneToMany(mappedBy = "book",
fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Lend> lends;
@ManyToOne
@JoinColumn(name = "book_id")
@JsonManagedReference
private Book book;
5-3. Member์ Lend (One to Many)
ํ ๋ช ์ ํ์์ด ๋์ ์ฌ๋ฌ๊ถ์ ๋์ถํ ์ ์์ต๋๋ค. ํ์๊ณผ ๋์ถ์ 1:N๊ด๊ณ์ ๋๋ค.
@JsonBackReference
@OneToMany(mappedBy = "member",
fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Lend> lends;
@ManyToOne
@JoinColumn(name = "member_id")
@JsonManagedReference
private Member member;
์์ ์ฝ๋๋ฅผ ํตํด์ ์ํฐํฐ๊ฐ์ ์ฐ๊ด๊ด๊ณ ๋งคํ์ ๋ง์ณค์ต๋๋ค. ์์์ ์ฌ์ฉํ ์ด๋ ธํ ์ด์ ๊ณผ ํ๋ฏธํฐ์ ์๋ฏธ๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
- @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 ์ธํฐํ์ด์ค๋ฅผ ๋ณด๋ฉด 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> {
}
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);
}
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);
}
์ฌ๋ฌ ํ๋๋ฅผ ๊ฒ์ํ๊ณ ์ถ๋ค๋ฉด 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> {
}
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;
}
@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;
}
- 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;
}
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;
}
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;
}
7-6. MemberCreationRequest
package com.covenant.springbootmysql.model.request;
import lombok.Data;
@Data
public class MemberCreationRequest {
private String firstName;
private String lastName;
}
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;
}
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));
}
@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));
}
REST API์ ์ค๊ณ ๊ท์น ์ค ์งํฉ(Collection)/{์งํฉ ๋ฒํธ} ๊ฐ ์์ต๋๋ค. (์ฐธ๊ณ : REST๋? REST API ๋์์ธ ๊ฐ์ด๋)
@PostMapping("/book")
public ResponseEntity<Book> createBook (@RequestBody BookCreationRequest request) {
return ResponseEntity.ok(libraryService.createBook(request));
}
POST ๋ฉ์๋๋ ๋์ ์์ฑ๊ณผ ๊ฐ์ด ๋ฆฌ์์ค ์์ฑ์ ์ฌ์ฉํ๋ ๋ฉ์๋์ ๋๋ค. application/json์ request body๋ฅผ ๋ฐ๋๋ก ์คํ๋ง๋ถํธ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ค์ ๋์ด์์ต๋๋ค.
@DeleteMapping("/book/{bookId}")
public ResponseEntity<Void> deleteBook (@PathVariable Long bookId) {
libraryService.deleteBook(bookId);
return ResponseEntity.ok().build();
}
DELTE ๋ฉ์๋๋ ๋์ ์ญ์ ์ ๊ฐ์ด ๋ฆฌ์์ค ์ญ์ ๋ฉ์๋์ ๋๋ค. ํด๋น ๋ฉ์๋๋ ๋์ ID์ ํด๋นํ๋ ๋์๋ฅผ ์ญ์ ํฉ๋๋ค. ****
@PostMapping("/member")
public ResponseEntity<Member> createMember (@RequestBody MemberCreationRequest request) {
return ResponseEntity.ok(libraryService.createMember(request));
}
ํ์ ์์ฑ API์ ๋๋ค.
@PatchMapping("/member/{memberId}")
public ResponseEntity<Member> updateMember (@RequestBody MemberCreationRequest request, @PathVariable Long memberId) {
return ResponseEntity.ok(libraryService.updateMember(memberId, request));
}
PATCH ๋ฉ์๋๋ ์ด๋ฏธ ์กด์ฌํ๋ ๋ฆฌ์์ค์ ๋ถ๋ถ ์ ๋ณด๋ฅผ ์์ ํฉ๋๋ค.
@PostMapping("/book/lend")
public ResponseEntity<List<String>> lendABook(@RequestBody BookLendRequest bookLendRequests) {
return ResponseEntity.ok(libraryService.lendABook(bookLendRequests));
}
๋์ ๋์ถ API์ ๋๋ค.
@PostMapping("/author")
public ResponseEntity<Author> createAuthor (@RequestBody AuthorCreationRequest request) {
return ResponseEntity.ok(libraryService.createAuthor(request));
}
์ ์ ์ถ๊ฐ 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