본문 바로가기

일상, 생각, 경험/그냥 얘기

입사 후, 첫 번째 프로젝트 후기

시간 빠르다... 벌써 입사한 지 3개월이 지났다.
그간 작업했던 프로젝트가 드디어 배포되기 시작했다.
그리고 3개월 수습 발표도 이번 주에 성공리에 잘 마쳤다.

쏘카에서 첫 프로젝트를 진행하며 느끼고 생각했던 것들을 하나씩 정리해 남겨둔다.


0. 첫 번째로 참여하게 된 프로젝트

내가 속한 데이터 엔지니어링팀은 이런저런 다양한 일들을 한다.
나는 팀장님과 함께 신규 가격 시스템 관련 파이썬 개발을 맡게 되었다.
내가 한 업무를 한 마디로 요약하면... 쏘카 차량의 대여 가격을 계산하는 모듈을 만드는 일이었다.
기존 레거시 가격 시스템과 결과는 비슷하되, 노후화된 레거시 로직 전체를 새로 구성하는 것이었다.
프로덕션으로 배포되는 코드를 새롭게 짜 본다니... 너무 설레잖아??

서비스 회사에서 프로덕션 코드라니 ... 설.레.잖.아.

결과적으로 약 세 달 좀 안되게 개발과 테스트를 진행했다.
여기에는 기존 레거시와 결과를 비교하는 검증 및 재현하는 작업도 포함되어 있다.
이 작업이 꽤나 중요해서, 신규 시스템의 결과가 레거시 시스템의 결과와 맞지 않으면, 기존 코드를 수정하거나 DB 스키마를 바꾸는 일도 종종 있었다. 막판에 이런 것들 때문에 시간이 꽤나 들어갔던 듯하다.

이제 내가 코딩을 하며 느꼈던 것들을 하나씩 적어보려 한다.


1. 스스로 설명되는 쉬운 코드가 좋은 코드다.

사실 개발 자체는 그렇게 어렵진 않았다. 아니 쉬웠다. 모듈의 입출력은 명확했고, 난 그저 "잘 짜기만" 하면 됐다. 그래서 디자인 패턴도 공부하고, 클린 코드, 리팩토링 책도 보며 같은 결과물을 내더라도 퀄리티 좋은 코드를 짜고 싶었다.

처음에는 유연하게 짜려고 시도했다. 어떠한 수정사항이 와도 내 코드의 수정을 최소화시키리...
요구사항에서의 본질을 보려고 했고, 잘 변하지 않는 것이 무얼까 생각했다. 그런 것들 위주로 "일반적인 개념" 들을 생각하며 클래스를 추상화하고 메쏘드를 구성해나갔다.
클래스는 잘게 쪼개어 나가 졌고, 메쏘드와 모듈들의 개수는 늘어져 갔다. 난 유연한 코드를 원했다.
나름대로, 또 잘 배운 대로 꽤나 괜찮은 코드를 짠 듯했다.

그런데 팀장님은 내 코드를 보시고 자신이 이해한 게 맞냐고 질문을 하셨다.
내 코드를 처음 보는 사람은 이게 무엇을 하는 건지, 한눈에 들어오지 않았던 것이다.
그리고 리팩토링 과정에서 내 코드를 일부 수정하셨고, 나는 수정된 코드를 다시 보았다.
좀 놀라웠다. "패턴" 에 맞춰 좀 더 "일반적인 개념" 으로 쓰였던 내 코드들은 좀 더 "도메인" 에 맞게 재구성되었으며, 클래스나 함수의 개수도 현저히 줄었다. 덜 유연할 순 있어도, 누가 봐도 무엇을 하는지 코드가 좀 더 명확해졌다.

코드를 잘 짜는 건 어렵다.
코드가 잘개 쪼개어지고, 변하는 것들(미래에 뭔가 수정사항이나 확장이 될만한)을 생각하고 짜면, 유연성을 얻으나 전반적인 아키텍처의 복잡도는 증가한다. 하나를 이해하려면 전반적인 아키텍처를 다 봐야 하고, 잘게 쪼개어지고 모듈이 늘어날수록 봐야 할게 많아진다.
반대로, 변하는 것들을 고려하지 않고 생각나는 대로 짜면, 코드의 결합도가 높아진다. 하나의 함수나 클래스가 비대해지며, 이후 추가 수정사항이 있을 때 여기저기 손 볼게 많아진다.

