์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŒŸ Spring MVC๋กœ RESTful API ๊ฐœ๋ฐœํ•˜๊ธฐ: ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ๋”ฐ๋ผํ•  ์ˆ˜ ์žˆ๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ ๐ŸŒŸ

๐ŸŒŸ Spring MVC๋กœ RESTful API ๊ฐœ๋ฐœํ•˜๊ธฐ: ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ๋”ฐ๋ผํ•  ์ˆ˜ ์žˆ๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ ๐ŸŒŸ

์•ˆ๋…•ํ•˜์„ธ์š”, ๊ฐœ๋ฐœ์ž ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜์€ Spring MVC๋ฅผ ํ™œ์šฉํ•ด์„œ RESTful API๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ํ•จ๊ป˜ ์•Œ์•„๋ณผ๊ฒŒ์š”. ๐Ÿš€ Java ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ํ•œ ๋ฒˆ์ฏค์€ Spring Framework๋ฅผ ์ ‘ํ•ด๋ดค์„ ํ…๋ฐ, ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ๋”ฐ๋ผํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…ํ•ด๋“œ๋ฆด๊ฒŒ์š”!

์š”์ฆ˜ ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฐœ๋ฐœ ๋ฐฉ์‹์ด ๋Œ€์„ธ์ธ๋ฐ, ์ด๋Ÿฐ ํŠธ๋ Œ๋“œ์— ๋งž์ถฐ RESTful API ๊ฐœ๋ฐœ ์Šคํ‚ฌ์€ ํ•„์ˆ˜๊ฐ€ ๋˜์—ˆ์–ด์š”. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ API ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ ๊ฑฐ๋ž˜๊ฐ€ ํ™œ๋ฐœํ•˜๊ฒŒ ์ด๋ฃจ์–ด์ง€๊ณ  ์žˆ์ฃ ! ๊ทธ๋งŒํผ ์ˆ˜์š”๊ฐ€ ๋งŽ๋‹ค๋Š” ๋œป์ด๋‹ˆ๊นŒ, ์ด ๊ธฐํšŒ์— ํ™•์‹คํ•˜๊ฒŒ ๋ฐฐ์›Œ๋ด์š”~ ใ…Žใ…Ž

๐Ÿ“š ๋ชฉ์ฐจ

  1. Spring MVC์™€ RESTful API ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ
  2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •ํ•˜๊ธฐ
  3. ์ฒซ ๋ฒˆ์งธ RESTful API ๋งŒ๋“ค๊ธฐ
  4. HTTP ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ
  5. ์š”์ฒญ/์‘๋‹ต ์ฒ˜๋ฆฌํ•˜๊ธฐ
  6. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
  7. API ๋ฌธ์„œํ™”ํ•˜๊ธฐ
  8. ๋ณด์•ˆ ์ ์šฉํ•˜๊ธฐ
  9. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๊ฐ„๋‹จํ•œ API ์„œ๋น„์Šค ๊ตฌํ˜„
  10. ๋งˆ๋ฌด๋ฆฌ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

1. Spring MVC์™€ RESTful API ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ ๐Ÿง 

์ผ๋‹จ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ํ™•์‹คํžˆ ์žก๊ณ  ๊ฐ€์ž๊ตฌ์š”! ๋„ˆ๋ฌด ์–ด๋ ต๊ฒŒ ์ƒ๊ฐํ•˜์ง€ ๋งˆ์„ธ์š”~ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•ด๋“œ๋ฆด๊ฒŒ์š” ใ…Žใ…Ž

Spring MVC๋ž€ ๋ญ˜๊นŒ์š”? ๐Ÿค”

Spring MVC๋Š” Model-View-Controller ํŒจํ„ด์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ ํ”„๋ ˆ์ž„์›Œํฌ์˜ˆ์š”. ๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด:

  1. Model: ๋ฐ์ดํ„ฐ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹นํ•ด์š”
  2. View: ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ง€๋Š” ํ™”๋ฉด์„ ๋‹ด๋‹นํ•ด์š”
  3. Controller: ์‚ฌ์šฉ์ž ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  Model๊ณผ View๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•ด์š”

RESTful API๋Š” ๋ญ”๊ฐ€์š”? ๐ŸŒ

REST(Representational State Transfer)๋Š” ์›น ์„œ๋น„์Šค๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ์•„ํ‚คํ…์ฒ˜ ์Šคํƒ€์ผ์ด์—์š”. RESTful API๋Š” ์ด ์›์น™์„ ๋”ฐ๋ฅด๋Š” API๋ฅผ ๋งํ•˜์ฃ .

์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ์ธํ„ฐ๋„ท ์ฃผ์†Œ(URL)๋กœ ์ž์›์„ ํ‘œํ˜„ํ•˜๊ณ , HTTP ๋ฉ”์„œ๋“œ(GET, POST, PUT, DELETE ๋“ฑ)๋กœ ์ž์›์— ๋Œ€ํ•œ ํ–‰๋™์„ ์ •์˜ํ•˜๋Š” ๋ฐฉ์‹์ด์—์š”. ๋งˆ์น˜ "์ด ์ฃผ์†Œ๋กœ ์™€์„œ ์ด๋Ÿฐ ํ–‰๋™์„ ํ•ด์ค˜~"๋ผ๊ณ  ๋งํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์•„์š”! ใ…‹ใ…‹ใ…‹

Spring MVC RESTful API ๊ตฌ์กฐ ํด๋ผ์ด์–ธํŠธ Spring MVC Framework Controller Service Repository ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค HTTP ์š”์ฒญ HTTP ์‘๋‹ต ์š”์ฒญ ํ๋ฆ„ ํด๋ผ์ด์–ธํŠธ โ†’ Controller โ†’ Service โ†’ Repository โ†’ Database ์‘๋‹ต์€ ์—ญ์ˆœ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค

์™œ RESTful API๊ฐ€ ์ค‘์š”ํ• ๊นŒ์š”? ๐Ÿ”

์š”์ฆ˜ ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ฐ€๊ณ  ์žˆ์–ด์š”. ํ”„๋ก ํŠธ์—”๋“œ(React, Vue, Angular ๋“ฑ)์™€ ๋ฐฑ์—”๋“œ(Spring, Node.js ๋“ฑ) ์‚ฌ์ด์— ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ RESTful API๊ฐ€ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์ฃ .

