본문 바로가기
Back-end & 알고리즘

@RestController vs @Controller, 실무자가 겪는 장애의 진짜 이유

by CodeByJin 2026. 6. 29.
반응형

많은 주니어 개발자가 스프링 프레임워크를 학습할 때, @RestController는 데이터를 반환하고 @Controller는 View를 반환한다는 아주 단순한 차이로 접근합니다. 하지만 실무 시스템에서 이 두 어노테이션의 선택은 단순히 '어떤 응답을 보낼 것인가'를 넘어, 운영 복잡도, 디버깅 난이도, 그리고 시스템 전반의 아키텍처 일관성을 결정짓는 전략적 선택이 됩니다.

 

특히 AI 기반 서비스나 고성능 백엔드 시스템을 설계할 때, 이 선택은 단순히 문법적 차이를 넘어 유지보수 비용에 직접적인 영향을 미칩니다. 이 글에서는 기술적 정의를 나열하는 대신, 왜 실무에서는 때때로 '당연해 보이는 선택'이 장애를 유발할 수 있는지, 그 트레이드오프를 중심으로 살펴봅니다.

실무에서 마주하는 함정: '혼용'의 공포

가장 빈번하게 발생하는 문제는 프로젝트 내에서 아무런 기준 없이 두 어노테이션을 섞어 쓰는 경우입니다. 이는 기술적인 오류가 아니라 '팀의 인지 부하(Cognitive Load)'를 가중시키는 설계 미스입니다.

 

대표적으로 AI 모델 추론 API를 개발할 때, 일부 엔드포인트는 @RestController를 쓰고, 내부 관리자 페이지를 위한 SSR(Server Side Rendering)용 엔드포인트는 @Controller를 사용하는 경우가 있습니다. 이 구조 자체는 잘못되지 않았습니다. 하지만 공통 예외 처리(Global Exception Handling)가 꼬이기 시작하면 이야기가 달라집니다.

  • 문제 상황: @RestControllerAdvice는 기본적으로 JSON 응답을 가정합니다. 하지만 @Controller가 혼재된 환경에서 에러 발생 시, 누군가는 JSON을 기대하고 누군가는 500 에러 페이지(HTML)를 기대하게 됩니다.
  • 운영 관점: 장애 발생 시 프론트엔드와 백엔드 간에 "에러 응답 형식이 왜 HTML인가요?" 혹은 "왜 API인데 302 리다이렉트가 발생하나요?"와 같은 불필요한 커뮤니케이션이 발생합니다. 이는 장애 대응의 골든타임을 갉아먹는 주범입니다.

스프링 @RestController @Controller 차이 비교
스프링 @RestController @Controller 차이 비교

 

프로젝트를 설계할 때 아래 체크리스트를 통해 기술 선택의 명분을 확보해야 합니다.

판단 항목 @RestController 선택 기준 @Controller 선택 기준
핵심 목적 순수 데이터 전달 (API 중심) 사용자 화면 제공 (SSR 중심)
장애 전파 범위 JSON 구조 깨짐, 직렬화 실패 템플릿 엔진 에러, 리다이렉트 루프
인터페이스 일관성 높음 (모든 응답이 동일 규격) 낮음 (상태에 따라 응답 형식 다름)

AI/LLM 서비스 구현 시 고려사항

특히 LLM 기반의 생성형 AI 서비스를 운영할 때는 Inference Latency(추론 지연 시간)가 핵심입니다. 이때 @RestController를 사용하면서 무심코 복잡한 객체를 직렬화(Serialization)하는 코드를 넣으면 문제가 발생합니다.


// 예시 코드: AI 추론 요청 처리 (운영 환경 관점)
@RestController
@RequestMapping("/api/v1/chat")
public class ChatController {

    private final ChatService chatService;

    // AI 추론은 보통 긴 시간이 소요됨. 
    // 타임아웃 처리가 누락되면 서버의 스레드 풀이 고갈됨.
    @PostMapping("/stream")
    public SseEmitter streamResponse(@RequestBody ChatRequest request) {
        log.info("Inference request start: {}", request.getRequestId());
        
        try {
            return chatService.processStream(request);
        } catch (InferenceTimeoutException e) {
            log.error("Inference latency exceeded. RequestId: {}", request.getRequestId());
            throw new CustomApiException("추론 시간이 초과되었습니다.", HttpStatus.REQUEST_TIMEOUT);
        }
    }
}

 

코드 리뷰 포인트: 위 코드에서 SseEmitter를 사용할 때 주의할 점은 '메모리 점유율'입니다. @RestController는 내부적으로 Jackson을 통해 직렬화 과정을 거치는데, 추론 결과가 대용량 텍스트일 경우 GC(Garbage Collection) 부하가 비약적으로 상승합니다. 성능이 중요한 지점에서는 단순히 API 구조를 맞추는 것에 그치지 않고, 응답 스트리밍 방식과 메모리 버퍼 최적화를 반드시 병행해야 합니다.

반응형

"기술은 일관성이 성능보다 우선할 때가 있다"

흔히 "어떤 어노테이션이 더 빠른가요?"라고 묻곤 합니다. 하지만 정답은 @RestController와 @Controller의 미세한 성능 차이는 무의미하다는 것입니다. 더 중요한 것은 '팀의 생산성'입니다. 우리 서비스가 API 위주의 마이크로서비스 아키텍처라면, 설령 화면이 필요한 페이지가 존재하더라도 그것을 별도의 프론트엔드 프로젝트(React, Next.js 등)로 분리하고 백엔드는 모든 컨트롤러를 @RestController로 통일하는 것이 운영 복잡도를 낮추는 최선의 전략입니다.

 

결국 실무자로서 가져야 할 태도는 "이 기술이 제공하는 편의 기능을 다 쓸 것인가?"가 아니라, "이 기술을 선택했을 때 우리 팀의 장애 대응 방식(Logging, Error Handling)이 얼마나 일관되게 유지될 것인가?"를 고민하는 것입니다. 섞어 쓰지 마십시오. 가능한 하나로 통일하고, 구조적으로 다른 것은 별도의 서비스로 분리하는 것이 장기적인 유지보수 비용을 줄이는 유일한 길입니다.

 

혹시 현재 운영 중인 프로젝트에서 두 어노테이션을 혼용하고 계신가요? 혼용하고 계시다면 Global Exception 처리가 각 타입에 맞게 파편화되어 있지는 않은지 먼저 점검해 보시기 바랍니다.

 

 

스택과 큐, 무심코 썼다간 장애로 돌아옵니다: 실무자가 반드시 알아야 할 함정

많은 주니어 개발자들이 코딩 테스트를 통해 스택(Stack)과 큐(Queue)를 '그저 데이터를 넣고 빼는 도구'로 배웁니다. 하지만 실무에서 이 둘을 선택하는 기준은 '자료구조의 정의'가 아니라 운영 환

byteandbit.tistory.com

* 본 포스팅에 사용된 이미지는 생성형 AI를 통해 생성된 이미지입니다. *

반응형