[TypeScript] 데코레이터(Decorator)

bolee·2023년 1월 15일
3

NestJS

목록 보기
3/3
post-thumbnail

데코레이터

데코레이터(Decorator)는 타입스크립트에서는 실험적으로 도입된 기능이다. 타입스크립트의 데코레이터는 자바(Java)의 어노테이션이나 파이썬(Python)의 데코레이터와 유사한 기능을 한다.그러나 자바 어노테이션은 컴파일 타임에 상관있지만 타입스크립트 데코레이터는 컴파일 타임에는 상관하지 않기 때문에, 오히려 파이썬(Python)의 데코레이터와 거의 비슷하다고 볼 수 있다.

데코레이터는 일종의 함수로 코드 조각을 장식해주는 역할을 하며, 타입스크립트에서는 그 기능을 함수로 구현하는 것이다.
메소드, 클래스, 프로퍼티, 파라미터 등 위에 @로 시작하는 데코레이터를 선언하여 장식하면 코드가 실행(런타임)이 되면 데코레이터 함수가 실행되어, 장식한 멤버를 보다 강력하게 만들어주는 것이다.

데코레이터를 잘 사용하면 횡단 관심사(cross-cutting concern)를 분리하여 관점 지향 프로그래밍을 적용한 코드를 작성할 수 있다.
또한 데코레이터 패턴은 클래스를 수정하지 않고 클래스의 멤버들의 정의를 수정 및 기능을 확장할 수 있는 구조적 패턴의 하나로, 데코레이터 패턴을 사용하면 전체 기능에 신경 쓰지 않고 특정 인스턴스에 초점을 맞출 수 있다.

지금까지의 데코레이터의 특징 정리하면 다음과 같다.

  • 데코레이터는 클래스 선언, 메서드(method), 접근자(accessor), 프로퍼티(property) 또는 매개변수(parameter)에 첨부할 수 있는 특수한 종류의 선언이다.
  • 데코레이터 함수에는 target(현재타겟), key(속성이름), descriptor(설명)가 전달된다. (단, 어떤 멤버를 장식했느냐에 따라 인수가 달라짐)
  • 메소드나 클래스 인스턴스가 만들어지는 런타임에 실행된다. 즉, 매번 실행되지 않는다.
  • 데코레이터는 클래스 또는 클래스 내부의 생성자, 프로퍼티, 접근자, 메서드, 그리고 매개변수에만 장식될 수 있다.

데코레이터 설정

타입스크립트의 데코레이터는 정식 지원하는 문법 기능이 아닌 실험적인(experimental) 기능이다.
타입스크립트에서 데코레이터를 사용하기 위해선 tsconfig.json 파일에서 experimentalDecorators 옵션을 활성화 해야 한다.

// tsconfig.json
{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    ...
  }
}

데코레이터 함수 선언과 데코레이터 팩토리

데코레이터 함수 선언

데코레이터를 사용하기 위해서는 테코레이터 함수를 선언해야 한다. 데코레이터 함수는 테코레이팅된 선언(데코레이터가 선언되는 클래스, 메서드 등)에 대한 정보와 함께 호출되는 함수여야 한다.

아래는 메서드 데코레이터의 예시이며 테코레이터 함수의 인자는 어떤 테코레이터 함수이냐에 따라 다르다.

function deco(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('데코레이터가 평가됨');
}

class TestClass {
  @deco()
  test() {
    console.log('함수 호출됨');
  }
}

const t = new TestClass();
t.test();
데코레이터가 평가됨
함수 호출됨

데코레이터 팩토리

데코레이터 팩토리 함수(decorator factory function)는 데코레이터 함수를 감싸는 래퍼(wrapper) 함수이다. 보통 데코레이터가 선언에 적용되는 방식을 원하는 대로 바꾸고 싶을 때 사용된다.

프로그래밍에서 함수에게 사용자가 인자를 전달할 수 있는 것과 유사하게, 데코레이터 함수 또한 팩토리를 사용해 사용자로부터 인자를 전달 받도록 설정할 수 있다.
즉, 데코레이터 팩토리는 사용자로부터 전달 받은 인자를, 내부에서 반환되는 데코레이터 함수는 데코레이터로 사용되는 것이다.

