๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

๐ŸŒˆ Chapter 8: ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํŒจํ„ด

๐Ÿ“š ๋‹จ์œ„ ํ…Œ์ŠคํŠธโ€‹

TDD๋ฅผ ์‹ค์ฒœ ์ค‘์ธ ์Šนํ˜„์ด๋Š” memoizedRestaurantApi ํผ์‚ฌ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์žฅ ๋จผ์ € ํ•  ์ผ์€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์Šนํ˜„์€ ์ƒฌ๋Ÿฟ์ด UI๋ฅผ ์ž์ฃผ ๊ณ ์น˜์ง€ ์•Š๋„๋ก ์ƒฌ๋Ÿฟ์ด ์‚ฌ์šฉํ•  ํ•จ์ˆ˜์™€ ๊ฐ™์€ getRestaurantsNearConference ํ•จ์ˆ˜๋ฅผ ์• ์ŠคํŒฉํŠธ๊ฐ€ ๊ฐ€๋ฏธ๋œ ์„œ๋“œํŒŒํ‹ฐ API์— ํ‘œ์ถœํ•˜๊ธฐ๋กœ ํ•œ๋‹ค.

์•ž์„œ ์„œ๋“œํŒŒํ‹ฐ API๋ฅผ ํ™•์žฅํ•˜์—ฌ getRestaurantsNearConference ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ ์ฝ”๋“œ์˜ ๋‹จ์œ„ํ…Œ์ŠคํŠธ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, API๊ฐ€ ์‹ค์ œ๋กœ ๋ฐ˜ํ™˜ํ•œ ๊ฐ์ฒด์˜ ํƒ€์ž…์€ ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ์—†๋‹ค. ๋”ฐ๋ผ์„œ ํ”„๋ผ๋ฏธ์Šค๋ฅผ ์“ด ๋น„๋™๊ธฐ ์ฝ”๋“œ ํ…Œ์ŠคํŠธ์˜ ์ž์งˆ๊ตฌ๋ ˆํ•œ ์š”์†Œ ๋•Œ๋ฌธ์— ์–ฝ๋งค์ด์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

describe('memoizedRestaurantApi', () => {
'use strict';

var api;
var service;
var returnedFromService;

beforeEach(() => {
api = ThirdParty.restaurantApi();
service = Conference.memoizedRestaurantApi(api);
returnedFromService = {};
});

describe('getRestaurantsNearConference(cuisine)', () => {
it('๊ธฐ๋Œ€ ์ธ์ž๋ฅผ ๋„˜๊ฒจ api์˜ getRestaurantsNearConference๋ฅผ ์‹คํ–‰', () => {
var cuisine = '๋ถ„์‹';
spyOn(api, 'getRestaurantsNearConference');
service.getRestaurantsNearConference(cuisine);
var args = api.getRestaurantsNearConference.calls.argsFor(0);
expect(args[0]).toEqual(cuisine);
});

it('์„œ๋“œํŒŒํ‹ฐ API์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => {
spyOn(api, 'getRestaurantsNearConference').and.returnValue(returnedFromService);

var value = service.getRestaurantsNearConference('Asian Fusion');
expect(value).toBe(returnedFromService);
});

it('๊ฐ™์€ ์š”๋ฆฌ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญํ•ด๋„ api๋Š” ํ•œ ๋ฒˆ๋งŒ ์š”์ฒญํ•œ๋‹ค', () => {
var cuisine = '๋ถ„์‹';

spyOn(api, 'getRestaurantsNearConference').and.returnValue(returnedFromService);

var iterations = 5;
for (var i = 0; i < iterations; i++) {
var value = service.getRestaurantsNearConference(cuisine);
}

expect(api.getRestaurantsNearConference.calls.count()).toBe(1);
});

it('๊ฐ™์€ ์š”๋ฆฌ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญํ•ด๋„ ๊ฐ™์€ ๊ฐ’์œผ๋กœ ๊ท€๊ฒฐ๋œ๋‹ค', () => {
var cuisine = 'ํ•œ์ •์‹';

spyOn(api, 'getRestaurantsNearConference').and.returnValue(returnedFromService);

var iterations = 5;
for (var i = 0; i < iterations; i++) {
var value = service.getRestaurantsNearConference(cuisine);
expect(value).toBe(returnedFromService);
}
});
});
});

