시작 하며...
Nest.js 와 같이 체계적으로 잘 설계되어 있는 프레임워크를 사용하다보면 아래와 같은 Decorator 들을 자주 볼 수 있다.
import { Injectable } from '@nestjs/common';
@Injectable()
export class TestService {
... 내부 구현들
}
}
이때 이 @Injectable() 데코레이터를 달아주면 어떤 기능이 있는지는 알고 있었지만, Typescript에서 제공하는 Decorator 가 정확하게 어떤 역할을 하는지 잘 모르고 있었던 것 같아 별도로 공부가 필요하다고 생각하게 되어 글을 쓰기 시작했다.
본 글에서는 데코레이터를 공부하며 알게된 개념들을 함께 정리해본다.
함수 == 일급 객체
오늘 주제를 이해하기 위해서 선행해서 알아두어야 하는 개념이 몇가지 있다.
그중 첫번째는 바로 JS와 Python 과 같은 언어들에서는 함수가 일급 객체로 취급 된다는 것이다.
function foo() {
console.log("푸!")
}
function bar(funcParam) {
funcParam()
console.log("바!")
return function ooh() {
console.log("오!")
}
}
const a = bar
a(foo)()
// 푸!
// 바!
// 오!
코드가 좀 어지러운데, 눈여겨 볼 포인트들은 다음과 같다.
- bar() 함수를 a 변수에 할당했다.
- foo 함수가 bar 함수의 매개변수로 설정되었다.
- bar 함수가 ooh 함수를 반환한다.
위 세가지 동작은 Javascript나 Python에서는 가능하지만 Java와 같은 언어들에서는 불가능하다. (그래서 자바에서 활용하는 방법이 람다 표현식이다.)
이렇게, 함수를 다른 변수들과 다르지 않게 동일하게 다룰 수 있는 것을 보고 "함수가 일급 객체로 취급된다" 라고 하는것이다.
이렇게 함수를 일급객체로 다룰 수 있으면 함수형 프로그래밍, 고차함수 등 다양한 프로그래밍 기법 등의 활용이 가능해진다.
함수형과 객체지향형은 호불호가 좀 있다. (개인적으로는 어려워서 함수형 별로 안좋아한다 ㅋㅋ)
클로저, 렉시컬 환경
클로저(Closure) 란?
클로저는 주변 상태(어휘적 환경)에 대한 참조와 함께 묶인(포함된) 함수의 조합입니다.
즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다.
- MDN
엄... 일단 무슨 소리인지 잘 모르겠다.
다음과 같은 코드가 있다고 가정 해보자.
class Friend{
friends = []
add_friend(name) {
this.friends.push(name);
console.log(this.friends);
}
}
const friend = new Friend();
friend.add_friend("김철수") // [ '김철수' ]
friend.add_friend("박영희") // [ '김철수', '박영희' ]
friend.add_friend("홍길동") // [ '김철수', '박영희', '홍길동' ]
Friend 클래스는 friends 멤버 변수를 가지고, add_friend 라는 메소드를 사용해 friends 배열에 새로운 친구를 추가하고 출력해준다.
우리가 흔히 사용하는 모습의 객체지향적인 코드라고 할 수 있다. 다음 코드를 보자.
function friend() {
let friends = []
function add_friend(name) {
friends.push(name)
console.log(friends)
}
return add_friend
}
const closure = friend()
closure("김철수") // [ '김철수' ]
closure("박영희") // [ '김철수', '박영희' ]
closure("홍길동") // [ '김철수', '박영희', '홍길동' ]
보면 하는 동작은 똑같은데, Friend 클래스가 friend 함수로 바뀌었다!
- closure 라는 변수에 friend 함수를 호출
- friend 함수는 add_friend 라는 함수를 반환
- closure("이름") 을 수행하면, friend 함수 안에 있는 add_friend 함수가 실행됨.
- closure("이름") 을 반복 수행하면 그 전에 했던 결과가 그대로 누적되어 출력됨 (friends 변수를 기억하고 있음!)
풀어보면, 개발자는 closure 라는 변수에 friend 호출해 주는 순간 friend 함수의 실행 컨텍스트는 종료되었다.
하지만 closure 변수에 담겨있는 함수를 활용하면 이미 종료된 friend 함수의 실행 컨텍스트에 존재하는 friends 라는 배열에 접근할 수 있는 것이다.
이렇게 손을 떠났지만 접근이 가능하게 해주는 것을 클로저(Closure) 라고 하고, 위 예시에서는 friends 배열이 렉시컬 환경(Lexical Environment) 라고 한다.
이제야 윗 내용이 조금 이해가 된다.
박통 버전 요약
클로저(Closure) - 이미 개발자 손을 떠난 데이터들에 접근 가능하게 해주는 것.
렉시컬 환경(Lexical Environment) - 손을 떠난 데이터들의 영역. (여기서는 friends 배열)
어려운 버전 요약
클로저(Closure) - 반환 당시 함수의 유효 범위를 벗어난 변수 또는 메서드에 접근하게 될 수 있는 것.
렉시컬 환경(Lexical Environment) - 함수가 정의된 시점의 상위 스코프 변수와 식별자들의 집합.
이거 왜씀?
Javascript 에서는 완전한 정보 은닉이 되지 않는다. (Typescript를 써도 실은 컴파일 단계에서 걸러줄 뿐 빌드된 파일의 형태에서는 접근이 된다. 참고 블로그)
class Friend{
friends = []
add_friend(name) {
this.friends.push(name);
console.log(this.friends);
}
}
const friend = new Friend();
friend.add_friend("김철수") // [ '김철수' ]
console.log(friend.friends) // [ '김철수' ] 정보 은닉 안됨 ㅉㅉ
반면에, 클로저 같은 경우는 외부에서 접근할 방법이 없다.
아직 멀었다.
클로저는 사실 Javascript 개발자라면 술술 나올정도로 중요한 개념이면서도 굉장히 내용이 많고 복잡하기에 필자의 빈약한 지식으로 모두 설명할 수 없다...
아래 양질의 자료들을 참고해서 확실히 공부해보자.
필자도 기회가 된다면 별도로 클로저, 스코프 체인 등을 정리해볼 예정이다.
클로저 - (MDN)
자바스크립트의 스코프와 클로저 - (NHN Cloud Meetup!)
클로저, 그리고 캡슐화와 은닉화 - (NHN Cloud Meetup!)
변수의 유효범위와 클로저 - (모던 자바스크립트 튜토리얼)
데코레이터
데코레이터(Decorator) 란?
데코레이터는 클래스 선언, 메서드, 접근자, 프로퍼티 또는 매개 변수에 첨부할 수 있는 특수한 종류의 선언입니다.데코레이터는 @expression 형식을 사용합니다. 여기서 expression은 데코레이팅 된 선언에 대한 정보와 함께 런타임에 호출되는 함수여야 합니다.
- Typescript 공식문서
쉽게 풀어쓰면, "클래스, 메서드, 속성, 매개변수 와 같은 곳에 어노테이션이나 메타데이터를 추가할 수 있는 기능"이 데코레이터 인 것이다.
본 게시글에서는 비교적 자주 사용하는 기법인 클래스 데코레이터와 메서드 데코레이터에 대해서 설명한다.
이것도 마찬가지로 말로 보면 어렵고, 코드로 보면 비교적 이해가 빠르다.
클래스 데코레이터
// Logger 데코레이터 함수 정의: 클래스가 정의될 때 호출.
function Logger(constructor: Function) {
console.log(`클래스 ${constructor.name} 가 정의되었습니다.`);
}
@Logger
class Example {
constructor() {
console.log("Example 인스턴스가 생성되었습니다.");
}
}
const example = new Example();
// "클래스 Example 가 정의되었습니다." (클래스 정의 시점)
// "Example 인스턴스가 생성되었습니다." (인스턴스 생성 시)
위 예제는 간단한 클래스 데코레이터를 보여주는 예시이다.
여기서 중요한 점은 Logger 함수(데코레이터)는 클래스가 정의되는 시점의 바로 직전에 정의 된다는 것이다.
또한, 데코레이터 함수는 반드시 생성자를 인자로 전달받는다.
이렇게 활용한 클래스 데코레이터는 클래스 정의를 관찰, 수정 혹은 교체 하는데 까지 사용할 수 있다.
function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
reportingURL = "http://www...";
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // Prints "Needs dark mode"
console.log(bug.type); // Prints "report"
// 데코레이터는 Typescript의 "타입 자체"를 바꾸는 것은 아님.
// 그렇기에 "reportingURL" 속성은 시스템에게 알려지지 않기에,
// 존재 여부를 타입시스템이 인지할 수 없음!!
console.log(bug.reportingURL); //
그래서 위와 같은 동작도 가능하다. 데코레이터가 해당 클래스의 생성자를 확장하여 새로운 멤버변수 reportingURL을 추가하는 것이다.
여기서 재미있는 점은, 이렇게 코드를 작성해보면 에러가 나지만 실행은 문제없이 된다는 것이다.
BugReport 클래스의 타입 자체를 바꾼것이 아니기에, reportingURL 속성은 존재하지 않아 찾을 수 없기 때문이다.
이것이 데코레이터가 런타임에 적용된다는 것을 보여주는 예시이다.
다음으로는 NestJS에서 많이 사용되는 @Injectable() 데코레이터의 구현부분을 보자.
export function Injectable(options?: InjectableOptions): ClassDecorator {
return (target: object) => {
Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
};
}
보면 매개변수로 options(생성자)를 인자로 받고 Reflect(Javascript 내에서 객체의 메타데이터를 관리하는데에 사용되는 전역 객체)객체를 사용해 해당 클래스의 메타데이터에 어떤 키값들을 조작하고 있다.
- INJECTABLE_WATERMARK 키에 true 값을 지정 (상수 값으로, 실제 문자열은 '__injectable__')
- SCOPE_OPTIONS_METADATA 키에 options 값을 지정 (상수 값으로, 실제 문자열은 ''scope:option')
이렇게 @Injectable 데코레이터가 적용된 클래스들은 내부적으로 어떤 메타데이터 값이 적용되고, NestJS는 이렇게 특정 값이 적용된 클래스들을 "아, 얘네는 provider 배열에 등록 가능한 애들이구나" 라고 판단해 애플리케이션 내에서 자동으로 의존성 주입이 될 수 있도록 도와준다. (@Injectable을 달지 않은채로 의존성 주입을 시도하면 오류가 발생하는 이유!)
이런식으로 데코레이터를 활용해서 간단하고 직관적으로 NestJS DI Container가 관리할 클래스들을 명시해줄 수 있다.
메서드 데코레이터
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 원래의 메서드를 백업
const originalMethod = descriptor.value;
// 데코레이터로 감싼 새로운 메서드 정의
descriptor.value = function (...args: any[]) {
console.log(`Called ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args); // 원래 메서드 실행
console.log(`Result:`, result);
return result;
};
}
class Example {
// 메서드에 데코레이터 적용
@Log
add(a: number, b: number): number {
return a + b;
}
}
const ex = new Example();
ex.add(3, 4);
// "Called add with args: [3, 4]"
// "Result: 7"
위 예제는 간단한 메서드 데코레이터를 보여주는 예시이다.
클래스 데코레이터와 마찬가지로 add 함수가 정의되는 시점 바로 직전에 정의되며, target, propertyKey, descriptor 총 3가지 변수를 인자로 전달받는다는 규칙이 있다.
- target : 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입
- propertyKey : 해당 멤버(함수)의 이름
- descriptor : 해당 멤버(함수)의 property 설명자
Log 함수는 다음과 같은 순서로 동작한다.
- 인자로 전달받은 기존 add 함수를 originalMethod라는 변수에 저장
- descriptor.value (해당 함수의 내용)를 새로운 함수로 재정의 한다.
- 해당 함수에서 시작전에 해당 함수명을 출력시킨다.
- 아까 저장해두었던 원본 함수를 실행해주고, 결과값을 result에 저장한다. (descriptor.value.apply 는 JS 환경의 모든 함수들이 상속받는 내장 메서드로, 호출되는 함수의 this context, 전달할 인자들을 직접 지정해줄 수 있다.)
- 결과를 출력하고, 반환한다.
쉽게 말하면, 기존 함수를 전달받고, 그 함수를 실행하기 전이나 후에 어떤 동작들을 저런식으로 처리해줄 수 있다.
이런식으로 메서드 데코레이터를 활용하면 높은 가독성, 중복 감소 효과를 불러올 수 있다. 하지만 디버깅시 추적이 번거롭다는 점, 오히려 한번만에 코드를 이해하기 어렵다는 점 등이 단점으로 꼽히기도 한다.
데코레이터랑 클로저랑 무슨 사이임?
클로저 개념을 데코레이터에 잘 적용하면 아래와 같은 것들도 만들 수 있다.
const Countable = (() => {
let count = 0; // 모든 데코레이터가 적용된 클래스의 인스턴스 생성 횟수를 저장할 변수 (스코프 사용)
return function <T extends { new (...args: any[]): {} }>(constructor: T): T {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
count++;
console.log(`전체 인스턴스 생성 횟수: ${count}`);
}
};
};
})();
@Countable
class FirstClass {
constructor() {
console.log("FirstClass 인스턴스 생성");
}
}
@Countable
class SecondClass {
constructor() {
console.log("SecondClass 인스턴스 생성");
}
}
const instance1 = new FirstClass();
const instance2 = new SecondClass();
const instance3 = new FirstClass();
// FirstClass 인스턴스 생성
// 전체 인스턴스 생성 횟수: 1
// SecondClass 인스턴스 생성
// 전체 인스턴스 생성 횟수: 2
// FirstClass 인스턴스 생성
// 전체 인스턴스 생성 횟수: 3
클로저 영역을 사용해서 해당 데코레이터가 적용된 클래스의 인스턴스가 생성될 때마다 값을 증가 시키고, 해당 값을 출력시키는 Countable 데코레이터를 만들 수 있다.
'Computer Science' 카테고리의 다른 글
Websocket 과 Socket.io에 대하여 (1) | 2025.01.25 |
---|---|
자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop) (1) | 2024.09.23 |