아래 예시를 보면, 클로저 변수로 사용자 인자를 받고 리턴값으로 데코레이터 함수를 반환하여 데코레이터를 실행하는 것을 볼 수 있다.

// 데코레이터 팩토리
function deco(value: string) {
  console.log('데코레이터가 평가됨');
  
  // 데코레이터 함수
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(value);
  };
}

class TestClass {
  // 데코레이터 팩토리를 사용하면 인자 값을 전달할 수 있다.
  @deco('HELLO')
  test() {
    console.log('함수 호출됨');
}

const t = new TestClass();
t.test();
데코레이터가 평가됨
HELLO
함수 호출됨

데코레이터 합성 / 멀티 데코레이터

데코레이터의 또 다른 장점으로는 하나의 멤버에 동시에 여러 개의 데코레이터를 장식할 수 있는 점이다. 이를 데코레이터 합성(Decorator Composition)이라고도 부른다.
여러 개의 데코레이터를 사용하면 수학의 함수 합성(function compsition)과 같이 합성할 수 있게 되기 때문에 이렇게 부르며, 아래 데코레이터 선언의 합성 결과는 수학적으로 f(g(x))f(g(x))와 같다.

@f
@g
test

멀티 데코레이터의 실행 흐름은 다음 순으로 처리된다.

  1. 각 데코레이터 표현식은 위에서 아래 방향(⬇︎)으로 평가(evaluate)된다.
  2. 실행 결과는 아래에서 위로(⬆︎) 함수를 호출(call)된다.

데코레이터 팩토리를 사용해 멀티 데코레이터의 실행 흐름을 살펴보면 다음과 같다.

// Size 데코레이터 팩토리
function Size() {
  console.log('Size(): 평가됨');
  // Size 데코레이터
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('Size(): 실행됨');
  };
}

// Color 데코레이터 팩토리
function Color() {
  console.log('Color(): 평가됨');
  // Color 데코레이터
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('Color(): 실행됨');
  };
}

// Button 클래스 정의
class Button {
  // 메서드에 멀티 데코레이터 적용
  @Size()
  @Color()
  isPressed() {}
}
Size(): 평가됨
Color(): 평가됨
Color(): 실행됨
Size(): 실행됨

이처럼 @expression에서 expression 표현식을 함수로 평가하는 순서는 위에서 아래이다.
그리고 expression이 함수로 평가된(데코레이터 팩토리) 후에, 실행 결과(데코레이터 함수)가 실행되는 순서는 아래에서 위가 된다.


데코레이터 종류

Class Decorator

클래스 데코레이터(class decorator)는 클래스 선언 직전에 선언되는 데코레이터이다. 클래스 데코레이터는 클래스의 생성자의 클래스 정의(definiton)을 읽거나 수정할 수 있어 기존의 클래스 정의를 확장하는 용도로 사용할 수 있다.
선언 파일(*.d.ts)과 선언 클래스(declare class) 내에서는 사용할 수 없다. 클래스 데코레이터 매개변수로는 클래스 생성자 자체를 받게 되는 특징이 있다.

클래스 데코레이터 매개변수

  • 첫 번째 argument (constructor) : 클래스(생성자 함수)가 전달된다. 타입스크립트가 클래스를 실행할 때 클래스의 생성자를 데코레이터의 constructor 파라미터로 자동 전달하므로, 생성자를 명시적으로 전달하지 않아도 된다.
  • 클래스 데코레이터 리턴값
    • class 또는 void
// 제네릭 - 해당 함수를 호출할 때 동적으로 <>에 정의된 타입을 받음. 여기선 클래스 생성자 타입으로 받는의미
function classDecorator<T extends { new (...args: any[]): {} }>(constructorFn: T)
{
	... 
} // 리턴값은 class 혹은 void

클래스 데코레이터 예제

아래 예시는 데코레이터가 Testconstructor를 상속해서 추가로 프로퍼티를 지정하고 리턴해주었는데, 이게 그대로 Test 클래스의 constructor 쪽으로 가서 확장이 된다고 생각하면 된다.
그래서 실행 결과가 기존의 Test의 생성자에 없던 프로퍼티들이 추가된 것으로 데코레이터가 클래스의 멤버를 확장한 것이다.

