Skip to content

[REFACTOR] OCR 데이터 전처리 및 인식률 개선#139

Merged
parkmineum merged 2 commits intodevfrom
refactor/#137_ocr
Aug 18, 2025
Merged

[REFACTOR] OCR 데이터 전처리 및 인식률 개선#139
parkmineum merged 2 commits intodevfrom
refactor/#137_ocr

Conversation

@parkmineum
Copy link
Collaborator

@parkmineum parkmineum commented Aug 18, 2025

🎋 이슈 및 작업중인 브랜치

🔑 주요 내용

  • OCR 하기 전 이미지 전처리를 통해 인식률을 개선하였습니다.

Check List

  • Reviewers 등록을 하였나요?
  • Assignees 등록을 하였나요?
  • 라벨(Label) 등록을 하였나요?
  • PR 머지하기 전 반드시 CI가 정상적으로 작동하는지 확인해주세요!

Summary by CodeRabbit

  • 신규 기능

    • OCR 성능 향상: 이미지 전처리, 다중 템플릿 시도, 유효성 기반 재시도/최적화 경로 추가
    • 건강지표 인식 고도화: 패턴 기반 범용 파서 도입, 값 정규화/검증 및 범위 체크로 정확도 개선
    • 설정 추가: 재시도 횟수/지연, 이미지 전처리, 다중 템플릿, 신뢰도 임계값 등 구성 가능
  • 리팩터링

    • OCR 요청 처리 및 로그 구조 중앙화, 파싱 파이프라인 일원화
  • 잡무(Chores)

    • 테스트 유틸 의존성 추가 및 경미한 포맷 정리

- OCR 이미지 전처리 유틸리티 클래스 구현
- 이미지 품질 분석 기능 (해상도, 대비 검사)
- 해상도 개선, 대비 향상, 노이즈 제거 기능
- OCR 정확도 향상을 위한 전처리 파이프라인 구축
@parkmineum parkmineum self-assigned this Aug 18, 2025
@parkmineum parkmineum added the ♻️ REFACTOR 리팩토링 관련 라벨 label Aug 18, 2025
@parkmineum parkmineum linked an issue Aug 18, 2025 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

  • build.gradle: spring-security-test를 testImplementation에 추가하고 주석/공백 정리.
  • OcrServiceImpl: 라인별 파싱을 제거하고 정규식 기반 보편 파서 도입. 건강지표 패턴 맵, 값 범위 검증, 전처리/정규화/유효성 검증 헬퍼 추가. extractText가 새 파이프라인을 사용하도록 변경. 로깅 확대.
  • ClovaOcrClient: 생성자에 ImagePreprocessingUtil 주입으로 변경. 최적화 경로, 다중 템플릿, 재시도 OCR 메서드 추가. 요청 처리 공통화, 결과 유효성/필드 카운트 검증 유틸 추가. JSON(ObjectMapper) 사용 및 로깅 추가.
  • ImagePreprocessingUtil: 새 컴포넌트 추가. 이미지 품질 분석, 전처리 파이프라인(해상도/대비/노이즈), 바이트 변환 및 결과 객체 제공.
  • application.yml: app.ocr 설정 블록 추가(재시도/지연/전처리/다중 템플릿/임계값).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#137_ocr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@parkmineum parkmineum changed the title [REFACTOR] OCR 전처리 및 인식률 개선 [REFACTOR] OCR 데이터 전처리 및 인식률 개선 Aug 18, 2025
@parkmineum parkmineum merged commit ba1afc1 into dev Aug 18, 2025
2 of 3 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
src/main/java/com/mumuk/global/util/ImagePreprocessingUtil.java (3)

28-29: 전처리 실패 시 기본값 처리 재고려 필요

이미지 품질 분석 실패 시 무조건 true를 반환하면 불필요한 전처리가 수행될 수 있습니다. 실패 원인에 따라 원본 이미지를 사용하는 것이 더 나을 수 있습니다.

         } catch (IOException e) {
             log.error("❌ 전처리 필요 여부 판단 실패", e);
-            return true; // 안전하게 전처리 필요하다고 처리
+            // 분석 실패 시 원본 사용을 위해 false 반환
+            return false;
         }

