[3] NestJS로 api

Kim TaeHyeong·4일 전

UMC

목록 보기
6/10

그냥 요청 받은 그 파일에서 모든 걸 수정할 수도 있지만
그러면 나중에 코드 관리하기 힘들어서 쪼개서 만든다

Controller => Service => Repository 꼴

// src/database.provider.ts
import { ConfigService } from '@nestjs/config';
import * as mysql from 'mysql2/promise';

// 의존성 주입(DI)에 사용할 우리만의 고유 이름표(토큰)
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';

export const databaseProviders = [
  {
    provide: DATABASE_CONNECTION,
    inject: [ConfigService],
    useFactory: (configService: ConfigService) => {
      return mysql.createPool({
        host: configService.get<string>('DB_HOST', 'localhost'),
        port: configService.get<number>('DB_PORT', 3306),
        user: configService.get<string>('DB_USER', 'root'),
        password: configService.get<string>('DB_PASSWORD', ''),
        database: configService.get<string>('DB_NAME', 'study'),
        waitForConnections: true, // 선로가 꽉 차면 에러 대신 대기
        connectionLimit: 10,      // 미리 뚫어둘 핫라인(커넥션) 개수 10개
        queueLimit: 0,
      });
    },
  },
];

configService.get('타겟', '기본 값')
.env에서 설정이 가능

app.module.ts

ㄴcontrollers로 BookController, RentalsController

ㄴproviders로 databaseProviders, BookService, BookRepository, RentalsService, RentalsRepository

ㄴㄴ BookController → BookRepository → BookService

ㄴㄴ RentalsController → RentalsRepository → RentalsService

꼴로 만들었다.

app.module.ts

import { Module } from '@nestjs/common';
import { createObserveModule } from '@nestjs/observe';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
import { ConfigModule } from '@nestjs/config'; //.env 알아먹게
import { databaseProviders } from './database.provider.js';

import { BookController } from './book.controller.js';
import { BookService } from './book.service.js';
import { BookRepository } from './book.repository.js';

import { RentalsController } from './rentals.controller.js';
import { RentalsService } from './rentals.service.js';
import { RentalsRepository } from './rentals.repository.js';

export const { ObserveModule, ObserveInstrument } = createObserveModule();

@Module({
  imports: [
    // Distributed tracing, auto-correlated logs, request/job metrics, error
    // telemetry, alarms, and more — out of the box. Sign up at https://observe.nestjs.com
    ObserveModule.forRoot({
      appKey: 'YOUR_APP_KEY',
      appSecret: 'YOUR_APP_SECRET',
      serviceId: 'study',
    }),

    ConfigModule.forRoot({
        isGlobal: true,
    })
  ],
  controllers: [
    AppController,

    BookController,
    RentalsController,
],
  providers: [
        ...databaseProviders, // db 커넥션 풀
        AppService,

        BookService,
        BookRepository,

        RentalsService,
        RentalsRepository,
    ],
    exports: [
        ...databaseProviders
    ]
})
export class AppModule {}

database.provider.ts

// src/database.provider.ts
import { ConfigService } from '@nestjs/config';
import * as mysql from 'mysql2/promise';

// 의존성 주입(DI)에 사용할 우리만의 고유 이름표(토큰)
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';

export const databaseProviders = [
  {
    provide: DATABASE_CONNECTION,
    inject: [ConfigService],
    useFactory: (configService: ConfigService) => {
      return mysql.createPool({
        host: configService.get<string>('DB_HOST', ''),
        port: configService.get<number>('DB_PORT', 3360),
        user: configService.get<string>('DB_USER', ''),
        password: configService.get<string>('DB_PASSWORD', ''),
        database: configService.get<string>('DB_NAME', ''), //뒤에 건 기본 값들
        waitForConnections: true, // 선로가 꽉 차면 에러 대신 대기
        connectionLimit: 10,      // 미리 뚫어둘 핫라인(커넥션) 개수 10개
        queueLimit: 0,
      });
    },
  },
];

book.controller.ts

// src/book.controller.ts
import { Controller, Get, Param, Patch } from '@nestjs/common';
import { BookService } from './book.service.js';
import { Body, Post } from '@nestjs/common';

