본문 바로가기
👩‍💻 Programming/JavaScript

객체에 대한 액세스를 제어하자_1. 게터(getters)와 세터(setters) from Secrets of the JavaScript Ninja

by codingBear 2022. 4. 18.
728x90
반응형

이번 글은 'Secrets of the JavaScript Ninja'의 'Chapter 8. Controlling access to objects'를 바탕으로 작성하였습니다.


핵심 Concepts

  • 객체 프로퍼티에 대한 접근을 제어하는 게터와 세터의 활용법
  • 프록시(proxies)를 활용하여 객체에 대한 접근 제어하는 법
  • 횡단 관심사(cross-cutting concern)를 위한 프록시 활용법

사전 지식 체크!

  • 게터와 세터를 통해 프로퍼티의 값에 접근했을 때의 이점은 무엇인가
  • 프록시와 게터와 세터의 주된 차이점은 무엇인가
  • 프록시 트랩(proxy trap)은 무엇인가? 트랩의 세 가지 유형을 대보자

들어가며

 프로퍼티 값의 유효성을 검사하거나, 로그(log)를 남기거나, UI에 데이터를 표현하는 등의 작업을 수행할 때 객체에서 무슨 일이 일어나는지 정확하게 관찰할 필요가 있다. 이번 글에서는 객체 내에 일어나는 변화를 관찰하고 접근을 제어하는 데 쓰이는 기술들을 알아보겠다.

 우선 특정 객체 프로퍼티에 대한 저근을 제어하는 메소드인 게터와 세터부터 살펴보자. 이번 장에서는 게터와 세터가 제공하는 내장 언어 및 로그를 남기거나 데이터 유효성 검사를 수행하거나 계산된(computed) 프로퍼티를 정의하는 방법 등에 대해 다룰 것이다. 

 또한 ES6에 도입된 프록시에 대해서도 알아보겠다. 이 객체는 다른 객체에 대한 접근을 제어한다. 이러한 프록시가 어떻게 작동하고 성능 측정이나 로그 기록, 그리고 자동 완성되는 객체 프로퍼티를 통해 null 예외를 어떻게 피하는지와 같은 횡단 관심사를 활용하여 코드를 확장할 때 이것을 어떻게 사용하는지 배울 것이다.


Controlling access to properties with getters and setters

 JavaScript에서 객체는 프로퍼티의 단순한 집합이다. 프로그램 상태를 추적하는 주된 방법은 프로퍼티를 수정하는 것이다. 예를 들자면 다음과 같다.

 

 

 위 예제 코드에서 프로퍼티 skillLevel을 가지는 ninja 객체를 생성하는 Ninja 생성자를 정의하였다. 이후 프로퍼티의 값을 수정하고 싶다면 ninja.skillLevel = 20 과 같이 작성하면 된다.

 특별한 문제가 없어 보이지만 아래와 같은 경우에는 어떤 일이 일어날까?

 

  • 예기치 못한 값이 할당되는 등(예를 들어 ninja.skillLevel = "high")의 우연한 실수를 막으려면 어떻게 해야 될까?
  • skillLevel 프로퍼티의 모든 변경 내용을 기록하고 싶을 때
  • 웹 페이지 UI에 skillLevel 프로퍼티의 최신값을 표시해야 할 때

 

 이러한 문제점들을 게터와 세터를 활용하여 완벽히 해결할 수 있다.

 아래 예제는 게터와 세터를 통해서만 접근할 수 있는 프라이빗 skillLevel 프로퍼티를 가지는 Ninja를 나타낸 코드이다.

 

 

  getSkillLevel과 setSkillLevel을 통해서만 접근 가능한 프라이빗 skillLevel 변수로써 ninjas를 생성하는 Ninja 생성자를 정의한다. 프로퍼티 값은 getSkillLevel 메서드를 통해서만 얻을 수 있고 새로운 프로퍼티 값은 setSkillLevel 메서드를 통해서만 설정 가능하다. 

 skillLevel 프로퍼티에 대한 모든 읽기 기록을 남기고 싶다면 getSkillLevel 메서드를 확장하고 쓰기 기록을 기록한다면 setSkillLevel 메서드를 확장하면 된다.

 

 

 위 같이 코드를 확장하여 로그 기록, 데이터 유효성 검사, UI 수정의 부수 효과 등 프로퍼티에 대한 모든 상호작용을 확인할 수 있다.

 하지만 여기서 의문이 하나 든다. 바로 skillLevel 프로퍼티는 데이터(숫자 100)을 참조하는 값 프로퍼티이지 함수가 아니라는 점이다. 안타깝게도 제어된 접근의 모든 이점을 누리려면 모든 프로퍼티와의 상호작용을 관련된 메서드를 명시적으로 호출함으로써 만들어야 한다. 이는 조금은 낯선 개념이다.

 다행히도 JavaScript는 진정한 의미의 게터와 세터를 제공한다. 게터와 세터는 일반적인 데이터 프로퍼티(ninja.skillLevel과 같은)로서 접근되는 프로퍼티이지만 요청된 프로퍼티의 값을 계산하고 전달된 값의 유효성을 검사하는 등 필요한 작업을 수행할 수 있는 메서드이기도 하다. 이에 대해 더 자세히 알아보자.


