[배민스토어] 신입 개발자 배민스토어 6개월 생존기

Aug.18.2023 유현호

Backend

어떤글인가요?

안녕하세요. 작년 겨울, 우아한테크코스를 수료하고 올해 1월에 배민스토어서비스개발팀으로 입사한 유현호입니다. 이제 막 6개월간의 수습 기간을 마치고 배민스토어에서 느꼈던 경험들과 배웠던 것들을 되돌아보고자 합니다. 기술적인 설명을 하는 글보단 팀원들과 함께 문제를 해결했던 경험과 배민스토어에서 느낀 점들을 회고하는 느낌으로 작성해 보았습니다! 가벼운 마음으로 읽어주시면 감사하겠습니다. 😀

입사 후 한 달간 그리고 담당한 기능들

입사 후 한 달 동안 대부분 문서를 보거나 회의에 참여하며 시간을 보냈습니다. 하지만 회의에서는 무슨 소리인지 모르겠는 단어들이 왔다 갔다 하고 있었어요. 회의가 정말 많았는데 곧 다가올 배민스토어 개편 프로젝트의 기획 리뷰 시간이 대부분이었습니다. (곧 폭풍 개발 기간이 다가온다는 뜻😭)

일반셀러 프로젝트’의 자세한 내용은 제현님이 작성하신 글을 참고해 주세요.

비슷한 시기에 함께 입사했던 우아한테크코스 동기들은 파일럿 프로젝트를 진행하고 있었습니다. 입사 전 공부했던 지식을 바탕으로 코드를 작성하고, 리뷰 받는 동기들이 부러웠어요. 하지만 곧 실제 앱에 동작하는 기능 개발을 담당하게 되었습니다. 담당당 기능의 지라 티켓이 만들어지고 운영 환경에 동작하는 기능을 개발하게 되니 오히려 파일럿 프로젝트를 진행하는 동기들이 반대로 저를 부러워하는 상황이 되었습니다.

동기들이 작성한 파일럿 프로젝트회고 글도도 많은 관심부탁드립니다.🤗

프로모션 시스템 엿보기: 파일럿 프로젝트
병아리개발자의 웹뷰 개발기: 파일럿 프로젝트
리뷰프로덕트팀 신입 개발자의 파일럿 프로젝트

당시 배민스토어서비스개발팀은 일반셀러 도입과 함께 새로운 아키텍처와 기술로 기존 코드를 마이그레이션 하는 작업을 진행하고 있었습니다. 저로선 모두 처음 사용해 보는 기술로 개발을 진행해야 했어요.

스크린샷 2023-08-02 오후 9 55 36

(무한도전 2012/09/29 방송 장면 중)

자바, Spring MVC, RDB에서 Kotlin, Spring WebFlux, NoSQL을 사용하면서 겪었던 어려웠던 점과 이를 해결했던 과정에 대해 짧게 얘기해 보려 합니다. 혼자가 아닌 팀원들과 함께 해결했던 기능을 위주로 설명하며 신입 개발자가 새로운 기술과 환경에 적응했던 경험을 소개하겠습니다.

멀티쿠폰 발급하기(페어 프로그래밍하기)

두근두근 처음으로 담당한 기능은 멀티 쿠폰 발급입니다. 대부분의 작업은 기존에 있던 코드를 신규 프로젝트로 이관하는 작업이었습니다. 편의상 자바를 사용한 하위 버전과 Kotlin을 사용한 상위 버전으로 구분하겠습니다.

image

위 이미지와 같이 쿠폰 한 번에 받기 버튼을 누르면 여러 브랜드의 쿠폰을 쿠폰함에 담는 기능입니다. 간단하게 떠오를 수 있는 동작 방식으로는 다음과 같습니다.

  • 여러 쿠폰을 요청받기
  • 로그인 한 회원인지 체크하기
  • 해당 쿠폰을 사용했는지 체크하기
    • 첫 주문 쿠폰의 경우 사용자가 주문한 기록이 있는지 체크하기
  • 첫 주문 여부에 따라 필터링된 쿠폰들로 발급 요청하기
  • 사용자에게 N 장중 몇 장의 쿠폰을 발급했는지 결과 응답하기