@Controller('books') // 이 컨트롤러로 들어오는 기본 주소: /books
export class BookController {
  // 주방장(BookService)을 주입받습니다.
  constructor(private readonly bookService: BookService) {}

  // HTTP GET 방식으로 /books 요청이 들어왔을 때 실행되는 핸들러
  @Get()
  async getBooks(): Promise<any> {
    return await this.bookService.getAllBooks();
  }

  @Get('category/:categoryID')
  async findBooksByCategoryID(@Param('categoryID') categoryID: number): Promise<any> {
    return await this.bookService.findBooksByCategoryID(categoryID);
  }

  @Post()
  async createBook(@Body() body: Record<string, any>): Promise<string> {
    return await this.bookService.createBook(body);
  }
}

book.repository.ts

// src/book.repository.ts
import { Injectable, Inject } from '@nestjs/common';
import type { Pool } from 'mysql2/promise';
import { DATABASE_CONNECTION } from './database.provider.js';

//ex) 레시피가 있음
@Injectable() // NestJS 컨테이너에 "나 주입 가능한 부품이야!"라고 등록
export class BookRepository {
  constructor(
    // 2단계에서 우리가 등록해둔 DB 커넥션 풀(DATABASE_CONNECTION)을 가져옵니다.
    @Inject(DATABASE_CONNECTION) private readonly pool: Pool, //안전하게 readonly
    /* 재료의 위치를 가리키는 스티커 */
  ) {}

  async findAll(): Promise<any> {
    const sql = 'SELECT * FROM book';
    
    // pool.query()는 [조회된 행들, 메타데이터 필드들] 형태의 배열을 돌려줍니다.
    // 우리는 실제 행 데이터만 필요하므로 구조 분해 할당으로 [rows]만 쏙 꺼냅니다.
    const [rows] = await this.pool.query(sql);
    return rows;
  }

  async findBooksByCategoryID(categoryID: number): Promise<any> {
    const sql = 'SELECT * FROM book WHERE category_id = ?;';

    const [rows] = await this.pool.execute(sql, [
        categoryID,
    ]);
    
    return rows;
  }

  async create(body: Record<string, any>): Promise<any> {
    const sql = 'INSERT INTO book (category_id, title, description, is_available) VALUES (?, ?, ?, true)';
  
    // 두 번째 인자로 넘긴 배열이 ? 자리에 순서대로 안전하게 바인딩됩니다.
    const [result] = await this.pool.execute(sql, [
      body.categoryId,
      body.title,
      body.description,
    ]);
    return result;
  }
}
/*
새 Promise 생성
- const p = new Promise((resolve, reject) => { ... resolve(value) 또는 reject(error) ... })
then으로 결과 받기
- p.then(value => { ... return값 } ).catch(err => { ... })
async 함수는 항상 Promise를 반환합니다.
함수 내부에서 await로 Promise가 해결될 때까지 기다립니다.
- async function getData() { const data = await fetchSomething(); return data; }
- getData().then(d => console.log(d)).catch(e => console.error(e));
*/

/*
constructor
class Person {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
처럼 new Person('길동', 20) 처럼
 */

book.service.ts

// src/book.service.ts
import { Injectable } from '@nestjs/common';
import { BookRepository } from './book.repository.js';

@Injectable()
export class BookService {
  // 창고지기(BookRepository)를 주입받습니다.
  constructor(private readonly bookRepository: BookRepository) {}

  async getAllBooks(): Promise<any> {
    return await this.bookRepository.findAll();
  }

  async findBooksByCategoryID(categoryID: number): Promise<any> {
    return await this.bookRepository.findBooksByCategoryID(categoryID);
  }

  // book.service.ts에 추가
  async createBook(body: Record<string, any>): Promise<string> {
    await this.bookRepository.create(body);
    return '도서 등록이 완료되었습니다!';
  }
}

rentals.controller.ts

// src/book.controller.ts
import { Controller, Param, Patch } from '@nestjs/common';
import { RentalsService } from './rentals.service.js';
import { Body, Post } from '@nestjs/common';

@Controller('rentals')
export class RentalsController {
    constructor(private readonly rentalsService: RentalsService) {}

    @Post()
    async rentBook(@Body() body: Record<string, any>): Promise<string> {
        return await this.rentalsService.rentBook(body);
    }