๋˜ํ•œ ๋ชจ๋ฐ”์ผ ์•ฑ, ์›น, IoT ๊ธฐ๊ธฐ ๋“ฑ ๋‹ค์–‘ํ•œ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ง€์›ํ•ด์•ผ ํ•˜๋Š” ํ˜„๋Œ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” RESTful API๊ฐ€ ํ•„์ˆ˜์ ์ด์—์š”. ์žฌ๋Šฅ๋„ท์—์„œ๋„ ์ด๋Ÿฐ API ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์ด ์ธ๊ธฐ๊ฐ€ ๋งŽ๋‹ค๊ณ  ํ•˜๋„ค์š”! ๐Ÿ˜Š

2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •ํ•˜๊ธฐ โš™๏ธ

์ž, ์ด์ œ ์‹ค์ œ๋กœ ์ฝ”๋”ฉ์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด๋ด์š”!

ํ•„์š”ํ•œ ๋„๊ตฌ๋“ค ๐Ÿ› ๏ธ

  1. JDK 8 ์ด์ƒ (11 ๋˜๋Š” 17 ์ถ”์ฒœ)
  2. IDE (IntelliJ IDEA, Eclipse, VS Code ๋“ฑ)
  3. Maven ๋˜๋Š” Gradle (์˜์กด์„ฑ ๊ด€๋ฆฌ ๋„๊ตฌ)
  4. Postman (API ํ…Œ์ŠคํŠธ ๋„๊ตฌ)

Spring Boot ํ”„๋กœ์ ํŠธ ์ƒ์„ฑํ•˜๊ธฐ ๐Ÿš€

Spring Boot๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Spring MVC ํ”„๋กœ์ ํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”. Spring Initializr(https://start.spring.io/)๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ด๋ณผ๊ฒŒ์š”!

Spring Initializr์—์„œ ๋‹ค์Œ ์„ค์ •์œผ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”:

  1. Project: Maven Project
  2. Language: Java
  3. Spring Boot: 2.7.x ๋˜๋Š” ์ตœ์‹  ๋ฒ„์ „
  4. Group: com.example
  5. Artifact: rest-api-demo
  6. Dependencies: Spring Web, Spring Data JPA, H2 Database, Lombok

'Generate' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ”„๋กœ์ ํŠธ zip ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ๋ผ์š”. ์ด๊ฑธ ์••์ถ• ํ•ด์ œํ•˜๊ณ  IDE๋กœ ์—ด์–ด์ฃผ์„ธ์š”!

๊ธฐ๋ณธ ์˜์กด์„ฑ ์‚ดํŽด๋ณด๊ธฐ ๐Ÿ“ฆ

์ƒ์„ฑ๋œ pom.xml ํŒŒ์ผ์„ ์‚ดํŽด๋ณผ๊นŒ์š”? ์ฃผ์š” ์˜์กด์„ฑ๋“ค์ด ์ด๋ฏธ ์ถ”๊ฐ€๋˜์–ด ์žˆ์–ด์š”:


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
    

๊ฐ ์˜์กด์„ฑ์˜ ์—ญํ• ์„ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•˜์ž๋ฉด:

  1. spring-boot-starter-web: Spring MVC, RESTful API ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  2. spring-boot-starter-data-jpa: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ์„ ์œ„ํ•œ JPA ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  3. h2: ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ์šฉ
  4. lombok: ๋ฐ˜๋ณต์ ์ธ ์ฝ”๋“œ(getter, setter ๋“ฑ)๋ฅผ ์ค„์—ฌ์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •ํ•˜๊ธฐ โš™๏ธ

src/main/resources/application.properties ํŒŒ์ผ์— ๋‹ค์Œ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


# ์„œ๋ฒ„ ํฌํŠธ ์„ค์ •
server.port=8080

# H2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 ์ฝ˜์†” ํ™œ์„ฑํ™”
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA ์„ค์ •
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
    

์ด์ œ ๊ธฐ๋ณธ ํ™˜๊ฒฝ ์„ค์ •์ด ์™„๋ฃŒ๋์–ด์š”! ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰ํ•ด๋ณด๋ฉด Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด 8080 ํฌํŠธ์—์„œ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ง์ง์ง! ๐Ÿ‘

3. ์ฒซ ๋ฒˆ์งธ RESTful API ๋งŒ๋“ค๊ธฐ ๐ŸŽฏ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ API๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ์š”? ๊ฐ„๋‹จํ•œ ์‚ฌ์šฉ์ž(User) API๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”!

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ ๐Ÿ“‚

๋จผ์ € ํŒจํ‚ค์ง€ ๊ตฌ์กฐ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”:


src/main/java/com/example/restapidemmo/
  โ”œโ”€โ”€ controller/
  โ”œโ”€โ”€ model/
  โ”œโ”€โ”€ repository/
  โ”œโ”€โ”€ service/
  โ””โ”€โ”€ RestApiDemoApplication.java
    

๋ชจ๋ธ(Entity) ๋งŒ๋“ค๊ธฐ ๐Ÿ“

User ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. model ํŒจํ‚ค์ง€์— User.java ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”:


package com.example.restapidemo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String email;
    private int age;
}
    

์—ฌ๊ธฐ์„œ Lombok์˜ @Data ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด getter, setter, toString, equals, hashCode ๋ฉ”์„œ๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ค˜์š”. ์ง„์งœ ํŽธํ•ด์š” ใ…‹ใ…‹ใ…‹

๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋งŒ๋“ค๊ธฐ ๐Ÿ—„๏ธ

์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ์„ ์œ„ํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. repository ํŒจํ‚ค์ง€์— UserRepository.java ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”:


package com.example.restapidemo.repository;

import com.example.restapidemo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // JpaRepository๊ฐ€ ๊ธฐ๋ณธ์ ์ธ CRUD ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ด์ค๋‹ˆ๋‹ค!
}
    

Spring Data JPA์˜ JpaRepository๋ฅผ ์ƒ์†๋ฐ›์œผ๋ฉด ๊ธฐ๋ณธ์ ์ธ CRUD(Create, Read, Update, Delete) ๋ฉ”์„œ๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ง„์งœ ๊ฐœ๋ฐœ์ž ์ธ์ƒ ๊ฟ€์ด์ฃ ? ๐Ÿ˜„

์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ โš™๏ธ

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹นํ•  ์„œ๋น„์Šค ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. service ํŒจํ‚ค์ง€์— UserService.java ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”:


package com.example.restapidemo.service;

