[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

[Java] Record

 

 

Java 14에서 프리뷰로 도입된 클래스 타입
순수히 데이터를 보유하기 위한 클래스

 

Java 14버전부터 도입되고 16부터 정식 스펙에 포함된 Record는 class처럼 타입으로 사용이 가능하다.

객체를 생성할 때 보통 아래와 같이 개발자가 만들어야한다.

 

public class Person {
   private final String name;
   private final int age;
 
   public Person(String name, int age) {
      this.name = name;
      this.age = age;
   }
 
   public String getName() {
      return name;
   }
 
   public int getAge() {
      return age;
   }
}
  • 클래스 Person 을 만든다.
  • 필드 name, age를 생성한다.
  • 생성자를 만든다.
  • getter를 구현한다.

 

보통 Entity나 DTO 구현에 있어서 많이 사용하는 형식이다.

이를 Record 타입의 클래스로 만들면 상당히 단순해진다.

 

public record Person(
	String name,
    int age
) {}

 

자동으로 필드를 private final 로 선언하여 만들어주고, 생성자와 getter까지 암묵적으로 생성된다. 또한 equals, hashCode, toString 도 자동으로 생성된다고 하니 매우 편리하다.

대신 getter 메소드의 경우 구현시 getXXX()로 명칭을 짓지만, 자동으로 만들어주는 메소드는 name(), age()와 같이 필드명으로 생성된다.



[참고 자료]

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

Stream API  (0) 2022.05.05
Error & Exception  (0) 2022.05.05
직렬화(Serialization)  (0) 2022.05.05
Primitive type & Reference type  (0) 2022.05.05
Promotion & Casting  (0) 2022.05.05

JAVA Stream

Java 8버전 이상부터는 Stream API를 지원한다

 

자바에서도 8버전 이상부터 람다를 사용한 함수형 프로그래밍이 가능해졌다.

기존에 존재하던 Collection과 Stream은 무슨 차이가 있을까? 바로 **'데이터 계산 시점'**이다.

Collection

  • 모든 값을 메모리에 저장하는 자료구조다. 따라서 Collection에 추가하기 전에 미리 계산이 완료되어있어야 한다.
  • 외부 반복을 통해 사용자가 직접 반복 작업을 거쳐 요소를 가져올 수 있다(for-each)

Stream

  • 요청할 때만 요소를 계산한다. 내부 반복을 사용하므로, 추출 요소만 선언해주면 알아서 반복 처리를 진행한다.
  • 스트림에 요소를 따로 추가 혹은 제거하는 작업은 불가능하다.

Collection은 핸드폰에 음악 파일을 미리 저장하여 재생하는 플레이어라면, Stream은 필요할 때 검색해서 듣는 멜론과 같은 음악 어플이라고 생각하면 된다.

 

외부 반복 & 내부 반복

Collection은 외부 반복, Stream은 내부 반복이라고 했다. 두 차이를 알아보자.

**성능 면에서는 '내부 반복'**이 비교적 좋다. 내부 반복은 작업을 병렬 처리하면서 최적화된 순서로 처리해준다. 하지만 외부 반복은 명시적으로 컬렉션 항목을 하나씩 가져와서 처리해야하기 때문에 최적화에 불리하다.

즉, Collection에서 병렬성을 이용하려면 직접 synchronized를 통해 관리해야만 한다.

 

 

Stream 연산

스트림은 연산 과정이 '중간'과 '최종'으로 나누어진다.

filter, map, limit 등 파이프라이닝이 가능한 연산을 중간 연산, count, collect 등 스트림을 닫는 연산을 최종 연산이라고 한다.

둘로 나누는 이유는, 중간 연산들은 스트림을 반환해야 하는데, 모두 한꺼번에 병합하여 연산을 처리한 다음 최종 연산에서 한꺼번에 처리하게 된다.

ex) Item 중에 가격이 1000 이상인 이름을 5개 선택한다.

List<String> items = item.stream()
    			.filter(d->d.getPrices()>=1000)
                          .map(d->d.getName())
                          .limit(5)
                          .collect(tpList());

filter와 map은 다른 연산이지만, 한 과정으로 병합된다.

만약 Collection 이었다면, 우선 가격이 1000 이상인 아이템을 찾은 다음, 이름만 따로 저장한 뒤 5개를 선택해야 한다. 연산 최적화는 물론, 가독성 면에서도 Stream이 더 좋다.

 

Stream 중간 연산

  • filter(Predicate) : Predicate를 인자로 받아 true인 요소를 포함한 스트림 반환
  • distinct() : 중복 필터링
  • limit(n) : 주어진 사이즈 이하 크기를 갖는 스트림 반환
  • skip(n) : 처음 요소 n개 제외한 스트림 반환
  • map(Function) : 매핑 함수의 result로 구성된 스트림 반환
  • flatMap() : 스트림의 콘텐츠로 매핑함. map과 달리 평면화된 스트림 반환

중간 연산은 모두 스트림을 반환한다.

Stream 최종 연산

  • (boolean) allMatch(Predicate) : 모든 스트림 요소가 Predicate와 일치하는지 검사
  • (boolean) anyMatch(Predicate) : 하나라도 일치하는 요소가 있는지 검사
  • (boolean) noneMatch(Predicate) : 매치되는 요소가 없는지 검사
  • (Optional) findAny() : 현재 스트림에서 임의의 요소 반환
  • (Optional) findFirst() : 스트림의 첫번째 요소
  • reduce() : 모든 스트림 요소를 처리해 값을 도출. 두 개의 인자를 가짐
  • collect() : 스트림을 reduce하여 list, map, 정수 형식 컬렉션을 만듬
  • (void) forEach() : 스트림 각 요소를 소비하며 람다 적용
  • (Long) count : 스트림 요소 개수 반환

 

Optional 클래스

값의 존재나 여부를 표현하는 컨테이너 Class

  • null로 인한 버그를 막을 수 있는 장점이 있다.
  • isPresent() : Optional이 값을 포함할 때 True 반환

 

Stream 활용 예제

  1. map()
  2. List<String> names = Arrays.asList("Sehoon", "Songwoo", "Chan", "Youngsuk", "Dajung");
    
    names.stream()
        .map(name -> name.toUpperCase())
        .forEach(name -> System.out.println(name));
  3. filter()
  4. List<String> startsWithN = names.stream()
        .filter(name -> name.startsWith("S"))
        .collect(Collectors.toList());
  5. reduce()sum : 55
  6. Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Optional<Integer> sum = numbers.reduce((x, y) -> x + y);
    sum.ifPresent(s -> System.out.println("sum: " + s));
  7. collect()
  8. System.out.println(names.stream()
                       .map(String::toUpperCase)
                       .collect(Collectors.joining(", ")));



[참고자료]

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

Record  (0) 2022.05.05
Error & Exception  (0) 2022.05.05
직렬화(Serialization)  (0) 2022.05.05
Primitive type & Reference type  (0) 2022.05.05
Promotion & Casting  (0) 2022.05.05

Error & Exception

Assembled by GimunLee (2019-11-19)

 

Goal

  • Error와 Exception의 차이점에 대해 설명할 수 있다.
  • Exception Handling을 할 수 있다.

 

Abstract

Error와 Exception은 같다고 생각할 수도 있지만 사실 큰 차이가 있습니다.

Error 는 컴파일 시 문법적인 오류와 런타임 시 널포인트 참조와 같은 오류로 프로세스에 심각한 문제를 야기 시켜 프로세스를 종료 시킬 수 있습니다.

Exception 은 컴퓨터 시스템의 동작 도중 예기치 않았던 이상 상태가 발생하여 수행 중인 프로그램이 영향을 받는 것우로 예를 들면, 연산 도중 넘침에 의해 발생한 끼어들기 등이 이에 해당합니다.

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우를 프로그램 오류라 하고, 프로그램 오류에는 에러(error)와 예외(exception) 두 가지로 구분할 수 있습니다. 에러는 메모리 부족이나 스택오버플로우와 같이 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습할 수 있는 비교적 덜 심각한 오류입니다. 이 예외는 프로그래머가 적절히 코드를 작성해주면 비정상적인 종류를 막을 수 있습니다.

Error의 상황을 미리 미연에 방지하기 위해서 Exception 상황을 만들 수 있으며, java에서는 try-catch문으로 Exception handling을 할 수 있습니다.

 

Exception Handling

잘못된 하나로 인해 전체 시스템이 무너지는 결과를 방지하기 위한 기술적인 처리입니다. JAVA에서는 예외와 에러도 객체로 처리합니다.

예외가 주로 발생하는 원인

  • 사용자의 잘못된 데이터 입력
  • 잘못된 연산
  • 개발자가 로직을 잘못 작성
  • 하드웨어, 네트워크 오작동
  • 시스템 과부하

 

Throwable 클래스

Throwable 클래스는 예외처리를 할 수 있는 최상위 클래스입니다. Exception과 Error는 Throwable의 상속을 받습니다.

 

Error (에러)

Error는 시스템 레벨에서 발생하여, 개발자가 어떻게 조치할 수 없는 수준을 의미합니다.

  • OutOfMemoryError : JVM에 설정된 메모리의 한계를 벗어난 상황일 때 발생합니다. 힙 사이즈가 부족하거나, 너무 많은 class를 로드할때, 가용가능한 swap이 없을때, 큰 메모리의 native메소드가 호출될 때 등이 있습니다. 이를 해결하기위해 dump 파일분석, jvm 옵션 수정 등이 있습니다.

 

Exception (예외)

예외는 개발자가 구현한 로직에서 발생하며 개발자가 다른 방식으로 처리가능한 것들로 JVM은 정상 동작합니다.

 

Exception의 2가지 종류

  1. Checked Exception : 예외처리가 필수이며, 처리하지 않으면 컴파일되지 않습니다. JVM 외부와 통신(네트워크, 파일시스템 등)할 때 주로 쓰입니다.
    • RuntimeException 이외에 있는 모든 예외
    • IOException, SQLException 등
  2. Unchecked Exception : 컴파일 때 체크되지 않고, Runtime에 발생하는 Exception을 말합니다.
    • RuntimeException 하위의 모든 예외
    • NullPointerException, IndexOutOfBoundException 등

 

대표적인 Exception Class

  • NullPointerException : Null 레퍼런스를 참조할때 발생, 뭔가 동작시킬 때 발생합니다.
  • IndexOutOfBoundsException : 배열과 유사한 자료구조(문자열, 배열, 자료구조)에서 범위를 벗어난 인덱스 번호 사용으로 발생합니다.
  • FormatException : 문자열, 숫자, 날짜 변환 시 잘못된 데이터(ex. "123A" -> 123 으로 변환 시)로 발생하며, 보통 사용자의 입력, 외부 데이터 로딩, 결과 데이터의 변환 처리에서 자주 발생합니다.
  • ArthmeticException : 정수를 0으로 나눌때 발생합니다.
  • ClassCastException : 변환할 수 없는 타입으로 객체를 변환할 때 발생합니다.
  • IllegalArgumentException : 잘못된 인자 전달 시 발생합니다.
  • IOException : 입출력 동작 실패 또는 인터럽트 시 발생합니다.
  • IllegalStateException : 객체의 상태가 매소드 호출에는 부적절한 경우에 발생합니다.
  • ConcurrentModificationException : 금지된 곳에서 객체를 동시에 수정하는것이 감지될 경우 발생합니다.
  • UnsupportedOperationException : 객체가 메소드를 지원하지 않는 경우 발생합니다.

 

주요 Method

  • printStackTrace() : 발생한 Exception의 출처를 메모리상에서 추적하면서 결과를 알려줍니다. 발생한 위치를 정확히 출력해줘서 제일 많이 쓰며 void를 반환합니다.
  • getMessage() : 한줄로 요약된 메세지를 String으로 반환해줍니다.
  • getStackTrace() : jdk1.4 부터 지원, printStackTrace()를 보완, StackTraceElement[] 이라는 문자열 배열로 변경해서 출력하고 저장합니다.

 

Exception Handling

JAVA에서 모든 예외가 발생하면 (XXX)Exception 객체를 생성합니다. 예외를 처리하는 방법에는 크게 2가지가 있습니다.

  1. 직접 try ~ catch 를 이용해서 예외에 대한 최종적인 책임을 지고 처리하는 방식
  2. throws Exception 을 이용해서 발생한 예외의 책임을 호출하는 쪽이 책임지도록 하는 방식 (주로 호출하는 쪽에 예외를 보고할 때 사용합니다.)

다른 메소드의 일부분으로 동작하는 경우엔 던지는 것을 추천합니다.

 

예외 잡기 (try ~ catch 구문)

로직 중에 예외가 발생할지도 모르는 부분에 try ~ catch 구문으로 보험 처리합니다.

  • try 에는 위험한 로직이 들어가고, catch 에는 예외 발생 시 수행할 로직이 들어갑니다.
  • try 중이라도 예외가 발생한 다음의 코드들은 실행되지 않으며 catch 구문으로 넘어갑니다.
  • catch 구문은 else if 처럼 여러개 쓸 수 있습니다.
  • finally 는 마지막에 실행하고 싶은 로직이 들어가며, 대표적으로 .close() 가 있습니다.

 

예외 던지기 (throws 구문)

예외 처리를 현재 메소드가 직접 처리하지 않고 호출한 곳에다가 예외의 발생 여부를 통보합니다. 호출한 메소드는 이걸 또 던질건지 직접 처리할 건지 정해야합니다. (return보다 강력합니다.)

public class ThrowsEx {
    public void call_A() throws Exception {
        call_B();
    }

    private void call_B() throws Exception {
        call_C();
    }

    private void call_C() throws Exception {
        System.out.println(1 / 0);
    }

    public static void main(String[] args) throws Exception {
        ThrowsEx test = new ThrowsEx();
        test.call_A();
    }
}

실행 결과는 아래와 같습니다.

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at exception.ThrowsEx.call_C(ThrowsEx.java:13)
    at exception.ThrowsEx.call_B(ThrowsEx.java:9)
    at exception.ThrowsEx.call_A(ThrowsEx.java:5)
    at exception.ThrowsEx.main(ThrowsEx.java:18)

 

Reference & Additional Resources

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

Record  (0) 2022.05.05
Stream API  (0) 2022.05.05
직렬화(Serialization)  (0) 2022.05.05
Primitive type & Reference type  (0) 2022.05.05
Promotion & Casting  (0) 2022.05.05

[Java] 직렬화(Serialization)

 

자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술

 

각자 PC의 OS마다 서로 다른 가상 메모리 주소 공간을 갖기 때문에, Reference Type의 데이터들은 인스턴스를 전달 할 수 없다.

따라서, 이런 문제를 해결하기 위해선 주소값이 아닌 Byte 형태로 직렬화된 객체 데이터를 전달해야 한다.

직렬화된 데이터들은 모두 Primitive Type(기본형)이 되고, 이는 파일 저장이나 네트워크 전송 시 파싱이 가능한 유의미한 데이터가 된다. 따라서, 전송 및 저장이 가능한 데이터로 만들어주는 것이 바로 **'직렬화(Serialization)'**이라고 말할 수 있다.

 

 

직렬화 조건


자바에서는 간단히 java.io.Serializable 인터페이스 구현으로 직렬화/역직렬화가 가능하다.

역직렬화는 직렬화된 데이터를 받는쪽에서 다시 객체 데이터로 변환하기 위한 작업을 말한다.

직렬화 대상 : 인터페이스 상속 받은 객체, Primitive 타입의 데이터

Primitive 타입이 아닌 Reference 타입처럼 주소값을 지닌 객체들은 바이트로 변환하기 위해 Serializable 인터페이스를 구현해야 한다.

 

직렬화 상황


  • JVM에 상주하는 객체 데이터를 영속화할 때 사용
  • Servlet Session
  • Cache
  • Java RMI(Remote Method Invocation)

 

직렬화 구현


@Entity
@AllArgsConstructor
@toString
public class Post implements Serializable {
private static final long serialVersionUID = 1L;
    
private String title;
private String content;

serialVersionUID를 만들어준다.

Post post = new Post("제목", "내용");
byte[] serializedPost;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
        oos.writeObject(post);

        serializedPost = baos.toByteArray();
    }
}

ObjectOutputStream으로 직렬화를 진행한다. Byte로 변환된 값을 저장하면 된다.

 

역직렬화 예시

try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedPost)) {
    try (ObjectInputStream ois = new ObjectInputStream(bais)) {

        Object objectPost = ois.readObject();
        Post post = (Post) objectPost;
    }
}

ObjectInputStream로 역직렬화를 진행한다. Byte의 값을 다시 객체로 저장하는 과정이다.

 

직렬화 serialVersionUID

위의 코드에서 serialVersionUID를 직접 설정했었다. 사실 선언하지 않아도, 자동으로 해시값이 할당된다.

직접 설정한 이유는 기존의 클래스 멤버 변수가 변경되면 serialVersionUID가 달라지는데, 역직렬화 시 달라진 넘버로 Exception이 발생될 수 있다.

따라서 직접 serialVersionUID을 관리해야 클래스의 변수가 변경되어도 직렬화에 문제가 발생하지 않게 된다.

serialVersionUID을 관리하더라도, 멤버 변수의 타입이 다르거나, 제거 혹은 변수명을 바꾸게 되면 Exception은 발생하지 않지만 데이터가 누락될 수 있다.

 

요약

  • 데이터를 통신 상에서 전송 및 저장하기 위해 직렬화/역직렬화를 사용한다.
  • serialVersionUID는 개발자가 직접 관리한다.
  • 클래스 변경을 개발자가 예측할 수 없을 때는 직렬화 사용을 지양한다.
  • 개발자가 직접 컨트롤 할 수 없는 클래스(라이브러리 등)는 직렬화 사용을 지양한다.
  • 자주 변경되는 클래스는 직렬화 사용을 지양한다.
  • 역직렬화에 실패하는 상황에 대한 예외처리는 필수로 구현한다.
  • 직렬화 데이터는 타입, 클래스 메타정보를 포함하므로 사이즈가 크다. 트래픽에 따라 비용 증가 문제가 발생할 수 있기 때문에 JSON 포맷으로 변경하는 것이 좋다.
  • JSON 포맷이 직렬화 데이터 포맷보다 2~10배 더 효율적



[참고자료]

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

Stream API  (0) 2022.05.05
Error & Exception  (0) 2022.05.05
Primitive type & Reference type  (0) 2022.05.05
Promotion & Casting  (0) 2022.05.05
Garbage Collection  (0) 2022.05.05

Primitive type & Reference type

Assembled by GimunLee (2019-11-19)

 

Goal

  • Primitive type에 대해 설명할 수 있다.
  • Reference type에 대해 설명할 수 있다.

 

Abstract

자바에는 기본형(Primitive type)과 참조형(Reference type)이 있습니다. 일반적인 분류는 다음처럼 가집니다.

