Max 16주차: 23-06-19 ~ 23-06-23
Max 17주차: 23-06-26 ~ 23-06-30
이번 회고를 하기 전에... 10주차 ~ 15주차 회고가 없는데
약 한달 간 자율 학습에 가까운 시간을 가지면서 회고보단 급한 기술 부채를 쳐냈다.
또 블로그보단 옵시디언에 공부한 내용을 많이 기록하면서 블로깅 습관을 잃어버린 것 같다.
다시 열심히 회고를 해보려고 한다! 💪
드디어 부트캠프의 꽃(?) 팀 프로젝트가 시작되었다.
팀 프로젝트는 카페 키오스크를 구현하는 것이었는데
첫 번째 프로젝트이고 2주간의 미션이라 그런지 엄청 어려운 요구사항은 없었다.
자세한 내용은 아래 깃허브 레포지토리에서 확인 가능하다.
Github: 카페 키오스크 프로젝트
총평을 해보자면 프론트와의 협업이 처음이라 초반에는 헤맸지만
이런저런 시행착오를 겪으면서 개선방향에 대해 생각해볼 수 있었다.
필자는 인프라랑 일부 API 개발을 담당했는데
이번 회고는 프로젝트를 진행하면서 겪었던 이슈와 Trouble Shooting 내용 위주가 될 것 같다.
조금 큰 이슈들은 위에 적어둔 레포지토리의 Wiki에 정리해두어서 Wiki에 없는 보다 자잘한 이슈들 위주로 회고할 예정이다.
우선 인프라 구조는 이렇게 잡았다.
아직 인프라를 많이 접해보지 못했고, 각각의 장단점을 모르기 때문에
구글링해보았을 때 현재 내 지식수준에서 이해가 가능하고 적용할 수 있는 구조를 택했다.
• • •
처음엔 잘 몰라서 백엔드만 배포하면 될줄 알고 있어서,
프론트엔드 분들이 firebase
로 배포를 해주셨는데 두가지 문제가 있었다.
1. CORS 문제
프론트는 firebase
로 배포하고, 백엔드는 AWS EC2
로 배포했기 때문에 당연히(?)도 CORS 문제가 발생했다.
(지금 생각해보면 당연한데 그때만 해도 엄청난 에러였다. 🥲)
구글링을 해보니 스프링으로 설정 가능한 부분이어서 코드를 추가해보니 API 요청과 응답은 문제가 없는 듯 했다.
다만 이부분은 당시에 *
로 모든 외부 IP에 대해 허용을 했었는데,
만약 나중에 또 이런 상황이 있다면 보안 문제로 인해 특정 도메인 혹은 IP에만 허용하도록 해야겠다고 생각했다.
2. https
통신 문제
firebase
로 배포하면 프론트는 https
로 배포가 되는데 백엔드는 도메인이 없기 때문에 http
였다.
찾아보니까 https에서 http로의 API 요청은 브라우저에서 차단한다.
크롬의 경우, 브라우저 설정을 통해 차단을 풀 수는 있지만 보안 경고가 계속 뜬다.
이를 해결하려면 SSL 인증서가 필요한데, SSL 인증서를 발급받으려면 도메인이 필요하기 때문에 불가능했다.
백엔드에서 프론트엔드 배포도 같이 하는 것이 좋을 것 같아서 백엔드에서 모든 배포를 하기로 했다.
• • •
처음에는 프론트엔드 배포를 한다는 것이 막막했다.
당시 월말이라 AWS 프리티어 사용량이 거의 남지 않아서 EC2 하나밖에 사용할 수 없는 상황이었다.
지금 생각해보면 바보같을 수도 있는데, 은연중에 EC2 하나당 서버 하나만 돌릴 수 있다고 생각했던 것 같다.
그리고 또 당시(라고 해봤자 2주전...이지만 배포하면서 포트에 대한 이해도가 갑자기 높아졌다.) 포트에 대한 개념이 부족해서
같은 서버에 넣는다고 해도 어떻게 통신하지? 했던 의문이 있었다.
서버 하나에 프론트 서버 백엔드 서버 배포하기
무튼 일단 t2.micro
인 EC2에 서버 2개를 돌려보고
부딪히면서 이슈를 해결해야겠다고 생각했다.
프론트엔드 분 도움을 받아 리액트 서버를 돌리기 위한 3가지 명령어를 배우고 서버에 띄어봤다.
npm install
npm run build
npm start
물론 이 사이에 이런저런 이슈들이 많았다...
리액트를 처음 빌드하다보니, package.json
을 지워버리기도 하고
node module
폴더가 없어서 서버가 안 돌아간다던지...ㅎ
근데 API 요청할 때 서버에서 응답을 못하고 있었다.
이유를 못찾고 있었는데
NGINX
가 궁금해서 새벽에 혼자 사용법 찾아보면서 이런저런 설정을 해보다가
API 요청 URL의 Proxy_Pass를 xxx.xxx.xx.xx:8080
이런식으로 설정해줬는데 갑자기 잘 되는 것이었다.!!
그렇다 원인은 리액트에서 API 요청할 때 포트 번호가 없었던 것이었다...
(그런데 firebase에서 배포했을 때는 어떻게 정상적으로 응답이 된거지..?)
이슈 해결하고 또 문득 들었던 생각이 Elastic IP를 사용하지 않아서 IP가 매번 바뀌고 리액트랑 스프링이랑 같은 서버 안에 있으니까 굳이 IP를 적지 않고 localhost:8080으로 보내면 요청이 받아질 것이라고 생각이 들었는데 왜인지 실패했다..
이건 이유를 차차 알아봐야겠다.
팀원분이 개발한 API에서 엔티티나 DTO를 사용해야 하는데 @Getter
가 없었어서 값을 못 불러오는 등
자잘한 이슈를 제외하면 큰 에러도 없었고 API 개발은 생각보다 무난했던 것 같다.
언급할만 이슈는 기획서 변경과 @Scheduled
어노테이션 관련 이슈가 있다.
• • •
기능 요구사항 중 "실시간 판매량을 집계하여 BEST 메뉴를 선정하여 카테고리 상단에 노출"하는 요구사항이 있었다.
팀원들이랑 여러방면으로 논의를 했었는데,
카페에서 실시간 판매량을 집계하여 노출하는 것의 목적과 의미가 서버 부하 대비 없는 것 같았다.
실시간 판매 노출 대상은 고객이고, 베스트 메뉴는 고객의 의사결정을 돕는 것이다.
특정 메뉴를 의도적으로 노출해 판매량을 늘리는게 아니라 실제 판매량을 집계하여 노출하는 것이라면
굳이 고객 구매 1회당 서버에 요청을 보내 BEST 메뉴를 계속 갱신하는 것과
전날의 합산 통계를 노출하는 것의 큰 차이는 없다고 판단이 들었다.
실제 상황에서도 기획서를 무조건 따르는게 아니라 논의를 통해 더 나은 방향으로 나아가는 것이 맞다고 생각이 들어 요구사항을 "전날 판매 기준으로 집계"로 변경하였다.
• • •
@Scheduled
어노테이션 관련 이슈전날 판매량 기준으로 집계하는 것을 매일 수동으로 할 수는 없으니 매일 00시에 자동으로 DB가 갱신되도록 하는 방법을 찾아야했다.
우리 팀이 찾은 방법으로는 다음과 같았다.
trigger
procedure
@Scheduled
어노테이션 활용이전에 호눅스 수업에서 procedure
는 레거시 프로젝트에 많이 남아 있다고 했다.
요새는 DB의 책임과 어플리케이션의 책임을 분리한다고 들었던 기억이 있었다.
필자는 자동 집계는 DB의 책임이 아니고 비즈니스 로직이라고 생각이 들었고
비즈니스 로직은 어플리케이션 단에서 책임지는 것이 맞다고 생각이 들었다.
무튼 다음과 같이 코드를 짰는데, 자동 집계가 안되는 이슈가 있었다.
@Repository
public class JdbcOrdersRepositoryImpl implements OrdersRepository {
private static int today;
private static Long orderNumber = 0L; // 주문 번호는 매일 0으로 초기화되어야 한다.
static {
dailyReset(); // 날짜가 바뀌었는지 확인하고 주문번호를 0으로 리셋한다.
setToday(); // 날짜가 바뀌었는지 확인할 수 있도록 오늘 날짜를 바꿔준다.
}
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public static void setToday() {
today = ZonedDateTime.now().toLocalDate().getDayOfMonth();
}
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public static void dailyReset() {
LocalDate current = ZonedDateTime.now().toLocalDate();
if (current.getDayOfMonth() - today >= 1) {
orderNumber = 0L;
}
}
알고보니 @Scheduled
어노테이션을 작동시키기 위해서는 @EnableScheduling
을 스프링 부트 실행파일에 선언해야 했다.
다음과 같이 @EnableScheduling
을 선언해주면 스케줄러가 잘 작동한다.
@SpringBootApplication
@EnableScheduling
public class KioskWebApplication {
public static void main(String[] args) {
SpringApplication.run(KioskWebApplication.class, args);
}
}
프로젝트를 진행하면서 개선해보고 싶은 것과 고민들이 있었는데
기록해놓고 차차 해결하고자 한다.
• • •
백엔드 팀원이 4명이었고 요구사항이 작아 API 개발을 잘게 쪼개다보니
Class를 최대한 겹치지 않게 짰는데도 어쩔 수 없이 개발하면서 팀원 간 코드 의존성이 생겼다.
이러면서 Git 충돌이 났었는데, 충돌이 안나게 협업을 어떻게 하면 좋을지 고민이었다.
지금 당장 생각나는건 인터페이스를 만들고 인터페이스 기반으로 작업하는 것이다.
• • •
@Scheduled
어노테이션이 잘 작동하는지 확인하기 위해서 테스트 코드를 짜는데 어려움이 있었다.
시간을 의도적으로 조작해야 했는데, 방법을 잘 모르겠어서 리플렉션을 사용했는데 맞지 않는 방법인 것 같았다.
@Test
@DisplayName("자정이 되면 주문번호가 0으로 초기화됩니다.")
public void dailyReset() throws NoSuchFieldException, IllegalAccessException {
// given
Field today = ordersRepository.getClass().getDeclaredField("today");
today.setAccessible(true);
today.set(ordersRepository, yesterday.getDayOfMonth());
// 생략...
}
• • •
현재 구현된 바로는 키오스크에서 표시되는 음료 이미지가 AWS S3
에 저장되어 있고
키오스크에서 주문이 완료되고 메인 페이지로 돌아갈 때마다 서버로 GET
요청을 보내서 이미지를 불러온다.
개인적으로 매번 이렇게 불러오는게 매우 비효율적이라고 생각이 들었다.
키오스크가 한번 부팅될 때 최초 1회 이미지를 다 불러와서 이후에는 서버 요청 없이 이미지를 사용하는 것이 좋다고 생각이 들었다.
그리고 여러 페이지에 동일한 이미지가 다양한 사이즈로 표시되는데 한 개의 이미지로 프론트에서 사이즈 조절하는 것보다 뭔가 다른 좋은 방법이 없을까 고민이 되었다.
이 두 고민은 꼭 해결해서 다음 혹은 다다음 프로젝트에 적용해보고 싶다.
• • •
join
여러 개 vs select
여러 번예를 들면 Entity ABC
가 있는데 필드 A
는 table A, B
는 table B, C
는 table C에 있다고 하면
join
을 여러 번 해서 한번에 Entity를 만드는게 좋을까?
아님 select
절로 여러번 쿼리를 날려서 조합을 해 Entity를 만드는게 좋을까?
join
을 몇번 하느냐, 테이블에 데이터 양이 얼마나 많은가에 따라 달라지겠지만
아직 잘 모르겠다.
• • •
Explain
으로 쿼리 성능 개선해보기다음 프로젝트 때 꼭 시도해볼 것!