본문 바로가기

프로젝트/잔소리

[잔소리 ] 트러블 슈팅

반응형

 

해당 포스트는..

해당 포스트는 프로젝트를 진행하면서 경험한 다양한 문제들과 이를 개선하는 과정를 정리한 포스트 입니다. 모든 이슈를 정리하지는 않으며, 사이사이에 해결하기가 까다롭고 어려웠던 문제 위주로 선별하여  개인 참고 및 동일한 문제를 경험하는 분들에게 도움을 드리고자 작성합니다.

원래 기존의 트러블 슈팅 작성 방식과 다른 방식으로 수정하였습니다. 이전 방식의 문제점은 가독성이었는데, 이를 조금 더 보기 쉬운 방식으로 수정하였습니다.

 

[중복 DB 인스턴스] Unix 소켓을 통한 Cloud SQL 연결 시 5432 포트 활성화 문제(24.08.17)

[요약] 중복 인스턴스가 생성되지 않도록

해당 문제의 원인은 결국 5432 포트를 사용하는 인스턴스를 중복으로 여러 개 생성하려고 할 때 포트 충돌이 발생한 것입니다. 로컬에서 문제가 발생한다면, 이미 실행중인 포트를 Kill 하는 방식으로 처리를 시도해봤겠지만, 이는 GCP 의 원격 클라우드에 배포하는 과정에서 발생하는 것이므로 해결과정을 찾는데 여러 시도를 하였고, 그 결과 1주일 이라는 시간이 걸렸습니다.

 

개선 방법은 디자인 패턴으로 싱글톤 패턴에 대해 짤막하게 공부했던 기억이 떠올랐습니다. 다만  클래스 기반 혹은 생성자 함수 기반으로 적용하는 싱글톤 패턴보다는 모듈단위로 접근가능한 파일이므로 전역변수와 조건문을 이용해 중복 인스턴스가 생성되지 않도록 방지하는 방향으로 문제를 개선하였습니다.

 

[문제상황] address alreay in use /app/ .s PGSQL .5432 로 인한 빌드 실패

GCP 의 Cloud Run + Cloud SQL  + Unix 소켓(+ SQL  Auth Proxy) 연동을 통해 Next.js 프로젝트를 배포하던 중, 빌드 과정에서 .s PGSQL .5332 소켓파일명이 표시되면서 이미지 주소가 사용되고 있다는 에러가 발생하고 있습니다.

 

 

 

 

[문제원인]  데이터베이스 연결을 시도하는 인스턴스가 여러 개

해당 문제가 발생한 원인은 데이터베이스 연결을 시도하는 도커 컨테이너 인스턴스가 여러 개 존재하였기 때문이었습니다. 

 

인스턴스가 여러 개 생성되는 이유는 GCP Cloud Run 의 인스턴스 확장 방식이 트래픽에 따라 유동적으로 증가와 감소가 이루어지기 때문에, 5432 포트에 대한 연결 시도하는 인스턴스의 경우도 이러한 인스턴스 확장으로 인해 발생한 문제였습니다.

 

 

[개선방법] 이미 인스턴스가 존재한다면, 기존 인스턴스를 내보내기

저의 경우 Cloud SQL 을 Cloud Run 과 연동하기 위해서 Unix 소켓과 sql 커텍터를 사용하고 있습니다. 원래라면 소켓을 사용할 필요가 없지만, 프리즈마와 같은 DB 맵핑 라이브러리를 지원하기 위해서는 필수적으로 사용해야 했습니다.

 

수정 전 코드

수정 전 코드는 프리즈마로 맵핑된 DB에 접근하기 위해서 매번 요청 시 마다 데이터베이스 인스턴스를 매번 생성하는 방식으로 작성되어 있습니다. 이는 로컬 환경에서는 문제가 없지만, Cloud Run의 인스턴스 확장 방식에서 중복된 인스턴스 생성을 동시에 요청함으로써 충돌이 발생하고 있었습니다.

인스턴스를 요청 시 마다 매번 생성한다는 것이 요점입니다.
// prisma/client.ts

import { resolve } from 'node:path';
import {
  AuthTypes,
  Connector,
  IpAddressTypes,
} from '@google-cloud/cloud-sql-connector';
import { PrismaClient } from '@prisma/client';

// reference: https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/blob/0dfbb524d736ef5f28ab7c87fd7a95223a989cb6/examples/prisma/postgresql/connect.ts

