{"id":16614276,"url":"https://github.com/biud436/stingerloom","last_synced_at":"2025-10-29T18:31:46.774Z","repository":{"id":182744590,"uuid":"669023365","full_name":"biud436/stingerloom","owner":"biud436","description":"직접 만든 Node.js 서버 프레임워크(Server Framework)입니다.","archived":false,"fork":false,"pushed_at":"2024-04-22T05:19:32.000Z","size":1079,"stargazers_count":4,"open_issues_count":1,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-05-02T04:36:47.672Z","etag":null,"topics":["fastify","nodejs","server","server-framework","typescript","view"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/biud436.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-21T06:55:23.000Z","updated_at":"2024-06-02T14:03:12.063Z","dependencies_parsed_at":"2024-04-15T05:59:22.487Z","dependency_job_id":"1ad0061d-d176-481e-96d8-d85ac3d1d4d3","html_url":"https://github.com/biud436/stingerloom","commit_stats":null,"previous_names":["biud436/fastify-test-server","biud436/custom-server-framework","biud436/stingerloom"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biud436%2Fstingerloom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biud436%2Fstingerloom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biud436%2Fstingerloom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biud436%2Fstingerloom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/biud436","download_url":"https://codeload.github.com/biud436/stingerloom/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238610550,"owners_count":19500674,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fastify","nodejs","server","server-framework","typescript","view"],"created_at":"2024-10-12T02:05:30.354Z","updated_at":"2025-10-29T18:31:46.767Z","avatar_url":"https://github.com/biud436.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 소개\n\n**Stingerloom**은 Node.js를 위한 서버 프레임워크로, 기존 프레임워크의 구조와 동작 원리를 스스로 구현해보며 학습하고자 하는 목적에서 출발한 프로젝트입니다.\n\n단순한 학습을 넘어, **실제로 운영 환경에서 사용할 수 있을 수준의 완성도**를 목표로 개발되고 있습니다.\n\n---\n\n# 사용법\n\n## 시작하기\n\n본 프레임워크를 이용하려면, 먼저 `@stingerloom/core` 패키지 설치를 비롯한 필요한 구성을 해야 합니다.\n\n이는 매우 번거롭고 복잡할 수 있지만, **Stingerloom**은 이러한 번거로움을 최소화하기 위해,\n\nCLI에서 새로운 프로젝트를 생성할 수 있는 기능을 제공합니다.\n\n새 프로젝트를 생성하려면 다음 명령어를 사용할 수 있습니다:\n\n```bash\nnpx create-stingerloom@latest --name \u003cmy-app\u003e\n```\n\n그 다음 다음 명령어를 실행하여 의존성을 설치합니다:\n\n```bash\ncd \u003cmy-app\u003e\nyarn install\n```\n\n서버를 시작하려면 다음 명령어를 실행합니다:\n\n```bash\nyarn start:dev\n```\n\n# 개요\n\n1. 주요 기능들\n   - [컨트롤러](https://github.com/biud436/stingerloom?tab=readme-ov-file#controller)\n   - [주입 가능한 클래스](https://github.com/biud436/stingerloom?tab=readme-ov-file#injectable)\n   - [예외 처리](https://github.com/biud436/stingerloom#exception-filter%EC%99%80-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8)\n   - [트랜잭션](https://github.com/biud436/stingerloom?tab=readme-ov-file#handling-database-transactions)\n   - [템플릿 엔진](https://github.com/biud436/stingerloom?tab=readme-ov-file#template-engine)\n   - [ORM](https://github.com/biud436/stingerloom?tab=readme-ov-file#orm)\n   - [자동 파일 생성](https://github.com/biud436/stingerloom?tab=readme-ov-file#cli)\n2. [인증 및 권한](https://github.com/biud436/stingerloom?tab=readme-ov-file#authorization)\n   - [세션](https://github.com/biud436/stingerloom?tab=readme-ov-file#handling-session)\n   - [세션 가드](https://github.com/biud436/stingerloom?tab=readme-ov-file#session-guard)\n   - [커스텀 매개변수 데코레이터](https://github.com/biud436/stingerloom?tab=readme-ov-file#custom-parameter-decorator)\n3. 지원되는 데코레이터들\n   - Controller\n   - Get\n   - Post\n   - Patch\n   - Delete\n   - Put\n   - InjectRepository\n   - Req\n   - Body\n   - Header\n   - ExceptionFilter\n   - Catch\n   - BeforeCatch\n   - AfterCatch\n   - Injectable\n   - Session\n   - Transactional\n   - TransactionalZone\n   - InjectQueryRunner\n   - UseGuard\n   - View\n   - Render\n   - Autowired\n   - BeforeTransaction\n   - AfterTransaction\n   - Commit\n   - Rollback\n   - Query\n   - Param\n   - Ip\n   - Cookie\n   - Column\n   - Entity\n   - Index\n\n## 지원 서버 엔진\n\nStingerloom은 다양한 HTTP 서버 엔진을 지원합니다:\n\n### 🧶 Loom Server (네이티브)\n\n- **순수 Node.js**: Express, Fastify 없이 Node.js 기본 `http` 모듈 사용\n- **경량화**: 외부 의존성 최소화로 빠른 시작 시간\n- **유연한 라우팅**: URL 파라미터 지원 (`/users/:id`)\n- **미들웨어 지원**: 요청/응답 처리 파이프라인\n- **JSON 우선**: JSON 요청/응답 자동 처리\n\n프레임워크 이름 \"Stingerloom\"에서 영감을 받아 \"Loom(베틀)\"이라는 이름으로 구현된 자체 HTTP 서버 엔진입니다.\n\n### ⚡ Fastify Adapter\n\n- **고성능**: 빠른 처리 속도와 낮은 오버헤드\n- **TypeScript 우선**: 강력한 타입 지원\n- **플러그인 생태계**: 풍부한 플러그인 지원\n\n### 🚀 Express Adapter\n\n- **검증된 안정성**: 널리 사용되는 웹 프레임워크\n- **광범위한 미들웨어**: 풍부한 미들웨어 생태계\n- **커뮤니티 지원**: 대규모 커뮤니티와 자료\n\n## 사용 기술\n\n이 서버 프레임워크는 다음 기술들을 사용합니다:\n\n- **HTTP 서버**: Loom (네이티브), Fastify, Express 지원\n- typeorm\n- typedi\n- reflect-metadata\n- mariadb\n- class-transformer\n- class-validator\n- http-status\n\n사용하는 ORM은 typeorm이며, Body 데코레이터의 직렬화/역직렬화를 위해 class-transformer와 class-validator가 사용됩니다.\n\n또한 메타데이터 수집을 위해 reflect-metadata가 사용됩니다.\n\n# 사용법\n\n이 프레임워크는 다음 데코레이터들을 지원합니다: `Controller`, `Get`, `Post`, `Patch`, `Delete`, `Put`, `InjectRepository`, `Req`, `Body`, `Header`, `ExceptionFilter`, `Catch`, `BeforeCatch`, `AfterCatch`, `Injectable`, `Session`, `Transactional`, `TransactionalZone`, `InjectQueryRunner`, `UseGuard`, `View`, `Render`, `Autowired`,`BeforeTransaction`, `AfterTransaction`,`Commit`,`Rollback` , `Query`, `Param`, `Ip`, `Cookie`, `Column`, `Entity`, `Index`.\n\n- [컨트롤러](https://github.com/biud436/stingerloom?tab=readme-ov-file#controller)\n- [주입 가능한 클래스](https://github.com/biud436/stingerloom?tab=readme-ov-file#injectable)\n- [예외 필터와 실행 컨텍스트](https://github.com/biud436/stingerloom#exception-filter%EC%99%80-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8)\n- [트랜잭션 처리](https://github.com/biud436/stingerloom?tab=readme-ov-file#handling-database-transactions)\n- [권한 처리](https://github.com/biud436/stingerloom?tab=readme-ov-file#authorization)\n- [커스텀 매개변수 데코레이터](https://github.com/biud436/stingerloom?tab=readme-ov-file#custom-parameter-decorator)\n- [템플릿 엔진](https://github.com/biud436/stingerloom?tab=readme-ov-file#template-engine)\n\n## 빌드 및 실행\n\n이 프로젝트는 TypeScript로 작성되어 있으므로 빌드하려면 터미널에서 다음 명령어를 입력해야 합니다.\n\n```bash\nyarn build\n```\n\n빌드가 완료되면 `dist` 폴더에 빌드된 파일들이 생성되며, 포함된 예제와 함께 다음 명령어로 서버를 실행할 수 있습니다:\n\n```bash\nyarn start:dev\n```\n\n샘플 프로젝트 없이 이 라이브러리만 설치하여 직접 서버를 구성할 수도 있습니다.\n\n향후 기본 개발 환경은 샘플 프로젝트를 제외하도록 재구성될 것입니다.\n\n## 컨트롤러\n\n컨트롤러는 클라이언트 요청을 처리하고 응답하는 클래스입니다.\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/biud436/stingerloom/assets/13586185/366498a8-c871-400f-8ca4-4742a9e5110d\" /\u003e\n\u003c/p\u003e\n\n`@Controller` 데코레이터는 특정 경로에 대해 HTTP 요청을 적절한 컨트롤러로 라우팅하기 위한 메타데이터를 수집합니다.\n\n```ts\n@Controller(\"/user\")\nexport class UserController {\n  @Autowired()\n  private readonly point: Point;\n\n  @Autowired()\n  private readonly userService!: UserService;\n\n  @Get(\"/:id\")\n  public async getUserById(\n    @Param(\"id|8E1527BA-2C2A-4A6F-9C32-9567A867050A\") id: string,\n    @Query(\"name\") name: string,\n  ) {\n    if (!name) {\n      throw new BadRequestException(\"'name' 속성이 필요합니다.\");\n    }\n\n    return await this.userService.findOneByPk(id);\n  }\n\n  @Get(\"/point\")\n  async getPoint() {\n    this.point.move(5, 5);\n    return {\n      x: this.point.x,\n      y: this.point.y,\n    };\n  }\n\n  @Post()\n  public async create(@Body() createUserDto: CreateUserDto) {\n    return await this.userService.create(createUserDto);\n  }\n\n  @Header(\"Content-Type\", \"application/json\")\n  @Get()\n  public async getUser(@Ip() ip: string) {\n    return await this.userService.getUser(ip);\n  }\n}\n```\n\n라우팅 맵은 StingerLoom에서 처리하므로 사용자가 수동으로 라우팅 맵을 생성할 필요가 없습니다.\n\n`@Body()` 데코레이터는 요청 본문을 역직렬화하여 `createUserDto`에 할당하며 유효성 검사를 수행합니다. 유효성 검사가 실패하면 보통 400 오류가 발생합니다.\n\n`@Req()` 데코레이터는 FastifyRequest의 인스턴스를 주입합니다.\n\n`@Header()` 데코레이터는 응답 헤더를 설정합니다. 이 데코레이터는 메소드에만 적용할 수 있으며, 생략하면 기본 `Content-Type: application/json` 헤더가 설정됩니다.\n\n```ts\n@Controller(\"/\")\nclass AppController {\n  @Get(\"/blog/:id/:title\")\n  async resolveIdAndTitle(\n    @Param(\"id|0\") id: number,\n    @Param(\"title\") title: string,\n  ) {\n    return { id, title };\n  }\n\n  @Get(\"/point/:x\")\n  async resolveNameAndTitle(@Param(\"x\") point: Point) {\n    return point;\n  }\n\n  @Get(\"/user/:id\")\n  async resolveUser(\n    @Param(\"id|8E1527BA-2C2A-4A6F-9C32-9567A867050A\") id: string,\n  ) {\n    return id;\n  }\n\n  @Get(\"/admin/:id\")\n  async resolveAdmin(@Param(\"id\") id: string) {\n    return id;\n  }\n}\n```\n\nStingerLoom에서는 `@Param()` 데코레이터를 통해 경로 매개변수를 쉽게 가져올 수 있으며 타입에 따라 자동으로 캐스팅됩니다.\n\n기본값을 주입하려면 `@Param()` 데코레이터의 인수로 `type|default` 형식을 사용합니다.\n\n커스텀 타입을 만들려면 문자열을 처리하고 해당 타입으로 반환하는 변환 객체를 정의합니다.\n\n```ts\nclass Point {\n  private x: number;\n  private y: number;\n\n  constructor(args: string) {\n    const [x, y] = args.split(\",\");\n\n    this.x = parseInt(x, 10);\n    this.y = parseInt(y, 10);\n  }\n\n  getX() {\n    return this.x;\n  }\n\n  getY() {\n    return this.y;\n  }\n}\n```\n\n`@Query`에도 동일하게 적용되며, 타입이 `number`로 지정되면 문자열이 내부적으로 숫자로 변환되어 할당됩니다.\n\n```ts\n@Controller(\"/\")\nclass AppController {\n  @Get(\"/blog\")\n  async resolveIdAndTitle(\n    @Query(\"id\") id: number,\n    @Query(\"title\") title: string,\n  ) {\n    return { id, title };\n  }\n\n  @Get(\"/point\")\n  async resolveNameAndTitle(@Query(\"point\") point: Point) {\n    return { x: point.getX(), y: point.getY() };\n  }\n}\n```\n\nStingerLoom 서버 프레임워크에서 주목해야 할 중요한 것은 생성자 부분입니다.\n\n```ts\n@Controller(\"/user\")\nexport class UserController {\n    constructor(\n        // 1. Point는 주입 가능한 클래스가 아니므로 매번 인스턴스화됩니다.\n        private readonly point: Point,\n        // 2. UserService는 주입 가능한 클래스이므로 싱글톤 인스턴스로 관리됩니다.\n        private readonly userService: UserService,\n    ) {}\n```\n\n`@Injectable` 장에서 설명한 바와 같이, `Point` 클래스는 `@Injectable` 데코레이터가 없으므로 컨테이너에서 관리되지 않습니다. 요청별로 관리되지 않으며, 컨트롤러나 `Injectable` 클래스에 주입될 때마다 새 인스턴스가 생성됩니다.\n\n```ts\nexport class Point {\n  public x: number;\n  public y: number;\n\n  constructor() {\n    this.x = 0;\n    this.y = 0;\n  }\n\n  public move(x: number, y: number) {\n    this.x += x;\n    this.y += y;\n  }\n}\n```\n\n따라서 `/user/point`를 연속으로 호출하면 다음과 같이 출력됩니다:\n\n```json\n{\"x\":5,\"y\":5}\n{\"x\":10,\"y\":10}\n```\n\n반면 `Injectable` 클래스는 싱글톤 인스턴스로 관리되므로 컨트롤러나 `Injectable` 클래스에 주입될 때마다 같은 인스턴스가 주입됩니다.\n\n이에 대한 예제는 다음 섹션인 [주입 가능한 클래스](https://github.com/biud436/stingerloom#injectable)를 참조하세요.\n\n[▲ 목차로 돌아가기](https://github.com/biud436/stingerloom#%EC%82%AC%EC%9A%A9%EB%B2%95)\n\n## 주입 가능한 클래스\n\n`@Injectable` 데코레이터가 있는 클래스는 다른 클래스의 생성자나 속성에 주입될 수 있습니다. 또한 생성자 매개변수나 속성의 타입을 분석하여 서버 컨테이너에서 관리하는 싱글톤 인스턴스를 생성합니다.\n\n하지만 `@Injectable` 데코레이터가 없어도 주입은 여전히 가능합니다. 다만 `@Injectable` 데코레이터가 표시되지 않으면 클래스는 매번 기본 생성자를 통해 단순히 인스턴스화되며 서버 컨테이너에서 관리되지 않습니다.\n\n```ts\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository\u003cUser\u003e,\n    private readonly discoveryService: DiscoveryService,\n  ) {}\n\n  async create(createUserDto: CreateUserDto) {\n    const safedUserDto = createUserDto as Record\u003cstring, any\u003e;\n    if (safedUserDto.role) {\n      throw new BadRequestException(\"'role' 속성은 입력할 수 없습니다.\");\n    }\n\n    const newUser = await this.userRepository.create(createUserDto);\n    const res = await this.userRepository.save(newUser);\n\n    return ResultUtils.success(\"사용자 생성 성공.\", res);\n  }\n\n  async validateUser(loginUserDto: LoginUserDto): Promise\u003cUser\u003e {\n    const { username, password } = loginUserDto;\n\n    const user = await this.userRepository\n      .createQueryBuilder(\"user\")\n      .select()\n      .where(\"user.username = :username\", {\n        username,\n      })\n      .getOne();\n\n    if (!user) {\n      throw new BadRequestException(\"사용자가 존재하지 않습니다.\");\n    }\n\n    const isPasswordValid = await bcrypt.compare(password, user.password);\n    if (!isPasswordValid) {\n      throw new BadRequestException(\"비밀번호가 일치하지 않습니다.\");\n    }\n\n    return user;\n  }\n\n  async getUser(ip: string) {\n    const user = await this.userRepository.find();\n    return ResultUtils.success(\"사용자 조회 성공\", {\n      user,\n      ip,\n    });\n  }\n}\n```\n\n강조된 싱글톤 인스턴스는 단 하나의 인스턴스만 생성된다는 의미입니다. 즉, 컨트롤러나 `Injectable` 클래스에 주입될 때마다 정확히 같은 인스턴스가 주입됩니다.\n\n기본적으로 생성자 기반 주입이 권장되지만, 원한다면 속성 기반 주입도 사용할 수 있습니다.\n\n```ts\n@Injectable()\nexport class UserService {\n  @Autowired()\n  private readonly discoveryService!: DiscoveryService;\n}\n```\n\n하지만 속성 기반 주입은 생성자 기반 주입보다 늦게 주입되므로 생성자 기반 주입이 권장됩니다.\n\n또한 `@InjectRepository`는 현재 생성자에서만 주입되므로 속성 기반 주입을 사용할 때 주의하세요.\n\n[▲ 목차로 돌아가기](https://github.com/biud436/stingerloom#%EC%82%AC%EC%9A%A9%EB%B2%95)\n\n## 예외 필터와 실행 컨텍스트\n\n예외 필터는 오류를 처리하고 재정의할 수 있는 데코레이터입니다. `@ExceptionFilter` 데코레이터가 첨부되고, 데코레이터의 인수로 오류 클래스가 지정됩니다. 그 다음 오류 클래스에 해당하는 오류가 발생하면 `@Catch` 데코레이터가 있는 메소드가 실행됩니다.\n`@BeforeCatch` 데코레이터가 있는 메소드는 `@Catch` 데코레이터가 있는 메소드가 실행되기 전에 실행되고, `@AfterCatch` 데코레이터가 있는 메소드는 `@Catch` 데코레이터가 있는 메소드가 실행된 후에 실행됩니다.\n\n```ts\n@ExceptionFilter(InternalServerException)\nexport class InternalErrorFilter implements Filter {\n  private readonly logger = new Logger();\n\n  @BeforeCatch()\n  public beforeCatch() {\n    this.logger.info(\"catch 전\");\n  }\n\n  @Catch()\n  public catch(error: any) {\n    this.logger.info(\"[내부 서버 오류] \" + error.message);\n\n    return {\n      message: error.message,\n      status: error.status,\n      result: \"failure\",\n    };\n  }\n\n  @AfterCatch()\n  public afterCatch() {\n    this.logger.info(\"catch 후\");\n  }\n}\n```\n\n이렇게 하면 다음과 같이 출력됩니다:\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/biud436/custom-server-framework/assets/13586185/998fe1e3-f705-4a9c-a453-7179f42fc770\" /\u003e\n\u003c/p\u003e\n\n예외 메소드들은 `@BeforeCatch -\u003e @Catch -\u003e @AfterCatch` 순서로 실행됩니다. 각 예외 컨텍스트는 예외 처리 클래스당 하나의 인스턴스를 공유하는 공유 인스턴스입니다.\n\n[▲ 목차로 돌아가기](https://github.com/biud436/stingerloom#%EC%82%AC%EC%9A%A9%EB%B2%95)\n\n## 데이터베이스 트랜잭션 처리\n\nStingerLoom은 트랜잭션을 처리하기 위한 `@Transactional` 데코레이터를 지원합니다.\n\nSpring에서 영감을 받아, 이 데코레이터의 기본 트랜잭션 격리 수준은 `REPEATABLE READ`입니다.\n\n트랜잭션 격리 수준은 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션의 변경사항을 볼 수 있는 수준을 의미합니다.\n\n주요 수준은 네 가지입니다: `READ UNCOMMITTED`, `READ COMMITTED`, `REPEATABLE READ`, `SERIALIZABLE`.\n\n`@Transactional` 기능은 현재 `@Injectable` 데코레이터가 있는 클래스에만 적용됩니다.\n\n또한 트랜잭션 처리를 위해서는 효율적인 검색을 위해 클래스에 `@TransactionalZone` 데코레이터가 표시되어야 합니다.\n\n`@TransactionalZone` 데코레이터는 트랜잭션 처리를 위해 `EntityManager`와 `QueryRunner`를 주입할 메소드들을 찾아 트랜잭션 처리를 수행합니다.\n\n다음은 트랜잭션을 처리하는 간단한 예제입니다.\n\n```ts\n@TransactionalZone()\n@Injectable()\nexport class AuthService {\n  constructor(private readonly userService: UserService) {}\n\n  @Transactional()\n  async checkTransaction2() {\n    const users = await this.userService.findAll();\n\n    return ResultUtils.success(\"트랜잭션 확인됨.\", {\n      users: plainToClass(User, users),\n    });\n  }\n\n  @BeforeTransaction()\n  async beforeTransaction(txId: string) {\n    // 이 코드는 트랜잭션이 시작되기 전에 실행됩니다.\n  }\n\n  @AfterTransaction()\n  async afterTransaction(txId: string) {\n    // 이 코드는 트랜잭션이 끝난 후에 실행됩니다.\n  }\n\n  @Commit()\n  async commit(txId: string) {\n    // 이 코드는 트랜잭션이 커밋된 후에 실행됩니다.\n  }\n\n  @Rollback()\n  async rollback(txId: string, error: any) {\n    // 이 코드는 트랜잭션이 롤백된 후에 실행됩니다.\n    // 이 메소드는 오류가 발생했을 때만 실행됩니다.\n  }\n\n  @Transactional({\n    rollback: () =\u003e new Exception(\"트랜잭션이 롤백되었습니다\", 500),\n  })\n  async rollbackCheck() {\n    const user = await this.userService.findOneByPk(\"test\");\n\n    return ResultUtils.success(\"롤백 테스트\", {\n      user,\n    });\n  }\n}\n```\n\n예제에서 볼 수 있듯이 매우 간단합니다. 반환까지 오류가 발생하지 않으면 트랜잭션이 성공적으로 커밋됩니다.\n\n다음은 또 다른 예제인 사용자 등록 예제입니다.\n\n```ts\n@TransactionalZone()\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository\u003cUser\u003e,\n    private readonly discoveryService: DiscoveryService,\n  ) {}\n\n  @Transactional()\n  async create(createUserDto: CreateUserDto) {\n    const safedUserDto = createUserDto as Record\u003cstring, any\u003e;\n    if (safedUserDto.role) {\n      throw new BadRequestException(\"'role' 속성은 입력할 수 없습니다.\");\n    }\n\n    const newUser = this.userRepository.create(createUserDto);\n\n    const res = await this.userRepository.save(newUser);\n\n    return ResultUtils.success(\"사용자 생성 성공.\", res);\n  }\n\n  // 생략...\n}\n```\n\n중간에 오류 처리 로직이 보일 것입니다. 간단하게 생각할 수 있습니다. 위 코드에서 오류가 발생하면 트랜잭션이 자동으로 롤백됩니다.\n\n롤백 후 특정 코드를 실행하고 싶다면 다음과 같이 할 수 있습니다.\n\n```ts\n    @Rollback()\n    async rollback(txId: string, error: any) {\n        // 이 코드는 트랜잭션이 롤백된 후에 실행됩니다.\n        // 이 메소드는 오류가 발생했을 때만 실행됩니다.\n    }\n```\n\n`@Rollback()` 데코레이터를 첨부하면, 메소드의 첫 번째 인수는 트랜잭션 ID이고 두 번째 인수는 오류 객체입니다.\n\n또는 트랜잭션이 롤백될 때 특정 오류를 반환하고 싶다면 다음과 같이 할 수 있습니다.\n\n```ts\n    @Transactional({\n        rollback: () =\u003e new Exception(\"트랜잭션이 롤백되었습니다\", 500),\n    })\n    async rollbackCheck() {\n        const user = await this.userService.findOneByPk(\"test\");\n\n        return ResultUtils.success(\"롤백 테스트\", {\n            user,\n        });\n    }\n```\n\n트랜잭션 ID는 실제 트랜잭션 ID가 아니라 서버에서 관리하는 트랜잭션 ID입니다.\n\n```ts\n@Injectable()\n@TransactionalZone()\nexport class GameMapService {\n  constructor(\n    @InjectRepository(GameMap)\n    private readonly gameMapRepository: Repository\u003cGameMap\u003e,\n    @InjectRepository(User)\n    private readonly userRepository: Repository\u003cUser\u003e,\n  ) {}\n\n  @Transactional()\n  async createGameMap() {\n    await this.userRepository.clear();\n\n    const qb = this.gameMapRepository.createQueryBuilder(\"gameMap\");\n    const maps = await qb\n      .select()\n      .leftJoinAndSelect(\"gameMap.users\", \"user\")\n      .getMany();\n\n    return maps;\n  }\n\n  @Commit()\n  async commitOk(txId: string) {\n    console.log(\"커밋 완료:\", txId);\n  }\n}\n```\n\n[▲ 목차로 돌아가기](https://github.com/biud436/stingerloom#%EC%82%AC%EC%9A%A9%EB%B2%95)\n\n## 권한 처리\n\nStingerLoom은 세션 기반 인증을 지원합니다.\n\nSessionObject를 상속하는 클래스는 세션 객체로 사용할 수 있습니다.\n\n```ts\n@Controller(\"/auth\")\nexport class AuthController {\n  constructor(private readonly authService: AuthService) {}\n\n  @Post(\"/login\")\n  async login(\n    @Session() session: SessionObject,\n    @Body() loginUserDto: LoginUserDto,\n  ) {\n    return await this.authService.login(session, loginUserDto);\n  }\n}\n```\n\n권한 처리는 아직 예제에서 구현되지 않았습니다.\n\n권한 처리를 위해서는 인증 가드(AuthGuard)의 개념과 권한 처리에 필요한 역할(role)의 개념을 구현해야 합니다.\n\n### 세션 처리\n\n더 실용적인 예제는 다음과 같습니다.\n\n```ts\n@Injectable()\nexport class AuthService {\n  @Autowired()\n  userService!: UserService;\n\n  async login(session: SessionObject, loginUserDto: LoginUserDto) {\n    const user = await this.userService.validateUser(loginUserDto);\n    session.authenticated = true;\n    session.user = user;\n\n    return ResultUtils.successWrap({\n      message: \"로그인 성공.\",\n      result: \"success\",\n      data: session.user,\n    });\n  }\n\n  async checkSession(session: SessionObject) {\n    return ResultUtils.success(\"세션 인증 성공\", {\n      authenticated: session.authenticated,\n      user: session.user,\n    });\n  }\n}\n```\n\n현재 버전에서는 위와 같이 세션 객체를 사용하여 인증을 구현할 수 있습니다.\n\n### 세션 가드\n\n세션 인증은 `@Session()` 데코레이터를 사용하여 세션 객체를 주입하고 세션 인증을 처리하는 SessionGuard를 추가하여 처리할 수 있습니다.\n\n코드는 다음과 같습니다.\n\n```ts\n@Injectable()\nexport class SessionGuard implements Guard {\n  canActivate(context: ServerContext): Promise\u003cboolean\u003e | boolean {\n    const req = context.req;\n    const session = req.session as SessionObject;\n\n    if (!session) {\n      return false;\n    }\n\n    if (!session.authenticated) {\n      return false;\n    }\n\n    return true;\n  }\n}\n```\n\n위 가드를 프로바이더에 추가하고 다음과 같이 컨트롤러나 라우터에 첨부하여 사용합니다.\n\n```ts\n@Controller(\"/auth\")\nexport class AuthController {\n  constructor(private readonly authService: AuthService) {}\n\n  @Get(\"/session-guard\")\n  @UseGuard(SessionGuard)\n  async checkSessionGuard(@Session() session: SessionObject) {\n    return ResultUtils.success(\"세션 가드 통과\", session);\n  }\n}\n```\n\n이렇게 하면 세션 인증을 통과한 로그인된 사용자에게만 라우터가 실행됩니다.\n\n인증되지 않은 사용자에게는 401 오류가 발생합니다.\n\n[▲ 목차로 돌아가기](https://github.com/biud436/stingerloom#%EC%82%AC%EC%9A%A9%EB%B2%95)\n\n## 커스텀 매개변수 데코레이터\n\n`createCustomParamDecorator` 함수를 사용하여 고유한 `ParameterDecorator`를 만들 수 있습니다.\n\n다음은 세션에서 사용자 정보와 사용자 ID를 가져오는 예제입니다.\n\n```ts\nexport const User = createCustomParamDecorator((data, context) =\u003e {\n  const req = context.req;\n  const session = req.session as SessionObject;\n\n  if (!session) {\n    return null;\n  }\n\n  return session.user;\n});\n```\n\n사용자 ID는 다음과 같이 가져올 수 있습니다.\n\n```ts\nexport const UserId = createCustomParamDecorator((data, context) =\u003e {\n  const req = context.req;\n  const session = req.session as SessionObject;\n\n  if (!session) {\n    return null;\n  }\n\n  return session.user.id;\n});\n```\n\n최종 사용법은 다음과 같습니다.\n\n```ts\n@Controller(\"/auth\")\nexport class AuthController {\n  constructor(private readonly authService: AuthService) {}\n\n  @Get(\"/session-guard\")\n  @UseGuard(SessionGuard)\n  async checkSessionGuard(\n    @Session() session: SessionObject,\n    @User() user: any,\n    @UserId() userId: string,\n  ) {\n    return ResultUtils.success(\"세션 가드 통과\", {\n      user,\n      userId,\n    });\n  }\n}\n```\n\n쿼리했을 때 결과는 다음과 같이 출력됩니다.\n\n```json\n{\n  \"message\": \"세션 가드 통과\",\n  \"result\": \"success\",\n  \"data\": {\n    \"user\": {\n      \"id\": \"4500949a-3855-42d4-a4d0-a7f0e81c4054\",\n      \"username\": \"abcd\",\n      \"role\": \"user\",\n      \"createdAt\": \"2023-08-28T09:22:37.144Z\",\n      \"updatedAt\": \"2023-08-28T09:22:37.144Z\"\n    },\n    \"userId\": \"4500949a-3855-42d4-a4d0-a7f0e81c4054\"\n  }\n}\n```\n\n## 템플릿 엔진\n\n템플릿 엔진을 사용하면 `@View` 데코레이터를 사용하여 HTML 파일을 렌더링할 수 있습니다.\n\n먼저 필요한 패키지를 설치해야 합니다. 터미널에서 다음 명령어를 입력합니다.\n\n```bash\nyarn add @fastify/view handlebars\n```\n\n`bootstrap.ts` 파일에서 템플릿 엔진을 미들웨어로 등록하면 모든 컨트롤러에서 템플릿 엔진을 사용할 수 있습니다.\n\n```ts\n    /**\n     * 미들웨어 추가.\n     *\n     * @returns\n     */\n    protected applyMiddlewares(): this {\n        const app = this.app;\n\n        app.register(fastifyCookie, {\n            secret: process.env.COOKIE_SECRET,\n            hook: \"onRequest\",\n        });\n\n        app.register(fastifyFormdody);\n        app.register(fastifySession, {\n            secret: process.env.SESSION_SECRET,\n        });\n\n        app.register(view, {\n            engine: {\n                handlebars,\n            },\n            root: `${__dirname}/views`,\n            includeViewExtension: true,\n        });\n\n        return this;\n    }\n```\n\n컨트롤러에서는 `@View` 데코레이터를 사용하여 템플릿에 매핑할 수 있습니다.\n\n```ts\n@Controller(\"/\")\nexport class AppController {\n  /**\n   * 로그인 페이지를 표시합니다.\n   */\n  @View(\"login\")\n  login() {\n    return {\n      username: \"사용자명\",\n      password: \"비밀번호\",\n    };\n  }\n\n  /**\n   * 이 페이지는 로그인된 사용자만 접근할 수 있습니다.\n   */\n  @View(\"memberInfo\")\n  @UseGuard(SessionGuard)\n  async memberInfo(@User() user: UserEntity) {\n    return {\n      username: user.username,\n    };\n  }\n}\n```\n\n뷰의 경로와 라우트가 다른 경우 다음과 같이 `@Render` 데코레이터를 사용하여 템플릿 리소스의 경로를 지정할 수 있습니다.\n\n```ts\n@Controller(\"/\")\nexport class AppController {\n  /**\n   * 이 페이지는 로그인된 사용자만 접근할 수 있습니다.\n   */\n  @Get(\"/info\")\n  @Render(\"memberInfo\")\n  @UseGuard(SessionGuard)\n  async memberInfo(@User() user: UserEntity) {\n    return {\n      username: user.username,\n    };\n  }\n}\n```\n\n필요한 매개변수를 반환하면 각 템플릿 엔진이 처리할 수 있습니다.\n\n다음은 `handlebars` 템플릿 엔진을 사용한 로그인 예제입니다.\n\n```hbs\n\u003c!-- login.hbs --\u003e\n\u003chtml lang=\"ko\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"UTF-8\" /\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /\u003e\n    \u003ctitle\u003e템플릿 렌더링 예제\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv\u003e\n      \u003ch2\u003e로그인\u003c/h2\u003e\n      \u003cform action=\"/auth/login\" method=\"post\"\u003e\n        \u003cinput type=\"text\" name=\"username\" placeholder=\"{{username}}\" /\u003e\n        \u003cinput type=\"password\" name=\"password\" placeholder=\"{{password}}\" /\u003e\n        \u003cinput type=\"submit\" value=\"로그인\" /\u003e\n      \u003c/form\u003e\n    \u003c/div\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n다음은 세션 정보를 표시하는 예제입니다.\n\n```hbs\n\u003c!-- memberInfo.hbs --\u003e\n\u003chtml lang=\"ko\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"UTF-8\" /\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /\u003e\n    \u003ctitle\u003e세션 예제\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cp\u003e로그인된 사용자 정보는 \u003cstrong\u003e{{username}}\u003c/strong\u003e입니다.\u003c/p\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n[▲ 목차로 돌아가기](https://github.com/biud436/stingerloom#%EC%82%AC%EC%9A%A9%EB%B2%95)\n\n## ORM\n\nORM은 객체와 관계형 데이터베이스 간의 매핑을 지원하는 도구입니다.\n\nStingerLoom은 자체 ORM을 제공하므로 타사 라이브러리 없이도 데이터베이스에 접근할 수 있습니다.\n\n`@Entity` 데코레이터를 사용하여 엔티티를 정의할 수 있습니다. 엔티티는 데이터베이스의 테이블에 매핑됩니다. synchronize 옵션을 사용하여 작성된 엔티티를 데이터베이스와 동기화할 수 있습니다.\n\n`@stingerloom/orm/decorators` 패키지에서 제공하는 `@Column`, `@Entity`, `@Index`를 사용하여 엔티티를 정의할 수 있습니다.\n\n```ts\n@Entity()\nclass MyNode {\n  @PrimaryGeneratedColumn()\n  id!: number;\n\n  @Column({\n    length: 255,\n    nullable: false,\n    type: \"varchar\",\n  })\n  name!: string;\n\n  @Column({\n    length: 255,\n    nullable: false,\n    type: \"varchar\",\n  })\n  type!: string;\n\n  @Column({\n    length: 255,\n    nullable: false,\n    type: \"varchar\",\n  })\n  @Index()\n  description!: string;\n}\n```\n\n리포지토리를 통해 데이터베이스에 접근할 수 있습니다. 리포지토리를 생성하는 방법은 두 가지입니다: `EntityManager`를 주입하여 `getRepository` 메소드를 사용하는 방법과 `@InjectRepository` 데코레이터를 사용하는 방법입니다. 후자의 방법은 TypeORM에서만 지원되며 커스텀 ORM에서는 아직 지원되지 않습니다. 향후 두 ORM에서 모두 사용할 수 있도록 하는 방법을 고려할 것입니다.\n\n```ts\n@Injectable()\nclass MyNodeService {\n    constructor(\n        @InjectEntityManager()\n        private readonly entityManager: EntityManager,\n    )\n\n    async findOne(id: number): Promise\u003cMyNode\u003e {\n\n        // MyNode 엔티티에 대한 리포지토리 가져오기\n        const myNodeRepository = this.entityManager.getRepository(MyNode);\n\n        // id가 1인 노드 찾기\n        const myNode = await myNodeRepository.findOne({\n            where: {\n                id\n            }\n        });\n\n        if (!myNode) {\n            throw new NotFoundException(\"노드를 찾을 수 없습니다.\");\n        }\n\n        return myNode;\n    }\n}\n```\n\n위와 같이 `@InjectEntityManager` 데코레이터를 사용하여 `EntityManager`를 주입하고 리포지토리를 가져올 수 있습니다.\n\n리포지토리 패턴을 통해 데이터베이스에 접근할 수 있습니다.\n\n[▲ 목차로 돌아가기](https://github.com/biud436/stingerloom#%EC%82%AC%EC%9A%A9%EB%B2%95)\n\n## CLI\n\nStingerLoom은 CLI를 지원합니다. CLI를 통해 모듈 파일을 쉽게 생성할 수 있습니다. 현재로서는 꽤 제한적이며, 추가 연구를 통해 Typescript 컴파일러를 사용하여 모듈 정보를 읽는 로직을 추가해야 한다고 생각합니다.\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/biud436/stingerloom/assets/13586185/67bd938e-d882-4119-9912-9a62b56c73a4\" /\u003e\n\u003c/p\u003e\n\n새로운 컨트롤러와 서비스를 자동으로 생성하려면 다음 명령어를 사용할 수 있습니다.\n\n```bash\nyarn cli\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbiud436%2Fstingerloom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbiud436%2Fstingerloom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbiud436%2Fstingerloom/lists"}