뒤로
김겨레
김겨레 ·

[사이드프로젝트 회고록] 이대 축제 부스 웹사이트 일주일 안에 배포하기 챌린지

때는 5월 1일, 나는 이화여대 중앙실전창업학회 UNIS의 정기모임에 참석했다. 그날은 종일 친구들과 연희동에서 놀고 학회에 간 상태라 아주 피곤하고 몽롱한 상태였다. 그리고 정기모임이 끝나고, 학회장 Y님이 내게 다가와 말씀하셨다.

겨레님, 혹시 다음주 축제(5/8)까지 저희 부스 웹사이트를 만들어주실 수 있을까요?

그때의 상황은 팀원도 구해지지 않았고, 하다못해 기획도 아직 나오지 않은 상황. 나는 일정을 확인하고 깨달았다. 일주일 간의 유사-해커톤이 내 앞에 다가오고 있었다고. 일주일 간, 나는 다양한 '챌린지'들을 겪으며 개발을 거쳤고 결국에는 배포에 성공했다. 오늘은 그 챌린지들에 대해 다뤄보고자 한다.

5628c7c364ad42e05e3d3f0b007000bc.png

Challenge A: 팀원을 구해라!

그때 우리의 상황은 기획을 맡아주실 학회장 두 분(Y님, M님), 백엔드를 맡을 나 이외에는 팀원이 구해지지 않은 상황이었다. 학회장 두 분께서는 UNIS의 알럼나이/현재 기수 멤버들에게 요청해보겠다고 하셨고, 나도 나 나름대로 팀원을 구해보겠다고 말씀드렸다. 그러나 나는 인맥이라는 게 거의 없었어서, 그냥 발품을 팔아가면서 팀원을 구하게 된다. 내가 취한 방식들은 다음과 같다.

  1. 영혼의 창업메이트 N님(@nacintosh)에게 디자인 해달라고 빌기

  2. 그냥 과 단톡에 제발 프로젝트 같이 해달라고 빌기

스크린샷 2024-11-02 오후 10.17.56.png

대강 이런 과정을 거쳤다. N님은 감사하게도 수락해주셨고, 부끄러움을 이겨내고 보낸 과 단톡에서는 아쉽게도 수확이 없었다(...)

하지만 다행히도! 학회장 두 분께서 프론트엔드 개발자 H님을 구해주신다. 아마 구해진 날이 5/2이었을 거다. 그러니까 프로젝트를 제안 받고 하루만에 인원이 다 구해졌던 것이다! 아주 다행이었다.

이렇게 팀원이 모였고, 우리는 해커톤사이드프로젝트를 시작하게 된다. 살면서 이렇게 정신 없는 일주일은 처음이었던 것 같다. (그러나 그 이후에도 더 바쁜 날들이 많이 발생하게 된다.)

웹사이트에서 구현해야 할 기능은 크게 세 가지가 있었다.

  1. 예약주문 기능

    1. 픽업 시간을 미리 정해둔다.

    2. 픽업 시간마다 슬롯 10개가 있다.

    3. 픽업 시간 30분 전에 예약주문이 닫힌다.

    4. 관리자는 관리자 페이지에서 예약 내역을 확인하고, 맛있는 파닭전을 만든다.

  2. 리뷰 기능

    1. 부스를 방문한 사람들을 대상으로 리뷰를 남길 수 있도록 한다.

    2. 맛있는 음식을 파니, 텍스트 이외에 사진!!도 남길 수 있도록 한다.

  3. 관리자 기능

    1. 예약주문한 사용자의 정보를 확인한다.

그때 백엔드 개발을 본격적으로 시작한 지 고작 4개월이었고, 이렇게 빠른 템포로 개발해 본 적이 없는 나에게는 볼드 처리된 부분들이 새롭게 공부해봐야 하는 부분들이었다. 새로운 챌린지였던 것이다.


Challenge B: 예약주문 기능을 구현해라!

스크린샷 2024-11-02 오후 10.32.50.png

예약 주문에 필요한 API는 두 가지가 있었다.

  1. 현재 예약 정보를 조회하는 API: 사용자들에게 예약 가능한 슬롯을 보여주기 위함

  2. 새로운 예약을 추가하는 API

나는 우선 예전 프로그래밍 수업 시간에 지겹도록 했던 좌석 예약 프로그램의 추억을 떠올리며 1번 API를 구현했다.

{
  "availabilityByTime": [1, 0, 1, 1, 1, ...], 
  "reservationList": [
    {
      "date": 3,
      "time": 4,
      "customerName": "김예약자" 
    },
    ...
  ]
}

