GeekNews 에서 프로그래밍 프로젝트 아이디어: 깃, 도커, 레디스를 직접 개발하며 배우는 법 라는 게시글을 보았다.
언어나 프레임워크의 기초를 뗀 후, 다음 단계의 도약을 고민하는 개발자들을 위한 수준 높은 프로젝트 가이드 라는 소개와
우리가 매일 쓰는 시스템의 '내부 원리'를 파헤치는 프로젝트들을 제안 이 매력적으로 다가왔다.
그렇게, codercrafters 를 알게됐다.
우리가 사용하던 요소들을 직접 구현할 수 있게 단계별로 제공해주는 프로젝트다.
백엔드 개발자라면, 흥미를 가질
등을 만들수 있다!
codecrafters 프로젝트 목록 화면
요금제가 존재하고 ( 할인 해줘도 조금 비싼듯... )
매달 1개씩의 Challenges 는 제공해준다고 한다.
codecrafters 요금제 안내 화면
C, C#, Go, Kotlin, Java, Javascript, Typescript 등등
다양한 언어로 미션을 수행할 수 있다.
codecrafters 지원 언어 목록
이번달은 Shell 구현이 무료로 제공되어서 해보았다.
추가로, Kotlin 을 학습해보고 싶어서 Kotlin 으로 진행했다.
진행 방법은
codecrafters 진행 방법 안내
git commit -am "[any message]"git push origin master그러면 웹사이트에서 테스트가 진행된다.
codecrafters 웹사이트 테스트 진행 화면
테스트를 통과하면 다음 Step 으로 넘어간다.
매번, commit - push 를 통해 테스트 하는게 번거롭다면
codecrafters cli 를 설치하고, codecrafters test 를 입력하면 된다.
그리고, 테스트를 통과하면 codecrafters submit 로 제출도 가능하다.
이 서비스는 만족스러웠다.
개발자로서 오랜만에 직접 코드를 작성하고, 새로운 도메인을 탐구하는 느낌을 받았다.
Shell 구현 정도는 어렵지 않겠지 라고 생각했는데 큰 오산이였다.
Step 을 구현하는 식이다보니, 다음 Step 을 구현하려고 할 때마다 코드 구조가 깨지게 되었다.
어디까지 리팩토링을 해야하는지 & 다음에 어떻게 확장이 되어야할지 를 계속 고민하게 만들었다.
기존, 구축되어 있는 프로젝트가 아닌
이를 위해, 객체지향과 클린코드 등을 생각해야만 했다. (오랜만에 우테코 미션을 하는 느낌)
추가로, LLM의 도움 없이 오랜만에 코드를 개선하다 보니, 뇌가 굳어버린 것 같아 스스로 현타가 좀 왔었다.
코드가 문제가 있는데,
등 회사에선 이슈를 위해 사용만 하던 요소들에 대한 개념들이 쏟아져 나왔다.
특히 우리팀은 이미지 프로세싱 때문에 node process 가 필요해서, ProcessBuilder 를 사용하고 있었다.
하지만, 이미 세팅이 되어있었기에 크게 관심을 안가지고 있었다.
ProcessBuilder 를 Shell 구현에 사용하게 되면서 사용법, 흐름, 내부 설계 등에 대해 자세히 학습하게 되었다.
Runtime.exec -> ProcessBuilder 가 나오게 되었는지등 구현 하며 나온 내용들에 대해 BFS 처럼 꼬리에 꼬리를 물며 탐구를 해나갔다.
특히, byte 배열과 stream 의 차이에 대해 한번 더 생각하게 되었다.
우리 코드는 byte 배열을 그대로 사용했지만 Stream 을 사용해 힙 메모리, 네이티브 메모리를 더 개선할 수 있겠다는 가능성을 보았다.
물론, 이는 대규모 코드 공사를 유발하긴 하겠지만...
ProcessBuilder와 Stream 관련 학습 내용 정리
Step 별로 진행을 하니, 이전 Step 까지 동작을 보장을 해줘야했다.
이번 Step 을 고치면서, 다른 Step 에서 한 내용을 건들수가 있었다.
EX) Shell Command 를 추가하며, Built-In Command 처리 방식이 달라진다든지
그래서, 통합 테스트 코드를 작성했다.
private fun execute(command: String, pathList: List<String> = emptyList()): String {
val input = ByteArrayInputStream(command.toByteArray())
val output = ByteArrayOutputStream()
val app = ShellApplication(input, output, pathList)
app.start()
return output.toString()
}/**
* exit 를 입력해야만, 종료가 되므로 맨 마지막에 무조건 종료가 되게 커맨드 구성
*/
private fun buildCommand(builder: StringBuilder.() -> Unit): String {
return StringBuilder().apply {
builder()
append("exit")
append(System.lineSeparator())
}.toString()
}
val command = buildCommand {
appendLine("type not-exist-command")
}@Test
fun `type {command} 를 찾지 못한다면, command not found 를 출력한다`() {
val command = buildCommand {
appendLine("type not-exist-command")
}
val result = execute(command, pathList = pathList)
assertTrue { result.contains("not-exist-command: not found") }
}그 후, 입력 - 출력을 통해 결과를 검증했다.
결론적으론, Step 마다 테스트를 작성해서
작성한 29개의 통합 테스트 목록
29개의 테스트를 작성했다.
그리고,
각 객체별 세부 로직 테스트 코드
각 객체들에 대해 테스트를 작성해서 세부 로직을 검증했다.
이를 통해
SpringBootTest 나 Mock 이 아니라 실제 동작하는 코드를 기반으로 테스트를 위의 효과를 얻었다.
회사에서 한동안 까먹었던 테스트의 궁극적인 장점을 다시 한번 느낄수 있었다.
터미널에서 제공되는 기능들을 어떻게 구현했는지 상상하고 직접 구현해보면서, 평소에 지나쳤던 개념들에 대해서도 더욱 깊은 학습할 수 있었다.
특히, 점진적으로 요구사항이 추가되는 환경에서
구조를 어떻게 유연히 가져갈지 고민하는게 실무에서도 도움이 되는거 같다.
내가 작성한 코드는 youngsu5582/shell-implement-challenge 에 있다.
모두 한번즈음 시도해봐도 좋을거 같다.
모든 Step 을 완료하진 않았다.
완료한 Shell 구현 단계들 (기본 Stages, Navigation, Quoting, Redirection, Pipeliens)
를 완료했다.
Autocompletion, History, History Persistence 는
Jline 이라는 라이브러리를 사용해야 하는 내용들이 있어서 하지않았다.
(화살표 기능 제공 및 Tab 입력시 자동 완선 기능 등은 일반적인 프로그래밍으로는 불가능)
Quoting 는 맨 마지막 명령어 처리 부분에서 계속 실패하고 있어서 Progress 상태..