Java Data Type 
ㄴ Primitive Type
    ㄴ Boolean Type(boolean)
    ㄴ Numeric Type
        ㄴ Integral Type
            ㄴ Integer Type(short, int, long)
            ㄴ Floating Point Type(float, double)
        ㄴ Character Type(char)
ㄴ Reference Type
    ㄴ Class Type
    ㄴ Interface Type
    ㄴ Array Type
    ㄴ Enum Type
    ㄴ etc.

 

Primitive type (기본형 타입)

  • JAVA에서는 총 8가지의 Primitive type을 미리 정의하고 제공합니다.
  • 자바에서 기본 자료형은 반드시 사용하기 전에 선언(Declared)되어야 합니다.
  • OS에 따라 자료형의 길이가 변하지 않습니다.
  • 비객체 타입입니다. 따라서 null 값을 가질 수 없습니다. 만약 Primitive type에 Null을 넣고 싶다면 Wrapper Class를 활용합니다.
  • 스택(Stack) 메모리에 저장됩니다.

  • boolean
    • 논리형인 boolean의 기본값은 false이며 참과 거짓을 저장하는 타입입니다. 주로 yes/no, on/off 등의 논리 구현에 주로 사용되며 두가지 값만 표현하므로 가장 크기가 작습니다.
    • boolean은 실제로 1bit면 충분하지만, 데이터를 다루는 최소 단위가 1byte이므로 메모리 크기가 1byte입니다.
  • byte
    • byte는 주로 이진데이터를 다루는데 사용되는 타입입니다.
  • short
    • short는 C언어와의 호환을 위해 사용되는 타입으로 잘 사용되지는 않는 타입입니다.
  • int
    • int 형은 자바에서 정수 연산을 하기 위한 기본 타입입니다. 즉, byte 혹은 short 의 변수가 연산을 하면 연산의 결과는 int형이 됩니다.
  • long
    • 수치가 큰 데이터를 다루는 프로그램(은행 및 우주와 관련된 프로그램)에서 주로 사용합니다.
    • long 타입의 변수를 초기화 할 떄에는 정수값 뒤에 알파벳 L을 붙여서 long 타입(즉, 8byte)의 정수 데이터임을 알려주어야 합니다. 만일 정수값이 int의 값의 저장 범위를 넘는 정수에서 L을 붙이지 않는다면 컴파일 에러가 발생합니다.
    long l = 2147483648; // 컴파일 에러 발생
    long l = 2147483648L;
  • float, double
    • 실수를 가수와 지수 형식으로 저장하는 부동소수점 방식으로 저장됩니다.
    • 가수를 표현하는데 있어 double형이 float형보다 표현 가능 범위가 더 크므로 double형이 보다 정밀하게 표현할 수 있습니다.
    • 자바에서 실수의 기본 타입은 double형이므로 float형에는 알파벳 F를 붙여서 float 형임을 명시해주어야 합니다.
    • float f = 1234.567;  // 무조건 double 타입으로 이해하려고 하므로 컴파일 에러가 발생합니다.
      float f = 1234.567F; // float type이라는 것을 표시해야 합니다.

 

Reference type (참조형 타입)

  • JAVA에서 Primitive type을 제외한 타입들이 모두 Reference type 입니다.
  • Reference type은 JAVA에서 최상인 java.lang.Object클래스를 상속하는 모든 클래스들을 말합니다. 물론 new로 인하여 생성하는 것들은 메모리 영역인 Heap 영역에 생성을 하게되고, Garbage Collector가 돌면서 메모리를 해제합니다.
  • 클래스 타입(class type) , 인터페이스 타입(interface type) , 배열 타입(array type) , 열거 타입(enum type) 이 있습니다.
  • 빈 객체를 의미하는 Null이 존재합니다.
  • 문법상으로는 에러가 없지만 실행시켰을 때 에러가 나는 런타임 에러가 발생합니다. 예를 들어 객체나 배열을 Null 값으로 받으면 NullPointException이 발생하므로 변수 값을 넣어야 합니다.
  • Heap 메모리에 생성된 인스턴스는 메소드나 각종 인터페이스에서 접근하기 위해 JVM의 Stack 영역에 존재하는 Frame에 일종의 포인터(C의 포인터와는 다릅니다.)인 참조값을 가지고 있어 이를 통해 인스턴스를 핸들링합니다.

 

String Class

클래스형에서도 String 클래스는 조금 특별합니다. 이 클래스는 참조형에 속하지만 기본적인 사용은 기본형 처럼 사용합니다. 그리고 불변(immutable)하는 객체입니다. String 클래스에는 값을 변경해주는 메소드들이 존재하지만 해당 메소드를 통해 데이터를 바꾼다 해도 새로운 String 클래스 객체를 만들어내는 것입니다. 일반적으로 기본형 비교는 == 연산자를 사용하지만 String 객체간의 비교는 .equals() 메소드를 사용해야 합니다.

 

Reference & Additional Resources

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

Error & Exception  (0) 2022.05.05
직렬화(Serialization)  (0) 2022.05.05
Promotion & Casting  (0) 2022.05.05
Garbage Collection  (0) 2022.05.05
문자열 클래스  (0) 2022.05.05

Promotion & Casting

Assembled by GimunLee (2019-11-19)

 

Goal

  • Promotion에 대해 설명할 수 있다.
  • Casting에 대해 설명할 수 있다.
  • 형변환할 때 발생할 수 있는 에러에 대해 설명할 수 있다.

 

데이터 타입 형변환 (타입변환)

Java에서 연산은 "2(byte 데이터 타입) + 3(byte 데이터 타입)" 과 같이 동일한 데이터 타입에서 가능합니다. 하지만, 프로그램을 만들다 보면 "2(byte 데이터 타입) + 3.5(double 데이터 타입)"과 같이 서로 다른 데이터 타입끼리의 연산이 필요할 때가 있습니다.

이럴경우 변수의 데이터 타입을 바꿔주는 작업이 필요한데, 이것이 데이터 타입의 형변환(타입변환)입니다. 이러한 형변환(타입변환)에는 크게 자동 형변환(Promotion)  강제 형변환(Casting) 이 있습니다. 또 다른말로 자동 형변환은 묵시적 타입 변환, 강제 형변환은 명시적 타입 변환이라고도 합니다.

 

Promotion (자동 형변환, 묵시적 형변환)

자동 형변환(Promotion)은 프로그램 실행 도중에 자동적으로 형변환(타입변환)이 일어나는 것을 말합니다. 또한, 자동 형변환(Promotion)은 작은 메모리 크기의 데이터 타입을 큰 메모리 크기의 데이터 타입으로 변환하는 행위를 말합니다.

byte a = 10; // 정수 10을 byte 데이터 타입의 변수인 a에 저장
int b = a;   // byte 데이터 타입의 변수인 a를 int 데이터 타입의 변수인 b에저장

위에 작성한 예시처럼 작은 메모리 크기의 데이터 타입(byte 데이터 타입)에서 큰 메모리 크기의 데이터 타입(int 데이터 타입)에 값을 저장하면, 별다른 문법 없이 형변환(타입변환)이 일어납니다. 이러한 형변환(타입변환)을 "자동 형변환(Promotion)"이라고 합니다

자동 형변환(Promotion)이 이루어지는 순서를 알아보겠습니다.

보시면, long 데이터 타입의 메모리 크기는 8byte이고, float 데이터 타입의 메모리 크기는 4byte인데, long 데이터 타입에서 float 데이터 타입으로 자동 형변환(Promotion)이 가능합니다. 그 이유는 표현할 수 있는 값의 범위가 float가 더 크기 때문입니다.

주의할 점은 메모리 크기가 큰 데이터 타입이라도, 타입 범위를 포함하지 못한다면 자동 형변환(Promotion) 이 불가능합니다.

  • byte 데이터 타입 -> char 데이터 타입 자동 형변환 불가
  • float 데이터 타입 -> long 데이터 타입 자동 형변환 불가

 

Casting (강제 형변환, 명시적 형변환)

특정 조건을 갖추지 못했지만, 형변환을 하고 싶을때 사용하는 것이 Casting (강제 형변환)입니다.

int intValue = 1;
byte byteValue = intValue;

위의 경우 intValue에 저장된 1이라는 값은 byte 데이터 타입에도 저장 가능한 값입니다. 그렇지만, 위 코드를 실행하면 컴파일 에러가 발생합니다. 그 이유는 저장될 값 1에 상관없이 int 데이터 타입이 byte 데이터 타입보다 메모리 크기가 크기 때문입니다. 그림으로 보자면,

이와 같은 그림이 나옵니다. int 데이터 타입에 저장된 정수 1의 값은 실제 메모리에 저장될 때 00000000 00000000 00000000 00000001 의 값을 가집니다. 이 값을 byte 데이터 타입에 끝에 1byte(00000001) 영역만 넣자니 앞에있는 3byte (00000000 00000000 00000000) 값이 날아갑니다. 그림으로 보면 이렇습니다.

앞에 3byte의 공간을 삭제하는 시점에서 많은 데이터가 날아가 정상적인 값이 저장될 수 없을 것입니다. 이와 같이 메모리 크기가 큰 int 데이터 타입에서 메모리 크기가 작은 byte 데이터 타입으로 자동 형변환(Promotion)이 된다면, 정상적이지 않은 값이 나올 수 있기 때문에 Java에서 자동 형변환(Promotion)을 하지 않습니다. 하지만, 우리가 형변환 하려는 정수 값은 1 이므로 byte 데이터 타입 범위 안에 충분히 들어가는 값입니다. 우린 그걸 머릿속으로 알고 있기 때문에 byte 데이터 타입으로 변환된다 하더라도 값이 정상적일 거라고 판단할 수 있습니다. 이럴 때 강제 형변환은 아래와 같이 해주시면 됩니다.

int intValue = 1;
byte byteValue = (byte) intValue;

 

형변환 연산

+, -, *, / 과 같은 기본적인 사칙연산은 같은 타입의 피연산자 간에만 수행되기 때문에 서로 다른 데이터 타입의 피연산자가 있을 경우 두 피연산자 중 크기가 큰 타입으로 자동 형변환(Promotion)된 후 연산이 수행됩니다. 예를 들어 int 데이터 타입의 피연산자와 double 타입의 피연산자를 덧셈하면 int 데이터 타입의 피연산자가 double 데이터 타입으로 자동 형변환(Promotion)되고 연산이 수행됩니다. 연산의 결과도 double 데이터 타입이 됩니다.

int intValue = 10;
double doubleValue = 5.5;
double result = intValue + doubleValue; 
// intValue 변수값과 doubleValue 변수값을 더해서 double 타입의 result 변수에 저장

만약 int 데이터 타입의 연산 결과를 얻고 싶다면, 강제 형변환(Casting)를 통해 아래와 같이 작성해주시면 됩니다.

int intValue = 10;
double doubleValue = 5.5;
int result = intValue + (int) doubleValue;
// intValue 변수값과 doubleValue변수값을 더해서 int 타입의 result 변수에 저장

 

Reference & Additional Resources

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

직렬화(Serialization)  (0) 2022.05.05
Primitive type & Reference type  (0) 2022.05.05
Garbage Collection  (0) 2022.05.05
문자열 클래스  (0) 2022.05.05
Intrinsic Lock  (0) 2022.05.05

Garbage Collection

Assembled by GimunLee (2019-10-28)

 

Goal

  • Garbage Collection의 역할에 대해 설명할 수 있다.
  • Garbage Collection의 메모리 해제 과정을 3단계로 설명할 수 있다.
  • Generational Gabage Collection에 대해 설명할 수 있다.
  • Generational Garbage Collection 과정에 대해 설명할 수 있다.
  • Minor GC와 Major GC의 차이점에 대해 설명할 수 있다.

 

Abstract

C/C++ 프로그래밍을 할 때 메모리 누수(Memory Leak)를 막기 위해 객체를 생성한 후 사용자하지 않는 객체의 메모리를 프로그래머가 직접 해제 해주어야 했습니다. 하지만, JAVA에서는 JVM(Java Virtual Machine)이 구성된 JRE(Java Runtime Environment)가 제공되며, 그 구성 요소 중 하나인 Garbage Collection(이하 GC)이 자동으로 사용하지 않는 객체를 파괴합니다.

GC에 대해서 알아보기 전에 'stop-the-world'라는 용어를 알아야합니다. 'stop-the-world'란, GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것입니다. 어떤 GC 알고리즘을 사용하더라도 'stop-the-world'는 발생하게 되는데, 대개의 경우 GC 튜닝은 이 'stop-the-world' 시간을 줄이는 것이라고 합니다.

GC를 해도 더이상 사용 가능한 메모리 영역이 없는데 계속 메모리를 할당하려고 하면, OutOfMemoryError가 발생하여 WAS가 다운될 수도 있습니다. 행(Hang) 즉, 서버가 요청을 처리 못하고 있는 상태가 됩니다.

따라서 규모 있는 JAVA 애플리케이션을 효율적으로 개발하기 위해서는 GC에 대해 잘 알아야한다고 합니다. 이번에는 GC에 대해 간단하게 알아보겠습니다.

 

Garbage Collection

C/C++ 언어와 달리 자바는 개발자가 명시적으로 객체를 해제할 필요가 없습니다. 자바 언어의 큰 장점이기도 합니다. 사용하지 않는 객체는 메모리에서 삭제하는 작업을 GC라고 부르며 JVM에서 GC를 수행합니다.

기본적으로 JVM의 메모리는 총 5가지 영역(class, stack, heap, native method, PC)으로 나뉘는데, GC는 힙 메모리만 다룹니다.

일반적으로 다음과 같은 경우에 GC의 대상이 됩니다.

  1. 객체가 NULL인 경우 (ex. String str = null)
  2. 블럭 실행 종료 후, 블럭 안에서 생성된 객체
  3. 부모 객체가 NULL인 경우, 포함하는 자식 객체

GC는 Weak Generational Hypothesis 에 기반합니다. 우선 GC의 메모리 해제 과정에 대해 살펴보겠습니다.

 

