티스토리 뷰
지난 글에서 SSE 란 무엇인지에 대해 알아보았다. 개념과 실전은 다르다고, 이번 글에서는 직접 스프링에서 SSE를 구현해보고 내가 겪은 트러블 슈팅과, 약간의 의문점?들을 해결해나간 과정을 정리해보려고 한다. 거두절미하고 시작해보자.
SseEmitter를 이용한 SSE 구현
스프링 4.2부터는 SSE 통신을 지원하는 SseEmitter API를 제공하여 보다 쉽게 SSE를 구현할 수 있다. 뒤에서 보겠지만 클라이언트 - 서버 간 SSE 연결 시 SseEmitter
객체를 하나 생성하고 이 객체를 통해 서버 → 클라이언트로 이벤트를 전송할 수 있다.
아래는 서버가 클라이언트에게 전송하는 메시지 내용인 Notification
엔티티로, 간단하게 id
, message
필드 그리고 수신자를 의미하는 User
엔티티에 대한 참조로만 구성되어 있다.
@Getter @Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
public class Notification {
@Column(name = "notification_id")
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String message;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
public interface NotificationRepository extends JpaRepository<Notification, Long> {
}
SSE 연결 API
SSE 연결은 클라이언트 → 서버로의 SSE 연결 요청으로부터 시작한다. 아래는 클라이언트 - 서버 간 생성된 연결을 의미하는 SseEmitter
객체를 유저 ID 별로 관리하는 클래스이다. (참고로 이 SseEmitter
는 DB가 아닌 메모리에서 관리되고 있어, 연결 상태에서 서버가 죽는다면 모든 연결 정보가 날아간다. 이에 대한 대처 방안은 조금 뒤에서 설명)
sendMessage
메소드는 해당 유저 ID로 생성된 모든 SSE 연결에 이벤트를 전송하는 기능을 수행한다. SseEmitter.event()
api를 통해 이벤트를 생성하는데, 이때 각각 이벤트 ID, 이벤트 이름, 데이터, 주석을 설정하고 있는 것이다.
여기서 주의할 점은 sseEmitterRepository
객체에 여러 쓰레드의 동시 접근이 가능하다는 것이다. 따라서 SseEmitter
저장소로는 thread-safe한 자료구조인 ConcurrentHashMap
, CopyOnWriteArrayList
를 사용했다.
@Service
public class SseService {
private final Map<Long, List<SseEmitter>> sseEmitterRepository = new ConcurrentHashMap<>();
//유저 ID로 SseEmitter 리스트 조회
public List<SseEmitter> getSseEmitters(Long userId) {
if (!sseEmitterRepository.containsKey(userId)) {
sseEmitterRepository.put(userId, new CopyOnWriteArrayList<>());
}
return sseEmitterRepository.get(userId);
}
//연결 성립된 SseEmitter 객체 저장
public void save(Long userId, SseEmitter sseEmitter) {
List<SseEmitter> sseEmitters = getSseEmitters(userId);
sseEmitters.add(sseEmitter);
}
//해당 SseEmitter 삭제
public void delete(Long userId, SseEmitter sseEmitter) {
List<SseEmitter> sseEmitters = getSseEmitters(userId);
sseEmitters.remove(sseEmitter);
}
//해당 유저 ID가 가진 모든 SSE 연결에 이벤트 전송
public void sendMessage(Long userId, NotificationCreateResponse notification) {
List<SseEmitter> sseEmitters = getSseEmitters(userId);
sseEmitters.forEach(sseEmitter -> {
try {
sseEmitter.send(SseEmitter.event()
.id(String.valueOf(notification.getId())) //이벤트 ID
.name("notification") //이벤트 이름
.data(notification, MediaType.APPLICATION_JSON) //데이터
.comment("comment") //주석
);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
아래는 SSE 연결 API로, SseEmitter
객체를 생성하고 각각 연결 timeout 시, 에러 발생 시, 연결 종료 시 실행할 콜백을 설정한 후 내부 저장소에 객체를 저장하고 있다. SSE 연결 시마다 새로운 SseEmitter
객체를 생성하기 때문에 기존의 SseEmitter
를 제거해주어야 한다. 따라서 각 콜백에서 자기 자신을 삭제하도록 등록해주었다. 또한 첫 연결 시 꼭 서버는 클라이언트에게 더미 데이터를 전송해야 하므로 send
메소드를 통해 데이터 전송 후 SseEmitter
객체를 반환하고 있다. (정확한 원인은 모르겠지만 더미 데이터를 전송하지 않으면 요청에 pending이 걸리고, 연결 종료 후 클라이언트가 재연결 요청을 할 때 503 에러가 발생한다.)
아래는 각 콜백 함수가 호출되는 상황을 정리한 것이다.
- onCompletion
: SSE 연결이 정상 완료되었을 때 호출(클라이언트가 SSE 연결 종료하는 경우, 서버에서 SseEmitter
객체의 complete
메소드를 호출하는 경우, 타임아웃 발생 후 연결이 닫히는 경우)
- onTimeout
: SSE 연결 후 설정된 타임아웃 시간 동안 서버가 아무런 메시지도 보내지 않을 때 호출
- onError
: SSE 연결 중 예외가 발생했을 때 호출
연결 타임아웃 시간이 지나 연결이 끊기면 자동적으로 클라이언트는 SSE 연결 API를 재요청하고 매번 SseEmitter
객체를 생성한다. 이때 sendMessage
메소드에서 이벤트 ID를 설정하여 보냈기 때문에, 클라이언트는 재연결 요청 시 가장 마지막에 수신한 이벤트 ID를 Last-Event-ID
헤더에 담아 보낸다. 서버는 이를 통해 클라이언트의 메시지 수신 여부를 판단하여 클라이언트가 받지 못한 메시지들을 재전송할 수도 있다. 따라서 서버가 죽어 메모리 단에서 관리되는 연결 정보(SseEmitter
)가 날아가더라도, 클라이언트가 보내는 재연결 요청의 Last Event ID 값을 통해 연결 상태를 복구할 수 있다. 참고로 반환되는 SseEmitter
객체는 ResponseBodyEmitterReturnValueHandler
가 처리한다. 만약 SseEmitter
관련 설정 값들(ex. id
, name
, retry
, timeout
)을 가지고 어떤 방식으로 응답을 생성하는지 알고 싶다면 해당 클래스를 공부해보자.
@RequiredArgsConstructor
@RestController
public class SseController {
private final SseService sseService;
private static final Long TIMEOUT = 30000L; //timeout = 30초
private static final Long RECONNECT_TIME_MILLIS = 10000L; //retry = 10초
@GetMapping("/connect")
public ResponseEntity<SseEmitter> connect(@RequestParam Long userId,
@RequestHeader(value = "Last-Event-Id", required = false) Long notificationId) throws IOException {
SseEmitter sseEmitter = new SseEmitter(TIMEOUT);
sseEmitter.onCompletion(() -> sseService.delete(userId, sseEmitter)); //SSE 연결 종료 시
sseEmitter.onTimeout(() -> sseService.delete(userId, sseEmitter)); //SSE 타임아웃 발생 시
sseEmitter.onError(e -> sseService.delete(userId, sseEmitter)); //SSE 연결 중 에러 발생 시
sseService.save(userId, sseEmitter);
//TODO: 클라이언트가 수신하지 못한 이벤트 처리
SseEmitter.SseEventBuilder dummyData = SseEmitter.event()
.name("connect")
.data("connected!")
.reconnectTime(RECONNECT_TIME_MILLIS);
sseEmitter.send(dummyData); //첫 연결 시 더미 데이터 전송
return ResponseEntity.ok(sseEmitter);
}
}
SSE 연결을 통한 알림 발송 API
클라이언트에서 아래 API를 호출하면 전송할 메시지인 Notification
객체를 생성하여 저장하고, 해당 유저 ID로 생성된 모든 SSE 연결에 이벤트를 전송한다.
@RequiredArgsConstructor
@RestController
public class NotificationController {
private final NotificationService notificationService;
private final SseService sseService;
@PostMapping("/notifications")
public ResponseEntity<Void> sendNotification(@RequestBody NotificationCreateRequest request) {
//Notification 객체 저장
NotificationCreateResponse notification = notificationService.saveNotification(request.getToUserId(), request.getMessage());
//수신자 userId로 생성된 모든 SSE 연결에 메시지 전송
sseService.sendMessage(request.getToUserId(), notification);
return ResponseEntity.ok().build();
}
}
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class NotificationService {
private final NotificationRepository notificationRepository;
@Transactional
public NotificationCreateResponse saveNotification(Long toUserId, String message) {
Notification notification = Notification.builder()
.message(message)
.user(User.builder().id(toUserId).build())
.build();
notificationRepository.save(notification);
return NotificationCreateResponse.builder()
.id(notification.getId())
.message(notification.getMessage())
.build();
}
}
@Getter
public class NotificationCreateRequest {
private Long toUserId;
private String message;
}
@Builder
@Getter
public class NotificationCreateResponse {
private Long id;
private String message;
}
timeout vs retry 차이
여기서 SssEmitter
객체 생성 시 설정해준 timeout
값과 reconnectTime
값의 차이가 정확히 무엇인지 궁금할 것이다. 각각은 MDN에서 정의한 SSE 스펙의 timeout, retry 값에 대응된다. 먼저 timeout은 클라이어트 - 서버 간 연결에서 이벤트를 전송하지 않는 동안의 최대 연결 지속 시간으로, 즉 서버가 timeout 시간 내에 매번 이벤트를 보낸다면 연결이 계속 유지되지만, timeout 시간 동안 한번도 이벤트가 전송되지 않으면 클라이언트 - 서버 연결은 끊기게 된다.
이렇게 연결이 끊긴 후 클라이언트는 자동으로 재연결 요청을 보내는데 retry는 재연결 요청을 보내기 전까지의 대기 시간, 즉 retry가 3000(ms 단위)로 설정되었다면 연결 종료 후 3초 뒤 재연결 요청을 보내게 되는 것이다.

자바스크립트 예시
이렇게 구현한 서버 API를 직접 요청하여 SSE 연결이 정상적으로 설정되고 서버가 보내는 이벤트 또한 잘 수신하는지 확인해보자. 자바스크립트는 이 SSE 통신을 지원하는 EventSource API 기본적으로 제공하고 있다. 아무? 브라우저의 콘솔 창을 열어 다음과 같이 EventSource
객체를 생성하면 클라이언트와 서버 간 연결이 설정된 것이다. addEventListener
로 수신할 이벤트(여기는 notification)를 설정한 후 포스트맨으로 POST /notifications API를 호출하여 클라이언트에게 메시지를 전송하였다. 이벤트를 수신한 클라이언트는 콜백 함수를 호출하여 콘솔에 수신한 메시지를 출력한 것을 확인할 수 있다.


SSE 연결이 끊겼을 때에 브라우저는 자동 재연결 요청을 한다고 했었다. 이때 현재까지 정상적으로 받은 이벤트 ID를 Last-Event-Id
헤더에 담아서 전송하는데, 아래의 두번째 /connect 요청은 timeout 후 재연결 요청으로 직전에 수신한 이벤트 ID를 Last-Event-Id: 58
로 보내고 있다.

트러블 슈팅
SSE 연결 시 무한 로딩 + 재연결 요청 시 503 에러 발생
첫 연결 시 더미 데이터 전송하지 않는다면 SSE 연결 시 무한 로딩이 발생하다 서버 설정에 따라 timeout 또는 503 Service Unavailable 에러가 발생한다. 만약 정상 연결되었다 하더라도 클라이언트의 재연결 요청 시 503 에러가 발생한다. Service Unavailable 에러가 나는 것으로 추측해봤을 때, 첫 연결 시 서버가 보내는 더미 데이터를 클라이언트는 SSE 연결 수락으로 간주하는 것 같다. 따라서 꼭 첫 연결에서는 SseEmitter.send
메소드를 호출하여 아무 데이터든 전송해야 한다.
프록시 서버 Nginx
로컬에서 스프링 서버만으로 테스트할 때에는 정상 동작하던 SSE가 Nginx를 프록시 서버로 사용하게 되면 SSE가 정상 동작하지 않고 계속해서 요청에 pending이 걸렸었다. 찾아보니 Nginx는 기본적으로 Upstream으로 요청을 보낼때 HTTP/1.0 버전을 사용한다고 한다. 따라서 클라이언트가 HTTP/1.1로 요청을 보내더라도 Nginx가 스프링 서버로 요청을 보낼 때에는 HTTP/1.0으로 보내 Connection: close
헤더를 사용하기 때문에, 지속 연결이 닫혀버려 SSE가 정상적으로 동작하지 않게 되는 것이다.
따라서 아래와 같은 nginx 설정을 추가해줘야 한다. Upstream 요청 시 HTTP/1.1 프로토콜을 사용하도록 설정해주었으며, HTTP/1.1에서 Connection: keep-alive
헤더는 기본 활성화되어 있어 만약 이 헤더가 그대로 전달된다면 일부 서버에 문제가 발생할 수 있기 때문에 proxy_set_header Connection ""
설정으로 해당 헤더를 제거해주었다.
http {
#...
server {
listen 8090;
server_name localhost;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_http_version 1.1; //Upstream 요청 시 HTTP/1.1 사용
proxy_set_header Connection ""; //Connection: keep-alive 헤더 제거
}
}
}
또한 알아두어야 할 것이 SseEmitter
의 timeout 설정을 100초로 해두었어도 위의 nginx 설정에서 proxy_read_timeout
시간을 60초로 해두었기 때문에 60초 동안 서버로부터 아무런 메시지도 받지 못한다면 SSE 커넥션은 닫히게 된다. 즉 프록시 서버를 사용하는 경우에는 SseEmitter
timeout 설정보다 프록시 서버의 timeout 설정이 우선할 수 있다는 것이다.

SseEmitter 저장소의 동시성 문제
SseEmitter
객체 생성 시 설정한 콜백들은 연결 별로 각기 다른 쓰레드에서 호출된다. 즉 SseEmitterService
내부의 저장소(SseEmitterRepository
)에 여러 쓰레드가 동시에 접근할 수 있다는 것인데, 만약 thread-safe하지 않은 자료구조를 사용한다면 ConcurrnetModificationException
이 발생할 수 있다. 따라서 SseEmitter
를 저장하는 자료구조는 꼭 thread-safe해야 한다.
DB Connection 고갈
SSE 연결 시 타임아웃이 발생하거나 서버 혹은 클라이언트가 직접 연결을 끊지 않는 이상 HTTP Connection이 유지된다. 이때 JPA의 OSIV(open-in-view)를 true로 설정하게 되면 DB Connection은 요청이 종료될 때까지 열려 있게 된다. 즉 DB Connection이 SSE 연결이 열려있는 동안 유지된다는 것인데, 이는 데이터베이스 커넥션 풀이 고갈되어 다른 요청들은 커넥션을 얻지 못하는 결과로 이어질 수 있다. 따라서 SSE 연결 시에는 DB Connection을 필요로 하는 작업(ex. DB 조회 등)을 하지 않거나 OSIV=false로 설정해야 한다.
정말 OSIV=true, 최대 커넥션 풀의 크기를 3으로 설정했을 때 SSE 연결을 3개 생성하면 DB Connection Pool이 고갈되는지 확인해보자. 여기서 주의할 점은 SSE 연결 요청 시 트랜잭션이 열려야 현재 요청에 DB Connection이 할당된다는 것이다. 즉 아래 GET /connect SSE 연결 요청 로직의 userService.getUser
메소드에서 트랜잭션을 시작하지 않는다면 우리가 의도한 결과(SSE 연결로 인한 DB Connection 고갈)가 발생하지 않는다.
spring:
jpa:
open-in-view: true #default 설정
datasource:
hikari:
maximum-pool-size: 3 #최대 pool 크기
@RequiredArgsConstructor
@RestController
public class SseController {
private final SseService sseService;
private final UserService userService;
private static final Long TIMEOUT = 30000L; //timeout = 30초
private static final Long RECONNECT_TIME_MILLIS = 10000L; //retry = 10초
@GetMapping("/connect")
public ResponseEntity<SseEmitter> connect(@RequestParam Long userId,
@RequestHeader(value = "Last-Event-Id", required = false) Long notificationId) throws IOException {
SseEmitter sseEmitter = new SseEmitter(TIMEOUT);
sseEmitter.onCompletion(() -> sseService.delete(userId, sseEmitter)); //SSE 연결 종료 시
sseEmitter.onTimeout(() -> sseService.delete(userId, sseEmitter)); //SSE 타임아웃 발생 시
sseEmitter.onError(e -> sseService.delete(userId, sseEmitter)); //SSE 연결 중 에러 발생 시
sseService.save(userId, sseEmitter);
User user = userService.getUser(userId); //트랜잭션이 열려 현재 요청에 DB Connection 할당
SseEmitter.SseEventBuilder dummyData = SseEmitter.event()
.name("connect")
.data("connected!")
.reconnectTime(RECONNECT_TIME_MILLIS);
sseEmitter.send(dummyData); //첫 연결 시 더미 데이터 전송
return ResponseEntity.ok(sseEmitter);
}
}
SSE 연결을 3번 요청했을 때 HikariPool의 상태는 다음과 같다. active 상태의 커넥션이 3개 idle 상태는 0개로 만약 DB Connection이 필요한 요청이 한번이라도 더 들어오게 된다면 해당 요청은 커넥션을 얻지 못해 에러가 발생한다.
HikariPool-1 - Pool stats (total=3, active=3, idle=0, waiting=0)
실제 DB Connection이 필요한 요청을 보냈을 때 HikariPool의 상태는 다음과 같았으며 상태 코드 500이 응답으로 떨어졌다. (프록시 서버가 존재하는 등 경우에 따라 504 Timeout 에러가 발생할 수도 있다.)
HikariPool-1 - Connection not added, stats (total=3, active=3, idle=0, waiting=1)
HikariPool-1 - Pool stats (total=3, active=3, idle=0, waiting=1)
HikariPool-1 - Fill pool skipped, pool has sufficient level or currently being filled.
HikariPool-1 - Timeout failure stats (total=3, active=3, idle=0, waiting=0)
이렇게 SSE 연결이 종료될 때까지 할당된 DB Connection이 유지되는 것은 아주 큰 이슈이다. 이로 인해 다른 요청들은 처리되지 못할 수 있기 때문이다. 따라서 SSE 연결 요청 시 해당 요청에 DB Connection이 할당되는 로직이 있는지 확인이 필요하며, 혹은 아예 OSIV=false로 설정하는 것이 안전할 것이다.
Scale-Out
현재 SseEmitter
객체는 메모리에 저장되어 있다. 따라서 서버의 인스턴스를 늘리는 Scale-Out 시 기능이 제대로 동작하지 않거나 클라이언트와 서버 1개가 지속적으로 연결되므로 성능 또한 악화될 수 있다. 이러한 경우에는 Redis Pub/Sub 같은 메시지 브로커나 분산 캐시를 통해 문제를 해결할 수 있다고 하는데, 자세한 내용은 직접 이를 적용해본 후 글로 정리할 예정이다.
Connection != Thread ?
SSE 구현을 하던 중 'HTTP의 지속적 연결이라는 것이 톰캣의 쓰레드를 계속 잡아먹고 있는 것은 아닐까?' 이런 의문이 생겼다. 그렇게 되면 톰캣의 쓰레드 풀 또한 고갈되어 다른 요청에 작업 쓰레드를 할당할 수 없어 이 또한 중대한 문제를 일으킬 것이다. 다행히도 다른 분이 동일한 궁금증을 가지고 있었고 또 다른 친절한 분의 인프런 QnA 답변을 통해 답을 얻을 수 있었다.
먼저 SSE 커넥션 유지 시 정말 쓰레드를 잡아 먹는지 실험을 해보자. 아래는 톰캣 설정에 대한 내용을 간단히 정리한 것으로 자세한 내용을 알고 싶다면 공식 문서를 참고하면 좋을 듯하다.
server.tomcat.max-connections |
서버가 동시에 처리할 수 있는 최대 연결 수 |
server.tomcat.accept-count |
현재 연결 수가 max-connections에 도달했을 때, 들어오는 요청을 대기시킬 큐의 최대 크기 |
server.tomcat.threads.max |
요청 처리 쓰레드의 최대 개수, 즉 서버가 동시에 처리할 수 있는 최대 요청 수 |
server:
tomcat:
max-connections: 3 #최대 동시 연결 수
accept-count: 1 #큐 대기 연결 수
threads:
max: 1 #최대 동시 작업 쓰레드 수
위 설명을 통해 알겠지만 여기서 연결(Connection)과 쓰레드(Thread)는 다른 개념이다. 인프런 QnA의 답변자 분은 커넥션은 '고속도로', 쓰레드는 '작업자'와 같은 비유를 들어 이를 설명했다. SSE 연결로 커넥션이 지속되는 것은 클라이언트 - 서버 사이에 길이 하나 뚫리는 것으로, 이 뚫린 길을 통해 서버 → 클라이언트에게 이벤트를 전송하는 일은 서버의 작업자가 수행하는 일로 생각하면 이해가 쉬울 것이다.
예를 들어 말씀드리면 서울에서 부산에 있는 공장까지 화물을 전달해야할 때 Connection은 서울과 부산을 잇는 고속도로, Thread는 부산 하차장에서 공장까지 화물을 옮기는 작업자라고 보시면 될 것 같습니다.
정말 Connection과 Thread가 다른 개념인지 눈으로 확인해보고 싶어 실험을 진행해보았다.
max-connections=3, threads.max=1일 때, SSE 연결 2개 → "hello world" 반환하는 API 1번 호출

SSE 연결이 2개로 max-connections인 3보다 작으므로, 단순 "hello world"를 반환하는 GET /hello API를 정상 호출할 수 있으며 응답 또한 원하는 대로 떨어졌다. (GET /hello API 호출 시에도 2개의 SSE 연결 유지 중) maxThread 값을 1로 설정했음에도 불구하고 응답이 정상적으로 떨어진 것을 보면 SSE 연결이 종료될 때까지 쓰레드를 점유하지 않는다는 것을 알 수 있다.
max-connections=3, threads.max=1, accept-count=1일 때, SSE 연결 3개 → "hello world" 반환하는 API 1번 호출
SSE 연결이 3개로 max-connections에 도달했으므로 더이상의 연결 요청은 처리할 수 없다. (이미 3개의 연결을 유지 중이므로) 그 대신 accept-count 값을 1로 설정했기 때문에 GET /hello 요청은 SSE 연결이 하나라도 종료될 때까지 큐에 대기하게 된다. 만약 큐에서 대기하는 시간이 길어진다면 요청이 정상적으로 처리되지 않고 다음과 같이 타임아웃 에러가 발생할 수 있다.

결론은 'SSE Connection은 연결 종료 시까지 Thread를 점유하지 않는다'이다. 즉 SSE 연결 증가가 톰캣의 쓰레드 고갈로 선형적 영향을 미치지는 않는다는 것이다. 다만 앞서 살펴봤 듯이 현재 SSE 연결 수가 톰캣의 max-connections 값을 초과하게 되면 다른 요청들은 SSE 연결이 하나라도 종료될 때까지 처리되지 못하고 큐에서 대기해야 한다. 이는 당연히 서버의 요청 처리 성능에도 영향을 미칠 수 있으므로 SSE 도입 시 고려해야 하는 부분이다.
마무리
스프링에서 SSE를 구현하는 방법에 대해 알아보았다. 서버의 데이터 변경 이벤트를 실시간으로 클라이언트에게 보내야 하는 경우(ex. 댓글 알림, 피드 알림 등) + 서버 → 클라이언트로 단방향 이벤트 전송만으로 충분한 경우 SSE를 통해 이를 간단하게 구현할 수 있다. 다만 앞서 언급한 단점들 또한 존재하므로 이를 실무에 적용할 때에는 충분한 테스트가 매우매우 필요하다.
끝.
참고
https://www.inflearn.com/community/questions/909440/%EC%A7%80%EC%86%8D-%EC%97%B0%EA%B2%B0%EA%B3%BC-sse
https://velog.io/@byeongju/max-connections-accept-count-threads.max
https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/
'Spring > Spring' 카테고리의 다른 글
[Spring] Link 헤더를 이용한 REST Pagination 구현 (0) | 2024.01.14 |
---|---|
[Spring] AOP로 공통 응답 형식 처리하기 (0) | 2024.01.14 |
[Spring Boot] Controller - Service 계층 리팩토링 with Facade 패턴 (0) | 2023.12.21 |
[Spring Boot] 날짜 데이터를 요청 및 응답으로 주고 받는 여러 가지 방법 (0) | 2023.11.23 |
[Querydsl] Spring Boot 3.x with Gradle 설정 (1) | 2023.10.20 |
- Total
- Today
- Yesterday
- 단위 테스트
- servlet filter
- 템플릿 콜백 패턴
- Assertions
- spring boot
- Linux
- C++
- mockito
- QueryDSL
- github
- junit5
- spring aop
- ParameterizedTest
- facade 패턴
- Spring Security
- 서블릿 컨테이너
- Front Controller
- Git
- vscode
- 전략 패턴
- rest api
- Transaction
- 디자인 패턴
- JPA
- Gitflow
- 모두의 리눅스
- SSE
- FrontController
- Java
- spring
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |