대학축제 프로젝트, Festino 회고록

bearMin·2024년 10월 7일

1. 프로젝트 소개

1) Festino

한국공학대학교의 축제 정보를 제공하는 서비스를 제작한 프로젝트이다!

이 프로젝트는 기획부터 개발, 배포까지 진행을 하였으며, 축제 기간동안 대략 3천명의 사용자와 8만회의 조회수를 기록한 많은 애정을 쏟았던 프로젝트이다!

Festino의 뜻은 축제를 뜻하는 Festival과 학교 마스코트의 이름인 Tino를 합친 단어로, 학교 축제 관련 서비스임을 나타내기에 아주 적합한 이름이라고 생각이 들어 팀원들의 투표로 인해 결정이 되었다.


2) 팀 구성

총 12명 = 프론트엔드 4명 + 백엔드 5명 + 디자인 3명

해당 프로젝트는 Dev-Tino 라는 개발 소모임에서 진행을 하였다. 팀장님께서 다른 학교의 프로젝트를 보고 아이디어를 제안하셨고, 이에 재밌을 것 같아서 해당 프로젝트에 참여하게 되었다.

따라서 개발은 Dev-Tino 교내 개발 소모임에서 활동하고 있는 프론트엔드 4명과 백엔드 5명이서 진행을 하였고, 조금 더 이 프로젝트를 제대로 해보자고 생각을 해서 디자이너 3명까지 추가로 영입을 하여 총 12명인 팀을 구성하였다.

큰 규모의 프로젝트를 처음 참여하다보니 누가 되지 않아야겠다는 마음가짐으로 임하게 되었으며, 다른 무엇보다도 너무 재밌을 것 같았다.


3) 일정

2024.06 ~ 2024.09 (약 3개월)

프로젝트는 2024년 6월부터 2024년 9월까지 약 3개월 정도의 기간을 두고 진행했다.

매주 일요일 개발팀 전체 대면 미팅을 통해 FE와 BE 각각 한 주 간의 진행상황을 공유하고 진행 도중 이슈가 있었다면 그 부분에 대해서 의견을 들으면서 점검을 하는 시간을 가졌다.

또한 Slack을 사용하여 FE와 BE 각각 Daily Report를 진행했다. 이를 통해 매일 다른 팀원들의 진행상황을 확인하고 문제점을 파악하여 개발 병목현상을 최대한 방지하려고 노력하였다.

학교 축제는 9월 11일부터 9월 13일까지 진행을 했기 때문에 적어도 8월까지는 서비스의 모든 부분의 정상 동작을 목표로 잡았다.

따라서 처음 계획은

  • 6월 말까지 세부 기획과 디자인을 마무리
  • 7월 말까지 개발을 진행하여 데모를 완성하고 피드백을 받기
  • 8월 말까지 피드백 바탕으로 리팩토링과 오류 찾고 수정

이었다.

이처럼 waterfall 개발 방법론으로 개발을 진행하였으나, 진행 도중 빠르게 수정사항을 도입해야하는 경우가 생겼고 waterfall 개발 방법론으로 진행을 하기에는 한계가 있다고 판단을 하였다.

따라서 8월 초부터는 Agile 방법론을 사용하여 전체 미팅 시 기획을 진행하고, 그 뒤에 개발 및 피드백을 진행하여 한 주에 하나의 기능을 완성하였다. 이렇게 Agile 방법론을 도입하여 개발을 진행한 결과 8월 말까지 모든 개발 일정을 마무리할 수 있었다!


2. 기획 및 설계

1) 레퍼런스

처음 팀장님이 아이디어를 생각해내셨던 것 처럼 이미 다른 학교에서는 축제 정보를 제공하는 서비스를 출시한 적이 있었다. 따라서 맨 처음 기획을 진행하기 전에 레퍼런스를 찾기 위해서 다른 학교들의 축제 정보 제공 사이트들을 찾아보게 되었다.

그 중, 가장 많은 참고를 했던 사이트는 동국대의 멋쟁이사자처럼이라는 코딩 연합 동아리에서 제작한 동국대학교의 2024 봄 대동제 사이트로 현재는 축제가 끝났기 때문에 많은 정보들을 확인할 수는 없었지만 정보 제공을 위해서 필요하다고 생각되는 기능들에 대한 자료조사로 많은 활용을 할 수 있었다.


2) 기획 회의

우리는 매주 일요일마다 대면 미팅을 진행하였다. 나중에는 개발만 하느라 정신이 없었지만 기획 당시에는 정말 많은 의견들을 주고 받았던 것 같다.

오후 3시에 시작해서 저녁 10시가 넘어서까지 계속 진행을 하였으며, 막차가 끊기기 전까지 회의를 진행했었다..

다른 프로젝트들에서도 기획을 하긴 했었지만 그때 했던 기획과는 다르게 세부 내용을 확실하게 잡고 가자는 마음가짐으로 많은 고민들을 했었던 것 같다.


3) 기능 확정

(1) 메인

타임테이블

축제 날짜별로 공연 정보를 확인할 수 있다.


부스 정보

학교 내에서 운영하는 부스들과 푸드트럭, 편의시설에 대한 정보를 확인할 수 있다.
이때 지도와 함께 제공하며, 상세보기 페이지에서 각 부스별 상세 정보를 확인할 수 있다.


테이블링

야간부스(주점)을 예약, 예약내역 조회를 할 수 있다.
(※ 특정 학과만 사용 가능)


주문

야간부스(주점)에서 테이블에 부착된 QR 코드를 통해 주문할 수 있다.
또한 주문한 내역을 조회할 수 있다.
(※ 특정 학과만 사용 가능)


(2) 어드민

부스 정보 관리

자신의 부스 정보를 추가, 수정, 삭제할 수 있다.
야간부스(주점)의 경우 테이블 번호를 커스텀 할 수 있다.


예약관리

야간부스(주점)에서 자신의 부스의 예약 정보를 관리할 수 있다.
예약 정보 관리와 더불어 문자 커스텀을 사용해 예약자에게 문자를 보낼 수 있다.


주문 관리

야간부스(주점)에서 주문을 받았을 시 해당 내역을 관리할 수 있다.
서비스 주문시 관리자가 직접 주문을 추가할 수 있다.


3. 개발

1) 개발 환경

FE : Vue.js
BE : Spring Boot
DB : MariaDB

프론트엔드는 Vue.js를 백엔드는 Spring Boot를 사용해서 개발하였다. 또한 Docker를 사용하여 개발 환경을 맞추고 배포를 진행하였다.

Vue.js와 Spring Boot를 사용한 이유는 Dev-Tino에서 프로젝트와 더불어 멘토링을 진행하고 있었는데, 이때 프론트엔드에서는 Vue.js를 사용하여 백엔드에서는 Spring Boot를 사용하여 멘토링을 진행하고 있었기에 가장 익숙하고 잘 사용할 수 있는 것을 사용하자는 의견을 반영하여 각각의 프레임워크를 선정하게 되었다.

프론트엔드에서 사용한 프레임워크는 Vue.js이다.

기획부터 데모 완성까지 빠르게 진행을 해야했기 때문에 새로운 프레임워크를 도입하기엔 무리라고 판단을 했다. 따라서 모두 사용해본 경험이 있는 Vue.js를 사용했다. 다만 반응형 구현을 위해 tailwind와 상태관리를 위해 pinia를 도입하면서 기존에 개발했던 방식보다 더 편리하게 개발을 할 수 있었다.


백엔드에서 사용한 프레임워크는 Spring Boot이다. Dev-Tino의 BE 팀원들 모두 Spring과 Spring Boot에 대한 공부를 진행하는 중이었다. 이번 프로젝트를 통해 공부한 내용을 직접 사용해보자는 생각에 선정하게 되었다. 또한 개발 일정이 넉넉하지 않았기 때문에 Spring을 사용하여 모든 초기 설정들까지 공부하면서 진행하기에는 기한 내에 개발을 못 끝낼 수도 있겠다는 의견도 있었다.

때문에 Spring Boot를 사용하여 개발을 진행하게 되었다.


데이터베이스는 MariaDB를 사용하였다.
RDB를 사용한 이유는 다음과 같다.

  1. 트랜잭션 처리
    : 예약과 주문을 관련해서는 중간에 오류가 발생하면 전체 작업을 롤백하여 데이터의 일관성을 유지하는 트랜잭션 처리가 필요하기 때문이다.

  2. 구조화된 데이터
    : 테이블 간의 관계를 설정하여 데이터의 효율적인 저장과 중복의 최소화를 유지하고, 데이터의 관리와 유지보수 측면에서 이점이 있기 때문이다.

  3. 데이터 무결성
    : 예약과 주문의 기능 관련해서는 특히나 더 정확하고 일관된 데이터 관리가 필수적이다. 이를 위해서 ACID 특성을 보장하는 RDB를 사용하였다.


2) 개발 진행

해당 프로젝트에서는 Server를 2개로 분리해서 사용을 하였다. 사용자에게 축제 관련한 정보를 제공하고 주점을 예약하거나 주점에서 주문을 할 수 있는 기능을 담당하는 Main Server와 각 주점에서 예약과 주문을 관리하고, 부스의 정보를 수정할 수 있는 Admin Server로 구분이 되어있다. Server를 2개로 분리해서 사용한 이유는 다음과 같다.

  1. 운영 안정성
    : 만일 Server가 1개로 사용이 되어있을 경우 Server가 장애를 일으킨다면 모든 서비스의 사용이 중지가 된다. 하지만 주점의 특성상 Server가 장애를 일으킬 경우 그 기간동안 아무런 진행이 불가능하기 때문에 운영에 안정성을 더하고자 Main Server와 Admin Server 2개로 분리하여 개발을 진행하였다.

  2. 확장성
    : 위의 운영 안정성과 이어서 필요한 경우 Server의 확장이 있어야하는데 이때 각각 Server를 확장할 수 있어 리소스의 낭비 없이 필요한 부분만 Scaling을 진행할 수 있다.

  3. 보안 강화
    : 실제 사용자들이 예약과 주문의 기능을 할 때 본인의 이름과 휴대폰번호를 제공한다. 이는 개인정보이기 때문에 외부에 누출이 되어서는 안된다. 따라서 주점의 관리자들과 개발진만 접근할 수 있도록 하였다. 또한 Main Server에서 발생하는 보안 위협이 Admin Server로 확산이 되는 것을 방지하기 위해서이다.

이중에서 나는 Main의 예약과 주문, Main과 Admin의 전반적인 보안 부분을 맡아서 진행하였다.

(1) 예약