GC의 메모리 해제 과정

  1. Marking
    • 프로세스는 마킹을 호출합니다. 이것은 GC가 메모리가 사용되는지 아닌지를 찾아냅니다. 참조되는 객체는 파란색으로, 참조되지 않는 객체는 주황색으로 보여집니다. 모든 오브젝트는 마킹 단계에서 결정을 위해 스캔되어집니다. 모든 오브젝트를 스캔하기 때문에 매우 많은 시간을 소모하게 됩니다.
  2. Normal Deletion
    • 참조되지 않는 객체를 제거하고, 메모리를 반환합니다. 메모리 Allocator는 반환되어 비어진 블럭의 참조 위치를 저장해 두었다고 새로운 오브젝트가 선언되면 할당되도록 합니다.
  3. Compacting
    • 퍼포먼스를 향상시키기 위해, 참조되지 않는 객체를 제거하고 또한 남은 참조되어지는 객체들을 묶습니다. 이들을 묶음으로서 공간이 생기므로 새로운 메모리 할당 시에 더 쉽고 빠르게 진행 할 수 있습니다.

 

Generational Garbage Collection 배경

위와 같이 모든 객체를 Mark & Compact 하는 JVM은 비효율적입니다. 다음과 같은 그래프를 보시겠습니다.

Y축은 할당된 바이트의 수이고 X축은 바이트가 할당될 때의 시간입니다. 보시다시피 시간이 갈수록 적은 객체만이 남습니다. 위와 같은 그래프에 기반한 것이 Weak Generational Hypothesis 입니다.

 

Weak Generational Hypothesis

신규로 생성한 객체의 대부분은 금방 사용하지 않는 상태가 되고, 오래된 객체에서 신규 객체로의 참조는 매우 적게 존재한다는 가설입니다.

이 가설에 기반하여 자바는 Young 영역과 Old 영역으로 메모리를 분할하고, 신규로 생성되는 객체는 Young 영역에 보관하고, 오랫동안 살아남은 객체는 Old 영역에 보관합니다.

 

Generational Gabage Collection

  1. Young 영역(Yong Generation 영역)
  2. 새롭게 생성한 객체의 대부분이 여기에 위치합니다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라집니다. 이 영역에서 객체가 사라질때 Minor GC 가 발생한다고 말합니다.
  3. Old 영역(Old Generation 영역)
  4. 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사됩니다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생합니다. 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC) 가 발생한다고 말합니다.
  5. Permanet 영역
  6. Method Area라고도 합니다. JVM이 클래스들과 메소드들을 설명하기 위해 필요한 메타데이터들을 포함하고 있습니다. JDK8부터는 PermGen은 Metaspace로 교체됩니다.

 

Generational Garbage Collection 과정

  1. 어떠한 새로운 객체가 들어오면 Eden Space에 할당합니다.

  2. Eden space가 가득차게 되면, minor garbage collection이 시작됩니다.

  3. 참조되는 객체들은 첫 번째 survivor(S0)로 이동되어지고, 비 참조 객체는 Eden space가 clear 될 때 반환됩니다.

  4. 다음 minor GC 때, Eden space에서는 같은 일이 일어납니다. 비 참조 객체는 삭제되고 참조 객체는 survivor space로 이동하는 것 입니다. 그러나 이 케이스에서 참조 객체는 두 번째 survivor space로 이동하게 됩니다. 게다가 최근 minor GC에서 첫 번째 survivor space로 이동된 객체들도 age가 증가하고 S1 공간으로 이동하게 됩니다. 한번 모든 surviving 객체들이 S1으로 이동하게 되면 S0와 Eden 공간은 Clear 됩니다. 주의해야할 점은 이제 우리는 다른 aged 객체들을 서바이버 공간에 가지게 되었다는 것입니다.

  5. 다음 minor GC 때, 같은 과정이 반복 됩니다. 그러나 이 번엔 survivor space들은 switch 됩니다. 참조되는 객체들은 S0로 이동합니다. 살아남은 객체들은 aged되죠. 그리고 Eden과 S1 공간은 Clear 됩니다.

  6. 아래 그램은 promotion을 보여줍니다. minor GC 후 aged 오브젝트들이 일정한 age threshold(문지방)을 넘게 되면 그들은 young generation에서 old로 promotion 되어집니다. 여기서는 8을 예로 들었습니다.

  7. minor GC가 계속되고 계속해서 객체들이 Old Generation으로 이동됩니다.

  8. 아래 그림은 전 과정을 보여주고 있습니다. 결국 major GC가 old Generation에 시행되고, old Generation은 Clear 되고, 공간이 Compact 되어집니다.

 

Conclusion

이외에도 정말 많은 내용이 있지만, 간단하게나마 GC의 개념과 작동원리에 대해 알아보았습니다. 개발자 기술 면접에서도 종종 나오니 이번에 확실하게 학습해두면 도움이 많이 될 것 같습니다.

 

Reference & Additional Resources

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

Primitive type & Reference type  (0) 2022.05.05
Promotion & Casting  (0) 2022.05.05
문자열 클래스  (0) 2022.05.05
Intrinsic Lock  (0) 2022.05.05
Java에서의 Thread  (0) 2022.05.05

+ Recent posts