일반적으로 클론 코딩은 개발자, 개발자 지망생이 실력을 키우기 위해 강의 등을 통해 코드를 그대로 옮겨 적으면서 실행해보는 것을 말한다. 글을 쓰는 작가들은 다른 사람이 쓴 글을 베껴 옮겨 적는 필사(筆寫)라는 훈련을 하기도 하는데, 클론 코딩도 이와 비슷하지만 사실 이것은 좁은 의미의 클론 코딩이다. 조금 더 나아가면 한 단계 높은 수준의 클론 코딩을 할 수도 있는데 그것은 아래에서 이야기해보도록 하고, 지금부터는 많은 사람들이 클론 코딩을 할 때 겪게 되는, 또는 잘못된 방법으로 클론 코딩을 했을 때 문제가 되는 사항들에 대해 이야기해보고자 한다.
지금 타자연습하는 건가요?
클론 코딩은 기본적으로는 다른 사람의 코드를 옮겨 적으면서 실행해보는 행위이지만, 정말 그것만 하는 사람이 있다. 코드를 적으면서 해야 하는 일은 강사자 또는 코드를 작성한 원작자가 왜 이렇게 코드를 썼는지, 그러한 의도는 무엇인지를 파악하는 것이다. 우리가 국어시간에 작가의 의도를 파악하는 공부를 했던 것처럼, 코드를 볼 때도 원작자가 이러한 코드를 작성하는 의도에 대한 이해는 반드시 필요하다. 클론 코딩을 하면서 아래와 같은 질문은 바람직하지 않다.
똑같이 적었는데 에러 나요. 어떻게 해결하는 거죠?
정말 똑같이 적었다는 것 이외에는 한 것이 없음을 증명하는 질문이다. 만약에 원코드와 똑같이 적었는데 에러가 난다면, 에러 메시지를 자세히 읽어보아야 한다. 대부분은 에러 메시지에 정답이 담겨있는 경우가 많다. 강의를 보면서 한다면 강사자와 자신의 개발환경을 비교해보는 것도 좋다. 변수의 이름, 파일의 이름, 또는 실행하는 방법, 환경이 다르거나 하는 이유가 대부분이다. 이러한 고민 없이 무작정 에러가 난다고만 하는 것은 클론 코딩을 하나마나한 것이다. 우리는 타자연습을 하기 위해 하는 것이 아니다.
Top-Down 과 Bottom-Up
클론 코딩을 진행하는 방법은 주로 Top-Down 방식과 Bottom-Up 방식이 있다. 탑다운 방식은 함수나 메서드의 추상화를 먼저 진행하면서 구현부는 가장 나중으로 진행하는 것이다. 탑다운 방식의 경우 나무보다 숲을 먼저 바라보게 되면서 애플리케이션의 전체 구조를 보게 되고 기능을 우선적으로 파악하면서 설계를 먼저 진행한다.
반면 바텀업 방식은 구현부를 먼저 보면서 추상화를 진행하게 된다. 이 방식의 경우 애플리케이션의 구조와 설계적인 측면보다는 내부 구현 사항부터 하기 때문에 눈으로 빠르게 결과를 보면서 익히고 싶다면 좋을 수도 있다. 그러나 이러한 경우 코드에 대한 이해가 부족한 상태로 접근하는 경우가 많아서 개인적으로는 장점보다 단점이 더 많은 방식이라고 보고 있다.
작동은 하는데, 왜 동작하는지 모르겠다거나 원작자가 왜 코드를 이렇게 썼는지 이해를 못하고 넘어가면 클론 코딩을 제대로 했다고 보기는 어렵다. 따라서 다음과 같은 일이 발생하면 안 된다.
클론 코딩을 끝냈는데, 여전히 혼자 짜라고 하면 못하겠어요!
탑다운 방식으로 구성하는 게시판 예제 살펴보기
개인차에 따르지만, 이 글에서 나는 클론 코딩을 할 때, 바텀업 방식보다 탑다운 방식을 권장하고 싶다. 간단하게 게시판의 예를 들어보기로 하자. Go 언어로 간단하게나마 예를 들어보자. 따라 하고 싶지 않다면 안 해도 상관없다.
탑다운 방식에서 게시판을 이해하기 위해서는 먼저 어떤 기능이 필요한지를 파악해야 한다. 또한 게시판은 여러 가지 종류가 있으므로 종류에 따라 어떤 기능이 필요할지 대략적으로 정리를 해야 한다. 이 포스트에서는 게시판의 기본적인 기능인 CRUD(Create, Read, Update, Delete) 과정을 아주 단순하게 구현한다.
이제 게시판의 구성 요소가 무엇이 있는지 고민하자. 게시판을 쓰는 행위를 하는 것은 일반적으로 사람이며 이는 Blogger
라고 표현할 수 있고, 그러한 사람이 작성한 것은 게시글, 즉 Post
다. Blogger
는 Create
라는 행위를 통해 Post
를 Blog
에 쓸 수 있다. 또한 여기서 일반적인 블로그가 그렇듯 Blog
는 다수의 Post
를 가진다는 사실도 우리는 이미 잘 알고 있으며. Blogger
는 티스토리로 가정하여 처럼 일반적으로 여러 개의 블로그를 소유할 수 있다고 여기도록 하자.
어떻게 사용하고 싶은가?
먼저 우리는 내부 구현을 짜기 전에 어떻게 호출해서, 어떤 결과를 내고 싶은지부터 생각해보자. 우리는 다음과 같이 하는 것을 목적으로 한다. 여담으로 일부 함수 및 메서드의 첫 번째 파라매터는 id
인데, id
는 예제라서 저렇게 준 것이지 사실 일반적으로는 Auto Increment
로 자동적으로 증가해서 추가하는 것이 보통이다. 따라서 프로덕트에서 id
는 데이터베이스에서 PK
로 사용하게 될 것이기 때문에 같은 id
로 생성하는 일은 없을 것이다.
package main
import (
"blog"
"fmt"
)
func main() {
// 'u' 블로거 생성
u := blog.NewBlogger(0, "pronist@naver.com", "1234")
// 'u' 블로거를 소유권자로하여 블로그 'b' 를 생성
b := blog.New(0, "https://pronist.tistory.com", u)
// 2개의 포스트 생성
posts := []*Post{
blog.NewPost(0, "What is Lorem Ipsum?", "Lorem Ipsum is simply dummy text of ..."),
blog.NewPost(1, "Why do we use it?", "It is a long established fact that ..."),
}
for _, post := range posts {
// 'u' 블로거가 'Post' 를 'b' 블로그에 작성
u.Create(b, post)
}
// 'u' 블로거가 'b' 블로그에 있는 게시글 읽기
fmt.Println(u.Read(b, 0), u.Read(b, 1))
}
코드를 설계하고 타입을 구성하여 위와 같은 코드가 올바르게 동작하도록 해볼 것이다. 블로거 u
와 블로그 b
를 생성하고 블로그의 소유권자를 u
로 설정한다. 그런다음 b
블로그에 id: [0, 1]
에 해당하는 포스트를 작성하고 마지막으로 b
블로그에 있는 포스트 id: [0, 1]
에 해당하는 글을 읽어오는 작업을 표현한 것이다.
type Post
Post
타입은 단 하나의 포스트를 표현하기 위한 타입이다. 글 제목과 본문을 가진다.
type Post struct {
id int
title string
content string
}
func NewPost(id int, title string, content string) *Post {
return &Post{id, title, content}
}
type Blog
한 개의 Blog
는 한 명의 Blogger
에게 소유되며, 다수의 Post
를 가질 수 있다고 했으므로 다음과 같이 표현할 수 있다. New()
메서드에서 블로그의 소유자를 설정하고 소유자의 블로그 목록에 해당 블로그를 추가하는 것을 주의 깊게 살펴보자.
type Blog struct {
id int
url string
owner *Blogger
posts map[int]*Post
}
func New(id int, url string, owner *Blogger) *Blog {
blog := Blog{id, url, owner, make(map[int]*Post)}
owner.blogs[blog.id] = &blog
return &blog
}
type Blogger
Blogger
는 다수의 블로그를 가질 수 있으므로 그것과 관련된 필드를 가질 것이다.
type Blogger struct {
id int
email string
password string
blogs map[int]*Blog
}
CRUD(Create, Read, Update, Delete)
CURD
는 Blogger
가 하는 행위이므로 메서드에 추가해보도록 하자. 여기서 한 가지 더 생각해볼 수 있는 것은, 소유권 증명이다. 읽기를 제외한 다른 행위(쓰기, 수정하기, 삭제하기)는 블로그를 소유한 유저만 할 수 있어야 한다. 이를 위해서는 블로그의 소유권을 증명해야 하는데, Blogger
의 blogs
, Blog
의 owner
필드를 사용하면 가능하다.
func (u *Blogger) Create(blog *Blog, post *Post) {
if _, ok := u.blogs[blog.owner.id]; ok {
blog.posts[post.id] = post
}
}
func (u *Blogger) Read(blog *Blog, id int) *Post {
return blog.posts[id]
}
func (u *Blogger) Update(blog *Blog, id int, title string, content string) {
if _, ok := u.blogs[blog.owner.id]; ok {
post := blog.posts[id]
post.title, post.content = title, content
}
}
func (u *Blogger) Delete(blog *Blog, id int) {
if _, ok := u.blogs[blog.owner.id]; ok {
delete(blog.posts, id)
}
}
지금까지의 과정 되돌아보기
어떤가? 다시 돌아보면 우리는 다음과 같은 과정을 거쳤다. 먼저 게시판을 만들기 위해 애플리케이션에 필요한 기능인 CRUD
가 필요하다는 것을 파악했으며 main
함수에서 사용법을 이미 대략적으로 표현해두었다. 두 번째로 그러한 게시판의 구성 요소들을 객체지향 관점으로 다가가 설계, 직접 코드로 타입을 선언하고 메서드를 정의했다. 이 과정에서 필요하다면 데이터베이스 모델링도 해야 될 것이다. 그리고 마지막으로는 메서드 내부를 파고들어 구현이 되도록 했다.
이것이 내가 권장하는 탑다운 방식의 클론 코딩이다. 이는 원하는 결과를 먼저 작성하는 테스트케이스를 먼저 작성하고 구현부를 구성해보는 유닛 테스팅과도 연관지어볼 수 있으므로 연습을 해두면 도움이 될 것이다.
조금 더 멀리
위의 게시판 예제의 '코드' 만을 따라한 것이 클론 코딩의 첫 번째 단계다. 사실 지금 한 것보다 이 앞 단계에 있는 한 가지 과정을 거쳐야 실력 향상에 큰 도움이 되는데, 그건 클론 코딩할 대상을 스스로 정하고 대상의 기능이나 인터페이스를 파악하는 것이다. 이는 코드를 따라한 것이 아니라 '기능' 을 따라한다. 일반적으로 강의에서는 클론 코딩할 대상을 강사자가 정해주고 애플리케이션 분석도 어느 정도 마친 상태에서 하기 때문에 학생들은 이러한 과정을 거칠기회가 없다.
그러나 학생 스스로 대상을 지정하고 그 애플리케이션이 어떤 기능/구조를 가지고 있는지부터 차근차근 살펴본다면 더욱 확실한 클론 코딩을 할 수 있고, 그에 더해 아무런 도움 없이 스스로의 힘으로 대상 애플리케이션과 유사하게 만들 수 있게 된다면 효과를 톡톡히 볼 수 있을 것이다.
이 글로 인해 당신의 클론 코딩이 더 나아지길 기대해본다. 참고도 클론 코딩은 개발 공부법 중 하나에 불과하므로 안 해도 상관은 없지만 나름 효과적인 공부법이다.