ํ—ฌ์Šค์ฒดํฌ/์Šค์ผ€์ค„๋Ÿฌ๋กœ ์„œ๋ฒ„ ๋กœ๊ทธ ๋„์šฐ๊ธฐ

ํ—ฌ์Šค? ๋ผ์ž‡์›จ์ž‡ ๋ฒ ์ด๋ฒ !

Profile Picture
JangAJang
2023-12-13

์›๋ณธ

ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ, ์„œ๋ฒ„์˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š” ํ”Œ๋กœ์šฐ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ–ˆ๋‹ค.

์‰˜์Šคํฌ๋ฆฝํŠธ๋ฅผ crontab์„ ์ด์šฉํ•ด์„œ slack์— ์„œ๋ฒ„ ์ƒํƒœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ๋ฐฉ๋ฒ•์„ ๋งŒ๋“ค์—ˆ์ง€๋งŒ,

์ด๊ฑธ๋กœ๋Š” ๋ถ€์กฑํ–ˆ๋‹ค.

๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๊ฒƒ์€ ์ผ์ข…์˜ ์„œ๋ฒ„์‹ค์ด์—ˆ๋‹ค.

์„œ๋ฒ„ ํ•˜๋‚˜์—์„œ DB, Main์„œ๋ฒ„๋ฅผ ๊ณ„์†ํ•ด์„œ ํ™•์ธํ•˜๋Š” ๋กœ์ง์„ ์›ํ–ˆ๋‹ค.

๋กœ์ปฌ์—์„œ๋„ ์‹คํ–‰์‹œ์ผœ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ๊ณ„์† ํ™•์ธํ•˜๊ณ , ๋‹ค๋ฅธ ์„œ๋ฒ„์—์„œ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹œ๊ทธ๋„๋ง์„ ์ฒ˜๋ฆฌํ–ˆ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์„ ํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ–ˆ์„๊นŒ?

ํฌ๊ฒŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€, ๋ชจ๋“ˆ ๊ตฌํ˜„, ํ”„๋กœ๋ฐ”์ด๋” ๊ตฌํ˜„์œผ๋กœ ๋‚˜๋ˆ„์–ด๋ณด์•˜๋‹ค.

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€

์ด ๋™์ž‘์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ, 3๊ฐ€์ง€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

npm install @nestjs/terminus
npm install @nestjs/schedule
npm install @nestjs/axios

์ด๋ ‡๊ฒŒ terminus(ํ—ฌ์Šค์ฒดํฌ๊ธฐ๋Šฅ), schedule(crontab๊ธฐ๋Šฅ), axios(์™ธ๋ถ€ ip์š”์ฒญ) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„ฃ์–ด์ฃผ๊ณ  ์‹œ์ž‘ํ–ˆ๋‹ค.

๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ๋˜์—ˆ๋Š”์ง€ ํ•˜๋‚˜์”ฉ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค๋ฉด์„œ ์ •๋ฆฌํ–ˆ๋‹ค.

๋ชจ๋“ˆ ๊ตฌํ˜„

const logger = new LoggerService('healthcheck');

@Module({
  imports: [TerminusModule, HttpModule, ScheduleModule.forRoot()],
  providers: [
    HealthCheckScheduler,
    HealthCheckService,
    TypeOrmHealthIndicator,
    HealthCheckExecutor,
    {
      provide: 'TERMINUS_ERROR_LOGGER',
      useValue: Logger.error.bind(logger),
    },
    {
      provide: 'TERMINUS_LOGGER',
      useValue: logger,
    },
  ],
  controllers: [],
})
export class HealthModule {}

HealthModule์„ ๋”ฐ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค.

ํ—ฌ์Šค์ฒดํฌ๊ธฐ๋Šฅ์„ ์œ„ํ•œ TerminusModule, axios๋ฅผ ์œ„ํ•œ HttpModule, ์Šค์ผ€์ค„๋ง์„ ์œ„ํ•œ ScheduleModule.forRoot()๋ฅผ importํ–ˆ๋‹ค.