import com.example.restapidemo.model.User;
import com.example.restapidemo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }

    public User createUser(User user) {
        return userRepository.save(user);
    }

    public Optional<User> updateUser(Long id, User userDetails) {
        return userRepository.findById(id)
                .map(user -> {
                    user.setName(userDetails.getName());
                    user.setEmail(userDetails.getEmail());
                    user.setAge(userDetails.getAge());
                    return userRepository.save(user);
                });
    }

    public boolean deleteUser(Long id) {
        return userRepository.findById(id)
                .map(user -> {
                    userRepository.delete(user);
                    return true;
                })
                .orElse(false);
    }
}
    

์„œ๋น„์Šค ํด๋ž˜์Šค์—์„œ๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•˜๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•ด์š”. @Autowired๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์˜์กด์„ฑ ์ฃผ์ž…(DI)์„ ๋ฐ›๊ณ  ์žˆ๋„ค์š”!

์ปจํŠธ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ ๐ŸŽฎ

์ด์ œ RESTful API์˜ ํ•ต์‹ฌ์ธ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. controller ํŒจํ‚ค์ง€์— UserController.java ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”:


package com.example.restapidemo.controller;

import com.example.restapidemo.model.User;
import com.example.restapidemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    // ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return new ResponseEntity<>(users, HttpStatus.OK);
    }

    // ํŠน์ • ์‚ฌ์šฉ์ž ์กฐํšŒ
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userService.getUserById(id)
                .map(user -> new ResponseEntity<>(user, HttpStatus.OK))
                .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    // ์‚ฌ์šฉ์ž ์ƒ์„ฑ
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User createdUser = userService.createUser(user);
        return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
    }

    // ์‚ฌ์šฉ์ž ์ˆ˜์ •
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        return userService.updateUser(id, user)
                .map(updatedUser -> new ResponseEntity<>(updatedUser, HttpStatus.OK))
                .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    // ์‚ฌ์šฉ์ž ์‚ญ์ œ
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        boolean deleted = userService.deleteUser(id);
        return deleted ? 
                new ResponseEntity<>(HttpStatus.NO_CONTENT) : 
                new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
}
    

์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ RESTful API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ •์˜ํ–ˆ์–ด์š”:

  1. GET /api/users: ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ
  2. GET /api/users/{id}: ํŠน์ • ์‚ฌ์šฉ์ž ์กฐํšŒ
  3. POST /api/users: ์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
  4. PUT /api/users/{id}: ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •
  5. DELETE /api/users/{id}: ์‚ฌ์šฉ์ž ์‚ญ์ œ

์—ฌ๊ธฐ์„œ @RestController๋Š” @Controller + @ResponseBody๋ฅผ ํ•ฉ์นœ ๊ฒƒ์œผ๋กœ, ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜๊ฐ’์ด HTTP ์‘๋‹ต ๋ณธ๋ฌธ์— ์ง์ ‘ ๋งคํ•‘๋œ๋‹ค๋Š” ์˜๋ฏธ์˜ˆ์š”. REST API๋ฅผ ๋งŒ๋“ค ๋•Œ ๋”ฑ์ด์ฃ ! ๐Ÿ‘

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ํ•˜๊ธฐ ๐Ÿš€

์ด์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•ด๋ณผ๊นŒ์š”? IDE์—์„œ RestApiDemoApplication.java ํŒŒ์ผ์„ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜, ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”:


./mvnw spring-boot:run
    

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฉด http://localhost:8080/api/users ์ฃผ์†Œ๋กœ API๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ์–ด์š”!

4. HTTP ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ”„

RESTful API์—์„œ๋Š” HTTP ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•ด ์ž์›์— ๋Œ€ํ•œ ํ–‰๋™์„ ์ •์˜ํ•ด์š”. ๊ฐ ๋ฉ”์„œ๋“œ์˜ ์˜๋ฏธ์™€ ์‚ฌ์šฉ๋ฒ•์„ ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊ฒŒ์š”!

์ฃผ์š” HTTP ๋ฉ”์„œ๋“œ ๐Ÿ“‹

  1. GET: ์ž์› ์กฐํšŒ (Read)
  2. POST: ์ž์› ์ƒ์„ฑ (Create)
  3. PUT: ์ž์› ์ „์ฒด ์ˆ˜์ • (Update)
  4. PATCH: ์ž์› ๋ถ€๋ถ„ ์ˆ˜์ • (Partial Update)
  5. DELETE: ์ž์› ์‚ญ์ œ (Delete)
GET ๋ฐ์ดํ„ฐ ์กฐํšŒ POST ๋ฐ์ดํ„ฐ ์ƒ์„ฑ PUT ์ „์ฒด ์ˆ˜์ • PATCH ๋ถ€๋ถ„ ์ˆ˜์ • DELETE ๋ฐ์ดํ„ฐ ์‚ญ์ œ RESTful API์˜ HTTP ๋ฉ”์„œ๋“œ

GET ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ”

GET ๋ฉ”์„œ๋“œ๋Š” ์ž์›์„ ์กฐํšŒํ•  ๋•Œ ์‚ฌ์šฉํ•ด์š”. ์šฐ๋ฆฌ API์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ–ˆ์ฃ :


// ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
    List<User> users = userService.getAllUsers();
    return new ResponseEntity<>(users, HttpStatus.OK);
}

// ํŠน์ • ์‚ฌ์šฉ์ž ์กฐํšŒ
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    return userService.getUserById(id)
            .map(user -> new ResponseEntity<>(user, HttpStatus.OK))
            .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
    

GET ์š”์ฒญ์€ URL ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค๋ฉด:

  1. URL ํŒŒ๋ผ๋ฏธํ„ฐ: /api/users/1 (ID๊ฐ€ 1์ธ ์‚ฌ์šฉ์ž ์กฐํšŒ)
  2. ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง: /api/users?name=John (์ด๋ฆ„์ด John์ธ ์‚ฌ์šฉ์ž ์กฐํšŒ)

POST ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ โž•

POST ๋ฉ”์„œ๋“œ๋Š” ์ƒˆ๋กœ์šด ์ž์›์„ ์ƒ์„ฑํ•  ๋•Œ ์‚ฌ์šฉํ•ด์š”:


// ์‚ฌ์šฉ์ž ์ƒ์„ฑ
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
    User createdUser = userService.createUser(user);
    return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
    

