
격리 수준
라티, DB 공부의 꽃이자 면접 단골 질문인 **트랜잭션 격리 수준(Isolation Level)**이군요. 인프라 삽질하느라 고생했는데, 이런 이론을 잘 다져두면 나중에 데이터가 꼬이는 대참사를 막을 수 있습니다.
학부 수준에서 면접관의 머릿속을 시원하게 해줄 핵심 내용을 냉철하게 정리해 드립니다.
1. 격리 수준이란? (The Concept)
격리 수준은 **“여러 트랜잭션이 동시에 실행될 때, 서로의 작업 내용을 얼마나 공유하거나 차단할 것인지”**를 결정하는 설정입니다. 격리 수준이 높을수록 데이터 정합성은 완벽해지지만, 동시 처리 성능(Concurrency)은 떨어지는 트레이드오프(Trade-off) 관계에 있습니다.
2. 격리 수준에 따라 발생하는 3대 재앙 (Anomalies)
격리 수준을 배우기 전에, 우리가 막아야 할 ‘나쁜 상황’들부터 알아야 합니다.
-
Dirty Read: 아직 커밋되지 않은 다른 트랜잭션의 데이터를 읽는 현상. (남이 쓰고 있는 메모를 훔쳐보는 것)
-
Non-repeatable Read: 한 트랜잭션 내에서 같은 쿼리를 두 번 보냈는데 값이 달라지는 현상. (그 사이 남이 데이터를 수정/삭제함)
-
Phantom Read: 한 트랜잭션 내에서 같은 조건으로 조회했는데, 결과 레코드 수가 달라지는 현상. (그 사이 남이 새로운 데이터를 삽입함)
3. MySQL(InnoDB)의 4단계 격리 수준
① READ UNCOMMITTED (1단계)
-
특징: 커밋되지 않은 데이터도 읽을 수 있음.
-
문제점: Dirty Read, Non-repeatable Read, Phantom Read 모두 발생.
-
냉철한 평: 정합성이 엉망이라 실무에서는 거의 안 씁니다.
② READ COMMITTED (2단계)
-
특징: 커밋된 데이터만 읽을 수 있음. 오라클(Oracle)의 기본 설정.
-
문제점: Non-repeatable Read, Phantom Read 발생.
-
작동 방식: 조회 시점의 스냅샷을 사용하지만, 쿼리가 실행될 때마다 최신 커밋본을 확인합니다.
③ REPEATABLE READ (3단계 - MySQL Default)
-
특징: 한 트랜잭션 내에서는 똑같은 결과를 보장함. MySQL InnoDB의 기본 격리 수준.
-
문제점: 이론적으로는 Phantom Read 발생 가능.
-
MySQL의 반전: InnoDB는 **MVCC(Multi-Version Concurrency Control)**와 Next-Key Lock 덕분에 이 단계에서도 Phantom Read가 거의 발생하지 않습니다. (학부 수준에서 이거 언급하면 가산점입니다.)
④ SERIALIZABLE (4단계)
-
특징: 가장 엄격함. 읽기 작업에도 공유 잠금(Shared Lock)을 걸어버림.
-
문제점: 동시성이 최악입니다. 아무도 데이터를 수정하지 못하고 줄 서야 합니다.
-
냉철한 평: 성능이 너무 구려서 특수한 상황 아니면 안 씁니다.
4. 한눈에 보는 요약표
| 격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | 발생 | 발생 | 발생 |
| READ COMMITTED | - | 발생 | 발생 |
| REPEATABLE READ | - | - | 발생 (MySQL은 방어) |
| SERIALIZABLE | - | - | - |
💡 라티를 위한 면접 꿀팁 (MVCC)
MySQL이 왜 REPEATABLE READ를 기본으로 쓰는지 물어본다면 이렇게 답하세요.
“InnoDB는 MVCC 기술을 통해 언두 로그(Undo Log)에 변경 전 데이터를 보관합니다. 덕분에 락을 걸지 않고도 읽기 일관성을 유지할 수 있어 성능과 정합성 두 마리 토끼를 다 잡았기 때문입니다.”
라티, 지금 직접 설치한 MySQL에서 SELECT @@TRANSACTION_ISOLATION; 쳐서 현재 설정 확인해 보세요. 혹시 MVCC가 실제로 어떻게 작동하는지(Undo 로그 등) 더 깊게 파보고 싶으신가요?
참고: 제공된 공고문이나 API 문서에는 DB 격리 수준에 대한 직접적인 언급은 없으나, 일반적인 백엔드 개발 지식을 바탕으로 작성되었습니다.
라티, 알림을 클릭했을 때 앱 내의 특정 페이지로 이동하게 하려면, 백엔드에서 보내는 FCM 페이로드의 data 영역에 이동할 경로 정보를 실어 보내고, 플러터(Flutter) 앱에서 이 값을 읽어 GoRouter로 연결해 주어야 합니다.
냉철하게 핵심 구현 단계를 3단계로 정리해 드립니다.
1. 백엔드: FCM 페이로드에 path 추가
현재 API 문서의 알림 발송 DTO(POST /api/notification/fcm/send 등)를 수정하여, 알림 클릭 시 이동할 앱 내 주소를 data 필드에 포함해야 합니다.
-
친구 요청 알림 시 예시 페이로드:
JSON
{ "to": "FCM_TOKEN", "notification": { "title": "친구 요청", "body": "라티님이 친구 요청을 보냈습니다." }, "data": { "path": "/contact/requests", // GoRouter에 정의된 경로 "type": "FRIEND_REQUEST" } }
2. 플러터: 알림 클릭 리스너 설정
앱이 백그라운드나 종료된 상태에서 알림을 클릭했을 때 data에 담긴 path를 읽어서 이동시키는 로직이 필요합니다. 보통 main.dart나 별도의 NotificationService에서 처리합니다.
Dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:iamhere/core/router/app_routes.dart';
void setupNotificationHandler(BuildContext context) {
// 앱이 백그라운드 상태일 때 알림을 클릭해 앱을 연 경우
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_handleNavigation(message);
});
// 앱이 완전히 종료된 상태에서 알림을 클릭해 앱을 연 경우
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) {
if (message != null) {
_handleNavigation(message);
}
});
}
void _handleNavigation(RemoteMessage message) {
final path = message.data['path'];
if (path != null) {
// GoRouter를 사용하여 해당 경로로 이동
// static으로 관리되는 router 인스턴스를 사용하거나 context를 활용
appRouter.push(path);
}
}
3. 주소 매핑 가이드 (라티의 코드 기반)
제공해주신 appRoutes 구조에 맞춰 백엔드에서 보내야 할 path 값들을 정리했습니다.
| 상황 | 이동할 페이지 | 백엔드 path 전달 값 |
|---|---|---|
| 친구 요청 도착 | 받은 친구 요청 목록 | /contact/requests |
| 도착 알림 수신 | 알림 내역 리스트 | /record/notifications |
| 누군가 내 친구 수락 | 내 친구 목록 | /contact |
| 약관 변경 공지 | 특정 약관 상세 | /terms-detail/5 (ID 포함) |
| 도착 완료(나에게) | 전송 이력 리스트 | /record/send-history |
💡 냉철한 추가 조언
-
파라미터 처리: 약관 상세(
/terms-detail/:termId)처럼 변수가 들어가는 경로는 백엔드에서path를 보낼 때/terms-detail/4와 같이 완성된 주소를 보내야 합니다. -
ShellRoute 주의: 현재
DefaultView를 사용하는ShellRoute안에 대부분의 페이지가 있습니다.appRouter.push(path)를 사용하면 하단 탭바가 유지된 채로 이동하겠지만, 만약 탭바를 가리고 싶다면rootNavigatorKey설정을 검토해야 합니다. -
인증 체크: 알림을 클릭해 이동할 때, 로그인이 안 되어 있다면
AppRoutes.auth로 리다이렉트시키는redirect로직이GoRouter설정에 반드시 포함되어 있어야 합니다. (안 그러면 에러 납니다.)
지금 백엔드 DTO에 path 필드 추가 가능한 상태인가요? 아니면 앱에서 type 값만 보고 분기 처리하실 건가요? 주소를 직접 보내는 게 훨씬 유연합니다.