providers๋กœ๋Š”

  • HealthCheckScheduler
    • ๋‚ด๊ฐ€ ๋งŒ๋“ค ์Šค์ผ€์ค„๋Ÿฌ์ด๋‹ค.
  • HealthCheckService
    • HealthCheckExecutor, ErrorLogger, Logger๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. TERMINUS_ERROR_LOGGER, TERMINUS_LOGGER๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ด ๋•Œ ๋กœ๊ฑฐ๋“ค์€ ๋‚ด๊ฐ€ ๋งŒ๋“  ๋กœ๊ฑฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์–ด์„œ ์ปค์Šคํ…€ ๋กœ๊ฑฐ๋ฅผ ๋“ฑ๋ก์‹œ์ผฐ๋‹ค.
    • ์ปค์Šคํ…€ ๋กœ๊ฑฐ๋ฅผ ์œ„ํ•ด provide & useValue๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ์–ด๋–ค ํ”„๋กœ๋ฐ”์ด๋”/์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๋ฐ›์•„์•ผ ํ•˜๋Š” ํŠน์ • ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • TypeOrmHealthIndicator : ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํƒœ๊ฐ€ ์‚ด์•„์žˆ๋Š”์ง€ ๋”ฐ๋กœ ping์„ ํ™•์ธํ•˜๋Š” ๋ฒ•์ด ์žˆ์—ˆ๋‹ค. ์ด๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ด๋ณด์•˜๋‹ค.

ํ”„๋กœ๋ฐ”์ด๋” ๊ตฌํ˜„

@Injectable()
export class HealthCheckScheduler {
  private readonly dbLogger = new LoggerService('DB');
  private readonly main = new LoggerService('MAIN');

  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private http: HttpHealthIndicator,
  ) {}

  @Cron('0 * * * * *')
  async checkDB() {
    try {
      await this.health.check([
        async () => await this.db.pingCheck('database'),
      ]);
      this.dbLogger.log(`${new Date()} : ON`);
    } catch (error) {
      this.dbLogger.error(`${new Date()} : OFF`, error.stack);
    }
  }

  @Cron('0 * * * * *')
  async checkMain() {
    try {
      await this.health.check([
        async () =>
          await this.http.pingCheck('gomterview-main', process.env.BE_URL),
      ]);
      this.main.log(`${new Date()} : ON`);
    } catch (error) {
      this.main.error(`${new Date()} : OFF`, error.stack);
    }
  }
}

์ด๋Ÿฐ์‹์œผ๋กœ ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค. ํ•˜๋‚˜์”ฉ ์ •๋ฆฌ๋ฅผ ํ•ด๋ณด์ž.

  • TypeOrmHealthIndicator
    • ์ด์ „์— ์ฃผ์ž…์‹œํ‚จ ํ”„๋กœ๋ฐ”์ด๋”์ด๋‹ค.
    • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š”๋ฐ ์ด์šฉํ•œ๋‹ค.
  • HttpHealthIndicator
    • HTTP ์š”์ฒญ์„ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š”๋ฐ ์ด์šฉํ•œ๋‹ค.
  • HealthCheckService
    • ์„œ๋ฒ„์˜ healthCheck(์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต ์ƒํƒœ๋“ฑ์„ ํ™•์ธ)์ด๋‚˜ pingCheck(ping์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต ์—ฌ๋ถ€ ํ™•์ธ)์— ์ด์šฉํ•œ๋‹ค.
  • @Cron : ํฌ๋ก ํƒญ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์ฃผ๊ธฐ์‹œ๊ฐ„์„ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์•ž ์ž๋ฆฌ๋ถ€ํ„ฐ ์ดˆ-๋ถ„-์‹œ-์ผ-์›”-๋…„ ์œผ๋กœ ์ฃผ๊ธฐ๋ฅผ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์ง€๊ธˆ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š”, ๋งค๋…„ ๋งค์›” ๋งค์ผ ๋งค์‹œ ๋งค๋ถ„ 0์ดˆ๋งˆ๋‹ค(์ฆ‰ 1๋ถ„๋งˆ๋‹ค) ์ฃผ๊ธฐ์ ์œผ๋กœ ํ•ด๋‹น ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค.
  • health.check([cb()])
    • ์ฝœ๋ฐฑ ๋กœ์ง์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด ์š”์ฒญ ์‘๋‹ต์„ ํ™•์ธํ•œ๋‹ค. ์—†๋‹ค๋ฉด ์˜ˆ์™ธ์ฒ˜๋ฆฌํ•˜๊ณ , ์ด ๋•Œ TERMINUS_ERROR_LOGGER๋กœ ๋กœ๊น…๋˜๋Š”๊ฒŒ ๊ธฐ๋ณธ์ด๋ฉฐ, ์ด๋ฅผ try-catchํ•ด์„œ ๋‚ด ์ปค์Šคํ…€ ๋กœ๊ทธ๋กœ ์‘๋‹ต์„ ๋ณด๋‚ด๋Š”๋ฐ์— ์‚ฌ์šฉํ–ˆ๋‹ค.