// 예약 등록
public ResponseReservationSaveDTO exec(RequestReservationSaveDTO requestReservationSaveDTO) throws IOException {
   // 부스가 운영중인지, 예약가능한지 여부 확인하는 예외 처리
   NightBoothDAO exceptionBoothDAO = getNightBoothDAOBean.exec(requestReservationSaveDTO.getBoothId());
   if(!exceptionBoothDAO.getIsOpen() || !exceptionBoothDAO.getIsReservation()) return null;

    // 이전 예약 기록이 있을 경우 예약이 불가능
    // 예약은 전화번호, RELEASE 기준으로 함
    // 전화번호, RELEASE 가 같은 경우 기존 예약을 취소하고 진행함
    ReservationDAO reservationDAO = getReservationByPhoneNumDAOBean.exec(requestReservationSaveDTO.getPhoneNum());

    // 이미 있는 경우 덮어쓰기
    if(reservationDAO != null) {
        reservationDAO.setReservationType(ReservationEnum.CANCEL);
        reservationDAO.setUpdateAt(DateTimeUtils.nowZone());

        NightBoothDAO oldNightBoothDAO = getNightBoothDAOBean.exec(reservationDAO.getBoothId());
        oldNightBoothDAO.setTotalReservationNum(oldNightBoothDAO.getTotalReservationNum() - 1);

        saveReservationDAOBean.exec(reservationDAO);
        saveNightBoothDAOBean.exec(oldNightBoothDAO);
    }

    // 야간부스 전체 예약수 관리를 위해 야간부스 정보를 가져옴
    NightBoothDAO nightBoothDAO = getNightBoothDAOBean.exec(requestReservationSaveDTO.getBoothId());
    if (nightBoothDAO == null) return null;

    // 예약 당일이 언제인지
    Integer date = checkReservationDAODateFieldBean.exec(nightBoothDAO);
    if(date == null) return null;

    // 예약을 등록한 뒤 reservationId 반환
    ReservationDAO createReservationDAO = createReservationDAOBean.exec(date, requestReservationSaveDTO);
    if (createReservationDAO == null) return null;

    nightBoothDAO.setTotalReservationNum(nightBoothDAO.getTotalReservationNum() + 1);

    saveReservationDAOBean.exec(createReservationDAO);
    saveNightBoothDAOBean.exec(nightBoothDAO);

    // 메세지 전송
    String check = saveReservationSendMessageBean.exec(requestReservationSaveDTO.getBoothId(), createReservationDAO.getPhoneNum(), createReservationDAO.getUserName());

    return ResponseReservationSaveDTO.builder()
		            .reservationId(createReservationDAO.getReservationId())
		            .messageStatus(check)
		            .build();
}

예약의 정책은 한번에 하나의 주점만 예약이 가능하며 해당 부스를 입장하거나 예약이 취소될 경우 다른 주점의 예약이 가능하다.

또한 이미 예약이 된 상황에서 다른 주점의 부스를 예약한다면 기존에 예약을 헀던 주점의 예약이 사라지고 새로 예약이 된다.


해당 부분을 구현하기 위해서 예외처리에 많은 신경을 썼다.

예약을 하려고 하는 부스의 운영여부가 true이면서 기존에 예약한 내역이 없다면 예약이 가능하다.

이전에 예약 내역이 있는 경우 덮어쓰기를 진행하고, 대기 순번을 알려주기 위해서 주점의 대기 순번 필드를 조회한다.

또한 예약은 축제 기간 동안만 가능하도록 설정을 해두었다. 따라서 현재 시간을 불러와서 축제기간이 아니라면 예약을 받지 않는다.

이후 예약 id를 반환해주는데, 이때 예약이 성공을 했다면 예약자에게 예약을 성공했다는 메시지도 전송을 하게 된다.


메시지의 전송은 Gavia를 사용하였다. 예약 성공 여부를 예약을 하고 사용자가 직접 조회를 하는 것은 자신이 예약이 되었는지도 잘 모를 뿐더러 사용감 측면에서도 좋지 않다는 의견에 따라 메시지까지 같이 보내주는 것으로 결정하였다.

이때 카카오톡으로 보내주는 방법도 역시 존재했으나 카카오톡으로 알림을 받게 하기 위해선 사업자가 필요했기 때문에 사용하기에 적합하지 않았다. 따라서 메시지를 전송하는 방식을 선택하게 되었다.

또한 테스트 진행시에 메세지를 전송하고 해당 메세지가 전송이 되는 응답 과정이 오래 걸리는 경우가 있었다. 요청을 보냈을 시에 바로 응답이 오지 않으면 문제가 생겼다고 판단을 하고 다시 예약을 진행할 수 있을 것 같다는 의견이 존재했다. 이를 위해 메세지가 전송이 되는 동안 대기 중 화면을 띄워주어 사용자 경험을 개선하였다.


(2) 주문

// 주문 등록
public UUID exec(RequestOrderSaveDTO requestOrderSaveDTO) {

    // 부스 정보 조회
    NightBoothDAO nightBoothDAO = getNightBoothDAOBean.exec(requestOrderSaveDTO.getBoothId());
    if (nightBoothDAO == null) return null;

    // 부스가 닫혀 있거나 주문 불가할 경우 주문 등록 실패
    if (!nightBoothDAO.getIsOpen() || !nightBoothDAO.getIsOrder()) return null;

    // 날짜 조회
    Integer date = checkOrderDAODateFieldBean.exec(nightBoothDAO);

    // 주문한 학과
    String adminName = getOrderBoothNameDAOBean.exec(requestOrderSaveDTO.getBoothId());

    // 주문한 학과가 없다면 주문 등록 실패
    if(adminName.isEmpty()) return null;

    // 주문 정보 생성
    OrderDTO orderDTO = createOrderDAOBean.exec(date, requestOrderSaveDTO);

    // 주문 정보 저장
    saveOrderDAOBean.exec(adminName, orderDTO);

    return orderDTO.getOrderId();
}

주문은 주점별 각 테이블에 QR 코드를 배치하였다. 사용자는 해당 QR 코드를 통해서 사이트에 접속을 할 수 있고 주문의 기능을 사용할 수 있다.


주문 관련해서도 예외처리가 매우 중요했다.

  1. 해당 부스 정보가 있는지
  2. 부스의 운영중 여부가 true이면서 주문 기능 사용 여부가 true인지
  3. 주문한 학과가 어디인지

