본문 바로가기
Book

[독서 기록] 요즘 우아한 개발, 우아한형제들 지음

by Renechoi 2024. 2. 3.
 
요즘 우아한 개발
우아한형제들(우형)의 배달의민족은 2010년 서비스를 시작해 이젠 푸드테크를 선도하는 플랫폼이 되었습니다. 우형은 ‘우아한 기술블로그’를 통해 꾸준히 기술 노하우를 공유해왔습니다. 이 책은 블로그의 글을 엄선해 우형이 성장하며 겪고 헤쳐간 온보딩, 개발, 문화, 이슈 관리 이야기를 담았습니다. 쉽게 읽을 수 있도록 코드를 최대한 제거하고, 개발자 저자 각자의 개성을 담은 문체는 살렸습니다. 요즘 IT 회사가 어떻게 일하는지 궁금한 개발자와 기획자에게 이 책이 좋은 길잡이가 되기를 희망합니다. 책 말미에 링크를 남겨놓았으니 원문이 궁금하다면 함께 읽어보세요!
저자
우아한형제들
출판
골든래빗(주)
출판일
2023.10.13

 

 

계속 다니고 싶은 회사의 기술 문화를 만든다는 것 

- 8p 

 

 

좋은 것은 널리 알려 나누려는 노력

- 13p

 

 

프로젝트의 히스토리를 남기는 문서화 방식은 다음과 같습니다.

- 1단계: 프로젝트/피처의 작업 시작 전에 대략적인 스케치를 진행하는 과정입니다. 프로젝트의 배경 및 무엇을 어떻게 진행할지에 대한 내용을 작성합니다.

- 2단계: 이슈 및 문제 해결 과정 등 프로젝트의 히스토리를 작성하는 과정입니다. 파일럿 프로젝트를 진행하면서 설계 과정을 N 단계 히스토리로 작성하며 문서화 방식에 적응하게 됩니다.

- 마지막 단계: 프로젝트/피처의 작업 후 회고를 진행합니다. 1단계에서 작성했던 내용을 바탕으로 성과/비교 중점으로 작성합니다. 

 

- 24p 

 

 

B마트에서는 더 효과적인 코드 리뷰 문화로 거듭날 수 있게 도와주는 코드 리뷰 봇이 있는데요. 코드 리뷰 봇은 PR을 요청했을 때 저 대신에 랜덤으로 선정된 리뷰어 인원들에게 새로운 PR이 요청되었음을 알리는 메시지를 슬랙으로 호출과 함께 보내줍니다. 또 PR에 새로운 코멘트와 리뷰가 달렸을 때, 그리고 승인될 때마다 슬랙으로 바로 알림을 줍니다.

- 28p 

 

 

 

저희 팀은 깃 플로 전략을 사용합니다. 새로운 기능, 수정에 대한 요구사항이 생기면 제일 먼저 지라 티켓을 생성합니다. 그런다음 develop 브랜치로부터 ticket 브랜치를 생성한 뒤에 코딩을 시작합니다. ... 모든 작업이 마무리 되면 release 브랜치를 생성하고 테스트 서버에 배포한 뒤 이상이 없으면 main 브랜치로 운영 서버 배포를 진행합니다.

- 37p 

 

 

테스트 없는 코드는 커밋 직후 죽어버립니다. 어떤 동료도 그것을 건드리고 싶어 하지 않을 겁니다. 우리는 팀입니다. 모두가 신뢰하고 이해할 수 있는 방향으로 진행해야 합니다. 팀원 모두가 노력한 덕분에 지금은 테스트 케이스가 당연한 것이 되었습니다. 

- 38p

 

 

 

- 주어진 일만 잘해야지 생각했는데 조금씩 동료와 팀이 잘하는 것까지 신경 쓰게 되었을 때

- 나무가 아닌 숲을 보게 되었을 때

- 일을 잘하기 위한 틀을 만드는 것에도 관여하게 될 때

 

- 42p 

 

 

우리는 시니어의 성장을 '주변에 주는 영향력'으로 정의하기로 했습니다. 꼭 리더가 아니더라도, 실무를 하면서도 주변 개발자들에게 긍정적인 영향을 주고 새로운 경험을 할 수 있게 기회를 열어주는 것. 그리고 이런 영향력을 발휘하기 위해 자신의 시야를 넓히는 것이 시니어에게 중요한 성장이죠.

- 43p 

 

 

'최고의 복지는 좋은 동료다'라는 말이 있습니다. 계속 이 회사에서 일하고 싶게 만드는 것도, 이직을 생각하게 되는 요인도 바로 동료입니다. 우선 우리가 생각하는 회사란 '평범한 사람들이 모여 비범한 일을 이루어 가는 곳'인데요, 이런 회사를 만들기 위해 필요한 좋은 동료를 정의해 보자면 '누구의 일로 나누지 않고 함께할 각오가 되어 있는 사람'입니다. 이런 동료가 함께 한다면 심리적 안정감은 물론이고, 같이 일하면서 배울점이 많다고 생각하게 되죠. 시니어들은 협업을 하는 과정에서 분명히 각자 하기 싫은 업무가 있을 텐데도 '제가 볼게요!'라고 뛰어드는 동료들이 많다는 사실에 새삼 놀랐다고 해요. 

- 47p 

 

 

 

- '이 분이 있으니 든든하다, 어떤 일이든 같이 할 수 있겠다'라는 생각이 드는 분들이 좋은 동료라 생각하는데 우아한형제들에 그런 분이 많다.

- 실력이 뛰어난 분들이 많다. 그분이 슬랙, 위키에 공유하는 내용이나 발표할 때 도움을 많이 받는다. 내가 배올 수 있는 사람이 좋은 동료이다.

- 우리 회사는 각자의 서비스에만 집중하는 게 아니라 공유와 협업이 매우 잘되는 분위기라서 좋은 분들이 좋은 영향력을 줄 수 있는 기회도 많다고 생각한다.

- 기술적으로 잘하는 동료도 좋지만 동료에게 자신감, 안정감을 느낄 수 있게 도움을 주는 분이 많은 것 같다.

- '착한 사람'이 좋은 동료라고 생각한다. 착하다는 것은 성격적인 기질뿐만 아니라 '이해의 폭이 넓음'을 의미한다. 업무량이 많아지고 힘들어지면 자기만 생각 하기 쉽다. 이전에 나도 그런 경험을 많이 했다. 그런데 우아한형제들은 착한 사람이 많다. 

 

- 48p 

 

 

시니어들이 만장일치로 뽑아준 우리 개발 문화 DNA는 바로 '공유'였어요. 더 구체적으로 정의하자면 '공유에 대한 심리적 문턱이 낮은 문화'입니다. 각 팀에서는 문제를 해결한 히스토리를 위키나 전사 슬랙을 통해 다 같이 공유하고, 같은 문제를 겪지 않기 위해 서로의 노하우를 축적해나가고 있습니다.

- 49p 

 

 

 

"솔직히 별거 아닌 것도 너무나 쉽게 공유해요. '저런 것까지 공유하나?' 싶을 때도 있어요. 공유하는 사람도, 공유받는 사람도 그런 상황이 너무나 자연스럽고 문턱이 매우 낮아요. 그래서 내가 뭔가를 공유하고 싶을 때 고민이 되지 않아요."

- 49p 

 

 

 

또 하나의 DNA는 '심리적 안정감'입니다. 우아한형제들 구성원은 피드백에 좀 더 열려 있고 다른 의견을 비난으로 받아들이지 않는 태도가 바탕에 깔려 있습니다. 빠르게 과제를 해결하다 보면 타 부서와의 협업이 매끄럽지 않을 때도 있지만, 이 과정에서 각자의 목소리를 낼 수 있고 결국 잘 해내기 위한 본질에 집중하면서 서로 맞춰가려고 하는 문화가 심리적 안정감을 형성하는 주된 요소입니다.

- 50p

 

 

 

"내가 무슨 말을 해도, 내가 부득이한 사정으로 자리를 비워도, 어떤 오류가 발생해도 나는 우아한형제들 안에서 안전하고 괜찮다고 느끼고, 목표 달성과 문제 해결을 위해 동료들과 함께 노력하고 있어요."

