반응형


Promise

“A promise is an object that may produce a single value some time in the future”

프로미스는 자바스크립트 비동기 처리에 사용되는 객체입니다. 여기서 자바스크립트의 비동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성’을 의미합니다. 비동기 처리에 대한 이해가 없으시다면 이전 글 ‘자바스크립트 비동기 처리와 콜백 함수’를 읽어보시길 추천드립니다 :)

Promise가 왜 필요한가요?

프로미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용합니다. 일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 받아오기 위해 아래와 같은 API를 사용합니다.

$.get('url 주소/products/1', function (response) {
  // ...
});

위 API가 실행되면 서버에다가 ‘데이터 하나 보내주세요’ 라는 요청을 보내죠. 그런데 여기서 데이터를 받아오기도 전에 마치 데이터를 다 받아온 것 마냥 화면에 데이터를 표시하려고 하면 오류가 발생하거나 빈 화면이 뜹니다. 이와 같은 문제점을 해결하기 위한 방법 중 하나가 프로미스입니다.

프로미스 코드 - 기초

그럼 프로미스가 어떻게 동작하는지 이해하기 위해 예제 코드를 살펴보겠습니다. 먼저 아래 코드는 간단한 ajax 통신 코드입니다.

function getData(callbackFunc) {
  $.get('url 주소/products/1', function (response) {
    callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
  });
}

getData(function (tableData) {
  console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});

위 코드는 제이쿼리의 ajax 통신을 이용하여 지정한 url에서 1번 상품 데이터를 받아오는 코드입니다. 비동기 처리를 위해 프로미스 대신에 콜백 함수를 이용했죠.

위 코드에 프로미스를 적용하면 아래와 같은 코드가 됩니다.

function getData(callback) {
  // new Promise() 추가
  return new Promise(function (resolve, reject) {
    $.get('url 주소/products/1', function (response) {
      // 데이터를 받으면 resolve() 호출
      resolve(response);
    });
  });
}

// getData()의 실행이 끝나면 호출되는 then()
getData().then(function (tableData) {
  // resolve()의 결과 값이 여기로 전달됨
  console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});

콜백 함수로 처리하던 구조에서 new Promise()resolve()then()와 같은 프로미스 API를 사용한 구조로 바뀌었습니다. 여기서 new Promise()는 좀 이해가 가겠는데 resolve()then()은 뭐 하는 애들일까요? 아래에서 함께 알아보겠습니다.

프로미스의 3가지 상태(states)

프로미스를 사용할 때 알아야 하는 가장 기본적인 개념이 바로 프로미스의 상태(states)입니다. 여기서 말하는 상태란 프로미스의 처리 과정을 의미합니다. new Promise()로 프로미스를 생성하고 종료될 때까지 3가지 상태를 갖습니다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

Pending(대기)

먼저 아래와 같이 new Promise() 메서드를 호출하면 Pending(대기) 상태가 됩니다.

new Promise();

이렇게 new Promise() 메서드를 호출할 때 콜백 함수의 인자로 resolve, reject에 접근할 수 있습니다.

new Promise(function (resolve, reject) {
  // ...
});

Fulfilled(이행)

여기서 콜백 함수의 인자 resolve를 아래와 같이 실행하면 Fulfilled(이행) 상태가 됩니다.

new Promise(function (resolve, reject) {
  resolve();
});

그리고 이행 상태가 되면 아래와 같이 then()을 이용하여 처리 결과 값을 받을 수 있습니다.

function getData() {
  return new Promise(function (resolve, reject) {
    var data = 100;
    resolve(data);
  });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function (resolvedData) {
  console.log(resolvedData); // 100
});

프로미스의 '이행' 상태를 좀 다르게 표현해보면 '완료' 입니다.

Rejected(실패)

new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있다고 했습니다. 여기서 reject 인자로 reject() 메서드를 실행하면 Rejected(실패) 상태가 됩니다.

new Promise(function (resolve, reject) {
  reject();
});

