[ONMU] Flutter 앱에 Sentry 도입해보기

RudinP·2026년 6월 22일

Microsoft Data School 3기

목록 보기
69/69

모바일 앱을 개발하다 보면 로컬 테스트나 QA에서는 보이지 않던 문제가 실제 사용자 환경에서 발생한다.
특히 Flutter 앱은 iOS, Android, OS 버전, 앱 버전, 네트워크 상태, 로그인 세션 상태에 따라 오류 양상이 달라진다.

문제는 이런 오류가 사용자 제보만으로는 추적하기 어렵다는 점이다.
“앱이 튕겼어요”, “버튼을 눌렀는데 안 돼요”라는 제보만으로는 어떤 API가 실패했는지, 어떤 앱 버전에서 발생했는지, 서버 장애인지 사용자 입력 문제인지 판단하기 어렵다.

그래서 ONMU Flutter 앱에 Sentry를 도입하기로 했다.
다만 이번 작업의 목표는 단순히 Sentry SDK를 붙이는 것이 아니었다. 앱에서 발생하는 오류를 분류하고, 사용자에게 보여줄 오류와 운영자가 추적해야 할 오류를 분리하는 것이 핵심이었다.
(참고로 발생했다고 바로 보고되는건 아니고, 약간의 지연시간(분 단위) 는 존재한다.)

이메일로도 옴!

이런식으로 프론트엔드 디버깅에서 중요한 내용까지 태그로 설정하여 파악 가능하다.

Sentry를 도입한 이유

Sentry는 앱에서 발생한 오류를 수집하고, 오류가 발생한 환경과 흐름을 추적할 수 있게 해주는 observability 도구다.

Sentry를 통해 다음과 같은 정보를 확인할 수 있다.

항목확인 가능한 내용
PlatformiOS, Android, Web 등 오류가 발생한 플랫폼
Environmentlocal, dev, staging, production
App version어떤 앱 버전에서 발생했는지
Release특정 배포 이후 오류가 증가했는지
Device / OS기기 모델, OS 버전
Stack trace오류가 발생한 코드 위치
Frequency같은 오류가 얼마나 자주 발생하는지
User impact몇 명의 사용자에게 영향을 줬는지
Breadcrumb오류 직전의 화면 이동, API 요청 등 흐름

즉 Sentry는 단순히 “앱이 죽었다”를 알려주는 도구가 아니다.
어떤 배포 이후, 어떤 환경에서, 어떤 기능을 사용하다가 문제가 발생했는지 확인할 수 있게 해준다.

하지만 Sentry를 제대로 사용하려면 먼저 앱 내부의 오류 처리 기준이 정리되어 있어야 한다.

기존 문제: 모든 API 오류가 비슷하게 보였다

ONMU Flutter 앱은 Spring API와 통신하기 위해 Dio를 사용하고 있다. Dio는 Flutter/Dart에서 많이 쓰이는 HTTP client 라이브러리로, API 요청 실패 시 DioException을 던진다.

예를 들어 다음 상황은 모두 DioException으로 전달될 수 있다.

상황실제 의미
400사용자의 입력값이 잘못됨
401로그인 세션이 없거나 만료됨
403권한이 없음
404리소스가 존재하지 않음
409이미 처리된 요청 또는 중복 요청
500서버 내부 오류
503서버 일시 장애 또는 배포 중
timeout네트워크 지연 또는 서버 응답 지연

문제는 이 오류들을 그대로 Sentry에 보내면 운영 신호가 흐려진다는 것이다.

예를 들어 사용자가 빈 검색어를 입력한 것, 이미 친구인 사용자를 다시 추가한 것, 로그인 전 /users/me 요청이 401을 반환한 것은 장애가 아니다.
반면 500, 503, 서버 응답 형식 불일치, 알 수 없는 예외는 반드시 확인해야 하는 문제다.

따라서 Sentry를 붙이기 전에 먼저 오류를 ONMU 앱 기준으로 다시 분류해야 했다.