프론트엔드로는 이런 응답이 가도록 했다. (지금 보니 고칠 게 많다) availabilityByTime은 각 슬롯의 예약 가능 여부를 1(True), 0(False)로 담은 것이고, reservationList는 타임슬롯의 날짜와 시간, 예약자의 이름을 담는다.

그리고 2번 API가 문제였는데, 나는 java의 LocalDateTime객체를 사용해서 30분 전까지만 주문이 들어오도록 하고, 등록에 실패하면 raw text만 보내는 미친 코드를 작성하게 된다(...)

// 새로운 예약 등록
    public Reservation addReservation(ReservationRequestDto reservationRequestDto) {
        // 1. 조건 검증
        if (reservationRequestDto.getCustomerPhone().equals("") || reservationRequestDto.getCustomerName().equals("")) return null;

        LocalDateTime currentTime = LocalDateTime.now();
        int date = reservationRequestDto.getDate() - 1;
        int time=4;
        if (reservationRequestDto.getTime().equals("AM 11:00")) time=0;
        else if (reservationRequestDto.getTime().equals("PM 12:30")) time=1;
        else if (reservationRequestDto.getTime().equals("PM 2:30")) time=2;
        else if (reservationRequestDto.getTime().equals("PM 4:00")) time=3;
        else return null;
        if (date>2 || date<0 || time==4) return null;
        if (currentTime.isBefore(compareTimes[date][time]) && reservationRepository.countReservations(date+1, time+1) < 10) {
            // 2. 엔티티 생성
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String createdAt = currentTime.format(formatter);
            Reservation reservation = new Reservation(
                    null,
                    createdAt,
                    reservationRequestDto.getDate(),
                    time+1,
                    reservationRequestDto.getCustomerName(),
                    reservationRequestDto.getCustomerPhone(),
                    reservationRequestDto.getMenu1(),
                    reservationRequestDto.getMenu2(),
                    reservationRequestDto.getMenu3(),
                    reservationRequestDto.getMenu4(),
                    reservationRequestDto.getMenu5()
            );
            reservationRepository.save(reservation);
            return reservation;
        }
        else {
            return null;
        }
    }

지금 보니까 보이는 것들이 있다.

  1. DTO 검증 안 깔끔한 거 무엇??

  2. if - else if로 time 변수 설정하는 거. ㅜ엇??

  3. (이건 컨트롤러 문제였지만) 실패 시 냅다 텍스트만 보내는 거 무엇??

성장했다는 뜻이니 다행으로 여겨야겠지만, 이런 코드를 디스콰이엇에 올려야 한다는 게 무척이나 통탄스럽다. 하여튼 나는 어떻게든 예약 주문을 구현하는 데 성공한다. 그리고 이제 하나의 적이 더 남아 있었다. 그건 바로....


Challenge C: 사진 업로드를 구현해라!

스크린샷 2024-11-02 오후 10.43.42.png

우리의 리뷰 작성에는 제목, 별점, 내용, (사진에선 잘렸지만) 닉네임, 그리고 이미지가 들어간다.

이미지?

나는 이런 거 할 줄 몰랐다. 지금까지는 간단한 것만 해왔기 때문이다. 그래서 나는 다양한 벨로그와 티스토리와 네이버 블로그와 AI의 도움을 받아서 대강 이미지 업로드 처리를 어떻게 해야 하는지 파악한다.

  1. AWS S3 버킷을 생성한다.

  2. 버킷 정보를 프로젝트 파일에 입력하고, github에는 올라가지 않도록 해 보안에 유의한다.

  3. 컨트롤러에서는 @RequestPart를 사용해 data와 file을 나눠서 받는다.

  4. data는 하던 대로 처리하고, file은 S3에 업로드하고 받을 수 있는 URL을 사용해 저장한다.

  5. 외래키를 사용하든, URL 발급 후 한 테이블에 저장하든, data와 file이 연결되도록 저장하면 끝!


지금은 시키면 자료 없이도 쉽게 개발할 수 있는 내용이지만, 당시의 나에게는 쉽지 않은 일이었다. 우선 S3이라는 것부터 생소했다.

S3은 '어디서나 원하는 양의 데이터를 검색할 수 있도록 구축된 객체 스토리지'로, aws에서 제공한다. 사진 파일 이외에도 다른 파일들을 '버킷'에 업로드할 수 있고, 버킷마다 고유한 이름을 갖고 있다. 그래서 보안이 중요하다. 관련된 자료는 S3 파일 업로드 백엔드라고만 검색해도 많이 나오니, 참고해주시면 좋겠다.

그 외에 내가 재밌게 읽은 아티클은 이런 게 있다.