์ฆ‰, ํ•ด๋‹น ๋กœ์ง๋“ค์€ Main์„œ๋ฒ„์™€, DB์„œ๋ฒ„์— ํ•‘ ์ฒดํฌ๋ฅผ ํ•˜๊ณ  ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ปค์Šคํ…€ ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅ์‹œํ‚ค๋Š” ๊ฒƒ์„ ์ฃผ๊ธฐํ™” ์‹œํ‚จ ๊ฒƒ์ด๋‹ค.

์ •๋ฆฌ

์ด๋Ÿฐ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ–ˆ์„ ๋•Œ, ๋งค ๋ถ„๋งˆ๋‹ค ์ƒํƒœ๋ฅผ ๋กœ๊น…์ฒ˜๋ฆฌํ•ด์ฃผ์—ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด ์›ํ•˜๋Š” ๋กœ๊ทธ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

ํ˜„์žฌ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ฐฑ์—”๋“œ๋Š” 3๊ฐœ์˜ ์„œ๋ฒ„๋ฅผ ์“ด๋‹ค(์จ๋ด์•ผ ์…‹ ๋‹ค ํ”„๋ฆฌํ‹ฐ์–ด ec2 ์ธ์Šคํ„ด์Šค์ด๋‹ค)

  • Main ์„œ๋ฒ„
  • Dev ์„œ๋ฒ„(ํ”„๋ก ํŠธ/๋ฐฑ์˜ ๊ฐœ๋ฐœ์šฉ)
  • DB ์„œ๋ฒ„

์ด ์ƒํ™ฉ์—์„œ Dev๊ฐ€ ํ•  ์ผ์ด ํ˜„์žฌ๋Š” ๋ณ„๋กœ ์—†๋‹ค(ํ”„๋ก ํŠธ 3๋ช…, ๋ฐฑ 2๋ช…์ด ์ „๋ถ€์ธ ํŒ€์ด๋‹ค)

Dev๊ฐ€ ์กฐ๊ธˆ ๋” ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์„œ๋ฒ„๊ฐ€ ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค๋Š” ์ทจ์ง€๋กœ ๋กœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

์ด์ œ ์šฐ๋ฆฌ์˜ Dev ์„œ๋ฒ„๋Š” ๋ชจ๋“  ํ”„๋ก ํŠธ/๋ฐฑ์—์„œ ๊ณ„์†ํ•ด์„œ ๋กœ๊น…ํ•˜๋ฉฐ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋ฒ„๊ฐ€ ๋˜์—ˆ๋‹ค. (์ด์ œ์•ผ Dev ์„œ๋ฒ„๋„ ์ข€ ์“ธ๋ชจ๊ฐ€ ์ƒ๊ฒผ๋‹ค )