์ด์–ด์„œ ๊ตฌํ˜„๋ถ€๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

var Conference = Conference || {};

Conference.memoizedRestaurantApi = function(thirdPartyApi) {
'use strict';

var api = thirdPartyApi;
var cache = {};

return {
getRestaurantsNearConference: function(cuisine) {
// ํ‚ค์›Œ๋“œ cuisine์— ํ•ด๋‹นํ•˜๋Š” ํ‚ค๊ฐ€ ์บ์‹œ์— ์žˆ๋Š”์ง€ ์ฐพ์•„๋ณด๊ณ , ์žˆ์œผ๋ฉด ์บ์‹œ๋œ ํ”„๋ผ๋ฏธ์Šค๋ฅผ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
if (cache.hasOwnProperty(cuisine)) {
return cache[cuisine];
}

// ์บ์‹œ์— ์—†์œผ๋ฉด ์„œ๋“œํŒŒํ‹ฐ API์— ์š”์ฒญ ํ›„ ์ „๋‹ฌ๋ฐ›์€ ํ”„๋ผ๋ฏธ์Šค๋ฅผ
// cache[cuisine] = returnedPromise;๋กœ ์บ์‹œ์— ์ถ”๊ฐ€ํ•œ ๋’ค ํ˜ธ์ถœ๋ถ€์— ํ”„๋ผ๋ฏธ์Šค๋ฅผ ๋„˜๊ฒจ์ค€๋‹ค.
var returnedPromise = api.getRestaurantsNearConference(cuisine);
cache[cuisine] = returnedPromise;
return returnedPromise;
}
}
}

๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๊ธฐ๋Šฅ ๋•๋ถ„์— getRestaurantsNearConference ํ•จ์ˆ˜๋Š” API ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค. ์„œ๋“œํŒŒํ‹ฐ restaurantApi๋ฅผ ์ด์šฉํ•œ ์ง€์ ์— ๋‹ฌ๋ž‘ memoizedRestaurantApi๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋˜๋‹ˆ๊นŒ ์•ˆ๋„ํ–ˆ๋˜ ์ƒฌ๋Ÿฟ์ด ์Šนํ˜„์—๊ฒŒ ํ•œ ๊ฐ€์ง€ ๋” ์ œ์•ˆํ•œ๋‹ค.

์ƒฌ๋Ÿฟ: ์Šนํ˜„, ๋„ค๊ฐ€ getRestaurantsNearConference๋ฅผ ๋„ฃ์œผ๋Ÿฌ๊ณ  ํ™•์žฅํ–ˆ๋“ฏ์ด ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ์• ์ŠคํŒฉํŠธ๋กœ restaurantApi๋ฅผ ํ™•์žฅํ•  ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ?
์Šนํ˜„: ์–ด, ๊ฐ€๋Šฅํ•ด. memoizedRestaurantApi ์—†์ด ๋‹ค๋ฅธ ๋ฐ์„œ๋„ ํ•„์š”ํ•˜๋ฉด ์–ผ๋งˆ๋“ ์ง€ ์“ธ ์ˆ˜ ์žˆ๋Š” ๋ฒ”์šฉ memoization ์• ์ŠคํŒฉํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ.

๐Ÿ“š AOP๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ถ”๊ฐ€ํ•˜๊ธฐโ€‹

๐ŸŽˆ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์• ์ŠคํŒฉํŠธ ์ƒ์„ฑํ•˜๊ธฐโ€‹

์Šนํ˜„์˜ ๋‹ค์Œ ๋ชฉํ‘œ๋Š” restaurantApi์ฒ˜๋Ÿผ ๋‹ค๋ฅธ ์ฝ”๋“œ์—์„œ๋„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๋•์„ ๋ณด๊ฒŒ๋” ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ฝ”๋“œ๋ฅผ ์ „ ์˜ˆ์ œ memoizedRestaurantApi์—์„œ ์ถ”์ถœํ•˜์—ฌ ์• ์ŠคํŒฉํŠธ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ฒƒ์ด๋‹ค.

๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•œ๋‹ค. returnValueCache๋Š” adviceํ•จ์ˆ˜ ํ•˜๋‚˜๋ฅผ ์ •์˜ํ•œ ๋ชจ๋“ˆ๋กœ ๊ตฌํ˜„๋œ๋‹ค. beforeEach ๋ธ”๋ก์˜ ๋‹ค์Œ ๋ฌธ์„ ์‹คํ–‰ํ•˜๋ฉด testFunction์„ ์–ด๋“œ๋ฐ”์ด์Šค๋กœ ์žฅ์‹ํ•œ๋‹ค.

Aop.around('testFunction', Aspects.returnValueCache().advice, testObject);
describe('returnValueCache', () => {
'use strict';

var testObject;
var testValue;
var args;
var spyReference;
var testFunctionExecutionCount;

beforeEach(() => {
// ํ…Œ์ŠคํŠธํ•  ๋•Œ๋งˆ๋‹ค ์šฐ์„  ์‹คํ–‰ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
testFunctionExecutionCount = 0;
testValue = {};
testObject = {
testFunction: function(arg) {
return testValue;
}
};

spyOn(testObject, 'testFunction').and.callThrough();
// ์• ์ŠคํŒฉํŠธ๊ฐ€ ์ ์šฉ๋œ ์ดํ›„์—๋Š”
// ์ŠคํŒŒ์ด๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ํ˜„์žฌ ์ฐธ์กฐ๊ฐ’์„ ๋ณด๊ด€ํ•ด๋‘”๋‹ค.
spyReference = testObject.testFunction;

// testObject.testFunction์„ returnValueCache ์• ์ŠคํŒฉํŠธ๋กœ ์žฅ์‹ํ•œ๋‹ค.
Aop.around('testFunction', Aspects.returnValueCache().advice, testObject);

args = [{ key: 'value' }, 'someValue'];
});

describe('advice(targetInfo)', () => {
it('์ฒซ ๋ฒˆ์งธ ์‹คํ–‰ ์‹œ ์žฅ์‹๋œ ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => {
var value = testObject.tetFunction.apply(testObject, args);
expect(value).toBe(testValue);
});

it('์—ฌ๋Ÿฌ ๋ฒˆ ์‹คํ–‰ ์‹œ ์žฅ์‹๋œ ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => {
var iterations = 3;

for (var i = 0; i < iterations; i++) {
var value = testObject.testFunction.apply(testObject, args);
expect(value).toBe(testValue);
}
});

it('๊ฐ™์€ ํ‚ค๊ฐ’์œผ๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ์‹คํ–‰ํ•ด๋„ ์žฅ์‹๋œ ํ•จ์ˆ˜๋งŒ ์‹คํ–‰ํ•œ๋‹ค', () => {
var iterations = 3;

for (var i = 0; i < iterations; i++) {
var value = testObject.testFunction.apply(testObject, args);
expect(value).toBe(testValue);
}

expect(spyReference.calls.count()).toBe(1);
});

it('๊ณ ์œ ํ•œ ๊ฐ ํ‚ค๊ฐ’๋งˆ๋‹ค ๊ผญ ํ•œ ๋ฒˆ์”ฉ ์žฅ์‹๋œ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค', () => {
var keyValues = ['value1', 'value2', 'value3'];

keyValues.forEach(function iterator(arg) {
var value = testObject.testFunction(arg);
});

// ์š”์ฒญ์„ ๊ฐ๊ฐ ๋‹ค์‹œ ์‹คํ–‰ํ•œ๋‹ค. ๊ฒฐ๊ณผ๋Š” ์บ์‹œ์—์„œ ๊ฐ€์ ธ์˜ค๋ฏ€๋กœ ์žฅ์‹๋œ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค.
keyValues.forEach(function iterator(arg) {
var value = testObject.testFunction(arg);
});

// ์žฅ์‹๋œ ํ•จ์ˆ˜๋Š” ๊ณ ์œณ๊ฐ’ ํ•˜๋‚˜๋‹น ๊ผญ ํ•œ ๋ฒˆ์”ฉ ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค.
expect(spyReference.calls.count()).toBe(keyValues.length);
});

// ์บ์‹œ ํ‚ค๊ฐ€ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ๋“ฑ๋“ฑ...
});
});

