
λ°±μλ κ°λ°μμ νμ νλ€ λ³΄λ©΄ "Controllerμμ λ°μμ Service κ±°μ³μ Repositoryλ‘ κ°μ"
λΌλ λ§μ λ£κ² λ©λλ€. κ·Όλ°... κ·Έκ² λμ§ λͺ¨λ₯΄λ©΄ μν΅μ΄ μ λμ£ .
μ΄ κΈμ Spring Bootλ₯Ό μ§μ μ§λ λ²μ΄ μλλΌ, νλ‘ νΈ κ°λ°μκ° νμ ν λ κΌ μμμΌ ν ꡬ쑰μ κ°λ λ§ μ 리νμ΅λλ€.
[λΈλΌμ°μ / μ±]
β
β HTTP μμ² (GET, POST, PUT, DELETE)
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Spring Boot μλ² β
β β
β ββββββββββββββββ β
β β Controller β β μμ²μ λ°λ 창ꡬ β
β ββββββββ¬ββββββββ β
β β β
β ββββββββΌβββββββ β
β β Service β β λΉμ¦λμ€ λ‘μ§ β
β ββββββββ¬βββββββ β
β β β
β ββββββββΌβββββββ β
β β Repository β β DBμ λν β
β ββββββββ¬βββββββ β
β β β
ββββββββββββΌβββββββββββββββββββββββββββββββ
β
βΌ
[Database]
MySQL, PostgreSQL λ±
μμ²μ΄ λ€μ΄μ€λ©΄ Controller β Service β Repository β DB μμλ‘ λ΄λ €κ°κ³ ,
μλ΅μ λ°λλ‘ DB β Repository β Service β Controller β λΈλΌμ°μ μμλ‘ μ¬λΌμ΅λλ€.
Controllerλ APIμ μλν¬μΈνΈλ₯Ό μ μνλ κ³³μ
λλ€.
νλ‘ νΈμμ fetch('/api/users') νλ©΄ κ°μ₯ λ¨Όμ λ§λλ κ³³μ΄ λ°λ‘ μ¬κΈ°μ
λλ€.
@RestController
@RequestMapping("/api/users")
public class UserController {
// GET /api/users β μ 체 μ μ λͺ©λ‘
@GetMapping
public List<UserResponse> getUsers() { ... }
// GET /api/users/1 β νΉμ μ μ
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) { ... }
// POST /api/users β μ μ μμ±
@PostMapping
public UserResponse createUser(@RequestBody UserRequest request) { ... }
// PUT /api/users/1 β μ μ μμ
@PutMapping("/{id}")
public UserResponse updateUser(@PathVariable Long id, @RequestBody UserRequest request) { ... }
// DELETE /api/users/1 β μ μ μμ
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) { ... }
}
| μ΄λ Έν μ΄μ | μλ―Έ | νλ‘ νΈ κ΄μ |
|---|---|---|
@RestController | μ΄ ν΄λμ€κ° API 컨νΈλ‘€λ¬μμ μ μΈ | - |
@RequestMapping("/api/users") | μ΄ μ»¨νΈλ‘€λ¬μ κΈ°λ³Έ URL κ²½λ‘ | fetch κ²½λ‘μ μλΆλΆ |
@GetMapping | HTTP GET μμ² μ²λ¦¬ | λ°μ΄ν° μ‘°ν |
@PostMapping | HTTP POST μμ² μ²λ¦¬ | λ°μ΄ν° μμ± |
@PutMapping | HTTP PUT μμ² μ²λ¦¬ | λ°μ΄ν° μ 체 μμ |
@PatchMapping | HTTP PATCH μμ² μ²λ¦¬ | λ°μ΄ν° μΌλΆ μμ |
@DeleteMapping | HTTP DELETE μμ² μ²λ¦¬ | λ°μ΄ν° μμ |
@PathVariable | URL κ²½λ‘μ λ³μ (/users/{id}) | URL νλΌλ―Έν° |
@RequestParam | 쿼리 νλΌλ―Έν° (?page=1) | Query String |
@RequestBody | μμ² λ³Έλ¬Έ(JSON)μ κ°μ²΄λ‘ λ³ν | fetch body |
// νλ‘ νΈ
const res = await fetch('/api/users/42');
// λ°±μλ (β μ΄ μμ²μ λ°μ)
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) { ... }
// β id = 42
// νλ‘ νΈ
const res = await fetch('/api/users?page=1&size=10');
// λ°±μλ
@GetMapping
public List<UserResponse> getUsers(
@RequestParam int page, // 1
@RequestParam int size // 10
) { ... }
// νλ‘ νΈ
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'νκΈΈλ', email: 'hong@test.com' })
});
// λ°±μλ
@PostMapping
public UserResponse createUser(@RequestBody UserRequest request) {
// request.name = "νκΈΈλ"
// request.email = "hong@test.com"
}
DTO(Data Transfer Object)λ APIλ‘ μ£Όκ³ λ°λ λ°μ΄ν°μ ꡬ쑰λ₯Ό μ μν ν΄λμ€μ
λλ€.
νλ‘ νΈμ TypeScript νμ
μ μμ λκ°μ μν μ ν©λλ€.
// μμ² DTO (νλ‘ νΈ β μλ²)
public class UserRequest {
private String name;
private String email;
private String password;
// getter, setter...
}
// μλ΅ DTO (μλ² β νλ‘ νΈ)
public class UserResponse {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
// getter...
}
// νλ‘ νΈμ TypeScript νμ
(λ°±μλ DTOμ λ§μΆ°μΌ ν¨)
interface UserRequest {
name: string;
email: string;
password: string;
}
interface UserResponse {
id: number;
name: string;
email: string;
createdAt: string; // Java LocalDateTime β JSONμμ string
}
π‘ λ°±μλ κ°λ°μμκ² DTO ꡬ쑰λ₯Ό 곡μ ν΄λ¬λΌκ³ μμ²νλ©΄ API μ°λμ΄ ν¨μ¬ νΈν΄μ§λλ€.
νΉμ Swagger(API λ¬Έμ)λ₯Ό ν΅ν΄ μλμΌλ‘ νμΈν μ μμ΅λλ€.
Serviceλ μ€μ κΈ°λ₯ λ‘μ§μ΄ λ΄κΈ°λ κ³³μ
λλ€.
νλ‘ νΈ κ°λ°μκ° μ§μ λ³Ό μΌμ μ μ§λ§, νμ
μ "μ΄ APIκ° μ΄λ€ μ²λ¦¬λ₯Ό νλμ§" μ΄ν΄ν λ λμμ΄ λ©λλ€.
@Service
public class UserService {
public UserResponse createUser(UserRequest request) {
// 1. μ΄λ©μΌ μ€λ³΅ 체ν¬
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException("μ΄λ―Έ μ¬μ© μ€μΈ μ΄λ©μΌμ
λλ€");
}
// 2. λΉλ°λ²νΈ μνΈν
String encodedPassword = passwordEncoder.encode(request.getPassword());
// 3. DBμ μ μ₯
User user = new User(request.getName(), request.getEmail(), encodedPassword);
User savedUser = userRepository.save(user);
// 4. μλ΅ DTOλ‘ λ³νν΄μ λ°ν
return new UserResponse(savedUser);
}
}
νλ‘ νΈμμ 409 Conflictκ° λ λ, μ΄ Service λ μ΄μ΄μμ μ€λ³΅ μ²΄ν¬ ν μλ¬λ₯Ό λμ§λ κ²½μ°κ° λ§μ΅λλ€.
Repositoryλ DBμ λ°μ΄ν°λ₯Ό μ μ₯νκ³ μ‘°ννλ μν μ ν©λλ€.
Spring Data JPAλ₯Ό μ°λ©΄ SQLμ κ±°μ μ μ§λ λ©λλ€.
public interface UserRepository extends JpaRepository<User, Long> {
// SELECT * FROM users WHERE email = ?
Optional<User> findByEmail(String email);
// SELECT COUNT(*) > 0 FROM users WHERE email = ?
boolean existsByEmail(String email);
// SELECT * FROM users WHERE name LIKE %keyword%
List<User> findByNameContaining(String keyword);
}
νλ‘ νΈ κ°λ°μλ Repositoryλ₯Ό μ§μ λ³Ό μΌμ΄ κ±°μ μμ§λ§,
"DBμ μ΄λ€ 컬λΌμ΄ μκ³ μ΄λ€ λ°μ΄ν°κ° μ μ₯λλμ§" μ΄ν΄νλ λ° λμμ΄ λ©λλ€.
Entityλ DB ν μ΄λΈ ꡬ쑰λ₯Ό Java ν΄λμ€λ‘ ννν κ²μ λλ€.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@CreatedDate
private LocalDateTime createdAt;
}
-- μ Entityλ μλ DB ν
μ΄λΈκ³Ό κ°μ΅λλ€
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at DATETIME
);
β οΈ Entity β DTO
Entityλ DB ν μ΄λΈ ꡬ쑰 κ·Έλλ‘μ΄κ³ , DTOλ API μλ΅μ©μΌλ‘ κ°κ³΅ν ννμ λλ€.
λΉλ°λ²νΈ κ°μ λ―Όκ°ν νλλ Entityμλ μμ§λ§ μλ΅ DTOμλ λΉ μ§λλ€.
νλ‘ νΈμμ API μλ¬λ₯Ό μ²λ¦¬ν λ, λ°±μλκ° μ΄λ»κ² μλ¬λ₯Ό λ°ννλμ§ μμμΌ ν©λλ€.
@RestControllerAdvice // λͺ¨λ Controllerμ μμΈλ₯Ό μ¬κΈ°μ μ²λ¦¬
public class GlobalExceptionHandler {
// 404 - 리μμ€ μμ
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFound(UserNotFoundException e) {
return new ErrorResponse("USER_NOT_FOUND", e.getMessage());
}
// 409 - μ€λ³΅
@ExceptionHandler(DuplicateEmailException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ErrorResponse handleDuplicateEmail(DuplicateEmailException e) {
return new ErrorResponse("DUPLICATE_EMAIL", e.getMessage());
}
// 400 - μ ν¨μ± κ²μ¬ μ€ν¨
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException e) {
return new ErrorResponse("VALIDATION_FAILED", "μ
λ ₯κ°μ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€");
}
}
// νλ‘ νΈμμ λ°λ μλ¬ μλ΅
{
"code": "USER_NOT_FOUND",
"message": "ν΄λΉ μ μ λ₯Ό μ°Ύμ μ μμ΅λλ€"
}
// νλ‘ νΈμμ μλ¬ μ²λ¦¬
const res = await fetch('/api/users/999');
if (!res.ok) {
const error = await res.json();
// error.code === "USER_NOT_FOUND"
// error.message === "ν΄λΉ μ μ λ₯Ό μ°Ύμ μ μμ΅λλ€"
console.error(error.message);
}
π‘ νλ§λ€ μλ¬ μλ΅ κ΅¬μ‘°κ° λ€λ¦ λλ€. λ°±μλ κ°λ°μμκ² μλ¬ μλ΅ ν¬λ§·μ κΌ νμΈνμΈμ.
λ‘κ·ΈμΈ, κΆν κ΄λ¦¬ λ±μ Spring Securityκ° λ΄λΉν©λλ€.
β λ‘κ·ΈμΈ μμ²
POST /api/auth/login
{ "email": "...", "password": "..." }
β‘ μλ²: μ΄λ©μΌ/λΉλ°λ²νΈ κ²μ¦ ν JWT ν ν° λ°κΈ
{ "accessToken": "eyJhbGci...", "refreshToken": "eyJhbGci..." }
β’ νλ‘ νΈ: ν ν° μ μ₯ (localStorage λλ httpOnly Cookie)
β£ μ΄ν API μμ² μ ν ν° μ²¨λΆ
Authorization: Bearer eyJhbGci...
β€ μλ²: ν ν° κ²μ¦ ν μμ² μ²λ¦¬
// νλ‘ νΈ - λ‘κ·ΈμΈ
const loginRes = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { accessToken } = await loginRes.json();
localStorage.setItem('token', accessToken);
// νλ‘ νΈ - μΈμ¦μ΄ νμν API μμ²
const res = await fetch('/api/users/me', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
@Configuration
public class SecurityConfig {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // μΈμ¦ μμ΄ μ κ·Ό κ°λ₯
.requestMatchers("/api/admin/**").hasRole("ADMIN") // ADMINλ§ μ κ·Ό κ°λ₯
.anyRequest().authenticated() // λλ¨Έμ§λ λ‘κ·ΈμΈ νμ
)
}
/api/auth/login β λꡬλ μ κ·Ό κ°λ₯
/api/users/me β λ‘κ·ΈμΈν μ¬μ©μλ§
/api/admin/stats β ADMIN κΆνλ§
β 401: ν ν° μμ / λ§λ£
β 403: κΆν λΆμ‘±
λ‘컬 κ°λ°ν λ μ΄λ° μλ¬ λ³΄μ μ μμΌμ κ°μ?
Access to fetch at 'http://localhost:8080/api/users' from origin
'http://localhost:3000' has been blocked by CORS policy
CORSλ λ€λ₯Έ μΆμ²(Origin)μμ μ€λ μμ²μ λΈλΌμ°μ κ° μ°¨λ¨νλ 보μ μ μ±
μ
λλ€.
λ°±μλμμ νμ© μ€μ μ ν΄μ€μΌ ν΄κ²°λ©λλ€.
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000"); // κ°λ° μλ²
config.addAllowedOrigin("https://my-app.com"); // νλ‘λμ
μλ²
config.addAllowedMethod("*"); // GET, POST, PUT, DELETE λͺ¨λ νμ©
config.addAllowedHeader("*"); // λͺ¨λ ν€λ νμ©
config.setAllowCredentials(true); // μΏ ν€ ν¬ν¨ μμ² νμ©
// ...
}
}
π‘ CORS μλ¬κ° λλ©΄ λ°±μλμ "νλ‘ νΈ origin νμ©ν΄μ£ΌμΈμ" λΌκ³ μμ²νλ©΄ λ©λλ€.
νλ‘ νΈμμ ν΄κ²°ν μ μλ λ¬Έμ κ° μλλλ€.
Spring Bootμμλ Swagger(OpenAPI)λ‘ API λ¬Έμλ₯Ό μλμΌλ‘ λ§λ€μ΄μ€λλ€.
http://localhost:8080/swagger-ui/index.html
λ°±μλμ Swaggerκ° μ€μ λμ΄ μλ€λ©΄ μ μ£Όμμμ λͺ¨λ APIλ₯Ό νμΈνκ³ μ§μ ν μ€νΈν΄λ³Ό μ μμ΅λλ€.
Swaggerμμ νμΈν μ μλ κ²λ€:
β
μλν¬μΈνΈ URL
β
HTTP λ©μλ (GET, POST, PUT, DELETE)
β
μμ² νλΌλ―Έν° / Body ꡬ쑰
β
μλ΅ κ΅¬μ‘° (DTO)
β
HTTP μν μ½λλ³ μλ΅
β
μ§μ API νΈμΆ ν
μ€νΈ
1. λ°±μλ κ°λ°μκ° API μ€κ³ (μλν¬μΈνΈ, μμ²/μλ΅ DTO μ μ)
2. Swagger λλ NotionμΌλ‘ API λͺ
μΈ κ³΅μ
- URL, Method, Request Body, Response ꡬ쑰, μλ¬ μ½λ
3. νλ‘ νΈ κ°λ°μκ° TypeScript νμ
μ μ
- λ°±μλ DTO κΈ°λ°μΌλ‘ interface μμ±
4. MSWλ‘ API λͺ¨νΉ ν νλ‘ νΈ κ°λ° λ³ν
- λ°±μλ μμ± μ μλ νλ‘ νΈ κ°λ° κ°λ₯
5. λ°±μλ μμ± ν μ€μ API μ°λ & ν
μ€νΈ
- CORS, μΈμ¦ ν ν°, μλ¬ μ²λ¦¬ νμΈ
src/
βββ main/
βββ java/
βββ com.example.app/
βββ controller/ β API μλν¬μΈνΈ (νλ‘ νΈκ° μ§μ μ¬μ©)
β βββ UserController.java
βββ service/ β λΉμ¦λμ€ λ‘μ§
β βββ UserService.java
βββ repository/ β DB μ κ·Ό
β βββ UserRepository.java
βββ entity/ β DB ν
μ΄λΈ ꡬ쑰
β βββ User.java
βββ dto/ β μμ²/μλ΅ λ°μ΄ν° ꡬ쑰 (νλ‘ νΈμ μ§μ μ°κ΄)
β βββ request/
β β βββ UserRequest.java
β βββ response/
β βββ UserResponse.java
βββ exception/ β μλ¬ μ μ & μ²λ¦¬
β βββ GlobalExceptionHandler.java
βββ config/ β μ€μ (CORS, Security, Swagger)
βββ CorsConfig.java
βββ SecurityConfig.java
βββ SwaggerConfig.java
β
API λͺ
μΈ λ¬Έμκ° μλμ? (Swagger URL λλ Notion)
β
μΈμ¦ λ°©μμ΄ JWTμΈκ°μ, μΈμ
μΈκ°μ?
- JWTλΌλ©΄ ν ν°μ μ΄λ€ ν€λμ λ£μ΄μΌ νλμ?
β
μλ¬ μλ΅ ν¬λ§·μ΄ μ΄λ»κ² λλμ?
- { code, message } μΈκ°μ?
β
λ μ§/μκ° ν¬λ§·μ΄ μ΄λ»κ² λλμ?
- "2024-01-01T00:00:00" (ISO 8601)?
β
νμ΄μ§λ€μ΄μ
μ μ΄λ»κ² μ²λ¦¬νλμ?
- ?page=0&size=10 μΈκ°μ?
- μλ΅μ totalPages, totalElementsκ° ν¬ν¨λλμ?
β
CORS νμ© Originμ μ λ‘컬 μ£Όμ(localhost:3000) μΆκ°ν΄μ€ μ μλμ?
β
νμΌ μ
λ‘λλ multipart/form-dataμΈκ°μ?
Spring Bootλ₯Ό μ§μ κ°λ°ν νμλ μμ§λ§, ꡬ쑰λ₯Ό μ΄ν΄νλ©΄ λ°±μλ κ°λ°μμμ μν΅μ΄ ν¨μ¬ μνν΄μ§λλ€.
μ΄ λ€μ― κ°μ§λ§ μ΄ν΄ν΄λ, νμ μ "μ΄ μλ¬κ° μ΄λμ λλ 건μ§", "μ΄ APIκ° μ΄λ€ λ°μ΄ν°λ₯Ό μ£Όλ 건μ§" ν¨μ¬ λΉ λ₯΄κ² νμ ν μ μμ΅λλ€. π
π μ°Έκ³