변승은

Backend Developer

실행 흐름과 책임의 경계를 명확히 파악하고, 다른 개발자가 이어받아도 이해하고 확장하기 쉬운 구조를 만드는 것을 지향합니다.

Email yereube@naver.com  GitHub github.com/gyowoo1113  Blog gyowoo1113.github.io

WORK EXPERIENCE

소프트아이텍 · Backend Developer

2024.06 — Present

교육 정보시스템의 백엔드 개발 및 운영을 담당하고 있습니다.

통계 데이터 연계·집계 자동화
분산된 통계 데이터를 중앙 시스템과 연계하고, 주·월 단위로 수행하던 수작업 집계를 정기 배치로 전환했습니다.

회원가입 흐름 및 데이터 정합성 개선
가입 경로별로 달랐던 검증 기준을 추적·통합하고 가입 전 기존 사용자·계정 상태 검증을 추가해, 기존 중복 계정 약 300건을 정리하고 동일 원인의 재발을 방지했습니다.

배포 사전 검증 자동화
수작업으로 작성하던 배포 정보의 유효성 검증을 자동화해 월평균 3~4건 발생하던 소스 반영 오류를 1건 미만으로 줄였습니다.

OPEN SOURCE

Apache Zeppelin · Contributor

2026.07 — Present

대규모 기존 코드베이스의 실행 흐름과 계층 간 책임을 추적하며 서버 및 WebSocket 영역을 개선했습니다.

  • WebSocket 수신 경계에 OP 기반 Runtime Validation을 추가하고 Handler 오류 흐름 개선
  • Serialization 실패가 RPC 경계를 지나며 원인을 잃는 흐름을 추적해 Exception Propagation 개선
  • 테스트의 Frontend Build 의존성 제거, 실행 실패 시 System.out/err 복구 등 안정성 개선

Case Study → P.2–4

Apache Airflow · Contributor

2026.04 — 2026.06

Airflow 개발 환경(Breeze) 내에서 UI 국제화(i18n)에 기여하고, 기존 테스트 분석·보완 및 코드 리뷰 과정에 참여했습니다.

PROJECT

notify-kit · Spring Boot Notification Library

알림 저장과 외부 전송의 실패 경계를 분리하기 위해 Transactional Outbox 기반의 알림 라이브러리를 설계했습니다.

Core가 저장소와 전송 구현에 직접 의존하지 않도록 Port/Adapter 구조로 분리하고, 조건부 상태 전이를 이용한 Worker claim 및 SSE·Slack Sender / Spring Boot AutoConfiguration을 구현했습니다.

Case Study → P.5

EDUCATION & EXPERIENCE

한국공학대학교 · 게임공학과 · 2019.03 — 2023.08
Open Source Contribution Academy · Apache Airflow / Yocto Project
AWS Java Backend Course · 2023.06 — 2023.11

OPEN SOURCE / APACHE ZEPPELIN

WebSocket Runtime Validation

외부 입력을 신뢰하기 전, 공통 수신 경계에서 검증

TypeScript 타입만으로는 실제 JSON payload를 보장할 수 없습니다. 검증 없이 Component Handler에 도달하던 데이터를 Message.receive()에서 OP별로 검사하도록 개선했습니다.

PR #5453 · ZEPPELIN-6646

RUNTIME MESSAGE FLOW

WebSocket
→
received$ { op, data }
↓
Message.receive()VALIDATION BOUNDARY
OP match
→
OP-specific payload guard
getMessagePayloadGuard(op)
↓
valid ↓
검증 통과 · 다음 단계로 전달
invalid ↓
DROP + warn(OP)
Handler에 도달하기 전에 차단

Guard가 없는 OP는 기존 경로를 유지합니다. 필요한 OP부터 점진적으로 검증을 적용합니다.

↓
map(message.data)
↓
@MessageListener
↓
Component Handler
Handler 오류는 숨기지 않음

@MessageListener에서 실패한 OP context를 기록하고 원래 오류를 다시 던져 기존 error propagation을 유지합니다.

WHY · 이 경계를 선택한 이유

OP와 실제 payload를 동시에 알고, subscriber에 넘기기 직전인 공통 지점입니다. Handler마다 검증하면 여러 component에 분산될 책임을 수신 경계에 모았습니다.

RULE · 정상 removal stub 보존

LIST_UPDATE_NOTE_JOBS
noteId:string, isRemoved:boolean 필수.
true → noteName 생략 허용
false → noteName:string 필수