function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  // Test 클래스의 constructor를 상속해서 new Test() 가 되면 추가된 생성자도 실행되도록 설정
  return class extends constructor { 
    first_prop = 'override'; // 데코레이터에서 새로 프로퍼티를 덮어씌움
    new_prop = 'new property'; // 데코레이터에서 새로 프로퍼티를 추가
  };
}

@classDecorator
class Test {
  first_prop: string;

  constructor(m: string) {
    this.first_prop = m;
  }
}

let t = new Test('abcdefg');
console.log(t);
console.log( t.first_prop ); // 'override'

// 데코레이터로 설정된 프로토타입 확장은 타입 단언(Type Assertion) 필요
console.log( (t as any).new_prop ); // 'new property'
Test { 
    first_prop: 'override', 
    new_prop: 'new property' 
}
override
new property

클래스 데코레이터 팩토리 사용 예제

클래스 데코레이터에 데코레이터 팩토리를 이용한 예제이다.
데코레이터 팩토리에 전달된 사용자 인자값을 데코레이터 함수에서 사용해 결과를 리턴한다.

// 데코레이터 팩토리
function classDecorator(param1: string, param2: string) {
  // 데코레이터 함수
  return function <T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      new_prop = param1;
      first_prop = param2;
    };
  };
}

@classDecorator('안녕하세요', '반갑습니다')
class Test {
  first_prop: string;

  constructor(m: string) {
    this.first_prop = m;
  }
}

let t = new Test('world');
console.log(t);
Test { 
    first_prop: '반갑습니다', 
    new_prop: '안녕하세요' 
}

프로토타입(prototype)을 이용한 확장

장식된 클래스의 constructor를 확장하는 것뿐만 아니라 프로토타입을 이용하여 확장할 수 있다.

function classDecoratorFactory<T extends { new (...args: any[]): {} }>(constructorFn: T) {

   // 프로토타입으로 새로운 프로퍼티를 추가
   constructorFn.prototype.print2 = function () {
      console.log('this is print2');
   };
   constructorFn.prototype.gender = 'male';

   // 클래스를 프로퍼티에 상속시켜 새로운 멤버를 추가 설정
   return class extends constructorFn {
      public name = 'mark';
      private _age = 36;

      constructor(...args: any[]) {
         super(args);
      }

      public print() {
         console.log('this is print');
      }
   };
}

@classDecoratorFactory
class Test2 {}

const test2 = new Test2();

console.log(test2); // class_1 { name: 'mark', _age: 36 }

// 클래스 Test의 타입에는 print 함수가 없고, 데코레이터로 동적으로 추가된 형태이니, 타입 단언을 사용
(test2 as any).print(); // this is print 
(test2 as any).print2(); // this is print2 

console.log((test2 as any).gender); // male
Test2 { name: 'mark', _age: 36 }
this is print
this is print2
male

Method decorator

메서드 데코레이터(method decorator)는 메서드 선언 직전에 선언되는 데코레이터로 메서드 관찰, 수정 또는 대체하는 데 사용할 수 있다.
메서드의 속성 설명자(property descriptor)에 적용되고 메서드의 정의를 읽거나 수정할 수 있다. 선언 파일(*.d.ts), 오버로드 메서드(overload method), 선언 클래스에 사용할 수 없다.

참고
Property Descriptor는 객체의 프로퍼티들을 기존보다 정교하게 정의할 수 있는 ES5의 스펙이며, 프로퍼티의 특성을 설명하는 역할을 하는 객체이다.
Property DescriptorObject.getOwnPropertyDescriptor를 사용해서 가져올 수 있다.

메서드 데코레이터 함수가 전달 받는 인자는 총 3가지로 다음과 같다.

  • 첫 번째 argument : static 프로퍼티가 속한 클래스의 생성자 함수 또는 인스턴스 프로퍼티에 대한 클래스의 prototype 객체
  • 두 번째 argument : 해당 method의 이름
  • 세 번째 argument : 해당 methodproperty descriptor