- 50p 

 

 

일도 많고, 코드 리뷰까지 하면... 기술 부채들은 언제 처리하나요?

- 52p

 

우아한형제들 하면 빼놓을 수 없는 문화가 바로 '피트스탑'입니다. 피트스탑이란 1년에 한 번 2주간, 진행하고 있는 과제를 멈추고 팀 상황을 재정비하고 그동안의 기술 부채를 해결하는 우아한형제들만의 제도입니다.

- 52p 

 

 

 

"피트스탑은 가장 자랑하고 싶은, 개발자에게 최고의 복지라고 생각합니다. 기술 부채, 리팩터링 등 자꾸 미뤄두게 되는데 그러다 보면 코드를 건드릴 수 없는 지경까지 이르게 됩니다. 그리고 이런 기술 부채의 끝은 '퇴사'로 이어집니다. 그런데 1년에 2주 모든 서비스를 쉬면서 피트스탑을 할 수 있게 회사에서 챙겨줘서 너무 좋아요."

- 52p 

 

 

 

"다른 회사외 비교했을 때 우아한형제들의 특징은 심리적 안정감을 주는 회사라는 겁니다. 시니어일수록 실수를 했을 때 질책이나 책임의 크기가 더 커지기 마련인데요, 책임을 묻는 것보다 다 같이 해결하려고 노력하고 다시 발생하지 않게 하는 데 집중합니다."

- 55p 

 

 

알람만으로는 충붆하지 않았고, 칸반에서 리뷰 MAX(최대치) 초과 시 모든 팀원이 업무르 중단하고 리뷰하기로 규칙을 정했습니다. 또한 업무에 집중하기 위해 진행 중인 업무(개발) 티켓을 동시에 여러 개 만들지 않고 하나씩 순서대로 진행하기로 결정했습니다.

- 60p 

 

 

- Keep: 잘하는 점. 계속 했으면 좋겠다 싶은 점

- Problem: 뭔가 문제가 있다 싶은 점. 변화가 필요한 점

- Try: 잘하는 것을 더 잘하려면, 문제가 있는 점을 해결하려면 우리가 시도해볼 것들 

 

- 66p 

 

우리 팀에서 경계하는 것 중에 하나가 '반복되는 질문에 신경질적인 반응을 보이는 것'입니다. 질문하길 꺼리는 분위기가 생기면 그로 인해 중요한 문제를 '괞히 질문해서 한소리 듣지 말자, 설마 이게 맞겠지, 그냥 내보내자'식으로 확인 없이 그냥 배포했다가 바로 장애로 이어지는 일이 발생합니다.

- 67p 

 

 

질문이 왜 반복되는지, 그럴 필요가 없게 만들 수는 없는지 고민이 필요해 보입니다. 문제의 초점을 '질문 하는 사람'에서 질문받는 사람'으로 돌립니다. 질문하는 사람에게 "왜 자꾸 질문해요?"라고 나무라는 게 아니라, '왜 자꾸 같은 질문이 나올까? 내가 질문을 덜 받으려면 어떻게 할까?'라고 생각해보는 것이지요.

- 68p 

 

 

코드와 최대한 가까운 거리에 주석으로 이유를 적어두는 것이 좋습니다.

- 69p 

 

 

젠킨스에서 빌드를 할 때 가장 최근 코드 커버리지를 기록하고 그것보다 커버리지가 떨어지면 빌드를 실패하게 하자.

- 72p 

 

 

 

"만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다." 우아한형제들 사무실에서 쉽게 발견할 수 있는 문구입니다. 

- 114p 

 

 

'어차피 사용하는 사람도 많지 않은 백오피스 서비스인데, 이렇게까지 해야 하나요?'라는 의문이 생길 수 있습니다. 편한 UI/UX는 사용자가 인지하지도 못 한 채 그냥 편하게 사용하지만, 불편함은 바로 알아차립니다. 이런 불편함을 사용자 몫으로 떠안게 하는 것이 아니라 어떻게 하면 사용자가 더 편하게 서비스를 사용할 수 있을지 고민하는 것이 프론트엔드 개발자에게 중요한 가치라고 믿습니다.

