[Javascript] Closure

closure는 주변 state(lexical environment를 의미)에 대한 참조와 함께 묶인 함수의 조합이다. 다시말해서, closure는 inner function이 outer function의 scope를 접근할 수 있게 해준다. JavaScript에서 closure는 함수 생성 시간에 함수가 생성될 때마다 만들어진다.

Lexical scoping

아래 예제를 보자

function init() {
  var name = 'Mozilla'; // name is a local variable created by init
  function displayName() { // displayName() is the inner function, a closure
    alert(name); // use variable declared in the parent function
  }
  displayName();
}
init();

closure는 inner function이 outer function의 scope에 접근할 수 있기 때문에 위의 예제에서 inner function인 displayName()이 outer function인 init()의 local 변수 name을 참조하고 있다.

lexical scoping은 nested 함수에서 변수 이름이 확인되는 방식을 정의한다. inner function은 parent function이 return 되었더라고 parent function의 scope를 가지고 있다. 아래 예제를 보자

/* lexical scope (also called static scope)*/
function func() {
    var x = 5;
    function func2() {
        console.log(x);
    }
    func2();
}

func() // print 5
/* dynamic scope */
function func() {
    console.log(x)
}

function dummy1() {
    x = 5;
    func();
}

function dummy2() {
    x = 10;
    func();
}

dummy1() // print 5
dummy2() // print 10

첫 번째 예제는 compile-time에 추론할 수 있기 때문에 static이며 두 번째 예제는 outer scope가 dynamic 하고 function의 chain call에 의존하기 때문에 dynamic이라고 불린다.

Closure

function makeFunc() {
  var name = 'Mozilla';
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

위의 예제는 처음의 init() 함수와 같은 효과를 가진다. 차이점은 inner function인 displayName()이 outer function이 실행되기 이전에 return 되었다는 것이다.

다른 programming language에서는 함수의 local variable은 함수가 실행되는 동안에서만 존재한다. makeFunc()가 호출되고 끝난다음에 더 이상 name 변수에 접근하지 못해야 할 것 같지만 JavaScript에서는 그렇지 않다.

그 이유는 JavaScript의 함수가 closure를 형성하기 때문이다. closure란 함수와 lexical environment의 조합이다. 이 environment는 closure가 생설 될 때 scope 내에 있던 모든 local 변수로 구성된다. 위의 경우에, myFunc는 makeFunc가 실행될 때 만들어진 displayName의 instance를 참조한다. displayName의 instance는 name 변수를 가진 lexical environment를 참조하는 것을 유지한다. 이러현 이유로 myFunc가 실행 될 때, name 변수는 사용가능한 상태로 남아있다.

closure는 매우 유용하다. 왜냐하면 data와 함수를 연결 시켜주기 때문이다. 이것은 data와 하나 또는 여러개의 method와 연결 되어있는 OOP(object-oriented programming)과 똑같다.

결국 closure를 이용하여 OOP의 object로 이용할 수 있다.

Emulating private methods with closures

Java와 다르게 JavaScript은 private를 구현하기 위한 native 방법을 제공하지 않는다. 그러나 closure를 통해서 private를 구현할 수 있다.

아래 예제는 Module Design Pattern을 따른다.

var counter = (function() {
  var privateCounter = 0;

  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment: function() {
      changeBy(1);
    },

    decrement: function() {
      changeBy(-1);
    },

    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value());  // 0.

counter.increment();
counter.increment();
console.log(counter.value());  // 2.

counter.decrement();
console.log(counter.value());  // 1.

위의 예제에서 counter.increment 와 counter.decrement, counter.value는 같은 lexical environment를 공유하고 있다.

공유된 lexical environment는 선언가 동시에 실행되는 anonymous function(IIFE)의 body에 생성되어 있다. lexical environment는 private 변수와 함수를 가지고 있어 anonymous function의 외부에서 접근할 수 없다.

아래는 anonymous function이 아닌 function을 사용한 예제이다

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },

    decrement: function() {
      changeBy(-1);
    },

    value: function() {
      return privateCounter;
    }
  }
};

var counter1 = makeCounter();
var counter2 = makeCounter();

alert(counter1.value());  // 0.

counter1.increment();
counter1.increment();
alert(counter1.value()); // 2.

counter1.decrement();
alert(counter1.value()); // 1.
alert(counter2.value()); // 0.

위의 예제는 closure 보다는 object를 사용하는 것을 추천한다. 위에서 makeCounter() 가 호출될 때마다 increment, decrement, value 함수들이 새로 assign되어 오버헤드가 발생한다. 즉, object의 prototype에 함수들을 선언하고 object를 운용하는 것이 더 효율적이다.

function makeCounter() {
    this.publicCounter = 0;
}

makeCounter.prototype = {
    changeBy : function(val) {
        this.publicCounter += val;
    },
    increment : function() {
        this.changeBy(1);
    },
    decrement : function() {
        this.changeBy(-1);
    },
    value : function() {
        return this.publicCounter;
    }
}
var counter1 = new makeCounter();
var counter2 = new makeCounter();

alert(counter1.value());  // 0.

counter1.increment();
counter1.increment();
alert(counter1.value()); // 2.

counter1.decrement();
alert(counter1.value()); // 1.
alert(counter2.value()); // 0.

Closure Scope Chain

모든 closure는 3가지 scope를 가지고 있다.

  • Local Scope(Own scope)
  • Outer Functions Scope
  • Global Scope