testObject.testFunction์— ์ŠคํŒŒ์ด๋ฅผ ์‹ฌ๊ณ ์„œ ์ด ํ•จ์ˆ˜์— ์• ์ŠคํŒฉํŠธ๋ฅผ ์ ์šฉํ•œ๋‹ค. ์ŠคํŒŒ์ด๊ฐ€ ์‹คํ–‰๋๋Š”์ง€ ํ™•์ธํ•˜๋ ค๊ณ  ํ•˜๊ธฐ ์ „๊นŒ์ง„ ๋งŒ์‚ฌ ์ˆœ์กฐ๋กญ๊ฒŒ ํ˜๋Ÿฌ๊ฐˆ ๊ฒƒ์ด๋‹ค.

testObject.testFunction์— ๋‘” ์ŠคํŒŒ์ด๋Š” ์ด ํ•จ์ˆ˜๋ฅผ ์• ์ŠคํŒฉํŠธ๋กœ ์žฅ์‹ํ•˜๋Š” ์ˆœ๊ฐ€ ์ข…์ ์„ ๊ฐ์ถœ ๊ฒƒ์ด๋‹ค. ๋”ฐ๋ผ์„œ ๋‹ค์Œ ๊ธฐ๋Œ€์‹์—์„œ calls๋Š” ์ด์ œ testObject.testFunction ํ”„๋กœํผํ‹ฐ๊ฐ€ ์•„๋‹ˆ์–ด์„œ ์‹คํŒจํ•œ๋‹ค.

expect(testObject.testFunction.calls.count()).toBe(1);

ํ•จ์ˆ˜์— ์• ์ŠคํŒฉํŠธ๋ฅผ ์ ์šฉํ•˜๊ธฐ ์ „, ๋ณธํ•จ์ˆ˜์˜ ์ฐธ์กฐ๊ฐ’์„ spyReference์— ๋ณด๊ด€ํ•˜์—ฌ ํ•ด๊ฒฐํ–ˆ๋‹ค.

expect(spyReference.calls.count()).toBe(1);

๋‹ค์Œ์€ returnValueCache ๊ตฌํ˜„๋ถ€์ด๋‹ค.

var Aspects = Aspects || {};

Aspects.returnValueCache = function() {
'use strict';

var cache = {};

return {
advice: function(targetInfo) {
// ํ•จ์ˆ˜์— ๋„˜๊ธด ์ธ์ž๋ฅผ ์บ์‹œ ํ‚ค๋กœ ์ด์šฉํ•œ๋‹ค.
// (๊ฐ์ฒด ์ฐธ์กฐ๊ฐ’ ๋น„๊ต๊ฐ€ ์•„๋‹Œ, ๋ฌธ์ž์—ด ๋น„๊ต๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด ๋ฌธ์ž์—ด๋กœ ๋ฐ”๊พผ๋‹ค).
var cacheKey = JSON.stringify(targetInfo.args);

if (cache.hasOwnProperty(cacheKey)) {
return cache[cacheKey];
}

// ์žฅ์‹๋œ ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์™€ ์‹คํ–‰ํ•œ ๋’ค ๊ทธ ๋ฐ˜ํ™˜๊ฐ’์„ ์บ์‹œ์— ์ €์žฅํ•œ๋‹ค.
var returnValue = Aop.next(targetInfo);
cache[cacheKey] = returnValue;
return returnValue;
}
};
};

๐ŸŽˆ returnValueCache ์• ์ŠคํŒฉํŠธ๋ฅผ restaurantApi์— ์ ์šฉํ•˜๊ธฐโ€‹

๋งˆ์ง€๋ง‰์œผ๋กœ restaurantApi ๋ฉ”์„œ๋“œ๋ฅผ restaurantApi ์• ์ŠคํŒฉํŠธ๋กœ ์žฅ์‹ํ•œ๋‹ค. restaurantApi ํ•จ์ˆ˜์— returnValueCache ์• ์ŠคํŒฉํŠธ๋ฅผ ์ง์ ‘ ์ ์šฉํ•˜๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ธ ํ•ด๊ฒฐ์ฑ…์ด๋‹ค. ๊ทธ๋ž˜์•ผ getRestaurantsNearConference๋„ ๊ธฐ์–ตํ•˜๋ฉด์„œ getRestaurantsWithinRadius๋ฅผ ์“ฐ๋Š” ๋‹ค๋ฅธ ํ•จ์ˆ˜๊นŒ์ง€ ์ž๋™์œผ๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํ˜œํƒ์„ ๋ˆ„๋ฆด ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ ์˜ˆ๋Š” getRestaurantsWithinRadius ํ•จ์ˆ˜๋ฅผ ๊ธฐ์–ตํ•˜๊ฒŒ๋” ThirdPartyRestaurantApiAspects.js๋ฅผ ์ˆ˜์ •ํ•œ ์ฝ”๋“œ๋‹ค.