코드를 잘 짠다는 건, 이 중간의 밸런스를 맞추는 게 아닐까 싶다. 책에 나온 대로 클린 하게 정답이 정해진 케이스는 잘 없는 거 같다. 한마디로 말하면, "중용" 이 필요하다. 적절하게 유연하며 적절하게 비대해야 한다. 미래 수정사항을 고려하여 너무 고도로 추상화하지 않아도 되고, 당장 필요한 것만 적절하게 그리고 명확하게 설계되어야 한다. 그래야 설명하지 않아도 되는, 스스로 설명되는 좋은 코드가 되는 거 같다.

잘 모르겠으면, 코드 리뷰를 받자. 다른 사람들에게 물어보자. 내 코드가 지금 이해하기 좋고 읽기 좋으냐고.
변수, 함수, 클래스, 모듈 이름, 프로젝트 구조부터 문서화(docstring) 까지. 결국 이 질문 하나로 명확해진다.


2. 이름은 꽤나 본질적인걸 담는다.

개발하며 제일 많이 고민한 게 무엇이냐 묻는다면, 이름 짓는 거라고 말할 거다. 커뮤니티에 돌아다니는 짤 보면 나뿐만이 아니고 다들 이름 짓는 게 제일 어렵다고 한다.

이름 짓는 거, 어렵다.
클린 코드 책에는 다음과 같은 문구가 등장한다.

하나의 변수, 함수, 클래스는 하나의 일만 잘해야 한다.

이름에는 이 "하나의 일" 이 무엇인지 담긴다.
근데 이게 명확치 않을 때가 있다. 뭐라고 지어야 이 "일" 을 잘 설명할 수 있을까? 단지 "하나의 일" 일 뿐인데.
굳이 부가 설명이 필요 없는, 한 큐에 설명되는 네이밍을 하고 싶었다.

일상생활에서 나는 누군가가 어떤 것을 부랴부랴 설명할 때, 마지막에 이렇게 물어보곤 한다.

그래서 너가 말하려는 그거. 한 마디로 표현해줘. 딱 한 마디로.

무언갈 잘 이해했다면, 나는 저 한 마디로 본질을 표현할 수 있다고 생각한다.
네이밍도 마찬가지다. 내가 이 로직과 해야 할 일을 잘 알고 있다면 네이밍에 그걸 담아낼 수 있다고 생각한다. 그리고 그렇게 해야 한다.
이미 내가 변수 이름 짓기 버벅거려한다면, 그건 이미 그 변수가 한 가지 일 이상을 하고 있거나 애초에 설계가 어딘가 잘못되었다는 신호였다.

항상 한 가지 일로 떨어지지 않는 경우도 있고, 로직 자체가 복잡해서 한 마디로 표현하기 어려운 경우도 있다.
또 새로운 개념을 추상화하다 보니, 어떤 단어가 직관적 일지 고민하기도 한다.
이래저래 가장 어려운 것 중 하나가 네이밍이고, 이 역시 여러 경험과 실력 향상을 통해 길러야 할 능력치인 거 같다.


3. 이름에 일관적인 패턴을 정하고 들어가면 좋다.

나는 변수 이름 하나를 지을 때도 price_list 로 할지 prices 로 할지 고민했다.
둘 다 복수개의 값을 담고 있는 뉘앙스를 주는데, list 로 할 경우 데이터 타입이 명확해진다.
근데 그렇다고 모든 변수 이름 끝에 _dict, _tuples 이렇게 달아줄 것이냐... 이것도 뭔가 별로다.
그래서 prices 로 정했다. 해석의 여지가 조금 있으나, 이게 가장 "적절해" 보였다.

이렇듯, 처음부터 아예 네이밍 패턴을 정하고 가면 좋다.
파이썬은 네이밍 규칙이 좀 프리 한 거 같아서, 누구에게 물어봐도 명확한 정답은 없었다.
그래서 프로젝트하면서 내가 나름 정한 것들 혹은 참고한 것들을 생각나는 대로 적어본다.

1) 복수 개의 데이터를 담는 변수는 s 로 통일한다.

price = 1300  # 데이터가 하나인 경우 단수로 표현
prices = [1300, 1500, 1700]  # 여러 개인 경우 복수로 표현

def get_price_by_id(id: int) -> int:
    """하나를 입력 받아 하나를 반환"""
    pass
def get_price_by_ids(ids: list) -> int:
    """여러 개(리스트)를 입력 받아 하나를 반환"""
    pass
