파일럿 프로젝트와 함께 우아한개발자 되어가기

Nov.29.2018 김다인

Culture

들어가며

안녕하세요! 배민서버개발팀 김다인입니다.

지난 10월 신입 개발자로 입사한 후 약 5주간에 걸쳐 파일럿 프로젝트를 진행했는데요,

그동안 제가 경험하고 느낀 것들을 공유해 드리고자 합니다.

시작

설렘 반, 긴장 반으로 입사한 당일, 팀장님께서는 저를 위한 선물이 준비 중이라고 하시면서 파일럿 프로젝트에 대한 이야기를 시작하셨는데요. 넷플릭스의 Full-cycle 개발자를 언급하시면서, 신입 개발자가 실제 업무에 투입되기 전, 직접 개발과 배포의 전 과정을 경험해보는 것을 통해 현재 서비스의 전체적인 시스템을 이해하고 그 과정에서 부족한 부분을 채울 기회를 제공해주고 싶다고 하셨습니다. (정말 좋은 팀이죠?^^)

그렇게 5주간의 파일럿 프로젝트를 부여받았고, 주제로는 간단 주문시스템 구현이 주어졌습니다. 웹프론트엔드와 백엔드 개발 및 배포까지의 모든 cycle을 혼자 진행하고, 각 단계를 마칠 때마다 팀원분들의 리뷰를 받는 것이 전체적인 과정이었습니다. 이름하여 "ShowMeTheCode"!

ShowMeTheCode
[사수이신 용근님이 한땀 한땀 만든 ShowMeTheCode 로고]

요구사항과 기술 스펙

프로젝트에 대해 정식으로 전달받은 날, 개발 및 배포에 대한 요구사항과 기술 스펙을 안내받았습니다. 객체 지향적인 코드, 통합 테스트, jira를 통한 이슈 관리, git 버전 관리, 그리고 무중단 배포 등이 요구사항이었고 기술 스펙은 아래와 같이 주어졌습니다.

필수 개발 스펙 선택 개발 스펙
Gradle Lombok
Spring Boot 2.x QueryDSL
Spring Data JPA Spring REST Docs
H2 Multi Module
RESTful API Spring Security
Vue.js

이후 이를 기반으로 개발을 진행할 때에 선택 스펙 중에는 Lombok만 사용했고 피드백 이후에는 QueryDSL을 추가로 사용했습니다. 요구사항이나 스펙은 주로 필수로 주어진 것들에 집중했는데, 마치고 나서는 선택사항들을 통해 더 많이 배울 수 있었을 것 같아 아쉬운 마음이 들었습니다. 하지만 팀원분들의 말씀으로는, 이제는 직접 부딪히면서 배울 때라고 하시네요.

일정

프로젝트의 각 단계는 사전에 정해진 시간 동안 진행되었습니다.

  • 1, 2주차: 개발 + 코드리뷰
  • 3주차: 배포 + 배포리뷰
  • 4, 5주차: 리뷰 반영 및 회고

2주차 마지막 날에는 코드리뷰, 3주차 마지막 날에는 배포리뷰가 진행되어 제가 작성한 코드와 이를 배포하는 과정에 대해서 각각 피드백을 받을 수 있었습니다. 받은 피드백을 적용, 개선한 것에 대해서도 다시 리뷰를 받을 수 있었습니다.(짱!)

1주차, 2주차: 개발

개발을 시작한 처음 이틀 동안에는 요구사항을 분석하고 기존 시스템을 공부하면서 사용자 스토리와 백로그, ERD를 작성하는 시간을 가졌습니다. 구현 단위별로 난이도와 시간을 고려해서 일정을 계획하고, 이를 이슈로 등록하여 이후 일정 순서에 따라 이를 하나씩 해결해나가는 방식으로 작업할 수 있도록 했습니다. 이런 방식의 계획과 작업은 앞서 참여한 두 번의 우아한테크캠프를 통해 처음 경험했었는데, 이후 이런 과정이 저에게 재밌게 느껴지는 것 같습니다. (우테캠 최고!)

사용자스토리, 백로그, 지라이슈

[사용자 스토리, 백로그, 지라 이슈 등록]

일정은 가장 주요한 기능부터 먼저 구현한 이후에 다른 기능들을 붙여나가는 식으로 진행했습니다. 이에 따라 1주차에는 상품과 주문(요청 & 취소)을 구현하고 2주차에는 주문내역 목록, 카테고리, 상품옵션, 예외처리, 회원 구분 등을 구현했습니다. 특징이 있다면 각 기능 개발은 인수 테스트를 작성하는 것으로부터 시작했습니다. 구체적인 설계를 돕고, 안정적인 코드 수정을 제공하는 등의 TDD의 장점을 최대한 이용하고자 노력했습니다.