- 120p 

 

 

사용자의 입력은 무조건 검증한다

- 123p 

 

 

사용자의 모든 입력은 검증해야 합니다. UI에서 먼저 검증해서 편리성을 높이고(이건 말 그대로 편리성을 위한 겁니다), 그리고 서버 측에서도 올바른 범위의 값을 입력했는지 무조건 검증해야만 합니다. 서버 측에서 검증되지 않으면 클라이언트에서 3중, 4중으로 검증했더라도 그냥 검증이 안 된 겁니다.

- 124p 

 

 

계산 로직은 사용자 편의를 위해 프론트엔드에서 일시적으로 해서 보여줄 수는 있으나 최종 결과는 서버에서 재계산해야 합니다. DB 조회 한 번 하는 거 귀찮다고 안 했다가 큰일 납니다.

- 125p 

 

 

조회 조건을 생성하는 사용자 요청 데이터는 무조건 서버 측 검증을 거쳐야만 합니다.

- 126p 

 

 

 

원격 분산 캐시를 사용한다면 몇 가지 주의할 점이 있습니다. 원격 분산 캐시는 네트워크 대역폭을 먹고 삽니다. 캐시에 너무 많은 데이터를 담으면 비록 캐시 히트율이 매우 높더라도 데이터가 네트워크 대역폭을 잡아먹어서 느려집니다. 따라서 실제 운영 서비스를 기준으로 성능 테스트를 하고 충분한 대역폭이 확보되어 있는지 확인했어야 합니다.

- 128p 

 

 

사용자의 로그인 실패 횟수를 트래킹해야 한다.

- 130p 

 

 

API 서버를 직접 호출해야만 하는 일이 자주 생긴다면, 해당 역할을 하는 인증과 권한 관리가 된 내부 관리자용 어드민 서비스를 만들어야 합니다.

- 136p 

 

 

로그를 외부 서버로 수집하는 것은 별도 프로세스에서 비동기로

- 138p 

 

 

오류 로그는 일상적인 것과 크리티컬한 것을 구분한다

- 141p 

 

 

 

제가 하고 싶은 말의 핵심은 이겁니다. '이런 저런 일이 생길 수도 있겠네, 그러면 큰 문제가 되겠는데?'라는 생각이 드는 순간 실제로 그 일이 발생했다고 가정해야 한다는 겁니다. 

- 143p 

 

 

 

 

모두가 주인인 듯 아무도 주인이 아니다.

- 148p 

 

 

프로젝트를 계획할 때 레거시 제거도 프로젝트 범위에 포함하자

- 149p 

 

 

명명 규칙을 미리 정하고 최대한 많은 사람에게 공유하자

- 149p

 

 

코드에 대한 오너십을 갖자

- 150p 

 

 

 

Support 채널에 ChatGPT 적용하기 

1. Support 채널 요청

2. ChatGPT로 키워드 추출 요청

3. 추출된 키워드 전달

4. 받은 키워드로 적절한 답변 조회

5. 조회된 답변 전달

6. 요청자에게 답변 전달 

- 219p 

 

 

 

정확히 말하면 요구사항으로 명시되어 있지 않더라도 당연히 되어야 하는 것들이 안 되는 경우가 많았는데요, 예를 들면 다음과 같습니다. 처음에 우리 팀은 사용자가 업주 번호를 입력해서 업주에 소속된 주문을 조회하는 기능을 구현했습니다. 여기서 뭔가 이상하지 않나요? '누가 업주 번호를 다 외우고 있어?'라는 생각이 드셨다면 정상입니다. 저는 너무나 당연하게 저렇게 구현하고 업주 번호로도 검색된다고 생각했습니다. 하지만 일반적으로 사용자는 업주 번호를 외우지 않고, 검색/클릭을 통해 찾습니다. 그래서 업주 번호를 아는 경우는 직접 입력할 수 있고, 모르는 경우 검색/클릭을 통해 입력할 수 있는 형태로 기능을 추가했습니다.

