ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JS 클로저 - 캡슐화와 정보 은닉
    Front-end 개발 2023. 8. 21. 09:01

    목차

    1. 요약

    2. 캡슐화와 정보 은닉

    3. 자주 발생하는 실수

    4. 결론


    객체 지향 프로그래밍의 캡슐화 (출처: stackify.com)

    자바스크립트 클로저를 공부하는데 글의 너무 길어져서 클로저의 개념과 활용과 분리하여 작성한다.

    본 내용은 모던 자바스크립트 딥다이브 스터디 강의 24장의 내용 中 일부이다. (스터디 영상 21분 부터 ~)

     

    앞선 글에서 클로저의 개념을 알아봤다.

    클로저(Closure)생명 주기가 종료된 외부 함수의 변수를 참조하면서 더 오래 유지되는 중첩 함수를 부르는 용어다.

     

    이번 글에서는 클로저를 활용한 캡슐화와 정보 은닉에 대해서 다룬 스터디 내용을 다룬다.

    1. 요약


    - 캡슐화(Encapsulation)는 프로퍼티와 메서드를 하나로 묶는 것을 말한다.

    - 정보 은닉이란 외부에 공개되지 않도록 감추어 적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지하는 것

    - 접근 제한자를 제공하지 않는 자바스크립트는 기본적으로 모든 프로퍼티와 메서드는 외부에 공개되어 있다.

    - Private Class Fields 가 추가되기 전까지 자바스크립트는 정보 은닉을 완전하게 지원하지 않았다.

    - 2022년 새로운 표준 사양인 Private Class Field 에는 일반적으로 외부에서 접근이 안된다. 하지만
      개발자 도구 콘솔창에서 정의한 클로저의 Private Field 에는 접근 가능하다. (디버깅을 위한 것으로 생각됨)

    - 또 다른 정보 은닉 방법으로 함수형 프로그래밍 기법인 고차 함수를 사용하는 방법이 있다.

    -  Private Class Fields 는 2019년 4월 이전의 구버전 크롬 브라우저에서는 작동하지 않는 호환성 문제가 있다.

    2. 캡슐화와 정보 은닉


    2-1. JS 캡슐화의 정보 은닉의 개념

    - 캡슐화(encapsulation)는 객체의 상태(state)를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작(behavior)인 메서드를 하나로 묶는 것을 말한다.

    - 캡슐화는 객체 지향 프로그래밍에서 중요한 개념 중 하나로 Prototpye 와 달리 Class 에서 하나의 단위로 묶는데,

    - 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉(information hiding)이라 한다.

    - 정보의 은닉은 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않도록 감추어 적절치 못한 접근으로부터 객체의 상태 변경을 방지해 정보를 보호하며 객체 간의 상호 의존성, 즉 결합도(coupling)를 낮추는 효과가 있다.

    - 대부분의 객체지향 프로그래밍(Object-Oriented Programming, OOP) 언어는 클래스를 정의하고 그 클래스를 구성하는 멤버(프로퍼티와 메서드)에 대하여 public, private, protected 같은 접근 제한자(access modifier)를 선언하여 공개 범위를 한정할 수 있다.

    - 하지만 자바스크립트에는 public, private, protected 같은 접근 제한자를 제공하지 않는다. 따라서 자바스크립트 객체의 모든 프로퍼티와 메서드는 기본적으로 외부에 공개(public)되어 있다.

     

    2-2. JS 의 프로퍼티와 메서드에 공개범위

    - 아래 예제의 this.name 은 생성자 함수의 인스턴스의 프로퍼티를 가리키기 때문에 외부에 노축되는 public

    - 반면 _age 는 지역 변수로 생성되고 사라지는 private 변수처럼 동작한다.

    - 그렇기 때문에 JS 에서 public 한 변수는 리턴될 객체(인스턴스)에 직접 할당해주며, private 변수로 사용되는 것은 내부에 변수 선언을 한다.

    function Person(name, age) {
        this.name = name; // public
        let _age = age; // private
        
        // 인스턴스 메서드
        this.sayHi = function() {
            console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
        };
    }
    
    const me = new Person('Lee', 20);
    me.sayHi(); // Hi! My name is Lee. I am 20.
    console.log(me.name); // Lee
    console.log(me._age); // undefined
    
    const you = new Person('Kim', 30);
    you.sayHi(); // Hi! My name is Kim. I am 30.
    console.log(you.name); // Kim
    console.log(you._age); // undefined

    - 클로저인 sayHi 에 의해서 me.sayHi()는 변수 _age 를 참조할 수 있지만, _age 를 외부에서 접근할 수 없다.

    - 그리고 위 예제의 sayHi 메서드는 인스턴스 메서드이므로 Person 객체가 생성될 때마다 중복 생성된다.

    - 모든 인스턴스가 하나의 메서드를 공유하도록 만들어 자원을 더 효율적으로 사용하도록 수정할 필요가 있다.

    - 다음과 같이 즉시 실행 함수 내에 Person 생성자 함수와 Person.prototype.sayHi 메서드를 사용하여 Person 객체중복 생성을 방지해 본다.

    const Person = (function () {
        let _age = 0; // private
        
        // 생성자 함수
        function Person(name, age) {
            this.name = name; // public
            _age = age;
        }
        
        // 프로토타입 메서드
        Person.prototype.sayHi = function () {
            console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
        };
        
        // 생성자 함수를 반환
        return Person;
    }());
    
    const me = new Person('Lee', 20);
    me.sayHi(); // Hi! My name is Lee. I am 20.
    console.log(me.name); // Lee
    console.log(me._age); // undefined
    
    const you = new Person('Kim', 30);
    you.sayHi(); // Hi! My name is Kim. I am 30.
    console.log(you.name); // Kim
    console.log(you._age); // undefined
    
    me.sayHi(); // Hi! My name is Lee. I am 30. (자유 변수 _age 를 공유한다)

    - 위 예제를 통해서 Person 객체의 중복 생성은 해결했지만, _age 를 공유한다는 새로운 문제가 발생한다.

    - 이는 Person.prototype.sayHi 메서드가 단 한 번 생성되는 클로저이기 때문에 발생하는 현상이다.

    - Person 생성자 함수가 여러 개의 인스턴스를 생성할 경우 _age 변수의 상태가 유지 되지 않았다. 이처럼 자바스크립트는 정보 은닉을 완전하게 지원하지 않는다. 인스턴스 메서드를 사용한다면 자유 변수를 통해 private을 흉내 낼 수는 있지만 프로토타입 메서드를 사용하며 자유 변수를 공유 하므로써 이마저도 불가능해진다. ES6의 Symbol 또는 WeakMap 을 사용하여 private 한 프로퍼티를 흉내 내기도 했으나 근본적인 해결책이 되지는 않는다.

     

    2-3.  Private Class Fields 의 적용

    - 다행이도 2022년에 표준으로 확정이 된, 클래스에 private 필드를 정의할 수 있는 새로운 표준 사양이 제안되었다.

    - Class Fields (Private instance methods and accessors, Class Public Instance Fields & Private Instance Fields, Static class fields and private static methods)

    - 현재 크롬에서 해당 기능이 정상적으로 작동한다. 사용방법은 샵(#)을 붙이는 것이다.

    class Person {
        #age;
        constructor(name,age) {
            this.name = name; // public
            this.#age = age;
        }
        sayHi() {
            console.log(`Hi! My name is ${this.name}. I am ${this.#age}.`);
        }
    }
    
    const me = new Person('Lee', 20);
    me.sayHi(); // Hi! My name is Lee. I am 20.
    console.log(me.name); // Lee
    console.log(me); // Person {name: 'Lee', #age: 20}
    console.log(me.#age) // Uncaught SyntaxError
    
    const you = new Person('Kim', 30);
    you.sayHi(); // Hi! My name is Kim. I am 30.
    console.log(you.name); // Kim
    console.log(you); // Person {name: 'Kim', #age: 30}
    console.log(you.#age) // Uncaught SyntaxError
    
    me.sayHi(); // Hi! My name is Lee. I am 20. (값이 유지된다.)
    console.dir(you); // Person

    - console.log(me) 의 결과를 보면 #age 가 보인다. 하지만 me.#age 에는 접근하여 값을 불러올 수 없다.

    -아래 에러 사진은 코드를 VScode Live server 에서 코드를 실행 후 크롬 브라우저로 확인한 것이다.

    Private Field 접근 에러 메시지

    - 하지만 신기한 것이, 크롬 브라우저 개발자 도구 콘솔창에서 직접 코드를 실행하면 me.#age에 접근 가능하다.

    크롬 개발자 도구 콘솔창에서 접근이 가능한 Private Fields

    - 이러한 이상한 현상이 발생하는 이유는 개발자 도구의 특성으로 보인다.

    - 원래 JavaScript 문법상 let 과 const 는 같은 식별자로 다시 선언 시 오류가 발생하지만 개발자 도구 콘솔 창에서는 에러가 나지 않는 것처럼 개발자 도구 콘솔창에서는 Private Field 에 접근 가능한 것으로 보인다.

    - 이는 개발자 도구에서 웹 페이지 디버깅을 위한 조치라고 생각된다.

    크롬 개발자 도구 콘솔창에서 변수를 다시 선언해도 에러가 발생하지 않는다.

     

    2-4. 호환성

    - 2022년 표준 사양이된 Private Class Fileds 는 호완성에서 좋지 않다.

    - 최신 사양이기 때문에 크롬 브라우저 기준, 2019년 4월 이전의 구버전에서는 작동하지 않는다.

    Private Class Fields 의 호환성

    3. 자주 발생하는 실수


    - 클로저를 사용할 때 자주 발생할 수 있는 실수를 보여주는 예제다.

    - 아래 코드의 결과는 어떻게 될까?

    var funcs = [];
    
    for (var i=0; i<3; i++) {
        funcs[i]= function() { return i; }; // return 을 배열 요소로 할당한다.
    }
    
    // func = [ function(){ return i; }, function(){ return i; }, function(){ return i; } ]
    
    for (var j=0; j < funcs.length; j++) {
        console.log(funcs[j]());
    }

    - 결과는 아래와 같다.

    더보기

    3

    3

    3

    - 전역 변수 i 를 Outer Environment Reference 에 의해서 참조하기 때문에 최종적인 i 의 값이 반영되어 출려된다.

    - 위 코드를 클로저를 사용하여 올바르게 실행해보자. 아래와 같이 구현하면 클로저는 i = 0, 1, 2 일 때 총 3개 생성된다.

    - ES6 에서는 더욱 간단하다. let 키워드로 변수 선언을 해주면 된다. let 은 블록 스코를 가지기 때문에 값이 유지된다.

    // 클로저로 올바르게 실행 시키기
    var funcs = [];
    
    for(var i=0; i<3; i++){
        funcs[i] = (function (id) {
            return function () {
                return id;
            };
        }(i));
    }
    
    for(var j=0; j < funcs.length; j++) {
        console.log(funcs[j]());
    }
    // 0 1 2
    
    // let 으로 변수를 선언하면 더 쉽게 해결된다.
    var funcs1 = [];
    
    for (let i=0; i<3; i++) {
        funcs1[i]= function() { return i; };
    }
    
    for (let i=0; i < funcs1.length; i++) {
        console.log(funcs1[i]());
    }
    
    // 0 1 2

    - 이처럼 var 키워드를 사용하지 않는 (for ... in, for ... of, while 문 등) ES6의 반복문은 코드 블록을 반복 실행할 때마다 새로운 렉시컬 환경을 생성하여 반복할 당시의 상태를 마치 스냅숏을 찍는 것처럼 저장한다.

    - 단, 이는 반복문의 코드 블록 내부에서 함수를 정의할 때 의미가 있다.

    - 또 다른 방법으로 함수형 프로그래밍 기법인 고차 함수를 사용하는 방법이 있다.

    - 이 방법은 변수와 반복복문의 사용을 억제할 수 있기 때문에 오류를 줄이고 가독성을 좋게 만든다.

    - 일단 Array 생성자 함수와 Array.from 메서드를 살펴보자

    const a = Array.from(new Array(3));
    console.log (a); // [undefined, undefined, undefined]
    
    // 콜백함수를 인자로 넣을 수 있다.
    // 반복문을 쓰지 않고 배열 생성
    const a = Array.from(new Array(3), (_,i) => i); // '_' : 사용하지 않는 파라미터
    console.log(a); // [1, 2, 3]
    
    // 인덱스 활용해서 값 바꾸기
    const a = Array.from([1,2,3], (v,i) => v+i); // v: 값, i: 인덱스
    console.log(a); // [1, 3, 5]

    - 이제 배열의 요소의 요소에 클로저를 넣는 예제를 구현하면 아래와 같다.

    // 요소가 3개인 배열을 생성하고 배열의 인덱스를 반환하는 함수를 요소로 추가한다.
    // 배열의 요소로 추가된 함수들은 모두 클로저다.
    const funcs = Array.from(new Array(3), (_,i) => () => i); // [f, f, f]
    
    // 배열의 요소로 추가된 함수들을 순차적으로 호출한다.
    funcs.forEach(f => console.log(f())); // 0 1 2
    
    // 외부에서 i 에 접근할 수 없다.
    console.log(funcs); // [f, f, f]
    console.log(funcs[0]()); // 0
    console.log(funcs[1]()); // 1
    console.log(funcs[2]()); // 1

    - Array.from() 내부의 콜백함수의 반환되는 화살표 함수 '() => i' 는 외부의 i 를 참조하여 클로저가 된다.

    - i는 외부에서 접근할 수 없는 자유 변수가 된다.

    - 배열의 요소를 각각 불러와 호출하면 0, 1, 2 가 출력된다.

    4. 결론


     클로저에 이어서 자바스크립트 정보 은닉 테크닉을 배울 수 있었다. 자바스크립트에서도 다른 객체 지향 프로그래밍 언어들과 같이 공개 범위를 한정할 수 있는 접근 제한자와 관련된 표준 사양이 추가 되었다. 하지만 크롬 브라우저 기준 2019 년 4월 이전의 구버전의 브라우저에서는 인식되지 않는 호환성 문제가 있다. 결과적으로 대규모 서비스를 운영하는 협업 개발자는 자바스크립트가 정보 은닉을 완전하게 지원하지 않는다는 것을 알아둘 필요가 있다.

     이번 글을 쓰면서 크롬 개발자 도구 콘솔에서 Private Class Fields 값이 조회가 되버리는 황당한 사건이 발생해 본래 작성 계획일 보다 한참 뒤쳐졌다. 예상치 못한 에러(?)에 당황하고 멘붕이 왔었고 것인데 앞으로는 이런 일을 자연스럽게 받아들이고 더 찾아보고 검색해보는 습관을 들여야겠다고 반성했다. 다행이도 이 건은 멋사 부트캠프 정규 수업에서 Private Class Fields 를 다루며 똑같은 현상이 발생해 강사님께서 설명해주셔서 해결되었는다. 모르면 질질 끌것이 아니라 잘 아는 사람에게 물어봐야 한다!

     

     

     

     

     

     

     

     

    출처: FE재남, CH 24 클로저 https://www.youtube.com/watch?v=pTVbFD5kpOI&t=1007s 

Designed by Tistory.