Skip to content

Commit ce07a2b

Browse files
committed
feat : 다수결 스무딩 적용
1 parent 8b101b6 commit ce07a2b

File tree

4 files changed

+173
-9
lines changed

4 files changed

+173
-9
lines changed

HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/HalfFiftyBeApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
56

67
@SpringBootApplication
8+
@EnableScheduling
79
public class HalfFiftyBeApplication {
810

911
public static void main(String[] args) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package HalfFifty.HalfFifty_BE.translation.bean;
2+
3+
import HalfFifty.HalfFifty_BE.translation.domain.DTO.FrameBufferResult;
4+
import org.springframework.scheduling.annotation.Scheduled;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.time.LocalDateTime;
8+
import java.util.*;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
import java.util.function.Function;
11+
import java.util.stream.Collectors;
12+
13+
@Component
14+
public class FrameBufferBean {
15+
private final Map<UUID, BufferData> userFrameBuffers = new ConcurrentHashMap<>();
16+
private final int BUFFER_SIZE = 5;
17+
private final int REQUIRED_MATCHES = 3;
18+
private final int TIMEOUT_MINUTES = 10; // 10분 후 자동 삭제
19+
20+
// 버퍼 데이터와 타임스탬프를 함께 저장하는 내부 클래스
21+
private static class BufferData {
22+
List<String> buffer;
23+
LocalDateTime lastAccess;
24+
25+
BufferData() {
26+
this.buffer = new ArrayList<>();
27+
this.lastAccess = LocalDateTime.now();
28+
}
29+
30+
void updateAccess() {
31+
this.lastAccess = LocalDateTime.now();
32+
}
33+
}
34+
35+
public FrameBufferResult addFrame(UUID userId, String predictedWord) {
36+
if (userId == null || predictedWord == null || predictedWord.trim().isEmpty()) {
37+
return new FrameBufferResult(false, null, 0, BUFFER_SIZE);
38+
}
39+
40+
BufferData bufferData = userFrameBuffers.computeIfAbsent(userId, k -> new BufferData());
41+
bufferData.updateAccess(); // 접근 시간 갱신
42+
43+
List<String> buffer = bufferData.buffer;
44+
buffer.add(predictedWord.trim());
45+
46+
// 버퍼 크기 제한
47+
if (buffer.size() > BUFFER_SIZE) {
48+
buffer.remove(0);
49+
}
50+
51+
// 다수결 확인
52+
if (buffer.size() >= BUFFER_SIZE) {
53+
return checkMajority(buffer, userId);
54+
}
55+
56+
return new FrameBufferResult(false, null, buffer.size(), BUFFER_SIZE);
57+
}
58+
59+
private FrameBufferResult checkMajority(List<String> buffer, UUID userId) {
60+
try {
61+
Map<String, Long> wordCounts = buffer.stream()
62+
.filter(Objects::nonNull)
63+
.filter(word -> !word.trim().isEmpty())
64+
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
65+
66+
Optional<Map.Entry<String, Long>> mostFrequent = wordCounts.entrySet().stream()
67+
.max(Map.Entry.comparingByValue());
68+
69+
if (mostFrequent.isPresent() && mostFrequent.get().getValue() >= REQUIRED_MATCHES) {
70+
// 확정되면 해당 사용자의 버퍼 초기화
71+
clearBuffer(userId);
72+
return new FrameBufferResult(true, mostFrequent.get().getKey(), BUFFER_SIZE, BUFFER_SIZE);
73+
}
74+
75+
return new FrameBufferResult(false, null, buffer.size(), BUFFER_SIZE);
76+
77+
} catch (Exception e) {
78+
System.err.println("Error in checkMajority: " + e.getMessage());
79+
return new FrameBufferResult(false, null, buffer.size(), BUFFER_SIZE);
80+
}
81+
}
82+
83+
public void clearBuffer(UUID userId) {
84+
if (userId != null) {
85+
userFrameBuffers.remove(userId);
86+
}
87+
}
88+
89+
public List<String> getBuffer(UUID userId) {
90+
if (userId == null) {
91+
return new ArrayList<>();
92+
}
93+
BufferData bufferData = userFrameBuffers.get(userId);
94+
return bufferData != null ? new ArrayList<>(bufferData.buffer) : new ArrayList<>();
95+
}
96+
97+
// 메모리 누수 방지: 10분마다 오래된 버퍼 자동 삭제
98+
@Scheduled(fixedRate = 600000) // 10분마다 실행
99+
public void cleanupOldBuffers() {
100+
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(TIMEOUT_MINUTES);
101+
102+
Iterator<Map.Entry<UUID, BufferData>> iterator = userFrameBuffers.entrySet().iterator();
103+
int removedCount = 0;
104+
105+
while (iterator.hasNext()) {
106+
Map.Entry<UUID, BufferData> entry = iterator.next();
107+
if (entry.getValue().lastAccess.isBefore(cutoffTime)) {
108+
iterator.remove();
109+
removedCount++;
110+
}
111+
}
112+
113+
if (removedCount > 0) {
114+
System.out.println("정리된 비활성 버퍼 개수: " + removedCount);
115+
}
116+
117+
System.out.println("현재 활성 버퍼 개수: " + userFrameBuffers.size());
118+
}
119+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package HalfFifty.HalfFifty_BE.translation.domain.DTO;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
7+
@Data
8+
@AllArgsConstructor
9+
@NoArgsConstructor
10+
public class FrameBufferResult {
11+
private boolean confirmed; // 확정 여부
12+
private String confirmedWord; // 확정된 단어
13+
private int currentBufferSize; // 현재 버퍼 크기
14+
private int requiredBufferSize; // 필요한 버퍼 크기
15+
}

HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,67 @@
22

33
import HalfFifty.HalfFifty_BE.translation.bean.DeleteTranslationBean;
44
import HalfFifty.HalfFifty_BE.translation.bean.FlaskSignLanguageBean;
5+
import HalfFifty.HalfFifty_BE.translation.bean.FrameBufferBean;
56
import HalfFifty.HalfFifty_BE.translation.bean.SaveTranslationBean;
7+
import HalfFifty.HalfFifty_BE.translation.domain.DTO.FrameBufferResult;
68
import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestSignLanguageDTO;
79
import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestTranslationDeleteDTO;
810
import HalfFifty.HalfFifty_BE.translation.domain.DTO.ResponseTranslationGetDTO;
911
import org.springframework.beans.factory.annotation.Autowired;
1012
import org.springframework.stereotype.Service;
1113

1214
import java.util.Map;
15+
import java.util.UUID;
1316

1417

1518
@Service
1619
public class TranslationService {
17-
SaveTranslationBean saveTranslationBean;
18-
FlaskSignLanguageBean flaskSignLanguageBean;
19-
DeleteTranslationBean deleteTranslationBean;
20+
private final SaveTranslationBean saveTranslationBean;
21+
private final FlaskSignLanguageBean flaskSignLanguageBean;
22+
private final DeleteTranslationBean deleteTranslationBean;
23+
private final FrameBufferBean frameBufferBean;
2024

2125
@Autowired
22-
public TranslationService(SaveTranslationBean saveTranslationBean, FlaskSignLanguageBean flaskSignLanguageBean, DeleteTranslationBean deleteTranslationBean) {
26+
public TranslationService(SaveTranslationBean saveTranslationBean,
27+
FlaskSignLanguageBean flaskSignLanguageBean,
28+
DeleteTranslationBean deleteTranslationBean,
29+
FrameBufferBean frameBufferBean) {
2330
this.saveTranslationBean = saveTranslationBean;
2431
this.flaskSignLanguageBean = flaskSignLanguageBean;
2532
this.deleteTranslationBean = deleteTranslationBean;
33+
this.frameBufferBean = frameBufferBean;
2634
}
2735

36+
// 기존 메서드 시그니처 그대로 유지하면서 내부적으로 버퍼링 처리
2837
public ResponseTranslationGetDTO signLanguageTranslation(RequestSignLanguageDTO requestSignLanguageDTO) {
38+
// AI 서버에서 예측 결과 받기
2939
Map<String, Object> aiResponse = flaskSignLanguageBean.exec(requestSignLanguageDTO);
3040

3141
if (aiResponse != null && Boolean.TRUE.equals(aiResponse.get("success"))) {
32-
String translatedWord = (String) aiResponse.get("predicted_label");
33-
Double probability = (Double) aiResponse.get("confidence");
34-
return saveTranslationBean.exec(requestSignLanguageDTO.getUserId(), translatedWord, probability);
35-
} else {
36-
return null;
42+
String predictedWord = (String) aiResponse.get("predicted_label");
43+
Double confidence = (Double) aiResponse.get("confidence");
44+
45+
// 프레임 버퍼에 추가하고 다수결 확인
46+
FrameBufferResult bufferResult = frameBufferBean.addFrame(
47+
requestSignLanguageDTO.getUserId(),
48+
predictedWord
49+
);
50+
51+
// 확정된 경우에만 저장하고 반환
52+
if (bufferResult.isConfirmed()) {
53+
return saveTranslationBean.exec(
54+
requestSignLanguageDTO.getUserId(),
55+
bufferResult.getConfirmedWord(),
56+
confidence
57+
);
58+
} else {
59+
// 아직 확정되지 않은 경우 null 반환
60+
// -> 프론트엔드에서는 기존처럼 "번역 실패"로 처리됨
61+
return null;
62+
}
3763
}
64+
65+
return null;
3866
}
3967

4068
// 번역 기록 삭제

0 commit comments

Comments
 (0)