    @Patch(':rentalID/return')
    async returnBook(@Param('rentalID') rentalID:number): Promise<string> {
        return await this.rentalsService.returnBook(rentalID);
    }
}

Get 안 써서 그냥 뺌

rentals.repository.ts

import { Injectable, Inject } from '@nestjs/common';
import type { Pool } from 'mysql2/promise';
import { DATABASE_CONNECTION } from './database.provider.js';

@Injectable()
export class RentalsRepository {
  constructor(
    @Inject(DATABASE_CONNECTION) private readonly pool: Pool,
  ) {}

  async rentBook(body: Record<string, any>): Promise<any> {
    const sql = 'INSERT INTO rental (user_id, book_id, rented_at, due_at) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))';

    const [result] = await this.pool.execute(sql, [
        body.userId,
        body.bookId,
    ]);
    return result;
  }

  async returnBook(rentalID: number): Promise<any> {
    const sql = 'UPDATE rental SET returned_at = NOW() WHERE rental_id = ?';

    const [result] = await this.pool.execute(sql, [
        rentalID
    ]);

    return result;
  }
}

rentals.service.ts

// src/book.service.ts
import { Injectable } from '@nestjs/common';
import { RentalsRepository } from './rentals.repository.js';

@Injectable()
export class RentalsService {
  constructor(private readonly rentalsRepository: RentalsRepository) {}

  async rentBook(body: Record<string, any>): Promise<string> {
    await this.rentalsRepository.rentBook(body);
    return '정상적으로 대여했습니다.'
  }

  async returnBook(rentalID: number): Promise<string> {
    await this.rentalsRepository.returnBook(rentalID);
    return '정상적으로 반납했습니다.'
  }
}

기본

/books
get

post

/rentals
post