def get_prices_by_id(id: int) -> list:
    """하나를 입력 받아 여러 개를 반환"""
    pass
def get_prices_by_ids(ids: list) -> list:
    """여러 개를 입력 받아 여러 개를 반환"""
    pass

미세한 차이지만, 네이밍만으로도 입력과 출력에 대한 힌트를 받을 수 있다.

2) 해시는 key_to_value 로 이름 짓기

car_id_to_class_name = {
    1: "레이",
    2: "아반떼"
}

마찬가지로 변수 이름만 봐도 어떤 구조인지 명확하다.

3) 반복문에서 긴 이름의 변수는 앞자리 하나로 축약하기

# 지양하는 방식
car_policies = [policy_with_period for policy_with_period in policies_with_period if policy_with_period.car_model_name in (car_info.model, "전체")]

# 지향하는 방식
car_polices = [p for p in policies_with_period if p.car_model_name in (car_info.model, "전체")]

코드가 좀 더 간결해져 훨씬 읽기 쉬워진다.

4) 클래스 내부에서만 사용되는 속성, 메서드는 _ 을 붙이자.

class BasePolicyLoader:
    def __init__(self, repository) -> None:
        self._repository = repository  # 내부에서만 사용하는 속성

    def load_policies(self) -> None:  # 외부에서 사용하는 메서드
        self._get_baseline_policy()

    def _get_baseline_policy(self) -> None:  # 내부에서만 사용하는 메서드
        pass

나중에 코드를 처음 보는 사람이 무엇에 집중해서 봐야 하는지 좀 더 명확해진다.
되게 별거 아닌 거 같은데, 나도 종종 까먹곤 한다. 습관화되어야 할 부분...

5) get 은 어디선가 가져올 때, find 는 주어진 파라미터에서 찾을 때.

class BasePolicyLoader:
    def _get_baseline_policy(self, baseline_id: int) -> BaselinePolicy:
        # DB 에서 원하는 데이터를 가져온다.
        return self._repository.get_baseline_policy(baseline_id)

    def _find_baseline_policy(baseline_id: int, baseline_policies: list) -> BaselinePolicy:
        # baseline_policies 에서 원하는 데이터를 찾아 반환한다.
        for baseline_policy in baseline_policies:
            if baseline_policy.id == baseline_id:
                return baseline_policy
        return None

get 은 코드에서 일반적으로 잘 쓰이는 동사인데, 가끔 어디까지 get 인가에 대해 명확하지 않을 때가 있다.
자바에선 일반적으로 게터, 세터 할 때 get 을 쓰는데 파이썬은 게터, 세터를 안 쓰니.. 뭔가 규칙이 필요하다.
그래서 나는 이 클래스의 외부에서 뭔가 가져오는 복잡한 작업을 할 때 get 을 사용하고,
단순하게 주어진 데이터에서 데이터를 가져오는 작업은 find 라는 네이밍을 사용했다.

한편, 이런 류의 네이밍을 결정해야 할 때 "관용적으로" 많이 쓰는 단어가 뭔지도 생각해봐야 한다.
예를 들면 create_ , make_ 이 둘 중 뭐가 더 많이 쓰일까?
애매하다 싶으면 바로 구글링 들어가면 된다.. 물론 그래도 안 나오는 경우도 많지만.

4. DB 커넥션 비용은 생각보다 많이 비싸다.

DB에 연결하여 뭔가를 하는 작업은 비용이 크다는 말은 이전에도 듣긴 했다. 여기서 비용은 프로그램의 처리 속도 등의 성능을 말한다.
DB 연결 과정, 네트워크 트래픽 (클라우드 서비스의 경우), 쿼리 처리 등등의 오버헤드가 붙게 되고, 프로그램 로직이 DB로부터 데이터를 받을 때까지 기다리게 된다는 것이다. 그리고 이는 서비스 병목현상으로 이루어질 수 있다.

처음에는 별생각 없이 코드를 짰다.
예를 들면 처음 내 로직은 이랬다.

class CarPolicyLoader:
    def _get_car_id_to_policy(self):
        """ DB 로 부터 car_ids 에 해당하는 정책을 가져옵니다. """

        car_id_to_policy = {}
        for car_id in self.car_ids:
            policy = self._repository.get_car_policy(car_id)  # DB에 접근하는 부분 
            car_id_to_policy[car_id] = policy

        return car_id_to_policy

