인프런 커뮤니티 질문&답변

leeflection님의 프로필 이미지
leeflection

작성한 질문수

모든 개발자를 위한 HTTP 웹 기본 지식

지속 연결과 SSE

작성

·

3.4K

0

안녕하세요 강사님,

제가 프로젝트를 하며 푸시알림을 구현할 때 SSE(Server Sent Event)를 이용하여 구현을 했던 적이 있습니다.

지금에서야 http 프로토콜과 관련하여 궁금증이 생겼는데 명확하게 해결이 안되서 질문 올려봅니다!

SSE라는 기술은 HTTP의 persistence connection을 이용해서 하는 기술인 것 까지는 알게되었습니다. 이때 Spring boot 내부의 톰캣은 스레드풀을 이용해서 스레드를 관리하고,

해당 연결이 끊기지 않는다는 뜻은 이 스레드가 반환이 되지 않는 것이 아닌가 하는 의문점이 생겼습니다. 톰캣의 설정을 따로 건드리지 않는다면 200개의 스레드를 관리한다고 알고 있었는데 200명의 사용자가 푸시알림을 받기 위해 연결을 지속중인 상황이라면

다른 요청이 들어왔을때 SSE 연결이 종료되기 전까지 큐에서 대기해야 하는 상황이 생길 수 있는지 궁금합니다!

답변 3

3

안녕하세요

저도 SSE를 프로덕션에 활용하기 위해 여러가지 테스트를 해봤는데요

결과적으로 HTTP connection은 유지되나, thread까지 잡아먹진 않았습니다.

(아마 thread까지 잡아먹는다면 SSE는 이미 사장된 기술이 되지 않았을까 싶습니다..)

 

스프링부트(spring mvc + sseEmitter 사용한 sse 구현을 하셨다면)를 사용하신다면 application.yml 혹은 properties에서 아래 프로퍼티 값을 변경하시면서 테스트 해보신다면 눈으로 확인하실 수 있으실 것 같습니다.

server.tomcat.max-connections=5
server.tomcat.threads.max=5

 

leeflection님의 프로필 이미지
leeflection
질문자

답변 정말 감사합니다 경민님!

알려주신 설정 대로 방금 테스트를 진행 해보았는데 조금 신기한 결과가 있어서요!

  1. Thread.sleep()을 하는 컨트롤러

  2. sse 구독 요청을 하는 컨트롤러

  3. 단지 answer라는 단어만 반환하는 컨트롤러

테스트는 PostMan으로 진행하였습니다.

1번 테스트

  • sleep을 5번 호출한 뒤 answer을 호출한다.

    • 예상대로 answer는 반환되지 않았습니다.

2번 테스트

  • sse 구독 요청을 5번 한 뒤 answer를 호출한다. (SSE timeout 명시)

  • SseEmitter sseEmitter = new SseEmitter(100000L * 45L);

    • 1번 테스트 결과와 동일하게 answer가 반환되지 않음

3번 테스트

  • sse 구독 요청을 5번 한 뒤 answer를 호출한다. (SSE timeout 명시 X)

  • SseEmitter sseEmitter = new SseEmitter();

    • 테스트 결과와 동일한 듯 했지만 조금 기다렸다가 호출을 해보니 answer를 반환함

image

thread를 잡아먹고 있는게 아닐까요..????

@Slf4j @Controller public class SseController { private static final Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>(); @GetMapping(value = "sub",produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseEntity<SseEmitter> connect(HttpServletResponse response){ String userId = UUID.randomUUID().toString(); log.info("시작 {}",userId); SseEmitter sseEmitter = new SseEmitter(100000L * 45L); sseEmitters.put(userId, sseEmitter); log.info("id 값은 {}",sseEmitters.keySet()); sseEmitter.onCompletion(()->{ log.info("onCompletion sseEmitters {}",userId); sseEmitters.remove(userId); }); sseEmitter.onTimeout(() -> { log.info("onTimeout sseEmitter {}",userId); sseEmitters.remove(userId); }); sseEmitter.onError((e) -> { log.info("Error seeEmitter {}", userId); sseEmitters.remove(userId); }); try { sseEmitter.send(SseEmitter.event().name("connect").data("Connection")); } catch (Exception e) { e.printStackTrace(); } return ResponseEntity.ok(sseEmitter); } @GetMapping("/sleep") public ResponseEntity<String> sleepM() throws Exception{ Thread.sleep(10000000); return ResponseEntity.ok("Sleep"); } @GetMapping("/answer") public ResponseEntity<String> answer(){ return ResponseEntity.ok("answer"); } }


leeflection님의 프로필 이미지
leeflection
질문자

@Slf4j
@Controller
public class SseController {
    private static final Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();

