한양대학교 ERICA캠퍼스 소프트웨어융합대학과 교내 IT 동아리 연합이 주최한 해커톤 HY-THON에 참여했다. 왜 우리 학교에는 아이디어톤만 있고 해커톤은 없을까 생각하고 있었는데 마침 주최한다길래 바로 신청서를 제출했다.
[HY-THON 소개]
HY-THON은 멋쟁이사자처럼, UMC, 구름톤 유니브, HY-END가 가진 "지속 가능한 교내 해커톤 행사"라는 공통된 목표로부터 만들어지게 되었습니다. 또, 개발자의 수요가 늘어나면서 개발에 대한 관심이 높아졌지만 교내에는 개발 경험을 쌓거나 개발자 간의 네트워킹의 기회가 적기 때문에 이를 해소하고자 개최되었습니다.
앞으로 HY-THON이 한양대학교 ERICA의 대표적인 교내 행사 중 하나로 자리 잡기를 희망하고 있습니다!
일주일 동안 6~7명끼리 팀을 이뤄 정해진 주제에 따라 앱/웹 서비스를 만들고, 마지막 날에 발표한 뒤 교수님들께 심사받는 방식이어서 다른 해커톤과는 다르다고 생각했다. 또, 포트폴리오를 따로 받진 않고 아래 적어둔 것들만 작성해서 제출하면 됐다.
- 소속 동아리: 멋쟁이사자처럼 / UMC / 구름톤 유니브 / HY-END
- 지원 가능 파트: 기획 / 디자인 / 프론트엔드 / 백엔드
- 사용하는 프레임워크/툴과 숙련도(상, 중상, 중, 중하, 하)
- 지원한 파트에서 자신이 할 수 있는 것
[진행방식]
1. 팀은 기술 스택이 맞는 팀원끼리 최대한 동아리/학회가 겹치지 않도록 구성
2. 행사 당일 7일 전(11.23.토)부터 주제 공개 및 개발 시작
3. 당일(11.30.토) 팀별로 개발 완성 및 제출 후 발표, 심사, 시상식 진행
4. 발표(10분)에는 PPT와 시연까지 발표 가능
5. 배포는 필수가 아니며, 로컬로 HDMI 연결하여 시연 가능
1. 아이디어 메이킹
주제
주제는 아래와 같다.
🍀 올해의 주제는 "보통의 하루"입니다.
우리는 언제나 더 나은 삶, 더 높은 성취를 향해 달려가지만, 때로는 너무 치열한 하루가 우리의 마음을 지치게 만들기도 합니다. 이번 해커톤에서는 "갓생이 아닌 걍생"에 초점을 맞춰, 사람들의 일상을 더 안정적이고 편안하게 만들어주는 솔루션을 고민하고자 합니다.
주제 설명:
- 목표: 바쁜 현대인들이 평온한 하루를 보낼 수 있도록 돕는 서비스나 아이디어를 개발
복잡하거나 대단하지 않아도 괜찮습니다. 보통의 하루가 특별해질 수 있도록, 여러분의 창의력을 마음껏 발휘해 보세요! 여러분의 멋진 아이디어를 기대합니다!
아이디어 회의
팀은 11/23(토)에 발표됐는데, 우리 팀은 기획자(1) & 디자이너(1) & 프론트엔드(Android / 2) & 백엔드(Spring Boot / 2)로 구성됐다. 이때 너디너리 해커톤에 참여하고 있었어서 아이디어 회의는 11/24(일) 저녁에 진행하게 됐다. 나는 HY-THON 단톡방에 뒤늦게 초대돼서 아이디어는 생각도 못 하고 참여했는데, 기획자님이 아이디어를 4~5개 정도 구상해 오셔서 그중에서 하나를 고르기로 했다.
- 일주일 뒤 전해지는 일기장
- 맛집 공유 & 식사메이트 구하기
- 플리 공유
- 한 줄 명언
- 사진 추억 & 공유
- 쉼터 지도
- ...
등 다양한 아이디어를 생각해 오셨는데... 내가 개발해야 한다고 생각하니 아무래도 기능보단 구현 가능성에 초점을 맞추게 됐다. AWS S3나 지도 API 적용 같은 새로운 기술을 배우기에는 일주일은 너무 짧다고 생각하기도 했다.
결론은 기획 의도가 주제에 제일 잘 맞고 구현하기 좋은 첫 번째 아이디어(일주일 뒤 정해지는 일기장)로 결정됐고, 다음 날까지 기획자님이 좀 더 구체화시키기로 정해졌다.
핵심 기능 정리
기획자님은 자신이 작성한 일기장과 동일한 카테고리를 가진 다른 사람의 일기를 편지처럼 메일로 전송받는 기능을 구현하고 싶다고 하셨는데, 프론트엔드가 Android(앱)이다 보니 어울리지 않아서 어떻게 할지 고민하다가 앱에서 확인할 수 있는 거로 바꾸게 됐다.
디자이너님이 계셔서 WF는 공유해 주셨지만... 따로 정리된 핵심 기능 파일이 없어서 WF만 보면서 열심히 정리해야 했다. 😥 사실 11.25(월) 밤늦게 백엔드끼리 디스코드로 회의하다가... 피그마(WF)만 보곤 핵심 기능을 모르겠어서 질문을 모아놓고 단톡방에 올렸다. 너무 늦은 시간이라 걱정했는데 프론트엔드 한 분이 바로 질문을 주셔서 디스코드 회의 참여 가능하신지 여쭤보고 냅다 회의를 열었다. 기획자님도 바로 참여해 주셔서 정말 다행이었다. 😅 역시 프로젝트에선 기획자님을 최대한 괴롭혀야 나중에 수정사항도 적어지고 개발을 빨리빨리 진행할 수 있다.
정리된 핵심 기능은 아래와 같다. 핵심 기능을 정리할 땐 이 기능이 프론트엔드의 영역인지 백엔드의 영역인지도 생각해봐야 하고, 사소한 글자 수 제한부터 기능의 전체적인 흐름까지 이해해야 한다. 그래야 딴 길로 새지 않고 기능을 확실하게 구현할 수 있다.
결론은,
[느린 우체국]
솔직하게 자신의 고민이나 힘든 점을 작성하고,
타인의 이야기를 통해 공감과 위로를 얻을 수 있는
편지 작성 서비스
이런 앱 서비스를 만들기로 했다! 같이 휴학한 동기에게 아이디어가 어떤 것 같냐고 물어봤는데, 예상한 대로 '일주일 뒤에 받는 타인의 이야기를 통해 공감과 위로를 어떻게 얻는지 모르겠다'라고 대답했다. 생각해 보면 정말 '굳이...?' 싶기도 하다. 실제로 발표 후 이것과 관련된 질문을 받기도 했다. '왜 굳이 일주일인가?', '전달받은 편지가 카테고리는 같더라도 나랑 아예 관련 없는 얘기라면 어떻게 공감과 위로를 얻을 수 있을까?'...
- 내가 생각하기엔, 내가 일주일 전에 적은 고민과 소재와 감정이 같은 다른 사람의 글을 전달받아 확인하면서 '아 내가 저번 주에 이런 고민을 했었지. 앞으로 하는 고민들도 이렇게 지나가겠구나.'라고 생각하는 계기가 될 수 있을 것 같다. 아니면 '다른 사람들도 비슷하구나?'라는 동질감을 얻을 수 있다는 것만으로 위로가 되지 않을까 싶다. 😊
- 나중엔 다른 사람의 글을 전달해 주는 과정에서 글을 분석해 내 글과 유사한 글만 전달한다든지, 전달받은 글을 평가(관련성만 평가)할 수 있게 만든다면 더욱더 공감과 위로를 준다는 취지에 어울리게 아이디어를 발전시킬 수 있을 것 같다.
2. 백엔드 개발 준비
ERD 설계
이번에는 API를 구현하기 전 엔티티를 생성하는 과정에서 ERD를 한 번 리팩터링 했다.
- Member
- 회원가입으로 입력받은 유저의 정보를 저장하는 테이블
- 회원가입 시 비밀번호는 BCryptPasswordEncoder로 암호화돼 저장된다.
- Diary
- 유저가 작성한 일기의 정보를 저장하는 테이블
- 유저는 하루동안 1개의 일기만 작성&수정할 수 있다.
- DiaryKeyword
- 유저가 작성한 일기의 소재와 감정을 저장하는 테이블
- 일기 번호와 소재 번호, 감정 번호가 저장된다.
- 소재와 감정이 같은 일기를 조회할 때 매핑 테이블로 사용한다.
- Subject
- 소재 카테고리(1~9)를 저장하는 테이블
- WORK(1), STUDY(2), FAMILY(3), FRIENDS(4), HEALTH(5), WEATHER(6), LOVE(7), MONEY(8), OTHER(9)
- Emotion
- 감정 카테고리(1~3)를 저장하는 테이블
- HARD(1), GOOD(2), SPECIAL(3)
엔티티를 생성하고 양방향 매핑을 하다가 두 테이블 사이에 외래키(FK)가 2개가 있도록 설계하는 방식이 좋은 설계가 아니라는 걸 알게 돼서 아래처럼 수정하게 됐다.
- Member
- 위와 동일
- Diary
- 위에서 DiaryKeyword 테이블, Subject 테이블, Emotion 테이블 포함을 삭제하고, Diary 테이블의 필드에 enum 타입으로 넣는 것으로 변경했다.
- 이렇게 설계해도 Spring Data JPA Query Methods를 사용하면 소재와 감정이 같은 일주일 전 일기를 편하게 조회할 수 있다.
- Transmission
- 전달받은 일주일 전 일기의 번호와 받은 사람의 번호를 저장하는 테이블
API 명세서 작성
URL이 같아도 HTTP method가 다르면 중복되지 않게 처리되기 때문에 경로를 더 붙일 필요 없이 깔끔하게 개발을 진행할 수 있어서 좋다. 중간에 CI/CD 오류나 API 호출 속도 지연 오류가 생겨서 고치느라 새로운 API 구현을 많이 맡지 못해서 아쉬웠다. 다른 백엔드 분이 학기 중이신데도 팀 프로젝트에 열심히 참여해 주셔서 너무 다행이었다. 🥹
서버 배포
아래 적어둔 글을 보면서 무중단 CI/CD 파이프라인을 구축하려고 했다. 분명 초반엔 성공했는데... ERD를 리팩터링 하고 엔티티를 다시 생성해서 올리자마자 CI(통합)는 성공했지만 CD(배포)에서 계속 오류가 생겼다. 💀
아래 [REFACTOR] PR부터 EC2를 새로 생성하고 Health Check를 하는 과정에서 오류가 생긴 듯했다.
- Elastic Beanstalk 환경을 생성하면 EC2와 S3까지 자동으로 생성된다. 원래는 기존에 있는 EC2를 죽이고 새로 EC2를 만들고 그 위에 Spring Boot 서버를 올려 배포하는데, 무중단으로 구축했기 때문에 기존에 있는 EC2는 내버려 두고 새로 EC2를 다 만든 다음 대체해 버린다. 이때, 대체하기 전에 새로 만든 EC2가 정상적으로 작동하는지 확인하기 위해 API 요청을 보내 Health Check를 한다. 여기서 50% 이상의 요청이 모두 500 Server Error를 뱉어서 배포에 실패한 것 같다.
EB 환경 로그도 살펴보고, GitHub Actions에서 Deploy까지 기다리도록 수정해서 로그를 확인해 보고 뜬 오류로 구글링도 열심히 해봤는데 해결할 수가 없었다. 😥 로그에 적혀 있던 오류들은 아래와 같다.
During an aborted deployment, some instances may have deployed the new application version.
To ensure all instances are running the same version, re-deploy the appropriate application version.
[ERROR] Ignoring not applicable command.
Environment health has transitioned from Info to Degraded.
50.0% of the requests are failing with HTTP 5xx.
Insufficient request rate (24.0 requests/min) to determine application health.
ELB processes are not healthy on 1 out of 2 instances.
Application update in progress completed (running for 3 minutes).
ELB health is failing or not available for 1 out of 2 instances.
Deployment failed: Error: Environment still has health Red 30 seconds after update finished.
그래서 결국엔 EC2를 직접 만들고 수동으로 빌드&배포하게 됐다. 사실 서버 배포도 무중단 CI/CD 파이프라인 구축만 해봐서 할 줄 몰랐는데, 최근에 나간 너디너리 해커톤에서 송글송글님이 알려 주셔서 정말 다행이었다. 이것도 혹시 모르니까 따로 꼭 정리해 둬야겠다.
3. 백엔드 개발 진행
회원가입 API 구현
회원가입 시 유저 이름과 비밀번호를 받고, 유저 이름이 중복되는지 확인한다. 중복되지 않는다면 비밀번호를 BCryptPasswordEncoder로 암호화해 유저를 새로 생성한 다음 DB에 저장한다.
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
@Transactional
public Member signupMember(MemberRequestDTO.MemberSignupDTO request) {
Member findMember = memberRepository.findByName(request.getName());
if (findMember != null) {
throw new MemberHandler(ErrorStatus.NICKNAME_ALREADY_EXIST);
}
Member signupMember = MemberConverter.toMember(request);
signupMember.encodingPassword(passwordEncoder.encode(request.getPassword()));
return memberRepository.save(signupMember);
}
로그인 API 구현
회원가입과 동일하게 유저 이름과 비밀번호를 입력받는다. 리포지토리에 해당하는 유저 이름이 없거나 비밀번호가 일치하지 않으면 에러를 반환한다. 에러가 없다면 유저의 번호와 이름으로 액세스 토큰을 발급하고 토큰을 반환한다.
- BCryptPasswordEncoder의 matches 메서드로 유저가 입력한 비밀번호와 DB에 저장된 비밀번호가 매칭되는지 확인할 수 있다.
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
public MemberResponseDTO.MemberLoginResultDTO login(MemberRequestDTO.MemberSignupDTO request) {
Member loginMember = memberRepository.findByName(request.getName());
if (loginMember == null) {
throw new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND);
}
if (!passwordEncoder.matches(request.getPassword(), loginMember.getPassword())) {
throw new MemberHandler(ErrorStatus.PASSWORD_NOT_MATCH);
}
String accessToken = jwtUtil.createAccessToken(loginMember.getId(), loginMember.getName(), null);
return MemberResponseDTO.MemberLoginResultDTO.builder()
.grantType("Bearer")
.accessToken(accessToken)
.build();
}
편지 발신 API 구현
일기를 작성하고 나면 일주일 뒤에 프론트엔드에서 해당 일기 번호(diary_id)로 편지 발신 API를 호출한다. 이때 호출 결과로 전달받은 일기의 제목, 내용과 전달 날짜를 반환한다.
- 이걸 백엔드에서 해야 할지 프론트엔드에서 해야 할지 고민이 많았는데, 프론트엔드에서 특정 기간 뒤에 API를 호출하는 기능이 있어서 다행이었다.
- 쿼리 메서드를 사용하면 find{대상}By{키워드1}And{키워드2};처럼 작성해서 리포지토리에서 원하는 값만 꺼내올 수 있다.
public Transmission transmitDiary(Member member, TransmissionRequestDTO.TransmitDTO request) {
Optional<Transmission> findTransmission = transmissionRepository.findByMemberAndDate(member, LocalDate.now());
if (findTransmission.isPresent())
throw new TransmissionHandler(ErrorStatus.TRANSMISSION_ALREADY_EXIST);
// 내 일기 찾기
Diary findDiary = diaryRepository.findById(request.getDiaryId())
.orElseThrow(() -> new DiaryHandler(ErrorStatus.DIARY_NOT_FOUND));
if (!Objects.equals(findDiary.getWriter().getId(), member.getId()))
throw new DiaryHandler(ErrorStatus.MEMBER_NOT_MATCH);
SubjectType subjectType = findDiary.getSubjectType();
EmotionType emotionType = findDiary.getEmotionType();
LocalDate writingDate = findDiary.getCreationDate();
// 내 일기와 소재, 감정이 동일한 일기 리스트 만들기
List<Diary> diaryList = diaryRepository.findDiariesBySubjectTypeAndEmotionTypeAndCreationDate(subjectType, emotionType, writingDate);
if (diaryList.isEmpty()) {
throw new TransmissionHandler(ErrorStatus.TRANSMISSION_NOT_FOUND);
}
// 일기 리스트에서 랜덤으로 하나 뽑기 (내 일기와는 다른 일기)
Diary randomDiary = randomDiary(diaryList, findDiary);
// 받은 일기와 내 정보 저장하기
Transmission transmission = TransmissionConverter.toTransmission(randomDiary, member);
return transmissionRepository.save(transmission);
}
문서화 & API 호출 시간 개선
저번 UMC 6th 프로젝트를 진행할 때 swagger만으로는 문서(API 명세서) 작업이 충분하지 않다는 걸 알게 됐다. 그래서 이번엔 배포 주소부터 각 API마다 어떤 방식으로 테스트하면 되는지, 액세스 토큰은 어떻게 입력하는 건지 정리해서 노션에 올려뒀다.
API 테스트를 할 때 정말 이상하게도 회원가입과 로그인 API를 호출하면 1분 정도가 지나야 결과가 반환됐다. 나머지 API는 다 거의 1초 만에 호출이 끝나서 더 이상했다. 처음엔 EC2에 오류가 생긴 줄 알고 재부팅도 해보고 새로 만들어 보기도 했는데 다 소용이 없었다. 😔
- 발표 날 새벽에 프론트엔드 분들이 API 연동을 하신다고 했는데 이때 서버가 꺼지면 대참사가 일어나니까 함께 밤을 새웠었다. 그때 이 오류도 고치려고 노력해 보고 CI/CD도 다시 해봤는데 실패했다.
- 두 API의 공통점이 뭘까 생각해 보다가 BCryptPasswordEncoder가 떠올랐고... 내가 어떻게 선언해 놨는지 살펴봤다.
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(20);
}
아래 글을 보면 BCryptPasswordEncoder는 생성자에 4~30까지의 숫자를 넣어 암호화 강도를 지정할 수 있다. Default 값이 10이니까 20이어도 크게 상관없을 거라고 생각했는데 이거 하나 때문에 시간이 엄청 오래 걸린 거였다. 암호화를 강하게 할수록 시간이 오래 걸리는 게 당연한데... 왜 이걸 생각 안 하고 구현했을까 싶다. 🙃 간단하게 숫자만 지워서 문제를 해결했다.
4. 스프린트 & 발표
스프린트
11월 30일 13시쯤 학연산클러스터 509호에서 대면 개발 및 발표가 진행됐다.
API도 미리 다 구현해 놔서 할 게 없을 줄 알았지만 아니었다. 프론트엔드 분들이 API를 연동하시면서 생기는 수정 사항(DTO 필드 변경 등)을 고치기도 하고, 앱 특성상 하루 동안만 일기를 작성하고 일주일 단위로 편지를 받을 수 있기 때문에 RDS에 더미데이터를 넣어 놓기도 했다. 더미데이터를 넣는 과정에서 EC2와 RDS의 기본 시간대가 영국인 걸 알게 돼서 구글링을 통해 급하게 수정했다. 🥹
- Spring Boot를 빌드할 때 application.yml에 넣어둔 환경변수를 인식을 못 하길래 환경변수도 다 지워버려야 했고, RDS가 테이블 로딩을 못 하길래 재부팅도 해야 했다. 당일 새벽엔 에러 핸들러를 추가했더니 swagger가 500 Server Error를 보여주면서 접속이 안 되기도 했다.
- 진짜 개발하는 과정에서 만날 수 있는 모든 오류를 만난 것 같다. 😭 그래도 해결 못 한 오류는 CI/CD 뿐이어서 다행이다.
발표
수정하고 에러를 고치다 보니 4시간이 훌쩍 지나서 발표 시간이 됐다. 발표는 각 팀당 최대 10분으로 7~8분 정도 발표하고 2~3분은 Q&A를 받는 방식이었다. ICT 융합학부와 인공지능학과의 고민삼 교수님, 유용재 교수님, 조용우 교수님 세 분께서 심사해 주셨다.
기술 구현에 초점을 둔 다른 해커톤과는 다르게 아이디어 기획, 운영, 서버 관리 등 교수님들 세분의 관점에서 프로젝트에 대한 심사를 들을 수 있어서 더 특별했던 것 같다. 예를 들면,
- 여기에서 이 기능은 왜 추가한 건지
- 기능의 특성상 런칭을 하게 된다면 서버가 엄청 커질 것 같은데, 서버를 어떻게 구축하고 관리할지 생각해 봤는지
- 기능만 보면 백엔드는 따로 필요 없고 프론트엔드 선에서 데이터만 관리해도 될 것 같은데 굳이 백엔드가 왜 필요한지
- 유사한 앱이 이미 시중에 많이 존재하는데 이 서비스를 써야 할 이유가 있을지
- 유사한 앱 조사는 했는지
- 아이디어가 주제와 반대되는 것 같은데 주제와 어떻게 연관된다고 생각하는지
- 이 기능에서 이걸 이렇게 제한해 둔 이유가 있는지
- ...
같은 질문을 주로 하셨다. 거의 다 기획과 운영에 관련된 질문이었다. 코드 관련한 자세한 질문은 따로 안 주셨고, GitHub에 커밋&브랜치 협업 규칙을 세우지 않았거나 PR을 많이 올리지 않았을 때만 질문을 주셨다.
우리 팀은 다행히도 아이디어에 관련한 질문만 3개 정도 주셨다. (사실 서버 관리 같은 백엔드 관련 질문을 하실까 봐 자리에서 떨고 있었다. 🫠) 기획자님이 당황하실 줄 알았는데 모든 질문을 예상했다는 듯이 답변을 척척 잘하셔서 놀랐다. 심사 기준에 기술성보단 아이디어에 대한 완성도, 독창성, 전달력 점수가 커서 결국은 기획자님의 Carry✨로 상을 받았다고 생각한다.
놀랍게도 Chat GPT를 활용해 내가 쓴 일기와 그림을 분석하고 각색해 주는 기능을 구현한 팀이 있었다. 발표를 들으면서 '대상은 이 팀 거구나...'하고 생각했다. 물론 정말 그렇게 됐다. 대부분의 팀이 일기나 습관을 주제로 선택했는데 각각 세부적인 기능은 달라서 발표를 듣는 재미도 있었다. 매 학기마다 열린다면 꾸준히 참가해도 괜찮을 것 같다. 물론 되도록이면 시험 기간이 아닐 때 열어줬으면 한다. 🙃
회원가입과 로그인 기능 구현 원툴로 다른 프로젝트나 해커톤에 더 참가하기엔 너무나도 부족하다는 걸 깨달았다. 알림, 채팅 기능 구현, Docker 사용, CI/CD 마스터, AWS EC2와 RDS, S3 설정 등 배워야 할 게 정말 많다. 이제 이론 공부는 그만하고 천천히 실습해 보자. 🔥