function methodDecorator(
	target: any, // static 메서드라면 클래스의 생성자 함수, 인스턴스의 메서드라면 클래스의 prototype 객체
	propertyKey: string, // 메서드 이름
	descriptor: PropertyDescriptor // 메서드의 Property Descriptor
) {
	... 
} // return 값 무시됨

메서드 데코레이터는 클래스, 메서드 이름, 호출된 메서드(해당 메서드는 Object.defineProperty()로 만들어 짐)로 정의된 메서드 순으로 전달받는다.

PropertyDescriptor interface
PropertyDescriptor는 객체 속성의 특성을 기술하는 객체이다.

interface PropertyDescriptor {
 configurable?: boolean;	// 속성의 정의를 수정할 수 있는지 여부
 enumerable?: boolean;	// 열거형인지 여부
 value?: any;				// 속성 값
 writable?: boolean;		// 수정 가능 여부
 get?(): any;				// getter
 set?(v: any): void;		// setter
}

메서드 데코레이터 예제1

만약 해당 메서드가 호출될 때 특정 무엇인가를 동작시키고 싶다면 데코레이터에서 descriptor를 다음과 같이 재정의 해주면 된다.

function methodDecorator() {
  return function (target: any, property: string, descriptor: PropertyDescriptor) {

    // descriptor.value는 test() 함수 자체를 가리킨다. 이 함수를 잠시 변수에 피신 시킨다.
    let originMethod = descriptor.value; 

    // 그리고 기존의 test() 함수의 내용을 다음과 같이 바꾼다.
    descriptor.value = function (...args: any) {
      console.log('before');
      originMethod.apply(this, args); // 위에서 변수에 피신한 함수를 call, apply, bind 를 통해 호출
      console.log('after');
    };
  };
}

class Test {
  property = 'property';
  hello: string;

  constructor(m: string) {
    this.hello = m;
  }

  @methodDecorator()
  test() {
    console.log('test');
  }
}

let test = new Test("world")
test.test()
before
test
after

데코레이터 인수의 descriptor.value 로 접근하면 장식된 함수 자체로 접근하게 되는데 이를 잠시 백업하고 나중에 실행하는 식으로 응용이 가능하다.
중요한 점은 백업된 메서드를 호출할 때 apply를 이용하여 this를 바인딩해줘야 한다는 점이다. 만일 this를 바인딩하지 않으면 해당 메서드를 어느 객체가 호출 했는지 알 수 없기 때문이다.

메서드 데코레이터 예제2

좀더 응용하자면, 메서드 데코레이터로 특정 메서드의 실행 전/후 로깅, 실행시간을 측정 가능하다.

function methodDecorator() {
  return function (target: any, property: string, descriptor: PropertyDescriptor) {
      
    // descriptor.value는 test() 함수 자체를 가리킨다. 이 함수를 잠시 변수에 피신 시킨다.
    let originMethod = descriptor.value;

    // 그리고 기존의 test() 함수의 내용을 다음과 같이 바꾼다.
    descriptor.value = function (...args: any) {
      let startTS = new Date().getTime();

      originMethod.apply(this, args); // 위에서 변수에 피신한 함수를 call,apply,bind 를 통해 호출

      let endTS = new Date().getTime();

      console.log(`실행시간: ${(endTS - startTS) / 1000} S`);
    };
  };
}

// ...
test
실행시간: 0 S

Accessor Decorators

접근자 데코레이터(accessor decorator)는 접근자(accessor) 바로 앞에 선언한다. 접근자의 속성 설명자에 적용되고 접근자의 정의를 읽거나 수정할 수 있다. 선언 파일(*.d.ts)과 선언 클래스에는 사용할 수 없다.

접근자(accessor)
객체 프로퍼티를 객체 외에서 읽고 쓸 수 있는 함수이다. 쉽게 말하자면 gettersetter이다.
타입스크립트에서는 getter, setter를 구현할 수 있는 get, set 키워드가 있다.

