Skip to content

Commit 2ca7165

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feat/mission
2 parents 4b17aaa + ff0910f commit 2ca7165

File tree

17 files changed

+269
-77
lines changed

17 files changed

+269
-77
lines changed

backend/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ dependencies {
5050
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
5151

5252
implementation("org.springframework.boot:spring-boot-starter-websocket")
53+
54+
implementation ("org.springframework.kafka:spring-kafka")
5355
}
5456

5557
tasks.withType<Test> {

backend/docker-compose.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
version: "3.7"
2+
name: redpanda-quickstart-one-broker
3+
networks:
4+
redpanda_network:
5+
driver: bridge
6+
volumes:
7+
redpanda-0: null
8+
services:
9+
redpanda-0:
10+
command:
11+
- redpanda
12+
- start
13+
- --kafka-addr internal://0.0.0.0:9092,external://0.0.0.0:19092
14+
# Address the broker advertises to clients that connect to the Kafka API.
15+
# Use the internal addresses to connect to the Redpanda brokers'
16+
# from inside the same Docker network.
17+
# Use the external addresses to connect to the Redpanda brokers'
18+
# from outside the Docker network.
19+
- --advertise-kafka-addr internal://redpanda-0:9092,external://localhost:19092
20+
- --pandaproxy-addr internal://0.0.0.0:8082,external://0.0.0.0:18082
21+
# Address the broker advertises to clients that connect to the HTTP Proxy.
22+
- --advertise-pandaproxy-addr internal://redpanda-0:8082,external://localhost:18082
23+
- --schema-registry-addr internal://0.0.0.0:8081,external://0.0.0.0:18081
24+
# Redpanda brokers use the RPC API to communicate with each other internally.
25+
- --rpc-addr redpanda-0:33145
26+
- --advertise-rpc-addr redpanda-0:33145
27+
# Tells Seastar (the framework Redpanda uses under the hood) to use 1 core on the system.
28+
- --smp 1
29+
# The amount of memory to make available to Redpanda.
30+
- --memory 1G
31+
# Mode dev-container uses well-known configuration properties for development in containers.
32+
- --mode dev-container
33+
# Enable logs for debugging.
34+
- --default-log-level=debug
35+
image: docker.redpanda.com/redpandadata/redpanda:v23.3.6
36+
container_name: redpanda-0
37+
volumes:
38+
- redpanda-0:/var/lib/redpanda/data
39+
networks:
40+
- redpanda_network
41+
ports:
42+
- 18081:18081
43+
- 18082:18082
44+
- 19092:19092
45+
- 19644:9644
46+
console:
47+
container_name: redpanda-console
48+
image: docker.redpanda.com/redpandadata/console:v2.4.3
49+
networks:
50+
- redpanda_network
51+
entrypoint: /bin/sh
52+
command: -c 'echo "$$CONSOLE_CONFIG_FILE" > /tmp/config.yml; /app/console'
53+
environment:
54+
CONFIG_FILEPATH: /tmp/config.yml
55+
CONSOLE_CONFIG_FILE: |
56+
kafka:
57+
brokers: ["redpanda-0:9092"]
58+
schemaRegistry:
59+
enabled: true
60+
urls: ["http://redpanda-0:8081"]
61+
redpanda:
62+
adminApi:
63+
enabled: true
64+
urls: ["http://redpanda-0:9644"]
65+
ports:
66+
- 8070:8080
67+
depends_on:
68+
- redpanda-0

backend/src/main/java/com/back/BackApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.cache.annotation.EnableCaching;
66
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
7+
import org.springframework.scheduling.annotation.EnableAsync;
78

89
@SpringBootApplication
910
@EnableJpaAuditing
1011
@EnableCaching
12+
@EnableAsync
1113
public class BackApplication {
1214
public static void main(String[] args) {
1315
SpringApplication.run(BackApplication.class, args);

backend/src/main/java/com/back/domain/member/controller/ApiV1MemberController.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,9 @@ public ResponseEntity<ApiResponse<LoginResDto>> login(
6565
@DeleteMapping("/logout")
6666
@Operation(summary = "로그아웃", description = "로그아웃")
6767
public ResponseEntity<ApiResponse<Void>> logout() {
68-
Member actor = rq.getActorFromDb();
69-
7068
rq.deleteCookie("apiKey");
7169
rq.deleteCookie("accessToken");
72-
if(actor.getSocialAccessToken() != null) {
73-
memberService.social_logout(actor);
74-
}
70+
rq.deleteCookie("JSESSIONID");
7571

7672
return ResponseEntity
7773
.status(HttpStatus.OK)

backend/src/main/java/com/back/domain/member/entity/Member.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ public class Member extends BaseEntity {
4646
// *** 개발자용 정보 ***
4747
private MemberRole role = MemberRole.USER;
4848
private String apiKey = null;
49-
private String socialAccessToken = null;
5049

5150
//생성자(회원 가입)
5251
public Member(String email, String password, String name) {

backend/src/main/java/com/back/domain/member/service/AuthService.java

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
import org.springframework.beans.factory.annotation.Value;
66
import org.springframework.stereotype.Service;
77

8-
import java.net.URI;
9-
import java.net.http.HttpClient;
10-
import java.net.http.HttpRequest;
11-
import java.net.http.HttpResponse;
128
import java.util.Map;
139

1410
@Service
@@ -19,36 +15,6 @@ public class AuthService {
1915
@Value("${custom.accessToken.expirationSeconds}")
2016
private int accessTokenExpirationSeconds;
2117

22-
private final String kakaoURL = "https://kapi.kakao.com/v1/user/logout";
23-
24-
void social_logout(String provider, String accessToken){
25-
HttpClient client= HttpClient.newHttpClient();
26-
try {
27-
switch (provider) {
28-
case "KAKAO" -> {
29-
HttpRequest request = HttpRequest.newBuilder()
30-
.uri(URI.create(kakaoURL))
31-
.header("Authorization", "Bearer " + accessToken)
32-
.POST(HttpRequest.BodyPublishers.noBody())
33-
.build();
34-
35-
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
36-
37-
System.out.println("responseCode : " + response.statusCode());
38-
System.out.println("responseBody : " + response.body());
39-
}
40-
case "GOOGLE" -> {
41-
42-
}
43-
case "NAVER" -> {
44-
45-
}
46-
}
47-
} catch (Exception e) {
48-
49-
}
50-
}
51-
5218
String genAccessToken(Member member) {
5319
long id = member.getId();
5420
String email = member.getEmail();

backend/src/main/java/com/back/domain/member/service/MemberService.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,17 @@ public Member login(String email, String password) {
5252
}
5353

5454
//로그인 (소셜 계정)
55-
public Member social_login(String email, String name, String socialAccessToken) {
55+
public Member social_login(String email, String name) {
5656
Member member = findByEmail(email).orElse(null);
5757

5858
//최초 로그인일 경우 가입 처리
5959
if(member == null) {
6060
member = signup(email, "", name);
6161
}
6262

63-
member.setSocialAccessToken(socialAccessToken);
64-
6563
return member;
6664
}
6765

68-
//로그아웃 (소셜 계정)
69-
public void social_logout(Member member) {
70-
String provider = member.getEmail().substring(1, member.getEmail().indexOf("]"));
71-
72-
//authService.social_logout(provider, member.getSocialAccessToken());
73-
member.setSocialAccessToken(null);
74-
}
75-
7666
//식별코드 생성
7767
public void genCode(Member member) {
7868
final String CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.back.domain.party.paryChat.config;
2+
3+
import org.apache.kafka.clients.consumer.ConsumerRecord;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.kafka.core.KafkaTemplate;
10+
import org.springframework.kafka.listener.ConsumerRecordRecoverer;
11+
import org.springframework.kafka.listener.DefaultErrorHandler;
12+
import org.springframework.util.backoff.FixedBackOff;
13+
14+
@Configuration
15+
public class KafkaConfig {
16+
17+
private static final Logger log = LoggerFactory.getLogger(KafkaConfig.class);
18+
19+
private final KafkaTemplate<String, String> kafkaTemplate;
20+
21+
@Value("${spring.kafka.dlq.topic:chat-messages-dlq}")
22+
private String dlqTopic;
23+
24+
public KafkaConfig(KafkaTemplate<String, String> kafkaTemplate) {
25+
this.kafkaTemplate = kafkaTemplate;
26+
}
27+
28+
private static final long INTERVAL_MS = 1000L;
29+
private static final long MAX_ATTEMPTS = 3L;
30+
31+
@Bean
32+
@SuppressWarnings("unchecked")
33+
public DefaultErrorHandler errorHandler() {
34+
FixedBackOff fixedBackOff = new FixedBackOff(INTERVAL_MS, MAX_ATTEMPTS - 1);
35+
36+
ConsumerRecordRecoverer recoverer = (record, exception) -> {
37+
ConsumerRecord<String, String> consumerRecord = (ConsumerRecord<String, String>) record;
38+
39+
log.error(
40+
"--- FINAL FAILURE --- Message moved to DLQ. Topic: {}, Partition: {}, Offset: {}, Key: {}, Exception: {}",
41+
consumerRecord.topic(),
42+
consumerRecord.partition(),
43+
consumerRecord.offset(),
44+
consumerRecord.key(),
45+
exception.getMessage(),
46+
exception // 예외 객체를 마지막 인자로 전달하여 스택 트레이스를 로그에 포함시킵니다.
47+
);
48+
49+
try {
50+
// DLQ로 메시지 발행
51+
kafkaTemplate.send(dlqTopic, consumerRecord.key(), consumerRecord.value()).get();
52+
} catch (Exception e) {
53+
// DLQ 전송 실패 시, 심각한 오류 로그 기록
54+
log.error("CRITICAL FAILURE: Failed to send message to DLQ! Data loss imminent. Original message: {}", consumerRecord.value(), e);
55+
}
56+
};
57+
58+
return new DefaultErrorHandler(recoverer, fixedBackOff);
59+
}
60+
}

backend/src/main/java/com/back/domain/party/paryChat/config/WebSocketConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
66
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
77
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
8+
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
89

910
@Configuration
1011
@EnableWebSocketMessageBroker
@@ -23,4 +24,9 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
2324
// 웹소켓 연결을 위한 STOMP 엔드포인트 설정
2425
registry.addEndpoint("/ws/chat").withSockJS();
2526
}
27+
28+
@Override
29+
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
30+
registration.setMessageSizeLimit(1024 * 1024); // 예: 1MB로 메시지 크기 제한
31+
}
2632
}

backend/src/main/java/com/back/domain/party/paryChat/controller/WebSocketController.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import com.back.domain.party.paryChat.dto.ChatMessageDto;
44
import com.back.domain.party.paryChat.entity.ChatMessage;
55
import com.back.domain.party.paryChat.service.ChatMessageService;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
67
import io.swagger.v3.oas.annotations.Operation;
78
import lombok.RequiredArgsConstructor;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
811
import org.springframework.data.domain.Page;
912
import org.springframework.data.domain.PageRequest;
1013
import org.springframework.data.domain.Pageable;
1114
import org.springframework.data.domain.Sort;
15+
import org.springframework.kafka.core.KafkaTemplate;
1216
import org.springframework.messaging.handler.annotation.MessageMapping;
1317
import org.springframework.messaging.handler.annotation.Payload;
1418
import org.springframework.messaging.simp.SimpMessageSendingOperations;
@@ -21,17 +25,34 @@
2125
@RequiredArgsConstructor
2226
public class WebSocketController {
2327

28+
private static final Logger log = LoggerFactory.getLogger(WebSocketController.class);
29+
2430
private final SimpMessageSendingOperations messagingTemplate;
2531
private final ChatMessageService chatMessageService;
32+
private final KafkaTemplate<String, String> kafkaTemplate;
33+
private final ObjectMapper objectMapper;
34+
35+
private static final String CHAT_TOPIC = "chat-messages";
2636

2737
@MessageMapping("/chat.sendMessage") // 클라이언트가 메시지를 보내는 경로 (예: /app/chat.sendMessage)
2838
public void sendMessage(@Payload ChatMessageDto chatMessageDto, @AuthenticationPrincipal User user) {
29-
// 1. 메시지를 데이터베이스에 저장
30-
chatMessageDto.setSenderEmail(user.getUsername()); // 인증된 사용자 이메일로 설정
31-
chatMessageService.saveMessage(chatMessageDto);
39+
// 1. 메시지 발신자 설정
40+
chatMessageDto.setSenderEmail(user.getUsername());
3241

33-
// 2. 메시지를 해당 파티의 채팅방으로 전송
42+
// 2. 메시지를 해당 파티의 채팅방으로 즉시 전송 (실시간성 확보)
3443
messagingTemplate.convertAndSend("/topic/party/" + chatMessageDto.getPartyId(), chatMessageDto);
44+
45+
// 3. 메시지 저장 작업을 Kafka로 오프로드 (비동기 및 안정성 확보)
46+
try {
47+
String messageJson = objectMapper.writeValueAsString(chatMessageDto);
48+
// 파티 ID를 키로 사용하여 같은 파티 메시지가 같은 파티션에 저장되도록 보장 (메시지 순서 보장)
49+
kafkaTemplate.send(CHAT_TOPIC, chatMessageDto.getPartyId().toString(), messageJson);
50+
} catch (Exception e) {
51+
// Kafka Producer 실패는 (일반적으로 일시적이지만) 심각하므로 ERROR 레벨로 기록합니다.
52+
log.error("Failed to send chat message to Kafka. Message: {}, Exception: {}", chatMessageDto, e.getMessage(), e);
53+
// Kafka 전송 실패는 데이터베이스에 기록을 남기지 못할 위험이 있지만,
54+
// 실시간 채팅 자체는 이미 클라이언트에 전달되었으므로 시스템을 중단하지 않고 로그만 남깁니다.
55+
}
3556
}
3657

3758
@MessageMapping("/chat.updateMessage")
@@ -66,7 +87,7 @@ public void deleteMessage(@Payload ChatMessageDto chatMessageDto, @Authenticatio
6687
// HTTP API를 통해 채팅 기록을 가져오는 엔드포인트 추가
6788
@GetMapping("/history")
6889
@Operation(summary = "채팅 기록 조회", description = "특정 파티의 채팅 기록을 조회합니다.")
69-
public Page<ChatMessage> getChatHistory(
90+
public Page<ChatMessageDto> getChatHistory(
7091
@PathVariable Integer partyId,
7192
@RequestParam(defaultValue = "0") int page,
7293
@RequestParam(defaultValue = "20") int size

0 commit comments

Comments
 (0)