[읽고 정리하기] 함수형으로 사고하기

[읽고 정리하기] 함수형으로 사고하기

이 글은

루이스 아텐시오, 『FUNCTIONAL PROGRAMMING IN JAVASCRIPT』, MANNING, 한빛미디어(2018)

를 읽고 이를 제 생각을 보태 정리한 글임을 밝힙니다.

책에서 발췌한 부분은 '명조체'로 구분하겠습니다 :)

최근 6주 간 진행되었던 프로젝트를 마치면서, 함수형 프로그래밍에 대한 관심이 생겼습니다.

협업을 하면서도 무언가 비효율적이고 장단이 맞지 않는 코드들 때문에 매주 리팩토링 시간에 의미 없는 고민만 계속했던 것 같네요.

그에 대한 좋은 대안으로서 고려해보기 위해, 당분간은 함수형 프로그래밍에 대해 이 책을 위주로 공부하고

이후 코드를 작성할 때 최대한 활용해볼 계획입니다.

매주 1장 이상 포스팅하는 것이 목표인데 잘 할 수 있으려나요 ㅎㅎ

일단 시작이 반이니까 시작해보도록 하겠습니다!

제 1장. 함수형으로 사고하기

오늘은 제 1장의 내용인 함수형으로 사고하기에 대해 포스팅을 다뤄보려고 합니다.

"객체지향(OO)은 가동부(moving parts)를 캡슐화하여 코드의 이해를 돕는다.

함수형 프로그래밍(FP)은 가동부를 최소화하여 코드의 이해를 돕는다."

- 마이클 페더스

이 책의 저자는 도입부에서 이렇게 말합니다.

"자바스크립트는 상태 공유가 보편적인, 참으로 동적인 언어라서 조금만 시간이 지나도 복잡해지면서 가독성이 떨어지고 관리하기 어려운 코드가 되기 일쑤이다."

백 번 공감하는 말입니다. 아직 제가 다른 언어에 대한 깊은 경험은 없지만, 자바스크립트로 프로젝트를 진행했을 때 점점 복잡해지고 가독성이 떨어지는 코드에 대한 고민이 생기기 시작했습니다.

또한 동적인 언어다. 이 말의 뜻이 무엇인지 궁금해지기 시작했습니다. 아마 이 책을 읽으며 답을 찾아갈 수 있겠지요.

저자는 또한 아래 5가지를 체크해보라고 말합니다. 하나라도 해당된다면 이 책이 아주 적합한 안내서가 될 것이라고 하네요.

확장성 : 추가 기능을 지원하기 위해 계속 코드를 리팩토링해야 하는가?

모듈화 용이성: 파일 하나를 고치면 다른 파일도 영향을 받는가?

재사용성: 중복이 많은가?

테스트성: 함수를 단위 테스트하기 어려운가?

헤아리기 쉬움: 체계도 없고 따라하기 어려운 코드인가?

저는 불행하게도(?) 어느 것 하나 확실하게 해당되지 않는다고 말할 수 없는 코드를 작성하고 있었습니다. 그나마 계속해서 리팩토링하고 모듈간 의존성을 줄이고 재사용성을 줄이려고 노력하고 있다 라고 변명할 수는 있겠네요.

"FP는 새로운 도구나 API가 아니다."

도입부의 마지막 말입니다. 그 어떤 문제를 해결해주는 도구나 API 같은 것이 아니라 하나의 패러다임이다. 라는 말을 하고 싶은 걸까요? 일단 확실히 제 관심을 끌기에 충분한 도입부였습니다. 빨리 그 내막을 살펴보고 싶군요.

다음 파트로 넘어가봅시다.

1.1 함수형 프로그래밍은 과연 유용한가?

자바스크립트는 함수형 스타일로 작성해야 더 효과적이라는 측면이 오랫동안 간과되었다. 자바스크립트는 상태 관리를 개발자에게 떠넘기는 동적인 플랫폼이기 때문이다. (결국, 각종 애플리케이션 버그를 양산하는 근원이다.)

이는 자바스크립트 코드를 함수형으로 작성하면 대부분의 문제가 해결된다.

전체 애플리케이션 품질을 향상시키는 동시에 자바스크립트 언어를 더 잘 이해하게 되니 스킬도 향상된다.

.

.

.