그리고, 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있습니다.

function getData() {
  return new Promise(function (resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function (err) {
  console.log(err); // Error: Request is failed
});



프로미스 처리 흐름 - 출처 : MDN

프로미스 코드 - 쉬운 예제

그럼 위에서 배운 내용들을 종합하여 간단한 프로미스 코드를 만들어보겠습니다. 이해하기 쉽게 앞에서 살펴본 ajax 통신 예제 코드에 프로미스를 적용해보겠습니다.

function getData() {
  return new Promise(function (resolve, reject) {
    $.get('url 주소/products/1', function (response) {
      if (response) {
        resolve(response);
      }
      reject(new Error("Request is failed"));
    });
  });
}

// Fulfilled 또는 Rejected의 결과 값 출력
getData().then(function (data) {
  console.log(data); // response 값 출력
}).catch(function (err) {
  console.error(err); // Error 출력
});

위 코드는 서버에서 제대로 응답을 받아오면 resolve() 메서드를 호출하고, 응답이 없으면 reject() 메서드를 호출하는 예제입니다. 호출된 메서드에 따라 then()이나 catch()로 분기하여 데이터 또는 오류를 출력합니다.

여러 개의 프로미스 연결하기 (Promise Chaining)

프로미스의 또 다른 특징은 여러 개의 프로미스를 연결하여 사용할 수 있다는 점입니다. 앞 예제에서 then() 메서드를 호출하고 나면 새로운 프로미스 객체가 반환됩니다. 따라서, 아래와 같이 코딩이 가능합니다.

function getData() {
  return new Promise({
    // ...
  });
}

// then() 으로 여러 개의 프로미스를 연결한 형식
getData()
  .then(function (data) {
    // ...
  })
  .then(function () {
    // ...
  })
  .then(function () {
    // ...
  });

그러면 위의 형식을 참고하여 실제로 돌려볼 수 있는 예제를 살펴보겠습니다. 비동기 처리 예제에서 가장 흔하게 사용되는 setTimeout() API를 사용하였습니다.

new Promise(function(resolve, reject){
  setTimeout(function() {
    resolve(1);
  }, 2000);
})
.then(function(result) {
  console.log(result); // 1
  return result + 10;
})
.then(function(result) {
  console.log(result); // 11
  return result + 20;
})
.then(function(result) {
  console.log(result); // 31
});

위 코드는 프로미스 객체를 하나 생성하고 setTimeout()을 이용해 2초 후에 resolve()를 호출하는 예제입니다.

resolve()가 호출되면 프로미스가 대기 상태에서 이행 상태로 넘어가기 때문에 첫 번째 .then()의 로직으로 넘어갑니다. 첫 번째 .then()에서는 이행된 결과 값 1을 받아서 10을 더한 후 그다음 .then() 으로 넘겨줍니다. 두 번째 .then()에서도 마찬가지로 바로 이전 프로미스의 결과 값 11을 받아서 20을 더하고 다음 .then()으로 넘겨줍니다. 마지막 .then()에서 최종 결과 값 31을 출력합니다.

실무에서 있을 법한 프로미스 연결 사례

실제 웹 서비스에서 있을 법한 사용자 로그인 인증 로직에 프로미스를 여러 개 연결해보겠습니다.

getData(userInfo)
  .then(parseValue)
  .then(auth)
  .then(diaplay);

위 코드는 페이지에 입력된 사용자 정보를 받아와 파싱, 인증 등의 작업을 거치는 코드를 나타내었습니다. 여기서 userInfo는 사용자 정보가 담긴 객체를 의미하고, parseValueauthdisplay는 각각 프로미스를 반환해주는 함수라고 가정했습니다. 아래와 같이 말이죠.

var userInfo = {
  id: 'test@abc.com',
  pw: '****'
};

function parseValue() {
  return new Promise({
    // ...
  });
}
function auth() {
  return new Promise({
    // ...
  });
}
function display() {
  return new Promise({
    // ...
  });
}

이처럼 여러 개의 프로미스를 .then()으로 연결하여 처리할 수 있습니다.

프로미스의 에러 처리 방법

앞에서 살펴본 프로미스 예제는 코드가 항상 정상적으로 동작한다고 가정하고 구현한 예제입니다. 실제 서비스를 구현하다 보면 네트워크 연결, 상태 코드 문제 등으로 인해 오류가 발생할 수 있습니다. 따라서, 프로미스의 에러 처리 방법에 대해서도 알고 있어야 합니다.

에러 처리 방법에는 다음과 같이 2가지 방법이 있습니다. 

1.then()의 두 번째 인자로 에러를 처리하는 방법

getData().then(
  handleSuccess,
  handleError
);

2.catch()를 이용하는 방법

getData().then().catch();

위 2가지 방법 모두 프로미스의 reject() 메서드가 호출되어 실패 상태가 된 경우에 실행됩니다. 간단하게 말해서 프로미스의 로직이 정상적으로 돌아가지 않는 경우 호출되는 거죠. 아래와 같이 말입니다.

function getData() {
  return new Promise(function (resolve, reject) {
    reject('failed');
  });
}

// 1. then()으로 에러를 처리하는 코드
getData().then(function () {
  // ...
}, function (err) {
  console.log(err);
});

// 2. catch()로 에러를 처리하는 코드
getData().then().catch(function (err) {
  console.log(err);
});

프로미스 에러 처리는 가급적 catch()로

앞에서 프로미스 에러 처리 방법 2가지를 살펴봤습니다. 개개인의 코딩 스타일에 따라서 then()의 두 번째 인자로 처리할 수도 있고 catch()로 처리할 수도 있겠지만 가급적 catch()로 에러를 처리하는 게 더 효율적입니다.

그 이유는 아래의 코드를 보시면 알 수 있습니다.

// then()의 두 번째 인자로는 감지하지 못하는 오류
function getData() {
  return new Promise(function (resolve, reject) {
    resolve('hi');
  });
}

getData().then(function (result) {
  console.log(result);
  throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function (err) {
  console.log('then error : ', err);
});

getData() 함수의 프로미스에서 resolve() 메서드를 호출하여 정상적으로 로직을 처리했지만, then()의 첫 번째 콜백 함수 내부에서 오류가 나는 경우 오류를 제대로 잡아내지 못합니다. 따라서 코드를 실행하면 아래와 같은 오류가 납니다.

'에러를 잡지 못했습니다(Uncaught Error)' 로그

하지만 똑같은 오류를 catch()로 처리하면 다른 결과가 나옵니다.

// catch()로 오류를 감지하는 코드
function getData() {
  return new Promise(function (resolve, reject) {
    resolve('hi');
  });
}

getData().then(function (result) {
  console.log(result); // hi
  throw new Error("Error in then()");
}).catch(function (err) {
  console.log('then error : ', err); // then error :  Error: Error in then()
});

위 코드의 처리 결과는 다음과 같습니다.

발생한 에러를 성공적으로 콘솔에 출력한 모습

따라서, 더 많은 예외 처리 상황을 위해 프로미스의 끝에 가급적 catch()를 붙이시기 바랍니다.







Rookie mistake #5: using side effects instead of returning
이 코드에 어떤 문제가 있는 것 같나요?
somePromise().then(function () {
  someOtherPromise();
}).then(function () {
  // Gee, I hope someOtherPromise() has resolved!
  // Spoiler alert: it hasn't.
});

좋습니다. 이 코드를 가지고 당신이 알아야 하는 promise의 모든 것에 대해서 이야기 할 수 있습니다.
진지하게, 이 방법은 꽤나 이상해 보일 수 있지만 한번 이해하고 난 다음에는 우리가 여태 이야기한 모든 에러들을 예방할 수 있는 매우 좋은 방법입니다.

이전에 말했는데 promise가 가진 최대의 이점은 우리에게 소중한 return과 throw를 되돌려준다는 것입니다. 하지만 실제로 이게 어떤 형태일까요?

모든 promise는 우리에게 then() 메서드를 제공합니다. (또는 catch() 메서드나요.) then() 메서드 내부를 보겠습니다.

somePromise().then(function () {
  // I'm inside a then() function!
});

이 함수의 내부에서 우리는 어떤 일을 할 수가 있을까요? 세가지가 가능합니다.
1. return 다른 promise
2. return 발생한 값 이나 undefined
3, throw 에러
이게 다입니다. 이제 이것들을 이해를 하면 여러분은 promise를 이해한 것입니다. 하나씩 살펴보겠습니다.

1. Return another promise
이는 위에서 살펴본 composing promise의 일반적인 패턴입니다.

getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // I got a user account!
});

첫 번째 함수가 getUserAccountById() 함수를 리턴 합니다, 다음 함수로 말이지요. 다음 메서드는 콜백으로 해당 함수의 리턴값을 받습니다. 여기서 return을 하는 것이 매우 중요한데 만약에 return을 하지 않는다면 다음 함수는 아마 undefined를 받게 될 것입니다. 우리는 userAccount가 필요한데 말입니다.

2. Return a synchronous value (or undefined)
undefined를 리턴 하는 일은 종종 발생하는 실수입니다. 하지만 동기적으로 발생한 값을 리턴 하는 것은 동기적인 코드를 promise방식의 코드로 변환하는 굉장한 방법입니다. 예를들어 우리가 사용자 캐시를 in-memory 에 가지고 있다고 하겠습니다. 우리는 아래의 코드처럼 구현할 수 있습니다.

getUserByName('nolan').then(function (user) {
  if (inMemoryCache[user.id]) {
    return inMemoryCache[user.id];    // returning a synchronous value!
  }
  return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
  // I got a user account!
});

굉장하지 않나요? 두 번째 함수는 userAccount가 동기적이나 비동기적으로 가져온 값인지 아닌지는 신경 쓰지 않습니다. 첫 번째 함수는 동기적인 값이나 비 동기적인 값 모두를 마음대로 전달할 수가 있습니다.
불행하게도 값을 리턴하지 않는 함수의 경우에는 자바스크립트는 undefined로 값을 처리해버립니다. 이는 예상치 못한 문제를 일으키게 됩니다.
이러한 이유로 저는 개인적으로는 then() 함수 안에서는 반드시 throw나 return을 통해서 무언가를 다음 함수로 전달을 시킵니다. 
여러분도 이렇게 해야 합니다.

3. Throw a synchronous error
throw는 promise를 더욱 굉장하게 만들어 주는 역할을 합니다. 동기적으로 에러가 발생하면 throw를 통해 에러를 전달합니다. 사용자가 로그아웃이 되었다면 에러를 발생시키는 코드입니다. 

getUserByName('nolan').then(function (user) {
  if (user.isLoggedOut()) {
    throw new Error('user logged out!'); // throwing a synchronous error!
  }
  if (inMemoryCache[user.id]) {
    return inMemoryCache[user.id];       // returning a synchronous value!
  }
  return getUserAccountById(user.id);    // returning a promise!
}).then(function (userAccount) {
  // I got a user account!
}).catch(function (err) {
  // Boo, I got an error!
});

우리의 catch()는 사용자가 로그아웃이 되었다면 동기적으로 에러를 받습니다. 그리고 물론 promise가 reject가 발생하는 경우 비동기적으로 에러를 받습니다. 다시 한번 강조하면 함수는 에러가 동기적이든 비동기적이든 전혀 신경 쓰지 않습니다.

이는 매우 유용한데 왜냐하면 개발 시에 코딩의 에러를 발견하는데 도움을 줍니다. 예를 들어 then() 메서드의 내부에 JSON.parse()를 한다고 가정 하겠습니다. 이 경우에 json에 문제가 있다면 동기적으로 에러를 발생시킬 수 있습니다. callback의 경우에는 에러를 무시하는 일이 발생합니다. 하지만 promise와 함께라면 우리는 간단하게 catch() 내부에서 이모든 일들을 처리할 수 있습니다.


Advanced mistakes
자, 이제 조금 더 깊이 있게 들어가보겠습니다.

이번에 다룰 내용들은 advanced 내용입니다. advanced로 분류가 된 이유는 이미 promise를 이해하고 받아들인 개발자들이 일으키는 실수이기 때문입니다. 하지만 우리는 이것들을 다룰 필요가 있습니다. 만약에 우리가 쉽게 해결할 수 있는 문제였다면 앞으로의 내용들을 beginner 섹션에 다뤘을 것입니다.

Advanced mistake #1: not knowing aboutPromise.resolve()
위의 다양한 예들을 통해 promise는 동기적인 코드를 비 동기적인 코드로 감싸주는 매우 유용한 기능입니다. 구현은 아래와 같이 합니다.

new Promise(function (resolve, reject) {
  resolve(someSynchronousValue);
}).then(/* ... */);

위의 표현을 Promise.resolve()를 통해 훨씬 간결하게 표현하는 것이 가능합니다.
Promise.resolve(someSynchronousValue).then(/* ... */);

이 방법은 어떠한 동기적인 에러를 잡는데 매우 유용합니다. 그래서 저는 대부분의 promise-returning API를 구현할 때 제일 첫 시점에 아래와 같이 형태를 잡아두고 시작을 합니다.

function somePromiseAPI() {
  return Promise.resolve().then(function () {
    doSomethingThatMayThrow();
    return 'foo';
  }).then(/* ... */);
}

그냥 기억하세요. 동기적으로 throw가 발생하는 코드는 코드상의 어딘가에서 에러가 묻혀버려 거의 디버깅이 불가능합니다. 하지만 만약 모든 것들을 Promise.resolve()로 감싼다면 우린 항상 이것들일 catch()를 통해 확인할 수가 있습니다.

이와 유사하게 Promise.reject()가 있는데 이는 당장 promise 를 거부시키는데 사용할수있습니다.
Promise.reject(new Error('some awful error'));


Advanced mistake #2: catch() isn't exactly likethen(null, ...)
저는 위에서 catch()는 매우 유용하다고 말했습니다. 그래서 아래의 두 개의 작은 코드는 동일합니다.
somePromise().catch(function (err) {
  // handle error
});

somePromise().then(null, function (err) {
  // handle error
});


하지만 아래의 두 개의 코드는 동일한 의미는 아닙니다.
somePromise().then(function () {
  return someOtherPromise();
}).catch(function (err) {
  // handle error
});

somePromise().then(function () {
  return someOtherPromise();
}, function (err) {
  // handle error
});

만약에 위의 두 코드가 왜 다른지 궁금하다면 첫번째 코드에서 첫 함수가 에러를 반환한다면 무슨 일이 벌어질지를 생각해보면 됩니다.
somePromise().then(function () {
  throw new Error('oh noes');
}).catch(function (err) {
  // I caught your error! :)
});

somePromise().then(function () {
  throw new Error('oh noes');
}, function (err) {
  // I didn't catch your error! :(
});


보시는 봐와 같이 then(resolveHandler, rejectHandler)의 형태로 구현을 한다면 에러가 resolveHandler를 통해 반환이 된경우라면 rejectHandler가 에러를 잡아내지 못하게 됩니다.

이러한 이유로 저는 절대 두번째 방식으로는 구현을 하지 않습니다. 항상 에러에 대비해서는 catch()를 사용합니다. 단, Mocha 를 통한 비동기적인 테스트 코드 구현은 제외합니다.
it('should throw an error', function () {
  return doSomethingThatThrows().then(function () {
    throw new Error('I expected an error!');
  }, function (err) {
    should.exist(err);
  });
});



Promise.all()


Promise.all(iterable) 메서드는 iterable 인자의 모든 프로미스가 이행하거나 iterable 인자가 비어있는 경우 이행하는 프로미스를 반환합니다. iterable 인자 내 임의의 프로미스가 거부하는 경우 첫 번째로 거절한 프로미스의 이유를 사용해 거절합니다.

구문

Promise.all(iterable);

Link to section매개변수

iterable
Array 또는 String과 같이 순회 가능한 객체.

Link to section반환값

  • 인자가 빈 경우, 이미 이행된 Promise.
  • 인자에 프로미스가 없는 경우, 비동기적으로 이행된 Promise. 단, 구글 크롬 58은 이미 이행된 프로미스를 반환합니다.
  • 다른 모든 경우, 대기 중인 Promise. 반환하는 프로미스는 나중에 비동기적으로(스택이 비는 시점에) , 모든 프로미스가 이행하거나 임의의 프로미스가 거부할 때 이행/거부합니다. "Promise.all 실패-우선 연산" 예제를 참고하세요. 반환하는 프로미스의 이행 값은 인자로 넘어온 프로미스의 순서와 일치하며 완료 순서에 영향을 받지 않습니다.

Link to section설명

이 메서드는 여러 프로미스의 결과를 집계할 때 유용하게 사용할 수 있습니다.

이행:
빈 iterable이 전달되면이 메서드는 이미 해결 된 promise를 동기적으로 반환합니다.
전달 된 promise가 모두 충족(fulfilled)되거나 promise가 아닌 경우 Promise.all에서 반환하는 promise는 비동기 적으로 수행됩니다.
모든 경우에 반환된 promise는 인수로 전달 된 반복 가능한 값의 모든 값 (non-promise value)을 포함하는 배열로 충족(fulfilled)됩니다.

거부:
전달 된 promise중 하나라도 거부하면 Promise.all은 다른 promise가 남아있는지에 여부에 관계없이 다른 모든 Promise를 버리고 문제가 되는 값과 함께 거절(reject)한다.

 

Link to section예제

Link to sectionPromise.all사용하기

Promise.all은 배열 내 모든 값이 결정(resolve)할 때까지 기다립니다.

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, "foo");
}); 