- 230p 

 

 

 

오늘 발생한 주문에 대해 지급금을 생성하는 경우 시간은 어떻게 설정할까요? 저는 자정에서 23:59:59로 설정했는데요, 이 경우 23:59:59:01초에 들어온 주문은 어떠한 지급금 생성에도 포함되지 않습니다. 나노초도 깔끔하게 무시해버리죠. 해당 피드백을 덛고 다음날 자정 직전까지라는 형태로 변경했습니다. 기존에는 between으로 매개변수를 둘다 포함하는 관계 start<= statusAt <= end 구조로 작성했다면 현재는 start<= statusAt < end 형태로 리팩터링했습니다.

- 231p 

 

 

MVC 구조로 프로젝트를 구성하는 분들은 Controller -> Service -> Repository 구조가 익숙할 텐데요, 이 경우 의존성의 방향이 Controller에서 Repository로 단방향으로 흐르는 것이 일반적입니다. 하지만 저는 Controller에서 받아온 Request Type을 그대로 받아서 Service에서 사용했습니다. 이 경우 문제가 되는 것은 다음과 같았습니다. 

- Service 가 받고 싶은 포맷(Parameter)이 Controller에 종속적이게 된다. Service가 Controller 패키지에 의존하게 된다.

- Service 층이 모듈로 분리되는 경우 해당 Type을 사용할 수 없다.

- 트랜잭션으로 처리되어야 하는 DTO 항목이, 항상 요청으로 들어온 값과 동일하지 않을 수 있다. 

 

마지막 문제점을 조금 더 설명해보겠습니다. 예를 들어 사용자 요청의 매개변수를 통해 외부 API를 여러 번 호출한 이후 Service 층을 호출하는 경우, Controller가 받은 Web DTO와 Service가 받아야 할 DTO가 달라집니다. 외부 API 호출뿐만 아니라 클라이언트 요청 이후 Service 층을 호출하기 전 다른 작업으로 인해 데이터 포맷이 달라질 수 있습니다. 이런 때에 Service 층이 Controller 층의 DTO에 의존해서 문제가 될 수 있습니다. 따라서 Service 층은 자신이 원하는 포맷으로 데이터를 받을 수 있어야 합니다. 

 

이러한 문제를 개선하기 위해 Service는 자신이 원하는 포맷에 맞게 데이터를 받고 Controller에서 그 포맷을 만들어주는 방식으로 리팩터링했습니다. 층을 분리하는 것이 습관적으로 하는 작업이 아니라 층별로 담당해야 하는 역할을 명확히 하고, 층별 의존 관계를 고려해 유지보수하기 좋은 형태를 만든다는 점을 배웠습니다. 추가로 Service에서 엔티티를 받고 엔티티를 반환하는 형태도 좋은 방법이라고 생각합니다. 다만 다음과 같은 이유로 저는 DTO를 받고, DTO를 반환합니다. 

 

- 불완전한 엔티티를 Service 파라미터로 받는 부분이 적절하지 않다.

- Service 메서드별로 원하는 포맷이 달라지는 경우 결국 DTO로 분리될 것이고, 이는 Service 파라미터가 엔티티/DTO로 받게되어 일관성을 위배할 수 있다.

- 반환 타입의 경우 Service를 사용하는 한 부분인 Presentation 층에서 도메인을 알고 있는 것 자체가 문제가 될 수 있다고 판단해서 DTO를 반환한다. 

 

- 231~233p 

 

 

'Util성 클래스를 무엇이라 정의할 것인가?'와 같은 용어에 대한 정의는 팀별로 다르겠지만, 팀 의견이 없다면 가장 범용적으로 사용되는 의미에 적합한 형태로 코드를 작성하는 것이 중요하다는 걸 느낄 수 있었습니다. 여담으로 롬복에서 제공하는 @UtilityClass라는 애너테이션을 사용해 final 클래스, static 메서드로 Util 클래스를 관리할 수 있습니다.

- 235p 

 

 

'모듈은 최소 스펙으로, 데이터의 형태는 확장 가능한 형태를 지향하고 있어요.', '미래를 고려한 설계는 오히려 복잡성을 증가시키는 경향이 있어요.' 

