πŸƒ ν”„λ‘ νŠΈ κ°œλ°œμžκ°€ μ•Œμ•„μ•Ό ν•˜λŠ” Spring Boot ꡬ쑰

κΉ½μ§€Β·2026λ…„ 3μ›” 10일
post-thumbnail

λ°±μ—”λ“œ κ°œλ°œμžμ™€ ν˜‘μ—…ν•˜λ‹€ 보면 "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 β†’ λΈŒλΌμš°μ € μˆœμ„œλ‘œ μ˜¬λΌμ˜΅λ‹ˆλ‹€.


1️⃣ 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 경둜의 μ•žλΆ€λΆ„
@GetMappingHTTP GET μš”μ²­ μ²˜λ¦¬λ°μ΄ν„° 쑰회
@PostMappingHTTP POST μš”μ²­ μ²˜λ¦¬λ°μ΄ν„° 생성
@PutMappingHTTP PUT μš”μ²­ μ²˜λ¦¬λ°μ΄ν„° 전체 μˆ˜μ •
@PatchMappingHTTP PATCH μš”μ²­ μ²˜λ¦¬λ°μ΄ν„° 일뢀 μˆ˜μ •
@DeleteMappingHTTP DELETE μš”μ²­ μ²˜λ¦¬λ°μ΄ν„° μ‚­μ œ
@PathVariableURL 경둜의 λ³€μˆ˜ (/users/{id})URL νŒŒλΌλ―Έν„°
@RequestParam쿼리 νŒŒλΌλ―Έν„° (?page=1)Query String
@RequestBodyμš”μ²­ λ³Έλ¬Έ(JSON)을 객체둜 λ³€ν™˜fetch body

ν”„λ‘ νŠΈ μ½”λ“œμ™€ 1:1 λ§€ν•‘

// ν”„λ‘ νŠΈ
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"
}

2️⃣ DTO β€” ν”„λ‘ νŠΈμ™€ μ£Όκ³ λ°›λŠ” 데이터 ν˜•νƒœ

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 λ¬Έμ„œ)λ₯Ό 톡해 μžλ™μœΌλ‘œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.


3️⃣ Service β€” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직의 μ§‘ν•©

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 λ ˆμ΄μ–΄μ—μ„œ 쀑볡 체크 ν›„ μ—λŸ¬λ₯Ό λ˜μ§€λŠ” κ²½μš°κ°€ λ§ŽμŠ΅λ‹ˆλ‹€.


4️⃣ Repository β€” DB와 λŒ€ν™”ν•˜λŠ” κ³³

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에 μ–΄λ–€ 컬럼이 있고 μ–΄λ–€ 데이터가 μ €μž₯λ˜λŠ”μ§€" μ΄ν•΄ν•˜λŠ” 데 도움이 λ©λ‹ˆλ‹€.


5️⃣ Entity β€” DB ν…Œμ΄λΈ”κ³Ό 1:1 λ§€ν•‘λ˜λŠ” 클래슀

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μ—λŠ” λΉ μ§‘λ‹ˆλ‹€.


6️⃣ μ˜ˆμ™Έ 처리 β€” μ—λŸ¬ 응닡이 μ–΄λ–»κ²Œ μ˜€λŠ”κ°€

ν”„λ‘ νŠΈμ—μ„œ 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);
}

πŸ’‘ νŒ€λ§ˆλ‹€ μ—λŸ¬ 응닡 ꡬ쑰가 λ‹€λ¦…λ‹ˆλ‹€. λ°±μ—”λ“œ κ°œλ°œμžμ—κ²Œ μ—λŸ¬ 응닡 포맷을 κΌ­ ν™•μΈν•˜μ„Έμš”.


7️⃣ Spring Security β€” 인증/인가

둜그인, κΆŒν•œ 관리 등은 Spring Securityκ°€ λ‹΄λ‹Ήν•©λ‹ˆλ‹€.

JWT 기반 인증 흐름 (κ°€μž₯ ν”ν•œ 방식)

β‘  둜그인 μš”μ²­
   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')}`
  }
});

Spring Securityμ—μ„œ λ³΄ν˜Έλ˜λŠ” 경둜

@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: κΆŒν•œ λΆ€μ‘±

8️⃣ CORS β€” ν”„λ‘ νŠΈκ°€ κ°€μž₯ 자주 λ§Œλ‚˜λŠ” μ„€μ •

둜컬 κ°œλ°œν•  λ•Œ 이런 μ—λŸ¬ 보신 적 μžˆμœΌμ‹ κ°€μš”?

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 ν—ˆμš©ν•΄μ£Όμ„Έμš”" 라고 μš”μ²­ν•˜λ©΄ λ©λ‹ˆλ‹€.
ν”„λ‘ νŠΈμ—μ„œ ν•΄κ²°ν•  수 μžˆλŠ” λ¬Έμ œκ°€ μ•„λ‹™λ‹ˆλ‹€.


9️⃣ Swagger β€” API λ¬Έμ„œ μžλ™ 생성

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, 인증 토큰, μ—λŸ¬ 처리 확인

πŸ“ Spring Boot ν”„λ‘œμ νŠΈ 폴더 ꡬ쑰

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λ₯Ό 직접 κ°œλ°œν•  ν•„μš”λŠ” μ—†μ§€λ§Œ, ꡬ쑰λ₯Ό μ΄ν•΄ν•˜λ©΄ λ°±μ—”λ“œ κ°œλ°œμžμ™€μ˜ μ†Œν†΅μ΄ 훨씬 μ›ν™œν•΄μ§‘λ‹ˆλ‹€.

  • Controller = λ‚΄κ°€ ν˜ΈμΆœν•˜λŠ” API μ—”λ“œν¬μΈνŠΈ
  • DTO = API둜 μ£Όκ³ λ°›λŠ” 데이터 νƒ€μž… (TypeScript interface와 동일)
  • Service = 둜직 처리 (μ—λŸ¬κ°€ μ—¬κΈ°μ„œ 많이 λ°œμƒ)
  • Repository = DB μ ‘κ·Ό
  • Entity = DB ν…Œμ΄λΈ” ꡬ쑰

이 λ‹€μ„― κ°€μ§€λ§Œ 이해해도, ν˜‘μ—… μ‹œ "이 μ—λŸ¬κ°€ μ–΄λ””μ„œ λ‚˜λŠ” 건지", "이 APIκ°€ μ–΄λ–€ 데이터λ₯Ό μ£ΌλŠ” 건지" 훨씬 λΉ λ₯΄κ²Œ νŒŒμ•…ν•  수 μžˆμŠ΅λ‹ˆλ‹€. πŸš€


πŸ“š μ°Έκ³ 

0개의 λŒ“κΈ€