C
Spring Boot/웹MVC/Lesson 06

REST API CRUD 실습 — 처음부터 끝까지

1시간·theory
이 챕터
2/2

REST API CRUD 실습 — 처음부터 끝까지

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ @RestController + @RequestMapping 으로 REST API 4종 (GET/POST/PUT/DELETE) 구현
  • ✅ @RequestBody / @PathVariable / @RequestParam 차이 + 사용
  • ✅ @RestControllerAdvice 로 전역 예외 처리 패턴

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

CRUD 의 4가지 HTTP 메서드

REST 의 기본 약속

작업HTTP 메서드URL 예시응답
목록 조회GET/users200 + 배열
단건 조회GET/users/1200 + 객체 / 404
생성POST/users201 + 생성된 객체
수정PUT/users/1200 + 수정된 객체
삭제DELETE/users/1204 (No Content)

URL 은 명사 (자원), 동작은 HTTP 메서드 가 REST 핵심.

1. User 도메인 + DTO

java
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
public class User {
    private Long id;
    private String email;
    private String name;
}

@Getter @Setter
public class UserCreateRequest {
    private String email;
    private String name;
}

Entity 와 DTO 분리 가 표준. DTO 는 API 입출력 전용, Entity 는 DB 매핑 전용.

2. 임시 저장소 (DB 대신 메모리)

java
@Repository
public class UserRepository {
    private final Map<Long, User> store = new ConcurrentHashMap<>();
    private final AtomicLong seq = new AtomicLong();

    public List<User> findAll()            { return new ArrayList<>(store.values()); }
    public Optional<User> findById(Long id) { return Optional.ofNullable(store.get(id)); }
    public User save(User u) {
        if (u.getId() == null) u.setId(seq.incrementAndGet());
        store.put(u.getId(), u);
        return u;
    }
    public void deleteById(Long id) { store.remove(id); }
}

3. 서비스 레이어

java
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository repo;

    public List<User> list() { return repo.findAll(); }

    public User get(Long id) {
        return repo.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found: " + id));
    }

    public User create(UserCreateRequest req) {
        return repo.save(new User(null, req.getEmail(), req.getName()));
    }

    public User update(Long id, UserCreateRequest req) {
        User u = get(id);
        u.setEmail(req.getEmail());
        u.setName(req.getName());
        return repo.save(u);
    }

    public void delete(Long id) { repo.deleteById(id); }
}

Controller 가 직접 Repository 를 만지지 않습니다 — Service 가 비즈니스 로직 담당.

Controller — @RequestBody · @PathVariable · ResponseEntity

4가지 어노테이션의 역할

  • @RequestBody — HTTP body 의 JSON 을 객체로 변환
  • @PathVariable — URL 경로의 {id} 같은 값 추출
  • @RequestParam?page=1 같은 쿼리스트링 추출
  • ResponseEntity상태 코드 + 헤더 + 본문 직접 제어

컨트롤러 전체 코드

java
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService service;

    // GET /users
    @GetMapping
    public List<User> list() {
        return service.list();
    }

    // GET /users/{id}
    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        return service.get(id);
    }

    // POST /users
    @PostMapping
    public ResponseEntity<User> create(@RequestBody UserCreateRequest req) {
        User created = service.create(req);
        return ResponseEntity
            .status(HttpStatus.CREATED)            // 201
            .body(created);
    }

    // PUT /users/{id}
    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @RequestBody UserCreateRequest req) {
        return service.update(id, req);
    }

    // DELETE /users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        service.delete(id);
        return ResponseEntity.noContent().build();    // 204
    }
}

응답 본문이 객체일 때 — Jackson 이 자동 직렬화

@RestController 가 붙어있으면 모든 반환값이 자동으로 JSON 으로 변환 됩니다. 별도 설정 없이 Map·List·DTO 그대로 리턴.

상태 코드를 신경 써야 할 때

조회 (GET) 는 그냥 객체 리턴, 생성 (POST) 은 201, 삭제 (DELETE) 는 204 — 이걸 명시하려고 ResponseEntity 를 씁니다.

실무에서는 모든 응답을 ResponseEntity 로 통일 하는 팀과 간단한 GET 은 객체만 리턴 하는 팀으로 갈립니다. 둘 다 정답.

전역 예외 처리 — @ControllerAdvice

매번 try-catch 는 지옥

java
// ❌ 컨트롤러마다 예외 처리
@GetMapping("/{id}")
public ResponseEntity<?> get(@PathVariable Long id) {
    try {
        return ResponseEntity.ok(service.get(id));
    } catch (NotFoundException e) {
        return ResponseEntity.status(404).body(e.getMessage());
    }
}

수십 개 메서드마다 try-catch → 보일러플레이트 폭발.

@ControllerAdvice예외 전담 빈

java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", e.getMessage()));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadInput(IllegalArgumentException e) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse("BAD_REQUEST", e.getMessage()));
    }

    @ExceptionHandler(Exception.class)   // *최후의 보루*
    public ResponseEntity<ErrorResponse> handleAll(Exception e) {
        log.error("Unhandled exception", e);
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "서버 오류"));
    }
}

@Getter @AllArgsConstructor
class ErrorResponse {
    private String code;
    private String message;
}

모든 컨트롤러의 예외를 한 곳에서 처리. 컨트롤러 코드는 비즈니스만 집중할 수 있게 됩니다.

커스텀 예외 클래스

java
public class NotFoundException extends RuntimeException {
    public NotFoundException(String msg) { super(msg); }
}

RuntimeException 을 상속 → throws 선언 불필요. 도메인별 (UserNotFoundException·OrderNotFoundException) 로 만들면 코드의 의도가 명확.

curl 테스트

bash
# 1. 생성
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Alice"}'
# → 201 {"id":1,"email":"[email protected]","name":"Alice"}

# 2. 조회
curl http://localhost:8080/users/1
# → 200 {"id":1,...}

# 3. 없는 ID 조회
curl -i http://localhost:8080/users/999
# → 404 {"code":"NOT_FOUND","message":"User not found: 999"}

# 4. 삭제
curl -X DELETE http://localhost:8080/users/1 -i
# → 204 No Content

Postman 사용자는 Collection 으로 4개 요청을 저장해 두면 원클릭 테스트 가 됩니다.

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 컨트롤러에 @ControllerAdvice 기반 전역 예외 처리 추가해줘"
  • "User CRUD API 4개 (GET/POST/PUT/DELETE) 컨트롤러 만들어줘"
  • "이 응답을 ResponseEntity 로 감싸서 201 / 204 상태코드 정확히 반환하게 해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

REST API CRUD 실습 — 처음부터 끝까지 - Spring Boot