98-123: 대비 검사 임계값 설정 가능하도록 개선

하드코딩된 분산 임계값(1000)은 이미지 특성에 따라 적절하지 않을 수 있습니다. 설정 파일에서 조정 가능하도록 하는 것이 좋습니다.

+    @Value("${app.ocr.contrast-threshold:1000}")
+    private double contrastThreshold;
+    
     private boolean hasLowContrast(BufferedImage image) {
         // 간단한 대비 검사 로직
         int[] histogram = new int[256];
         
         // ... (히스토그램 계산 코드)
         
-        return variance < 1000; // 임계값은 조정 가능
+        return variance < contrastThreshold;
     }

201-221: ImageQualityResult 클래스를 record로 변경 고려

Java 14+ 환경에서는 간단한 데이터 클래스를 record로 구현하는 것이 더 간결합니다.

-    public static class ImageQualityResult {
-        private final boolean sizeIssue;
-        private final boolean contrastIssue;
-        
-        public ImageQualityResult(boolean sizeIssue, boolean contrastIssue) {
-            this.sizeIssue = sizeIssue;
-            this.contrastIssue = contrastIssue;
-        }
-        
-        public boolean hasSizeIssue() {
-            return sizeIssue;
-        }
-        
-        public boolean hasContrastIssue() {
-            return contrastIssue;
-        }
-        
-        public boolean hasAnyIssue() {
-            return sizeIssue || contrastIssue;
-        }
-    }
+    public record ImageQualityResult(boolean sizeIssue, boolean contrastIssue) {
+        public boolean hasSizeIssue() {
+            return sizeIssue;
+        }
+        
+        public boolean hasContrastIssue() {
+            return contrastIssue;
+        }
+        
+        public boolean hasAnyIssue() {
+            return sizeIssue || contrastIssue;
+        }
+    }
src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java (3)

25-76: 정규식 패턴 최적화 및 유지보수성 개선

정규식 패턴들이 복잡하고 중복된 부분이 많습니다. 패턴을 외부 설정으로 관리하거나 더 체계적으로 구성하는 것이 좋습니다.

패턴 관리를 위한 별도 클래스 생성을 고려하세요:

@Component
public class HealthPatternManager {
    private final Map<Pattern, String> patterns;
    
    public HealthPatternManager(@Value("${app.ocr.patterns-config}") Resource patternsConfig) {
        // JSON 또는 YAML 파일에서 패턴 로드
        this.patterns = loadPatternsFromConfig(patternsConfig);
    }
}

136-185: parseHealthDataUniversally 메서드의 성능 최적화 필요

현재 구현은 모든 패턴에 대해 순차적으로 매칭을 시도하므로 O(n*m) 복잡도를 가집니다. 텍스트가 길거나 패턴이 많을 경우 성능 문제가 발생할 수 있습니다.

병렬 처리를 통한 성능 개선:

+    import java.util.concurrent.ConcurrentHashMap;
+    import java.util.stream.Collectors;
+    
     private Map<String, String> parseHealthDataUniversally(String rawText) {
-        Map<String, String> result = new LinkedHashMap<>();
-        Set<String> usedValues = new HashSet<>();
+        Map<String, String> result = new ConcurrentHashMap<>();
+        Set<String> usedValues = ConcurrentHashMap.newKeySet();
         
         log.info("🔍 범용 건강 데이터 파싱 시작");
         
         String cleanedText = preprocessText(rawText);
         
-        // 각 패턴에 대해 매칭 시도
-        for (Map.Entry<Pattern, String> entry : HEALTH_PATTERNS.entrySet()) {
+        // 병렬 처리로 성능 향상
+        HEALTH_PATTERNS.entrySet().parallelStream().forEach(entry -> {
             Pattern pattern = entry.getKey();
             String categoryPrefix = entry.getValue();
             
             Matcher matcher = pattern.matcher(cleanedText);
             
             while (matcher.find()) {
                 // ... (기존 로직)
             }
-        }
+        });
         
-        log.info("🔍 파싱 완료, 추출된 항목 수: {}", result.size());
-        return result;
+        // LinkedHashMap으로 변환하여 순서 유지
+        Map<String, String> orderedResult = new LinkedHashMap<>(result);
+        log.info("🔍 파싱 완료, 추출된 항목 수: {}", orderedResult.size());
+        return orderedResult;
     }