하위 버전에서 작성된 코드를 바탕으로 간단하게 재구성해 보았습니다.

public MultiCouponRes multipleIssue(MultiCouponReq req, Member member) {
    // 회원 검증하기
    if (member.isGuest()) {
        throw new RuntimeException("로그인이 필요합니다.");
    }

    // 첫 주문 쿠폰이고, 고객이 이미 주문한 적이 있는 쿠폰들 리스팅
    var usedCouponIds = req.getCouponIds().stream()
            .filter(couponId -> {
                CouponClientRes res = couponClient.getCouponInfo(couponId);
                return res.isFirstOrderCoupon() && !orderClient.isFirstOrder(member, res.getBrandIds());
            })
            .toList();

    // 위 실패케이스에 대한 쿠폰들을 제외하고 혜택 플랫픔으로 요청할 쿠폰들 리스팅
    var issuableCouponIds = req.getCouponIds().stream()
            .filter(couponId -> !usedCouponIds.contains(couponId))
            .toList();

    if (issuableCouponIds.isEmpty()) {
        throw new RuntimeException("발급 가능한 쿠폰이 없습니다.");
    }

    var res = couponClient.multipleIssue(issuableCouponIds, member);

    // 위 실패케이스에 대한 쿠폰들을 응답 객체로 변환하여 응답 리스트에 추가하기
    res.addResults(
        usedCouponIds.stream()
                .map(it -> MultipleCouponIssueRes.fail(it.value(), "대상이 아닌 쿠폰은 발급되지 않았습니다."))
                .toList()
    );

    if (res.allFailed()) {
        throw new RuntimeException("발급 가능한 쿠폰이 없습니다.");
    }

    // 결과 응답 및 결과 메시지 만들기
    return MultiCouponRes.builder()
            .couponIds(res.getCouponIds())
            .resultMessage(res.getResultMessage())
            .build();
}

기존 비즈니스 로직은 서비스 레이어와 DTO 등 여러 곳에 나뉘어져 있었습니다. 이 로직들을 도메인 클래스로 모으면서 Kotlin으로 작성해 보았습니다. 당시에는 WebFlux가 익숙하지 않아 우선 Kotlin 코드로 전환하는 쉬운 작업부터 진행했습니다. 동기적인 코드로 작성해 보고, WebFlux로 전환하려 했습니다. 제가 생각한 설계 방식으로 리팩토링하는 것은 어렵지 않았습니다.

fun multipleIssue(req: MultiCouponReq, member: Member): MultiCouponRes? {
    // 회원 검증하기
    if (member.isGuest()) {
        throw RuntimeException("로그인이 필요합니다.")
    }

    // 첫 주문 쿠폰이고, 고객이 이미 주문한 적이 있는 쿠폰들을 제외한 쿠폰 id (혜택 플랫픔으로 요청할 쿠폰 리스팅)
    val coupons = Coupons(usedCouponCheckService.issueableCoupons(req.couponIds))

    // 발행한 쿠폰들 추가
    coupons.add(couponClient.multipleIssue(issuableCouponIds, member))

    // 결과 응답 및 결과 메시지 만들기
    return MultiCouponRes(
        couponIds = coupons.ids,
        message = coupons.message()
    )
}

위와 같이 시험 삼아 작성한 코드를 WebFlux로 전환하려 했습니다. 하지만 WebFlux로 전환하는 일은 쉬운 일이 아니었습니다. 당시엔 리액터의 map, flatMap, filter 연산 정도만 알고 반환 값에만 겨우 맞춰가며 파이프라인을 작성할 수 있는 상태에서 기존 코드를 이관하기엔 어려운 작업이었습니다.