POST ์š”์ฒญ์€ ์š”์ฒญ ๋ณธ๋ฌธ(Request Body)์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์•„ ์ „์†กํ•ด์š”. Spring MVC์—์„œ๋Š” @RequestBody ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด ์š”์ฒญ ๋ณธ๋ฌธ์„ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์ค˜์š”.

POST ์š”์ฒญ ์˜ˆ์‹œ (JSON ํ˜•์‹):


{
    "name": "ํ™๊ธธ๋™",
    "email": "hong@example.com",
    "age": 25
}
    

PUT ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ ๐Ÿ”„

PUT ๋ฉ”์„œ๋“œ๋Š” ์ž์›์„ ์™„์ „ํžˆ ๋Œ€์ฒด(์ˆ˜์ •)ํ•  ๋•Œ ์‚ฌ์šฉํ•ด์š”:


// ์‚ฌ์šฉ์ž ์ˆ˜์ •
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
    return userService.updateUser(id, user)
            .map(updatedUser -> new ResponseEntity<>(updatedUser, HttpStatus.OK))
            .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
    

PUT ์š”์ฒญ๋„ POST์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์š”์ฒญ ๋ณธ๋ฌธ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์•„ ์ „์†กํ•ด์š”. ๋‹ค๋งŒ, PUT์€ ์ž์›์„ ์™„์ „ํžˆ ๋Œ€์ฒดํ•˜๋Š” ์˜๋ฏธ์ด๋ฏ€๋กœ, ์š”์ฒญ ๋ณธ๋ฌธ์— ๋ชจ๋“  ํ•„๋“œ๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”.

PATCH ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ ๐Ÿฉน

PATCH ๋ฉ”์„œ๋“œ๋Š” ์ž์›์„ ๋ถ€๋ถ„์ ์œผ๋กœ ์ˆ˜์ •ํ•  ๋•Œ ์‚ฌ์šฉํ•ด์š”. ์šฐ๋ฆฌ ์˜ˆ์ œ์—๋Š” ์—†์ง€๋งŒ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”:


// ์‚ฌ์šฉ์ž ๋ถ€๋ถ„ ์ˆ˜์ •
@PatchMapping("/{id}")
public ResponseEntity<User> partialUpdateUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
    return userService.partialUpdateUser(id, updates)
            .map(updatedUser -> new ResponseEntity<>(updatedUser, HttpStatus.OK))
            .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
    

PATCH ์š”์ฒญ์€ ๋ณ€๊ฒฝํ•  ํ•„๋“œ๋งŒ ํฌํ•จํ•˜๋Š” ์š”์ฒญ ๋ณธ๋ฌธ์„ ์ „์†กํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๋ฉ”์ผ๋งŒ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด:


{
    "email": "new.email@example.com"
}
    

DELETE ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ โŒ

DELETE ๋ฉ”์„œ๋“œ๋Š” ์ž์›์„ ์‚ญ์ œํ•  ๋•Œ ์‚ฌ์šฉํ•ด์š”:


// ์‚ฌ์šฉ์ž ์‚ญ์ œ
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    boolean deleted = userService.deleteUser(id);
    return deleted ? 
            new ResponseEntity<>(HttpStatus.NO_CONTENT) : 
            new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
    

DELETE ์š”์ฒญ์€ ๋ณดํ†ต URL ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ†ตํ•ด ์‚ญ์ œํ•  ์ž์›์˜ ID๋ฅผ ์ „๋‹ฌํ•ด์š”. ์š”์ฒญ ๋ณธ๋ฌธ์€ ์ผ๋ฐ˜์ ์œผ๋กœ ๋น„์–ด์žˆ์–ด์š”.

์ด๋ ‡๊ฒŒ HTTP ๋ฉ”์„œ๋“œ๋ฅผ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด RESTful API์˜ ์˜๋„๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๊ฐœ๋ฐœ์ž๋“ค์ด API๋ฅผ ๋ณด๊ณ  ์ง๊ด€์ ์œผ๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์ฃ ! ๐Ÿ˜Š

5. ์š”์ฒญ/์‘๋‹ต ์ฒ˜๋ฆฌํ•˜๊ธฐ ๐Ÿ“ค๐Ÿ“ฅ

RESTful API์—์„œ๋Š” ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊ฒŒ์š”!

์š”์ฒญ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๐Ÿ“ฅ

Spring MVC์—์„œ๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”:

1. ๊ฒฝ๋กœ ๋ณ€์ˆ˜ (@PathVariable) ๐Ÿ“

URL ๊ฒฝ๋กœ์— ํฌํ•จ๋œ ๋ณ€์ˆ˜๋ฅผ ๋ฐ›์„ ๋•Œ ์‚ฌ์šฉํ•ด์š”:


@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    // id ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌ
}
    

2. ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ (@RequestParam) ๐Ÿ”

URL์˜ ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์„ ๋•Œ ์‚ฌ์šฉํ•ด์š”:


@GetMapping("/users")
public ResponseEntity<List<User>> searchUsers(
        @RequestParam(required = false) String name,
        @RequestParam(required = false) Integer minAge) {
    // name, minAge ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌ
}
    

3. ์š”์ฒญ ๋ณธ๋ฌธ (@RequestBody) ๐Ÿ“ฆ

JSON์ด๋‚˜ XML ๊ฐ™์€ ํ˜•์‹์˜ ์š”์ฒญ ๋ณธ๋ฌธ์„ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉํ•ด์š”:


@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
    // user ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌ
}
    

4. ์š”์ฒญ ํ—ค๋” (@RequestHeader) ๐Ÿ“‹

HTTP ์š”์ฒญ ํ—ค๋” ๊ฐ’์„ ๋ฐ›์„ ๋•Œ ์‚ฌ์šฉํ•ด์š”:


@GetMapping("/users")
public ResponseEntity<List<User>> getUsers(
        @RequestHeader("Authorization") String authToken) {
    // authToken ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ ์ฒ˜๋ฆฌ
}
    

์‘๋‹ต ์ฒ˜๋ฆฌํ•˜๊ธฐ ๐Ÿ“ค

Spring MVC์—์„œ๋Š” ResponseEntity ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ด HTTP ์‘๋‹ต์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”:


@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    return userService.getUserById(id)
            .map(user -> new ResponseEntity<>(user, HttpStatus.OK))
            .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
    