접근자 데코레이터 매개변수

  • 첫 번째 argument : static 프로퍼티가 속한 클래스의 생성자 함수 또는 인스턴스 프로퍼티에 대한 클래스의 prototype 객체
  • 두 번째 argument : 해당 method의 이름
  • 세 번째 argument : 해당 methodproperty descriptor

접근자 데코레이터 리턴값

  • Property Descriptor 형태
  • void

접근자 데코레이터 예제

특정 프로퍼티가 열거가 가능하지 결정하는 데코레이터 예제이다.

function Enumerable(enumerable: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = enumerable;
  };
}

class Person {
  constructor(private name: string) {}
  
  @Enumberable(true)
  get getName() {
    return this.name;
  }
  
  @Enumberable(false)
  set setName() {
    this.name = name;
  }
}

const person = new Person('hello');
for (let key in person) {
  console.log(`${key}: ${person[key]}`);
}

결과를 출력하면 getName은 출력되지만 setName은 열거하지 못하게 되었기 때문에 for 문에서 key로 받을 수 없다.

name: hello
getName: hello

Property Decorators

속성 데코레이터(property decorator)는 클래스의 프로퍼티 선언 바로 전에 선언 된다.
속성 데코레이터는 메서드 데코레이터와 다르게 데코레이터 함수에 Property Descriptor가 인자로서 제공되지 않는다는 차이가 있다. 대신에 속성 데코레이터도 마찬가지로 Property Descriptor 형식의 객체를 반환해서 프로퍼티의 설정을 바꿀 수 있다.

속성 데코레이터 매개변수

  • 첫 번째 argument : static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  • 두 번째 argument : 해당 property의 이름

속성 데코레이터 리턴값

  • Property Descriptor 형태
  • void
function propertyDecorator(
  target: any, // static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  propertyName: string, // 프로퍼티 이름
): any {
	...
} // return하는 값이 Property Descriptor 형태 또는 void. 이로서 해당 property의 writable 등을 조작할 수 있음

속성 데코레이터 예제

리턴값으로 Object.defineProperty()를 반환함으로써, 이를 통해 속성의 부가적인 설정을 할 수 있다.

예를들어 다음과 같이, 앞의 데코레이터는 boolean 타입의 인자를 전달받아 writable로 설정하게 되는데 writablefalse일 경우 해당 속성은 값을 수정할 수 없게 된다.

function writable(isWritable: boolean) {
  return function (target: any, propertyName: any): any {
    return {
      writable,
    };
  };
}

class Test {
  property = 'property';

  @writable(false)
  public data1 = 0;

  @writable(true)
  public data2 = 0;
}

const t = new Test();
t.data1 = 1000;
t.data2 = 1000; // 런타임 에러 !! - data2는 writable이 false라서 값을 대입할 수가 없다.

속성 데코레이터 getter, setter 설정 예제

좀더 응용하자면 다음과 같이 gettersetter도 설정이 가능하다.

function SetDefaultValue(numberA: number, numberB: number) {
  return (target: any, propertyKey: string) => {
    const addNumber = numberA * numberB;
    let value = 0;

    // 데코레이터가 장식된 DataDefaultType의 num 이라는 프로퍼티의 객체 getter / setter 설정을 추가한다.
    Object.defineProperty(target, propertyKey, {
      get() {
        return value + addNumber; // 조회 할때는 더하기 시킴
      },
      set(newValue: any) {
        value = newValue - 30; // 설정 할때는 30을 뺌
      },
    });
  };
}

class DataDefaultType {
  @SetDefaultValue(10, 20)
  num: number = 0;
}

const test = new DataDefaultType();

test.num = 30;
console.log(`num is 30, 결과 : ${test.num}`); // num is 30, 결과 : 200

test.num = 130;
console.log(`num is 130, 결과 : ${test.num}`); // num is 130, 결과 : 300
num is 30, 결과 : 200
num is 130, 결과 : 300

Parameter Decorators

매개변수 데코레이터(parameter decorator)는 생성자 또는 메서드의 매개 변수에 선언되어 적용된다. 데코레이터는 가로로 써도 인식되기 때문에 함수의 매개변수 왼쪽 옆에 명시해 주면 된다. 선언 파일(*.d.ts), 선언 클래스에서는 사용할 수 없다.

