프리코스를 참여하는 친구와 함께 모각코를 하다가, 친구가 물었다.
야 기능명세서는 어떻게 쓰는거냐?
작년의 내 모습이 떠올랐다.
독학으로 협업 경험이 없던 나에게 작년 프리코스는 개발자로서의 전환점이었다.
음... 그때 누가 이런 방식으로 방향성을 제시해줬으면 조금 더 많이 고민할 수 있었을텐데..!
너무 어렵다.
필자도 작년 프리코스에서 이런 키워드들을 처음 접하고, 미션에 열심히 적용하느라 너무 힘들었다.
다만! 너무 어렵다면, 우선 아래의 방식으로 기능 명세서
를 분석해보며 첫 걸음을 내딛어보자.
물론, 이때 얻은 키워드들이 1년 동안의 좋은 학습 방향성을 제공해준 것은 명백하다. 또한, 이러한 키워드들을 열심히 학습해서 현재의 미션에 한 번은 적용해보는 것이 나쁜 경험은 아니라고 생각된다.
1주차 미션이 끝나면, 반드시 다른 사람들의 PR을 분석해보고 따로 키워드 적어서 공부하고 적용해보자! :)
필자는 기능명세서를 작성하기 전, 아래의 단계를 거쳐 문제를 분석한다.
- 어떤 기능들이 필요한가?
- 필요한 객체는 무엇인가?
- 이 객체들은 어떻게 상호작용하고 있는가?
예를 들어, 다음과 같은 문제를 생각해보자.
문제 : 금액을 입력받아 붕어빵을 판매하는 게임을 만드시오.
이 문제를 풀기 위한 접근 방법을 조금 더 구체적으로 살펴보자.
우선, 구현해야 할 애플리케이션의 기능들을 체계적으로 나열해보자. 이렇게 하면 무엇을 해야 할지 명확히 알 수 있고, 각 기능이 서로 어떻게 연결되어 있는지 이해할 수 있다.
- [ ] 금액을 입력받는다. - [ ] 붕어빵의 수량을 계산한다. - [ ] 수량만큼 붕어빵을 생산한다. - [ ] 수량만큼의 붕어빵을 건네준다.
간단해보이지만, 이렇게 기능을 구체적으로 나누고, 각각의 세부 사항까지 고려하면서 작성하는 것은 프로그램의 전반적인 흐름을 이해하는 데 큰 도움이 된다. 또한, 이 과정을 통해 미처 생각하지 못했던 부분이나 잠재적인 문제점들을 미리 발견할 수 있다.
여기서 객체
란 특정한 역할을 담당하는 주체 또는 요소
를 말한다.
이 게임에서는 손님
은 붕어빵 장수
에게 돈을 지불하고, 붕어빵 장수
는 해당 금액에 알맞은 갯수의 붕어빵
을 손님
에게 제공한다.
이 과정 속에서 각 객체는 아래와 같은 역할을 수행한다.
손님
속성: 지불하고자 하는 금액
행동: 금액을 지불한다, 붕어빵을 받는다.
붕어빵 장수
속성: 붕어빵 가격
#행동: 금액을 받아 붕어빵의 수량을 계산한다. 붕어빵을 제공한다.
이를 기반으로 기능명세서를 분리해보자.
## 손님 - [ ] 구입 금액을 입력할 수 있다. - [ ] 붕어빵을 구매한다. ## 붕어빵 장수 - [ ] 손님에게 금액을 받아 그에 맞는 수량을 계산한다. - [ ] 수량에 맞는 붕어빵을 전달한다. ## 붕어빵 - 맛있다. 얌미
우리가 분석한 객체는 서로 상호작용하며 유기적으로 동작한다.
위의 분석에서 각 "객체"들의 상호작용을 생각해보자.
## 손님
- [ ] 구입 금액을 입력할 수 있다.
- [ ] 금액을 입력받아 붕어빵 장수에게 전달한다.
- [ ] 붕어빵 장수가 건네준 붕어빵을 받아서 먹는다.
## 붕어빵 장수
- [ ] 손님이 건네준 돈을 확인한다.
- [ ] 금액에 맞는 붕어빵 개수를 계산한다.
- [ ] 계산한 개수만큼 붕어빵을 만든다.
- [ ] 붕어빵이 든 봉투를 손님에게 건네준다.
## 붕어빵
- 맛있다.
어느정도 문제 분석과 기능명세서 작성이 완료되었다면 기능명세서 단위로 커밋을 진행한다.
너무 기능명세서를 완벽하게 쓰지 않아도 괜찮다. :)
코드를 작성하다보면 새로운 기능이 떠오를 것이고, 그때 천천히 기능명세서를 수정하는 것이 좋다!
살아있는 기능 명세서를 만들어보자.
git commit -m "docs: 기능 명세서 추가(구현 목록 추가)"
자 이제 기능 명세서 작성은 1차적으로 완료되었다.
이제 우리가 작성한 기능명세서를 기반으로 구현과 커밋을 진행해보자 :)
이제 우리가 작성한 기능명세서를 기반으로 코드를 구현 => 커밋 => 기능명세서 수정
을 진행해보자.
붕어빵 장수의 기능 중 하나를 구현해보자.
// JS :)
class 붕어빵_장수 {
private 붕어빵_가격 = 1_000;
붕어빵_수_계산(금액){
return parseInt(금액 / this.붕어빵_가격, 10);
}
}
## 손님
- [ ] 구입 금액을 입력할 수 있다.
- [ ] 금액을 입력받아 붕어빵 장수에게 전달한다.
- [ ] 붕어빵 장수가 건네준 붕어빵을 받아서 먹는다.
## 붕어빵 장수
- [ ] 손님이 건네준 돈을 확인한다.
- [x] 금액에 맞는 붕어빵 개수를 계산한다.
- [ ] 계산한 개수만큼 붕어빵을 만든다.
- [ ] 붕어빵이 든 봉투를 손님에게 건네준다.
## 붕어빵
- 맛있다.
필자는 기능명세서의 내용을 그대로 쓰는편이다.
> git add .
> git commit -m "feat: 금액에 맞는 붕어빵 개수를 계산한다"
코드를 작성하다보면 추가적인 구현 사항이 떠오르거나, 설계가 변경되기 마련이다.
기능명세서를 수정하는 것에 두려워하지 말자!
## 손님
- [ ] 구입 금액을 입력할 수 있다.
- [ ] 금액을 입력받아 붕어빵 장수에게 전달한다.
- [ ] 붕어빵 장수가 건네준 붕어빵을 받아서 먹는다.
## 붕어빵 장수
- [ ] 손님이 건네준 돈을 확인한다.
- [x] 금액에 맞는 붕어빵 개수를 계산한다.
- [ ] 전달받은 금액이 유효한 금액인지 확인한다. << 추가된 부분!
- [ ] 계산한 개수만큼 붕어빵을 만든다.
- [ ] 붕어빵이 든 봉투를 손님에게 건네준다.
## 붕어빵
- 맛있다.
커밋에 올리자!
git add .
git commit -m "docs: 기능 명세서 수정(구현목록 추가)"
역시나 커밋 메시지는 자유롭게! :)
대략적으로 붕어빵 장수를 추상화해보자.
추상화의 정도는 본인이 스스로 결정한다.
class 붕어빵_장수 {
private 오늘_번_돈 = 0;
private 붕어빵_가격 = 1_000;
private 붕어빵_생성기;
constructor(팥_붕어빵_생성기) {
this.붕어빵_생성기 = 팥_붕어빵_생성기;
}
private 붕어빵_수_계산(금액){
return parseInt(금액 / this.붕어빵_가격, 10);
}
private 팥_붕어빵_만들기(개수) {
return this.팥_붕어빵_생성기(개수);
}
// 외부에서 접근해야하기 때문에 private을 추가하지 않는다.
붕어빵_판매(금액) {
this.진짜_돈인지_확인(금액);
this.오늘_번_돈 += 금액;
const 개수 = this.붕어빵_수_계산(금액);
const 따끈따끈_붕어빵들 = this.팥_붕어빵_만들기(개수);
return 따끈따끈_붕어빵들;
}
// ..codes
}
어떨때는 오버엔지니어링이 될 수도, 언더엔지니어링이 될 수도 있다.
하지만 너무 슬퍼하지 말자. 성장의 과정일 뿐이다.
프리코스 크루들의 PR을 열심히 염탐하고, 함께 리뷰하고 토론을 하며 매주 발전하는 코드를 작성한다면 그게 가장 큰 성과이지 않을까 생각한다. 🥳
충분한 고민
이 들어가있고, 의사결정 근거가 들어있는 코드
에 대한 토론 및 리뷰가 필요하다면 언제든 프리코스의 [FE]펭구스를 링크해주시면 감사하겠다. 🥳
FE / BE / AOS 구분 없이, 동일한 미션에 대해 함께 고민한 것을 나누는 것이 가장 빠른 성장의 길이었다! :)
저도 어제 기능 명세 작성하느라 시간이 엄청 오래 걸렸는데 잘 작성했는지 긴가민가 했거든요.
그런데 펭구스님의 글과 겹치는 내용이 많아서 올바른 방향으로 고민했다는 생각이 드네요 ㅎㅎ
앞으로도 종종 찾도록 하겠습니다 🐧🐧
"1. 어떤 기능이 있는가?"에 있는 형태로만 작성했는데, 덕분에 더 발전시킬 수 있었어요!!!!☺️👍🏿👍🏿👍🏿
펭구스,,,귀한 자료 감사합니다!!🙏
단순 기능 명세한 다음에 구현하면서 객체에게 기능을 할당할 것인가, 아니면 객체가 어떤 역할을 할 지까지도 미리 명세해둘까 라는 고민이 있었는데 덕분에 갈피가 잡히네요. 펭구스 감사합니다 :D
ㅋㅋㅋㅋㅋㅋ 왠지 객체의 소리를 들을 때가 새록새록 떠오르네요.. 프리코스에서도 펭구스님을 만나게 되어 반갑네여 프리코스 화이팅입니다! 🙇♀️ 글 잘 보고 가요 ㅎㅎ
기능 명세에 좋은 가이드인것같네요!
붕어빵 얌미 ㅋㅋㅋㅋㅋ귀엽네요
입구에서 기다리겠습니다 펭구스님!🙇🏻