이렇게 크게 3가지의 예외처리를 진행하였다.


주문이 된 뒤에는 주점 관리자가 admin을 사용하여 관리한다. 맨 처음 주문이 들어갔을 때는 입금 대기 상태이며, 입금 내역을 확인한 후 조리 중으로 넘기거나 내역이 확인되지 않아 주문취소로 넘길 수 있다.


또 하나의 특이한 점은 주문은 학과별로 Table을 분리했다는 점이다.

주문의 동작 방식은 다음과 같다.

  1. 사용자가 주문을 한다.
  2. 관리자가 입금 내역을 확인하고 해당 주문의 상태를 입금 대기에서 조리 중으로 변경한다.
  3. 조리 중으로 넘어오게 되면 메뉴별로 Cook Table에 데이터를 넣는다.

이처럼 주문을 사용할 때에는 Order와 Cook이라는 두 테이블 간의 관계가 설정이 되어있다. 이 Order에서 Cook으로 변환하는 부분에 대해서는 Admin에서 구현이 되어있다.


이때 Order와 Cook을 모든 학과가 같은 테이블을 사용하게 될 경우 한 테이블에 너무 많은 데이터들이 몰리게 되고 이는 성능 측면에서도 좋지 않고, 실제 관리가 어려워질 수 있다는 문제점도 있었다. 이를 해결하기 위해서 학과별로 테이블을 분리하여 따로 관리를 진행하였고, 그 결과 성능의 측면에서도 관리 측면에서도 만족스러운 성과를 낼 수 있었다.


예약도 주문과 동일하게 했나❓

예약은 주문과 달리 테이블 간의 밀접한 관계가 없었으며, 오히려 분리를 할 경우 그에 따른 비용 측면에서 더 손해가 발생할 것이라는 생각이 있었다. 따라서 예약은 테이블을 분리하지 않고 하나의 테이블로 관리하기로 결정하였다.


(3) 보안

Admin 서버와 Main 서버에서 적용한 보안규칙이 다르다.

Main 서버에는 로그인 기능이 존재하지 않는다. 따라서 사이트의 주소를 알고 있다면 누구나 접속이 가능하다. 하지만 주점의 예약과 주문의 기능을 사용하기에 해당 환경은 적절하지 않다. 그 이유는 CSRF를 방지할 수 없기 때문이다.

💡CSRF란?
웹 보안 취약점의 일종이며, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 
웹 애플리케이션에 요청하게 만드는 공격

따라서 이 CSRF를 대응하기 위해서 CSRF Token을 제작하였다. 동작방식은 다음과 같다.

  1. 유저가 웹페이지를 접속하면, 고유 UUID를 발급하여 Cookie에 저장
  2. 주문/예약 요청 전에 토큰 발급 API 호출
  3. 토큰이 포함되어서 요청이 오면, 토큰 만료

이때 Token의 만료는 따로 처리를 진행하지 않고, 만료 시간을 1분으로 설정하는 것으로 대체하였다. 너무 짧게 설정할 경우 인증을 하는 중간에 만료가 되어 요청을 처리하지 못할 가능성이 존재하기 때문이다. 따라서 Token을 발급받은 뒤 바로 주문이나 예약의 요청을 진행하는 시간과 사람이 많은 야외임을 고려하여 네트워크의 속도를 따져봤을 때 1분이 가장 적절하다고 결론이 지어졌다.


이뿐만 아니라 무차별 요청 공격에 대비를 하기 위해서 예약은 1초에 100번의 rate limit을, 주문은 1초에 3번의 rate limit을 설정해두었다. 해당 코드의 구현은 Bucket4j 라이브러리를 사용하여 구현하였다.

dependencies {
	// Rate Limiting
	implementation 'com.bucket4j:bucket4j-core:8.3.0'
}
<// Reservation Rate Limiting: 1초에 100개 허용
private Bucket resolveReservationBucket() {
    Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofSeconds(1)));
    return buckets.computeIfAbsent("reservation", k -> Bucket.builder().addLimit(limit).build());
}

예약은 1초에 최대 100번을, 주문은 1초에 최대 3번까지의 호출이 가능하도록 설정하였다. 해당 수치는 기능을 사용하는 학생회와의 논의 후 정하게 되었다.

대략 재학생 7000명을 기준으로 주점 예약을 진행한다고 하였을 때 동시에 예약을 누를 수 있는 최대 수치가 100회 이내라고 생각을 하였으며, 주점의 테이블 수와 그동안 진행했던 경험을 토대로 주문은 최대 1초에 3회로 설정하게 되었다.

해당 설정을 진행한 뒤 첫번째날 모니터링에서 문제없이 동작이 되는 것을 확인하였다. 다만 축제 당일 날씨가 좋지 않았던 관계로 주점을 찾는 사람들이 많이 없었고 그로 인해 예약 건수는 예상보다 많이 없었다. 때문에 1초에 100번보다 조금 덜 설정을 했어도 괜찮았겠다는 생각을 하였다.


Admin 서버에는 로그인의 기능이 존재한다. 다만 이 유저 관련해서는 권한을 제공하지 않고 개발진들만 사용할 수 있도록 하였으며 각 주점별로 계정을 생성해서 배부하였다. 때문에 회원가입의 기능은 OAuth와 같은 방식을 사용하지 않고 기본적인 사이트의 회원가입 기능으로 구현하였다.

실제 동작 방식을 살펴보면 로그인을 할 때 클라이언트에서는 id, pw를 Base64로 인코딩한 값을 서버로 넘겨준다. 서버에서는 해당 값을 디코딩하고 로그인의 성공 여부를 확인한다.

