[데브코스] Spring Boot 인증·인가(Auth) (35강) - Rq 클래스에서 @RequestScope를 빼도 되는 이유

zuno·2026년 1월 13일

35강을 듣다 보면 이런 생각이 든다.

“Rq는 싱글톤이어도 된다면서?”
“컨트롤러도 하나인데 요청이 동시에 들어오면 섞이는 거 아니야?”
“getActor() 같은 메서드가 여러 번 동시에 실행되면 위험한 거 아냐?”

이 글에서는 이 질문들을 초보자 기준으로 하나씩 정리한다.


1️⃣ 먼저 결론부터

먼저 결론을 정확하게 적어두자.

Rq는 ‘조건부로’ 싱글톤이어도 된다.
그리고 지금 구조에서는 동시 요청이 와도 섞이지 않는다.

단, 왜 괜찮은지 이유를 이해하지 않으면
“되는 것처럼 보이는데 사실 위험한 코드”를 만들기 쉽다.


2️⃣ 컨트롤러는 1개인데 요청이 여러 개면 왜 괜찮을까?

Spring 서버에서 가장 많이 오해하는 지점이다.

@RestController
public class ApiV1PostController {
}

컨트롤러가 하나라는 말은,

  • ❌ 요청이 하나씩 처리된다
  • ❌ 메서드가 한 번에 하나만 실행된다

가 아니다.

실제 동작 방식

  • 요청 100개가 동시에 들어오면
  • 서버는 스레드 풀에서 여러 스레드를 꺼낸다
  • 각 스레드가 같은 컨트롤러 메서드를 동시에 실행한다

즉,

컨트롤러 객체 1개
 ├─ Thread-1 → write()
 ├─ Thread-2 → delete()
 ├─ Thread-3 → modify()

👉 객체 수와 동시 실행은 전혀 다른 개념이다.


3️⃣ 그럼 getActor()는 어떻게 실행될까?

요청이 3개 동시에 들어오면,
같은 메서드 코드가 스레드별로 동시에 실행된다.

public Member getActor() {
    String headerAuthorization = req.getHeader("Authorization");

    if (headerAuthorization == null || headerAuthorization.isBlank())
        throw new ServiceException("401-1", "Authorization 헤더가 존재하지 않습니다.");

    if (!headerAuthorization.startsWith("Bearer "))
        throw new ServiceException("401-2", "Authorization 헤더가 Bearer 형식이 아닙니다.");

    String apiKey = headerAuthorization.substring("Bearer ".length()).trim();

    Member member = memberService
            .findByApiKey(apiKey)
            .orElseThrow(() -> new ServiceException("401-3", "API 키가 유효하지 않습니다."));

    return member;
}

요청 3개 → 실제 실행 흐름

  • 요청 A → Thread-1에서 getActor() 실행
  • 요청 B → Thread-2에서 getActor() 실행
  • 요청 C → Thread-3에서 getActor() 실행

👉 코드는 같지만 실행 흐름은 완전히 분리된다.


4️⃣ 그럼 왜 안 섞일까? (핵심 이유)

이유 1️⃣ 메서드 안 변수는 전부 지역변수다

String headerAuthorization
String apiKey
Member member

이 변수들은:

  • 각 스레드의 스택 영역에 생성
  • 다른 스레드가 접근 불가능
  • 서로 절대 공유되지 않음

👉 지역변수는 동시 실행돼도 섞일 수 없다.


이유 2️⃣ HttpServletRequest는 프록시다

private final HttpServletRequest req;

req진짜 request 객체가 아니다.

  • 스프링이 주입하는 프록시 객체
  • 메서드 호출 시점에
  • 현재 스레드가 처리 중인 요청의 실제 request로 연결된다

즉,

  • Thread-1 → 요청 A의 request
  • Thread-2 → 요청 B의 request
  • Thread-3 → 요청 C의 request

👉 같은 req 필드를 써도 실제 데이터는 분리된다.


5️⃣ 그럼 언제 진짜로 섞일까?

딱 한 가지 경우다.

요청 데이터를 필드(전역 변수)에 저장할 때

❌ 위험한 예시:

@Component
public class Rq {
    private String apiKey; // ❌ 공유 필드

    public Member getActor() {
        this.apiKey = req.getHeader("Authorization");
        ...
    }
}

이러면:

  • 요청 A가 apiKey = A
  • 요청 B가 apiKey = B
  • 서로 덮어쓰면서 섞인다

👉 이 경우에는 @RequestScope가 필요하다.


6️⃣ 그래서 @RequestScope는 언제 필요할까?

@RequestScope가 필요한 경우

  • 요청별 상태를 필드에 저장해야 할 때
  • 요청 동안 값이 유지돼야 할 때
@Component
@RequestScope
public class Rq {
    private Member actor; // 요청별 상태
}

@RequestScope가 없어도 되는 경우 (현재 구조)

  • 요청 데이터를 필드에 저장하지 않음
  • 매번 request에서 읽어서
  • 지역변수로만 사용
@Component
public class Rq {
    private final HttpServletRequest req;

    public Member getActor() {
        return ...
    }
}

👉 이 경우 싱글톤이어도 안전하다.


7️⃣ 초보자용 한 문장 정리

같은 메서드가 동시에 실행돼도
요청 데이터가 지역변수와 ThreadLocal 기반 request에서만 사용되면
동시 요청은 절대 섞이지 않는다.


8️⃣ 최종 핵심 요약

  • 컨트롤러는 1개여도 된다
  • 요청은 스레드 단위로 처리된다
  • 같은 메서드는 여러 스레드에서 동시에 실행될 수 있다
  • 지역변수는 스레드마다 분리된다
  • HttpServletRequest는 프록시라 요청별로 안전하다
  • 요청 데이터를 필드에 저장하면 위험하다
  • 그래서 @RequestScope는 선택 사항이다

이 강의의 진짜 핵심은 이 문장이다.

“싱글톤이 위험한 게 아니라,
싱글톤에 요청 상태를 저장하는 게 위험하다.”

0개의 댓글