6th UMC HACKATHON Seoul
7월 4일 낮부터 5일 낮까지 첫 해커톤에 참여했다. 신청은 6월 중순쯤 받았는데 워크북도 거의 끝나가고 있던 터라 7월쯤 되면 해커톤에 참여할 역량이 있을 거라고 생각했고, 선착순으로 마감된다고 하길래 바로 신청!
ERD 설계는 조금 자신 있었고 서버 배포나 API 개발은 워크북에서 해봤으니까 괜찮을 줄 알았다. 나 혼자 개발하는 것도 아니고 서버는 3~4명은 될 테니까 ㅎㅎㅎ...🙃
전체적인 시간표는 이렇게 구성됐다. 물론 이대로 흘러가진 않았다. 🫠
아이디어 메이킹
나는 프론트(Android) 4명, 백엔드(Spring Boot) 3명인 4조에 배정됐다.
점심을 먹기 전 브레인스토밍으로 노래를 정하고, 노래에 맞는 서비스를 생각했다.
무려 5시 반까지 회의를 진행한 결과...
[트게더]
소중한 사람들과 함께 추억의 서랍을 만들어 사진과 메모로 채우고,
미래의 특정 연도에 열어보는 특별한 시간 여행 서비스!
로 정해졌다. 노래는 [LUCY - Opening]으로 골랐는데, 노래 주제가 겹치는 팀도 있어서 노래와 서비스에 대해 고민하느라 회의가 점점 늦어진 것 같아서 아쉽다. 노래부터 정하기 보단 서비스를 생각하고 거기에 노래를 맞추는 게 오히려 나았을 것 같기도 하다. 🥲
프로젝트 개발
백엔드(3), 프론트(4), PM & 디자이너 이런 순서로 앉아서 다른 파트 분들이 기능과 디자인을 더 고안할 동안 백엔드 파트끼리 회의를 했다. 세 명 모두 UMC spring 파트도 처음이고 프로젝트(해커톤)도 처음이어서... 발표 때까지 우당탕탕 하면서 개발을 진행했다. ㅋㅋㅋㅋㅋ
워크북에선 이미 짜놓은 IA와 WF를 바탕으로 ERD 설계, 프로젝트 세팅, API 명세서 작성 등을 진행해서 해커톤에서도 비슷할 줄 알았는데 24시간 밖에 없어서 그렇게 흘러가진 않았다.
1. ERD 설계 & API 명세서 작성
우선, 기능이 완벽하게 구체화될 때까지 기다리는 건 너무 늦을 것 같아서 대략적인 핵심 기능을 써두고 그에 맞춰 ERD 설계부터 했다. 초반엔 다 같이 진행하다가 어느 정도 구현됐다 싶어서 다른 1명은 API 명세서를 작성했다.
솔직히 하면서 이게 맞나 싶었다. 보통 ERD가 이렇게 앙증맞은가...? 하면서도 갈수록 추가되겠거니 싶어서 일단 짜놓고 바로 Github로 넘어갔다.
HTTP method랑 API 관련한 강의를 더 찾아봐야겠다. 고칠 점을 찾아보고 다음 프로젝트를 해볼 때 적용하고 싶다.
2. 프로젝트 기본 세팅
다른 팀원들이 API 명세서와 ERD를 더 구체화하는 동안 Github에 올릴 프로젝트 초기 세팅을 맡았다. 워크북에서 진행한 걸 바탕으로 계층형 구조로 디렉터리를 만들었고 CI/CD와 swagger, API 응답 통일, 에러 핸들링 등에 필요한 것들도 미리 만들어서 Github에 push 했다. (지금 보니 validation은 쓰지도 않았다. API 구현도 힘들어서 파라미터를 검증할 시간도 없었나 보다.)
근데 디렉터리만 만들어서 올린 web, service, converter 등이 Github에 안 올라간 걸 확인할 수 있었다. 이유가 뭔진 모르겠는데 일단 안에 파일이 있는 디렉터리들은 올라가 있길래 나머지 것들에 TempController 등을 추가해서 다시 올렸더니 잘 올라갔다. 자세한 이유는 나중에 따로 찾아봐야겠다. 🤨
찾아보니까 아무것도 없는 디렉터리는 push되지 않는다고 한다. 흠...
다른 팀원들은 remote로 연동, pull로 받아와 domain과 repository를 ERD를 바탕으로 반씩 맡아서 올렸다. branch는 기능을 추가할 때마다 issue를 파서 올리는 거라고 생각했는데, 그냥 feature/1처럼 하나씩 맡아서 본인 전용 branch로 썼다. 이럴 거면 branch를 닉네임으로 만드는 것도 덜 헷갈리고 좋았을 것 같다.
중간중간 conflict도 나서 revert를 여러 번 하기도 했다. 충돌이 나면 어떻게 해결할지 궁금했는데 다신 경험하고 싶지 않다. ㅋㅋㅋㅋㅋ 🫠
commit 컨벤션도 정하려고 했는데 흠... feat, fix, chore에 형식 정도만 정했어도 commit이 깔끔했을 텐데... 🥲 PR 리뷰 설정이나 Label 등 아쉬운 부분이 너무 많은 것 같다.
3. 서버 배포 준비
'에이 서버 배포도 해본 프로젝트 경력자가 1명쯤은 있겠지!' 하고 생각했는데 그게 나였다. 🤔 심지어 나도 그냥 워크북을 따라 해 본 수준이라 많이 당황했던 기억이 있다. 배포된다는 확신이 없어서 '이게 맞을까...' 하고 생각하면서 워크북을 열심히 따라 했다. 당연한 거지만 서버가 배포돼야 프론트랑도 API 테스트를 할 수 있고, 기본 배포가 돼야 채점을 할 수 있기 때문에 해커톤 동안 이거에만 집중했다.
develop branch에 머지하면 Github action으로 CI가 돌아가게 구현해 놔서 팀원들이 머지할 때마다 돌아가는지 확인했는데 몇 개가 안 돌아가는 게 보였다. 원인을 찾아보니까 Spring Build with Gradle 부분에서 막히고 있었고, 더 찾아보니 중괄호나 세미콜론을 빼먹었다는 에러였다. Github에 push 하기 전... spring app을 돌려보지 않고 그냥 commit 해서 컴파일 오류 때문에 동작하지 않는 거였다. 😕 주의해 달라고 말하고 CD tool로 elastic beanstalk을 골랐다. docker는 해본 적이 없어서 eb로만 배포를 진행했다.
배포는 성공했다! EB의 도메인 주소 뒤에 임시 컨트롤러에 구현해 둔 경로를 입력했더니 잘 보이는 걸 확인할 수 있었다. 배포가 안된 줄만 알고 swagger를 로컬에서만 확인하고 있었는데 말이다. 😥 CI/CD 파이프라인(Github action으로 AWS EB에 서버 자동 배포)을 구현한 건 따로 글을 작성하는 게 나을 것 같다. VPC에 EC2, EB, RDS도 모두 정리해 봐야겠다. 이후 프론트랑 계속 문답을 주고받으며 API를 더 구현했다.
4. API 구현
내가 서버 배포를 준비하는 동안 팀원 둘이 보물상자와 추억으로 나눠 API 명세서를 바탕으로 구현했다. 서버 배포에만 집중하느라 다 끝나고 나서야 확인을 했는데 조금 당황했다. DTO response가 request마다 세부적으로 나눠져있지 않았다. builder가 아닌 setter로 DTO를 객체로 바꾸고 있기도 했고, 양방향 매핑에서도 오류가 있었다. 팀원들과 열심히 오류를 수정하면서 구글링 실력이 많이 는 것 같다.
구현한 API들을 swagger로 테스트했는데 몇 개는 문제없이 DB에도 잘 들어갔는데 핵심 기능인 보물상자 생성과 추억 생성에서 계속 오류가 발생했다. 주어진 ID가 NULL이면 안된다는 오류가 계속 떠서 MySQL 쿼리문도 작성해 보고 column도 수정해 봤지만 해결할 수 없었다. service나 converter 등 대부분의 코드를 내가 직접 구현하지 않아서 코드를 수정해보려고 해도 어디서부터 손봐야 될지가 생각나질 않았다. 전체적인 코드를 수정을 못 한 게 제일 아쉽다. 🥲
보물상자에 추억을 저장할 때 사진과 메모를 추가하는 기능을 구현해야 했는데, 이건 워크북 12주 차에서 AWS S3로 구현하는 거였다. MultipartFile 타입(form-data)으로 사진과 메모를 받고 그게 S3에 자동으로 저장되고 모두가 열어볼 수 있게 했어야 했다. 12주 차를 볼 시간이 없기도 했고, VPC를 만들고 지우는 게 버거워서 워크북만 읽었더니 구현을 할 수가 없었다. 이게 핵심 기능이라고 생각해서 더 노력해 봤지만... 발표 때까지 구현을 못해서 아쉬웠다.
public Memory createMemory(MemoryRequestDTO.CreateMemoryDto request, MultipartFile file) {
Memory memory = MemoryConverter.convertToEntity(request);
// S3
String uuid = UUID.randomUUID().toString();
Uuid savedUuid = uuidRepository.save(Uuid.builder()
.uuid(uuid).build());
String imageUrl = s3Manager.uploadFile(s3Manager.generateReviewKeyName(savedUuid), file);
// Handle TreasureBox
TreasureBox treasureBox = treasureBoxRepository.findById(request.getTreasureBoxId())
.orElseThrow(() -> new RuntimeException("TreasureBox not found"));
memory.setTreasureBox(treasureBox);
// Handle Images
// List<Image> images = imageRepository.findAllById(request.getImageIds());
// memory.setImages(images);
// S3
imageRepository.save(ImageConverter.toImage(imageUrl, memory));
return memoryRepository.save(memory);
}
중간중간 프론트에서 요구하는 API를 새로 추가해야 되는 경우도 있었다. 보물상자 1개 조회와 목록을 조회하는 API를 따로 만들어야 했는데, DTO response를 그대로 넘기다 보니 리스트에 대한 이름을 붙여주지 않아서 프론트에선 그 목록에 접근할 수가 없다고 했다.
'data': {
'treasureBoxId': ...,
'': ...,
'': ...
}
이런 식으로 보물상자 목록에 이름을 붙이면 된다고 하셔서 열심히 구글링을 해서 방법을 찾아냈다.
우선 서비스에서 목록을 가져와 리스트에 저장하고, 리스트에서 하나씩 꺼내 프론트에서 원하는 정보만 골라 build 한다. 이후 build 한 리스트를 Result로 감싸 이름을 붙여주면 된다. Controller에서 할 일이 맞는진 모르겠지만 아무튼 제대로 작동하는 걸 확인했다.
//보물상자 리스트
@GetMapping("/treasurebox/list")
@Operation(summary = "보물상자 리스트", description = "보물상자의 목록을 조회하는 API")
public ApiResponse<Result> getTreasureBoxes() {
List<TreasureBox> treasureBoxes = treasureBoxService.getTreasureBoxes();
List<TreasureBoxResponseDTO> collect = treasureBoxes.stream()
.map(m -> new TreasureBoxResponseDTO().builder()
.id(m.getId())
.deadline(m.getDeadline())
.status(m.getStatus())
.title(m.getTitle())
.build())
.collect(Collectors.toList());
return ApiResponse.onSuccess(new Result(collect));
}
@Data
@AllArgsConstructor
static class Result<T> {
private T data;
}
이외에도 swagger로 테스트할 때 순환 참조가 발생하는 문제도 있었고.. 만날 수 있는 오류는 다 만났던 것 같다. 🫠
프로젝트 발표
8팀이 10분씩 발표를 진행했다. 보면서 써보고 싶다고 생각한 서비스들도 꽤 있었다.
협업을 맛보려고 신청했지만 협업에 대해 알기 보단 내 실력이 얼마나 부족한 상태인지를 알 수 있는 시간이 된 것 같다. 방학 동안 진행하는 본 프로젝트에선 프론트엔드랑은 어떻게 협업하는지, PR이나 Issue는 어떻게 진행하는지 등을 배우고 제대로 된 협업을 경험하고 싶다. 😊