위 코드는 car_ids 를 모두 순회하며 DB에서 데이터를 하나씩 가져온다.
car_ids 의 데이터 수만큼 DB에 접근하고, 접근할 때마다 SELECT 절을 사용하게 된다.
이 이후에 진행되는 로직을 고려할 때, 일관되고 아주 깔끔한 로직이었다.

그런데 이 코드는 놀라울 치마나 느렸다. 실제로 테스트 배포했을 때 정말 말도 안 되게 느렸고, DB 인스턴스에 엄청나게 부하가 걸렸다.
그리고 병목현상이 금방 발생하며 뒤로 갈수록 더더 느려졌다.
사용자가 결과를 받아보기까지 대략 6~7초가 걸렸던 걸로 기억한다.
일관되고 깔끔하긴 했으나, 사용할 수 없는 로직이었다.

결국 코드를 다음과 같이 수정해야 했다.

class CarPolicyLoader:
    def _get_car_policy(self):
        """ DB 로 부터 car_ids 에 해당하는 정책을 가져옵니다. """

        car_id_to_policy = {}
        policies = self._repository.get_car_policy(self.car_ids)  # DB에 접근하는 부분 
        for car_id in self.car_ids:
            policy = self._find_car_policy(car_id, policies)
            car_id_to_policy[car_id] = policy

        return car_id_to_policy

위 코드는 DB 접근을 한 번만 한다. 그리고 원래 DB에서 하던 일(car_idpolicy 를 가져오는 일)을 파이썬 코드로 한다.
물론 이렇게 하면 파이썬 코드의 시간 복잡도는 한층 올라가지만, 속도는 그래도 기존보다 훨씬 빨라진다.
즉, DB 비용보다 코드 처리 비용이 싸다는 것이다.

물론 이것도 느려서.. 나중에는 아예 별도의 쓰레드로 만든 뒤, 캐싱하는 작업까지 했지만...
아무튼 처리 속도에 대해서 이야기할 때, 항상 이 DB 오버헤드 비용을 최적화 대상의 우선순위로 생각해야 함을 뼈저리게 느낄 수 있었다.


5. 로깅과 예외처리

난 사실 이전에 로깅과 예외처리에 대해서 깊게 생각해본 적이 별로 없었다.
사실 학부생 수준에서 하는 프로젝트에 이것들에 대해서 얼마나 생각해볼 기회가 있나 싶다. 잠깐 돌리는 프로그램을 만들거나 나만 보는 프로그램을 만드니깐.

그런데 프로덕션 수준에서 이 둘은 굉장히 중요하다. 그리고 꽤나 알아야 할게 많고, 아는 걸로 끝나는 게 아니라 잘 짜야된다.
일단 로깅과 예외처리 작업을 하게 되면, 기존 코드 로직이 좀 더 복잡해진다. 또 곳곳에 raiselogging 으로 코드가 너저분해진다.

이를 최대한 안 너저분하고 클린 하게 잘 짜는 것이 실력이다.
예를 들어, 적절하게 raise 해야 할 곳을 넣어주고, 이를 catch 하여 처리할 곳을 모아둔다던가, 로깅 찍을 곳도 한 군데로 모아둔다던가 등의 설계 디자인과 연관되어 생각해야 경우가 있다.
예외도 구조적으로 잘 정의해야 되는데, 나의 경우 내가 만든 에러를 프론트 쪽에서 받아보기 때문에 이 시스템을 모르는 개발자도 명확하게 알 수 있도록 예외를 정의해야 하며, 상속관계를 잘 생각해야 한다. 또한 예외 메시지는 명확하고 유용하면서도 내가 후에 디버깅을 잘할 수 있도록 짜야한다.

여하튼 온갖 경우와 시나리오들을 생각해야 하는데, 물론 100%란 건 없고 최대한 그렇게 되도록 노력하며 짜야함을 느낄 수 있었다.
아직 많이 부족하고, 앞으로 많이 배워야 할 부분이라고 생각한다.


6. 초기에는 TDD는 일단 최소한으로.

테스트 코드는 필요하다. 적어도 후에 코드를 보는 사람이 이 코드를 볼 때 어떤 걸 위주로 봐야 하는지, 어떻게 동작하는지 등을 살펴보고 싶을 때 가장 먼저 테스트 코드에서 힌트를 얻을 수 있기 때문이다. 또 내 코드를 후에 리팩토링 하려면, 테스트 코드라는 보험을 들어놔야 한다.

