Wizley

단어 vs 문장: 키워드 알림 시스템 설계 시행착오

노티노티 서비스를 운영한 지도 벌써 4년 차가 되었다. 그간 여러 기능을 넣고, 빼고, 다시 다듬으면서 서비스 유지에 힘이 든다는 것을 느껴 유지보수 리소스를 최소화 하고자 스스로에게 질문을 던졌다.

이 서비스의 핵심 가치는 무엇인가?

재학 시절에 내게 필요한 공지사항이 언제쯤 올라올지, 올라왔는지 매번 확인하느라 불편함을 겪었다. 이건 비단 나뿐만이 아니라 재학생 모두가 겪고 있는 상황일 거라 판단했다. 그래서 이 키워드 알림 기능을 앞세워 서비스를 만들었고, 이제는 이 기능을 개선하고자 노력하고 있다.

단어 하나로 문장을 이해할 수 있을까?

내가 만들고자 하는 알림 시스템에는 제약사항이 존재한다. 유저는 반드시 수신하고자 하는 내용의 키워드를 입력하는 것으로 끝이어야 하고, 그 외에는 어떠한 input을 받지 않기로 한다.

“장학금”, “수강신청”, “졸업”

위 키워드와 관련된 공지사항을 찾아 알림을 보내야 한다. 겉으로 보기에는 크게 어렵지 않은 문제였다.

그 단어가 제목에 포함되어 있으면 알림을 보내면 되지 않을까?

그렇게 첫 번째 버전을 만들게 되었다.

1. 제목에 키워드가 들어가면 알림을 보내자

로직은 단순했다.

if (title.includes(keyword)) {
  sendNotification();
}

빠르고, 명확하고, 설명 가능했다. 하지만 곧 한계를 마주했다.

예를 들어 다음과 같은 공지사항 제목이 있다.

2026학년도 장학제도 개편 안내

유저가 등록한 “장학금”.

거리가 아주 가까운 공지사항이지만 겨우 “금”이라는 글자 하나 때문에 알림을 수신하지 못하는 경우가 생기게 된 것이다.

이 외에도 여러 문제가 있었다.

  • 키워드가 제목이 아닌 본문에만 등장하는 경우
  • 유사 표현 (채용 vs 인턴)

관련이 있는데 알림이 안 가는 문제가 다수 발생했고, 개선할 필요성을 느꼈다.

그래서 두 번째 시도를 했다.

2. 의미를 이해하게 만들자

욕심이 생겼다.

단어 매칭이 아니라, 의미를 이해하게 만들면 되지 않을까?

조금은 사람처럼 문맥을 이해하고, 판단할 수 있으면 좋겠다는 생각이 들었다.

공지사항, 키워드를 전부 벡터화 하고, Vector Search를 통해 유사성을 검색해 알림을 보내기로 한다.

키워드와 공지의 문맥을 비교하기 위해서는 제목 뿐만 아니라 본문의 내용까지 필요했다.

  1. 공지사항의 본문에는 이미지만 있는 경우도 다수 존재
  2. 공지사항 본문에는 너무나도 세부적인 내용이 많아 텍스트가 embedding 모델에 곧바로 들어가기 어려움

따라서 공지사항을 llm으로 요약하는 전처리 과정을 추가로 넣어 embedding에 적합하게 구성했다.

// 전처리
const res = await client.responses.create({
  model: "gpt-4o",
  temperature: 0,
  input: [...],
});
const text = res.output_text;

// 임베딩
const embedding = await client.embeddings.create({
  model: "text-embedding-3-small",
  input: text,
});

pinecone이라는 외부 Serverless 벡터 DB에 저장해 검색을 하고, 유사도 점수가 일정 threshold 이상이면 알림을 전송했다.

기대했던 건 다음과 같다.

  • 채용에 관한 글 vs 인턴 키워드에 대한 매칭 (유사 표현 매칭)
  • 장학제도에 관한 글 vs 장학금 키워드에 대한 매칭 (유사 토픽 매칭)