    @GetMapping(value = "sub",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public ResponseEntity<SseEmitter> connect(HttpServletResponse response){
        String userId = UUID.randomUUID().toString();
        log.info("시작 {}",userId);

        SseEmitter sseEmitter = new SseEmitter(100000L * 45L);

        sseEmitters.put(userId, sseEmitter);
        log.info("id 값은 {}",sseEmitters.keySet());

        sseEmitter.onCompletion(()->{
            log.info("onCompletion sseEmitters {}",userId);
            sseEmitters.remove(userId);
        });
        sseEmitter.onTimeout(() -> {
            log.info("onTimeout sseEmitter {}",userId);
            sseEmitters.remove(userId);
        });
        sseEmitter.onError((e) -> {
            log.info("Error seeEmitter {}", userId);
            sseEmitters.remove(userId);
        });

        try {
            sseEmitter.send(SseEmitter.event().name("connect").data("Connection"));
        } catch (Exception e) {
            e.printStackTrace();
        }

        return ResponseEntity.ok(sseEmitter);
    }
    @GetMapping("/sleep")
    public ResponseEntity<String> sleepM() throws Exception{
        Thread.sleep(10000000);
        return ResponseEntity.ok("Sleep");
    }
    @GetMapping("/answer")
    public ResponseEntity<String> answer(){
        return ResponseEntity.ok("answer");
    }
}

혹여나 저의 짧은 지식으로 혼란만 가중시킨 것이 아닌가 걱정스런 마음으로 친절히 올려주신 코드로 저 역시 테스트를 하였습니다. 결론적으로는 제가 위에 말씀드렸던 내용처럼 커넥션만 잡아먹고 thread는 잡아먹지 않는 결과가 나왔습니다.

 

저는 아래와 같은 흐름으로 테스트 하였습니다.

  1. Thread 유지 여부 테스트

    • 우선 아래와 같이 서버에 가용가능한 쓰레드를 3개로 설정합니다.

#server.tomcat.max-connections=3

# 톰캣서버의 가용가능한 최대 쓰레드는 3개로 설정
server.tomcat.threads.max=3
# 기동시점부터 최소로 유지되어야 하는 쓰레드는 3개
server.tomcat.threads.min-spare=3

1-1) Thread.sleep() 컨트롤러 테스트

  • Thread.sleep() 컨트롤러에 3번 요청합니다.

  • 그리고 나서 answer 컨트롤러에 요청합니다.

    -> answer는 반환되지 않았습니다.

※ 1-1) 테스트 후 반드시 서버를 재기동 해주세요

1-2) sse 구독 컨트롤러 테스트

  • sse 구독 컨트롤러에 3번 요청합니다.

  • 그리고 나서 answer 컨트롤러에 요청합니다.

    -> answer가 잘 반환됩니다.

  1. Connection 유지 테스트

    • 위의 테스트에서 설정했던 Thread관련 설정은 모두 주석 처리하고, 톰캣이 맺을 수 있는 최대 Connection의 개수를 3개로 설정하는 설정만 추가합니다.

server.tomcat.max-connections=3

# 톰캣서버의 가용가능한 최대 쓰레드는 3개로 설정
#server.tomcat.threads.max=3
# 기동시점부터 최소로 유지되어야 하는 쓰레드는 3개
#server.tomcat.threads.min-spare=3

2-1) sse 구독 컨트롤러 테스트

  • sse 구독 컨트롤러에 3번 요청합니다.

  • 그리고 나서 answer 컨트롤러에 요청합니다.

    -> answer가 반환되지 않습니다.

 

질문자님께서도 위와 같은 프로세스로 한번 진행해보시고 결과 공유해주시면 감사하겠습니다.

 

leeflection님의 프로필 이미지
leeflection
질문자

다시 답변주셔서 정말 감사드립니다!!

저도 경민님과 같은 시나리오로 테스트 수행해본 결과 모두 동일하게 동작하였습니다!

server.tomcat.max-connections=5

제가 네트워크에 대한 지식이 부족해서..

연결이 끊기지 않는다는 뜻이 결국 소켓에 연결을 끊지 않는다는 의미..?이고

열려있으니 서버에서 다시 보내줄 수 있고,

쓰레드는 단지 실행단위 일뿐 http 지속 연결과는 아무 관련이 없다 라고 결론을 지어봐도 될까요..?