이때 패스워드는 BCrypt라는 라이브러리를 사용하여 암호화한 뒤 DB에 저장하였고, DB에 저장된 값은 복호화가 불가능하다. 따라서 클라이언트에서 넘어온 값이 암호화된 값과 일치하는지 비교할 때는 BCrypt 라이브러리 중 checkpw를 사용하여 해당 비밀번호가 일치하는지 확인하였다.

// 암호화된 비밀번호를 확인했을 때 일치한다면 true, 다르다면 false
boolean flag = BCrypt.checkpw(passWord, userDAO.getPassWord());

이후 로그인이 성공하게 된다면 Token을 발급하고 Token은 Cookie에 저장이 된다.


Token은 Access Token과 Refresh Token 2가지로 구성이 되어있으며, Access Token은 1시간의 만료시간을, Refresh Token은 24시간의 만료시간을 가지고 있다. Access Token에는 사용자의 고유 아이디와 role이 들어가있고 Refresh Token에는 사용자의 고유 아이디가 들어가있다.

실제 주점에서 사용하는 관리자들이 Admin 기능을 활용하기 위해서는 로그인이 필수이다. 그 이유는 모든 기능을 사용하기 전 동작 방식이 Access Token에 있는 Role을 확인하여 해당 기능을 사용할 수 있는지 여부를 확인한 뒤에 동작을 할 수 있기 때문이다.

이처럼 Admin에서는 Token과 Role의 확인을 통해 많은 보안 규칙을 생성할 수 있었다.


3) 개발 중 생긴 이슈

(1) Main과 Admin 테이블 일치

서버는 2개로 나뉘어져 있지만 테이블은 하나로 공유를 하고 있기 때문에 Main을 담당하는 팀원들과 Admin을 담당하는 팀원들 간의 소통이 매우 중요했다. 테이블명과 해당 테이블의 필드가 일치하지 않는다면 로직에 문제가 없다고 해도 결과값이 예상과 달라질 수 있기 때문이다.

실제로 테이블명과 테이블의 필드가 맞지 않아서 동작을 진행했을 때 DB를 확인해봤을 때 자꾸 이상한 필드가 추가되어있는 것을 발견하게 되었고, 문제를 찾아보니 Admin과 Main에서 모두 사용하지 않는 필드가 지워져있지 않아서 생기는 것이었다. 이처럼 Main과 Admin에서 테이블명과 필드명이 일치하지 않아 생기는 문제들을 미연에 방지하기 위해 기획 단계에서 DB 구성과 API 명세서를 작성을 해두었지만, 개발을 진행하는 도중 필드를 추가하거나 주문 테이블을 분리하게 되면서 기존의 내용을 건드려야하는 경우가 발생했다. 이때는 팀원들과 상의를 하면서 일치화를 먼저 한 뒤에 작성을 하여 위와 같은 문제가 발생하지 않도록 진행하였다.


(2) Security

사용자들이 실제로 사용을 하는 것이다보니 보안 관련해서 신경을 매우 많이 쓸 수 밖에 없었다. 또한 Login 관련해서 Spring Security를 사용해야했다.

다만, Spring Security를 처음 다루다보니 많이 미숙했고, 시작부터 어떻게 해야할지 막막했다. 하지만 Spring Security 관련한 레퍼런스들은 매우 많이 나와있다보니 우선 레퍼런스들을 찾아보면서 어떻게 구현을 해야할지 생각을 해보려고 했다.

그러나 레퍼런스들과 공식 사이트들을 찾아보면서 알게 된 것이 있다. 2024년 5월을 기준으로 버전업이 되었다. 이 버전이 바뀌면서 기존 코드와 변한 것들이 있다. 하지만 이를 다루는 레퍼런스들이 적기 때문에 참고할 만한 자료들이 적었고 기존의 코드로 작성을 해보려고 하자 많은 오류들이 발생하기도 하였다.


하지만 포기하지않고 최대한 최신 버전을 사용한 레퍼런스를 찾아서 해당 내용을 익히기 위해서 많이 읽어보았다. 특히 SecurityFilterChain 부분에서 아주 미세하게 기존의 코드와 변한 점이 많다는 것을 알게 되었고 해당 부분을 수정해가면서 오류들을 잡아낼 수 있었다.

// 이전 버전
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      http
              .httpBasic().disable() // HTTP 기본 인증 비활성화
              .cors().configurationSource(corsConfigurationSource()) // CORS 활성화
              .and()
              .csrf().disable() // CSRF 비활성화 (JWT 사용 시 주로 비활성화)
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 비활성화 (JWT 사용 시)
              .and()
              .authorizeRequests() // 인증 절차 설정 시작
              .antMatchers("/api/v1/img/").permitAll() // 특정 경로 허용
              .antMatchers("/api/v1/auth/n/").permitAll() // 인증 없이 접근 가능한 경로
              .antMatchers("/api/v1/auth/y/**").hasAnyAuthority("ROLE_USER", "ROLE_TT_ADMIN", "ROLE_ST_ADMIN") // 권한이 필요한 경로
              .anyRequest().authenticated() // 나머지 요청은 인증 필요
              .and()
              .formLogin().disable() // 기본 로그인 폼 비활성화
              .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) // JWT 인증 필터 추가
              .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class); // JWT 예외 처리 필터 추가
      return http.build();
}
// 최신 버전
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
      return httpSecurity
              .httpBasic(AbstractHttpConfigurer::disable) // HTTP 기본 인증 비활성화
              .csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화
              .cors(AbstractHttpConfigurer::disable) // CORS 비활성화 (필요한 경우 설정 가능)
              .authorizeHttpRequests(requests -> {
                  requests.requestMatchers("/api/user/login", "/api/user/join").permitAll(); // 인증 없이 허용되는 경로
                  requests.requestMatchers(HttpMethod.POST, "/api/articles").authenticated(); // 인증 필요한 경로
              })
              .sessionManagement(sessionManagement -> 
                      sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 관리 설정
              .build();
}