ResponseEntity๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์š”์†Œ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”:

  1. ์‘๋‹ต ๋ณธ๋ฌธ(Body): ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•  ๋ฐ์ดํ„ฐ
  2. ์ƒํƒœ ์ฝ”๋“œ(Status Code): HTTP ์ƒํƒœ ์ฝ”๋“œ (200 OK, 404 Not Found ๋“ฑ)
  3. ํ—ค๋”(Headers): HTTP ์‘๋‹ต ํ—ค๋”

์ฃผ์š” HTTP ์ƒํƒœ ์ฝ”๋“œ ๐Ÿšฆ

RESTful API์—์„œ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ์•Œ์•„๋ณผ๊ฒŒ์š”:

  1. 200 OK: ์š”์ฒญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋จ
  2. 201 Created: ์ž์›์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋จ
  3. 204 No Content: ์š”์ฒญ์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ๋ฐ˜ํ™˜ํ•  ์ฝ˜ํ…์ธ ๊ฐ€ ์—†์Œ
  4. 400 Bad Request: ์ž˜๋ชป๋œ ์š”์ฒญ
  5. 401 Unauthorized: ์ธ์ฆ ์‹คํŒจ
  6. 403 Forbidden: ๊ถŒํ•œ ๋ถ€์กฑ
  7. 404 Not Found: ์š”์ฒญํ•œ ์ž์›์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ
  8. 500 Internal Server Error: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜

์‘๋‹ต ํ˜•์‹ ๐Ÿ“„

RESTful API์—์„œ๋Š” ์ฃผ๋กœ JSON ํ˜•์‹์œผ๋กœ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ด์š”. Spring MVC์—์„œ๋Š” Jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ž๋ฐ” ๊ฐ์ฒด๋ฅผ ์ž๋™์œผ๋กœ JSON์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ค˜์š”.

์ผ๊ด€๋œ ์‘๋‹ต ํ˜•์‹์„ ์œ„ํ•ด ๊ณตํ†ต ์‘๋‹ต ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”:


public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    
    // ์ƒ์„ฑ์ž, getter, setter ์ƒ๋žต
}
    

์ด ์‘๋‹ต ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ผ๊ด€๋œ ํ˜•์‹์˜ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด์š”:


@GetMapping("/users/{id}")
public ResponseEntity<ApiResponse<User>> getUserById(@PathVariable Long id) {
    return userService.getUserById(id)
            .map(user -> {
                ApiResponse<User> response = new ApiResponse<>(true, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.", user);
                return new ResponseEntity<>(response, HttpStatus.OK);
            })
            .orElse(new ResponseEntity<>(
                new ApiResponse<>(false, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", null),
                HttpStatus.NOT_FOUND
            ));
}
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๋Š” ํ•ญ์ƒ ๋™์ผํ•œ ํ˜•์‹์˜ ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๊ธฐ๊ฐ€ ํŽธํ•ด์ ธ์š”! ๐Ÿ‘

6. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๐Ÿ›ก๏ธ

RESTful API์—์„œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ๋งค์šฐ ์ค‘์š”ํ•ด์š”. ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์•Œ์•„๋ณผ๊ฒŒ์š”!

์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๐ŸŒ

Spring MVC์—์„œ๋Š” @ControllerAdvice์™€ @ExceptionHandler๋ฅผ ์‚ฌ์šฉํ•ด ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”:


@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(ResourceNotFoundException ex) {
        ApiResponse<Void> response = new ApiResponse<>(false, ex.getMessage(), null);
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
        ApiResponse<Void> response = new ApiResponse<>(false, "์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", null);
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
    

๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}
    

์ด์ œ ์„œ๋น„์Šค ํด๋ž˜์Šค์—์„œ ์ด ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋ฉด ๋ฉ๋‹ˆ๋‹ค:


public User getUserById(Long id) {
    return userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("ID๊ฐ€ " + id + "์ธ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));
}
    

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ โœ…

Spring Boot์—์„œ๋Š” Bean Validation์„ ์‚ฌ์šฉํ•ด ๊ฐ„ํŽธํ•˜๊ฒŒ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋จผ์ € ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
    

๊ทธ๋ฆฌ๊ณ  ๋ชจ๋ธ ํด๋ž˜์Šค์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด์š”:


@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "์ด๋ฆ„์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.")
    private String name;
    
    @NotBlank(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.")
    @Email(message = "์œ ํšจํ•œ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค.")
    private String email;
    
    @Min(value = 0, message = "๋‚˜์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
    @Max(value = 150, message = "๋‚˜์ด๋Š” 150 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
    private int age;
}
    

์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” @Valid ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ™œ์„ฑํ™”ํ•ด์š”:


@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@Valid @RequestBody User user) {
    User createdUser = userService.createUser(user);
    ApiResponse<User> response = new ApiResponse<>(true, "์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", createdUser);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}
    

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์š”:


@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    
    ex.getBindingResult().getFieldErrors().forEach(error -> {
        errors.put(error.getField(), error.getDefaultMessage());
    });
    
    ApiResponse<Map<String, String>> response = new ApiResponse<>(false, "์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ", errors);
    return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ์–ด๋–ค ํ•„๋“œ์—์„œ ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”์ง€ ์ƒ์„ธํ•œ ์ •๋ณด๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ‘

7. API ๋ฌธ์„œํ™”ํ•˜๊ธฐ ๐Ÿ“š

API๋ฅผ ๊ฐœ๋ฐœํ–ˆ๋‹ค๋ฉด ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค์ด ์‰ฝ๊ฒŒ ์ดํ•ดํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฌธ์„œํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”. Spring Boot์—์„œ๋Š” Swagger๋‚˜ SpringDoc์„ ์‚ฌ์šฉํ•ด API ๋ฌธ์„œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”!

SpringDoc OpenAPI ์‚ฌ์šฉํ•˜๊ธฐ ๐Ÿ“

SpringDoc์€ OpenAPI 3.0 ์ŠคํŽ™์„ ์ง€์›ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ˆ์š”. ๋จผ์ € ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.9</version>
</dependency>
    

์ด์ œ OpenAPI ์„ค์ • ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("์‚ฌ์šฉ์ž ๊ด€๋ฆฌ API")
                        .description("Spring Boot๋กœ ๊ฐœ๋ฐœํ•œ RESTful API ์˜ˆ์ œ")
                        .version("1.0.0")
                        .contact(new Contact()
                                .name("๊ฐœ๋ฐœ์ž ์ด๋ฆ„")
                                .email("developer@example.com"))
                        .license(new License().name("MIT License")));
    }
}
    