ONMU 오류 Taxonomy 설계

공통 오류 타입으로 OnmuException을 만들고, 오류 종류를 OnmuErrorKind로 분류했다.

Error Kind의미
validation입력값 오류
unauthorized인증 필요, 세션 만료
forbidden권한 없음
notFound리소스 없음
conflict중복 요청, 이미 처리된 요청
rateLimited요청 과다
server서버 내부 오류
unavailable서버 일시 사용 불가
timeout요청 시간 초과
network네트워크 연결 문제
contractMismatch서버 응답 형식이 앱 계약과 다름
externalProviderOAuth, 지도, 외부 API 오류
permissionDenied기기 권한 거절
cancelled사용자가 취소한 동작
backgroundSyncpush token, read sync 등 백그라운드 실패
unknown분류되지 않은 오류

OnmuException에는 사용자 메시지와 개발자용 메시지를 분리해서 담았다.

class OnmuException implements Exception {
  final OnmuErrorKind kind;
  final String userMessage;
  final String technicalMessage;
  final int? statusCode;
  final String? method;
  final String? endpoint;
  final String feature;
  final bool retryable;
  final bool reportable;
  final Object? cause;
}

여기서 중요한 필드는 retryablereportable이다.

retryable은 사용자가 다시 시도할 수 있는 오류인지 나타낸다.
reportable은 Sentry에 보고할 가치가 있는 오류인지 나타낸다.

이렇게 분리하면 같은 API 실패라도 사용자 경험과 운영 보고를 다르게 처리할 수 있다.

DioException을 OnmuApiException으로 정규화

ViewModel이 DioException을 직접 다루지 않도록 OnmuApiClient에서 API 오류를 정규화했다.

흐름은 다음과 같다.

DioException
→ OnmuApiException
→ OnmuErrorKind 매핑
→ UI 상태 변환
→ Sentry 보고 여부 판단

예를 들어 503은 다음처럼 해석된다.

HTTP 503
→ kind: unavailable
→ retryable: true
→ reportable: true
→ 사용자에게 재시도 UI 표시
→ Sentry 보고

반대로 친구 추가 중 이미 친구인 경우의 409는 장애로 보지 않는다.

HTTP 409
→ kind: conflict
→ userMessage: 이미 친구예요.
→ retryable: false
→ reportable: false
→ Sentry 보고 제외

이 구조를 통해 ViewModel은 HTTP status code에 직접 의존하지 않고, 앱이 이해할 수 있는 오류 타입만 다루게 된다.

HTTP 오류 처리 정책

HTTP status와 네트워크 오류는 다음 기준으로 매핑했다.

HTTP / 상황ONMU Error Kind사용자 처리Sentry 보고
400, 422validationform error, snackbar보고 안 함
401unauthorized로그인 이동, 세션 만료 처리정상 bootstrap에서는 보고 안 함
403forbidden권한 없음 안내예상 가능한 권한 오류는 보고 안 함
404notFound없음/삭제됨 안내보고 안 함
409conflict이미 처리됨, 중복 안내보고 안 함
429rateLimited잠시 후 재시도샘플링
500server재시도 UI보고
502, 503, 504unavailable재시도 UI보고
timeouttimeout네트워크/재시도 안내샘플링
offline/socketnetwork네트워크 안내낮은 샘플링
JSON 필수값 누락contractMismatchfallback 또는 오류 UI반드시 보고
알 수 없는 예외unknown일반 오류 UI보고

이 표를 기준으로 “사용자가 해결할 수 있는 오류”와 “운영자가 확인해야 하는 오류”를 분리했다.

Sentry Report Policy

Sentry에 보고하는 오류는 다음으로 제한했다.

보고 대상이유
server서버 내부 장애 가능성
unavailable배포, 인프라 장애 가능성
contractMismatchFlutter와 Spring API 계약 불일치
unknown분류되지 않은 예외
OAuth 설정 오류client id, redirect URI, callback parsing 문제
push token 등록 실패화면을 막지는 않지만 운영 추적 필요
지도 tile manifest 오류지도 기능 저하 가능성
반복적인 realtime reconnect 실패채팅/실시간 기능 품질 문제