map, flatMap 연산을 하고 나면 반환된 결괏값을 가지고 다음 파이프라인으로 넘어갑니다. 멀티 쿠폰의 최종 응답은 발급에 실패한 쿠폰과 성공한 쿠폰의 데이터를 모두 가지고 결과를 만드는데요. filter 연산을 사용하여 발급 불가능한 쿠폰을 제외하면 다음 파이프라인에선 알기가 쉽지 않았죠. 그리고 위 Kotlin으로만 이관한 코드에서 보인 Coupon 클래스는 couponClient로 응답한 결과를 가지고 생성이 되는데요. WebFlux에선 비동기적인 연산으로 Mono가 반환되면서 도메인 코드 안에 비즈니스 로직들을 담기란 쉽지 않았어요. 이때 WebFlux 스터디를 리딩 중인 지산 님에게 고민을 얘기하였습니다.

(‘WebFlux 전환기‘에 관한 글은 지산 님의 기술 블로그를 참고해 주세요.)

이미 골머리를 앓고 있던 제 모습을 보든 지산 님은 페어 프로그래밍을 해보자고 해 주셨습니다. 저는 며칠 동안 정책서를 통해 비즈니스 로직을 이해한 뒤 코드를 수정할 수 있었는데요. 지산 님은 하위 버전의 코드를 직접 보면서 비동기적인 처리와 동기적인 처리를 구분해 가며 코드를 척척 작성하셨습니다.

fun multipleIssueCoupons(req: MultiCouponIssueReq, member: Member): Mono<MultiCouponRes> {
    return validateMember(member)
        .thenMany(req.couponGroupSeqs.toFlux())
        .flatMap { couponGroupSeq ->
            checkIssuable(couponGroupSeq, member).map { it to couponGroupSeq }
        }
        .collectMultimap({ it.first }, { it.second })
        .flatMapMany { couponGroupSeqByIssuable ->
            val issuableSeqs = couponGroupSeqByIssuable[true]
            if (issuableSeqs.isNullOrEmpty()) {
                StoreServiceException(BaseApiErrorCode.E09000).toMono()
            } else {
                val failedCoupons = couponGroupSeqByIssuable[false].orEmpty().map { Coupon.fail() }.toFlux()
                multipleIssueCoupons(issuableSeqs.toList(), member).concatWith(failedCoupons)
            }
        }
        .collectList()
        .map { coupons ->
            val result = MultipleCoupons(coupons)
            MultiCouponRes(
                memberCouponIds = result.memberCouponIds,
                resultMessage = result.message
            )
        }
}

지산 님과 페어 프로그래밍을 하면서 프로젝트 리액터의 공식 문서를 함께 살펴봤고, 마블 다이어그램을 보는 방법도 배울 수 있었습니다.’ flatMapMany, collectMultimap, zipWhen 등 새로운 연산자들도 알게 되었습니다.

검색하는 방식, 문제에 접근하는 방식, 코드를 작성하는 방식을 옆에서 보고 느낄 수 있기 때문에 페어로 개발하는 과정은 스터디보다 훨씬 습득이 빨랐습니다. 새로운 기술을 빠르게 습득하고 적용하는 것도 중요하지만 주어진 기간 안에 정상적으로 동작하는 기능을 개발하는 것도 일을 하는 데 꼭 필요합니다. 혼자 해결하기 힘들다면 팀원들에게 문제를 공유하고 도움을 요청해 보면 어떨까요?

푸드/배민홈 뱃지 아키텍처

image

배달의민족으로 음식을 주문하시는 분들께선 위 아이콘들이 익숙할 거예요. 저도 식사 시간이 되면 해당 지면에서 어떤 아이콘을 누를지 고민을 자주 하는데요. 이번에 소개할 경험담은 하단에 노출되는 편의점, 뷰티케어, 건강식품 등 배민스토어의 아이콘들을 배민1/배달 홈에 노출하는 기능입니다.

쿠폰 발급과 마찬가지로 하위 버전에서 만들어진 기능을 상위버전으로 옮기는 작업이었습니다. 이 기능을 구현하려면 배민1/배달 화면의 일부로서 작동하는 API를 만들어야 합니다. 그러려면 배민1/배달 지면이 식사 시간마다 받아내는 막대한 트래픽을 버텨내야 합니다. 다른 API보다 성능과 안정성에 더더욱 신경을 쓸 수밖에 없습니다.