특히 가장 많이 바뀐 SecurityFilterChain를 살펴보면

  1. authorizeRequests -> authorizeHttpRequests
    : 새로운 방식에서는 authorizeHttpRequests를 사용해 요청별로 인증 및 권한을 설정
  2. 람다 표현식 사용
    : 새 버전에서는 authorizeHttpRequests, sessionManagement 등 대부분의 설정이 람다 방식으로 이루어짐
  3. AbstractHttpConfigurer 사용
    : httpBasic().disable(), csrf().disable() 같은 설정들이 AbstractHttpConfigurer::disable로 표현되며, 더 모듈화된 설정이 가능
  4. 필터 설정 위치
    : addFilterBefore 같은 필터 추가 부분은 그대로 유지되나, 간소화된 구조로 변형 가능

이러한 내용들을 위주로 변경이 되었다.

변경된 내용들을 차근차근 공부하면서 개발을 진행하였고, 주어진 스프린트 기간 안에 해당 개발을 끝낼 수 있었다.

지금 생각해보면 Spring Security를 다뤄본 적이 없었기 때문에 오히려 처음부터 차근차근 쌓아나갈 수 있었던 것 같다.


4. 협업

1) 진행과정

우리가 웹사이트를 만들었다고 끝이 아니었다. 축제 특성상 정보들을 보여주어야하는데 이 정보제공 동의를 받기 위해서는 축제 기간 동안 부스를 여는 동아리들과 주점을 여는 학생회, 축제를 주관하는 총학생회, 외부에서 축제 기간에만 오시는 푸드트럭 등 많은 외부와의 협업이 필요했다.

따라서 해당 프로젝트를 기획할 때부터 각 학과 학생회와 총학생회에 협업을 진행하고 싶다고 문의를 드렸고, 미팅을 잡아서 많은 얘기들을 나누게 되었다.


부스를 운영하는 동아리들은 동아리의 공식 인스타 혹은 지인의 인맥을 통해 직접 연락을 취하였다. 다만 축제 기간이 가까워져서 어떤 동아리들이 부스를 운영하는지 알 수 있었기 때문에 축제 전에 정보를 올리기 위해서 빠르게 연락이 필요했고, 부스를 운영하는 모든 동아리들에게 정보제공 동의를 받을 수 있었다.

푸드트럭의 경우에는 미리 연락을 할 방법이 없었다. 때문에 축제 첫날 아침에 푸드트럭을 직접 돌아다니면서 정보제공 동의를 구했고 사진과 메뉴의 정보를 빠르게 작성하여 오픈 전에 마무리 지을 수 있었다.


2) 협업 중 생긴 이슈

(1) 총학생회와 협업 무산

축제 관련 정보들을 웹사이트에 빠르게 업데이트하기 위해서는 총학생회와의 협업이 중요했다. 때문에 기획을 진행하고 기획된 내용을 바탕으로 총학생회와 미팅을 진행하여 협업 관련한 의사를 물어봤고, 총학생회와 많은 이야기를 한 뒤에 그 대답을 들을 수 있었다.

결론은, 무산이었다..


이유는 우리 학교에서는 기존에 사례들이 없었고, 첫 시도였기 때문에 기획만 가지고는 총학생회가 가지고 가야할 리스크가 너무 컸다. 그 예로 우리는 교내 정식 동아리가 아닐뿐더러 기획한대로 프로젝트가 나오지 않았을 경우, 혹은 진행 도중 프로젝트에 문제가 생겼을 경우 총학생회측이 부담을 해야하는 문제들이 생기기 때문이었다.

따라서 이번에는 총학생회와의 협업 없이 우리끼리 프로젝트를 진행하기로 하였고, 이 프로젝트는 내년에도 발전시키고 진행할 계획이 있었기 때문에 다음에 총학생회와의 협업을 추진해보기로 하였다.

이렇게 총학생회측과 협업이 무산이 되면서 생긴 문제점들이 몇몇 존재했다.

  1. 총학생회와 무관한 프로젝트임을 명시
  2. 총학생회가 주관하는 부스 관련 정보들에 대해선 올리지 않기
  3. 연예인 정보 관련해서 올리지 않기

총학생회와의 협업이 무산이 되었으나 프로젝트는 그와 상관없이 계속 진행하기로 하였고, 잘 모르는 재학생들은 해당 프로젝트를 총학생회와 협업하여 진행한 것으로 착각을 할 수 있었기 때문에 총학생회와 무관한 프로젝트임을 명시해주어야했다. 또한 총학생회 관련 정보들은 올리지 않기로 하였고 연예인 정보 관련해서는 총학생회측이 계약한 부분이 있었기 때문에 마찬가지로 올리지 않기로 하였다.

이와 관련해서 두번째 문제가 생기기 시작했다.


(2) 기능의 추가

협업의 무산은 기획 이후에 확정이 났지만, 프로젝트를 진행하기로 하였고 해당 프로젝트의 홍보를 위해서 총학생회측과 이야기를 하던 중 우리가 올리면 안되는 정보들에 관해서 얘기를 듣게 되었다. 충분한 협의 끝에 프로젝트에서 올리기로 했었던 총학생회가 운영하는 부스 정보들과 연예인 정보 관련해서는 총학생회 인스타로 넘기기로 하였다.

때문에 실제 배포 전에 급하게 기능들을 수정해야했고, 특히 연예인 정보를 제공하는 기능 부분에서는 추천곡이라는 기능을 아예 사용할 수 없게 되었다.

