Home > Spring > πŸƒ[Spring] APIμ—μ„œ μ˜ˆμ™Έ λ˜μ§€κΈ°.

πŸƒ[Spring] APIμ—μ„œ μ˜ˆμ™Έ λ˜μ§€κΈ°.
Spring Framework

πŸƒ[Spring] APIμ—μ„œ μ˜ˆμ™Έ λ˜μ§€κΈ°.

Java λ°±μ—”λ“œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ API 호좜 쀑 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ©΄, μ˜ˆμ™Έκ°€ μ μ ˆν•˜κ²Œ μ²˜λ¦¬λ˜μ§€ μ•ŠλŠ” ν•œ ν΄λΌμ΄μ–ΈνŠΈμ— 였λ₯˜ 응닡이 λ°˜ν™˜λ©λ‹ˆλ‹€.

기본적으둜 μ˜ˆμ™Έκ°€ λ°œμƒν•˜λ©΄ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ˜ˆμ™Έκ°€ λ°œμƒν•œ μ‹œμ μ—μ„œ 싀행을 μ€‘λ‹¨ν•˜κ³ , μ˜ˆμ™Έμ— λŒ€ν•œ 처리λ₯Ό ν•˜μ§€ μ•ŠμœΌλ©΄ λ‚΄λΆ€ μ„œλ²„ 였λ₯˜(HTTP 500)λ₯Ό ν΄λΌμ΄μ–ΈνŠΈμ— λ°˜ν™˜ν•˜κ²Œ λ©λ‹ˆλ‹€.

1️⃣ μ˜ˆμ™Έ 처리 흐름.

1. μ˜ˆμ™Έ λ°œμƒ.

  • API의 μ»¨νŠΈλ‘€λŸ¬λ‚˜ μ„œλΉ„μŠ€ λ ˆμ΄μ–΄μ—μ„œ νŠΉμ • λ‘œμ§μ„ μˆ˜ν–‰ν•˜λŠ” 도쀑 μ˜ˆμ™Έκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ˜ˆμ™Έκ°€ λ°œμƒν•˜λ©΄ μ‹€ν–‰ 흐름이 μ˜ˆμ™Έ 처리 λΈ”λ‘μœΌλ‘œ λ„˜μ–΄κ°€κ±°λ‚˜, μ˜ˆμ™Έκ°€ 호좜된 λ©”μ„œλ“œλ₯Ό 톡해 μƒμœ„λ‘œ μ „νŒŒλ©λ‹ˆλ‹€.

2. μ˜ˆμ™Έ μ „νŒŒ.

  • λ§Œμ•½ ν•΄λ‹Ή μ˜ˆμ™Έλ₯Ό try-catch 블둝 λ‚΄μ—μ„œ μ²˜λ¦¬ν•˜μ§€ μ•ŠμœΌλ©΄ μ˜ˆμ™ΈλŠ” 호좜된 λ©”μ„œλ“œλ₯Ό 따라 μƒμœ„λ‘œ μ „νŒŒλ©λ‹ˆλ‹€.
  • 이 κ³Όμ •μ—μ„œ μ΅œμ’…μ μœΌλ‘œ 컨트둀러 λ ˆλ²¨μ΄λ‚˜ κ·Έ μ΄μƒμ—μ„œ μ˜ˆμ™Έλ₯Ό μž‘μ§€ μ•ŠμœΌλ©΄, Spring Framework와 같은 λ°±μ—”λ“œ ν”„λ ˆμž„μ›Œν¬κ°€ μ˜ˆμ™Έλ₯Ό λ°›μ•„ μ²˜λ¦¬ν•˜κ²Œ λ©λ‹ˆλ‹€.