동작 방식을 간단히 적어 보면 다음과 같습니다.

  1. 푸드 어드민에서 배민스토어의 아이콘을 등록한다.
  2. 배민스토어 배치 서버는 푸드 어드민에 등록된 아이콘을 조회한 뒤 DB에 저장한다.
  3. 푸드에서 스토어로 노출할 수 있는 아이콘 목록을 호출한다.
  4. 우선순위, 정렬조건, 사용자 위·경도에 맞는 노출 가능한 아이콘 목록을 반환한다.

image

배치 서버에서 배치를 돌며 스토어의 아이콘들을 조회하여 저장합니다. 하루에 한 번 전체 데이터를 갱신하고 5분에 한 번씩 추가/삭제된 아이콘, 우선순위 등 변경이 된 아이콘들에 대해 데이터를 갱신합니다. 데이터는 RDB와 Redis에 저장합니다. 캐시의 경우 동 코드를 키로 가진 아이콘 리스트와 아이콘 id를 키로 가진 필드들을 저장하고 있었어요.

image

하위 버전은 RDB를 사용하기 때문에 where 절을 통해서 특정 조건에 맞는 데이터를 조회하고 수정하기가 용이했습니다. 하지만 상위버전에선 Redis, DDB를 사용했는데요. Redis, DDB 모두 Key-Value 형태로 저장하기 때문에 RDB에서 사용하는 where 절을 그대로 사용하기엔 어려운 구조였습니다. 또한 RDB를 사용하려면 비동기 처리를 위한 Spring R2DBC를 사용해야 했는데 당시 버전으로는 조회할 때 write DB에 붙는 이슈와 배민스토어의 아키텍처와는 맞지 않기 때문에 어려움을 겪는 상황이었습니다. 겪고 있는 이슈를 하위 버전에서 해당 기능을 만드신 분께 의견을 물어보았습니다.

여러 논의를 하며 해결한 방법은 로컬 캐시와 Redis Pub/Sub을 사용한 방식이었습니다. 배치 서버에선 푸드어드민으로부터 배민스토어 아이콘을 조회한 후 동 코드를 키로 설정하고 아이콘 객체를 리스트로 만들어 API 서버로 데이터를 Publish 합니다. API 서버에서 Subscribe 하여 기존의 로컬 데이터를 모두 삭제하고 다시 저장합니다. 애플리케이션 레벨에서 데이터를 관리하기 때문에 개발하기 더 수월한 구조가 되었습니다.

이때는 Redis도 생소했지만, Reds Pub/Sub은 처음 들어 봤습니다. 당시 Redis Pub/Sub을 사용하기 위한 학습 테스트 코드가 작성되어 있었는데 이를 바탕으로 수월하게 기능을 만들 수 있었습니다. 또한 애플리케이션에서 @Scheduled로 동작하는 배치를 젠킨스로 옮기면서 처음부터 직접 젠킨스 서버를 구축하는 경험을 할 수 있었습니다.

배치뿐 아니라 Jenkins를 사용하여 수기로 풀임포트 등을 실행할 수 있도록 인터널 API를 만들고 아이템을 추가해 보는 경험을 할 수 있었습니다. 겪고 있는 이슈를 빠르게 공유하고 생각하지 못했던 설계 방식을 공유받게 되었습니다. 직접 회의 시간도 잡아보고 다양한 아키텍처에 대해 논의해 보며 지식을 얻을 수 있었습니다.

팀에 잘 적응하기

우아한형제들에 최종 합격이라는 메일을 받은 기쁨이 아직도 선명히 기억나네요. 더욱이 가고 싶던 배민스토어에 합류하게 되어 기대감이 컸어요. 동시에 회사에 들어가게 된다면 어떻게 팀에 잘 적응할 수 있을까 고민했었죠.