patch

  • 3계층 아키텍처 이외에 어떤 아키텍처들이 있을까?

    • 2계층 아키텍처 (2-Tier)

      • 구성: 프런트엔드 클라이언트와 데이터베이스가 직접 연결되거나, 프런트엔드가 간단한 비즈니스 로직만 수행하는 형태. 중소 규모 앱에서 간단하게 쓰이나 확장성은 제한적임.
      • a 간단 구현.
    • N계층 / 다층 아키텍처 (Multi-/N-tier)

      • 기본 컨셉은 3계층의 확장 버전. 프리젠테이션, 비즈니스 로직, 데이터 계층 외에 서비스 계층, API 게이트웨이, 도메인 계층 등을 별도 계층으로 구분해 확장성과 시험 가능성을 높임.
      • a 폴더 여러 개 나누는 것처럼 확장성 높이기
    • 마이크로서비스 아키텍처

      • 애플리케이션을 작고 독립적으로 배포 가능한 서비스 단위로 분해. 각 서비스는 자체 DB를 가질 수 있고, API 게이트웨이와 이벤트 버스를 통해 통신. 확장성, 독립성은 좋지만 운영 복잡성 증가.
      • a 지금은 큰 거 하나 ⇒ 그 아래서 나누기 인데, 큰 거를 여러 개로 나눔
    • 서비스 지향 아키텍처(SOA)

      • 여러 서비스 간 재사용 가능한 비즈니스 기능을 서비스로 묶어 네트워크를 통해 연계. 대규모 엔터프라이즈에서 여전히 사용되나 마이크로서비스로 대체되는 경우도 많음.
      • a 중복을 줄이려는 노력
    • 헥사고날 아키텍처 / 클린 아키텍처(온ion/포츠 앤 어댑터)

      • 핵심 도메인 로직을 중심으로 포트(입출력 인터페이스)와 어댑터로 외부 의존성을 분리. 테스트 용이성, 도메인 독립성에 강점.

        헥사고날 아키텍처의 아이디어는 입력과 출력을 설계의 가장자리에 배치하는 것입니다.
        비즈니스 로직은 REST 또는 GraphQL API를 노출시키는지 여부에 의존해서는 안됩니다.
        마찬가지로, 데이터베이스나 API 또는 단순한 CSV 파일과 같은 데이터를 어디에서 가져오는지에 의존해서는 안 됩니다.
        
        즉, 비즈니스 로직을 설계의 중심으로 하면서,
        노출시키는 영역(outputs)과 데이터를 가져오는 영역(inputs)에 의존하지 않는 디자인을 의미입니다.

        출처:

        https://gngsn.tistory.com/258

        [ENFJ.dev:티스토리]

    • 이벤트 주도 아키텍처(EDA) / 스트리밍 아키텍처

      • 비동기 이벤트 버스(Kafka 등)로 생산자-소비자 간 결합을 줄이고 확장성을 확보. 대용량 데이터 처리나 실시간 분석에 적합.

      • a 통신 중개가 있어서 일관성 관리 굿. replay 가능 (복구 ?)

    • CQRS(쓰기/읽기 분리) + 이벤트 소싱

      • 쓰기 모델과 읽기 모델을 분리하고, 상태를 이벤트로 저장/재생하는 패턴. 복잡한 도메인에서 일관성/성능을 맞추기 용이.
      • a 쓰기랑 읽기 분리해서 성능 극대화 가능. 이벤트로 만들어서 replay 가능
    • 서버리스 아키텍처

      • 컴퓨팅 자원을 서버 관리 없이 함수 단위로 실행. 트래픽에 따라 자동 확장되나, 장기 실행 작업이나 특정 워크로드에 적합한지 검토가 필요.
      • a 서버리스로 호출 때만 이용. 그러나 cold start, 타 호스팅 서버로 이전 어려움이 있음
    • 마이크로 프런트엔드

      • 프런트엔드를 기능 단위로 나눠 각각 독립적으로 배포하는 방식. 대규모 프런트엔드 팀에 유리.
      • a 프론트엔드를 쪼개서 독립적으로 처리. 상품 목록란, 상품 상세 페이지란 등..
    • 도메인 주도 설계(DDD) 기반 아키텍처

      • 도메인 모델 중심으로 구조화하고, 바운디드 컨텍스트 간 명확한 경계를 두어 복잡한 비즈니스 로직을 관리. 구현은 위의 아키텍처 패턴들과 결합해 사용 가능.
      • a 그냥 하위로 나누는 방법인데 좀 더 잘 나누는 기법
  • SQL Injection을 포함한 대표적인 웹 보안 공격 기법

    • SQL Injection (SQLi)
      • 의도치 않은 SQL 코드가 실행되도록 입력을 남용하는 공격. 예: 입력을 그대로 쿼리에 삽입 시, 의도치 않은 데이터 접근.
      • 예방: 파라미터화된 쿼리/준비된 문장(prepared statements), ORM의 안전한 쿼리 사용, 입력 검증.
      • a SQL + query + 꼴일 때 주로 뚫림. 이런 거 payload로 github 같은 곳에 많음
      • https://github.com/payload-box/sql-injection-payload-list
    • Cross-Site Scripting(XSS)
      • 악의적 코드가 웹 페이지에 주입되어 브라우저에서 실행. 저장 XSS, 반사 XSS가 대표적.
      • 예방: 출력 인코딩, 콘텐츠 보안 정책(CSP), 입력 검사.
      • a 같은 거 넣는 거. 보통 불러오기 부분에서 잘못 처리하면 뚫림
    • Cross-Site Request Forgery(CSRF)
      • 사용자가 신뢰하는 사이트에 대해 의도치 않게 요청을 보내도록 유도.
      • 예방: CSRF 토큰, SameSite 쿠키 설정.
      • a 링크 같은 거 덮어씌우는거. ex) 어차피 로그인 정보 있으니 뭐 글 작성 시키게 하거나 등등
    • 인증/세션 관리 취약점(Broken Authentication and Session Management)
      • 세션 토큰 관리 미흡, 비밀번호 저장 방식 부적절 등.
      • 예방: 강력한 비밀번호 저장(해시+솔트), 토큰 만료/재발급, 다중 인증 도입.
      • a 그냥 local storage 같은 데 저장하면 그럼
    • Insecure Direct Object References(IDOR)
      • 사용자가 직접 자원 식별자를 조작해 접근 권한 없는 리소스에 접근하는 문제.
      • 예방: 권한 검증 로직 중앙화, 접근 제어 체크 강화.
      • a 그냥 /admin 해도 들어가지는 거. 서버가 클라 너무 믿으면 생김
    • 보안 구성 미스로 인한 노출(Security Misconfiguration)
      • 기본 설정 노출, 디버그 페이지 공개, 불필요한 포트/서비스 열려 있음.
      • 예방: 기본 보안 설정 자동 검사, 정적 보안 스캐닝.
      • a /admin 같은 거
    • 민감 데이터 누출(Sensitive Data Exposure)
      • 데이터 암호화 부재, 약한 암호화 알고리즘 사용 등.
      • 예방: 전송 중/저장 시 암호화, 키 관리 강화.
      • a http나 평문 씀
    • XXE(XMLEntity Injection)
      • XML 파서의 외부 엔티티를 이용한 공격.
      • 예방: 외부 엔티티 비활성화, 신뢰된 파서 사용.
      • a xml 파싱하는 거 이용
    • 비안전한 직렬화(Insecure Deserialization)
      • 직렬화된 객체를 악용해 코드 실행이나 원격 공격.
      • 예방: 객체 직렬화 시 검증, 서명된 데이터만 허용.
      • a 옛날에 log4j, python exec랑 비슷. 이것도 클라 신뢰해서 그럼
    • 서버사이드 요청 위조(SSRF)
      • 서버가 임의의 내부/외부 자원에 요청을 보내는 문제.
      • 예방: 입력 검증, 네트워크 접근 제어.
      • a /admin 같은 거
    • 경로 탐색(Path Traversal)/디렉터리 트래버설
      • 파일 시스템의 임의 경로 접근.
      • 예방: 입력 화이트리스트, 파일 경로 검증.
      • a ../ 같이 맘대로 폴더 옮겨다녀서 데이터 가져오기
    • 클릭재킹(Clickjacking)
      • 사용자의 클릭을 가려 다른 의도된 행동으로 유도.
      • 예방: X-Frame-Options/CSP 프레임 설정.
      • a 투명한 웹페이지 등 클릭을 다른 이벤트 보내는 걸로 만드는 거
    • 무차별 대입/자격증 stuffing(Brute Force/Credential Stuffing)
      • 계정 로그인 시도 무차별 시도.
      • 예방: 로그인 시도 제어, 계정 차단, 다중 인증.
      • a 그냥 비번 무작위 대입법
  • 커넥션 풀(Connection Pool)이란?

    • 정의
      • 애플리케이션이 데이터베이스와의 연결을 미리 만들어 두고 재사용하는 기술. 필요할 때마다 새 연결을 여는 대신 풀에서 이미 만들어진 연결을 얻어 사용하고, 다 사용한 연결은 다시 풀에 반환하는 방식.
      • a Unity의 Object Pooling 방식이랑 비슷
    • 왜 필요한가
      • 연결 생성/해제 비용가 크고 속도가 느림. 풀링으로 응답 속도 개선, 데이터베이스 리소스 관리(동시 접속 수 제한) 용이.
    • 핵심 구성 요소
      • 초기 커넥션 수, 최대 풀 크기, 커넥션 검사/유효성 검사 쿼리, 커넥션 대기 시간(타임아웃), 커넥션 재활용 정책.
    • 일반적인 주의점
      • 커넥션 누수, 풀 고갈, 만료된 커넥션 회수, 트랜잭션 경계 관리, 다중 쓰레드 환경에서의 안전성.
    • 예시 기술
      • Java: HikariCP, Tomcat JDBC Pool
      • Python: SQLAlchemy의 기본/커넥션 풀
      • Node.js: 각 DB 드라이버의 풀 옵션(예: pg의 Pool)
    • 실무 팁
      • 프로덕션 환경에서는 적절한 풀 크기와 타임아웃 설정, 주기적 연결 유효성 검사, 예외 시 자동 재생성 옵션 사용.
  • Raw SQL vs ORM

    • Raw SQL의 장점
      • 최대한의 제어력과 성능 최적화 가능. 복잡한 쿼리나 데이터베이스 특유의 기능 사용에 유리.
      • a 사용자 맘대로 가능
    • ORM의 장점
      • 작성 코드 양 감소, 객체지향 모델과의 자연스러운 매핑, 데이터 유효성 검사/트랜잭션 관리의 편의성, 데이터 접근 코드의 유지보수성 향상.
      • a 편함
    • 주의점
      • ORM이 항상 최적의 성능을 내지는 않음. N+1 문제, 잘못된 쿼리 생성, 미세 조정의 한계.
      • a 약간 오류 가능
    • 언제 어떤 것을 쓸까
      • 간단한 CRUD 중심의 애플리케이션이나 도메인 모델이 잘 매핑되는 경우: ORM 권장.
      • 복잡한 조인, 고성능 쿼리, 데이터 웨어하우징 등의 특이한 쿼리: Raw SQL 우선 고려 후 필요시 ORM과 혼합 사용.
      • a 복잡성 잘 따라서
    • 안전한 실전 포인트
      • 파라미터 바인딩으로 SQL 인젝션 방지. ORM도 내부적으로 파라미터 바인딩을 사용하지만, 여전히 네이티브 SQL 혼합 시 주의.
      • a SQL Injection 주의
    • 예시
      • Raw SQL: SELECT id, name FROM users WHERE email = ?; (파라미터 바인딩)
      • ORM(예시: Python SQLAlchemy): User.query.filter_by(email='example@example.com(새 창 열림)').first()
  • INSERT 외에 데이터를 다루는 핵심 SQL 문법

    • 기본 카테고리
      • DML(Data Manipulation Language): SELECT, INSERT, UPDATE, DELETE
      • DDL(Data Definition Language): CREATE, ALTER, DROP
      • TCL(Transaction Control Language): BEGIN/COMMIT/ROLLBACK
      • DCL(Data Control Language): GRANT/REVOKE
      • DML 확장: MERGE, UPSERT 등 DB별 명령
      • CTE/윈도우 함수 등 부가적 도구도 많음
    • 예시와 핵심 패턴
      • SELECT: 데이터 조회와 조인, 정렬, 필터, 페이징
        • 예: SELECT id, name FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 10;
        • 조인 예: SELECT u.id, o.total FROM users u JOIN orders o ON u.id = o.user_id;
        • 그룹/집계 예: SELECT status, COUNT() FROM orders GROUP BY status HAVING COUNT() > 5;
        • 서브쿼리 예: SELECT id, name FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);
        • 윈도우/CTE 예: WITH recent_orders AS (SELECT FROM orders WHERE created_at > NOW() - INTERVAL '7 days') SELECT FROM recent_orders;
      • INSERT: 새 데이터 추가
        • 예: INSERT INTO users (name, email) VALUES ('홍길동', 'hong@example.com(새 창 열림)');
      • UPDATE: 기존 데이터 수정
        • 예: UPDATE products SET price = price * 1.05 WHERE category = 'books';
      • DELETE: 데이터 삭제
        • 예: DELETE FROM sessions WHERE last_seen < NOW() - INTERVAL '30 days';
      • MERGE / UPSERT: 존재하면 업데이트, 없으면 삽입
        • PostgreSQL(ON CONFLICT): INSERT INTO t(id, val) VALUES (1, 'A') ON CONFLICT (id) DO UPDATE SET val = EXCLUDED.val;
        • MySQL(ON DUPLICATE KEY UPDATE): INSERT INTO t(id, val) VALUES (1, 'A') ON DUPLICATE KEY UPDATE val = VALUES(val);
        • SQL Server(MERGE): MERGE INTO t USING (VALUES(1,'A')) AS s(id, val) ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.val = s.val WHEN NOT MATCHED THEN INSERT (id, val) VALUES (s.id, s.val);
      • TRUNCATE: 테이블의 모든 행을 빠르게 제거
        • 예: TRUNCATE TABLE log_entries;
      • 트랜잭션 예제
        • 예: BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT;
        • a 중간에 뻑나도 무결성 유지
      • 권한 관리 및 스키마 변화
        • 예: GRANT SELECT ON public.orders TO reporting_user;
        • ALTER TABLE 변경, CREATE/DROP TABLE 등은 필요시 사용
    • 주의점
      • 데이터 무결성 유지: 트랜잭션 경계 설정, 적절한 제약 조건(외래키, 고유 키) 유지.
      • 성능: 대용량 조인, 서브쿼리, 인덱스 활용, 필요 시 CTE/윈도우 함수의 적절한 사용.
      • 보안: 파라미터 바인딩으로 인젝션 방지, 입력 유효성 검사.

0개의 댓글