// global scope
var e = 10;
function sum(a){
  return function(b){
    return function(c){
      // outer functions scope
      return function(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

console.log(sum(1)(2)(3)(4)); // log 20

// You can also write without anonymous functions:

// global scope
var e = 10;
function sum(a){
  return function sum2(b){
    return function sum3(c){
      // outer functions scope
      return function sum4(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

var s = sum(1);
var s1 = s(2);
var s2 = s1(3);
var s3 = s2(4);
console.log(s3) //log 20

위의 예제를 통해서 closure는 모든 outer function scope를 가진다는 것을 알 수 있다.

Creating closures in loops: A common mistake

아래 예제를 보자

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

위의 코드는 정상적으로 동작하지 않는다. 모든 element에서 age의 help text가 보일 것이다. 그 이유는 onfocus가 closure이기 때문이다. closure는 function 선언과 setupHelp의 fucntion scope를 가지고 있다. 3개의 closure를 loop에 의해서 만들어지며 같은 lexical environment를 공유하고 있다. 하지만 item은 var로 선언이 되어있어 hoisting이 일어난다. item.help는 onfocus 함수가 실행될 때 결정되므로 항상 age의 help text가 전달이 된다. 아래는 해결방법이다.

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

하나의 lexical environment를 공유하는 대신 makeHekpCallback 함수가 새로운 lexical environment를 만들었다.

다른 방법으로는 anonymous closure(IIFE)를 이용한다.

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

let keyword를 사용해서 해결할 수 있다.

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (let i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

Performane consideration

closure가 필요하지 않을 때 closure를 만드는 것은 메모리와 속도에 악영향을 끼치낟.

예를들어, 새로운 object/class를 만들 때, method는 object의 생성자 대신에 object의 prototype에 있는 것이 좋다. 왜냐하면 생성자가 호출될 때마다, method는 reassign 되기 때문이다.

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

위의 예제에서 getName과 getMessage는 생성자가 호출될 때마다 reaasign된다.

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

prototype 전부를 다시 재선언하는 것은 추천하지 않는다.

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

'Language > Javascript' 카테고리의 다른 글

[Javasript] Object Prototype  (0) 2022.05.08
[Javascript] ES2015+ 요약 정리  (0) 2022.05.08

Object Prototype

Prototype은 JavaScript object가 다른 object에서 상속하는 매커니즘이다.

A prototype-based language?

JavaScript는 종종 prototype-based language로 설명된다. prototype-based language는 상속을 지원하고 object는 prototype object를 갖는다. prototyp object는 method와 property를 상속하는 template object 같은 것이다.

object의 prototype object 또한 prototype object를 가지고 있으며 이것을 prototype chain 이라고 부른다.

JavaScript에서 연결은 object instance와 prototype(_proto_ 속성 또는 constructor의 prototype 속성) 사이에 만들어진다

Understanding prototype objects

아래 예제를 보자.

function Person(first, last, age, gender, interests) {

  // property and method definitions
  this.name = {
    'first': first,
    'last' : last
  };
  this.age = age;
  this.gender = gender;
  //...see link in summary above for full definition
}

우리는 object instace를 아래와 같이 만들 수 있다.

let person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);

person1에 있는 method를 부른다면 어떤일이 발생할 것인가?

person1.valueOf()

valueOf()를 호출하면

  • 브라우저는 person1 object가 valueOf() method를 가졌는지 확인한다. 즉, 생성자인 Person()에 정의되어 있는지 확인한다.
  • 그렇지 않다면 person1의 prototype object를 확인한다. prototype object에 method가 없다면 prototype object의 prototype object를 확인하며 prototype object가 null이 될 때까지 탐색한다.

'Language > Javascript' 카테고리의 다른 글

[Javascript] Closure  (0) 2022.05.08
[Javascript] ES2015+ 요약 정리  (0) 2022.05.08

[Javascript] ES2015+ 요약 정리



ES2015+의 등장

기존의 자바스크립트 문법에 다른 언어의 장점들을 더한 편리한 기능들이 많이 추가되었다. 이 중에 활용도가 높은 부분에 대해서 알아보자.

 

1. const, let


자바스크립트에서 변수를 선언할 때 var를 이용해왔다. 하지만 이제 var는 const와 let으로 대체할 것이다.

const와 let은 함수 스코프를 가지는 var와는 달리 블록 스코프를 갖는다.

블록 스코프는 if, while, for, function 등에서 사용하는 중괄호에 속하는 범위를 뜻한다. 따라서 const와 let을 이 중괄호 안에서 사용하게 된다면, 그 스코프 범위 안에서만 접근이 가능하다. 이를 통해 호이스팅에 관련된 문제는 자연스럽게 해결할 수 있다.

 

그렇다면 const와 let은 무슨 차이일까?

간단히 말해서 let은 대입한 값을 계속 수정할 수 있지만, const는 한번 대입하면 다른 값 대입을 할 수 없고 초기화 시 값이 필요다.

const a = 0;
a = 1; // error


let b = 0;
b = 1; // 1

const c; // error 



2. 템플릿 문자열


백틱(`)을 이용해 새로운 문자열을 만들 수 있다.

백틱은 키보드에서 tab키 위에 있다

 

백틱을 활용해서 문자열 안에 변수도 넣을 수 있게 되었다. 기존에는 변수가 등장할 때마다 따옴표를 닫고 +를 통해 연결했는데 이제 백틱을 활용하면 변수가 포함된 문자열을 한번에 모두 작성이 가능해졌다.

 

var string = num1 + ' + ' + num2 + ' = ' + result;

const string = `${num1} + ${num2} = ${result}`;

 

아래가 훨씬 가독성이 좋아졌다. 또한, 백틱 안에 따옴표를 함께 작성하는 것도 가능하다.



3. 객체 리터럴


다음 코드는 oldObject 객체에 동적으로 속성을 추가하는 상황이다.

  • 기존 코드
var sayNode = function() {
    console.log('Node');
};
 
var es = 'ES';
var oldObject = {
    sayJS: function(){
        console.log('JS');
    },
    sayNode: sayNode,
};
 
oldObject[es + 6] = 'Fantastic';
 
oldObject.sayNode();
oldObject.sayJS();
console.log(oldObject.ES6);

 

이제 위와 같은 코드를 아래처럼 수정할 수 있다.

var sayNode = function() {
    console.log('Node');
};
 
var es = 'ES';
 
const newObject = {
    sayJS() {
        console.log('JS');
    },
    sayNode,
    [es+6]: 'Fantastic',
};
 
newObject.sayNode();
newObject.sayJS();
console.log(newObject.ES6);

 

oldObject와 newObject를 비교해보자.

객체의 메서드에 함수를 연결할 때 이제 :와 같은 콜론과 function을 붙이지 않아도 가능해졌다.

또한 sayNode : sayNode와 같이 중복되는 이름의 변수는 그냥 간단히 sayNode 하나만 작성하면 된다.

또한 객체의 속성명을 동적으로 생성이 가능하다. 이전에는 객체 리터럴 바깥에서 [es+6]으로 만들었지만, 이제 객체 리터럴 안에서 만들 수 있는 모습을 확인할 수 있다.

코드의 양을 줄이고, 편리하니 익숙해지면 좋다.



4. 화살표 함수


기존의 function {}도 이전처럼 사용이 가능하지만, ES2015 이후로 화살표 함수가 생기면서 많이 사용되고 있다.

function add1(x, y) {
	return x+y;
}

const add2 = (x, y) => x + y;

 

두 가지 모두 똑같은 기능을 하는 함수다. 하지만 화살표 함수에서는 function 대신 => 기호로 선언한다. 이는 return문을 줄일 수 있는 장점이 있다. 또한 화살표 함수는 function과 this 바인드 방식에서 차이점이 존재한다.

 

  • 기존 코드
var relationship1 = {
    name: 'kim',
    friends: ['a', 'b', 'c'],
    logFriends: function() {
        var that = this; // relationship1을 가리키는 this를 that에 저장
 
        this.friends.forEach(function(friend){
            console.log(that.name, friend);
        });
    },
};
relationship1.logFriends();

 

relationship1.logFriends()에서 forEach문 안에 function 선언문을 사용했다.

이로써 각자 다른 함수 스코프 this를 가지게 되므로 friends 값을 가져오기 위해서 that이라는 변수를 만들어 이에 this 값을 미리 저장해놓는 모습이다.

 

const relationship2 = {
    name: 'kim',
    friends: ['a', 'b', 'c'],
    logFriends() {
        this.friends.forEach(friend => {
            console.log(this.name, friend);
        });
    },
};
relationship2.logFriends();

 

이번에는 forEach문에서 function을 선언하지 않고 화살표 함수를 사용했다.

따라서 바로 바깥 스코프인 logFriends()의 this를 그대로 사용이 가능한 상황입니다. 이런 상황에서는 function 대신 화살표 함수를 사용하면서 따로 바깥 스코프의 this를 저장해놓고 불러오지 않아도 되서 편리하다.



5. 비구조화 할당


객체나 배열에서 속성 혹은 요소를 꺼내올때 사용한다.

 

  • 기존 코드
var candyMachine = {
    status: {
        name: 'node',
        count: 5,
    },
    getCandy: function(){
        return "Hi";
    }
};
 
var getCandy = candyMachine.getCandy;
var count = candyMachine.status.count;

기존에는 객체에서 속성을 가져올 때 이처럼 작성했다.

 

const candyMachine1 = {
    status: {
        name: 'node',
        count: 5,
    },
    getCandy1() {
        return "Hi";
    }
};
 
const { getCandy1, status: { count } } = candyMachine1;

console.log(getCandy1()) // Hi
console.log(count) // 5

하지만 이처럼 간단하게 한 줄로 나타내는 것이 가능해졌다. 여러 단계 안의 속성도 count1을 가져오는 것처럼 작성이 가능하다.

 

이 뿐만 아니라, 배열에서도 마찬가지로 적용이 가능하다.

var array = ['nodejs', {}, 10, true];
var node = array[0];
var obj = array[1];
var bool = array[array.length - 1];

 

array라는 배열 안에 4가지 요소를 넣고 가져오는 모습을 확인할 수 있다.

 

const array1 = ['nodejs', {}, 10, true];
const [node, obj, , bool] = array1;

 

bool은 true를 가져오기 위해 배열의 마지막 부분에 작성한 걸 볼 수 있다. 이처럼 작성하면 맨 끝이라고 자동으로 인식해주니 상당히 편한 장점이 있다.

 

이처럼 비구조화 할당을 이용하면, 배열이 위치마다 변수를 넣어 똑같은 역할을 하도록 만들 수 있다. 코드 줄도 상당히 줄일 수 있고, 특히 Node.js에서는 모듈을 사용하기 때문에 이런 방식이 많이 사용된다고 한다.



6. 프로미스(promise)


자바스크립트와 Node는 비동기 프로그래밍으로 이벤트 주도 방식을 활용하면서 콜백 함수를 많이 사용하게 된다. 콜백 함수 자체가 복잡한 것도 있고, 이해하기 어려운 자바스크립트 내용 중 하나이기도 하다.

 

이에 ES2015부터는 콜백 대신 API들이 프로미스 기반으로 재구성되고 있다. 따라서 프로미스에 대해 잘 이해하고 사용하게 된다면, 복잡한 콜백 함수의 지옥에서 벗어날 수 있으니 확실히 알고 있어야 한다.

 

promise 객체 구조는 아래와 같다.

const condition = true;
 
const promise = new Promise((resolve, reject) => {
    if (condition){
        resolve('성공');
    } else {
        reject('실패');
    }
});
 
promise
    .then((message) => {
        console.log(message);
    })
    .catch((error) => {
        console.log(error);
    });

 

new Promise로 프로미스를 생성할 수 있다. 그리고 안에 resolve와 reject를 매개변수로 갖는 콜백 함수를 넣는 방식이다.

이제 선언한 promise 변수에 then과 catch 메서드를 붙이는 것이 가능하다.

resolve가 호출되면 then이 실행되고, reject가 호출되면 catch가 실행된다.

이제 resolve와 reject에 넣어준 인자는 각각 then과 catch의 매개변수에서 받을 수 있게 되었다.

즉, condition이 true가 되면 resolve('성공')이 호출되어 message에 '성공'이 들어가 log로 출력된다. 반대로 false면 reject('실패')가 호출되어 catch문이 실행되고 error에 '실패'가 되어 출력될 것이다.

 

이제 이러한 방식을 활용해 콜백을 프로미스로 바꿔보자.

function findAndSaveUser(Users) {
    Users.findOne({}, (err, user) => { // 첫번째 콜백
        if(err) {
            return console.error(err);
        }
        user.name = 'kim';
        user.save((err) => { // 두번째 콜백
            if(err) {
                return console.error(err);
            }
            Users.findOne({gender: 'm'}, (err, user) => { // 세번째 콜백
                // 생략
            });
        });
    });
}

 

보통 콜백 함수를 사용하는 패턴은 이와 같이 작성할 것이다. 현재 콜백 함수가 세 번 중첩된 모습을 볼 수 있다.

즉, 콜백 함수가 나올때 마다 코드가 깊어지고 각 콜백 함수마다 에러도 따로 처리해주고 있다.

 

프로미스를 활용하면 아래와 같이 작성이 가능하다.

function findAndSaveUser1(Users) {
    Users.findOne({})
        .then((user) => {
            user.name = 'kim';
            return user.save();
        })
        .then((user) => {
            return Users.findOne({gender: 'm'});
        })
        .then((user) => {
            // 생략
        })
        .catch(err => {
            console.error(err);
        });
}

 

then을 활용해 코드가 깊어지지 않도록 만들었다. 이때, then 메서드들은 순차적으로 실행된다.

에러는 마지막 catch를 통해 한번에 처리가 가능하다. 하지만 모든 콜백 함수를 이처럼 고칠 수 있는 건 아니고, find와 save 메서드가 프로미스 방식을 지원하기 때문에 가능한 상황이다.

지원하지 않는 콜백 함수는 util.promisify를 통해 가능하다.

 

프로미스 여러개를 한꺼번에 실행할 수 있는 방법은 Promise.all을 활용하면 된다.

const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
 
Promise.all([promise1, promise2])
    .then((result) => {
        console.log(result);
    })
    .catch((error) => {
        console.error(err);
    });

 

promise.all에 해당하는 모든 프로미스가 resolve 상태여야 then으로 넘어간다. 만약 하나라도 reject가 있다면, catch문으로 넘어간다.

기존의 콜백을 활용했다면, 여러번 중첩해서 구현했어야하지만 프로미스를 사용하면 이처럼 깔끔하게 만들 수 있다.



7. async/await


ES2017에 추가된 최신 기능이며, Node에서는 7,6버전부터 지원하는 기능이다. Node처럼 비동기 프로그래밍을 할 때 유용하게 사용되고, 콜백의 복잡성을 해결하기 위한 프로미스를 조금 더 깔끔하게 만들어주는 도움을 준다.

 

이전에 학습한 프로미스 코드를 가져와보자.

function findAndSaveUser1(Users) {
    Users.findOne({})
        .then((user) => {
            user.name = 'kim';
            return user.save();
        })
        .then((user) => {
            return Users.findOne({gender: 'm'});
        })
        .then((user) => {
            // 생략
        })
        .catch(err => {
            console.error(err);
        });
}

 

콜백의 깊이 문제를 해결하기는 했지만, 여전히 코드가 길긴 하다. 여기에 async/await 문법을 사용하면 아래와 같이 바꿀 수 있다.

 

async function findAndSaveUser(Users) {
    try{
        let user = await Users.findOne({});
        user.name = 'kim';
        user = await user.save();
        user = await Users.findOne({gender: 'm'});
        // 생략
 
    } catch(err) {
        console.error(err);
    } 
}

 

상당히 짧아진 모습을 볼 수 있다.

function 앞에 async을 붙여주고, 프로미스 앞에 await을 붙여주면 된다. await을 붙인 프로미스가 resolve될 때까지 기다린 후 다음 로직으로 넘어가는 방식이다.

 

앞에서 배운 화살표 함수로 나타냈을 때 async/await을 사용하면 아래와 같다.

const findAndSaveUser = async (Users) => {
    try{
        let user = await Users.findOne({});
        user.name = 'kim';
        user = await user.save();
        user = await user.findOne({gender: 'm'});
    } catch(err){
        console.error(err);
    }
}

 

화살표 함수를 사용하면서도 async/await으로 비교적 간단히 코드를 작성할 수 있다.

예전에는 중첩된 콜백함수를 활용한 구현이 당연시 되었지만, 이제 그런 상황에 async/await을 적극 활용해 작성하는 연습을 해보면 좋을 것이다.



[참고 자료]

'Language > Javascript' 카테고리의 다른 글

[Javascript] Closure  (0) 2022.05.08
[Javasript] Object Prototype  (0) 2022.05.08

Spring Security - Authentication and Authorization

 

API에 권한 기능이 없으면, 아무나 회원 정보를 조회하고 수정하고 삭제할 수 있다. 따라서 이를 막기 위해 인증된 유저만 API를 사용할 수 있도록 해야하는데, 이때 사용할 수 있는 해결 책 중 하나가 Spring Security다.

 

스프링 프레임워크에서는 인증 및 권한 부여로 리소스 사용을 컨트롤 할 수 있는 Spring Security를 제공한다. 이 프레임워크를 사용하면, 보안 처리를 자체적으로 구현하지 않아도 쉽게 필요한 기능을 구현할 수 있다.

 

 

Spring Security는 스프링의 DispatcherServlet 앞단에 Filter 형태로 위치한다. Dispatcher로 넘어가기 전에 이 Filter가 요청을 가로채서, 클라이언트의 리소스 접근 권한을 확인하고, 없는 경우에는 인증 요청 화면으로 자동 리다이렉트한다.

 

Spring Security Filter

Filter의 종류는 상당히 많다. 위에서 예시로 든 클라이언트가 리소스에 대한 접근 권한이 없을 때 처리를 담당하는 필터는 UsernamePasswordAuthenticationFilter다.

인증 권한이 없을 때 오류를 JSON으로 내려주기 위해 해당 필터가 실행되기 전 처리가 필요할 것이다.

 

API 인증 및 권한 부여를 위한 작업 순서는 아래와 같이 구성할 수 있다.

  1. 회원 가입, 로그인 API 구현
  2. 리소스 접근 가능한 ROLE_USER 권한을 가입 회원에게 부여
  3. Spring Security 설정에서 ROLE_USER 권한을 가지면 접근 가능하도록 세팅
  4. 권한이 있는 회원이 로그인 성공하면 리소스 접근 가능한 JWT 토큰 발급
  5. 해당 회원은 권한이 필요한 API 접근 시 JWT 보안 토큰을 사용

 

이처럼 접근 제한이 필요한 API에는 보안 토큰을 통해서 이 유저가 권한이 있는지 여부를 Spring Security를 통해 체크하고 리소스를 요청할 수 있도록 구성할 수 있다.

 

Spring Security Configuration

서버에 보안을 설정하기 위해 Configuration을 만든다. 기존 예시처럼, USER에 대한 권한을 설정하기 위한 작업도 여기서 진행된다.

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable() // rest api 이므로 기본설정 사용안함. 기본설정은 비인증시 로그인폼 화면으로 리다이렉트
                .cors().configurationSource(corsConfigurationSource())
                .and()
                .csrf().disable() // rest api이므로 csrf 보안이 필요없으므로 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증하므로 세션은 필요없으므로 생성안함.
                .and()
                .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크
                .antMatchers("/*/signin", "/*/signin/**", "/*/signup", "/*/signup/**", "/social/**").permitAll() // 가입 및 인증 주소는 누구나 접근가능
                .antMatchers(HttpMethod.GET, "home/**").permitAll() // home으로 시작하는 GET요청 리소스는 누구나 접근가능
                .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); // jwt token 필터를 id/password 인증 필터 전에 넣는다

    }



[참고 자료]

'Web > Spring' 카테고리의 다른 글

[Spring Data JPA] 더티 체킹 (Dirty Checking)  (0) 2022.05.08
JPA  (0) 2022.05.08
[Spring Boot] Test Code  (0) 2022.05.08
[Spring Boot] SpringApplication  (0) 2022.05.08
Spring MVC  (0) 2022.05.08

[Spring Data JPA] 더티 체킹 (Dirty Checking)

 

 

트랜잭션 안에서 Entity의 변경이 일어났을 때
변경한 내용을 자동으로 DB에 반영하는 것

 

ORM 구현체 개발 시 더티 체킹이라는 말을 자주 볼 수 있다.

더티 체킹이 어떤 것을 뜻하는 지 간단히 살펴보자.

 

JPA로 개발하는 경우 구현한 한 가지 기능을 예로 들어보자

ex) 주문 취소 기능

@Transactional  
public void cancelOrder(Long orderId) {  
    //주문 엔티티 조회  
    Order order = orderRepository.findOne(orderId);  

    //주문 취소  
    order.cancel();  
}

orderId를 통해 주문을 취소하는 메소드다. 데이터베이스에 반영하기 위해선, update와 같은 쿼리가 있어야할 것 같은데 존재하지 않는다.

하지만, 실제로 이 메소드를 실행하면 데이터베이스에 update가 잘 이루어진다.

  • 트랜잭션 시작
  • orderId로 주문 Entity 조회
  • 해당 Entity 주문 취소 상태로 Update
  • 트랜잭션 커밋

이를 가능하게 하는 것이 바로 '더티 체킹(Dirty Checking)'이라고 보면 된다.

 

그냥 더티 체킹의 단어만 간단히 해석하면 변경 감지로 볼 수 있다. 좀 더 자세히 말하면, Entity에서 변경이 일어난 걸 감지한 뒤, 데이터베이스에 반영시켜준다는 의미다. (변경은 최초 조회 상태가 기준이다)

Dirty : 상태의 변화가 생김

Checking : 검사

JPA에서는 트랜잭션이 끝나는 시점에 변화가 있던 모든 엔티티의 객체를 데이터베이스로 알아서 반영을 시켜준다. 즉, 트랜잭션의 마지막 시점에서 다른 점을 발견했을 때 데이터베이스로 update 쿼리를 날려주는 것이다.

  • JPA에서 Entity를 조회
  • 조회된 상태의 Entity에 대한 스냅샷 생성
  • 트랜잭션 커밋 후 해당 스냅샷과 현재 Entity 상태의 다른 점을 체크
  • 다른 점들을 update 쿼리로 데이터베이스에 전달

 

이때 더티 체킹을 검사하는 대상은 영속성 컨텍스트가 관리하는 Entity로만 대상으로 한다.

준영속, 비영속 Entity는 값을 변경할 지라도 데이터베이스에 반영시키지 않는다.

 

기본적으로 더티 체킹을 실행하면, SQL에서는 변경된 엔티티의 모든 내용을 update 쿼리로 만들어 전달하는데, 이때 필드가 많아지면 전체 필드를 update하는게 비효율적일 수도 있다.

이때는 @DynamicUpdate를 해당 Entity에 선언하여 변경 필드만 반영시키도록 만들어줄 수 있다.

@Getter
@NoArgsConstructor
@Entity
@DynamicUpdate
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String product;



[참고 자료]

'Web > Spring' 카테고리의 다른 글

Spring Security - Authentication and Authorization  (0) 2022.05.08
JPA  (0) 2022.05.08
[Spring Boot] Test Code  (0) 2022.05.08
[Spring Boot] SpringApplication  (0) 2022.05.08
Spring MVC  (0) 2022.05.08

JPA

Java Persistence API

 

개발자가 직접 SQL을 작성하지 않고, JPA API를 활용해 DB를 저장하고 관리할 수 있다.

 

JPA는 오늘날 스프링에서 많이 활용되고 있지만, 스프링이 제공하는 API가 아닌 자바가 제공하는 API다.

자바 ORM 기술에 대한 표준 명세로, 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스다.

 

ORM(Object Relational Mapping)

ORM 프레임워크는 자바 객체와 관계형 DB를 매핑한다. 즉, 객체가 DB 테이블이 되도록 만들어주는 것이다. ORM을 사용하면, SQL을 작성하지 않아도 직관적인 메소드로 데이터를 조작할 수 있다는 장점이 있다. ( 개발자에게 생산성을 향상시켜줄 수 있음 )

종류로는 Hibernate, EclipseLink, DataNucleus 등이 있다.

 

스프링 부트에서는 spring-boot-starter-data-jpa로 패키지를 가져와 사용하며, 이는 Hibernate 프레임워크를 활용한다.


JPA는 애플리케이션과 JDBC 사이에서 동작하며, 개발자가 JPA를 활용했을 때 JDBC API를 통해 SQL을 호출하여 데이터베이스와 호출하는 전개가 이루어진다.

즉, 개발자는 JPA의 활용법만 익히면 DB 쿼리 구현없이 데이터베이스를 관리할 수 있다.

 

JPA 특징

  1. 객체 중심 개발 가능SQL 중심 개발이 이루어진다면, CRUD 작업이 반복해서 이루어져야한다.
  2. 하나의 테이블을 생성해야할 때 이에 해당하는 CRUD를 전부 만들어야 하며, 추후에 컬럼이 생성되면 관련 SQL을 모두 수정해야 하는 번거로움이 있다. 또한 개발 과정에서 실수할 가능성도 높아진다.
  3. 생산성 증가SQL 쿼리를 직접 생성하지 않고, 만들어진 객체에 JPA 메소드를 활용해 데이터베이스를 다루기 때문에 개발자에게 매우 편리성을 제공해준다.
  4. 유지보수 용이쿼리 수정이 필요할 때, 이를 담아야 할 DTO 필드도 모두 변경해야 하는 작업이 필요하지만 JPA에서는 엔티티 클래스 정보만 변경하면 되므로 유지보수에 용이하다.
  5. 성능 증가사람이 직접 SQL을 짜는 것과 비교해서 JPA는 동일한 쿼리에 대한 캐시 기능을 지원해주기 때문에 비교적 높은 성능 효율을 경험할 수 있다.

 

제약사항

JPA는 복잡한 쿼리보다는 실시간 쿼리에 최적화되어있다. 예를 들어 통계 처리와 같은 복잡한 작업이 필요한 경우에는 기존의 Mybatis와 같은 Mapper 방식이 더 효율적일 수 있다.

Spring에서는 JPA와 Mybatis를 같이 사용할 수 있기 때문에, 상황에 맞는 방식을 택하여 개발하면 된다.



[참고 사항]

'Web > Spring' 카테고리의 다른 글

Spring Security - Authentication and Authorization  (0) 2022.05.08
[Spring Data JPA] 더티 체킹 (Dirty Checking)  (0) 2022.05.08
[Spring Boot] Test Code  (0) 2022.05.08
[Spring Boot] SpringApplication  (0) 2022.05.08
Spring MVC  (0) 2022.05.08

[Spring Boot] Test Code

 

테스트 코드를 작성해야 하는 이유

  • 개발단계 초기에 문제를 발견할 수 있음
  • 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 시 기존 기능이 잘 작동하는 지 확인 가능함
  • 기능에 대한 불확실성 감소

 

개발 코드 이외에 테스트 코드를 작성하는 일은 개발 시간이 늘어날 것이라고 생각할 수 있다. 하지만 내 코드에 오류가 있는 지 검증할 때, 테스트 코드를 작성하지 않고 진행한다면 더 시간 소모가 클 것이다.

1. 코드를 작성한 뒤 프로그램을 실행하여 서버를 킨다.
2. API 프로그램(ex. Postman)으로 HTTP 요청 후 결과를 Print로 찍어서 확인한다.
3. 결과가 예상과 다르면, 다시 프로그램을 종료한 뒤 코드를 수정하고 반복한다.

위와 같은 방식이 얼마나 반복될 지 모른다. 그리고 하나의 기능마다 저렇게 테스트를 하면 서버를 키고 끄는 작업 또한 너무 비효율적이다.

이 밖에도 Print로 눈으로 검증하는 것도 어느정도 선에서 한계가 있다. 테스트 코드는 자동으로 검증을 해주기 때문에 성공한다면 수동으로 검증할 필요 자체가 없어진다.

새로운 기능이 추가되었을 때도 테스트 코드를 통해 만약 기존의 코드에 영향이 갔다면 어떤 부분을 수정해야 하는 지 알 수 있는 장점도 존재한다.

 

따라서 테스트 코드는 개발하는 데 있어서 필수적인 부분이며 반드시 활용해야 한다.

 

테스트 코드 예제

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HomeController.class)
public class HomeControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void home_return() throws Exception {
        //when
        String home = "home";

        //then
        mvc.perform(get("/home"))
                .andExpect(status().isOk())
                .andExpect(content().string(home));
    }
}

 

  1. @RunWith(SpringRunner.class)

테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킨다.

스프링 부트 테스트와 JUnit 사이의 연결자 역할을 한다고 생각하면 된다.

  1. @WebMvcTest

컨트롤러만 사용할 때 선언이 가능하며, Spring MVC에 집중할 수 있는 어노테이션이다.

  1. @Autowired

스프링이 관리하는 Bean을 주입시켜준다.

  1. MockMvc

웹 API를 테스트할 때 사용하며, 이를 통해 HTTP GET, POST, DELETE 등에 대한 API 테스트가 가능하다.

  1. mvc.perform(get("/home"))

/home 주소로 HTTP GET 요청을 한 상황이다.

  1. .andExpect(status().isOk())

결과를 검증하는 andExpect로, 여러개를 붙여서 사용이 가능하다. status()는 HTTP Header를 검증하는 것으로 결과에 대한 HTTP Status 상태를 확인할 수 있다. 현재 isOK()는 200 코드가 맞는지 확인하고 있다.

 

프로젝트를 만들면서 다양한 기능들을 구현하게 되는데, 이처럼 테스트 코드로 견고한 프로젝트를 만들기 위한 기능별 단위 테스트를 진행하는 습관을 길러야 한다.



[참고 자료]

'Web > Spring' 카테고리의 다른 글

[Spring Data JPA] 더티 체킹 (Dirty Checking)  (0) 2022.05.08
JPA  (0) 2022.05.08
[Spring Boot] SpringApplication  (0) 2022.05.08
Spring MVC  (0) 2022.05.08
[Spring] Bean Scope  (0) 2022.05.08

[Spring Boot] SpringApplication

 

스프링 부트로 프로젝트를 실행할 때 Application 클래스를 만든다.

클래스명은 개발자가 프로젝트에 맞게 설정할 수 있지만, 큰 틀은 아래와 같다.

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

 

@SpringBootApplication 어노테이션을 통해 스프링 Bean을 읽어와 자동으로 생성해준다.

이 어노테이션이 있는 파일 위치부터 설정들을 읽어가므로, 반드시 프로젝트의 최상단에 만들어야 한다.

SpringApplication.run()으로 해당 클래스를 run하면, 내장 WAS를 실행한다. 내장 WAS의 장점으로는 개발자가 따로 톰캣과 같은 외부 WAS를 설치 후 설정해두지 않아도 애플리케이션을 실행할 수 있다.

또한, 외장 WAS를 사용할 시 이 프로젝트를 실행시키기 위한 서버에서 모두 외장 WAS의 종류와 버전, 설정을 일치시켜야만 한다. 따라서 내장 WAS를 사용하면 이런 신경은 쓰지 않아도 되기 때문에 매우 편리하다.

실제로 많은 회사들이 이런 장점을 살려 내장 WAS를 사용하고 있고, 전환하고 있다.

'Web > Spring' 카테고리의 다른 글

[Spring Data JPA] 더티 체킹 (Dirty Checking)  (0) 2022.05.08
JPA  (0) 2022.05.08
[Spring Boot] Test Code  (0) 2022.05.08
Spring MVC  (0) 2022.05.08
[Spring] Bean Scope  (0) 2022.05.08

Spring MVC Framework

 

스프링 MVC 프레임워크가 동작하는 원리를 이해하고 있어야 한다

 

클라이언트가 서버에게 url을 통해 요청할 때 일어나는 스프링 프레임워크의 동작을 그림으로 표현한 것이다.

 

MVC 진행 과정


  • 클라이언트가 url을 요청하면, 웹 브라우저에서 스프링으로 request가 보내진다.
  • Dispatcher Servlet이 request를 받으면, Handler Mapping을 통해 해당 url을 담당하는 Controller를 탐색 후 찾아낸다.
  • 찾아낸 Controller로 request를 보내주고, 보내주기 위해 필요한 Model을 구성한다.
  • Model에서는 페이지 처리에 필요한 정보들을 Database에 접근하여 쿼리문을 통해 가져온다.
  • 데이터를 통해 얻은 Model 정보를 Controller에게 response 해주면, Controller는 이를 받아 Model을 완성시켜 Dispatcher Servlet에게 전달해준다.
  • Dispatcher Servlet은 View Resolver를 통해 request에 해당하는 view 파일을 탐색 후 받아낸다.
  • 받아낸 View 페이지 파일에 Model을 보낸 후 클라이언트에게 보낼 페이지를 완성시켜 받아낸다.
  • 완성된 View 파일을 클라이언트에 response하여 화면에 출력한다.

 

구성 요소


Dispatcher Servlet

모든 request를 처리하는 중심 컨트롤러라고 생각하면 된다. 서블릿 컨테이너에서 http 프로토콜을 통해 들어오는 모든 request에 대해 제일 앞단에서 중앙집중식으로 처리해주는 핵심적인 역할을 한다.

기존에는 web.xml에 모두 등록해줘야 했지만, 디스패처 서블릿이 모든 request를 핸들링하면서 작업을 편리하게 할 수 있다.

 

Handler Mapping

클라이언트의 request url을 어떤 컨트롤러가 처리해야 할 지 찾아서 Dispatcher Servlet에게 전달해주는 역할을 담당한다.

컨트롤러 상에서 url을 매핑시키기 위해 @RequestMapping을 사용하는데, 핸들러가 이를 찾아주는 역할을 한다.

 

Controller

실질적인 요청을 처리하는 곳이다. Dispatcher Servlet이 프론트 컨트롤러라면, 이 곳은 백엔드 컨트롤러라고 볼 수 있다.

모델의 처리 결과를 담아 Dispatcher Servlet에게 반환해준다.

 

View Resolver

컨트롤러의 처리 결과를 만들 view를 결정해주는 역할을 담당한다. 다양한 종류가 있기 때문에 상황에 맞게 활용하면 된다.



[참고사항]

'Web > Spring' 카테고리의 다른 글

[Spring Data JPA] 더티 체킹 (Dirty Checking)  (0) 2022.05.08
JPA  (0) 2022.05.08
[Spring Boot] Test Code  (0) 2022.05.08
[Spring Boot] SpringApplication  (0) 2022.05.08
[Spring] Bean Scope  (0) 2022.05.08

[Spring] Bean Scope

 

 

Bean의 사용 범위를 말하는 Bean Scope의 종류에 대해 알아보자

 

Bean은 스프링에서 사용하는 POJO 기반 객체다.

상황과 필요에 따라 Bean을 사용할 때 하나만 만들어야 할 수도 있고, 여러개가 필요할 때도 있고, 어떤 한 시점에서만 사용해야할 때가 있을 수 있다.

이를 위해 Scope를 설정해서 Bean의 사용 범위를 개발자가 설정할 수 있다.

 

우선 따로 설정을 해주지 않으면, Spring에서 Bean은 Singleton으로 생성된다. 싱글톤 패턴처럼 특정 타입의 Bean을 딱 하나만 만들고 모두 공유해서 사용하기 위함이다. 보통은 Bean을 이렇게 하나만 만들어 사용하는 경우가 대부분이지만, 요구사항이나 구현에 따라 아닐 수도 있을 것이다.

따라서 Bean Scope는 싱글톤 말고도 여러가지를 지원해준다.

 

Scope 종류

  • singleton
  • 해당 Bean에 대해 IoC 컨테이너에서 단 하나의 객체로만 존재한다.
  • prototype
  • 해당 Bean에 대해 다수의 객체가 존재할 수 있다.
  • request
  • 해당 Bean에 대해 하나의 HTTP Request의 라이프사이클에서 단 하나의 객체로만 존재한다.
  • session
  • 해당 Bean에 대해 하나의 HTTP Session의 라이프사이클에서 단 하나의 객체로만 존재한다.
  • global session
  • 해당 Bean에 대해 하나의 Global HTTP Session의 라이프사이클에서 단 하나의 객체로만 존재한다.

request, session, global session은 MVC 웹 어플리케이션에서만 사용함

 

Scope들은 Bean으로 등록하는 클래스에 어노테이션으로 설정해줄 수 있다.

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
 
@Scope("prototype")
@Component
public class UserController {
}



[참고 자료]

'Web > Spring' 카테고리의 다른 글

[Spring Data JPA] 더티 체킹 (Dirty Checking)  (0) 2022.05.08
JPA  (0) 2022.05.08
[Spring Boot] Test Code  (0) 2022.05.08
[Spring Boot] SpringApplication  (0) 2022.05.08
Spring MVC  (0) 2022.05.08

+ Recent posts