export async function connect() {
  /** 프로덕션 */
  if (process.env.NODE_ENV === 'production') {
    const path = resolve(`.s.PGSQL.5432`); // postgres-required socket filename
    const connector = new Connector();
    await connector.startLocalProxy({
      instanceConnectionName: 'naing:asia-northeast1:naggl', // process.env.INSTANCE_CONNECTION_NAME || '', // DB 연결용 인스턴스 이름
      ipType: IpAddressTypes.PUBLIC, // 공개 IP 접근 명시
      authType: AuthTypes.IAM, // DB 인스턴스 접근 시 사용자 IAM 을 사용하여 접근토록 설정
      listenOptions: { path }, // postgres 소켓 경로 설정(포트 5432로 연결)
    });
    const hostPath = process.cwd();
    const datasourceUrl = `postgresql://${process.env.DB_USER}:${process.env.DB_PASS}@localhost/${process.env.DB_NAME}?host=${hostPath}`;

    console.log('log:', datasourceUrl);

    const prisma = new PrismaClient({ datasourceUrl });

    // 커텍터 연결이 완료된 후(인증이 완료 되었으므로) 연결을 종료합니다. 이후 부터는 prisma 클라이언트 사용 시 자동 온/오프 됩니다.
    return {
      prisma,
      async close() {
        await prisma.$disconnect();
        connector.close();
      },
    };

    /** 개발 */
  } else {
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
    const prisma = globalForPrisma.prisma || new PrismaClient();

    globalForPrisma.prisma = prisma;

    return {
      prisma,
      async close() {
        await prisma.$disconnect();
      },
    };
  }
}

 

수정 후 코드

수정 후에는  let prisma 를 전역변수(정확히는 모듈 내에서만 유효한 변수)로 선언하고, 인스턴스 생성 시 해당 변수에 할당되도록 로직을 개선하고, 만일 해당 인스턴스가 이미 존재한다면, 해당 인스턴스를 해보내도록 개선하였습니다.

중복된 데이터베이스 인스턴스가 생성되지 않도록 prisma 변수를 전역적으로 관리하고 이를 재사용했다는 것이 요지입니다.
import { resolve } from 'node:path';
import {
  AuthTypes,
  Connector,
  IpAddressTypes,
} from '@google-cloud/cloud-sql-connector';
import { PrismaClient } from '@prisma/client';

let prisma: PrismaClient | null = null;
let connector: Connector | null = null;

export async function connect() {
  if (prisma) {
    // 이미 연결되어 있는 경우, 기존 클라이언트를 반환
    return { prisma, close };
  }

  /** 프로덕션 */
  if (process.env.NODE_ENV === 'production') {
    const path = resolve(`.s.PGSQL.5432`); // postgres-required socket filename
    connector = new Connector();
    await connector.startLocalProxy({
      instanceConnectionName: 'nang:asia-nheast1:nanql', // DB 연결용 인스턴스 이름
      ipType: IpAddressTypes.PUBLIC, // 공개 IP 접근 명시
      authType: AuthTypes.IAM, // DB 인스턴스 접근 시 사용자 IAM 을 사용하여 접근토록 설정
      listenOptions: { path }, // postgres 소켓 경로 설정(포트 5432로 연결)
    });
    const hostPath = process.cwd();
    const datasourceUrl = `postgresql://${process.env.DB_USER}:${process.env.DB_PASS}@localhost/${process.env.DB_NAME}?host=${hostPath}`;

    console.log('log:', datasourceUrl);

    prisma = new PrismaClient({ datasourceUrl });
  } else {
    // 개발 환경
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
    prisma = globalForPrisma.prisma || new PrismaClient();

    globalForPrisma.prisma = prisma;
  }

  async function close() {
    if (prisma) {
      await prisma.$disconnect();
    }
    if (connector) {
      connector.close();
    }
  }

  return { prisma, close };
}

 

[개선결과] 정상적으로 빌드에 성공하였습니다.

이후 다시 빌드를 시도해보면 정상적으로 빌드가 완료되어 배포된 것을 확인할 수 있을 겁니다.

 

[개선 후기]  CS 지식이 중요한 이유?

싱글톤 패턴을 적용하여 개선한 문제는 아니었지만, 싱글톤 패턴에 착안하여 생각이 난 방식이었기 때문에, 이전에 정보처리기사를 통해서 공부했던 기억이 큰 도움이 되었습니다. 너무 도커 컨테이너 상에서 해결해야 하는 문제라는 편협한 시각에서 해당 이슈를 바라보았기 때문에 해결하는 데 시간이 소요된 부분이 없지 않아 있지만, 기초 베이스가 더 튼튼하였다면 빠르게 개선 가능했을 문제이지 않았을까 생각도 듭니다.

 

반응형