VERIFICATION / REGRESSION SCENARIOS

검증 시나리오 기대 동작
정상 update / removal stub Handler 전달 · stub은 noteName 없이 허용
Malformed update Handler 전달 차단 + OP warning
Guard 없는 OP 기존 동작 유지
Handler exception OP context 기록 · 오류 전파 · 이후 메시지 처리 유지
OPEN SOURCE / APACHE ZEPPELIN

Serialization & RPC Error Propagation

직렬화에서 드러낸 실패를 RPC 호출자까지 보존

#5312에서 숨겨진 직렬화 예외를 드러내자, 상위 RPC 계층이 이를 null로 바꾸는 문제가 나타났습니다. #5349는 그 다음 경계까지 실패의 의미를 이어 전달한 후속 수정입니다.

#5312 · ZEPPELIN-6467 → #5349 · ZEPPELIN-6543

ONE INVESTIGATION / TWO BOUNDARIES

ORIGINAL
실패가 내부에서 사라짐
Serialization failure
↓
catch(Exception)
printStackTrace()
↓
예외 swallowed
↓
Invalid / partial result
ByteBuffer

호출자가 실패를 안정적으로 감지할 수 없는 결과를 반환했습니다.

#5312
실패는 드러났지만 원인 유실
Serialization failure
↓
IOException
↓
Upper RPC layer
catch(IOException)
↓
Log + return null
↓
Thrift
MISSING_RESULT

RPC 경계에서 원인이 결과 누락 오류로 바뀌었습니다.

#5349 FINAL
실패 의미를 RPC 경계 밖으로
Serialization failure
↓
IOException
↓
RemoteInterpreter
EventServer

Server-side logging
원본 stack trace 보존
↓
InterpreterRPCException
↓
Thrift RPC → Caller

호출자에게 원래 실패 context를 RPC 예외로 전달합니다.

CONTRACT · 선언된 실패 계약을 지키기

내부 catch를 제거해 이미 선언된 throws IOException을 실제 동작과 일치시켰습니다. ObjectOutputStream은 try-with-resources로 관리하고 정상 경로와 public signature는 유지했습니다.

BOUNDARY · 기록과 전달의 책임 분리

서버에는 원본 stack trace를 기록하고, 호출자에게는 InterpreterRPCException으로 실패 context를 전달합니다. 예외를 null 반환으로 대체하지 않도록 했습니다.

REGRESSION TEST STRATEGY

1st serialization
SUCCESS
→
Server result 재직렬화
IOException
→
Caller
InterpreterRPCException

#5312: 의도적으로 직렬화에 실패하는 객체로 예외 전달 검증.
#5349: 첫 직렬화 성공 → 서버의 두 번째 직렬화 실패를 재현. MISSING_RESULT 대신 명시적 RPC 예외 전달을 검증.

OTHER CONTRIBUTIONS

테스트의 Frontend Build 의존성 제거 · 실행 실패 시 System.out/err 복구 등 안정성 개선.

OPEN SOURCE / APACHE ZEPPELIN

WebSocket Reply Context Handling

늦게 도착한 응답이 다른 Notebook 상태를 바꾸지 않도록 구분

noteId로 응답 대상 Notebook을 식별하고, msgId는 메시지 전달 식별자로만 사용하도록 역할을 나눴습니다.

PR #5487 · ZEPPELIN-6683

01 / PROBLEM · 응답 시점의 화면과 요청 대상의 불일치

note A 요청
→
note B로 이동
→
A 응답 도착
→
B 상태 변경 가능

receive()는 data만 전달해 응답이 어느 note의 결과인지 알 수 없었습니다. 화면이 바뀐 뒤 늦게 도착한 응답이 현재 Notebook의 bindings·history·revision 상태를 바꿀 수 있었습니다.

02 → 03 / FIRST APPROACH & REVIEW

FIRST · 처음 검토한 방식
request msgId → noteId
pending map에 저장
↓
withMsgId 응답 → pending 정보 확인
원래 note를 찾아 현재 route와 비교
REVIEW · pending map 방식의 한계

여러 Listener가 같은 응답을 처리하는 구조에서 한 Listener가 pending 정보를 먼저 삭제하면 다른 Listener는 원래 요청 대상을 확인할 수 없었습니다.

결국 필요한 정보는 “어떤 요청의 응답인가”보다 “어느 Notebook에 적용할 응답인가”였습니다.