Defining getters and setters

 JavaScript에서 게터와 세터 메소드는 두 가지 방식으로 정의 가능하다.

 

  • 객체 리터럴 혹은 ES6 클래스 정의
  • 내장 메서드 Object.defineProperty

 

 늘 그랬듯 예제를 통해 살펴보자. 다음 예제는 닌자의 목록을 저장하는 객체를 갖고 목록의 첫 번째 닌자를 가져오거나(get) 설정(set)한다.

 

 

 이 예제에서는 배열을 참조하는 일반적인 프로퍼티인 ninjas를 가지는 ninjaCollection 객체 및 firstNinja에 대한 게터와 세터를 정의한다. 게터와 세터의 일반적인 문법은 다음과 같다.

 

 

 위 예제를 보면 알 수 있듯 게터와 세터 프로퍼티를 정의할 땐 get 키워드와 set 키워드를 접두사로 사용한다.

 listing 8.2에서 게터와 세터는 메시지를 기록한다. 또한 게터는 인덱스 0에 해당하는 닌자 값을 반환하고, 세터는 같은 인덱스에 해당하는 닌자에 새로운 값을 할당한다.

 

 

 여기서 유의할 점은 게터 프로퍼티는 메서드가 아니라 마치 일반적인 객체 프로퍼티인 것처럼 액세스된다는 점이다. 

 게터 프로퍼티에 액세스하고 나면 관련된 게터 메서드는 묵시적으로 호출되고 'Getting firstNinja'라는 메시지가 기록되며 인덱스 0에 해당하는 닌자 값이 반환된다.

 계속해서 세터 메서드의 이점을 살려 firstNinja 프로퍼티를 작성하여 일반적인 객체 프로퍼티에 새 값을 할당하듯 작업을 한다.

 앞선 경우와 비슷하게 firstNinja 프로퍼티는 세터 메서드를 갖기 때문에 이 프로퍼티에 값을 할당할 때마다 세터 메소드는 묵시적으로 호출된다. 이는 "Setting firstNinja"라는 메시지를 남기고 인덱스 0에 해당하는 닌자의 값을 수정한다.

 이로써 수정 작업을 끝마치고 인덱스 0의 새로운 닌자 값에 ninjas 모음 및 게터 메소드로써 접근할 수 있는 것이다.

 게터로 프로퍼티에 접근할 때(ninjaCollection.firstNinja를 통하여) 게터 메서드는 즉시 호출되어 "Getting firstNinja"라는 메시지를 남긴다. 이후 출력값이 "Yoshi"인지, 메시지가 "Yoshi is the first ninja"로 나오는지 확인한다. firstNinja 프로퍼티에 새로운 값을 주는 작업도 이와 비슷하게 수행한다. 예제를 보면 알 수 있듯 세터 메서드가 묵시적으로 호출되어 "Setting firstNinja"라는 메시지를 남긴다.

 이러한 작업에서 중요한 점은 원래부터 게터와 세터는 일반적인 프로퍼티로서 액세스 가능한 프로퍼티를 특정하는 기능을 제공한다는 점이다. 하지만 이 둘은 프로퍼티가 액세스될 때 즉시 호출되어 실행되는 메서드이기도 하다. 아래 그림에 이 같은 내용이 자세히 나와 있다.

 

 

 게터와 세터를 정의하는 이러한 문법은 직관적이다. 다음으로는 ES6 클래스를 활용한 게터와 세터 작성법에 대해 알아보겠다.

 

 

 위 코드는 ES6 클래스를 listing 8.2를 수정한 예이다.

 