'상태 관리를 개발자에게 떠넘기는 동적인 플랫폼이다.'

그렇군요. 이 책에서 동적이라는 얘기는 상태 관리를 개발자에게 떠넘기는 것을 의미하나 봅니다.

1.2 함수형 프로그래밍이란?

한 마디로 함수 사용을 강조하는 소프트웨어 개발 스타일이다.

단지 어떤 결과를 만드는 함수를 적용하는 그런 단순한 차원이 아니라

"애플리케이션의 부수효과를 방지하고 상태 변이를 감소하기 위해 데이터의 제어 흐름과 연산을 추상화하는 것입니다."

이 책은 각 단어에 대해 하나씩 탐구한다.

.

.

.

얼핏 알듯말듯 하지만 아직까지는 완전히 와닿지는 않네요.

밑에 예시를 한 번 보시죠.

document.querySelector('#msg').innerHTML = 'Hello World';

위와 같이 msg라는 id를 갖는 요소에 h1태그로 Hello World라는 문자열을 삽입하는 것을 떠올려 봅시다.

이를 위한 함수를 만들어보면 어떻게 만들 수 있을까요?

function printMessage(elementId, format, message) { document.querySelector(`#${elementId>`).innerHTML = `<${format}>${message}`; }

일단 첫 번째 예시로 위와 같이 id, 태그 포맷, 들어갈 내용인 message를 인자로 받고

해당 요소에 innerHtml로 달아주는 함수를 떠올려 볼 수 있습니다.

var printMessage = run(addToDom('msg'),h1, echo);

하지만 이 책에서의 예시는 위와 같습니다.

run함수는 addToDom, h1, echo 함수들을 마치 자전거 체인처럼 연결해서 쓰는 구조로서,

각 함수의 반환값이 다른 함수의 입력값으로 전달되게끔 합니다.

이러한 코드는 재사용성과 믿음성(?)이 좋고 이해하기 쉬우며,

작은 조각들로 프로그램을 나눈 후 전체적으로 헤아리기 쉬운 형태의 프로그램으로 다시 조합하는 과정이라고 할 수 있죠.

과정을 살펴보자면,

echo함수에 'Hello World'라는 문자열이 반환되면,

h1함수로 전달되고

h1함수의 반환값이 마지막 addToDom함수로 전달되게 되죠.

이를 조금 더 확장한다면 아래와 같이 쓸 수 있습니다.

var printMessage = run(console.log, repeat(2), h2, echo);

repeat 함수를 넣어서 2번 반복시키고

마지막에 DOM에 추가하는 것이 아닌 console.log를 통해 콘솔에 출력하게 되죠

저자는 FP 특유의 선언적 개발 방식을 온전히 이해하려면 아래와 같은 기본 개념을 숙지해야 한다고 하네요.

기본 개념

선언적 프로그래밍 순수함수 참조 투명서 불변성

이 부분 꼭 기억해놓으시고

책을 읽으며 하나씩 알아가보도록 합시다!

1.2.1 함수형 프로그래밍은 선언적

함수형 프로그래밍은 큰 틀에서 선언적 프로그래밍 패러다임에 속합니다.

내부적으로 코드를 어떻게 구현했는지, 데이터는 어떻게 흘러가는지 밝히지 않은 채 연산/작업을 표현하는 사상입니다.

그렇습니다. 여기서 중요한 단어는 바로 '선언적'이라는 단어죠.

한 번 예시를 보도록 하겠습니다.