첫 번째는 ‘신뢰 자본 쌓기’였습니다. 이미 친근감이 쌓여있는 팀원들 사이에서 수용적인 태도를 유지했어요. 팀원들과 유대감과 신뢰를 먼저 쌓기로 했어요. 더욱이 입사 초기 시기에는 ‘일반셀러 프로젝트’를 진행하기 위해 새로운 아키텍처, 공통 스펙을 만들기 위한 논의가 많이 진행되는 시기였습니다. 이미 많은 회의를 통해 의사결정이 끝나가는 상황에서 제 의견을 강하게 주장하기보다 열심히 들어보면서 팀원들의 생각과 방향에 대해 이해하려고 노력했습니다.

두 번째는 개선할 부분 찾기였습니다. 입사하고 약 한 달 가까이 문서와 코드를 보는 시간이 많았습니다.(돌아가고 싶다!) 2가지의 개선할 수 있는 부분을 찾았는데 첫 번째가 위키 문서였고 두 번째가 테스트 코드였습니다. 팀 문서를 보면서 카테고리가 많고 깊어 원하는 문서를 찾기가 쉽지 않았습니다.

또한 문서화가 잘되지 않는 부분들도 있다는 걸 느꼈습니다. 제가 할 수 있는 영역으로는 다음 신규 입사자를 위한 가이드에 체크리스트를 최신화하고 제가 담당한 기능들에 대해 문서화를 해 두는 것이었습니다. 추후엔 보기 편한 팀 문서를 만들고자 다짐했었습니다. 하위 버전에서는 테스트 코드가 부족한 상황이었는데요. 빠르게 성장해 나가야 하는 배민스토어 특성상 업무가 계속 주어지고, 변경되어 테스트 코드까지 작성하기엔 시간이 부족한 상황이었죠. 초기에는 맡은 기능에 대해 업무 기간이 여유 있었기 때문에 테스트 코드까지 작성하기엔 충분한 시간이 있었습니다. 맡은 기능에 대해선 모두 테스트 코드를 작성하였습니다. (현재는…) 지금도 기억나는 순간이 하나 있는데요. 제가 작성한 테스트 코드를 보면서 많이 배웠다, 고맙다고 얘기해주던 팀원의 말이 열심히 배워 동료들에게 도움을 줄 수 있는 개발자가 되어야지 하는 마음가짐을 잊지 않게 해줍니다.

현재는?

입사 초기에는 주어진 기능과 역할에만 집중했었는데 현재는 주어진 기능 이외에도 팀 차원에서 더 개선할 수 있는 부분을 찾아 일을 하고 있습니다. 특히 현재는 배민스토어에서 다른 도메인을 담당하던 전시개발팀과 셀러개발팀이 합쳐진 상태인데요. 두 팀의 위키 또한 합쳐져야 하므로 문서들을 정리해야 할 필요가 더 커졌습니다. 따라서 입사 초기부터 계획했던 위키 개선을 주도적으로 진행하고 있습니다.

또한 팀원들의 코드를 보며 단위테스트를 작성하고 있고 거대한 코드들을 공통으로 mocking 하여 테스트하기 편한 환경을 만들기 위해 고민 중입니다. 또한 주어진 업무 이외에도 제가 잘할 수 있는 부분을 활용하여 팀을 개선해 나가려고 노력하고 있어요. 공부하고 싶은 영역은 너무 넓은데 이를 다 할 수는 없기 때문에 잘할 수 있는 것을 더 잘하기 위한 공부와, 업무에 적용할 수 있는 영역을 먼저 학습해 나가고 있습니다. 계속해서 개발적인 영역뿐 아니라 팀의 문화도 개선하며 신뢰받는 동료가 되기 위해 노력하고 있습니다.

예비 개발자분들과 입사한 지 얼마 안 된 신입 개발자분들에게