ERD

[ERD]

일정을 최대한 상세히 나눈 덕분인지, 계획한 대로 일정이 진행될 수 있도록 노력을 많이 했습니다. 초반에 야근을 꽤 했지만, 덕분에 후반부에는 여유로운 편이었습니다. 다만 이후 리뷰를 받고 나서는 여유로웠을 그 때에 제가 설계하고 구현한 것들에 대해서 왜 그렇게 했는지에 대한 면밀한 검토가 있었으면 좋았겠다는 생각이 들었습니다.

코드리뷰

2주차 마지막 날, 작성한 코드에 대해 팀원분들로부터 피드백을 받는 시간을 가졌습니다. 주문시스템과 밀접한 연관이 있는 결제정산개발팀분들도 오셔서 회의실을 가득 채워서 진행되었습니다. 2주의 개발 일정 중에서 가장 도움이 많이 되었던 시간이었습니다.
코드리뷰

[공포의 코드리뷰. 공포의 대상은 팀원들이 아니라 제 코드라는 것을 알게 되었습니다]

나름 잘했다고 생각했는데, 막상 리뷰를 받아보니 개선투성이라는 것을 알게 되었습니다. 기본적인 객체지향 원칙도 지키지 못했고, controller와 service의 역할 구분에 대해서 명확하게 인지하고 있지 않다는 것도 알게 되는 등, 그동안 빠르게 배워오는 과정에서 어디에 구멍들이 있었는지에 대해서 금방 알 수 있었습니다.

슬램덩크-자신의 부족함을 아는 것이 그 첫 번째

특히 ‘왜 그렇게 한 것인가’에 대한 말들이 저를 많이 돌아보게 했는데요, 스스로 모르면서 사용하는 것들이 꽤 많다는 것을 알게 되었고, 모든 것을 알 수는 없지만 그래도 각각이 어떤 역할과 기능을 수행하는 것인지 알고 사용하고자 하는 태도가 필요하다는 생각이 들었습니다.

dependencies {
    compile('org.springframework.boot:spring-boot-starter-actuator')
    ...
    compile('org.hibernate:hibernate-java8')
    compile('pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0')
    ...
}

[Q. 이건 왜 넣은 거죠? A. 어디서 보고…]

상품과_옵션
[Q. 상품과 옵션을 연결하지 않을 특별한 이유가 있는가? A. …]

상품과 옵션을 연결하지 않은 것도 그 당시에는 나름 고민하고 야심 차게 결정한 것이었지만, 리뷰를 진행할 당시에는 그렇게 했던 이유가 떠오르지 않았습니다. 스스로 설계하고 코드를 작성하는 과정 이후 한 번 더 섬세하게 검증하는 시간을 가졌다면 더 잘 설명할 수 있었겠죠? 특히 의존성을 추가하는 것은 매우 신중해야 한다는 것도 알게 되었습니다.