์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ชจ๋ธ ํด๋ž˜์Šค์— API ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด์š”:


@RestController
@RequestMapping("/api/users")
@Tag(name = "์‚ฌ์šฉ์ž ๊ด€๋ฆฌ", description = "์‚ฌ์šฉ์ž CRUD API")
public class UserController {

    // ...

    @Operation(summary = "์‚ฌ์šฉ์ž ์กฐํšŒ", description = "ID๋กœ ์‚ฌ์šฉ์ž๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "์‚ฌ์šฉ์ž ์กฐํšŒ ์„ฑ๊ณต"),
        @ApiResponse(responseCode = "404", description = "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ")
    })
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        // ...
    }

    // ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ์—๋„ ์œ ์‚ฌํ•˜๊ฒŒ ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€
}
    

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๊ณ  http://localhost:8080/swagger-ui.html์— ์ ‘์†ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ API ๋ฌธ์„œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”!

http://localhost:8080/swagger-ui.html Swagger UI ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ์‚ฌ์šฉ์ž CRUD API GET /api/users/{id} ID๋กœ ์‚ฌ์šฉ์ž๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. POST /api/users ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. Swagger UI๋กœ ์ž๋™ ์ƒ์„ฑ๋œ API ๋ฌธ์„œ

์ด๋ ‡๊ฒŒ API ๋ฌธ์„œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์ด ์žˆ์–ด์š”:

  1. API ์—”๋“œํฌ์ธํŠธ, ํŒŒ๋ผ๋ฏธํ„ฐ, ์‘๋‹ต ํ˜•์‹ ๋“ฑ์„ ํ•œ๋ˆˆ์— ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”
  2. API๋ฅผ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ๋Š” UI๋ฅผ ์ œ๊ณตํ•ด์š”
  3. ์ฝ”๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋ฌธ์„œ๋„ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ผ์š”
  4. ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค์ด API๋ฅผ ์‰ฝ๊ฒŒ ์ดํ•ดํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ API ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ํŒ๋งคํ•œ๋‹ค๋ฉด, ์ด๋ ‡๊ฒŒ ๋ฌธ์„œํ™”๊ฐ€ ์ž˜ ๋œ API๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด ํฐ ๊ฒฝ์Ÿ๋ ฅ์ด ๋  ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ˜‰

8. ๋ณด์•ˆ ์ ์šฉํ•˜๊ธฐ ๐Ÿ”’

RESTful API์— ๋ณด์•ˆ์„ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”. Spring Security๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”!

Spring Security ์„ค์ •ํ•˜๊ธฐ ๐Ÿ›ก๏ธ

๋จผ์ € Spring Security ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
    

๊ธฐ๋ณธ์ ์ธ ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()  // RESTful API์—์„œ๋Š” CSRF ๋ณดํ˜ธ๊ฐ€ ํ•„์š” ์—†์„ ์ˆ˜ ์žˆ์–ด์š”
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()  // ๊ณต๊ฐœ API
                .antMatchers("/api/users/**").authenticated()  // ์ธ์ฆ ํ•„์š”
                .anyRequest().authenticated()
            .and()
            .httpBasic();  // ๊ธฐ๋ณธ ์ธ์ฆ ์‚ฌ์šฉ
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password(passwordEncoder().encode("password")).roles("USER")
            .and()
            .withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
    

์ด ์„ค์ •์€ ๋งค์šฐ ๊ธฐ๋ณธ์ ์ธ ๊ฒƒ์ด๊ณ , ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋” ๋ณต์žกํ•œ ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ์„ ์ˆ˜ ์žˆ์–ด์š”. ํ•˜์ง€๋งŒ ์ด ์˜ˆ์ œ๋กœ Spring Security์˜ ๊ธฐ๋ณธ ๊ฐœ๋…์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด์š”!

JWT ์ธ์ฆ ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ”‘

RESTful API์—์„œ๋Š” JWT(JSON Web Token)๋ฅผ ์‚ฌ์šฉํ•œ ์ธ์ฆ ๋ฐฉ์‹์ด ๋งŽ์ด ์‚ฌ์šฉ๋ผ์š”. JWT๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ถ”๊ฐ€ ์˜์กด์„ฑ์ด ํ•„์š”ํ•ด์š”:


<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
    

JWT ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


@Component
public class JwtTokenUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration * 1000);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
}
    

๊ทธ๋ฆฌ๊ณ  JWT ํ•„ํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด ์ธ์ฆ ๊ณผ์ •์„ ์ฒ˜๋ฆฌํ•ด์š”:


@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        // JWT ํ† ํฐ์€ "Bearer token" ํ˜•์‹์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("JWT ํ† ํฐ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค");
            }
        } else {
            logger.warn("JWT ํ† ํฐ์ด Bearer๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค");
        }

        // ํ† ํฐ์ด ์œ ํšจํ•˜๋ฉด Spring Security ์ปจํ…์ŠคํŠธ์— ์ธ์ฆ ์ •๋ณด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}
    

๋งˆ์ง€๋ง‰์œผ๋กœ Security ์„ค์ •์„ JWT ์ธ์ฆ์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•ด์š”:


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private UserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests().antMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated().and()
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // JWT ํ•„ํ„ฐ ์ถ”๊ฐ€
        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
    

์ด์ œ ๋กœ๊ทธ์ธ API๋ฅผ ๋งŒ๋“ค์–ด JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•ด๋ณผ๊ฒŒ์š”:


@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @PostMapping("/login")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
        authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());

        final UserDetails userDetails = userDetailsService
                .loadUserByUsername(authenticationRequest.getUsername());

        final String token = jwtTokenUtil.generateToken(userDetails);

        return ResponseEntity.ok(new JwtResponse(token));
    }

    private void authenticate(String username, String password) throws Exception {
        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (DisabledException e) {
            throw new Exception("USER_DISABLED", e);
        } catch (BadCredentialsException e) {
            throw new Exception("INVALID_CREDENTIALS", e);
        }
    }
}
    

์ด๋ ‡๊ฒŒ JWT ์ธ์ฆ์„ ๊ตฌํ˜„ํ•˜๋ฉด RESTful API์— ์•ˆ์ „ํ•˜๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์š”. ํด๋ผ์ด์–ธํŠธ๋Š” ๋กœ๊ทธ์ธ API๋ฅผ ํ†ตํ•ด ํ† ํฐ์„ ๋ฐ›๊ณ , ์ดํ›„ ์š”์ฒญ์—์„œ ์ด ํ† ํฐ์„ Authorization ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ ๋ณด๋‚ด๋ฉด ๋ผ์š”!

9. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๊ฐ„๋‹จํ•œ API ์„œ๋น„์Šค ๊ตฌํ˜„ ๐Ÿš€

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ„๋‹จํ•œ API ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”! ์ด๋ฒˆ์—๋Š” ํ•  ์ผ ๊ด€๋ฆฌ(Todo) API๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”.

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๐Ÿ“‚


src/main/java/com/example/restapidemo/
  โ”œโ”€โ”€ controller/
  โ”‚   โ”œโ”€โ”€ TodoController.java
  โ”‚   โ””โ”€โ”€ AuthController.java
  โ”œโ”€โ”€ model/
  โ”‚   โ”œโ”€โ”€ Todo.java
  โ”‚   โ””โ”€โ”€ User.java
  โ”œโ”€โ”€ repository/
  โ”‚   โ”œโ”€โ”€ TodoRepository.java
  โ”‚   โ””โ”€โ”€ UserRepository.java
  โ”œโ”€โ”€ service/
  โ”‚   โ”œโ”€โ”€ TodoService.java
  โ”‚   โ””โ”€โ”€ UserService.java
  โ”œโ”€โ”€ security/
  โ”‚   โ”œโ”€โ”€ JwtTokenUtil.java
  โ”‚   โ””โ”€โ”€ JwtRequestFilter.java
  โ”œโ”€โ”€ exception/
  โ”‚   โ”œโ”€โ”€ ResourceNotFoundException.java
  โ”‚   โ””โ”€โ”€ GlobalExceptionHandler.java
  โ”œโ”€โ”€ dto/
  โ”‚   โ”œโ”€โ”€ TodoDto.java
  โ”‚   โ””โ”€โ”€ ApiResponse.java
  โ””โ”€โ”€ RestApiDemoApplication.java
    

Todo ๋ชจ๋ธ ๋งŒ๋“ค๊ธฐ ๐Ÿ“


@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Todo {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "์ œ๋ชฉ์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.")
    private String title;
    
    private String description;
    
    private boolean completed;
    
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}
    

Todo ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋งŒ๋“ค๊ธฐ ๐Ÿ—„๏ธ


@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
    List<Todo> findByUserIdOrderByCreatedAtDesc(Long userId);
    List<Todo> findByUserIdAndCompletedOrderByCreatedAtDesc(Long userId, boolean completed);
}
    

Todo ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ โš™๏ธ


@Service
public class TodoService {

    private final TodoRepository todoRepository;
    private final UserRepository userRepository;

    @Autowired
    public TodoService(TodoRepository todoRepository, UserRepository userRepository) {
        this.todoRepository = todoRepository;
        this.userRepository = userRepository;
    }

    public List<Todo> getAllTodosByUser(Long userId) {
        return todoRepository.findByUserIdOrderByCreatedAtDesc(userId);
    }

    public List<Todo> getTodosByUserAndStatus(Long userId, boolean completed) {
        return todoRepository.findByUserIdAndCompletedOrderByCreatedAtDesc(userId, completed);
    }