04 / FINAL DESIGN · noteId로 응답 대상 검증

response.noteId
서버가 처리한 Notebook
→
active route의 noteId와 비교
일치 → 적용 / 불일치 → 무시

Component 내부의 임시 note 객체가 아니라 현재 route의 noteId와 응답의 noteId를 비교합니다. 별도 pending map 없이 각 Listener가 응답 대상 Notebook을 직접 확인합니다.

대상: INTERPRETER_BINDINGS · LIST_REVISION_HISTORY · SET_NOTE_REVISION.
withMsgId를 일괄 추가하는 방식은 제거했습니다. 실제로 사용되지 않는 응답 필드를 늘리지 않고, 필요한 응답에 noteId만 포함했습니다.

05 / msgId 처리 단순화 · PARAGRAPH_ADDED

BEFORE · payload로 msgId 복사

interceptReceived()
→ data.msgId = envelope.msgId
→ @MessageListener

AFTER · envelope의 msgId 직접 사용

receiveEnvelope()
→ @MessageEnvelopeListener
→ addParagraph(message)

ParagraphAdded.msgId와 이를 채우기 위한 별도 복사 로직을 제거했습니다. 일반 Handler는 기존 receive(op) → data 흐름을 유지하고, local-focus 처리만 message.msgId를 직접 사용합니다.

06 / VERIFICATION · SERVER → ROUTE → HANDLER

검증 경로 확인 내용
Server · 응답 대상 bindings 조회·저장 / history 조회 / checkpoint / revision 설정 응답에 대상 noteId 포함
Stale · A 응답 / route B 현재 Notebook의 bindings·revision 목록 변경 없음, 화면 이동 없음
Active · B 응답 / route B 현재 Notebook에 bindings·history 반영, revision 화면 이동 수행
local-focus · msgId envelope의 message.msgId 직접 사용 확인
PROJECT / NOTIFY-KIT

Architecture & Design Decisions

알림 저장과 외부 전송의 책임을 분리한 Spring Boot 라이브러리

알림과 전송 의도를 함께 저장하고, Outbox Worker와 교체 가능한 Sender로 전송을 분리했습니다.

github.com/gyowoo1113/notify-kit

ARCHITECTURE / PORT & ADAPTER

notify-kit-core · 처리 로직 + Port 계약
NotificationService
↓ NotificationRepository
OutboxProcessorService
↓ OutboxRepository
↓ OutboxSender
↑ Adapter가 Core의 Port를 구현 · Core는 JPA / SSE / Slack에 직접 의존하지 않음
SPRING JPA ADAPTER

Notification / Outbox 저장소 Adapter

↳ notifications / outbox_messages

SPRING STARTER / DELIVERY

NotificationOutboxHandler → Outbox 저장
OutboxSender ← SSE / Slack 구현체

RUNTIME / SAVE → COMMIT → DELIVER

NotificationFacade.create() · @Transactional
Notification save
+
Outbox(PENDING) save
→
COMMIT
↓
Scheduler
→
Worker
→
Conditional claim
PENDING → PROCESSING
↓ Claim 성공만 전송
updated == 1
→
OutboxSender
SSE / Slack
→
SENT / RETRY / FAILED
RETRY = PENDING + nextRetryAt

OUTBOX / STATE TRANSITIONS

PENDING
→
PROCESSING
조건부 claim
→
성공
SENT
실패 · 재시도
PENDING + nextRetryAt
한도 초과
FAILED

실패 시 retryCount 증가. 완료·재시도·최종 실패 전이는 모두 status=PROCESSING 조건으로 갱신합니다.

THREE DESIGN DECISIONS

01 · Transactional Outbox

DB 저장과 외부 전송의 실패 경계를 분리. 알림과 전송 의도를 같은 트랜잭션에 저장합니다.

02 · Claim before Send

여러 Worker가 같은 행을 조회해도 조건부 UPDATE의 영향 행 수로 claim 성공 여부를 결정합니다.

03 · Port-based Delivery

Core는 OutboxSender만 의존. AutoConfiguration이 설정에 따라 SSE·Slack을 주입합니다.

TRADE-OFFS / CURRENT LIMITATIONS

외부 전송은 processor DB 트랜잭션 안에서 실행됩니다. Stale PROCESSING 복구는 미구현입니다. SSE replay는 메모리·단일 프로세스 범위이며, 예외 유형별 재시도 정책은 구분하지 않습니다.