화요일

k8s OOMKilled 에러 진단 및 해결 완벽 가이드

<div style="font-family:'Noto Sans KR',sans-serif;line-height:1.8;max-width:860px;margin:0 auto">

<p style="color:#666;font-size:14px">🔍 검색 키워드: k8s OOMKilled, Kubernetes 메모리 부족, 파드 종료, 메모리 리소스 설정, 컨테이너 메모리 누수</p>

<h2>k8s OOMKilled 에러 진단 및 해결</h2>

<p>Kubernetes에서 파드가 갑자기 종료되고 재시작되는 문제는 보통 메모리 부족 때문입니다.</p>

<h3>1. 증상: 파드가 자꾸 크래시된다</h3>

<pre style="background:#1e1e1e;color:#d4d4d4;padding:16px;border-radius:6px;overflow-x:auto"><code>$ kubectl get pods

NAME              READY   STATUS    RESTARTS   AGE

my-app-abc123     0/1     CrashLoopBackOff    5      2m

Last State:  Terminated (reason:OOMKilled, exit code:137)</code></pre>

<p>exit code 137은 Linux에서 SIGKILL(9) 신호를 받았다는 뜻입니다.</p>

<h3>2. 원인</h3>

<ul>

<li>메모리 limit이 너무 낮게 설정됨</li>

<li>애플리케이션의 메모리 누수</li>

<li>노드의 물리 메모리 부족</li>

</ul>

<h3>3. 해결 방법</h3>

<h4>방법 1: 메모리 limit 증가</h4>

<pre style="background:#1e1e1e;color:#d4d4d4;padding:16px;border-radius:6px;overflow-x:auto"><code>resources:

  limits:

    memory: 512Mi

  requests:

    memory: 256Mi</code></pre>

<h4>방법 2: HPA로 자동 확장</h4>

<pre style="background:#1e1e1e;color:#d4d4d4;padding:16px;border-radius:6px;overflow-x:auto"><code>apiVersion: autoscaling/v2

kind: HorizontalPodAutoscaler

metadata:

  name: my-app-hpa

spec:

  minReplicas: 2

  maxReplicas: 10

  metrics:

  - type: Resource

    resource:

      name: memory

      target:

        type: Utilization

        averageUtilization: 70</code></pre>

<h3>정리표</h3>

<table border="1" cellpadding="10" cellspacing="0" style="width:100%;border-collapse:collapse">

<tr style="background:#f5f5f5"><th style="text-align:left">확인 항목</th><th style="text-align:left">명령어</th><th style="text-align:left">의미</th></tr>

<tr><td>파드 상태</td><td>kubectl describe pod</td><td>OOMKilled 여부</td></tr>

<tr><td>실제 메모리 사용</td><td>kubectl top pod</td><td>현재 사용량</td></tr>

<tr><td>노드 여유</td><td>kubectl top node</td><td>전체 노드 메모리 상태</td></tr>

</table>

<hr>

<p><strong>팁</strong>: requests는 평상시 사용량의 70~80%, limits는 requests의 1.5~2배로 설정하는 것이 best practice입니다.</p>

</div>

Zustand persist 직렬화 에러 완벽 해결법

🔍 검색 키워드: Zustand persist 에러, 직렬화 실패, JSON stringify, localStorage 버그, 상태 관리 디버깅

Zustand persist 직렬화 에러 해결법

증상

Zustand의 persist 미들웨어를 사용할 때 다음과 같은 에러가 발생한다:

TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
      at persist.ts:42

원인

Zustand persist는 상태를 JSON으로 직렬화하여 localStorage에 저장합니다. 순환 참조, 함수, Date/Map/Set 등 특수 객체, undefined 값이 포함되면 에러가 발생합니다.

해결 방법

방법 1: partialize로 직렬화 가능한 상태만 저장

const useStore = create(
    persist(
        (set) => ({ user: { id: 1, createdAt: new Date() }, count: 0 }),
            {
                  name: 'app-storage',
                        partialize: (state) => ({
                                user: { id: state.user.id, createdAt: state.user.createdAt.toISOString() },
                                        count: state.count,
                                              }),
                                                  }
                                                    )
                                                    );

방법 2: 버전 마이그레이션

const useStore = create(
    persist(
        (set) => ({ version: 2, theme: 'dark' }),
            { name: 'app-storage', version: 2, migrate: (s, v) => v < 2 ? { ...s, version: 2 } : s }
              )
              );

정리표

에러 상황원인해결법
Converting circular structure to JSON순환 참조 객체partialize로 필드 선택
Unexpected token u in JSONundefined 값 저장됨초기값을 null로 설정
함수가 저장되지 않음JSON 미지원 타입함수는 상태에서 제외

: Chrome DevTools → Application → Local Storage에서 저장된 값을 직접 확인하고 JSON.parse()로 유효성을 검사하세요.

금요일

Prisma N+1 쿼리 문제 완벽 해결 가이드

🔍 검색 키워드: Prisma N+1 쿼리 문제, relation 로드 최적화, Prisma include, eager loading, 데이터베이스 쿼리 최적화

Prisma N+1 쿼리 문제 완벽 해결 가이드

1. 증상: N+1 쿼리 문제 발생

API 요청 하나에 데이터베이스 쿼리가 1 + N개 날아간다:

// 문제 있는 코드
const users = await prisma.user.findMany(); // Query 1: 사용자 100명 조회

for (const user of users) {
  user.posts = await prisma.post.findMany({
    where: { authorId: user.id }
  }); // Query 2-101: 각 사용자마다 posts 조회 (100번 반복)
}

// 총 101개의 쿼리 발생! 🚨