- 237p 

 

 

외부 라이브러리를 사용하는 것

- 제어권의 유무

- 공식 지원 유무

- 커스텀 지원

- 안정성

- 마지막 수정/커밋 

 

- 238p 

 

 

배치를 구현하며 처음 했던 고민은 'WAS와 데이터베이스 중 어디서 연산해야 하는가?'였습니다. 저는 자바에서 연산을 처리하는 방식을 선택했는데요, 결제수단별 집계를 위해 작성한 로직은 다음과 같습니다.

- 주문 데이터를 조회

- 업주별 주문상세(결제 타입 및 금액이 포함되어 있음) 집계

- 집계된 주문 상세를 결제 타입별로 다시 집계

 

DB에서 그룹핑하지 않고 자바에서 그룹핑을 하면 다음과 같은 문제가 있습니다.

 

- 그룹핑 로직을 자바에서 수행하기에 이를 관리하는 일급 컬렉션, 자료구조가 복잡해진다.

- 복잡한 자료구조를 갖기 때문에 단위 테스트가 불편하다.

- 그룹핑 조건이 추가되면 또 다른 일급 컬렉션을 만드는 등 유지 보수가 어렵다. 

 

이러한 문제를 해결하기 위해 DB에서 그룹핑과 합산을 모두 하는 형태로 변경했습니다. 이 경우 DB에서 연산해서 조회하는 경우 일급 컬렉션도 필요 없고, 쿼리로 집계된 결제수단별 주문 데이터를 저장하는 작업만 수행하면 됩니다. 

 

데이터 25,000건을 대상으로 로직을 계산할 때 다음과 같은 결과를 얻을 수 있습니다. 성능에 문제가 되는 상황이 아니라면 유지보수하기 용이한 방향으로 로직을 작성하는 것이 좋다고 판단해 DB에서 연산하는 형태로 리팩터링했습니다. 

- 239~240p 

 

 

애매했던 부분은 데이터를 저장/수정하는 작업을 모두 Writer에서 수행하는 것이 맞을까 혹은 해당 배치의 핵심적인 저장/수정 내용이 아닌 부분은 Processor의 단계 중 일부러 처리할까 하는 의문이 들었습니다. 이에 대해 팀에서는 일반적으로는 다음과 같은 처리 바익을 가진다고 피드백받았습니다.

1. Reader에서 읽어온 데이터마다 다른 API를 요청하거나 각 데이터 저장/수정이 필요한 경우 Processor에서 처리하고(Reader에서 Processor로 데이터는 단건씩 넘어가기 때문에) 이후 최종 데이터만 Writer에서 저장한다.

2. 일괄적으로 동일하게 저장/수정해야 하는 경우 Writer에서 처리한다.

 

- 242p 

 

 

기본에 충실한 개발자가 되자

부끄럽지만 저는 비즈니스 요구사항보단, '어떤 설계가 더 나은 설계이고 어떻게 해야 더 나은 코드를 짤 수 있을까?'에 대한 고민이 늘 앞섰습니다. 물론 이런 고민이 나쁘다고 생각하진 않지만 무엇보다도 요구사항을 명확히, 그리고 당연한 것을 당연히 되도록 하는 일을 우선으로 해야 합니다.

- 244p 

 

 

클라이언트 개발 이전에 서버 테스트를 진행한다.

- 249p 

 

 

'한 아이를 키우려면 온 마을이 필요하다'라는 아프리카 속담을 들어본 적 있나요? 저는 이 속담이 IT 업계에도 딱 맞는다는 생각을 자주 하게 되는데, 하나의 서비스를 잘 키우기 위해서 수많은 외부 시스템의 도움이 필요하기 때문입니다.

- 288p 

 

 

외부 시스템 장애를 회피할 수 있는 몇 가지 방법

- 의존성 제거

- 벤더 이중화 

- 장애 격리

- 미작동 감내

- 293~295p 

 

 

- 첫 번째 질문은 항상 장애에 영향을 받은 고객의 관점에서 시작해야 합니다.

- 306p 

 

 

반응형