격리 수준

라티, DB 공부의 꽃이자 면접 단골 질문인 **트랜잭션 격리 수준(Isolation Level)**이군요. 인프라 삽질하느라 고생했는데, 이런 이론을 잘 다져두면 나중에 데이터가 꼬이는 대참사를 막을 수 있습니다.

학부 수준에서 면접관의 머릿속을 시원하게 해줄 핵심 내용을 냉철하게 정리해 드립니다.


1. 격리 수준이란? (The Concept)

격리 수준은 **“여러 트랜잭션이 동시에 실행될 때, 서로의 작업 내용을 얼마나 공유하거나 차단할 것인지”**를 결정하는 설정입니다. 격리 수준이 높을수록 데이터 정합성은 완벽해지지만, 동시 처리 성능(Concurrency)은 떨어지는 트레이드오프(Trade-off) 관계에 있습니다.


2. 격리 수준에 따라 발생하는 3대 재앙 (Anomalies)

격리 수준을 배우기 전에, 우리가 막아야 할 ‘나쁜 상황’들부터 알아야 합니다.

  1. Dirty Read: 아직 커밋되지 않은 다른 트랜잭션의 데이터를 읽는 현상. (남이 쓰고 있는 메모를 훔쳐보는 것)

  2. Non-repeatable Read: 한 트랜잭션 내에서 같은 쿼리를 두 번 보냈는데 값이 달라지는 현상. (그 사이 남이 데이터를 수정/삭제함)

  3. 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 ReadNon-repeatable ReadPhantom 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

💡 냉철한 추가 조언

  1. 파라미터 처리: 약관 상세(/terms-detail/:termId)처럼 변수가 들어가는 경로는 백엔드에서 path를 보낼 때 /terms-detail/4와 같이 완성된 주소를 보내야 합니다.

  2. ShellRoute 주의: 현재 DefaultView를 사용하는 ShellRoute 안에 대부분의 페이지가 있습니다. appRouter.push(path)를 사용하면 하단 탭바가 유지된 채로 이동하겠지만, 만약 탭바를 가리고 싶다면 rootNavigatorKey 설정을 검토해야 합니다.

  3. 인증 체크: 알림을 클릭해 이동할 때, 로그인이 안 되어 있다면 AppRoutes.auth로 리다이렉트시키는 redirect 로직이 GoRouter 설정에 반드시 포함되어 있어야 합니다. (안 그러면 에러 납니다.)

지금 백엔드 DTO에 path 필드 추가 가능한 상태인가요? 아니면 앱에서 type 값만 보고 분기 처리하실 건가요? 주소를 직접 보내는 게 훨씬 유연합니다.