보고하지 않는 오류도 명확히 정했다.

제외 대상이유
400, 422사용자 입력 오류
정상적인 401로그인 전/세션 없음은 정상 흐름
403예상 가능한 권한 제한
404삭제된 리소스 접근 가능성
409이미 처리된 요청, 중복 요청
이미지 선택 취소사용자 액션
권한 거절사용자 선택
빈 검색어, 폼 미완성클라이언트 검증 대상
optimistic update rollback 자체실패 처리 흐름의 일부

이 정책을 통해 Sentry가 사용자 행동에서 발생한 정상적인 실패로 오염되지 않도록 했다.

Sampling Rate 정책

모든 오류를 같은 비율로 Sentry에 보내지도 않았다.
오류 종류에 따라 중요도와 발생 빈도가 다르기 때문이다.

Sampling rate는 0.0 ~ 1.0 범위로 관리했다.

의미
0.0전혀 보고하지 않음
0.1약 10%만 보고
0.5약 50% 보고
1.0전부 보고

ONMU에서는 다음과 같은 기준을 잡았다.

Error KindSampling Rate이유
server1.0서버 내부 장애는 반드시 추적해야 함
unavailable1.0배포/인프라 장애 가능성이 있어 전부 보고
contractMismatch1.0Flutter와 Spring API 계약 불일치는 즉시 확인 필요
unknown1.0분류되지 않은 오류는 원인 파악 필요
rateLimited0.3반복 발생 가능성이 있어 일부만 수집
timeout0.2사용자 네트워크 영향이 커서 샘플링
network0.1오프라인/불안정 네트워크 노이즈를 줄이기 위함
validation0.0사용자 입력 오류는 보고하지 않음
unauthorized0.0정상적인 세션 없음 흐름은 보고하지 않음
conflict0.0중복 요청/이미 처리됨은 장애가 아님

또한 sampling rate 값은 항상 0.0 ~ 1.0 범위로 clamp되도록 했다.

final safeRate = samplingRate.clamp(0.0, 1.0);

설정값이 잘못 들어오더라도 Sentry 보고량이 의도치 않게 폭증하거나 완전히 깨지지 않게 하기 위해서다.

입력값실제 적용값
-0.50.0
0.20.2
1.71.0

이를 통해 반드시 봐야 하는 오류는 놓치지 않으면서, 모바일 환경에서 흔한 네트워크성 오류가 Sentry issue를 과도하게 만들지 않도록 했다.

개인정보와 민감 정보 보호

Sentry는 오류 분석에 유용하지만, 잘못 사용하면 민감 정보가 외부 도구에 남을 수 있다.
그래서 Sentry tag에 넣을 수 있는 값도 제한했다.

허용한 값은 다음과 같다.

Tag예시
featureauth, group, plan, chat
kindserver, validation, contractMismatch
statusCode500, 503
methodGET, POST
endpoint_template/api/v1/groups/{groupId}
retryabletrue, false
environmentstaging, production

반대로 다음 값은 Sentry에 보내지 않도록 했다.

금지 항목이유
JWT, Authorization header인증 정보
request body사용자 입력 포함 가능
response body개인정보 포함 가능
nickname, email개인 식별 정보
채팅 내용사적 대화
메모, 기록 내용사용자 생성 콘텐츠
실제 위치 상세값민감 정보 가능성

즉 Sentry에는 “어디서 어떤 종류의 오류가 났는지”만 남기고, “사용자가 무엇을 입력했는지”는 남기지 않는 방향으로 설계했다.

ViewModel 상태 변환

Sentry 보고와 사용자 UI 처리는 분리했다.

사용자에게 오류를 어떻게 보여줄지는 OnmuUiError로 변환해서 처리했다.