기능을 사용하지 못하는 부분에 대해서는 수정은 간단하였지만 열심히 만든 기능을 사용하지 못하게 되는 부분에 대해서는 매우 안타까웠다..


또한 주문이나 예약 관련해서 각 학생회들과 미팅을 진행했을 때 각 학생회측에서 요구사항들이 있었고, 이는 기획 단계에서 없었던 부분이기에 추가 기획이 필요한 상황이었다. 다만, 해당 요구사항들이 한번에 나온 것이 아닌 각 학과에서 사용 → 요구사항 → 반영 후 다시 사용 → 요구사항 과 같은 굴레가 계속 되었다.

이 때 들어온 요구사항들로는 테이블 번호의 커스텀, 예약 관련 문자 메세지의 커스텀 등이 있었고, 개발팀 내에서 보안 관련해서 추가사항들이 존재했다. 이러한 부분들은 팀원들 모두 필요하다고 생각하기도 하였고, 개인적으로는 완벽한 프로젝트로 발전을 시키기 위해 욕심이 났다. 따라서 우리는 추가사항들을 반영하여 기능들을 수정하거나 추가하게 되었다.


때문에 진행하던 개발의 속도를 조금 더 높일 필요가 있었다. 하지만 우리는 waterfall 방법론을 사용하고 있었기에 개발 속도에 한계가 존재했다. 이를 위해서 우리는 Agile 개발 방법론을 도입하여 요구사항이 들어온다면 일주일 안에 개발을 끝낼 수 있도록 스프린트를 짜고 수행하였다.

디자인팀과 개발팀들 간의 소통이 매우 중요했고 특히 개발팀들의 연락은 밤이고 낮이고 끊이지 않고 계속되었다.. 하지만 그렇게 고생한 덕분에 개발 속도에는 차질이 없이 진행될 수 있었다.


(3) 연락이 안되는 부스들

축제 관련 정보를 제공해야하지만, 실제 개발은 예약과 주문 등의 각 학생회측과 협업하여 진행하는 기능들에 조금 더 치중해있었다. 또한 총학생회와의 협업이 무산이 되었기 때문에 각 학생회가 운영하는 주점을 제외한 다른 부스들에 대한 정보는 알 수가 없었다.


그래서 우리는 총학생회 인스타를 통해 올라온 정보들을 수시로 확인하면서, 정보 제공 동의를 위해 부스를 운영하는 동아리들과 컨택을 진행하였다. 프로젝트의 소개와 어떠한 방식으로 정보가 들어가는지, 정보 제공을 해주실 수 있는지에 대한 이야기를 나누었고, 홍보를 위해서 대부분의 동아리들은 긍정적인 답변을 해주셨다.

다만, 축제가 거의 다가와서 컨택을 진행해야했기 때문에 부스 준비로 바쁜 몇몇 동아리들은 축제 당일까지도 연락이 되지 않았고, 축제 전에 모든 부스의 정보들을 올리진 못하였다.


또한, 푸드트럭의 경우에는 축제 당일이 아니라면 미리 확인할 수 있는 방법이 없었기에 이 역시도 축제 전에 모든 정보들을 올릴 수 없었다.

따라서 연락이 되지 않는 동아리들과 연락을 취할 방법이 없었던 푸드트럭들은 축제 당일 아침에 직접 발로 뛰어서 연락을 취하였고, 그 자리에서 정보 제공 동의를 받았다. 정보 제공 동의를 받은 뒤에는 빠르게 데이터를 넣어서 최대한 빠른 시간 내에 프로젝트를 완성시킬 수 있었다.


5. 서비스 출시

1) 출시 및 홍보

출시가 다가오자 홍보를 진행해야했다. 이때 홍보는 직접했다..

여러 홍보 전략이 있었지만, 오프라인과 온라인 모두 홍보를 진행하였다.

온라인에서는 에브리타임과 인스타그램을 통해 게시물을 직접 올리고 홍보를 진행하였고

에브리타임인스타

오프라인에서는 포스터를 제작하여 교내 게시판에 직접 부착을 하였다.


2) 서비스 중 생긴 이슈

(1) 화면 문제

기존화면문제화면

기존화면과 문제화면을 각각 살펴보면 문제화면에서 뜨는 구매후기, 보기하기 등과 같이 문법에 맞지 않는 이상한 글들이 적혀있었다. 처음에는 해킹을 의심하였으나 지나친 생각인 것 같기에 프론트 팀원의 장난이 아닐까라는 의심을 하기까지 이르렀다..

두가지 가정이 모두 아니었기에 어떤 부분에서 문제가 생긴 건지 살펴보았고, 문제는 어이없게도 구글 자동번역이었다. 구글 자동번역을 진행할 시에 같은 한글이라도 한번 더 번역을 진행하여 이상하게 나타나는 경우가 있었고, 두번째 사진에서 구글 번역을 통해 세번째 사진이 나오는 것을 확인하였다. 이는 첫번째 사진과 동일하게 나타나는 것까지 확인을 하였고 번역으로 인한 화면 오류를 찾아내었다.


(2) 동일한 주문이 여러개 들어감

첫째날 로직에 이상이 없는지 확인하던 중 주문 취소 내역을 보게 되었다. 이때, 주문 취소 내역을 살펴보니 동일한 주문이 몇초 안되는 간격으로 들어간 것을 확인하였고 사용한 학생회 측에서도 이를 확인하여 중복된 주문은 직접 취소를 하는 경우가 발생하였다.

해당 오류를 발견하고, 모든 일정이 끝나고 난 뒤에 문제를 찾아보았을 때 느린 네트워크 환경에서 동작이 처리되기 전에 주문완료 버튼을 여러번 누르면 동일한 주문이 여러번 들어간다는 것을 확인하였다.