파라미터 데코레이터 매개변수

  • 첫 번째 argument : static 프로퍼티가 속한 클래스의 생성자 함수 또는 인스턴스 프로퍼티가 속한 클래스의 prototype 객체
  • 두 번째 argument : 매개변수가 들어있는 method의 이름
  • 세 번째 argument : 메서드 파라미터 목록에서의 index
function parameterDecorator(
  target: any, // static 메서드의 파라미터 데코레이터라면 클래스의 생성자 함수, 인스턴스의 메서드라면 prototype 객체
  methodName: string, // 매개변수가 들어있는 메서드의 이름
  paramIndex: number // 매개변수의 순서 인덱스
) {
    ...
} // 리턴값은 무시됨

매개 변수 데코레이터 예제

아래 예제는 제대로 된 값이 전달되었는지 확인하는 데코레이터이다. 매개 변수 데코레이터는 단독으로 사용하기 보다는 메서드 데코레이터와 함께 사용할 때 유용하게 사용된다.

import { BadRequestException } from '@nestjs/common';

// 매개 변수 데코레이터 함수
// target 클래스의 validators 속성에 유효성을 검사하는 함수 할당
function MinLength(min: number) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    target.validators = {
      minLength: function(args: string[]) {
        return args[parameterIndex].length >= min;
      }
    }
  }
}

// 메서드 데코레이터 함수
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value;
  
  // 설명자의 value에 유효성 검사 로직이 추가된 함수를 할당
  descriptor.value = function(...args) {
    // target에 저장해둔 validators를 모두 수행한다. 이때 원래 메서드에 전달된 인수들을 각 validator에 전달한다.
    Object.keys(target.validators).forEach(key => {
      if (!target.validators[key](args)) {
        throw new BadRequestException();
      }
    })
    // 기존 함수를 실행
    method.apply(this, args);
  }
}

class User {
  private name: string;
  
  @Validator
  setName(@MinLength(3) name: string) {
    this.name = name;
  }
}

const t = new User();
t.setName('hello');
console.log('---------------');
t.setName('hi');	// BadRequestException

데코레이터 요약 및 호출 순서

각 데코레이터의 특징을 간략하게 정리하면 다음과 같다.

데코레이터역할호출 시 전달되는 인수선언 불가능한 위치
클래스 데코레이터클래스의 정의를 읽거나 수정constructor.d.ts 파일, declare 클래스
메서드 데코레이터메서드의 정의를 읽거나 수정target, propertyKey, propertyDescriptor.d.ts 파일, declare 클래스, 오버로드 메서드
접근자 데코레이터접근자의 정의를 읽거나 수정target, propertyKey, propertyDescriptor.d.ts 파일, declare 클래스
속성 데코레이터속성의 정의를 읽음target, propertyKey.d.ts 파일, declare 클래스
매개변수 데코레이터매겨변수의 정의를 읽음target, propertyKey, parameterIndex.d.ts 파일, declare 클래스

다양한 타입의 데코레이터에 대한 호출 순서는 다음과 같이 잘 정의되어 있다.

  1. 매개 변수, 메서드 데코레이터에 이어서 접근자, 속성 데코레이터가 각 인스턴스 멤버에 적용된다.
    • 평가 순서는 메서드/접근자/속성 > 매개변수
  2. 매개 변수 데코레이터에 이어서 메서드, 접근자, 속성 데코레이터가 각 정적 멤버에 적용된다.
    • 평가 순서는 메서드/접근자/속성 > 매개변수
  3. 매개 변수 데코레이터가 생성자 > 클래스 데코레이터가 클래스에 적용된다.
    • 평가 순서는 클래스 > 생성자

속성/접근자/메서드 데코레이터의 호출 순서는 코드에서 나타나는 순서에 따라 달라진다.


참고 자료

1개의 댓글

comment-user-thumbnail
2024년 3월 11일

안녕하세요, 이 글이 저한테 도움이 크게 됩니다. 혹시 퍼가도 될까요? 본 글 링크랑 작성자 블로그 명 다 퍼간 글에 명시 할 예정입니다.

답글 달기