Promise.all([p1, p2, p3]).then(function(values) { 
  console.log(values); // [3, 1337, "foo"] 
});

Link to sectionPromise.all 실패-우선 연산

Promise.all 은 배열 내 요소 중 어느 하나라도 거절(reject)하면 즉시 거절합니다. 즉 Promise.all 은 빠르게 실패합니다: 만약 timeout 이후 결정(resolve)하는 4개의 Promise를 가지고 있고, 그 중 하나가 거절(reject)하면 Promise.all 은 즉시 거절합니다.


all 구문중 실패시하는  Promise 함수가 있다면 해당 실패 promise 함수의 인자가 넘어오게 된다

성공하면 모든 인자가 출력 된다


var p1 = new Promise(function(resolve, reject) { 
  setTimeout(resolve, 1000, "one"); 
}); 
var p2 = new Promise(function(resolve, reject) { 
  setTimeout(resolve, 2000, "two"); 
});
var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 3000, "three");
});
var p4 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 4000, "four");
});
var p5 = new Promise(function(resolve, reject) {
  reject("reject");
});

Promise.all([p1, p2, p3, p4, p5]).then(function(value) { 
  console.log(value);
}, function(reason) {
  console.log(reason)
});

//From console:
//"reject"


ref : https://joshua1988.github.io/web-development/javascript/promise-for-beginners
ref  :http://yubylab.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Promise-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
ref : 
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all




반응형

'서버(Server) > Server' 카테고리의 다른 글

통신 모델 OSI 7 계층  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
Promise [2]  (0) 2018.08.16
[javascript] Singleton 싱글톤  (0) 2018.07.02
공개/비공개 스태틱 멤버와 클로저를 활용한 객체 생성  (0) 2018.07.01

+ Recent posts