206-240: isValidHealthValue 메서드의 매직 넘버 제거

하드코딩된 값들(0, 10000, 3)을 상수로 정의하여 가독성과 유지보수성을 향상시키세요.

+    private static final double MIN_VALID_VALUE = 0.0;
+    private static final double MAX_VALID_VALUE = 10000.0;
+    private static final int MAX_DECIMAL_PLACES = 3;
+    
     private boolean isValidHealthValue(String category, String key, String value) {
         try {
             double val = Double.parseDouble(value);
             
             double[] range = VALUE_RANGES.get(category);
             if (range != null) {
                 // ... (기존 로직)
             } else {
                 // 범위가 정의되지 않은 경우 기본 검사
-                if (val <= 0 || val > 10000) {
+                if (val <= MIN_VALID_VALUE || val > MAX_VALID_VALUE) {
                     log.debug("⚠️ 기본 범위 벗어남: {} = {}", key, val);
                     return false;
                 }
             }
             
             // 소수점 자릿수 체크
             String[] parts = value.split("\\.");
-            if (parts.length > 1 && parts[1].length() > 3) {
+            if (parts.length > 1 && parts[1].length() > MAX_DECIMAL_PLACES) {
                 log.debug("⚠️ 소수점 자릿수 초과: {} = {}", key, value);
                 return false;
             }
src/main/java/com/mumuk/global/client/ClovaOcrClient.java (1)

325-369: MultipartFile 구현의 잠재적 메모리 문제

익명 클래스로 구현된 MultipartFile이 byte 배열을 직접 참조하고 있어 대용량 이미지 처리 시 메모리 문제가 발생할 수 있습니다.

Spring의 MockMultipartFile을 사용하거나 스트림 기반 구현을 고려하세요:

+import org.springframework.mock.web.MockMultipartFile;
+
     private MultipartFile createMultipartFileFromBytes(byte[] bytes, String originalFilename) {
-        return new MultipartFile() {
-            // ... 익명 클래스 구현
-        };
+        return new MockMultipartFile(
+            "file",
+            originalFilename,
+            "image/jpeg",
+            bytes
+        );
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 367226e and 71bb700.

📒 Files selected for processing (5)
  • build.gradle (1 hunks)
  • src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java (3 hunks)
  • src/main/java/com/mumuk/global/client/ClovaOcrClient.java (4 hunks)
  • src/main/java/com/mumuk/global/util/ImagePreprocessingUtil.java (1 hunks)
  • src/main/resources/application.yml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/com/mumuk/global/client/ClovaOcrClient.java (2)
src/main/java/com/mumuk/global/util/FileResourceUtil.java (1)
  • FileResourceUtil (13-37)
src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java (1)
  • Slf4j (17-280)
🔇 Additional comments (1)
build.gradle (1)

82-83: Spring Security 테스트 의존성 추가 확인

OCR 기능 개선과 직접적인 연관은 없지만, Spring Security 테스트 의존성 추가는 적절합니다. 향후 OCR API 엔드포인트에 대한 보안 테스트를 위해 필요할 수 있습니다.

Comment on lines +86 to +116
public String callClovaOcrWithMultipleTemplates(MultipartFile imageFile) {
try {
log.info("다중 템플릿 OCR 시작");

// 인바디 관련 여러 템플릿 시도
int[] templateIds = {38491, 0}; // 38491: 기본 템플릿, 0: 범용 템플릿

String bestResult = null;
int maxFieldCount = 0;

for (int templateId : templateIds) {
log.info("템플릿 {} 시도", templateId);
String result = callClovaOcrWithTemplate(imageFile, templateId);
int fieldCount = countExtractedFields(result);

log.info("템플릿 {} 결과: {} 필드 추출", templateId, fieldCount);

if (fieldCount > maxFieldCount) {
maxFieldCount = fieldCount;
bestResult = result;
}
}

log.info("최적 결과 선택: {} 필드", maxFieldCount);
return bestResult;

} catch (Exception e) {
log.warn("다중 템플릿 실패, 기본 템플릿 사용");
return callClovaOcr(imageFile);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

다중 템플릿 OCR의 하드코딩된 템플릿 ID

템플릿 ID(38491, 0)가 하드코딩되어 있습니다. 설정 파일에서 관리하는 것이 좋습니다.

+    @Value("${app.ocr.template-ids:38491,0}")
+    private int[] templateIds;
+    
     public String callClovaOcrWithMultipleTemplates(MultipartFile imageFile) {
         try {
             log.info("다중 템플릿 OCR 시작");
             
-            // 인바디 관련 여러 템플릿 시도
-            int[] templateIds = {38491, 0}; // 38491: 기본 템플릿, 0: 범용 템플릿
-            
             String bestResult = null;
             int maxFieldCount = 0;
             
             for (int templateId : templateIds) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String callClovaOcrWithMultipleTemplates(MultipartFile imageFile) {
try {
log.info("다중 템플릿 OCR 시작");
// 인바디 관련 여러 템플릿 시도
int[] templateIds = {38491, 0}; // 38491: 기본 템플릿, 0: 범용 템플릿
String bestResult = null;
int maxFieldCount = 0;
for (int templateId : templateIds) {
log.info("템플릿 {} 시도", templateId);
String result = callClovaOcrWithTemplate(imageFile, templateId);
int fieldCount = countExtractedFields(result);
log.info("템플릿 {} 결과: {} 필드 추출", templateId, fieldCount);
if (fieldCount > maxFieldCount) {
maxFieldCount = fieldCount;
bestResult = result;
}
}
log.info("최적 결과 선택: {} 필드", maxFieldCount);
return bestResult;
} catch (Exception e) {
log.warn("다중 템플릿 실패, 기본 템플릿 사용");
return callClovaOcr(imageFile);
}
}
@Value("${app.ocr.template-ids:38491,0}")
private int[] templateIds;
public String callClovaOcrWithMultipleTemplates(MultipartFile imageFile) {
try {
log.info("다중 템플릿 OCR 시작");
String bestResult = null;
int maxFieldCount = 0;
for (int templateId : templateIds) {
log.info("템플릿 {} 시도", templateId);
String result = callClovaOcrWithTemplate(imageFile, templateId);
int fieldCount = countExtractedFields(result);
log.info("템플릿 {} 결과: {} 필드 추출", templateId, fieldCount);
if (fieldCount > maxFieldCount) {
maxFieldCount = fieldCount;
bestResult = result;
}
}
log.info("최적 결과 선택: {} 필드", maxFieldCount);
return bestResult;
} catch (Exception e) {
log.warn("다중 템플릿 실패, 기본 템플릿 사용");
return callClovaOcr(imageFile);
}
}
🤖 Prompt for AI Agents
In src/main/java/com/mumuk/global/client/ClovaOcrClient.java around lines 86 to
116, the template IDs (38491, 0) are hardcoded; move them into configuration and
inject them so they can be changed without code edits. Add a new property in
application.properties/yml (e.g. clova.ocr.template-ids=38491,0), add a field in
ClovaOcrClient to receive that value (List<Integer> or String that you parse
into ints) via @Value or @ConfigurationProperties, validate/parse the configured
values on bean init (fall back to a safe default if missing), and replace the
hardcoded int[] templateIds with the injected list when iterating; also log when
configuration is missing/invalid and ensure behavior remains the same (use
configured order and keep the existing fallback to callClovaOcr on exceptions).

Comment on lines +121 to +159
public String callClovaOcrWithRetry(MultipartFile imageFile, int maxRetries) {
Exception lastException = null;

for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
log.info("OCR 시도 {}/{}", attempt, maxRetries);

// 첫 번째 시도는 기본, 이후는 전처리 적용
boolean enablePreprocessing = attempt > 1;
String result = callClovaOcrWithOptimization(imageFile, enablePreprocessing);

// 결과 품질 검증
if (isValidOcrResult(result)) {
log.info("OCR 성공 (시도 {})", attempt);
return result;
} else {
log.warn("OCR 결과 품질 불량 (시도 {})", attempt);
if (attempt < maxRetries) {
Thread.sleep(1000 * attempt); // 점진적 대기
}
}

} catch (Exception e) {
lastException = e;
log.warn("OCR 실패 (시도 {}): {}", attempt, e.getMessage());

if (attempt < maxRetries) {
try {
Thread.sleep(2000 * attempt); // 점진적 대기
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}

throw new RuntimeException("OCR 최대 재시도 횟수 초과", lastException);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

재시도 로직의 Thread.sleep 사용 문제

Thread.sleep()을 사용한 재시도 지연은 스레드를 블로킹하여 성능 문제를 일으킬 수 있습니다. Spring의 @Retryable 어노테이션이나 비동기 처리를 고려하세요.

+import org.springframework.retry.annotation.Backoff;
+import org.springframework.retry.annotation.Retryable;
+
-    public String callClovaOcrWithRetry(MultipartFile imageFile, int maxRetries) {
-        Exception lastException = null;
-        
-        for (int attempt = 1; attempt <= maxRetries; attempt++) {
-            try {
-                // ... 재시도 로직
-                Thread.sleep(1000 * attempt);
-            } catch (Exception e) {
-                // ... 예외 처리
-            }
-        }
-        throw new RuntimeException("OCR 최대 재시도 횟수 초과", lastException);
-    }
+    @Retryable(
+        value = {RuntimeException.class},
+        maxAttempts = 3,
+        backoff = @Backoff(delay = 1000, multiplier = 2)
+    )
+    public String callClovaOcrWithRetry(MultipartFile imageFile) {
+        String result = callClovaOcrWithOptimization(imageFile, false);
+        if (!isValidOcrResult(result)) {
+            throw new RuntimeException("OCR 결과 품질 불량");
+        }
+        return result;
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/mumuk/global/client/ClovaOcrClient.java around lines 121 to
159, the current retry logic blocks threads using Thread.sleep; replace this
with a non-blocking retry strategy by refactoring the method to use Spring Retry
(annotate the OCR call method with @Retryable and configure maxAttempts and
Backoff for progressive waits) or implement an async retry using
CompletableFuture/ScheduledExecutorService so sleeps are not on the calling
thread; remove all Thread.sleep calls, surface InterruptedException handling
appropriately, and ensure the final failure still wraps the last exception and
is propagated.

Comment on lines +266 to +286
private int countExtractedFields(String ocrResult) {
try {
JsonNode root = objectMapper.readTree(ocrResult);
JsonNode fields = root.path("images").get(0).path("fields");

if (fields.size() > 0) {
String text = fields.get(0).path("inferText").asText();
// 인바디 관련 키워드 개수 계산
String[] keywords = {"체중", "체지방", "골격근", "BMI", "단백질", "체수분", "무기질"};
int count = 0;
for (String keyword : keywords) {
if (text.contains(keyword)) count++;
}
return count;
}
return 0;
} catch (Exception e) {
log.warn("필드 개수 계산 실패: {}", e.getMessage());
return 0;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

countExtractedFields 메서드의 키워드 하드코딩

건강 관련 키워드가 하드코딩되어 있어 OcrServiceImpl의 HEALTH_PATTERNS와 동기화되지 않을 수 있습니다.

키워드를 공통 상수로 관리하거나 설정 파일에서 관리하는 것이 좋습니다:

// 공통 상수 클래스
public class OcrConstants {
    public static final String[] HEALTH_KEYWORDS = {
        "체중", "체지방", "골격근", "BMI", "단백질", "체수분", "무기질"
    };
}

🏁 Script executed:

#!/bin/bash
# OcrServiceImpl과 ClovaOcrClient의 키워드 동기화 확인
echo "=== OcrServiceImpl의 건강 지표 패턴 ==="
rg -A1 'HEALTH_PATTERNS.put' src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java | grep -oP '"\K[^"]+(?=")'

echo "=== ClovaOcrClient의 키워드 ==="
rg -A2 'String\[\] keywords' src/main/java/com/mumuk/global/client/ClovaOcrClient.java

Length of output: 2941


공통 상수로 건강 키워드 관리 리팩터 필요
ClovaOcrClient의 countExtractedFields 메서드에서 하드코딩된 키워드 배열이 OcrServiceImplHEALTH_PATTERNS에 정의된 패턴 목록과 일치하지 않아, 유지보수 및 동기화 누락 위험이 있습니다. 두 곳에서 동일한 키워드를 참조하도록 공통 상수 또는 설정 파일로 분리해주세요.

문제가 되는 위치:

  • src/main/java/com/mumuk/global/client/ClovaOcrClient.java
    • String[] keywords = {"체중", "체지방", "골격근", "BMI", "단백질", "체수분", "무기질"};
  • src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java
    • 다수의 HEALTH_PATTERNS.put(...) 에서 사용하는 키워드

예시 공통 상수 클래스:

package com.mumuk.global.constants;

public final class OcrConstants {
    private OcrConstants() {}

    public static final String[] HEALTH_KEYWORDS = {
        "체중", "체지방량", "골격근량", "BMI", "단백질",
        "체수분", "무기질", "체지방률", "기초대사율",
        "수축기혈압", "이완기혈압", "혈당", "당화혈색소",
        "총콜레스테롤", "HDL콜레스테롤", "LDL콜레스테롤",
        "ALT", "AST", "크레아티닌", "요소질소",
        "CRP", "TSH", "비타민D", "기타지표"
    };
}
  • ClovaOcrClient와 OcrServiceImpl에서 위 OcrConstants.HEALTH_KEYWORDS 를 참조하도록 변경해주세요.
🤖 Prompt for AI Agents
In src/main/java/com/mumuk/global/client/ClovaOcrClient.java around lines
266-286, the method currently uses a hardcoded String[] keywords which must be
replaced with a shared constant to avoid duplication; update the method to
reference com.mumuk.global.constants.OcrConstants.HEALTH_KEYWORDS instead of the
inline array, add the necessary import, and remove the local array; also ensure
src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java references the
same OcrConstants.HEALTH_KEYWORDS (or populates HEALTH_PATTERNS from it) so both
classes use the single shared constant and keep keyword lists synchronized.

Comment on lines +173 to +198
private BufferedImage removeNoise(BufferedImage image) {
// 간단한 블러 필터로 노이즈 제거
BufferedImage denoised = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);

for (int y = 1; y < image.getHeight() - 1; y++) {
for (int x = 1; x < image.getWidth() - 1; x++) {
int totalR = 0, totalG = 0, totalB = 0;

for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int rgb = image.getRGB(x + dx, y + dy);
totalR += (rgb >> 16) & 0xFF;
totalG += (rgb >> 8) & 0xFF;
totalB += rgb & 0xFF;
}
}

int avgR = totalR / 9;
int avgG = totalG / 9;
int avgB = totalB / 9;

denoised.setRGB(x, y, (avgR << 16) | (avgG << 8) | avgB);
}
}

return denoised;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

노이즈 제거 알고리즘의 엣지 처리 개선 필요

현재 구현은 이미지 경계(1픽셀)를 처리하지 않아 검은 테두리가 생길 수 있습니다.

     private BufferedImage removeNoise(BufferedImage image) {
         // 간단한 블러 필터로 노이즈 제거
         BufferedImage denoised = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
         
+        // 경계 픽셀 복사
+        for (int x = 0; x < image.getWidth(); x++) {
+            denoised.setRGB(x, 0, image.getRGB(x, 0));
+            denoised.setRGB(x, image.getHeight() - 1, image.getRGB(x, image.getHeight() - 1));
+        }
+        for (int y = 0; y < image.getHeight(); y++) {
+            denoised.setRGB(0, y, image.getRGB(0, y));
+            denoised.setRGB(image.getWidth() - 1, y, image.getRGB(image.getWidth() - 1, y));
+        }
+        
         for (int y = 1; y < image.getHeight() - 1; y++) {
             for (int x = 1; x < image.getWidth() - 1; x++) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private BufferedImage removeNoise(BufferedImage image) {
// 간단한 블러 필터로 노이즈 제거
BufferedImage denoised = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
for (int y = 1; y < image.getHeight() - 1; y++) {
for (int x = 1; x < image.getWidth() - 1; x++) {
int totalR = 0, totalG = 0, totalB = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int rgb = image.getRGB(x + dx, y + dy);
totalR += (rgb >> 16) & 0xFF;
totalG += (rgb >> 8) & 0xFF;
totalB += rgb & 0xFF;
}
}
int avgR = totalR / 9;
int avgG = totalG / 9;
int avgB = totalB / 9;
denoised.setRGB(x, y, (avgR << 16) | (avgG << 8) | avgB);
}
}
return denoised;
private BufferedImage removeNoise(BufferedImage image) {
// 간단한 블러 필터로 노이즈 제거
BufferedImage denoised = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
// 경계 픽셀 복사
for (int x = 0; x < image.getWidth(); x++) {
denoised.setRGB(x, 0, image.getRGB(x, 0));
denoised.setRGB(x, image.getHeight() - 1, image.getRGB(x, image.getHeight() - 1));
}
for (int y = 0; y < image.getHeight(); y++) {
denoised.setRGB(0, y, image.getRGB(0, y));
denoised.setRGB(image.getWidth() - 1, y, image.getRGB(image.getWidth() - 1, y));
}
for (int y = 1; y < image.getHeight() - 1; y++) {
for (int x = 1; x < image.getWidth() - 1; x++) {
int totalR = 0, totalG = 0, totalB = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int rgb = image.getRGB(x + dx, y + dy);
totalR += (rgb >> 16) & 0xFF;
totalG += (rgb >> 8) & 0xFF;
totalB += rgb & 0xFF;
}
}
int avgR = totalR / 9;
int avgG = totalG / 9;
int avgB = totalB / 9;
denoised.setRGB(x, y, (avgR << 16) | (avgG << 8) | avgB);
}
}
return denoised;
}

Comment on lines +62 to +67
ocr:
max-retries: 3
retry-delay-ms: 2000
enable-image-preprocessing: true
enable-multi-template: true
confidence-threshold: 0.7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

OCR 설정값들의 적절성 검토

설정값들이 추가되었지만, 프로덕션 환경에서의 적절성을 검토해야 합니다:

  • max-retries: 3retry-delay-ms: 2000는 최대 6초의 지연을 발생시킬 수 있습니다
  • confidence-threshold: 0.7이 실제 코드에서 사용되지 않고 있습니다

🏁 Script executed:

#!/bin/bash
# confidence-threshold 설정이 코드에서 사용되는지 확인
rg -n "confidence-threshold" --type java

Length of output: 40


추가 검증을 위해 다음 스크립트를 실행해 주세요:


🏁 Script executed:

#!/bin/bash
# Spring ConfigurationProperties 어노테이션으로 ocr 설정이 바인딩되는 클래스 확인
rg -n '@ConfigurationProperties' -C2 --type java

# Java 코드 내에서 confidenceThreshold 필드(카멜케이스) 검색
rg -n 'confidenceThreshold' --type java

# @Value 어노테이션으로 직접 주입되는지 확인
rg -n "@Value.*confidence-threshold" --type java

Length of output: 139


OCR 설정 프로덕션 적합성 검토 및 미사용 프로퍼티 정리

설정값 검토가 필요합니다:

  • max-retries: 3 × retry-delay-ms: 2000 조합으로 요청 실패 시 최대 6초 지연이 발생할 수 있습니다. 프로덕션 SLA에 부합하는지 확인해 주세요.
  • confidence-threshold: 0.7 프로퍼티가 현재 코드에 바인딩되거나 사용되지 않고 있습니다. 실제 로직에서 사용하도록 구현하거나, 불필요하다면 설정에서 제거해 주세요.
🤖 Prompt for AI Agents
In src/main/resources/application.yml around lines 62 to 67, review and adjust
the OCR retry and confidence settings: verify that max-retries: 3 and
retry-delay-ms: 2000 (up to ~6s total delay) meet production SLA and either
lower max-retries or retry-delay-ms (or add exponential backoff/timeout caps) to
comply with latency requirements; also address confidence-threshold: 0.7 which
is not bound/used — either wire this property into the OCR decision logic (load
it into the OCR properties/config class and apply it where recognition results
are filtered/accepted) or remove the unused property from the YAML and code to
avoid dead configuration.

@jaemin0413 jaemin0413 deleted the refactor/#137_ocr branch August 27, 2025 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ REFACTOR 리팩토링 관련 라벨

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] OCR 인식 정확도 개선

1 participant