클린 코드, 테스트 코드 하면 빠질 수 없이 등장하는 게 TDD (Test Driven Development) 다.
나도 해보고 싶었다. TDD. 일단 테스트 코드를 하나씩 짜가며 개발했고, 이후에 정교하게 테스트 코드를 완성해나갔다.

그런데 중간에 프로젝트 요구사항이 바뀌는 경우, 내가 짜 놓은 코드들의 인터페이스 자체가 바뀌는 경우가 발생했다.
예를 들면, 팀장 회의 이후(나는 참여하지 않는..), 이전에 필요하지 않던 파라미터가 추가된다던지, 아니면 콜러 쪽에서 넘어오는 데이터 구조 자체가 바뀌는 경우가 있었다.
프로젝트의 요구사항이 바뀌는 일이 있을 거라 "이미" 생각하고 있었다. 마틴 파울러 아저씨가 책에서 분명 바뀐다고 강조했듯이...

그런데, 가끔 좀 심각하게 바뀌는 일들은 내 테스트 코드를 전면으로 다 바꿔야 하는 일을 만들곤 한다.
가끔은 기존 테스트 코드를 다 버리고 새로 짜야하는 경우도 있었다.
이럴 때마다 나름 정교하게 테스트한다고 이런저런 경우 다 넣어놓은 코드들은 노력이 무색하게 사라져 갔다.
개발만큼 테스트가 중요하다고 생각했으니... 개발 코드가 날라가는 마음이었다.

생각보다, 내 생각보다 더 생각보다, 프로젝트 요구사항은 자주 바뀐다.
프로젝트를 진행하면 진행할수록 해당 프로젝트에서 진짜 필요한 게 뭔지, 모두가 점점 알게 된다.
처음에 왜 이런 질문을 못했지? 이런 거 왜 아무도 말 안 했지? 이거 왜 명확하게 하지 않았지?
내가 생각한 거랑 너가 생각한 게 다르네..? 이런 얘기들이 나오게 된다.
마틴 파울러 아저씨가 책에서 말한 거, 그대로 경험했다ㅋ

그래서 프로젝트 초기에는 요구사항이 크게 바뀔 수 있으니, 일단 테스트 코드는 최소한으로 짜고, 빠른 개발에 집중하는 게 맞다고 생각한다. TDD 생각해서 테스트 코드 정교하게 짜고, 리팩토링 신나게 해도 요구사항이 크게 바뀌면.. 그냥 코드 쌩으로 버릴 수도 있다.
TDD는 프로젝트 요구사항이 어느 정도 안정화되었을 때, 혹은 요구사항이 애초에 잘 안 바뀌고 명확한 프로젝트를 할 때 빛을 발하는 거 같다.
한 줄 요약하면, TDD 가 언제 어디서나 늘 짱은 아닌 거 같다는 거다.


7. 마무리

프로덕션 코드를 개발해본 건 처음이라, 설레면서도 걱정되는 코딩을 했던 거 같다.
그리고 실제로 MSA 환경에 100% 배포가 되고 유저로부터 서버 로그가 실시간으로 찍히는 걸 보며 되게 신기했었다.
와 이게 진짜 배포가 된 거구나... 쏘카 앱을 사용하는 사람이 이제 내 코드 로직을 타는구나...

주변 동료분들은 내가 쏘카에서 가격이라는 메인 시스템을 다루는 프로젝트를 한 거라며 자부심을 가지라고도 하셨다. 신입인데 이렇게 핵심적인 프로젝트를 담당하게 돼서 매우 영광이었다.

프로젝트 기간 동안 찍은 커밋 흔적... 늘 개발만 한 건 아니였다. (빈칸에 대한 변명 ;;;)

한편, 파이썬에 정말 푹 빠질 수 있었던 기회였다.
파이썬 클린코드, 리팩토링 개정 2판, 이펙티브 파이썬, 슬기로운 파이썬 트릭 등의 책들을 꼼꼼히 보며, 필요한 것들을 바로바로 적용해볼 수 있었고, 그 과정에서 새롭게 알게 된 것도 많았다. 출퇴근길에 오고가며 파이콘 영상을 보기도 했는데, 파이썬을 좋아하는 사람들과 파이썬만의 문화에 나도 같이 동화되곤 하였다. 나도 이제 파이썬을 정말 좋아하게 된 거 같다. 

회사에서는 남은 올해도 가격 관련해서 개발할 게 많다고 하니...
이번 프로젝트에서 배우고, 써먹고 느낀 것을 바탕으로 다음 프로젝트에서는 좀 더 잘하는 사람이 되어야겠다.