3. Spring의 κΈ°λ³Έ λ™μž‘.

  • Spring MVCμ—μ„œλŠ” μ˜ˆμ™Έκ°€ μ „νŒŒλ˜μ–΄ μ»¨νŠΈλ‘€λŸ¬κΉŒμ§€ λ„λ‹¬ν•˜κ³ λ„ μ²˜λ¦¬λ˜μ§€ μ•ŠμœΌλ©΄, Spring이 DefaultHandlerExceptionResolverλ₯Ό 톡해 기본적인 μ˜ˆμ™Έ 처리λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.
  • μ²˜λ¦¬λ˜μ§€ μ•Šμ€ μ˜ˆμ™ΈλŠ” 기본적으둜 HTTP μƒνƒœ μ½”λ“œ 500(λ‚΄λΆ€ μ„œλ²„ 였λ₯˜)둜 λ§€ν•‘λ˜μ–΄ ν΄λΌμ΄μ–ΈνŠΈμ— λ°˜ν™˜λ©λ‹ˆλ‹€.
  • ν΄λΌμ΄μ–ΈνŠΈλŠ” 이 μƒνƒœ μ½”λ“œλ₯Ό 톡해 μ„œλ²„μ—μ„œ 였λ₯˜κ°€ λ°œμƒν–ˆμŒμ„ μΈμ‹ν•˜κ²Œ λ©λ‹ˆλ‹€.

2️⃣ μ˜ˆμ™Έ 처리λ₯Ό μœ„ν•œ 방법.

1. @ExceptionHandler μ‚¬μš©.

  • Spring MVCμ—μ„œλŠ” 컨트둀러 λ ˆλ²¨μ—μ„œ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ @ExceptionHandler μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • νŠΉμ • μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμ„ λ•Œ ν•΄λ‹Ή λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ–΄ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κ³ , μ μ ˆν•œ 응닡을 ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ λ°˜ν™˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
@RestController
public class MyController {
    @GetMapping("/example")
    public String example() {
        if (someConditionFails()) {
            throw new MyCustomException("Something went wrong!");
        }
        return "success";
    }
    
    @ExceptionHandler(MyCustomException.class)
    public ResponseEntity<String> handleMyCustomException(MyCustomException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

이 경우 MyCustomException이 λ°œμƒν•˜λ©΄ HTTP μƒνƒœ μ½”λ“œ 400κ³Ό ν•¨κ»˜ μ˜ˆμ™Έ λ©”μ‹œμ§€κ°€ λ°˜ν™˜λ©λ‹ˆλ‹€.

2. @ControllerAdvice μ‚¬μš©.

  • @ControllerAdviceλŠ” μ „μ—­ μ˜ˆμ™Έ 처리기λ₯Ό μ •μ˜ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€.
  • 이λ₯Ό 톡해 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ „λ°˜μ— λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό μ€‘μ•™μ—μ„œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MyCustomException.class)
    public ResponseEntity<String> handleMyCustomException(MyCustomException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception ex) {
        return new ResponseEntity<>("An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

이 경우 νŠΉμ • μ˜ˆμ™ΈλΏλ§Œ μ•„λ‹ˆλΌ λͺ¨λ“  일반적인 μ˜ˆμ™Έλ„ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

3. ResponseEntity와 ν•¨κ»˜ μ˜ˆμ™Έλ₯Ό λ°˜ν™˜.

  • ResponseEntityλ₯Ό μ‚¬μš©ν•˜μ—¬ 직접 μ˜ˆμ™Έ λ°œμƒ μ‹œ HTTP 응닡과 μƒνƒœ μ½”λ“œλ₯Ό λ°˜ν™˜ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
@GetMapping("/example")
public ResponseEntity<String> example() {
    if (someConditionFails()) {
        return new ResponseEntity<>("Custom error message", HttpStatus.BAD_REQUEST);
    }
    return new ResponseEntiry<>("success", HttpStatus.OK);
}

3️⃣ μ‹€μ œ μ½”λ“œ 사둀.

// UserUpdateRequest - DTO
public class UserUpdateRequest {
    private long id;
    private String name;
    
    public long getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
}

// UserController - Controller
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequest request) {
    String sql = "UPDATE user SET name = ? WHERE id = ?";
    jdbcTemplate.update(sql, request.getName(), request.getId());
}

@DeleteMapping("/user")
public void deleteUser(@RequestParam String name) {
    String sql = "DELETE FROM user WHERE name = ?";
    jdbcTemplate.update(sql, name);
}

1. 문제 상황.

  • μœ„ μ½”λ“œμ—μ„œμ˜ 문제 상황은 μ—†λŠ” μœ μ €λ₯Ό μ—…λ°μ΄νŠΈ ν•˜κ±°λ‚˜ μ‚­μ œν•˜λ € 해도 200 OKκ°€ λ‚˜μ˜¨λ‹€λŠ” μ μž…λ‹ˆλ‹€.

2. ν•΄κ²° λ°©μ•ˆ.

  • APIμ—μ„œ μ˜ˆμ™Έλ₯Ό λ˜μ € 500 Internal Server Error이 λ‚˜μ˜€κ²Œ ν•˜λ©΄λ©λ‹ˆλ‹€.
@GetMapping("/user/error-test")
public void errorTest() {
    throw new IllegalArgumentException();
}

  • POSTMAN을 ν™œμš©ν•˜μ—¬ http://localhost:8080/user/error-test둜 GET ν˜ΈμΆœμ„ 보내면 500 Internal Server Error이 λ°œμƒν•©λ‹ˆλ‹€.

4️⃣ ν™œμš© μ˜ˆμ‹œ.

// UserUpdateRequest - DTO
public class UserUpdateRequest {
    private long id;
    private String name;
    
    public long getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
}

// UserController - Controller
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequest request) {
    String readSql = "SELECT * FROM user WHERE id = ?";
    boolean isUserExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0 request.getId()).isEmpty();
    