주어진 프로퍼티에 대해 항상 게터와 세터를 모두 정의할 필요는 없다. 게터만을 작성한다고 가정해보자. 이 경우 프로퍼티에 새로 값을 쓰려고 시도할 텐데 해당 작업은 스트릭트(strict) 모드인지 비스트릭트(nonstrict) 모드인지에 따라 달라진다. 비스트릭트 모드라면 프로퍼티에 값을 할당해도 게터는 아무런 값도 얻지를 못한다. JavaScript 엔진이 요청을 무시하는 것이다. 반면 스트릭트 모드에서는 JavaScript 엔진은 세터 없이 게터만 있는 프로퍼티에 값을 할당하려고 한다는 에러를 출력한다. 

 

 ES6 클래스와 객체 리터럴을 통해 게터와 세터를 특정하는 작업은 간단하다. 허나 여기서 놓친 부분이 있다. 관습적으로 게터와 세터는 프라이빗 객체 프로퍼티에 대한 액세스를 제어하는 데 쓰인다. 아쉽게도 JavaScript는 프라이빗 객체 프로퍼티를 가지지 않는다. 대신 이를 변수를 정의하고 정의한 변수를 감싸는 객체 메소드를 정의함으로써 클로저를 통해 따라 만들 수 있다. 객체 리터럴과 클래스를 사용하는 경우 게터와 세터 메서드는 프라이빗 객체 프로퍼티를 위해 사용할 수 있는 변수와 동일한 함수 범위 내에 생성되지 않기 때문에 이 같은 작업을 수행할 수 없다. 따라서 내장 메서드인  Object.defineProperty를 사용해야 한다.

 Object.defineProperty는 프로퍼티 설명자 객체를 전달하여 새로운 프로퍼티를 정의하는 데 쓰인다. 무엇보다도 프로퍼티 설명자는 프로퍼티의 게터와 세터 메소드를 정의하는 get과 set 프로퍼티를 포함한다. 

 다음 예제는 내장 게터와 세터를 사용하여 프라이빗 객체 프로퍼티에 대한 액세스를 제어하는 코드이다.

 

 

 위 예제에서는 우선  프라이빗 변수로 쓸 _skillLevel 변수를 가진 Ninja 생성자 함수를 정의한다.

 그 다음 새롭게 생성한 객체에다 this 키워드에 의해 참조되는 skillLevel 프로퍼티를 내장 메서드 Object.defineProperty로써 정의한다.

 위 코드를 통해 skillLevel 프로퍼티에 프라이빗 변수에 대한 액세스를 제어하는 기능을 부여하고자 하기 때문에 프로퍼티에 액세스할 때마다 호출되는 get과 set 메서드를 정의한다.

 객체 리터럴이나 클래스에 정의되는 게터와 세터와는 달리, Object.defineProperty를 통해 정의된 get과 set 메서드는 프라이빗 skillLevel 변수와 동일한 범위에 생성된다. 두 메서드는 프라이빗 변수 주위에 클로저를 만들어 두 메서드를 통해서만 프라이빗 변수에 액세스 가능하게 만든다. 이 같은 차이점을 제외하고 나머지 코드는 앞선 예제와 똑같이 작동한다.

 이떄까지 살펴보았듯 Object.defineProperty를 활용한 접근법은 객체 리터럴과 클래스의 게터와 세터보다 복잡하다. 하지만 프라이빗 객체 프로퍼티를 다루어야 하는 상황과 같은 특정한 경우 매우 쓸 만하다.

 게터와 세터는 정의되는 방법에 상관없이 일반적인 객체 프로퍼티처럼 쓰이는 객체 프로퍼티를 정의하게 해준다. 그러나 특정한 프로퍼티에 읽기 혹은 쓰기 작업을 할 때마다 부가적인 코드를 실행하는 메서드이기도 하다. 이는 로그를 남기고, 할당된 값의 유효성을 검사하며, 특정 변화가 일어날 때 코드의 나머지 부분에 알려주는 데 매우 유용하다. 이러한 기능을 구현한 예를 살펴보자.