이제 입사한 지 6개월 정도 된 주니어라 이런 말을 하는 것도 어색하지만 제가 느낀 점들을 이야기해 보려고 합니다. 6개월 정도 일하다 보니 이런 생각을 하는구나! 정도로 읽어주세요.😀

  1. 내가 배운 기술만 사용하리란 법은 없다.

    지금까지 배워온 기술을 회사에서 그대로 쓰지 않을 수도 있습니다. 제 경험이 그렇듯이 다른 기술을 사용하는 상황도 많고 극단적으로는 서버 개발자가 프론트엔드 또는 앱을 개발하는 상황도 있겠죠. 배민스토어에서 일을 하면서 느낀 점은 새로운 기술을 적용해야 한다면 기능을 만들 수 있는 최소한의 지식을 위주로 학습하여 적용하는 능력이 필요하다고 느꼈습니다. 이 이후에 트러블 슈팅이나 스터디를 진행하면서 더 깊이 있는 학습을 통해 기존에 만들었던 기능들을 리팩토링한다면 개인의 학습과 팀의 성장을 모두 끌어낼 수 있지 않을까요?

    다소 이상적이지만 제가 전달하고 싶은 얘기는 다음과 같아요. 경험하지 않은 기술을 써야 한다면 ‘이걸 공부해야지’보단 ‘이걸 왜 써야 하지?’라는 접근을 해보는 거예요. 당장 기능을 개발하기 위해 깊이 있는 지식이 필요한 상황은 아직까진 없었어요. 적어도 내가 사용하는 기술의 근거를 가질 수 있고, 비교군들을 보다 보면 언젠가 다가올 회의에서도 내 의견을 말 할 수 있고 회의의 내용이 이해되는 순간들이 생길 거예요.

  2. 커뮤니케이션 능력은 생각보다 더 중요하다.

    커뮤니케이션은 같은 팀원들뿐만 아니라 정말 다양한 사람들과 하게 됩니다. 개발자는 개발만 잘하면 되는 것이 아닌가? 라는 생각이 들 순 있지만 자신의 의견을 상대방에게 잘 전달하고 받아들이고 설득하는 능력. 슬랙과 같은 메신저를 통해 커뮤니케이션해야 하는 상황이라면 텍스트로 전달되는 딱딱함을 부드럽게 하는 능력들이 그 사람을 신뢰하게 되고 함께 일하기 즐거운 사람으로 기억되더라고요. 그래서 더 중요하다고 생각합니다.

  3. 팀 도메인 지식 쌓기!

    저는 1월에 입사하여 올해는 팀 도메인과 팀에서 사용하는 기술 위주로 학습하기로 마음먹었습니다. 이것만 해도 학습할 양은 너무 많더라고요. 팀의 도메인 지식을 갖추면 신규 기능을 개발할 때 해결책을 찾는 데 유리하고 연관된 사람들과 원활한 커뮤니케이션을 할 수 있어요. 또한 담당한 도메인의 지식을 공유함으로써 조금 더 신뢰받는 동료가 되겠다고 생각해요. 아직도 모르는 부분이 많지만, 단위테스트를 작성해 보면서 조금씩 아는 영역을 넓혀나가고 있습니다.

배민스토어서비스개발팀은?

배민스토어는 푸드 서비스에 비해 출시된 지 얼마 안 된 비교적 짧은 서비스입니다. 음식 배달을 넘어서 다양한 상품들을 제공하는 배민스토어는 안정적인 시스템을 갖춘 우아한형제들이라는 큰 회사 안에 있는 스타트업이라고 느꼈어요. 안정적인 인프라를 갖춘 환경에서 서버를 새롭게 띄워 처음부터 개발하는 과정을 경험해 볼 수 있어요. 또한 사업적으로도 새로운 기능들이 빠르게 요구되고, 아키텍처를 개선하기 위해 여러 도전을 해볼 수 있습니다. 사용자의 위치를 기반으로 배달이 가능한 가게들을 찾아서 상품을 노출해야 하므로 기존 시장에 있는 커머스와는 다르게 서비스를 운영해야 한다는 점이 매력적입니다. 앞으로 성장할 배민스토어의 미래와 저희의 기술적인 도전에 많은 성원 부탁드립니다.🙇‍♂️


[배민스토어] 일반셀러 프로젝트 시리즈 더 보기