// getRestaurantsWithinRadius์— ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํŒจํ„ด ์ ์šฉ
Aop.around(
// ๋ฐ˜ํ™˜๊ฐ’์„ ์ˆ˜์ •ํ•ด์•ผ ํ•  ํ•จ์ˆ˜
'restaurantApi',
// ๋ฐ˜ํ™˜๊ฐ’์„ ์ˆ˜์ •ํ•˜๋Š” ํ•จ์ˆ˜
function addMemoizationToGetRestaurantsWithinRadius(targetInfo) {
// ThirdParty.restaurantApi()๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ์›๋ณธ API
var api = Aop.next.call(this, targetInfo);

// getRestaurantsWithinRadius ํ•จ์ˆ˜๋ฅผ ์žฅ์‹ํ•˜์—ฌ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ถ”๊ฐ€
Aop.around('getRestaurantsWithinRadius',
Aspects.returnValueCache().advice, api);

// ๊ณ ์นœ API๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
return api;
},
ThirdParty
);

// ThirdParty.restaurantApi()์— getRestaurantsNearConference ๋ฉค๋ฒ„ ์ถ”๊ฐ€
Aop.around(
// ๋ฐ˜ํ™˜๊ฐ’์„ ์ˆ˜์ •ํ•ด์•ผ ํ•  ํ•จ์ˆ˜
'restaurantApi',
// ๋ฐ˜ํ™˜๊ฐ’์„ ์ˆ˜์ •ํ•˜๋Š” ํ•จ์ˆ˜
function addGetRestaurantsNearConference(targetInfo) {
'use strict';

// ThirdParty.restaurantApi()๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ์›๋ณธ API
var api = Aop.next.call(this, targetInfo);

// API์— ์ถ”๊ฐ€ํ•  ํ•จ์ˆ˜
function getRestaurantsNearConference(cuisine) {
return api.getRestaurantsWithinRadius(
'์šธ์‚ฐ ๋‚จ๊ตฌ ์‹ ์ •๋กœ 20๋ฒˆ๊ธธ 988', 2.0, cuisine
);
}

// ์—†์œผ๋ฉฐ ์ด ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.
api.getRestaurantsNearConference =
api.getRestaurantsNearConference || getRestaurantsNearConference;

// ๊ณ ์นœ API๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
return api;
},

// ๋ฐ˜ํ™˜๊ฐ’์„ ์ˆ˜์ •ํ•ด์•ผ ํ•  ํ•จ์ˆ˜์˜ ์ด๋ฆ„๊ณต๊ฐ„
ThirdParty
);

๐Ÿ“š ์ •๋ฆฌํ•˜๊ธฐโ€‹

๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํŒจํ„ด ๊ตฌํ˜„ ์‹œ ๋‹ค์Œ ๋‘ ๊ฐ€์ง€๋ฅผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ผญ ํ™•์ธํ•˜๋ผ.

  • ๋ฐ˜ํ™˜๊ฐ’์„ ๊ธฐ์–ตํ•  ํ•จ์ˆ˜ ๋˜๋Š” ์ž์›์€ ์–ด๋–ค ํ‚ค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋งจ ์ฒ˜์Œ ํ˜ธ์ถœํ•  ๋•Œ ํ•œ ๋ฒˆ๋งŒ ์ ‘๊ทผํ•œ๋‹ค.
  • ๊ทธ ์ดํ›„๋กœ ๊ฐ™์€ ํ‚ค๋ฅผ ์ธ์ž๋กœ ๊ธฐ์–ตํ•œ ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜๋ฉด ๋งจ ์ฒ˜์Œ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ์™€ ๊ฐ™์€ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.