var array = [0,1,2,3,4,5,6,7,8,9]; for(let i=0;i

위의 array를 for문을 돌면서 제곱하는 로직입니다.

[0,1,2,3,4,5,6,7,8,9].map( function(num) { return Math.pow(num, 2); } };

이 코드는 고차함수인 map을 이용해서 위와 똑같은 로직을 수행했죠.

이전 코드와 비교하면 루프 카운터를 직접 관리하고 배열 인덱스에 정확하게 접근하도록 신경써야 하는 일을 개발자가 할 필요가 없어져서 부담이 줄어들게 됩니다.

모두들 아시겠지만 이는 ES6부터 도입된 화살표 함수(람다 표현식)을 써서 더 간략하게 쓸 수 있습니다.

[0,1,2,3,4].map(num => Math.pow(num,2));

한층 깔끔하고 직관적이 되었네요

왜 루프를 제거해야 할까요?

루프는 재사용하기도 어렵거니와 다른 연산에 끼워 넣기도 어려운 명령형 제어 구조물입니다.

또한 루프는 반복시마다 값이나 상태가 계속 바뀝니다.

이는 함수형 프로그래밍이 지향하는 무상태성과 불변성에 반합니다.

무상태 코드란, 전역 상태를 바꾸거나 혼선을 일으킬 가능성이 없으며,

무상태, 즉 상태를 두지 않으려면 부수효과와 상태 변이를 일으키지 않는 순수함수를 써야한다.

1.2.2. 순수함수와 부수효과

순수함수의 특성을 정리하면,

- 주어진 입력에만 의존할 뿐, 평가 도중 또는 호출 간 변경될 수 있는 숨겨진 값이나 외부 상태와 무관하게 작동합니다.

- 전역 객체나 레퍼런스로 전달된 매개변수를 수정하는 등 함수 스코프 밖에서 어떠한 변경도 일으키지 않습니다.

코드를 하나 보겠습니다.

var counter = 0; function increment() { return ++counter; }

이 함수는 외부 변수 counter 를 읽고 수정하므로 불순합니다(?)

Date.now() 처럼 날짜/시간 함수도 미리 헤아릴 수 있는 일정한 결괏값을 내지 않기 때문에 순수함수가 아니다.

this 키워드를 거쳐 인스턴스 데이터에 접근하는 것 역시시 부수효과가 유발되는 흔한 예이다.

특히 자바스크립트의 this는 해당 함수의 런타임 콘텍스트에 따라 달라지므로 여타 프로그래밍 언어와는 작동 방식이 다릅니다. 그래서 코드가 더더욱 헷갈릴 수 있어 필자는 가급적 this를 쓰지 않습니다.

와... 그렇네요... 무심코 쓰던 Date.now()도 순수함수라고 볼 수 없군요.

this 키워드는 보통 class 를 쓸 때 쓰게 되는 것 같은데

대표적으로 함수형으로 코드를 짜려면 class 문법은 피해야겠네요.

부수효과가 발생하는 상황에는 어떤 것들이 있을까요?

- 전역 범위에서 변수, 속성, 자료구조를 변경

- 함수의 원래 인수 값을 변경

- 사용자 입력을 처리

- 예외를 일으킨 해당 함수가 붙잡지 않고 그대로 예외를 던짐

- 화면 또는 로그 파일에 출력

- HTML 문서, 브라우저 쿠키, DB에 질의

알아두고 넘어가면 좋을 것 같습니다. 부수효과를 발생시키지 않는 순수함수를 짜는 것이 목표이니까요!

부수효과를 줄이려면 어떻게 해야할까요?

- 긴 함수를 하나의 목적을 가진 짧은 함수로 각각 분리한다.

- 함수가 해야 할 작업에 필요한 인수를 모두 명시하여 부수효과 개수를 줄인다.

인수를 모두 명시하여야 한다는 말이 저는 되게 도움이 되는 것 같네요.

외부 값을 참조하는 것이 아닌 받은게 무엇이든 그걸 처리한다는 느낌(?)이니까요.

커링

커링이란, 유명한 FP 기법으로 함수의 여러 인수를 부분적으로 나누어 세팅하는 것입니다.

커링의 예를 코드로 살펴볼까요?

하나의 함수를 3가지 단계로 나누어 커링하는 예시를 보도록 하겠습니다.

var find = curry((db, id)=> { let obj = db.find(id); if(obj === null){ throw new Error('객체를 찾을 수 없습니다!'); } return obj; });

위 함수는 객체가 저장된 곳을 가리키는 레퍼런스인 db와 검색할 학생 id를 받네요.

var csv = student => `${student.ssn}, ${student.firstname}, ${student.lastname}`;

얘는 학생 객체를 콤마로 분리된 문자열로 바꾸고

var append = curry((selector, info) => { document.querySelector(selector).innerHTML = info; });

얘는 selector 함수와 학생 정보인 info를 받아 HTML로 달아주는 함수네요

여기서 학생 상세 정보를 페이지에 표시하려면 학생 데이터가 필요하겠네요.

이렇게만 개선해도 벌써 여러 가지 장점이 생깁니다.

- 재사용 가능한 컴포넌트로 나뉘어 코드가 훨씬 유연

- 이렇게 잘게 나뉜 함수를 재사용하면 신경 써서 관리할 코드 크기가 확 줄기 때문에 생산성 높음

- 프로그램이 해야 할 일들을 고수준에서 단계별로 명확하게 보여주는 선언적 스타일로, 코드 가독성 향상

- 무엇보다 중요한 건, HTML 객체와의 상호작용을 자체 함수를 빼내어 순수하지 않은 로직을 순수함수에서 배제했다는 점.

아직은 느슨하지만 부수효과를 줄임으로써 외부 조건 변화에 덜 취약한 프로그램이 되었습니다.

위 예시에서 find 함수가 예외를 내는 별도의 null 체크 분기문이 포함된 것은 일관된 반환값을 보장하기 위함이고,

이를 통해 전체 함수 결과를 예측 가능한 방향으로 유도함으로써 참조 투명성이라는 순수함수 본연의 특징을 지키기 위함입니다.

참조 투명성에 대해 더 알아봅시다.

1.2.3 참조 투명성과 치환성

참조 투명성은 순수함수를 정의하는 좀 더 공식적인 방법이다.

여기서 순수성이란 함수의 인수와 결괏값 사이의 순수한 매핑 관계를 의미한다.

어떤 함수가 동일한 입력을 받았을 때 동일한 결과를 내며 이를 참조 투명한 함수라고 함.

var increment = counter => counter + 1;

이런 함수는 코드를 테스트하기 쉽고 전체 로직을 파악하는 것도 쉽습니다.

참조 투명성 혹은 등식 정합성은 수학에서 나온 용어이지만 프로그래밍 언어의 함수는 수학 함수처럼 움직이지 않기 때문에 참조 투명성은 전적으로 개발자의 숙제로 남습니다

이런 식으로 구축한 프로그램은 시스템의 상태를 머릿속으로 그려볼 수 있고 코드를 재작성하거나 치환하더라도 원하는 결과를 얻을 수 있기 때문에 헤아리기가 쉽습니다.

var sum = (total, current) => total + current; var total = arr => arr.reduce(sum); var size = arr => arr.length; var divide = (a, b) => a / b; var average = arr => divide(total(arr), size(arr)); average(input); // 90

1.2.4. 불변 데이터 유지하기

불변 데이터는 한번 생성된 후에는 절대 바뀌지 않습니다.

다른 언어도 그렇듯이 문자열, 숫자 등 자바스크립트의 모든 기본형은 처음부터 불변입니다.

그러나 배열 등의 객체는 불변이 아니어서 함수 인수로 전달해도 원래 내용이 변경되어 부수효과가 발생할 수 있습니다.

또 예시를 하나 보죠.

var sortDesc = arr => { arr.sort( (a, b) => b - a ); };

이 코드는 얼핏 보기에는 부수효과가 없는 좋은 코드 같지만 함정이 있습니다.

배열은 불변객체가 아니기 때문에 배열의 원소를 정렬하게 되면 부수효과를 일으킵니다.저자의 따르면

이는 언어 자체의 심각한 결함이라고 하는데

이를 극복하는 방안은 다음 장 이후에 논의하겠습니다.

함수형 프로그래밍에 대해 더 간명하게 정의를 내리자면,

함수형 프로그래밍은, 외부에서 관찰 가능한 부수효과가 제거된 불변 프로그램을 작성하기 위해 순수함수를 선언적으로 평가하는 것입니다.

오늘날 자바스크립트 개발자가 직면한 문제의 원인은,

대부분 뚜렷한 체계 없이 분기 처리를 남발하고 외부 공유 변수에 지나치게 의존하는 덩치 큰 함수를 과용하는 데 있습니다.

안타깝지만 아직도 많은 애플리케이션이 이런 딱한 상황에 처해 있고, 심지어 성공적이라는 작품조차 많은 파일이 한데 뒤섞여 추적/디버깅이 어려운 가변/전역 데이터를 공유하는 촘촘한 그물망이 형성된 경우가 있습니다.

(이미 구축된 애플리케이션을 함수형으로 바꾸는 것도 정말 만만치 않겠네요)

함수를 순수 연산의 관점에서 데이터를 절대 변경하지 않는 고정된 작업 단위로 바라본다면 확실히 잠재적인 버그는 줄게 될 것입니다.

이번 파트에서 포인트는 바로 이것이네요. 순수 연산, 데이터 불변. 하지만 아직 의문이 해결되지 않습니다.

어딘가에서는 데이터를 변경해야하는 점이 생기고 어딘가는 불순한(?) 연산이 필요한데

이를 완벽히 틀어막을 수 있을까요?

이것에 대한 답은 마지막에 나옵니다. ㅎㅎ

이번 장 마지막으로 함수형 프로그래밍의 좋은 점을 살펴보고 마치도록 합시다.

1.3 함수형 프로그래밍의 좋은 점

FP로 개발한 자바스크립트 애플리케이션은 어떤 점이 좋은지 고수준에서 살펴보자.

- 간단한 함수들로 작업을 분해한다.

- 흐름 체인으로 데이터를 처리한다.

- 리액티브 패러다임을 실현하여 이벤트 중심 코드의 복잡성을 줄인다.

간단한 함수로 분해하고 흐름 체인은 여태 봐왔던 내용이라 감이 옵니다.

리액티브 패러다임이 뭘까요?

이벤트 중심 코드의 복잡성을 줄인다?

일단 계속 읽어보겠습니다.

1.3.1 복잡한 작업을 분해하도록 유도

FP에서 모듈화는 단일성의 원리와 밀접한 관련이 있습니다.

모름지기 함수는 저마다 한 가지 목표만 바라봐야 한다는 사상이지요.

아까 살펴보았던 run 함수는 이 책에서 가장 중요한 합성이라는 기법을 구현한 함수로, 두 함수를 합성하면 첫 번째 함수의 결과를 다음 하수에 밀어 넣는 새로운 함수가 탄생합니다.

f * g = f(g(x))

함수 합성은 고수준의 추상화를 통해 자세한 내막을 밝히지 않아도 코드가 수행하는 전단계를 일목요연하게 나타냅니다.

다른 함수를 인수로 받는 함수를 고계함수(고차함수)라고 합니다

그래서 map, filter 등등을 고차함수라 했던 거군요!

1.3.2 데이터를 매끄럽게 체이닝하여 처리

함수 체인은 필요한 시점까지 실행을 미루는 느긋한 평가를 수행합니다.

다른 데에선 전혀 쓸 일이 없는 일련의 코드를 전부 실행하지 않아도 되니 CPU 부하가 줄어들어 성능이 좋아지죠.

이러면 다른 함수형 언어에 기본 탑재된 필요 시 호출(call by need) 동작을 효과적으로 모방할 수 있습니다.

여기서 새로운 깨달음을 얻게 됩니다.

느긋한 평가와 이로 인해 일어나는 CPU 부하를 줄일 수 있다...오?!call by need를 흉내낼 수 있는 방법이군요?!

아래는 체이닝의 예시입니다.

_.chain(enrollment) .filter(student => student.enrolled > 1) .pluck('grade') .average() .value();

1.3.3 복잡한 비동기 애플리케이션에서도 신속하게 반응

비동기 처리는 다양하게 있습니다. 원격 데이터 조회, 사용자 입력 데이터 처리, 지역 저장소와 연동 등...

콜백 패턴은 성공/실패 처리 로직이 중첩된 형태로 흩뿌려져 있기 때문에 코드의 선형 흐름이 깨지고 무슨 일을 하는지 파악하기 어렵습니다.

앵귤러JS 같은 웹 프레임워크가 아직 널리 쓰이고 있긴 하지만, RxJS처럼 FP의 강력한 장점으로 무장하여 난제를 척척 해결하는 신흥 강자들이 실무에 등장하고 있습니다.

리액티브 프로그래밍은 함수형 프로그래밍 중에서 가장 흥미진진한 응용 분야일 겁니다.

자바스크립트 개발자들이 서버는 물론 클라이언트 측에서 매일매일 씨름하는 비동기 코드, 이벤트 중심 코드의 복잡도를 현저하게 줄이는 데 큰 도움이 되지요.

리액티브 패러다임의 가장 큰 장점은, 더 높은 수준으로 코드를 추상하여 비동기, 이벤트 기반 프로그램을 설정하느라 반복되는 판박이 코드는 아예 잊고 비즈니스 로직에만 전념할 수 있게 해준다는 겁니다. 또 함수를 체인으로 묶고 합성하는 FP의 능력을 최대한 이끌어낼 수 있습니다.

함수형 프로그래밍에 기반을 둔 리액티브 프로그램은 순수함수를 이용하여 map, reduce처럼 많이 쓰는 연산으로 데이터를 처리할 수 있고 람다 표현식의 간결함을 누릴 수 있다는 이점이 있습니다.

리액티브 패러다임이 뭘까요?

리액티브 패러다임은 옵저버블이라는 아주 중요한 장치를 매개로 움직입니다.

이 옵저버블을 이용하면 스트림을 구독해서 원하는 연산을 우아하게 합성 및 체이닝할 수 있습니다.

자, 예시를 보시죠!

Rx.Observable.fromEvent(document.querySelector('#student-ssn'), 'keyup') .pluck('srcElement', 'value') .map(ssn => ssn.replace(/^\s*|\s*$|\-/g,'')) .filter(ssn => ssn !== null && ssn.length ===9) .subscribe(validSsn => { console.log(`올바른 SSN ${validSsn}!`); });

복잡해보이죠?

하지만 여기서 가장 주목해야 할 부분은, 수행하는 모든 연산이 완전한 불변이고

비즈니스 로직은 모두 개별 함수로 나뉘었다는 점입니다.

굳이 리액티브/함수형 섞어 쓸 필요는 없지만 함수형으로 사고하다 보면

두 가지를 혼용하게 되어 결구 함수형 리액티브 프로그래밍(FRP)라는 정말 기막힌 아키텍처에 눈을 뜨게 된다고 하네요.

아직까지 저는 리액티브 패러다임에 대해 잘 이해하지 못했지만

함수형 프로그래밍에 대한 필요성은 확실히 느낄 수 있었습니다.

처음에 언급한 마이클 페더스의 말에 의하면 함수형 프로그래밍을 코드에 적용하는 건 전부를 얻거나 전부를 잃는 식의 접근 방법이 아닙니다.

이미 객체 지향형 아키텍처와 FP를 병용하여 혜택을 본 애플리케이션 사례는 많습니다.

(여기서 느꼈습니다. 모든 구조를 FP로 쓰라는 것이 아니구나...!)

FP는 불변성과 공유 상태를 엄격하게 통제하므로 멀티스레드 프로그램도 보다 직관적으로 작성할 수 있습니다.

자바스크립트는 싱글스레드로 작동하는 플랫폼이므로 멀티스레드는 우리가 걱정하거나 이 책에서 다룰 주제는 아닙니다.

자!

여기까지 1장의 내용이었습니다.

저는 1장을 읽고 빨리 함수형 프로그래밍을 접해보고 싶은 생각이 마구 들었습니다.

작은 단위로 나누고 부수효과를 줄이고 체이닝하여 로직을 처리하는 것이

평소 제가 다루던 코드 패턴과는 조금 차이가 있네요.

다음 주에는 2장을 읽고 다시 찾아오도록 하겠습니다.

1장의 내용은 거의 필요성에 대한 설득이다보니, 저자의 어구가 많았는데요.

2장부터는 제 방식대로 정리해서 풀어가볼 계획입니다...!

요약

순수 함수를 사용한 코드는 전역 상태를 바꾸거나 깨드릴 일이 전혀 없으므로 테스트, 유지보수가 더 쉬운 코드를 개발하는 데 도움이 됩니다.

함수형 프로그래밍은 코드를 선언적으로 작성하므로 헤아리기 쉽고 전체 애플리케이션의 가독성 역시 향상됩니다. 또 함수와 람다 표현식을 조합하여 깔끔하게 코딩할 수 있습니다.

여러 원소로 구성된 컬렉션 데이터는 map, reduce 같은 연산을 함수 체인으로 연결하여 물 흐르듯 매끄럽게 처리할 수 있습니다.

함수형 프로그래밍은 함수를 기본적인 구성 요소로 취급합니다. 이는 일급/교차함수 개념에 기반을 두며 코드의 모듈성, 재사용성을 높입니다.

리액티브/함수형 프로그래밍을 융합하면 이벤트 기반 프로그램 특유의 복잡성을 줄일 수 있습니다.

from http://hanastro.tistory.com/52 by ccl(A) rewrite - 2021-12-18 23:01:15