Using getters and setters to validate property values

 이때까지 살펴봤듯 세터는 일치하는 프로퍼티에다 새 값을 쓸 때마다 실행되는 메서드이다. 이러한 특징을 프로퍼티의 값을 갱신하려고 할 때마다 일어나는 작업을 구현하는 데 쓸 수 있다. 이러한 작업 중 하나는 바로 전달된 값의 유효성을 검사하는 것이다. 다음 예제는 skillLevel 프로퍼티에 정수가 할당되었는지 검사하는 코드이다.

 

 

 위 예제 코드는 앞선 listing 8.4에 코드를 몇 줄 추가한 것이다. 가장 큰 차이점 중 하나는 skillLevel 프로퍼티에 새로운 값이 할당될 때마다 전달된 값이 정수인지 확인한다는 점이다. 만일 정수가 아니라면 예외가 일어나고 프라이빗 _skillLevel 변수는 수정되지 않는다. 모든 것이 잘 작동하여 정수 값을 받으면 프라이빗 _skillLevel 변수에 새 값이 할당되고 작업은 끝난다. 

 위 코드에서는 try-catch문을 활용하여 문자열을 skillLevel 프로퍼티에 할당할 수 있는지 점검하였는데 예외가 일어나게 된다. 

 이 같은 작업을 통해 특정 프로퍼티에 엉뚱한 유형의 값이 할당되지 않도록 방지할 수 있다. 특히 JavaScript는 값이 수시로 바뀌는 동적인 언어이기 때문에 이 같은 작업이 더 유용하게 쓰인다. 

 이는 세터 메서드의 유용한 사용법 중 하나에 불과하다. 이 같은 원리로써 값 변경 이력을 추적하거나 로그를 남기거나 변경 알림을 구현하는 등 다양한 데 기능을 구현 가능하다.


Using getters and setters to define computed properties

 특정 객체 프로퍼티에 대한 액세스를 제어하는 것 이외에도 게터와 세터는 각 요청당 값이 계산되는 프로퍼티인 계산된 프로퍼티(computed properties)를 정의할 수 있다. 계산된 프로퍼티는 값을 저장하지 않는다. 대신 간접적으로 다른 프로퍼티를 검색하고 설정하기 위해 get 혹은 set 메서드를 제공하는 것이다. 다음 예제는 프로퍼티 fullTitle의 값을 계산하는 name과 clan이라는 두 가지 프로퍼티를 가진 코드이다.

 

 

 우선 name과 clan이라는 두 가지 프로퍼티를 가진 shogun 객체를 정의한다. 또한 계산된 프로퍼티인 fullTitle에 대한 게터와 세터도 함께 정의한다.

 get 메서드는 요청이 있을 때 name과 clan 프로퍼티를 합쳐서 fullTitle 프로퍼티의 값을 계산해낸다. set 메서드는 내장 메서드인 split을 활용하여 할당된 문자열을 공백 문자 기준으로 조각 내는 기능을 한다. 첫 번째 조각은 name 프로퍼티에 할당된 name 값이고 두 번째 조각은 clan 프로퍼티에 할당된 clan 값이다. 

 위 코드는 두 가지 작업을 수행하는 것이다. fullTitle 프로퍼티를 읽는 것은 해당 값을 산출해내고 fullTitle 프로퍼티에 쓰는 것은 프로퍼티 값을 구성하는 프로퍼티를 수정한다.

 계산된 프로퍼티를 쓰면 코드의 개념적 명확성을 향상시킬 수 있다. 만약 특정한 값(여기서는 fullTitle 값)이 객체의 내부 상태에만 의존한다고 하면(여기서는 name 및 clan 프로퍼티) 함수 대신 데이터필드, 프로퍼티로 표현하는 것이 더 바람직하다. 


함께 보기

  •  getter & setter

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get

 

getter - JavaScript | MDN

The get syntax binds an object property to a function that will be called when that property is looked up.

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/set

 

설정자 - JavaScript | MDN

set 구문은 객체의 속성에 할당을 시도할 때 호출할 함수를 바인딩합니다.

developer.mozilla.org

https://ko.javascript.info/property-accessors

 

프로퍼티 getter와 setter

 

ko.javascript.info

 

728x90
반응형

댓글