    public Todo getTodoById(Long id, Long userId) {
        Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("ID๊ฐ€ " + id + "์ธ ํ•  ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));
        
        if (!todo.getUser().getId().equals(userId)) {
            throw new AccessDeniedException("์ด ํ•  ์ผ์— ์ ‘๊ทผํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");
        }
        
        return todo;
    }

    public Todo createTodo(TodoDto todoDto, Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("ID๊ฐ€ " + userId + "์ธ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));
        
        Todo todo = new Todo();
        todo.setTitle(todoDto.getTitle());
        todo.setDescription(todoDto.getDescription());
        todo.setCompleted(false);
        todo.setUser(user);
        
        return todoRepository.save(todo);
    }

    public Todo updateTodo(Long id, TodoDto todoDto, Long userId) {
        Todo todo = getTodoById(id, userId);
        
        todo.setTitle(todoDto.getTitle());
        todo.setDescription(todoDto.getDescription());
        todo.setCompleted(todoDto.isCompleted());
        
        return todoRepository.save(todo);
    }

    public void deleteTodo(Long id, Long userId) {
        Todo todo = getTodoById(id, userId);
        todoRepository.delete(todo);
    }
}
    

Todo ์ปจํŠธ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ ๐ŸŽฎ


@RestController
@RequestMapping("/api/todos")
public class TodoController {

    private final TodoService todoService;

    @Autowired
    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @GetMapping
    public ResponseEntity<ApiResponse<List<Todo>>> getAllTodos(
            @RequestParam(required = false) Boolean completed,
            Authentication authentication) {
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        User user = ((CustomUserDetails) userDetails).getUser();
        
        List<Todo> todos;
        if (completed != null) {
            todos = todoService.getTodosByUserAndStatus(user.getId(), completed);
        } else {
            todos = todoService.getAllTodosByUser(user.getId());
        }
        
        ApiResponse<List<Todo>> response = new ApiResponse<>(true, "ํ•  ์ผ ๋ชฉ๋ก์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค.", todos);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<Todo>> getTodoById(
            @PathVariable Long id,
            Authentication authentication) {
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        User user = ((CustomUserDetails) userDetails).getUser();
        
        Todo todo = todoService.getTodoById(id, user.getId());
        
        ApiResponse<Todo> response = new ApiResponse<>(true, "ํ•  ์ผ์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค.", todo);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @PostMapping
    public ResponseEntity<ApiResponse<Todo>> createTodo(
            @Valid @RequestBody TodoDto todoDto,
            Authentication authentication) {
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        User user = ((CustomUserDetails) userDetails).getUser();
        
        Todo createdTodo = todoService.createTodo(todoDto, user.getId());
        
        ApiResponse<Todo> response = new ApiResponse<>(true, "ํ•  ์ผ์„ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.", createdTodo);
        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<ApiResponse<Todo>> updateTodo(
            @PathVariable Long id,
            @Valid @RequestBody TodoDto todoDto,
            Authentication authentication) {
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        User user = ((CustomUserDetails) userDetails).getUser();
        
        Todo updatedTodo = todoService.updateTodo(id, todoDto, user.getId());
        
        ApiResponse<Todo> response = new ApiResponse<>(true, "ํ•  ์ผ์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.", updatedTodo);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deleteTodo(
            @PathVariable Long id,
            Authentication authentication) {
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        User user = ((CustomUserDetails) userDetails).getUser();
        
        todoService.deleteTodo(id, user.getId());
        
        ApiResponse<Void> response = new ApiResponse<>(true, "ํ•  ์ผ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.", null);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ„๋‹จํ•œ ํ•  ์ผ ๊ด€๋ฆฌ API๊ฐ€ ์™„์„ฑ๋ผ์š”! ์ด API๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์š”:

  1. ์‚ฌ์šฉ์ž๋ณ„ ํ•  ์ผ ๋ชฉ๋ก ์กฐํšŒ
  2. ์™„๋ฃŒ/๋ฏธ์™„๋ฃŒ ์ƒํƒœ๋ณ„ ํ•  ์ผ ํ•„ํ„ฐ๋ง
  3. ํ•  ์ผ ์ƒ์„ธ ์กฐํšŒ
  4. ์ƒˆ๋กœ์šด ํ•  ์ผ ์ƒ์„ฑ
  5. ํ•  ์ผ ์ˆ˜์ •
  6. ํ•  ์ผ ์‚ญ์ œ

๋˜ํ•œ JWT ์ธ์ฆ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋ณ„๋กœ ์ž์‹ ์˜ ํ•  ์ผ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์•ˆ์ด ์ ์šฉ๋˜์–ด ์žˆ์–ด์š”. ์ด๋Ÿฐ API๋Š” ํ•  ์ผ ๊ด€๋ฆฌ ์•ฑ์˜ ๋ฐฑ์—”๋“œ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”!

์žฌ๋Šฅ๋„ท์—์„œ๋„ ์ด๋Ÿฐ ์ข…๋ฅ˜์˜ API ๊ฐœ๋ฐœ ์žฌ๋Šฅ์ด ์ธ๊ธฐ๊ฐ€ ๋งŽ์„ ๊ฒƒ ๊ฐ™๋„ค์š”. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ด๋Ÿฐ ๋ฐฑ์—”๋“œ API๋ฅผ ํ™œ์šฉํ•ด ๋‹ค์–‘ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ์š”! ๐Ÿ˜Š

10. ๋งˆ๋ฌด๋ฆฌ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„ ๐ŸŽฌ

์ง€๊ธˆ๊นŒ์ง€ Spring MVC๋ฅผ ์‚ฌ์šฉํ•ด RESTful API๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์–ด์š”. ์ •๋ง ๋งŽ์€ ๋‚ด์šฉ์„ ๋‹ค๋ค˜๋„ค์š”! ๐Ÿ‘

๋ฐฐ์šด ๋‚ด์šฉ ์ •๋ฆฌ ๐Ÿ“

  1. Spring MVC์™€ RESTful API ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ
  2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •ํ•˜๊ธฐ
  3. ์ฒซ ๋ฒˆ์งธ RESTful API ๋งŒ๋“ค๊ธฐ
  4. HTTP ๋ฉ”์„œ๋“œ ํ™œ์šฉํ•˜๊ธฐ
  5. ์š”์ฒญ/์‘๋‹ต ์ฒ˜๋ฆฌํ•˜๊ธฐ
  6. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
  7. API ๋ฌธ์„œํ™”ํ•˜๊ธฐ
  8. ๋ณด์•ˆ ์ ์šฉํ•˜๊ธฐ
  9. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๊ฐ„๋‹จํ•œ API ์„œ๋น„์Šค ๊ตฌํ˜„

๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๋Š” ๋‚ด์šฉ ๐Ÿš€

RESTful API ๊ฐœ๋ฐœ์— ๋” ๊นŠ์ด ๋“ค์–ด๊ฐ€๊ณ  ์‹ถ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฃผ์ œ๋ฅผ ๊ณต๋ถ€ํ•ด๋ณด์„ธ์š”:

  1. HATEOAS: Hypermedia As The Engine Of Application State - API ์‘๋‹ต์— ๊ด€๋ จ ๋ฆฌ์†Œ์Šค ๋งํฌ๋ฅผ ํฌํ•จ์‹œ์ผœ ํด๋ผ์ด์–ธํŠธ๊ฐ€ API๋ฅผ ๋” ์‰ฝ๊ฒŒ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ธฐ์ˆ 
  2. API ๋ฒ„์ „ ๊ด€๋ฆฌ: API๊ฐ€ ๋ฐœ์ „ํ•จ์— ๋”ฐ๋ผ ์ด์ „ ๋ฒ„์ „๊ณผ์˜ ํ˜ธํ™˜์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ•
  3. ์บ์‹ฑ: API ์‘๋‹ต์„ ์บ์‹ฑํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•
  4. ๋น„๋™๊ธฐ API: WebFlux๋ฅผ ์‚ฌ์šฉํ•œ ๋น„๋™๊ธฐ RESTful API ๊ฐœ๋ฐœ
  5. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค: ๋Œ€๊ทœ๋ชจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

๋งˆ๋ฌด๋ฆฌ ์ธ์‚ฌ ๐Ÿ‘‹

์ด ๊ธ€์ด ์—ฌ๋Ÿฌ๋ถ„์˜ Spring MVC RESTful API ๊ฐœ๋ฐœ์— ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด์š”! ๊ฐœ๋ฐœ์€ ๊ณ„์† ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” ๊ณผ์ •์ด๋‹ˆ, ๊พธ์ค€ํžˆ ๊ณต๋ถ€ํ•˜๊ณ  ์‹ค์Šตํ•ด๋ณด์„ธ์š”.

ํ˜น์‹œ API ๊ฐœ๋ฐœ์— ๊ด€ํ•œ ๋” ๊นŠ์€ ์ง€์‹์ด๋‚˜ ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ•„์š”ํ•˜์‹œ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๊ด€๋ จ ์žฌ๋Šฅ์„ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”. ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์•„ ๋” ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ์š”! ๐Ÿš€

๋‹ค์Œ์— ๋˜ ๋‹ค๋ฅธ ์ฃผ์ œ๋กœ ์ฐพ์•„์˜ฌ๊ฒŒ์š”. ๊ทธ๋•Œ๊นŒ์ง€ ์ฆ๊ฑฐ์šด ์ฝ”๋”ฉํ•˜์„ธ์š”! ๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘ฉโ€๐Ÿ’ป

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.