대학교 다닐 때 '시크릿가든'이라는 드라마가 있었다. 현빈과 하지원 주연의 판타지 멜로드라마였는데 거기서 현빈이 자주 하는 대사가 있었다. (친절하게 유튭 링크)
"이게 최선입니까? 확실해요?"
김주원 (현빈)
비록 우리가 현빈은 아니지만, 성장하길 원한다면 같은 질문을 우리 스스로에게 해봐야 한다. 내가 짠 코드, 내가 설계한 구조, 내가 선택한 기술 스택,, 이게 최선입니까? 확실해요?
우리가 짠 코드, 잘 동작하니 더 이상 문제없는 걸까?
보통 IT 회사들이 개발자를 뽑기 위해 여러 과정을 거치는데 그중에 코딩 테스트도 있다. 그러다 보니 구직자 입장에서 취업 준비를 위해 코딩 테스트 사이트에서 사전에 알고리즘 문제를 풀곤 하는데, 일단 풀어서 테스트를 통과하기만 하면 거기서 더 개선해 볼 여지는 없는지 고민해 보지 않고 바로 다음 문제로 넘어가는 경우들이 꽤 있는 것 같다.
문제를 풀었으니 문제 될 것 없지 않냐고 생각할 수도 있겠지만, 개발자로 성장하려면 문제를 풀었더라도 더 나은 방식으로 풀 순 없었는지 고민해 보는 것이 좋다. 그런 태도가 결국엔, 똑같은 결과라도 보다 빠르게 혹은 보다 적은 메모리로 같은 결과를 만들어낼 수 있기 때문이다.
재밌는 예를 들어 설명하겠다. 내가 지은 소설이다.
취업을 준비 중인 김개발은 코딩 테스트 사이트에서 연습 삼아 문제를 풀다가 아래와 같은 문제를 만났다.
정수로 이뤄진 리스트와 타깃 정수가 주어졌을 때, 배열에 있는 정수 중 두 개의 합으로 타깃 숫자를 만들 수 있다면 true를, 만들 수 없다면 false를 리턴하는 함수를 작성하시오. 단, 같은 수를 두 번 사용할 순 없습니다.
예 1)
입력 : nums = [1, 4, 5, 6], target = 9
출력 : true
예 2)
입력 : nums = [2, 3, 5], target = 6
출력 : false
예 3)
입력 : nums = [2, 4, 5, 4], target = 8
출력 : true
(아래 내용을 이어서 보기 전에 10~15분 정도 각자 풀어보는 것도 추천한다. 참고로 이 문제는 https://leetcode.com/problems/two-sum/을 조금 튜닝한 문제이다)
'뭐야 이번 건 너무 쉽잖아. 요건 1분 컷이다' 김개발은 호기롭게 문제를 풀기 시작했다. 키보드 위를 신명 나게 넘나드는 두 손은 막힘없이 뻥 뚫린 아우토반을 질주하는 람보르기니 같았다. 2분 10초 만에 아래처럼 코드를 작성하곤 1분 컷을 하지 못한 것에 내심 아쉬워하며 제출하기를 클릭했다. 수십 가지의 테스트를 한방에! 모두! 통과했다는 결과가 나오자 온몸에 전율이 흘렀다. '캬~~~ 이 맛에 코딩하지~~ㅎㅎ 다음 손니임~~' 혼자 중얼거리며 두근거리는 마음으로 Next 버튼을 눌렀다.
boolean isTwoSum(List<Integer> nums, int targetNum) {
for (int i = 0 ; i < nums.size() ; i++) {
for (int j = 0 ; j < nums.size() ; j++) {
if (i == j) {
continue;
}
if (nums.get(i) + nums.get(j) == targetNum) {
return true;
}
}
}
return false;
}
문제를 더 풀려다가 밤도 깊었고 졸려서 바로 침대에 누워 잠이 들었는데 꿈에 현빈이 나와서 김개발에게 일갈하는 것 아닌가? '이게 최선입니까? 확실해요????!!! 예????!!!!'
화들짝 놀라 잠에서 깬 김개발의 등엔 식은땀이 주르르 흐르고 있었다. 핸드폰을 켜 보니 새벽 네 시였다. 핸드폰 바탕화면엔 현빈이 나를 보며 말하고 있었다. '이게 최선입니까? 확실해요????' 초심을 잃지 않으려고 해둔 배경화면이었는데 어느덧 잊고 있었다.
아까 풀었던 문제를 더 잘 풀어야만 잠이 잘 올 것 같았다. 자리에 앉아 제출했던 코드를 다시 곰곰이 살펴보기 시작했다. '더 바꿀 건 없을까? 흠... 어디 보자~~~ 엥?? 왜 j가 0부터 시작하지?? 같은 리스트를 i와 j가 도는 거니까 j는 i+1부터 시작해도 되잖아?!'
처음에 제출한 코드에 불필요한 반복이 있었다는 것을 알아챈 김개발은 뭔가 깨달았다는 듯이 급하게 코드를 고쳐나갔고 수정된 버전은 아래와 같았다.
boolean isTwoSum(List<Integer> nums, int targetNum) {
for (int i = 0 ; i < nums.size() ; i++) {
for (int j = i + 1 ; j < nums.size() ; j++) {
if (nums.get(i) + nums.get(j) == targetNum) {
return true;
}
}
}
return false;
}
제출하기를 눌렀더니, 와우! 테스트를 통과했다!! 왜 처음에는 이 부분이 보이지 않았던 것인지 아쉬워하면서 다시 편안한 마음으로 침대로 가려는 순간,, 약간 찝찝한 생각이 들었다. '어쨌든 수정한 버전도 시간 복잡도는 O(N^2)란 말이지.. 이게 좀 찝찝하단 말이지.. 이게 최선일까? .. 현빈 형~~ 이게 최선일까?? 응??'
김개발은 조금 더 성능을 개선하고 싶었다. 침대로 가지 않고 책상에 앉아 계속 끙끙거리며 고민하는데, 자다 깨서 그런지 머리 회전이 잘 되지가 않는 것이었다. 집요하게 계속 파다가 얼마 전에 봤던 쉬운코드의 [개발자로 성장하기 - 주의사항] 편에서 너무 막힐 때는 자고 다음날 하라는 글을 봐서 내일오늘 다시 집요하게 파기로 하고 일단 잠자리에 들었다.다음날오늘 일찍 일어나 다시 책상에 앉았다. 머리가 상쾌하다. 창문을 활짝 열었더니 상쾌한 아침 바람이 들어온다. 미세먼지가 없는 날은 역시나 좋다. 숨을 가다듬고 다시 찬찬히 생각을 시작했다. 'O(N^2)보다는 빠르게 하고 싶은데.. 그렇다면 현재의 이중 루프 구조로는 안된다는 말인데.. 이중 루프를 안 쓰고 풀 수 있나?? 한 번의 루프로 혹은 두세 번의 루프로 가능하나??'
손가락으로 책상을 따닥따닥 두드려가며 고민을 하는데 갑자기 김개발의 머릿속에 떠오르는 자료구조가 하나 있었으니 바로 HashMap이었다. HashMap을 활용하면 O(N)으로도 처리할 수 있겠다는 생각이 들자 심장박동 수가 증가하며 괜히 들뜨기 시작했다. '그래. 일단 HashMap에 nums에 있는 정수를 다 집어넣고, nums을 한 번 더 루프 돌면서 HashMap에 있는 키값과 비교해 보는 거야! 중복 이슈도 해결해야 하니까 HashMap의 key는 nums에 있는 각 정수면 되겠고 value는 해당 정수가 몇 번 등장하는지를 카운트하면 되겠다!'
번뜩이는 아이디어로 이것저것 시도한 끝에 김개발이 수정한 다음 버전은 아래와 같았다.
boolean isTwoSum(List<Integer> nums, int targetNum) {
Map<Integer, Integer> numToCount = new HashMap<>();
for (int num : nums) {
if (!numToCount.containsKey(num)) {
numToCount.put(num, 0);
}
numToCount.put(num, numToCount.get(num) + 1);
}
for (int num : nums) {
if (!numToCount.containsKey(targetNum - num)) {
continue;
}
if (targetNum - num == num && numToCount.get(num) < 2) {
continue;
}
return true;
}
return false;
}
코드 라인은 길어졌지만 이중 루프가 사라지고 두 개의 단일 루프만 있을 뿐이다. 그로 인해 O(N)으로의 성능 개선이 있었으니 만족스러운 결과였다. '그래, 이 정도면 현빈 형도 더 이상 뭐라 못할 거야' 이보다 더 좋을 순 없다고 생각하며 자리를 박차고 일어나서 세 걸음 정도 움직였다가 다시 자리에 앉았다. '에잇,, 딱 십 분만 더 생각해 보자.'
김개발의 머릿속에는 '루프 두 개를 하나로 합칠 순 없을까?' 이 질문이 계속 떠나지 않고 있었던 것이다. 쌩까고 무시하고 싶었지만 그럴 수 없었다. 이게 최선이 아닐 수 있으니까.. (현빈 횽 당신은 대체..)
'어디 보자~~ 루프 두 개를 합치려면 Map에 집어넣는 것과 key가 있는지 확인하는 것을 한 번의 루프로 처리해야 해. 중복 여부까지 고려하면서 말야.. 그게 가능해?' 코드를 다시 찬찬히 살펴봤다. 그리고 몇 분 뒤,, '아!!!' 짧은 탄식과 함께 무릎을 탁 치고는 드디어 김개발도 현빈도 만족스러울 최종 코드를 아래와 같이 완성시켰다.
boolean isTwoSum(List<Integer> nums, int targetNum) {
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
if (numSet.contains(targetNum - num)) {
return true;
}
numSet.add(num);
}
return false;
}
더할 나위 없이 깔끔하게 작성된 코드에 흡족해하며 김개발은 핸드폰을 꺼내 바탕화면을 쳐다보며 중얼거렸다.
'현빈 형~~ 이게 최선이야. 확실해.'
우리가 작성한 코드가 정말 최선인지 재차 점검하는 것은 것은 단지 코딩 테스트를 통과하기 위함만이 절대 아니다. 현업에서도, 똑같은 일을 하는 함수라도 어떻게 짜느냐에 따라 성능이 달라지게 되고, 그런 태도나 습관 하나하나가 모이다 보면 제품의 품질(성능, 리소스 사용량, 가독성 등등)에 생각보다 많은 영향을 끼치게 된다.
위의 이야기는 성능 개선에 초점을 맞춰서 썼지만, 네이밍(이름 짓기), 가독성, 함수나 클래스의 적절한 분리 등등 이게 최선인지 확인해야 할 여러 항목들이 있다. 가령 위의 예만 놓고 봐도, 메서드 이름은 isTwoSum이 적절한지 아니면 isTwoSumPossible 등등의 더 명확한 의미를 줄 적절한 이름이 있는지 고민해 봐야 한다. 마찬가지로 파라미터 이름도 targetNum이 최선인지 아니면 targetSum이 더 최선인지 생각해 봐야 한다.
이런 질문들을 습관처럼 꾸준히 하다 보면 개선하고픈 포인트들이 보이기 시작하고, 그렇다면 어떻게 개선하는 것이 좋을지 고민하게 되고, 그러다 보면 자연스럽게 리팩토링에 대한 관심이 생기기 마련이다.
리팩토링
개발을 하면서 이게 최선인지 질문하다 보면 코드 구조적으로 아쉬운 순간들이 반드시 생긴다. 과거의 내가 실력이 부족했을 수 있고 (성장했으니 과거의 나의 부족함이 보이는 것이다), 과거엔 맞았지만 지금은 트래픽의 증가나 제품의 방향성 변경으로 수정이 필요하게 될 수도 있다. 혹은 시간에 쫓겨서 급하게 짜다 보니 생긴 구조적 결함일 수도 있다.
이유야 어찌 됐든 중요한 것은 고치고 싶다는 생각이 들었다는 점이다. '이게 최선은 아니다'라는 인식이 리팩토링의 출발점이다. 다들 잘 아시는 것처럼, 리팩토링이라는 것이 결과는 동일하지만 내부를 개선한다는 의미이지 않는가?! 그렇다면, 어떻게 하면 좀 더 좋은 코드, 좀 더 좋은 구조로 나아갈 수 있을까? 리팩토링은 매우 큰 주제다. 게다가 리팩토링을 각 잡고 제대로 다루기엔 나도 더 많은 경험치가 필요하다. 그러므로 여기서는 나의 경험을 바탕으로 언제 리팩토링을 하는지, 리팩토링을 하지 않으면 무엇이 안 좋은지를 간략히 소개하겠다. 더 깊게 파기를 원하시는 분들은 마틴 파울러의 [리팩토링]이나 로버트 마틴의 [클린 코드]를 읽어보길 추천한다. 신입 개발자나 주니어 개발자에게는 어려울 수도 있지만 그럼에도 여러 번 정독하더라도 소화하려는 노력을 해보시면 좋겠다.
나는 아래와 같은 것들이 눈에 보이면 '이건 최선이 아니다'라는 생각이 들어 리팩토링을 한다.
- 더 좋은 성능으로 동작할 수 있을 것 같을 때
앞에서 김개발 이야기로 설명했던 부분이니 더 이상의 설명은 생략한다.
- 이해하기에 명확하지 않을 때
가독성이 떨어지거나 읽어도 의미가 모호하거나 심지어 의미를 왜곡시킬 때 리팩토링을 시작한다. 네이밍(이름 짓기)을 예로 들자면, 함수의 이름과 파라미터 이름만 봤을 때 어떤 일을 하는 함수인지 직관적으로 예상되지 않고 오히려 모호하다면 개선의 여지가 없는지 고민한다. 함수의 구현체를 봤을 때, 지역 변수 이름은 적절한지, 구현체가 너무 길지는 않은지, 너무 길다면 더 작은 단위의 함수들로 적절히 나눠줄 순 없는지, 잘 읽히는지, 잘 읽히지 않다면 어디가 원인인지 등을 점검한다. 적절한 빈 줄을 사용하여 가독성을 높여주는지도 확인하고, 코드 만으로 도저히 부족하다면 적절한 주석이 들어가 있는지도 점검한다. 그리고 개선하기 위해 리팩토링을 수행한다. 만약 리팩토링을 하지 않는다면, 모호한 코드를 제대로 이해하기 위해 동료 개발자들이 더 많은 시간을 써야 하는 불필요한 비용이 지속적으로 발생할 것이다.
- 하나의 모듈이 여러 가지 일을 할 때
함수는 한 번에 한 가지 일만 해야 한다는 말을 많이 들어봤을 것이다. 함수도 당연하고 뿐만 아니라 클래스도 마찬가지로 그래야만 한다. 예를 들어 봇을 통해 메시지를 보내는 로직을 BotMessageSender라는 이름의 클래스로 구현했다고 하자. 만약 BotMessageSender 안에 메시지를 포맷팅하는 로직까지 들어가 있다면, 하나의 클래스가 두 가지 일을 하는 것이다. BotMessageSender는 포맷팅된 메시지를 파라미터로 받아서 보내는 일'만' 해야 하고, 포맷팅을 하는 클래스는 MessageFormatter라는 이름으로 따로 구현하여 로직을 분리시켜야 한다.
만약 하나의 모듈이 여러 가지 일을 하도록 내버려 두면, 그 모듈은 덩치가 계속 커져서 가독성이 떨어지고, 잘 분리되어 있지 않기 때문에 여러 기능들이 집약되어 있어서 하나를 잘못 건드려도 여러 기능에 동시에 문제가 발생할 수 있다.
- 중복된 코드들이 있을 때
중복을 제거해야 한다는 것은 학부 때부터 강조되는 내용이다. 중복 코드가 존재하면 수정할 때마다 중복된 부분을 모두 챙겨줘야 한다. 한쪽만 고치고 다른 한쪽은 고치지 않는다면 예상대로 동작하지 않을 위험이 있기 때문이다. 이 얼마나 비효율적인 일인가? (물론 중복을 허용하는 것이 더 이득인 경우도 있다고 생각하지만 이 부분은 특정 케이스 한정이라 논외로 하자)
위의 예를 이어서 설명하면, MessageFormatter로 메시지 포맷팅 로직을 분리했기 때문에, 행여 추후에 동일한 메시지를 이메일로도 제공해야 하는 요구 사항이 들어온다 해도, EmailSender만 추가로 구현하면 된다. 만약 BotMessageSender 안에 메시지 포맷팅 로직이 포함되어 있다면, EmailSender를 구현할 때 동일한 포맷팅 로직을 복붙하여 넣어버리는 실수를 할지도 모른다. 그렇게 되면 훗날 포맷팅 로직을 수정해야 할 일이 생겼을 때, BotMessageSender와 EmailSender를 모두 챙겨줘야 해서 같은 일을 두 번 해야 할 뿐만 아니라, 팀 내부적으로도 구성원 모두가 '포맷팅 로직을 수정하려면 두 곳을 모두를 챙겨야 한다'라는 사실을 인지하고 있어야 하는 불필요한 비용이 발생하게 된다.
- 기능 확장이 어렵거나 손이 많이 가는 구조일 때
현재 구조에서 계속해서 기능이 추가되면서 확인해야 할 지점도 더 늘어나고, 코드 복잡도도 더 증가할 때 리팩토링을 하게 된다.
예를 들어 10분에 한 번씩 특정 주식 가격을 확인하여 일정 조건 이상의 변화가 감지되면 몇몇 메신저 그룹들에게 알림 메시지로 쏴주는 기능을 구현했다고 하자. 그런데 만약 그 조건을 조금씩 바꿔가며 테스트해 보고 싶어 한다면? 새로운 조건들이 계속 추가되고 있다면? 조건들을 조합한 조건을 만들 필요가 생겼다면? 알림을 받기 원하는 메신저 그룹들이 계속 추가된다면? 일부 메신저 그룹은 일부 조건에 대해서만 알림 메시지를 받고 싶어 한다면? 메시지를 다국어 지원해야 한다면? 메신저 그룹 별로 정해진 시간에 메시지를 보내야 한다면? 이렇듯, 처음에는 간단한 기능으로 시작했기 때문에 단순한 설계로도 충분했던 것이, 기능 추가가 지속되면서 더 고도화되고 유연한 구조로 리팩토링할 필요가 생기게 된다.
만약 리팩토링을 하지 않고 기능 추가만을 계속하게 되면 초기의 단순한 설계 위에 다양한 기능을 억지로 꾸겨 넣게 되는데 그러다 보면 if-else 대환장파티가 열리게 되고, 점점 그 누구도 건드리고 싶지 않은 기괴한 형태의 코드로 성장(?)하게 된다. 불필요한 개발 공수가 더 많이 필요해지는 것은 말할 것도 없고, 복잡 난해한 코드로 인해 예상치 못한 서비스 장애가 발생할 가능성도 커진다.
그러므로 이때는 다양한 요구 사항에 유연하게 대응할 수 있도록 확장이 용이한 형태로 코드 구조를 개선하는 것이 좋다. 그리고 한 곳으로 모을 수 있는 정보들은 가시성을 높이기 위해 모으는 것이 좋다. 위의 예를 빌리자면, 조건은 조건들끼리 메신저 그룹들은 메신저 그룹들끼리 관리될 수 있도록 DB든 프로퍼티든 한곳으로 모아서 최대한 한눈에 볼 수 있도록 하는 것이 좋다.
- 기술 스택의 변경이 필요할 때
초기에 잘못된 기술 스택을 선택해서 계속해서 불필요한 비용이 나가는 경우가 있다. 굳이 쓰지 않아도 될 기술인데 새로운 것을 쓰고 싶은 마음에 썼다거나, 기술의 장단점을 충분히 파악하지 않고 다른 유명한 곳에서 많이 쓴다니까 일단 썼다거나, 이전의 경험에 비추어 잘 썼던 기억이 있다 보니 현 상황에서도 적절한 기술인지 제대로 검토하지 않고 썼다거나 등등 여러 이유들로 맞지 않는 옷을 입은 것처럼 적절하지 않은 기술을 계속 쓰고 있다고 판단되면 기술 스택 변경을 포함하는 리팩토링을 수행한다.
또는, 초기에는 적절한 기술이었지만, 규모가 커지고 확장되면서 더 이상 맞지 않거나 불충분한 기술 스택이 됐다거나, 당면한 여러 이슈들을 해결해 줄 수 있는 신뢰할만한 기술을 알게 됐다면, 기술 스택 변경을 포함하는 리팩토링을 수행한다.
기술 스택 변경을 포함하는 리팩토링은, 개인적으로는 제일 까다롭고 오래 걸리는 리팩토링이라고 생각한다. 그럼에도 품질과 성능의 향상, 제품의 고도화에 기여를 할 수 있음이 자명하다면, 혹은 지금 리팩토링하지 않으면 훗날 우리의 발목을 끊임없이 잡고 늘어질 것 같다면, 용기를 내어 걸어가야 할 길이다.
...
보다 나은 품질의 제품을 만들기 위해, 혹은 보다 유지/보수/확장이 용이한 제품을 만들기 위해 최선을 찾아가는 과정은 우리를 프로 개발자로 성장시키는 여정이다. 지금 돌이켜 보면 나는 완벽주의 성향이 있다 보니 완벽한 제품을 만들고 싶다는 욕구 때문에 계속에서 좋은 코드와 구조가 무엇인지 고민했던 것 같고, 이것이 성장의 원동력 중 하나가 되지 않았나 싶다. 왜 그런 것 있잖은가? 내가 짜 놓고도 너무 만족스러운.. 그런 느낌 말이다.
초기 셋업, 충분히 검토한 것 맞죠?
리팩토링을 하다 보면 '애초에 처음부터 잘했으면 더 좋았을 텐데'라는 생각이 들곤 한다. 그렇다고 어떻게 처음부터 완벽하게 설계하고 완벽하게 구조를 짤 수 있겠는가? 시간이 지남에 따라 스펙이 변경되거나 기능이 추가/확장되는 일은 당연한 일이고 그에 맞춰서 기존의 구조와 설계를 리팩토링하는 것은 자연스러운 일이다. 오히려 처음부터 너무 완벽을 추구하는 것 또한 경계해야 한다.
여기서 강조하고 싶은 것은 초기에 DB 구조를 설계할 때나 기술 스택들을 선택할 때 이게 정말 최선인지 짚고 넘어가야 한다는 점이다. 예를 들어, 초기 DB 설계를 잘못해서 이후에 DB 구조를 변경하는 작업을 하게 됐다고 가정해 보자. DB 구조를 개선하기 위해서는 이미 저장된 데이터를 새로운 DB 구조로 옮기는 작업을 해야 한다. 게다가 변경된 DB 구조에 맞게 동작하도록 애플리케이션 코드도 수정해서 배포해야 한다. 그리고 이것은 결코 작은 작업이 아니다. 아주 초기 스타트업에서 이런 일이 발생했다면 언제부터 언제까지 서비스를 이용하지 못한다고 공지한 뒤에 서비스를 잠시 멈추고 작업을 할 수도 있겠지만, 대부분의 경우에는 서비스 무중단으로 위와 같은 작업을 수행하기 원할 것이다. (사용자에게 피해를 주고 싶지 않을 테니..) 무중단으로 위와 같은 작업을 수행하는 것은 매우 까다로운 일이다. DB 구조를 변경하고 데이터를 이전하고 변경된 DB 구조를 반영한 코드를 배포하는 이 모든 과정 동안 사용자는 전혀 타격 없이 정상적으로 서비스를 사용할 수 있어야 하기 때문이다.
신입 개발자나 주니어 개발자는 보다 나은 DB 구조를 설계하고 적절한 기술 스택을 선택하는 일이 어려울 수 있다. 그러니 반드시 실력 있는 시니어 개발자와 함께 하길 추천한다. 실력 있는 시니어 개발자가 어떤 논리로 DB를 설계하는지, SQL을 선택한다면 왜 SQL을 선택한 것인지, NoSQL을 사용한다면 어떤 이유에서 NoSQL을 사용하기로 결정한 것인지, SQL을 쓴다면 왜 이렇게 테이블을 분리하는 것인지, 테이블의 키 설정과 인덱스 설정은 어떻게 하는 것인지, 어떤 이유에 근거하여 이 기술 스택을 선택한 것인지 등등.. 함께 따라가면서 잘 모르겠으면 물어도 보고 다른 좋은 아이디어가 있으면 제안도 하면서 시니어 개발자와 함께 최선의 선택을 하기 위해 노력해야 한다. 시니어 개발자가 없는 상황이라면, 심지어 혼자 결정해야 한다면.... 건투를 빈다.... 인맥이든 인터넷맥이든 최대한 여러 방면으로 자문을 구하여 보다 나은 선택을 할 수 있도록 노력하는 것이 좋다.
시니어 개발자가 직접 DB 설계나 기술 스택을 고민하고 있다면, 충분히 검토하고 동료들의 (심지어 주니어로부터도) 리뷰도 받아 가면서 이 구조가 정말 최선인지, 이 기술 스택을 쓰는 게 우리 서비스 특성상 혹은 우리 팀의 상황 상 적절한지 충분히 숙고한 뒤 결정해야 한다.
개인적으로는 아무리 바빠도 이런 종류의 결정은 시간을 더 쓰더라도 제대로 고민하고 결정하는 것이 좋다고 본다. 안 그러면 이후에 계속 발목 잡히는 일이 발생할지도 모른다.
사용자 입장에서 생각해 봅시다
모든 서비스는 결국 사용자가 있기에 존재한다. 아무리 멋진 무언가를 만들었다고 하더라도 쓰는 사람이 거의 없으면 무슨 의미가 있겠는가? 그런 의미에서, 우리가 사용자에게 제공하는 서비스의 품질이 정말 최선인지 생각해 보아야 한다.
예를 들면 아래와 같은 질문들을 우리가 개발하는 서비스에 대입해 보자.
- 사용하기에 편한가요?
- 직관적인가요?
- 느리진 않나요?
- 매끄럽게 동작 하나요?
- 인터페이스가 일관성이 있나요?
- 친절한가요?
- 화면 가독성이 좋나요?
- 사용자가 원하는 기능에 도달하기까지 터치 횟수가 너무 많지는 않나요?
- 무겁진 않나요?
- 배터리를 많이 잡아먹진 않나요?
- 너무 강압적이진 않나요?
사용자의 입장에서 생각해 보고 이를 제품에 잘 녹이기 위해 고민하다 보면, 제품에 대한 철학이 생기게 되고 좋은 제품을 만들기 위한 욕심이 생기게 된다. 그러면서 성장하는 것 같다. 기술적으로든 기술 외적으로든.
그러니 제품을 만들었다는 것에 만족하고 끝내면 안 된다. 우리는 '좋은' 제품을 만들기 위해 고민해야 한다. 사용자가 우리 제품에 감동하고 만족해서 계속 더 쓰고 싶을 만큼, 우리는 우리 제품의 품질에 욕심을 내야 하고 최선을 다해야 한다. 사용자가 사용해야 제품이 존재하고 제품이 존재해야 우리도 개발할 수 있음을 기억하자. 그리고 솔직히 말해서, 우리가 만든 제품을 사용자가 만족해하면 우리도 기분이 좋잖아요~~ :)
현실은 녹록지 않음을
최선에 도달하기 위해, 혹은 그 근처 가까이라도 가기 위해서는 어느 정도 시간 확보가 필요하다. 하지만 현실에서는 그만큼의 시간을 주지 않을 때가 더 많다. 일정은 늘 빡빡한 것 같고, 개발해야 할 새로운 기능은 언제나 우리를 기다리고 있다. 애자일을 하는 회사에서는 팀별로 시간 산정할 때 융통성 있게 알음알음 이런 시간을 확보하기도 하지만, 전사적으로 이 부분에 대한 중요성을 인식하고 공감하는 회사가 생각보다는 잘 없는 것 같다.
이게 최선인지 고민하는 문화가 잘 정착하기 위해서는 회사의 상위 리더십 레벨에서부터 이 부분을 공감해 주는 것이 필요하다고 본다. 즉, 최선의 것을 개발팀이 잘 만들어 줄 것이라고 기대하면서도, 정작 최선의 것을 찾을 수 있는 시간은 포함하지 않는 개발 일정을 리더십이 세우고 있다면 그건 좋이 않다는 생각이다. 특히 리팩토링의 중요성을 전사적으로 공감하는 것이 중요하다. 결과는 그대로인데 내부를 개선하는 것이 리팩토링이기 때문에 비 개발자 입장에서는 리팩토링을 하고 있는 개발자들을 이해하기 어려울 수 있다. (종종 리팩토링을 저평가하는 개발자도 보긴 했다..) 리팩토링을 이렇게 이해하면 어떨까? 서울에서 부산까지 가는 열차를 기존에는 무궁화호를 타고 갔다면 리팩토링 후에는 SRT를 타고 가는 것이다. 지금 당장은 SRT로 전환하는 공사를 하기보다 다른 일들을 하는 것이 더 급해 보일 수 있겠지만, 어느 것이 장기적으로 봤을 때 더 좋은지는 자명하다. 매일매일 서울과 부산을 왕복해야 한다면 더더욱!!
위의 이유가 아니더라도 시간 확보가 어려운 경우는 또 있다. 여러 상황을 놓고 봤을 때신속하게 개발해서 먼저 시장을 선점하는 것이 중요하다고 회사가 판단한 경우에는 이게 최선인지 고민할 시간 없이 빠르게 제품을 내놓는 것이 가장 중요해진다. 이때는 개발팀도 전략적으로 속도를 최우선의 가치로 두고 개발하기 때문에 최선의 것을 고민하기보다는 빠르게 개발하는 것이 더 바람직한 상황이다. (물론 이후에 반드시 리팩토링할 시간을 가져야 한다. 그 시간을 간과하고 계속 빠르게만 개발한다면... RIP..)
그렇다. 여러 이유로 현실에서 최선의 것을 추구하는 것이 쉽지만은 않다. 상황에 따라서는 차선으로 만족해야 할 수도 있다. 이상과 현실 사이에서 줄을 잘 타는 것 또한 성장의 한 종류가 아닐까 싶다.
그러니, 이게 최선인지 점검하려는 마음의 자세를 늘 잃지 말되, 현실의 상황을 고려한 유연함도 장착하자. (그렇다고 현실을 핑계 삼아 안일해지진 말자.)
이번 챕터는 쓰다 보니 할 말도 많고 다루어야 할 내용도 많아서 시간이 꽤 오래 걸렸다. 글을 쓴다는 것도 개발과 마찬가지로 구조를 잘 잡고 쉽게 읽히도록 매끄럽게 다듬는 과정이 필요한데, 그렇게 하려면 여러 번 다시 읽어보고 썼다 지웠다 옮겼다 수정했다를 반복해야 하고, 그러다 보니 중간부터는 점점 귀찮고 하기 싫고 대충 하고 싶어지는 것이었다.
그때 현빈 형이 말했다.
"이게 최선입니까? 확실해요?"