남겨주신 궁금증에 대해 나름대로 추상화하여 제가 가지고 있는 얇은 지식 내에서 말씀드립니다.(오류 많음 주의)

  1. Thread와 Connection은 다른 개념인가?

    -> 다른 개념입니다. 예를 들어 말씀드리면 서울에서 부산에 있는 공장까지 화물을 전달해야할 때 Connection은 서울과 부산을 잇는 고속도로, Thread는 부산 하차장에서 공장까지 화물을 옮기는 작업자라고 보시면 될 것 같습니다.

    제가 위에서 SSE가 Thread를 잡아먹는다면 이미 사장된 기술이었을 것이라 말씀드렸는데요, 위에 예시에 적용해보면 부산에 있는 작업자가 화물을 트럭에 싣고, 화물이 서울에 잘 도착할 때까지 그 자리에서 가만히 서 있는 모양이 됩니다.

    서울에서 부산으로 내려오는 화물은 계속 쌓이고 있는데(큐 대기), 작업자가 내가 보낸 화물이 서울까지 잘 갔는지 멀뚱멀뚱 기다리고 있다면 사장님 입장에선 속이 터지겠죠?(서버가 터집니다.)


  2. Thread와 HTTP 지속 연결과는 아무 관련이 없는가?
    -> 이 것은 '아무 관련이 없다' 라고 말씀드리기에는 제가 기술적 깊이가 깊지 않아 어려운 감이 있긴하지만... 그냥 제 지식 내에서 말씀드리자면, 기술이(SSE같은) 어떻게 Connection과 Thread를 사용하느냐에 따라 달라지는 것 같습니다.

    저희가 테스트한 SSE의 경우에는 Thread와 Connection(HTTP 지속연결)과 연관점이 없다고 볼 수 있으나,
    Blocking IO를 사용하는 TCP 통신(궁금하신 HTTP 지속 연결은 아니지만..)에서는 Receiver가 응답을 받을 때까지 Sender Thread가 Blocking 되니 이는 또 Connection과 Thread 사이에 연관점이 있다라고 하면 있다고 말할 수도 있는 부분이라 생각이 듭니다.

    Thread와 Connection에 관한 더 재미있는 이야기를 보시려면 C10K problem, httpd vs nginx, java NIO 에 대해 검색해보시면 좋을 것 같습니다.

leeflection님의 프로필 이미지
leeflection
질문자

친절히 답변 해주셔서 감사드립니다!

해당 부분 공부해보겠습니다!!

1

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. leeflection님

제가 SSE를 사용하지 않아서 정확한 답을 드리기는 어려운데요. 김경민님이 이야기해주신 내용이 맞다 생각합니다.

감사합니다.

leeflection님의 프로필 이미지
leeflection
질문자

감사합니다!

0

안녕하세요, 인프런 AI 인턴입니다.

SSE(Server Sent Event)는 HTTP의 persistence connection을 이용한 기술로, 서버에서 클라이언트로 데이터를 보내주는 기능을 합니다. 톰캣은 스레드풀을 이용해서 스레드를 관리하고, 해당 연결이 끊기지 않는다는 것은 스레드가 아직 반환되지 않고 해당 연결에서 작업을 계속 수행하고 있다는 것을 의미합니다.

톰캣의 설정을 따로 건드리지 않는다면, 톰캣은 기본적으로 200개의 thread를 관리합니다. 하지만, 이는 톰캣의 스레드풀에 대한 설정으로 제한이 아니기 때문에 서버에 사용 가능한 메모리에 따라 증가할 수 있습니다.

200명의 사용자가 푸시알림을 받기 위해 연결을 지속중일 때, 다른 요청이 들어왔을 때 SSE 연결이 종료되기 전까지 큐에서 대기하는 상황이 발생할 가능성이 있습니다. 이러한 상황이 발생할 경우, 다른 요청이 처리되기 전까지 클라이언트의 요청은 대기 상태가 됩니다.

따라서, 이러한 상황을 방지하려면 톰캣의 설정을 조정하거나 적절한 스레드 관리 방법을 채택해야 합니다. 또는, 대량의 사용자를 다루는 경우는 클라우드 서비스를 사용하여 확장성 있는 서비스를 제공하는 것이 좋습니다.

그러나, 이러한 상황은 일반적인 웹 개발에서 자주 발생하지 않으며, 잘 관리된 서버에서는 큰 문제가 되지 않습니다.

도움이 되셨길 바랍니다. 감사합니다.

leeflection님의 프로필 이미지
leeflection

작성한 질문수

질문하기