모듈 인터페이스 설계 및 구현 과정
설계 개요
문서 유사도 측정 및 표절 탐지 기능을 구현하기 위한 시스템을 설계했습니다. 이 시스템은 긴 텍스트 문서를 처리하는 데 중점을 두며, 코사인 유사도 알고리즘과 BoW(Bag-of-Words) 모델을 핵심 기술로 사용합니다. 구현은 Python 3.11 환경에서 진행되었습니다. 전체 표절 탐지 알고리즘은 DocumentAnalyzer 클래스 내에 캡슐화되어 객체 지향 방식으로 구성되었으며, 이는 코드의 모듈성과 유지보수성을 보장합니다.
함수 설계 및 모듈 간 상호작용
DocumentAnalyzer 클래스는 다음 주요 메서드들을 포함합니다.
__init__: 텍스트 토큰화를 위한Jieba분할기를 초기화합니다.retrieve_document_content(file_path): 지정된 경로에서 문서 내용을 읽어옵니다.preprocess_and_vectorize(text_data): 텍스트 내용을 토큰화하고 전처리하여 단어 목록을 생성합니다. 이 목록은 이후 BoW 모델 구축에 사용됩니다.calculate_cosine_similarity(tokens1, tokens2): 두 문서의 토큰 목록을 받아 BoW 모델을 구축하고, 이를 기반으로 코사인 유사도를 계산합니다.execute_comparison(path1, path2): 전체 문서 비교 과정을 실행하고 유사도 결과를 반환합니다.
보조 함수인 main_application_entry()는 명령줄 인수를 처리하고 전체 프로그램 흐름을 제어합니다.
각 함수는 다음 관계에 따라 호출됩니다:
main_application_entry는DocumentAnalyzer인스턴스를 생성하고,execute_comparison메서드를 호출합니다.execute_comparison은 내부적으로retrieve_document_content를 호출하여 문서 데이터를 가져옵니다.- 가져온 데이터는
preprocess_and_vectorize를 통해 처리되고 토큰 목록으로 변환됩니다. - 최종적으로
calculate_cosine_similarity를 호출하여 유사도 점수를 얻습니다.
알고리즘 설계 및 흐름
알고리즘의 주요 단계는 다음과 같습니다.
- 입력 텍스트에 대해
Jieba분할기를 사용하여 단어 시퀀스로 처리하고, 구두점과 같은 불필요한 기호를 제거합니다. - 원본 문서와 비교 대상 문서를 읽고, 각각 분할 및 필터링 처리를 수행합니다.
Gensim라이브러리를 활용하여 각 문서의 토큰 목록을 기반으로 BoW(Bag-of-Words) 모델을 구축합니다.- 두 문서의 BoW 모델을 기반으로 코사인 유사도를 계산하고, 결과를 0에서 1 사이의 값으로 정규화합니다.
- 계산된 유사도 결과를 파일로 출력합니다.
이 알고리즘의 핵심은 효율적인 텍스트 분할과 BoW 모델의 구축에 있습니다. 텍스트 분할은 문서 내용을 단어 빈도 벡터로 변환하는 기초를 마련하며, BoW 모델은 문서 유사도 분석의 핵심 기반을 제공합니다.
이 방식은 복잡한 의미론적 분석 없이 구현이 비교적 간단하며, 대량의 문서 처리에 적합합니다. 또한 단어의 순서에 둔감하여 구조화된 복사보다는 내용 자체의 중복을 감지하는 데 효과적이며, 별도의 거대한 사전(Lexicon) 데이터베이스에 의존하지 않습니다.
성능 개선
초기 구현은 주어진 불용어 목록에 기반한 Simhash 알고리즘을 사용했습니다. 이 방식은 불용어 목록을 반복적으로 확인하는 과정에서 높은 시간 비용을 초래했으며, 특히 _find_and_load 함수와 같은 특정 함수에서 과도한 오버헤드가 발생했습니다.
또한, 초기 Simhash 기반 알고리즘은 텍스트 간의 차이가 큰 경우에도 50% 이상의 유사도를 반환하는 등, 매우 상이한 문서들을 명확하게 구분하지 못하는 한계가 있었습니다.
이러한 문제점을 해결하기 위해 코사인 유사도와 BoW 모델 기반의 알고리즘으로 개선 작업을 진행했습니다. 개선된 알고리즘은 함수들의 평균 실행 시간을 단축하고, 자원 활용 효율성을 높였습니다.
특히, 현저히 다른 텍스트에 대해서는 훨씬 낮은 유사도 점수를 정확하게 산출하여, 문서 간의 차이를 훨씬 효과적으로 식별할 수 있게 되었습니다. 개선 후에도 특정 내부 함수(예: 리소스 로딩 관련 함수)가 여전히 가장 많은 시간을 소비하지만, 전반적인 평균 오버헤드는 크게 감소하여 성능이 향상되었음을 확인했습니다.
실행 결과
다음은 유사한 두 텍스트 파일인 source_doc.txt와 modified_doc.txt에 대한 실행 결과입니다.
두 파일의 경로를 입력했을 때, 시스템은 99.17%의 유사도 결과를 출력했습니다. 이는 예상 결과와 일치하는 수치입니다.
모듈 단위 테스트
코드의 안정성을 확보하기 위해 18개의 단위 테스트 케이스를 similarity_checker_tests.py 파일에 구현했습니다. 개선된 코드는 모든 테스트를 성공적으로 통과했습니다.
공간 제약으로 인해 다음은 세 가지 주요 테스트 케이스를 보여줍니다. (아래 코드는 단위 테스트 설명 목적의 간소화된 예시이며, 실제 구현에서는 DocumentAnalyzer 클래스가 별도의 모듈에서 임포트됩니다.)
테스트 1: 완전히 동일한 텍스트
완전히 동일한 두 텍스트의 유사도는 1.0에 가까워야 합니다.
import unittest
import tempfile
import os
import jieba
import re
import math
from collections import Counter
# --- 단위 테스트 설명 목적의 DocumentAnalyzer 클래스 ---
class DocumentAnalyzer:
def __init__(self):
# 실제 구현에서는 Jieba 초기화 로직이 여기에 포함될 수 있습니다.
pass
def retrieve_document_content(self, file_path):
try:
with open(file_path, 'r', encoding='UTF-8') as doc_file:
return doc_file.read()
except Exception: # 예외 처리 간소화
return ""
def preprocess_and_vectorize(self, content_data):
if not content_data:
return []
segmented_tokens = jieba.lcut(content_data) # Jieba 직접 사용
filtered_tokens = []
for token in segmented_tokens:
if re.match(r"[a-zA-Z0-9\uac00-\ud7a3]+", token): # 한글, 영문, 숫자만 포함
filtered_tokens.append(token)
return filtered_tokens
def calculate_cosine_similarity(self, tokens1, tokens2):
if not tokens1 or not tokens2:
return 0.0
vector1 = Counter(tokens1)
vector2 = Counter(tokens2)
all_tokens_union = set(vector1.keys()).union(set(vector2.keys()))
dot_product = sum(vector1.get(token, 0) * vector2.get(token, 0) for token in all_tokens_union)
magnitude1 = math.sqrt(sum(count**2 for count in vector1.values()))
magnitude2 = math.sqrt(sum(count**2 for count in vector2.values()))
if magnitude1 == 0 or magnitude2 == 0:
return 0.0
similarity = dot_product / (magnitude1 * magnitude2)
return similarity
# --- DocumentAnalyzer 클래스 끝 ---
class TestDocumentSimilarity(unittest.TestCase):
def setUp(self):
self.analyzer = DocumentAnalyzer()
def test_identical_texts_similarity(self):
doc_a_content = "이것은 완전히 동일한 테스트 문서입니다."
doc_b_content = "이것은 완전히 동일한 테스트 문서입니다."
vector_a = self.analyzer.preprocess_and_vectorize(doc_a_content)
vector_b = self.analyzer.preprocess_and_vectorize(doc_b_content)
similarity_score = self.analyzer.calculate_cosine_similarity(vector_a, vector_b)
self.assertAlmostEqual(similarity_score, 1.0, delta=0.1)
테스트 2: 완전히 다른 텍스트
완전히 다른 내용의 두 텍스트의 유사도는 0.4 미만이어야 합니다.
# ... (상단 import 및 DocumentAnalyzer 클래스 정의는 동일) ...
class TestDocumentSimilarity(unittest.TestCase):
def setUp(self):
self.analyzer = DocumentAnalyzer()
def test_distinct_texts_similarity(self):
doc_a_content = "인공지능과 머신러닝에 대한 첫 번째 테스트 문서입니다."
doc_b_content = "용기는 인간의 찬가이며, 공포는 생물의 본능이다."
vector_a = self.analyzer.preprocess_and_vectorize(doc_a_content)
vector_b = self.analyzer.preprocess_and_vectorize(doc_b_content)
similarity_score = self.analyzer.calculate_cosine_similarity(vector_a, vector_b)
self.assertLess(similarity_score, 0.4)
테스트 3: 빈 문서 처리
내용이 있는 문서와 빈 문서의 유사도는 0.0이어야 합니다.
# ... (상단 import 및 DocumentAnalyzer 클래스 정의는 동일) ...
class TestDocumentSimilarity(unittest.TestCase):
def setUp(self):
self.analyzer = DocumentAnalyzer()
def test_empty_document_handling(self):
non_empty_content = "내용이 포함된 문서입니다."
empty_content = "" # 빈 문자열
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f1:
f1.write(non_empty_content)
file_path_1 = f1.name
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f2:
file_path_2 = f2.name
try:
retrieved_content_1 = self.analyzer.retrieve_document_content(file_path_1)
retrieved_content_2 = self.analyzer.retrieve_document_content(file_path_2)
vector_a = self.analyzer.preprocess_and_vectorize(retrieved_content_1)
vector_b = self.analyzer.preprocess_and_vectorize(retrieved_content_2)
similarity_score = self.analyzer.calculate_cosine_similarity(vector_a, vector_b)
self.assertEqual(similarity_score, 0.0)
finally:
os.unlink(file_path_1)
os.unlink(file_path_2)
모든 단위 테스트가 완료된 후, 코드 커버리지 분석 도구(예: PyCharm 내장 기능)를 사용하여 코드 실행률을 검증했으며, 높은 커버리지를 달성했음을 확인했습니다.
모듈 예외 처리
시스템의 견고성을 높이기 위해 주요 단계에서 발생할 수 있는 예외 상황에 대한 처리를 구현했습니다.
예외 1: 문서 읽기 오류
사용자가 유효하지 않은 문서 경로(예: 파일 없음, 권한 문제, 인코딩 오류)를 제공할 경우, FileNotFoundError나 UnicodeDecodeError와 같은 예외로 인해 프로그램이 중단되는 것을 방지합니다. try-except 블록을 사용하여 모든 잠재적인 읽기 예외를 포착하고, 구체적인 오류 메시지를 출력한 후 빈 문자열("")을 반환하여 후속 처리가 안전하게 이루어지도록 합니다.
# DocumentAnalyzer 클래스 내 메서드
def retrieve_document_content(self, file_path):
try:
with open(file_path, 'r', encoding='UTF-8') as doc_file:
return doc_file.read()
except FileNotFoundError:
print(f"오류: 파일을 찾을 수 없습니다 - {file_path}")
return ""
except UnicodeDecodeError:
print(f"오류: 문서 인코딩 문제 발생 - {file_path}. UTF-8 인코딩을 확인하세요.")
return ""
except Exception as e:
print(f"문서 읽기 중 예상치 못한 오류 발생 ({file_path}): {e}")
return ""
해당 로직에 대한 테스트 스니펫:
# TestDocumentSimilarity 클래스 내 메서드 (setUp 메서드 이후)
def test_retrieve_nonexistent_file(self):
result = self.analyzer.retrieve_document_content("non_existent_doc_path_123.txt")
self.assertEqual(result, "") # 빈 문자열이 반환되어야 함
예외 2: 콘텐츠 처리 오류
이전 단계에서 문서 읽기가 실패하여 빈 문자열("")이 반환된 경우, preprocess_and_vectorize 메서드가 빈 내용을 안전하게 처리하도록 합니다. 메서드 초기에 content_data가 비어 있는지 확인하여, 비어 있다면 빈 리스트([])를 즉시 반환하여 불필요한 분할 및 필터링 시도를 방지합니다.
# DocumentAnalyzer 클래스 내 메서드
import re
import jieba # 가정: jieba가 설치되어 있고 사용 가능
def preprocess_and_vectorize(self, content_data):
if not content_data: # 빈 문자열 또는 None 값 처리
return []
segmented_tokens = jieba.lcut(content_data)
filtered_tokens = []
for token in segmented_tokens:
if re.match(r"[a-zA-Z0-9\uac00-\ud7a3]+", token): # 한글, 영문, 숫자 문자만 유지
filtered_tokens.append(token)
return filtered_tokens
해당 로직에 대한 테스트 스니펫:
# TestDocumentSimilarity 클래스 내 메서드 (setUp 메서드 이후)
def test_preprocess_empty_content(self):
result = self.analyzer.preprocess_and_vectorize("")
self.assertEqual(result, []) # 빈 리스트가 반환되어야 함
모델 개선 제안 및 사용 방법
모델 개선 제안
현재 모델은 다음과 같은 한계점을 가지고 있습니다.
- 단문 및 동의어 비중이 높은 텍스트: 짧은 텍스트나 동의어 사용 비중이 높은 유사 텍스트의 경우, 정확한 유사도 구분에 어려움을 겪을 수 있습니다.
- 문맥 및 어순 관계 인식 부족: 단어의 순서나 문맥적 의미에 민감한 텍스트의 경우, 이러한 관계를 충분히 반영하지 못할 수 있습니다.
이러한 한계점들을 극복하기 위한 개선 방향은 다음과 같습니다.
- 단어 시퀀스 정보 활용: N-gram 모델 또는 시퀀스 기반 모델을 도입하여 단어의 순서 정보를 고려합니다.
- 의미론적 벡터 통합: Word2Vec, BERT와 같은 임베딩 모델을 활용하여 단어 및 문장의 의미론적 유사도를 측정하고, 이를 기존 BoW 모델과 결합하거나 대체합니다.
- 심층 학습 모델 적용: 텍스트 분류 및 유사도 측정에 특화된 심층 학습(Deep Learning) 모델을 도입하여 더 복잡한 패턴을 학습하고 예측 정확도를 높입니다.
사용 방법
-
main_similarity_checker.py명령줄에서 다음 형식으로 스크립트를 실행합니다:
python main_similarity_checker.py [원본_파일_경로] [표절_의심_파일_경로] [결과_출력_파일_경로]실행 후, 유사도 결과는 지정된
결과_출력_파일_경로에 저장됩니다. -
similarity_checker_tests.py단위 테스트를 실행하려면
DocumentAnalyzer클래스가 포함된 모듈이 올바르게 임포트되었는지 확인한 후, 다음 명령을 사용합니다:python similarity_checker_tests.py이 명령은 정의된 모든 단위 테스트를 실행하고 결과를 표시합니다.