enum OnmuErrorPresentation {
  silent,
  snackbar,
  inline,
  fullScreen,
  retryableCard,
}

상황별 UI 처리는 다음과 같이 나눴다.

상황UI 처리
초기 화면 로딩 실패full screen 또는 retryable card
저장, 삭제, 좋아요 등 mutation 실패기존 화면 유지 + snackbar
optimistic update 실패rollback + snackbar
push token, read sync 실패silent
contract mismatchfallback 가능하면 fallback + Sentry 보고
unauthorized로그인 흐름으로 전환

예를 들어 채팅 메시지 전송 실패는 앱 전체 오류 화면으로 보내지 않는다.

pending message
→ failed message로 변경
→ “메시지를 보내지 못했어요.” 표시
→ reportable 오류면 Sentry 보고

반대로 push token 등록 실패는 사용자 화면에 직접 보여줄 필요가 없다.

push token 등록 실패
→ 화면 영향 없음
→ backgroundSync 오류로 기록
→ reportable이면 Sentry 보고

이렇게 하면 사용자 경험을 유지하면서도 운영 관점에서 필요한 오류는 추적할 수 있다.

적용 구조

이번 작업에서 구성한 구조는 다음과 같다.

단계작업
1OnmuErrorKind, OnmuException 정의
2OnmuApiException으로 DioException 정규화
3OnmuContractException으로 JSON contract mismatch 처리
4OnmuReportPolicy로 Sentry 보고 여부 분리
5sampling rate 정책 추가
6OnmuUiError로 ViewModel UI 상태 변환
7OnmuErrorReporter 래퍼 추가
8추후 Sentry SDK 연결 시 reporter 내부 구현만 교체 가능하게 설계

전체 흐름은 다음과 같다.

DioException
  ↓
OnmuApiException
  ↓
OnmuReportPolicy
  ↓
Sampling Rate
  ↓
OnmuErrorReporter
  ↓
Sentry

UI 처리는 별도 흐름으로 분리했다.

OnmuException
  ↓
OnmuUiError
  ↓
snackbar / fullScreen / retryableCard / silent

테스트 기준

오류 정책은 테스트로 고정했다.

테스트기대값
DioException(statusCode: 400)validation, reportable=false
DioException(statusCode: 503)unavailable, retryable=true, reportable=true
receive timeouttimeout, retryable=true
필수 JSON 필드 누락contractMismatch, reportable=true
친구 추가 409“이미 친구예요.”, reportable=false
채팅 전송 실패pending message가 failed로 변경
push token 등록 실패사용자 화면 영향 없음, report policy 적용

테스트를 통해 단순히 예외 클래스를 추가하는 데서 끝내지 않고, 실제 ViewModel 상태와 운영 보고 정책이 의도대로 동작하는지 확인했다.

결론

이번 Sentry 도입에서 가장 중요했던 점은 SDK 설치가 아니었다.
핵심은 앱에서 발생하는 오류를 운영 가능한 신호로 바꾸는 것이었다.

기존처럼 DioException을 그대로 다루면 사용자 입력 오류, 인증 만료, 중복 요청, 서버 장애, API contract mismatch가 모두 비슷한 실패처럼 보인다. 그렇게 되면 Sentry를 붙여도 실제로 대응해야 할 문제를 찾기 어렵다.

그래서 ONMU에서는 오류를 먼저 앱 기준의 taxonomy로 분류하고, 사용자에게 보여줄 오류와 Sentry에 보고할 오류를 분리했다. 여기에 sampling rate와 민감 정보 필터링 정책을 더해, 중요한 오류는 놓치지 않으면서도 노이즈와 개인정보 노출 위험을 줄였다.

결과적으로 Sentry는 단순한 crash reporting 도구가 아니라, Flutter 앱과 Spring API 사이의 계약 문제, 서버 장애, 특정 배포 이후의 회귀를 추적하는 운영 도구로 사용할 수 있게 되었다.

profile
성장하기 위한 기록

0개의 댓글