https://techblog.woowahan.com/11392/

나는 이 작업을 하면서 @RequestPart 등 평소에는 사용하지 않는 어노테이션을 활용해야 했고, 그러면서 HTTP 통신에 사용되는 다양한 방식들도 익힐 수 있게 된다. S3이라는 편한 시스템이 있다는 것도 알게 되었다.

@PostMapping(value = "/reviews/add", consumes = {"multipart/form-data"})
    public ResponseEntity<?> addReview(@RequestPart(name="data") ReviewDto reviewDto, @RequestPart(name="file", required = false) List<MultipartFile> multipartFilelist) {
        List<ReviewImage> reviewImages;
        // 1. 리뷰 등록
        Review review = reviewService.addReview(reviewDto);
        if (review == null) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("리뷰 등록에 실패했습니다.");
        // 2. 리뷰 본문 사진 등록
        if (multipartFilelist != null) {
            reviewImages = s3Service.addReviewImages(review, multipartFilelist);
            if (reviewImages == null) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("사진 등록에 실패했습니다.");
        }
        // 3. 응답
        return ResponseEntity.status(HttpStatus.OK).build();
    }

당시에 사용한 컨트롤러 코드다. 나는 Review 엔티티를 먼저 만들고, 만든 Review 엔티티를 활용해서 이미지를 저장하고 있다. 나중에 데이터를 조회할 때는 Review를 먼저 불러오고, 붙어있는 사진들을 ReviewImages에서 불러오면 된다. 배열을 레코드 1개에 저장하는 과정은 복잡하므로, 이미지 n개: 게시글 1개 방식으로 매핑될 때는 이렇게 작업하는 게 편하다.

사실 이 작업을 처음 시작할 때에는 내가 이미지 업로드를 제대로 할 수 있을 거라고 생각하지 않았는데..., 기한이 짧으니까 어떻게든 하게 되는 것 같다.

개발자 꿀팁: 해커톤에 나가서 어려운 기능들을 만들어보자

여기까지 개발을 끝내고, aws ec2와 rds를 사용해서 배포 작업까지 끝나서 HTTP 주소가 나오자... 나는 내게 주어진 모든 챌린지들이 끝났다고 생각했다. 그게 아마 최종 배포 2일 전이었을 거다. 그리고 그때, 프론트엔드 H님께 연락이 온다.

겨레님, 혹시 배포 주소 HTTPS로 주실 수 있을까요? 프론트에서 연결하려니 보안 문제가 생깁니다!

IMG_0048.JPG

당시의 내 기분은 이랬다. (사진: 아일릿의 원희님인데 제가 개인적으로 좋아합니다)


Hidden Challenge: HTTPS 배포 주소를 만들어라!

스크린샷 2024-11-02 오후 11.01.55.png

여기서 잠깐, HTTP와 HTTPS의 차이는 무엇일까?

HTTP 메시지는 일반 텍스트이므로, 권한이 없는 당사자가 인터넷을 통해 쉽게 액세스하고 읽을 수 있습니다. 반면, HTTPS는 모든 데이터를 암호화된 형태로 전송합니다. 사용자가 민감한 데이터를 제출할 때 제3자가 네트워크를 통해 해당 데이터를 가로챌 수 없음을 확신할 수 있습니다.

aws에서는 둘의 차이를 이렇게 설명한다. 중요한 키워드는 '암호화'다. HTTP 주소는 암호화가 되어 있지 않기 때문에, HTTPS를 사용하는 프론트 서버(vercel 등)에서 연결하려고 하면 보안 문제로 액세스가 막힐 수 있다. 우회하는 방법도 있다고는 하지만, HTTPS로 백엔드까지 배포해야 깔끔한 건 사실이다.

그래서 나는 이때 처음 HTTP와 HTTPS의 차이를 공부하고, HTTP로 이미 배포되어 있는 내 서버를 가비아에서 구매한 도메인을 통해 HTTPS로 연결하는 데 성공한다. 이때의 나는 ACM이고 도메인이고 아무것도 모르는 상태였기에, 여러 자료를 찾아봤다. 그리고 지금까지도 많이 참고하고 있는 아티클은 아래 링크에 있다.

EC2 HTTPS로 연결하기 (1) - 도메인 구매하고 ACM 인증서 발급하기 :: 마음대로 개발 LIFE

