묻고 답해요
156만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨
리액트 & 스프링 부트 SSE 알람 기능 구현 CORS에러
헌재 상황은 NGINX로 HTTPS로 백엔드를 배포하고 있고 리액트는 로컬에서 작업 중인데 알람 기능을 구현할 때 OPTIONS가 CORS에러를 일으킵니다.```@Service @RequiredArgsConstructor @Log4j2 @Transactional public class NotificationService { // SSE 이벤트 타임아웃 시간 private static final Long DEFAULT_TIMEOUT = 24L * 60 * 60 * 1000; // SSE 연결 타임아웃 (1일) private final CustomNotificationRepository customNotificationRepository; private final MemberRepository memberRepository; private final CommunityRepository communityRepository; private final NotificationRepository notificationRepository; // 메시지 알림 public SseEmitter subscribe(String memberEmail, String lastEventId) throws Exception { // 회원 조회 MemberEntity findMember = memberRepository.findByMemberEmail(memberEmail); // 매 연결마다 고유 이벤트 ID 부여 String eventId = makeTimeIncludeId(findMember); log.info("eventId {} ", eventId); // SseEmitter 생성후 Map에 저장 SseEmitter sseEmitter = customNotificationRepository.save(eventId, new SseEmitter(DEFAULT_TIMEOUT)); // 사용자에게 모든 데이터가 전송되었다면 emitter 삭제 sseEmitter.onCompletion(() -> { log.info("onCompletion callback"); customNotificationRepository.deleteById(eventId); }); // Emitter의 유효 시간이 만료되면 emitter 삭제 // 유효 시간이 만료되었다는 것은 클라이언트와 서버가 연결된 시간동안 아무런 이벤트가 발생하지 않은 것을 의미한다. sseEmitter.onTimeout(() -> { log.info("onTimeout callback"); customNotificationRepository.deleteById(eventId); }); // 503 에러를 방지하기 위한 더미 이벤트 전송 sendToClient(eventId, sseEmitter, "알림 서버 연결 성공 [memberId = " + findMember.getMemberId() + "]"); // 클라이언트가 미수신한 Event 목록이 존재할 경우 전송하여 Event 유실을 예방 if (hasLostData(lastEventId)) { sendLostData(lastEventId, findMember.getMemberId(), sseEmitter); } return sseEmitter; } private static @NotNull String makeTimeIncludeId(MemberEntity findMember) { String eventId = findMember.getMemberId() + "_" + System.currentTimeMillis(); return eventId; } private void sendToClient(String eventId, SseEmitter sseEmitter, Object data) { try { sseEmitter.send(SseEmitter.event() .name("connect") .id(eventId) .data(data)); } catch (IOException e) { customNotificationRepository.deleteById(eventId); throw new RuntimeException("알림 서버 연결 오류"); } } private boolean hasLostData(String lastEventId) { return !lastEventId.isEmpty(); } private void sendLostData(String lastEventId, Long memberId, SseEmitter sseEmitter) { Map<String, Object> eventCaches = customNotificationRepository.findAllEventCacheStartWithByMemberId(memberId); eventCaches.entrySet().stream() .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0) .forEach(entry -> sendToClient(entry.getKey(), sseEmitter, entry.getValue())); } ...이렇게 서비스를 구현하고@RestController @RequestMapping("/api/v1/notify") @RequiredArgsConstructor @Log4j2 public class NotificationController implements NotificationControllerDocs { private final NotificationService notificationService; // 메시지 알림 // SSE 통신을 위해서는 produces로 반환할 데이터 타입을 "text/event-stream"으로 해주어야 함 @GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") public SseEmitter subscribe(@AuthenticationPrincipal UserDetails userDetails, @RequestHeader(value = "last-event-id", required = false, defaultValue = "") final String lastEventId, HttpServletResponse response) { try { if (userDetails == null) { throw new IllegalArgumentException("인증 정보가 필요합니다."); } response.setHeader("Connection", "keep-alive"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("X-Accel-Buffering", "no"); String email = userDetails.getUsername(); SseEmitter responseEmitter = notificationService.subscribe(email, lastEventId); log.info("Subscribed to email: " + email); log.info("response: " + responseEmitter); return responseEmitter; } catch (Exception e) { throw new RuntimeException(e); } } ...NGINX 설정server { listen 443 ssl; server_name meettify.store; ssl_certificate /etc/letsencrypt/live/meettify.store/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/meettify.store/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # SSE용 헤더 설정 location /api/v1/notify/subscribe { # OPTIONS preflight 요청을 처리하도록 설정 if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' 'http://localhost:5173' always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always; add_header 'Access-Control-Max-Age' 3600 always; return 204; } proxy_pass https://meettify.store; # Spring Boot 서버로 요청 전달 proxy_http_version 1.1; proxy_set_header Connection ''; # SSE 연결 유지 proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection ''; proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; # SSE 연결 타임아웃 방지 proxy_set_header Cache-Control no-cache; proxy_read_timeout 86400s; keepalive_timeout 86400s; # 필요에 따라 시간 조정 add_header Access-Control-Allow-Origin "http://localhost:5173"; add_header Access-Control-Allow-Credentials "true"; } # 기본 프록시 설정 (백엔드 애플리케이션) location / { proxy_pass http://ubuntu-api-1:8080; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Connection ''; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection ''; proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; } 이렇게 엔진엑스 설정까지 한다음@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedHeaders("Authorization", "Content-Type") .allowCredentials(true) .exposedHeaders("Cache-Control", "Content-Type", "X-Accel-Buffering") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"); } } 이렇게 설정했는데 계속 CORS에러가 발생합니다.해당 문제가 지속적으로 반복중입니다. ㅠㅠ
-
미해결
스프링 알림 1:N 발송 어떻게 구현해야할까요?
안녕하세요. 스프링으로 알림 서비스 API 만들어보고 있습니다.여기서 알림이라 하면, 인프런에서 '종 아이콘' 누르면 나오는 사이트 내부에 있는 알림입니다. 현재 Notification 테이블은 Member 테이블이 @ManyToOne으로 매핑되어있는 상태입니다.즉, 한 유저는 여러개의 알림을 가질 수 있습니다. 'OO님이 본인 게시글에 답글을 달았습니다'와 같은 1:1 알림 전송은 Notification insert가 댓글 달때 한번만 일어나므로 상관없지만, '스프링 핵심 원리 -기본편 강의에 새소식이 있어요!!'와 같은 1:N 알림을 보내려면, 스프링 핵심원리를 듣는 모든 수강생한테 알림을 보내야하니까, 수강생수만큼 Notification Insert가 나가야하잖아요??그러면 수강생 수가 100만명이면 Insert가 100만명 나가는건데, 너무 '비효율적'이고 'DB 공간 낭비'라고 생각되서요. 대규모 서비스에서 사용되는 좋은 방법 추천해주실 분 계신가요?인프런에서 1:N 알림은 어떤 방법을 쓰고 있을까요~?