์ฆ‰ ๋ฉ”์ธ๊ณผ Dev๋Š” ๊ฐ™์€ ๊นƒํ—ˆ๋ธŒ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์“ฐ์ง€๋งŒ, Main์€ Health๊ฐ€ ์—†์ด, Dev๋Š” Health๋ฅผ ๊ฐ€์ง€๊ณ  ๋ฐฐํฌํ•˜๋Š” ์‹์œผ๋กœ ์ง„ํ–‰ํ–ˆ๋‹ค.

PS(ํ•œ๊ตญ๋ง๋ก  ์ถ”์‹ )

Main์—์„œ๋„ ๊ฐ™์€ LoggerService๊ฐ€ ์กด์žฌํ•˜๋Š”๋ฐ, ์ด๊ฑธ ์จ๋จน์–ด๋ณธ์ ์ด ์—†๋‹ค.

๊ทธ๋ž˜์„œ ์ด๊ฑธ ๊ฐ€์ง€๊ณ  ๋‘ ๊ฐ€์ง€ ๋กœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

  • ๋งค ์š”์ฒญ๋งˆ๋‹ค ์–ด๋Š api๋กœ ์š”์ฒญ์ด ์™”๋Š”์ง€
  • ์˜ˆ์™ธ ๋ฐœ์ƒ์‹œ์— ๋ฐœ์ƒํ•œ ์—”๋“œํฌ์ธํŠธ, ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๋•Œ์˜ ์—๋Ÿฌ์ฝ”๋“œ
async function bootstrap() {
  initializeTransactionalContext();
  const app = await NestFactory.create(AppModule, {
    abortOnError: true,
  });
  const expressApp = app.getHttpAdapter().getInstance();
  const logger = new LoggerService('traffic');
  ...
  expressApp.use((req, res, next) => {
    logger.info(req.url);
    next();
  });
  await app.listen(8080, '0.0.0.0');
}

์ด๋ ‡๊ฒŒ ๋กœ๊ฑฐ๋ฅผ ํ•˜๋‚˜ ๋‹ฌ์•„์„œ, ์–ด๋–ค api๊ฐ€ ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋Š”์ง€ ๋กœ๊ทธ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ–ˆ๋‹ค(์ด ๋•Œ logger๋Š” ์ถœ๋ ฅ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๋กœ์ปฌ์— ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ์ž‘์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ๊นŒ์ง€ ๋‹ด๊ฒจ์žˆ๋‹ค)

๋˜ํ•œ, ์˜ˆ์™ธ์ฒ˜๋ฆฌ์‹œ์— ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์ปค์Šคํ…€ ์˜ˆ์™ธ๊ฐ€ ์–ด๋””์—์„œ ํ„ฐ์ง€๋Š”์ง€ ํ™•์ธํ•˜๊ฒŒ ํ–ˆ๋‹ค.

class HttpCustomException extends HttpException {
  constructor(message: string, errorCode: string, status: number) {
    super({ message: message, errorCode: errorCode }, status);
    errorLogger.error(errorCode, super.stack);
  }
}

์ด๋Ÿฐ์‹์œผ๋กœ ์–ด๋–ค ์—”๋“œํฌ์ธํŠธ์—์„œ ์–ด๋–ค ์—๋Ÿฌ์ฝ”๋“œ๊ฐ€ ๋‚˜์™”๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์ด ๋•Œ stack๊นŒ์ง€ ๊ฐ™์ด ์ถœ๋ ฅํ•˜๊ฒŒ ํ–ˆ๋‹ค.

๋‹จ์ˆœํ•˜์ง€๋งŒ, ์˜๋„์น˜ ์•Š์€ ์ผ€์ด์Šค๋‚˜ ํ„ฐ์ง€๋ฉด ์•ˆ๋˜๋Š” ์˜ˆ์™ธ์ผ€์ด์Šค๋ฅผ ๋กœ๊น…ํ•˜๋Š”๋ฐ ์šฉ์ดํ•  ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. (์ „๋ถ€๋‹ค 500์œผ๋กœ ๋งŒ๋“ค์–ด ์ˆจ๊ฒจ๋‘๊ธด ํ–ˆ์ง€๋งŒ, errorCode๋ฅผ ํ†ตํ•ด ๊ฐœ๋ฐœ์ž๋งŒ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ์ˆ˜์ •ํ•ด๋‘์—ˆ๋‹ค)

์ด๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ , ์ด๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์€ ๋กœ๊ทธ๋ฅผ ๋ฉ”์ธ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ–ˆ๋‹ค.

๋ฌผ๋ก  ์™ธ๋ถ€ ํ”„๋กœ๊ทธ๋žจ๋“ฑ์„ ํ™œ์šฉํ•˜๋ฉด ๋” ์ˆ˜์›”ํ•˜๊ฒŒ ๋กœ๊น…์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ ์•Œ์ง€๋งŒ, ์•„์ง์€ ๋ถ€์ŠคํŠธ์บ ํ”„ ๊ธฐ๊ฐ„์ด์–ด์„œ ์ตœ๋Œ€ํ•œ ๋งŽ์€ ๊ฒƒ์„ ํ•™์Šตํ•ด์„œ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œํ•˜๊ณ ์žˆ๋‹ค.

๋ฉ”์ธ๊ณผ ๋ฐ๋ธŒ์—์„œ ๊ฐ์ž ์ž๊ธฐ๊ฐ€ ๋ณด์—ฌ์ค˜์•ผ ํ•  ๊ฒƒ๋“ค์ด ์žˆ๋‹ค๊ณ  ๋Š๋‚€๋‹ค.

๋ฉ”์ธ์—์„œ๋Š” ๋‚ด๊ฐ€ ์–ด๋–ค ์š”์ฒญ์„ ๋ฐ›์•˜๊ณ , ์–ด๋–ค ์—๋Ÿฌ๊ฐ€ ์žˆ๊ณ , ์–ด๋–ค ์ด์œ ์ธ์ง€ ์•Œ๋ ค์ค˜์•ผ ํ•˜๋ฉฐ,

dev์„œ๋ฒ„์—์„œ๋Š” ๋‹ค๋ฅธ ์„œ๋ฒ„๋“ค์˜ ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

์ด๋ฅผ ์œ„ํ•œ ๋กœ๊ทธ๋ฅผ ๋‹ฌ๊ณ ๋‚˜๋‹ˆ, ํ”„๋ก ํŠธ์—์„œ '๋ญ๊ฐ€ ์ด์ƒํ•ด์š”! ๋ญ๊ฐ€ ์•ˆ๋ผ์š”!'๋ฅผ ํ•˜๋Š” ๊ฒƒ ๋ณด๋‹จ, ๋” ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์ด ๋‚˜์™”๋‹ค.

๊ฐ•์˜์ค‘์—์„œ๋„, ๋ฉ˜ํ† ๋ง์ค‘์—์„œ๋„ ๋กœ๊น…์— ๋Œ€ํ•ด์„œ๋Š” ๋งŽ์€ ๋กœ๊ทธ๋ฅผ ๋งŒ๋“ค์–ด๋‘๊ณ , ๊ทธ ์ค‘์—์„œ ํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ์ฐพ์•„๊ฐ€๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค๊ณ  ํ•œ๋‹ค.

์ด๋ฅผ ์œ„ํ•œ ์‹œ์ž‘์œผ๋กœ ํ•˜๋‚˜์”ฉ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๋นผ๋Š” ์‹์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค.

๋‹ค๋“ค ๋””๋ฒ„๊น… ์ž˜ํ•˜๋Š” ๊ทธ๋‚ ๊นŒ์ง€ ๋กœ๊ทธ๋ฅผ ๋ณด์ž!!

(์‚ฌ์‹ค ํ„ฐ๋ฏธ๋„๋กœ ๋กœ๊ทธ ๋ณด๋‹ˆ๊นŒ ์ง„์งœ the love๋‹ค..ํ•˜ํ•˜)

๊ทธ๋Ÿผ...twenty thousand...๐Ÿ”ฅ

ยฉ 2024 Adultlee. All rights reserved.Made with โค by ์ด์„ฑ์ธ