백엔드 개발자분께서 Ec2를 HTTPS로 연결하는 방법을 다뤄주신 문서인데, 전 과정이 캡처본과 함께 올라와 있다. 나는 이걸 참고하면서 연결의 전 과정을 거쳤다. 중요한 것만 다루자면 아래 과정이 필요하다.

  1. 가비아에서 도메인을 발급받는다.

  2. ACM에서 인증서를 발급받아 (약 20분~n시간 소요) 도메인을 인증한다

  3. HTTP로 배포된 ec2 서버의 보안 그룹을 수정해, 도메인과 연결한다

  4. 로드 밸런서를 생성해서 HTTPS 도메인으로 들어오는 요청을 HTTP로 연결한다

  5. Health Check(서버의 상태를 주기적으로 확인하여 서버의 정상 작동 여부를 판단하는 과정) 성공 판정을 위해 상태코드 200을 반환하는 API를 만들어준다

  6. Health Check까지 성공하면 끝!

이런 과정이 있다. 가비아에서 판매하는 도메인은 가격이 다양한데, 짧게 서버를 열고자 하면 그냥 1년 할인가로 싸게 파는 도메인들을 구매하면 된다. 보통 나는 1900원짜리로 하나 산다.

이 과정을 공부하고 수행하기까지의 과정이 하루-이틀 정도 걸렸다. 사실 그때는 개념을 잘 이해하지 못하고 따라하는 느낌이 강했는데, 이때를 기점으로 항상 백엔드 배포 시에는 HTTPS 주소로 배포해왔어서 현재는 그때보다는 더 잘 아는 상태이다.

물론, 그렇다고 해도 아직 모르는 게 많아서 aws 개념을 중심으로 책을 찾아 공부해볼까 한다. 관련해서 어떤 책을 읽고 어떤 강의를 들으면 좋은지 아시는 분은 댓글 부탁드립니다...

여기까지 다 진행했을 때가 5/7 새벽이었다. 이 이후로 우리 팀은 QA를 진행하고, 5/8 서버 배포 후 갑자기 난 에러 (배열이 비어있을 때 처리를 어떻게 할지 정하지 않은 내 탓이었다... 죄송합니다...)를 해결하고, 새벽 3시 정도에 드디어 잠을 청할 수 있게 된다. 축제의 날이 밝아오고 있었다.


배포에 성공하다: Childhood's End

ChildhoodsEnd(1stEd).jpg

갑작스럽지만, 아서 클라크의 <유년기의 끝>이라는 명작을 읽어보신 적이 있는지 질문하고 싶다. 정말 괜찮은 SF인데, 내가 아직까지도 제일 좋아하는 작품으로 꼽는 소설들 중 하나다.

처음으로 '릴리즈'해 사용자를 제대로 받아본 경험이 이 'UNIS 주막'이었는데, 지금 회고록을 쓰면서 돌아보니 이때의 내가 배운 게 정말 많았다는 걸 다시금 느낀다. 책의 내용과는 전혀 상관이 없지만, 그리고 아직도 배워야 할 게 많지만, 내게 이 과정이 '백엔드 유년기'의 끝이라고 느껴졌기 때문이다. (책을 읽으신 분이라면 이 비유가 정말 웃기게 들릴 것 같다. 정말 제목만 따온 비유다.)

일주일 간 고생하면서 내가 배운 점들은 크게 이렇다. 기술적인 면들을 제외하자면 말이다.

  1. QA는 정말 중요하다!

    1. 당시에도 QA를 하면서 수정한 것들이 꽤 있었다. 제대로 테스트해보지 않고 배포하면 어떤 문제가 생길지 모른다.

  2. 백엔드의 꽃은 릴리즈!

    1. 릴리즈하기 전까지는 QA를 하면서도 몰랐는데, 배포한 이후에 문제가 발생했다.

    2. 그리고 실사용자를 받아보면서 어떤 점이 문제가 되는지, 어떻게 하면 코드를 효율적으로 짜는지 새로 배울 수 있었다.

  3. 아직 갈 길이 멀다!

    1. 여러 기술들을 새로 사용했지만, 그걸 정확히 이해하면서 활용한 건 아니었다.

    2. 구현은 누구나 한다. 이론적인 부분들을 채워나가야겠다.

정말 많은 걸 배웠기에, 백엔드 개발자로서 한 걸음 더 나아갈 수 있었다. 고작 이 프로젝트 이후 5개월이 지났는데도 코드를 보면서 바꿀 점이 보이고, 그때보다 더 많은 걸 알고 있다고 느끼기 때문이다.

나는 지금 백엔드 개발자로서 어디쯤 와있을까? 유년기가 아직 지속되고 있을지도 모르고, 어쩌면 청소년기에 이르렀을지도 모르겠다. 더 자라날 수 있도록 공부를 멈추지 말아야겠다. 성장할 여지는 아직 남아 있으니까.

1

댓글

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.