// 로그:
// Query 1: SELECT * FROM "User" WHERE 1=1
// Query 2: SELECT * FROM "Post" WHERE "authorId" = 1
// Query 3: SELECT * FROM "Post" WHERE "authorId" = 2
// ...
// Query 101: SELECT * FROM "Post" WHERE "authorId" = 100

API 응답 속도가 매우 느리고, 데이터베이스 CPU 사용률이 급증한다. 동시 사용자가 많아지면 connection pool 고갈로 이어진다.

2. 원인 분석

Lazy Loading의 위험성

Prisma의 기본 동작은 "필요할 때만 로드하는" lazy loading이다. relation을 접근할 때마다 새로운 쿼리를 발생시킨다.

Include 미사용

relation 데이터가 필요한데도 includeselect를 사용하지 않아 N번의 추가 쿼리가 필요하다.

깊은 relation 체인

user.posts.comments.author.profile 같은 깊은 relation 체인은 기하급수적으로 쿼리가 증가한다.

루프 내에서의 데이터베이스 접근

forEach, for-of 루프 내에서 직접 데이터베이스 쿼리를 실행하는 구조.

3. 해결방법

방법 1: Include를 사용한 Eager Loading

// 1단계: 단순 include
const users = await prisma.user.findMany({
  include: {
    posts: true  // 모든 posts를 함께 로드
  }
});
// 총 2개 쿼리:
// Query 1: SELECT * FROM "User"
// Query 2: SELECT * FROM "Post" WHERE "authorId" IN (1, 2, ..., 100)

// 2단계: 조건부 include
const users = await prisma.user.findMany({
  include: {
    posts: {
      where: { published: true },  // published=true인 posts만
      orderBy: { createdAt: 'desc' },
      take: 5  // 최근 5개만
    }
  }
});

// 3단계: 깊은 relation include (2단계까지만 권장)
const users = await prisma.user.findMany({
  include: {
    posts: {
      include: {
        comments: {  // posts의 comments도 로드
          include: {
            author: true  // comments의 author도 로드
          }
        }
      }
    }
  }
});

방법 2: Select를 사용한 필드 최적화

// 필요한 필드만 선택
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    email: true,
    posts: {
      select: {
        id: true,
        title: true,
        slug: true
      }
    }
  }
});

// 장점:
// 1. 필요 없는 컬럼(password, bio 등)을 제외 → 네트워크 대역폭 절약
// 2. 쿼리 성능 향상
// 3. 클라이언트에 민감한 정보 노출 방지

방법 3: BatchLoad 패턴 (DataLoader)

// dataloader 라이브러리 설치: npm install dataloader

import DataLoader from 'dataloader';

// User의 posts를 배치로 로드하는 DataLoader
const postsByUserIdLoader = new DataLoader(async (userIds) => {
  const postsByUserId = await prisma.post.findMany({
    where: { authorId: { in: userIds } }
  });

  // userIds 순서대로 결과 반환
  return userIds.map(userId =>
    postsByUserId.filter(post => post.authorId === userId)
  );
});

// GraphQL resolver에서 사용
const userResolver = {
  posts: (user) => postsByUserIdLoader.load(user.id)
};

// 효과:
// 100개의 개별 쿼리 대신 1개의 배치 쿼리로 통합

방법 4: 정규화된 API 응답 구조

// 비정규화 응답 (nested - 중복 데이터)
{
  users: [
    {
      id: 1,
      name: 'Alice',
      posts: [
        { id: 101, title: 'Post 1', author: { id: 1, name: 'Alice' } }
      ]
    }
  ]
}

// 정규화 응답 (flat - 효율적)
{
  users: [{ id: 1, name: 'Alice' }],
  posts: [{ id: 101, title: 'Post 1', authorId: 1 }]
}

// Prisma에서 정규화 응답 생성
const [users, posts, comments] = await Promise.all([
  prisma.user.findMany(),
  prisma.post.findMany(),
  prisma.comment.findMany()
]);

const response = {
  users,
  posts,
  comments
};
// 이렇게 하면 클라이언트가 필요한 대로 조합 가능

방법 5: 쿼리 성능 측정 및 디버깅

// .prisma/client 에서 로깅 활성화
const prisma = new PrismaClient({
  log: [
    { emit: 'stdout', level: 'query' },
    { emit: 'stdout', level: 'info' },
    { emit: 'stdout', level: 'warn' },
    { emit: 'stderr', level: 'error' }
  ]
});

// 또는 런타임 환경에서
prisma.$on('query', (e) => {
  console.log(`${e.query} - ${e.duration}ms`);
});

// 성능 프로파일링 (Node.js console.time)
console.time('fetch users with posts');
const users = await prisma.user.findMany({
  include: { posts: true }
});
console.timeEnd('fetch users with posts');
// fetch users with posts: 45.231ms

4. 정리표

상황 해결 방법 쿼리 수 성능 개선도
N+1 발생 include 미사용 1 + N -
단순 관계 include true 2 ⬆⬆⬆
조건 필터 include with where 2 ⬆⬆⬆
깊은 관계 (2단계) 중첩 include 3-4 ⬆⬆⬆
GraphQL 환경 DataLoader 1-2 (배치) ⬆⬆⬆⬆
민감한 필드 select 사용 2 + 대역폭↓ ⬆⬆⬆
매우 깊은 관계 정규화 응답 N (분산) ⬆⬆⬆

핵심: N+1은 Prisma만의 문제가 아니라 ORM의 근본적인 특성. include로 eager loading을 하거나, API 설계 단계부터 정규화된 응답 구조를 고려해야 한다. GraphQL을 사용한다면 DataLoader는 필수다.