벡터 검색의 본질은 고차원 벡터 공간에서 가까운 표현을 찾는 것이다.

등록한 키워드가 “졸업”이라고 했을 때

  1. [채용] 교육혁신처 학사운영팀 행정조교 채용 공고
  2. 2025학년도 전기(2026년 2월) 졸업대상자 명단 안내
  3. 2025학년도 전기 학위수여식 안내

과연 어떤 글이 “졸업”과 매칭을 이루어 알림을 보내게 할 것인가? 이런 식의 이분법의 결정을 짓기에는 어려움이 컸다.

Embedding 공간에서는 이런 공지들이 서로 완전히 멀리 떨어지지 않는다. 대부분의 공지사항과 유저가 등록하는 키워드의 비교에서 점수대가 비슷했고, threshold를 조정하는 것만으로 알림을 보낼지 말지 선택할 수 없었다.

상대적인 유사도에 사용하기에는 적합하지만, 절대적인 기준을 제공하기는 어렵다.

3. 형태소 기반(NLP) 매칭으로 알림을 보내자

문득 새로운 아이디어가 떠올랐다.

한국어를 의미가 담긴 명사만 추출해서 유저의 키워드와 매칭하면 정확도를 매우 높일 수 있지 않을까?

다시 돌아와 다음을 비교해보자.

“2026학년도 수강과목 신청 안내” vs “수강신청”

이는 다음과 같이 명사로 분리되어 비교할 수 있다.

“학년, 수강, 과목, 신청, 안내” vs “수강, 신청”

이 방식은 벡터 검색처럼 과한 의미의 확장을 막고, 유사 토픽(장학제도 vs 장학금)에 대해 방지할 방안으로 충분했다.

from kiwipiepy import Kiwi

kiwi = Kiwi()

tokens = list(
    {token.form for token in kiwi.tokenize(text) if token.tag in ("NNG", "NNP")}
)

감사하게도 한국어 형태소 분석기 라이브러리인 Kiwi를 이용해 쉽게 구현할 수 있었다.

또한, 노이즈를 줄이기 위해 의미가 있는 [NNG: 일반 명사, NNP: 고유 명사]만을 추출해서 사용했다.

const required = Math.max(1, Math.ceil(q.kiwi["length"] * 0.4));
if (matched < required) continue;

그리고 과도한 Recall을 방지하기 위해 유저 키워드도 형태소로 분리해 40% 이상 매칭이 되면 알림을 보내게끔 조정했다.

그리고 아마 최종적으로 이 방안을 채택하지 않을까 싶다.

Trade-off

결과적으로 키워드 알림 시스템 구현을 위한 심층적인 방법을 크게 두 가지 고민했다.

  1. 벡터 검색(의미 확장을 위한 문맥 분석과 유사성 검색)
  2. 형태소 매칭(명사로 분리 및 동일 내용 매칭)

각각의 장단점을 소개하자면

Vector Search

장점

  • 유사 표현 매칭 가능
    • 채용 vs 인턴
    • 졸업 vs 학위수여식
  • 키워드가 직접적으로 등장하지 않아도 탐지 가능

단점

  • 대부분의 공지사항이 비슷한 점수대로 수렴 (threshold를 결정하는 새로운 기준 필요)
  • Embedding + Vector DB 비용 발생

가장 관련 있는 키워드 후보. 즉, 랭킹을 탐색하고자 한 것이 아니라 이 키워드가 이 공지사항에 해당되는지를 판단하는 이분법적인 문제에는 애매한 방법이지 않을까 싶다.

형태소 매칭

장점

  • 외부 비용 문제 없음
  • threshold 설계가 직관적

단점

  • 의미 확장 불가

결국에는 Trade-off 단계를 마주한 것 같다. Precision을 높이고 Recall을 줄일 것인지, Recall을 높이고 Precision을 줄일 것인지.

내 개인적인 선택은 Precision을 높이는 방향이다. 하루에 공지사항이 4~5개 등록되는 상황에 원치 않는 알림이 도착하면 그것대로 불편할 것 같다.