이를 해결하기 위해서 주문완료 버튼을 한번 클릭했을 시 주문완료 버튼 비활성화 처리를 진행하였고, 동일 주문이 막히는 것을 확인할 수 있었다.


(3) 예약팀수가 일치하지 않음

현재 로직은 예약을 진행할 시에 부스의 대기중인 팀 수가 증가하게 된다. 이를 예약 테이블과 부스 테이블에서 따로 관리를 한다. 따로 관리하는 이유는 예약 가능한 부스를 조회할 때 예약 테이블을 필수적으로 조회하게 되는데, 여러 테이블의 조회를 줄이기 위해서이다. 따라서 예약 테이블에 값이 들어오게 된다면 예약한 부스 테이블의 대기중인 팀 수를 증가시켜준다. 그리고 입장 또는 취소를 하게되면 대기중인 팀 수가 줄어든다.

그러나 여기서 문제가 발생한다. 학과측에서 예약자들을 입장 또는 취소로 관리하지 않고 예약 명단에 그대로 남겨둔 것이다. 따라서 이미 입장을 해서 예약 명단에 없음에도 불구하고 값이 넘어가지 않아 대기중인 팀 수가 쌓이게 되었고 그 상태로 영업을 중지한 뒤에 두번째날 다시 영업을 시작하여 전날 쌓여있던 대기중인 팀 수가 그대로 남아있는 오류가 발생하였다.

이를 해결하기 위해서 DB에서 값을 직접 변경하였다. 해당 필드는 연관이 되어있는 부분이 적으며 어떤 식으로 오류가 발생했는지 인지가 가능했고 단순 대기중인 팀 수만 줄여준다면 문제를 해결할 수 있기 때문에 DB에서 해당 값을 찾아 직접 변경해주었다.


6. 실제 사용 현황

9/119/129/13
활성 사용자수1,467명676명418명
누적 사용자수2,475명2,873명3,091명
전체 조회수29,009회11,620회5,201회
전체 예약 건수33건28건11건
전체 주문 건수445건490건373건

전체 사용자수3,091명
전체 조회수80,357회
전체 예약 건수72건
전체 주문 건수1,308건

축제 마지막날 기준으로 전체 조회수 약 8만회전체 사용자수 약 3천명의 기록을 확인할 수 있었고, 생각보다 더 많은 관심을 받아서 매우 기분좋게 성공적으로 프로젝트를 끝낼 수 있었다!


7. 아쉬웠던 점

1) 총학과의 협업 무산

총학생회와의 협업을 통해 축제의 모든 정보를 발빠르게 접할 수 있는 서비스를 제공하려고 했으나, 총학과의 협업 무산과 여러 문제들로 인해 연예인 라인업과 일정, 총학생회에서 진행하는 여러 이벤트 관련 부스들에 대한 정보들은 모두 총학생회 인스타로 넘어갈 수 있도록 수정하였다.

사실 축제에 연예인 라인업은 매우 중요한 정보이기 때문에 해당 부분을 제공하지 못한 서비스라서 아쉬운 느낌이 든 건 사실이다..


2) 자동화

해당 서비스를 기획한 것은 편하게 사용을 할 수 있도록 하기 위해서이다. 그러나 우리가 정보제공을 하기 위해서 부스 운영 여부를 확인해서 변경을 해주어야 했으며, 종료할 때도 마찬가지로 직접 변경을 해주어야 했다. 우리는 운영시간이라는 필드가 있었기 때문에 해당 필드에 있는 정보들을 사용해서 운영 여부 정도는 자동화로 진행했으면 어땠을까? 라는 생각이 들었다.


3) 코드 작성

코드를 짤 때 너무 생각없이 짠다던가 조금 더 깔끔하게 짤 수 있었을 것 같은데 싶은 부분들이 보였고, 이 부분에 대한 지식이 너무 없었기에 해당 부분에 대해서 리팩토링의 여유가 없었던 것이 아쉬웠다.

특히 초반에는 모듈을 분리해서 작성을 하였으나 후반에는 구현에만 집중을 하다보니 어디서 어떻게 작성을 했는지도 모를만큼 급하게 한 것이 아쉬웠다.

실제 테스트를 진행하기 위해서 로컬 서버에서 직접 모든 API를 호출하는 방식을 사용했었는데 이를 조금 더 간편하게 하기 위해서 Test Code를 작성하자는 얘기가 나왔었다.

다만 갑자기 보안 이슈 등 버그 픽스에 집중을 하게 되면서 Test Code 작성을 하지 않게 되었음이 아쉬었다..


4) 배포 공부

DevOps 관련해서 많은 관심을 가지고 있었는데, 이번 프로젝트에서 해당 부분에 대해서 옆에서 살펴보면서 공부를 해봐야겠다는 개인적인 목표를 이루지 못했다.

다른 기능들과 관련해서 여러 수정사항들이나 보안 관련해서 공부할 내용이 많았기 때문에 초기에 혼자만 생각했던 목표를 이루지 못해 아쉬웠고 해당 부분에 대해서는 따로 천천히 공부를 해볼 예정이다.


8. 소감

실제 유저들이 사용하는 서비스를 출시하는 것은 인생 처음이었기에 많이 떨리고, 개발을 할 때에도 신경써야할 부분들이 많았다. 하지만 이러한 경험을 해볼 수 있었다는 것에 매우 기뻤다.

특히, 프로젝트가 성공적으로 마무리가 되었으며 생각보다 훨씬 더 많은 관심을 받았기에 두고두고 오래 기억에 남을 프로젝트가 될 것 같다.

profile
소소한 공부기록

0개의 댓글