메모리 주소 관점에서의 매개 변수와 구조 분해 할당
구조 분해 할당은 모던 자바스크립트 문법으로서 변수 선언 시 사용되며, 객체나 배열의 특정 요소를 선택적으로 변수에 할당하여 쉽게 사용할 수 있게 한다. 이를 통해 코드를 더 간결하고 읽기 쉽게 만들 수 있다.
어느정도 숙련된 자바스크립트/타입스크립트 사용자라면 숨쉬듯이 사용하는 문법이기도 하다. 그런데 이 문법을 메모리 주소와 연관지어 생각해본 적은 한 번도 없었다. 적어도 오늘 어떤 일이 있기 전까지는.
나는 NestJs로 백엔드 작업을 하며 유저 기능을 위한 커스텀 가드를 만들고 있었다. 아주 간단한 작업이었으며, 나는 별 생각 없이 코드를 마무리지었다. 그리고 코드를 한번 쓱 보는데 뭔가 이상하다는 생각이 들었다. validateRequest 함수 내에서 this.authService.verify() 메소드의 리턴 값이 request.user에 할당되고 있다.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: Request) {
const accessToken = request.cookies['accessToken'];
if (!accessToken) return false;
try {
request.user = this.authService.verify(accessToken, 'access');
return true;
} catch {
throw new UnauthorizedException('로그인이 필요합니다');
}
}
}
그리고 컨트롤러에서는 req 객체로부터 user 프로퍼티를 조회하고, 문제 없이 값을 가져왔다. 나는 이게 잘 이해가 되지 않았다. validateRequest 함수는 request를 리턴하는 게 아니라 boolean 값을 리턴한다. 그런데도 불구하고 컨트롤러의 req 객체는 아무 문제 없이 user 프로퍼티를 조회할 수 있었다. 이게 어떻게 가능한 거지??
@Controller('report')
export class ReportController {
constructor(private reportService: ReportService) {}
@Post()
@UseGuards(AuthGuard)
async createReport(
@Body() createReportDto: CreateReportDto,
@Req() req: Request,
) {
const { email } = req.user as User;
const newReport = await this.reportService.createReport(
createReportDto,
email,
);
return newReport;
}
}
메모리 주소 관점에서의 파라미터
영어 관용구 중에서는 take something for granted라는 표현이 있다. 대충 무언가의 소중함을 잊고 살다가 크게 ㅈ되고 나서야 소중함을 깨닫게 된다…같은 뉘앙스를 가지고 있다. 자바스크립트 개발자들이 자주 잊고 사는 것 중에 하나는 함수의 파라미터가 한국말로는 매개 변수라는 점이며, 실제로도 변수라는 사실이다.
이 포스트의 첫 문장에서 드러낸 것처럼 구조 분해 할당은 변수 선언 시 사용되며, 이는 매개 변수에 대해서도 예외가 아니다. 변수를 선언한다는 것은 메모리 공간에 데이터를 저장하고, 데이터가 저장된 메모리 공간 상의 주소를 변수가 참조한다는 의미이다. 이러한 관점에서 파라미터와 함수를 다시 바라보면 이전과는 전혀 다르게 (어쩐지 절차지향적 프로그래밍 같기도 한) 보인다.
아래의 코드는 메모리 주소 관점에서 동일하고 볼 수 있다. 메모리 공간에 1이라는 숫자를 저장하고 변수 a가 그 메모리 주소를 참조한다. 변수 b도 동일한 메모리 주소를 참조하도록 한다. 그리고 변수 b의 메모리 주소에 있는 값과 1을 더한 값을 메모리 공간에 저장하고, 그 주소를 변수 c가 참조하도록 한다.
const a = 1
function test(b) {
return b + 1
}
const c = test(a)
const a = 1
let b
b = a
const c = b + 1
따라서 함수에 인자를 제공하는 행위는, 바꿔 말하면 변수가 참조하고 있는 메모리 주소를 그대로 넘겨주는 행위라고 볼 수 있다. 함수에 제공하는 인자가 원시값이 아닌 객체라 할 지라도 이 사실은 변함이 없다.
그런데 여기에 구조 분해 할당이 끼어든다면 어떻게 될까?
객체와 구조 분해 할당의 메모리 주소
처음 내가 이상하게 여겼던 커스텀 가드의 코드를 간략하게 추상화하면 대략 아래와 같다. test 함수는 아무것도 리턴하고 있지 않지만 a 함수의 변수 obj는 값이 바뀌었다!
function a() {
const obj = { a: 1, b: 2, c: 0 }
test(obj)
return obj
}
function test(b) {
b.c = 3
}
console.log(a()) // { a: 1, b: 2, c: 3 }
위의 코드에서 함수라는 개념을 지우고 메모리 주소 관점에서 다시 작성하면 아래와 같다. 결국 내가 고민하던 것들은 사실 자바스크립트를 얼마간 배워봤다면 누구라도 한 번쯤은 들어봤을, 참조형 재할당 문제에 지나지 않았다. 변수 b와 변수 obj가 같은 메모리 주소를 참조하고 있기에 발생하는 현상인 것이다.
const obj = { a: 1, b: 2, c: 0 }
let b
b = obj
b.c = 3
console.log(obj)
그런데 만약 첫 번째 예시에서 test 함수가 b 변수 대신 구조 분해 할당을 사용한다면 결과는 어떻게 될까? 메모리 주소 관점에서 구조 분해 할당은 아래와 같이 일어난다.
변수 c에는 obj.c가 가지고 있는 메모리 주소가 할당된다. 그러나 곧 자바스크립트는 메모리 공간에 3이라는 숫자를 저장하고 변수 c가 그 메모리 주소를 참조하도록 변경한다. 콘솔에 찍히는 obj 객체의 obj.c 의 메모리 주소는 여전히 숫자 0이 저장되어있는 메모리 공간을 가리키고 있다.
function a() {
const obj = { a: 1, b: 2, c: 0 }
test(obj)
return obj
}
function test({ c }) {
c = 3
}
console.log(a()) // { a: 1, b: 2, c: 0 }
const obj = { a: 1, b: 2, c: 0 }
let c = obj.c
c = 3
console.log(obj)
결론
메모리 주소의 관점에서 함수의 파라미터와 아규먼트는 동일한 메모리 주소를 넘겨받고 넘겨준다. 맨 처음에 이야기했던 커스텀 가드의 경우 canActivate와 validateRequest와 컨트롤러가 동일한 메모리 주소를 사용했기 때문에 값을 리턴하거나 하지 않아도 NestJs의 요청 생애주기에 따라 값을 주고받을 수 있었던 셈이다.
구조 분해 할당 없이 같은 메모리 주소를 가지고 작업하는 함수는 활용하기에 따라 여러 방면에서 다양하게 사용할 수 있을 것 같다. 그러나 큰 힘에는 큰 책임이 따르는 것처럼 멋모르고 사용했다가는 크게 ㅈ될 수 있다. 모쪼록 메모리 주소 관점에서 함수와 변수를 바라보면 좀 더 안전하게 코드를 작성할 수 있지 않을까 한다.
블로그의 정보
Ayden's journal
Beard Weard Ayden