@PostMapping
public ResponseEntity createOrder(..., @RequestBody ReserveOrderRequestDto reserveOrderRequestDto) {
        ...
        Order order = orderService.createOrder(reserveOrderRequestDto);
        orderService.updateDeliveryInfo(order, loginUser);

        ReservePayRequestDto reservePayRequestDto = null;
        try {
            reservePayRequestDto = new ReservePayRequestDto(order, ...);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        ReservePayResponseDto reservePayResponseDto = billingApiRequestSender.reservePay(reservePayRequestDto);

        orderService.updatePayInfo(order, reservePayResponseDto.getBillingTradeNo(), reservePayResponseDto.getPayAuthToken());

        log.debug("response", reservePayResponseDto);
        String responseUrl = reservePayResponseDto.getRequestUrl();
        return new ResponseEntity(new StringDto(responseUrl), HttpStatus.CREATED);
    }

[컨트롤러 단에서 주문 생성을 처리하는 함수]

개선점을 많이 지적받은 것 중 하나입니다. 현재 서비스 계층에서에서 트랜잭션으로 함께 처리되어야 할 Order 관련 함수들이 함께 묶여있지 않은 채로 컨트롤러 계층에 노출되어 있는데, 이 부분이 어떻게 개선돼 가는지에 대해서는 4,5주차: 리뷰 반영 및 회고에서 소개합니다.

3주차: 배포

3주차에는 기존에 로컬 환경에서만 실행되던 웹 애플리케이션을 AWS의 Elastic Beanstalk와 RDS, 그리고 Jenkins를 사용하여 실제 웹 환경에 배포하는 과정이 진행되었습니다. 이때는 1, 2주차와 달리 처음 해보면서 부딪히며 배워가는 것에 가까웠습니다. 그래서인지 더 배운 것이 많았습니다.

배포 과정을 시작하면서, 개발 과정과 달리 Jenkins와 RDS 세팅, Beanstalk은 전혀 경험해본 적 없으니 얼마만큼의 시간이 걸리는지, 계획은 어떻게 세워야 하는지 등에 대해서 전혀 감을 잡을 수 없었습니다. 자주 사용하게 될 AWS에 대해서도 익숙지 않았기 때문에 공부를 해야 감이 잡히겠다 해서 먼저 이에 관련된 공부를 시작했습니다. 하지만 이걸 알자니 저것도 알아야 하고 하는 식으로 지식이 너무 장황해지기 시작해서 고민하다가, ‘이러지 말고 일단 해보자!’ 하는 생각으로 마음을 바꿨습니다.

이에 따라 3주차는 일정 계획 없이 완료되는 대로 다음 순서를 진행하는 방식으로 진행되었습니다.

막상 시작해보니 Beanstalk이 많은 것들을 자동으로 해주어서 그런지, 배포환경을 구축하는 것 자체는 그리 오래 걸리지 않았습니다. 다만 RDS에서 한글 사용이 가능하도록 인코딩 설정을 다 했는데도 자꾸 한글이 깨지는 문제가 발생했습니다. 이 문제를 해결하는데 이틀 정도 걸렸는데, 알고 보니 Beanstalk에서 제공해주는 ec2 linux 환경의 기본 언어가 한국어로 되어있지 않아, Spring에서 한글이 포함된 쿼리문을 실행하는 과정에서부터 한글이 인식되지 않는 문제였습니다. 모두 �문자로 변경되어 있었는데, Beanstalk 콘솔에서 LANG=ko_KR.UTF-8 속성 하나만 추가하는 것으로 문제는 허무하게 해결이 되었습니다.

인코딩_문제
[한글을 보는 순간, 어찌나 반가왔는지 !]

진전이 없던 이 과정에서 로그와 환경변수들을 들여다보고, 여러 가능성을 생각해보고, instance와 DB 내부를 들여다보면서 이리저리 시도한 것이 이들과 가깝게 만들어줬고, 더 깊이 이해하게 되었다고 느낍니다.

  • 추가로, Beanstalk instance만 RDS에 접근하게 하기 위해서 security group을 새로 만들었는데, 항상 매뉴얼대로 따라 하던 종류의 일을 이번 기회에 이것이 무엇인지, 어떻게 사용하는 것인지 알게 되었습니다. security group 자체를 source로 지정하는 방식에 대해서도 알게 되었습니다.

  • 또한 instance가 2, 3개가 되기만 해도 배포 및 환경 업데이트를 하는 데 시간이 너무 많이 걸려서 작업이 느려지는 경향이 있었습니다. 실제 배포 과정에서 Beanstalk이 정말 느리다는 얘기가 왜 나오는지 알 것 같았습니다. 그래서 개발 및 테스트 과정에서는 1개로 줄이고 나중에 필요하면 늘리는 식으로 진행했습니다.

배포 리뷰

3주차 마지막 날에는 코드리뷰 때와 마찬가지로 배포에 대한 리뷰가 진행되었습니다.

  • Beanstalk과 같은 서비스에 너무 의존하지 않는 것이 좋다는 것 -> 설정 사항들을 가능하면 코드 레벨에서 관리하라
  • ddl-auto 설정은 무조건 none으로 한다
  • 우리 팀에서 자동배포를 하지 않는 이유
  • 위험을 최소화하기 위해서 배포는 단계적으로 되어야 한다
  • AWS는 이것 말고도 알아야 할 것이 많다!

등의 실제 서비스를 운영하는 개발자 관점에서 하는 이야기들을 많이 들을 수 있어서 유익했습니다.

4, 5주차: 리뷰 반영 및 회고

4, 5주차에는 앞서 받은 피드백들을 적용하고 전체 과정에 대해서 회고하는 시간을 가졌습니다. 이를 위해 알아야 하는 기술과 개념들을 공부하면서 코드를 개선했고, 어떤 리뷰를 받았으며 앞으로 어떤 공부를 해야 하는지, 무엇을 느꼈는지 등을 정리하는 시간을 가졌습니다. 본 포스팅도 그 과정의 일부입니다 🙂

앞에 소개했던 컨트롤러 단의 createOrder 함수가 코드리뷰를 통해 아래와 같이 개선되었습니다.

@PostMapping
    public ResponseEntity createOrder(..., @RequestBody ReserveOrderRequestDto reserveOrderRequestDto) {
        ...
        List<Option> options = optionService.getOptions(reserveOrderRequestDto.getOptionIdListDto().getOptionIdList());
        OrderDto orderDto = orderService.createOrderDto(reserveOrderRequestDto, options, loginUser);
        ReservePayRequestDto reservePayRequestDto = new ReservePayRequestDto(orderDto, ...);
        ReservePayResponseDto reservePayResponseDto = billingApiRequestSender.reservePay(reservePayRequestDto);

        orderService.createOrder(reserveOrderRequestDto, reservePayResponseDto, options, loginUser);

        log.debug("response", reservePayResponseDto);
        String responseUrl = reservePayResponseDto.getRequestUrl();
        return new ResponseEntity(new StringDto(responseUrl), HttpStatus.CREATED);
    }
  • 이전에 Order 객체를 만들고 업데이트해주었던 함수들을 모두 orderService.createOrder라는 하나의 함수로 합치고, 이를 결제 서버와의 통신이 완료되면 진행하도록 변경했습니다.
  • OrderDto를 만들어서 결제 서버에 전달하고, OrderOrderDto 각각의 생성을 위해 orderService.createOrder 안에 있던 optionService.getOptions 함수를 컨트롤러로 빼냈습니다.
  • 이에 따라 Order 객체의 생성 및 정보 업데이트는 한 트랜잭션으로 수행될 수 있게 됩니다.
...
List<Option> options = optionService.getOptions(reserveOrderRequestDto.getOptionIdListDto().getOptionIdList());
ReservePayResponseDto reservePayResponseDto = billingService.reservePay(reserveOrderRequestDto, options, loginUser, ...);
orderService.createOrder(reserveOrderRequestDto, reservePayResponseDto, options, loginUser);
...

이후 용근님으로부터 한 번 더 피드백을 받았습니다.

  • 연관성 있는 과정들을 BillingService 안으로 넣었습니다.

  • BillingApiRequestSenderBillingService 로 이름을 변경했습니다.

  • OrderDtoOrderService보다 BillingService에서 만드는 것으로 변경합니다.

  • 컨트롤러와 서비스들 사이의 역할 경계가 뚜렷해지고 의도가 명확해졌습니다.

이 외에도 QueryDSL을 이용해 여러 DB 테이블을 한 번에 읽어오거나 특정 칼럼만 읽어오는 식으로 성능을 높였고, 외부와 통신하는 과정에는 TimeOutException을 추가하는 등 실제 서비스하는 환경에서 필요한 부분들을 적용해볼 수 있었습니다.

이런 식으로 피드백을 받고 개선하는 과정들을 반복적으로 경험하는 것이 좋은 개발 능력을 쌓아가는 데에 참 많은 도움이 되는 것 같습니다.

들었던 생각들

먼저 이렇게 개발과 배포의 전 과정을 경험해볼 수 있고, 필요한 기술들을 업무시간에 집중적으로, 자유롭게 파볼 수 있는 흔치 않은 기회를 가질 수 있게 되어서 참 좋았습니다. 또한 리뷰를 받으면서 무엇이 부족한지 알고, 동시에 앞으로 성장할 일이 정말 많이 남아있다는 것에 다시 감탄했습니다.

한편으로는 개발자로서 공부할 것이 참 많은데, 이것들을 언제 다 배우나 싶기도 합니다. 지금은 이해하지 못해 미래의 과제로 남겨둔 것들도 있었는데요. 신입으로서 경험하는 모든 것들이 처음엔 생소하겠지만, 한 번씩 그 허들을 넘고 나면 비슷한 것들은 더 수월하게 배워나갈 수 있지 않을까 기대해 봅니다.

책도 여러 권 샀고, 스터디도 시작합니다. 앞으로도 빨리 지나갈 시간 속에서 가만히 시간을 흘려보낼 것인가, 계속 성장하면서 그 시간을 소유할 것인가 다시 돌아봅니다. 저도 머지않아 훌륭한 시니어가 되기를 바라며.ㅎㅎ

이제 진짜 시작!

파일럿 프로젝트를 마치고, 저도 이제 실무에 참여할 수 있게 되었습니다. 두 번의 우아한테크캠프를 거치고 이렇게 정식 우아한개발자가 되니 감회가 참 새롭네요. 이렇게 좋은 기회를 마련해주신 멋진 팀장님과 팀원분들께 다시 한번 감사, 앞으로 함께하게 될 분들께 미리 감사 인사드립니다!

슬램덩크-하이파이브