    if (isUserNotExist) {
        throw new IllegalArgumentException();
    }
    
    String sql = "UPDATE user SET name = ? WHERE id = ?";
    jdbcTemplate.update(sql, request.getName(), request.getId());
}

@DeleteMapping("/user")
public void deleteUser(@RequestParam String name) {
    String readSql = "SELECT * FROM user WHERE name = ?";
    boolean isUserExist = jdbcTemplate.query(readSql (rs, rowNum) -> 0, name).isEmpty();
    
    if (isUserNotExist) {
        throw new IllegalArgumentException();
    }
    
    String sql = "DELETE FROM user WHERE name = ?";
    jdbcTemplate.update(sql, name);
}
  • μœ„μ™€ 같이 APIμ—μ„œ 데이터 쑴재 μ—¬λΆ€λ₯Ό 확인해 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ©΄ λ©λ‹ˆλ‹€.
String readSql = "SELECT * FROM user WHERE id = ?";
  • idλ₯Ό κΈ°μ€€μœΌλ‘œ μœ μ €κ°€ μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•˜κΈ° μœ„ν•΄ SELECT 쿼리λ₯Ό μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€.
boolean isUserExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0 request.getId()).isEmpty();
  • SQL을 λ‚ λ € DB에 데이터가 μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
    • SELECT SQL의 κ²°κ³Όκ°€ 있으면 0으둜 λ³€ν™˜λ©λ‹ˆλ‹€.
    • μ΅œμ’…μ μœΌλ‘œ List둜 λ°˜ν™˜λ©λ‹ˆλ‹€.
      • 즉, ν•΄λ‹Ή idλ₯Ό 가진 μœ μ €κ°€ 있으면 0이 λ‹΄κΈ΄ Listκ°€ λ‚˜μ˜€κ³ , μ—†λ‹€λ©΄ 빈 Listκ°€ λ‚˜μ˜€κ²Œ λ©λ‹ˆλ‹€.
      • jdbcTemplate.query()의 결과인 Listκ°€ λΉ„μ–΄μžˆλ‹€λ©΄, μœ μ €κ°€ μ—†λ‹€λŠ” λœ»μž…λ‹ˆλ‹€.
if (isUserNotExist) {
    throw new IllegalArgumentException();
}
  • λ§Œμ•½ μœ μ €κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ IllegalArgumentException을 λ˜μ§‘λ‹ˆλ‹€.