일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 유사 칸토어 비트열
- ERD
- 백준
- DFS
- 협업프로젝트
- 카우치코딩
- Java
- pom.xml
- 테이블 해시 함수
- Spring Framework
- Fun편log
- couchcoding
- maven
- GitHub
- 와이어 프레임
- 마법의 엘리베이터
- Qoddi
- 토이프로젝트
- 배포
- 코딩테스트
- 트랜잭션
- 토이 프로젝트
- 프로그래머스
- LEVEL 2
- 그리디 알고리즘
- 알고리즘
- 6주포트폴리오
- 빌드 툴
- 프로젝트 설계
- Spring
- Today
- Total
소통 하고싶은 개발자
FUN편log 프로젝트 4주차 회고 본문
목차
- 시작하며
- Pull Request 피드백 수정
- 파이어베이스 적용기
- 첫 배포 성공
- 1 회차 멘토링
- 파이어베이스 연동 성공
- 시큐리티 설정이 추가된 브랜치 배포
- 2 회차 멘토링
- Be-Fe 첫 임시 연동
- 마치며
시작하며
저번 주에 드디어 Qoddi의 담당자와 접선에 성공하고 4주차 멘토링이 시작되기 전까지 해야 하는 것들이 있었다.
- Jpa StoreSummary부분 만들어서 pull request
- 배포해보기(Qoddi)
- 시큐리티 만들어서 cors 해오기 (일단 보류)
내가 처음이라 그런지 모르겠지만 위의 항목 중 1, 2번은 그럭저럭 할 수 있을 것 같았지만 3번 시큐리티 만들기는 좀 어렵지 않을까 라는 생각이 들었다. Spring Jpa도 처음 다뤄보는 기술인 만큼 꽤 시간을 소요하면서 공부했는데, 스프링 시큐리티(Spring Securit)도 처음이었기 때문이다.
일단 StoreSummary를 구현하는 작업을 진행하고 있었기 때문에 하던 작업을 완료하고 이전과 마찬가지로 리뷰어를 설정한 후에 Pull Request를 생성했다.
Pull Request 피드백 수정 (StoreSummary)
StoreSummary는 편의점의 요약 정보를 담고있는 Entity Class로 기존에는 만들 계획이 없었지만 멘토님의 피드백을 듣고 일리가 있다고 생각하여 만들게 되었다.
처음엔 편의점 요약 정보 요청을 Front-end를 통해 수신하면, StoreService에서 즉각 계산하여 Json형태로 응답하도록 작성했었다.
- Jpa를 통해 findByStoreId()를 사용하여 List<Review>의 형태로 해당 편의점에 달린 Review들을 모두 가져온다.
- List의 Size를 리뷰 개수로, 전체 Review의 StarCount(별점)의 평균을 구해서 별점 평균으로 만든다.
- 별점 평균을 구할 땐 stream(). mapToDouble(). average(). orElse(0.0)으로 구했다.
- average()로 구한 평균값을 바로 가져오려고 했을 때 만약 스트림이 비어있었으면 에러가 발생하기 때문이다.
- Jpa를 통해 findByStoreId()를 사용해서 List<Keyword>의 형태로 해당 편의점을 설명할 수 있는 문구인 "키워드"를 가져온다.
- List <Keyword>의 각 항목을 키워드 내용을 기준으로 병합하여 그 수가 높은 순서대로 정렬하고 StoreSummary에 넣어준다.
- 만들어진 StoreSummary를 dto형태로 만들어 컨트롤러에서 응답할 때 사용한다.
- 스프링에 있는 메시지 컨버터에서 객체를 Json으로 바꿔준다.
위 방식대로 작성 후 멘토님께서 "리뷰를 조회할 때마다 전체 리뷰와 키워드를 가져오면 부하가 걸릴 수도 있으니, StoreSummary 테이블을 따로 만들고 리뷰가 저장될 때마다 StoreSummary 테이블을 업데이트하자"는 피드백을 주셨다.
왜냐면 일반적으로 리뷰를 "조회"하는 것보다는 "작성 및 수정"의 행위가 훨씬 적게 발생하기 때문이다.
나도 피드백에 깊이 공감하여 아래와 같이 조치했다.
- DB 명세서에 StoreSummary 테이블 생성
- StoreSummary 조회 요청을 처리하는 Service 메소드에 DB에서 조회하는 부분을 제외한 불필요한 부분들 제거
- 해당 부분을 Git에 Push -> 자동으로 진행 중이던 Pull Request에 수정사항 적용됨
- 멘토님께서 소스 확인하고 merge 진행
첫 배포 성공
앞서 진행하던 StoreSummary 작업을 끝낸 직후에 나는 Qoddi사이트에서 첫 배포에 성공할 수 있었다.
요약하자면 계정이 막히기 전까지 배포가 되지 않았던 이유는 Procfile 파일이 없었기 때문이라고 한다.
Qoddi가 프로젝트 폴더에서 Procfile파일을 찾지 못했기 때문에 뭔가 자동으로. jar파일을 탐색해서 실행하려고 했는데 탐색하던. jar파일이 plain.jar였고, 해당 파일에는 manifest 파일이 존재하지 않았기 때문에 Qoddi 로그에 계속 manifest파일을 찾을 수 없다는 로그를 발생시켰던 것이다.
나는 바로 Procfile을 만들고 다시 배포를 시도했다.
그런데 또 안되는 것이다.
배포는 성공했는데 사이트에 배포된 사이트에 접근이 안 되는 상황이 발생한 것이었고, 다시 한번 로그를 봤는데 다행히 이전과 오류 내용이 똑같지는 않아서 약간 안심했다. (만약 똑같았으면 슬펐을 것 같았다..)
./run.sh: Permission denied
이번에는 로그를 보니 방금 생성한 Procfile을 Qoddi에서 실행하려고 했으나 해당 파일에 대한 실행 권한이 없어서 실행을 시킬 수 없었고, 때문에 배포에 실패했다는 내용이었다.
chmod 755 Procfile
때문에 나는 위 명령어를 사용해서 Procfile의 실행 권한을 부여한 후에 다시 배포를 시도했고, 결과는 성공이었다!
파이어베이스 적용기
4주 1 회차 멘토링이 시작되기 전에 멘토님께서 참고용 자료로 스프링 부트(Spring Boot)에 파이어베이스(Firebase)를 적용하는 방법이 기술된 블로그 링크를 공유해주셨다.
블로그 내용을 보니 카우치 코딩 멘토링을 위해 이전에 멘토님께서 직접 만드신 내용 같았는데 일단 블로그 내용을 보고 차근차근 따라해보기로 했다.
우선 파이어베이스 계정 및 프로젝트를 만들었다.
그 후에 서비스 계정이란 걸 만들고 비공개 키를 획득했다.
아무래도 위에서 새 비공개 키를 생성하고 만든 Json이 파이어베이스 API를 사용하기 위한 키 값인 듯했다.
키 값을 얻어낸 후 본격적으로 백엔드식 클론 코딩(?)을 시도했다.
아무것도 모르지만 블로그에 나와있는 코드를 따라서 타이핑해보고 각 코드가 어떤 의미인지 유추해보기도 하고 모르겠는 건 일차적으로 구글링 해서 정보를 얻을 수 있기 때문이었다.
일단 build.gradle에 스프링 시큐리티와 파이어베이스 어드민 의존성을 추가했다.
그 이후에 블로그에 나온 대로 FirebaseInitializer, SecureConfig를 작성했다.
(작성된 코드가 코드는 대부분 블로그 내용과 비슷하고 내 깃허브 저장소에서 코드를 볼 수 있기 때문에 코드는 생략하려고 한다.)
시큐리티 코드를 직접 도입해보는 건 처음이었지만, 인프런에서 스프링 부트 강의를 들으면서 시큐리티에 대한 부분적인 지식은 있었기 때문에 그나마 이해하면서 작성할 수 있었던 듯하다.
대략적으로 각 클래스의 역할은 다음과 같다.
- FirebaseInitializer : FirebaseApp, FirebaseAuth와 같이 파이어베이스 API를 사용하기 위해 Bean으로 등록하는 configuration 클래스
- FirebaseApp : 파이어베이스 API를 사용하기 위해 API 키에 대한 인증 수행
- FirebaseAuth : 등록된 FirebaseApp을 통해 OAuth와 같은 사용자 인증을 하기 위한 객체
- SecureConfig : 스프링 시큐리티에 대한 커스텀 설정을 작성하는 configuratioin 클래스 (현재는 스프링 버전의 변경으로 인해 블로그에 나온 대로 작성할 수는 없지만 다른 자료를 참고하여 작성했다.)
- FirebaseTokenFilter : FilterChain에 등록할 필터
- InterFirebaseTokenFilter : FirebaseTokenFilter를 상속받은 클래스로 실제 인증 작업을 수행하지는 않고 내부 테스트를 진행할 때 인증 과정을 패스할 수 있도록 작성됨.
- ProdFirebaseTokenFilter : FirebaseTokenFilter를 상속받은 클래스로 실제 인증 작업을 수행하는 필터
위 클래스들에 대해 나름 꼼꼼히 작성하고 어플리케이션을 실행 시도를 여러 차례 실행하면서 멘토링이 시작되기 전까지 스프링 부트 앱을 실행시키는 데까지는 성공했다.
1 회차 멘토링
멘토링이 시작되고 저번 주와 비슷하게 진행 상황 공유 및 이슈 공유의 시간을 짧게 가진 뒤에 멘토님으로부터 이번 주의 핵심 목표인 로그인 기능과 관련된 강의를 듣게 되었다.
강의 주제는 대략적으로 세션/쿠키/JWT의 특징과 사용 이유, 꿀팁(?)이었는데 기존에 알고 있었던 정보가 리프레쉬되면서 자세하게는 몰랐던 특징들을 알 수 있어서 좋았다.
이후에 질문 답변 시간이 주어졌다.
나와 프론트엔드 분들께서는 사전에 깃 플로우(GitFlow) 방식으로 깃 저장소를 관리하기 위해 브랜치를 만들었었다.
나는 백엔드라 그런지 각 브랜치를 만들 때 개발해야 하는 항목과 역할에 맞게 브랜치를 나누는 게 어렵지 않았고, 원격 브랜치와 로컬 브랜치를 잘 옮겨가며 Pull Request를 생성하면서 작업을 진행할 수 있었고, 여러 번의 Pull Request가 끝나자 처음으로 Achievement 배지를 획득할 수 있었다.
하지만 프론트엔드는 기능 요소와 디자인 요소의 간극때문에 내가봐도 브랜치를 나누는게 어려워 보였고, 나처럼 프론트엔드 분들도 깃허브의 고수가 아니었기 때문에 브랜치 병합 쪽에 문제가 발생해서 해당 건의 질문 답변 시간을 가졌다.
이후에 내가 코드를 작성하면서 궁금했던 점들에 대해 멘토님께 질문하고 답변을 얻는 시간을 가진 뒤에 멘토링은 종료되었다.
- [질문 1] FirebaseOptions.setCredentials()는 어떤 메소드이고 왜 사용하는지 궁금합니다.
- 답변 : 해당 메소드는 인증서를 설정하는 메소드이고, 해당 메소드를 통해 API를 사용할 수 있는 인증을 수행할 수 있습니다.
- [질문 2] FirebaseOptions.setStorageBucket()에서 버킷이라는 용어의 뜻이 궁금합니다.
- 답변 : 이전 기수에서는 스토리지 버킷을 이미지 파일을 저장할 용도로 사용했습니다. 버킷은 스토리 지안에 파티션이 들어있다는 느낌으로 이해하면 됩니다.
- [추가적인 피드백] 스프링에서 로그를 만들고 싶을 때는 일반적으로 @Sl4j 어노테이션을 선언해서 기록합니다. 요즘은 로그를 작성하는 프로세스가 별도로 있는 경우가 많기 때문에 현업에서는 각 회사의 방식대로 로그를 기록하면 됩니다.
파이어베이스 연동 성공
1 회차 멘토링이 종료되고, 멘토님께서 저번 기수 분들이 수행했던 코드를 공유해주셨다.
덕분에 이번에도 저번 기수 분들의 코드를 양분 삼아 백엔드식 클론 코딩을 진행할 수 있었다.
(물론 내가 이해하고 받아들이는 게 제일 중요하기 때문에 이해하면서 코드를 작성했다)
이번에 작성한 코드는 다음과 같다.
- AbstractAuthService : AuthService의 추상 객체
- InterAuthService : AbstractAuthService를 상속받았으며 내부 테스트를 위해 파이어베이스 인증 작업을 수행하지 않고 패스하기 위해 만든 클래스
- ProdAuthService : AbstractAuthService를 상속받았으며 실제 파이어베이스 인증 작업을 수행하는 클래스
여기서 AuthService란 로그인 요청과 같이 OAuth 인증 작업을 수행하는 컨트롤러에서 사용할 객체로 파이어베이스 API를 사용하여 Access Token 인증 및 Token 데이터 추출과 같은 작업을 수행하기 위해 만든 Service 객체다.
여기서 AuthService를 추상화한 이유는 로컬 네트워크에서 테스트할 때는 프론트로부터 받은 액세스 토큰 자체가 존재하지 않고, 인증 작업을 수행할 수 없는 상태이기 때문이다.
때문에 컨트롤러에서 추상 클래스를 가지도록 만들었다.
그리고 이를 상속받은 구현체들에게 @Profile 어노테이션을 사용해서 application.properties 설정 파일만 수정해서 테스트할 수 있는 환경으로 만들었다.
2 회차 멘토링
멘토링이 시작되고 멘토님께서 처음으로 프론트엔드에서 백엔드의 API를 호출을 테스트해보자고 하셨다.
원래 서버에 요청을 보낼 때는 서버에서 클라이언트에 대한 cors를 설정해줘야 한다고 알고 있었다.
하지만 아직 cors설정이 되지 않은 상태로 배포되어있기도 했고, 프론트엔드에서도 내부 테스트를 진행할 때 항상 실제 서버에 붙여서 진행할 수는 없었기 때문에 요청을 보낼 때 프록시라는걸 설정해서 보내겠다고 했다.
나는 상황을 인지하고 제발 잘 되기를 빌고 있었다.
하지만 프론트엔드분이 몇몇 프록시와 관련된 설정을 마치고 요청을 보냈는데 404에러가 발생했고, 이번 멘토링은 프론트는 프론트대로 백엔드는 백엔드대로 문제가 있는지 다시 한번 확인해보는 시간을 가지기로 했다.
그런데 내가 배포가 처음이라 그런지 아무리 봐도 문제가 될만한 거리를 발견하지 못했다.
왜냐면 파이어베이스를 적용한 브랜치를 배포하기 전에 이미 배포를 성공한 적이 있고, 프론트엔드에 부탁해서 30분 동안 유효한 Access Token을 바로 로직에 넣고 돌렸을 때 문제없이 회원가입이 완료되었기도 했다.
또한 파이어베이스 시크릿 키도 Qoddi의 환경변수로 적절히 넣었다고 생각했다.
그런데 파이어베이스를 프로젝트에 적용한 뒤의 빌드 로그를 찾아보니 빌드가 실패되었다는 내용이 적혀있었다.
뭔가 배포를 한 번 성공했었으니 이젠 잘 되겠지라는 무의식 속의 안도감에 배포가 잘 되었는지부터 확인하지 않고 배포 시도만 된 상태로 다음 작업을 진행하려고 했었던 것 같다.
멘토님께서는 저번에 해결한 적 있는 문제 상황 같다고 말씀하셨다.
나는 속으로 '이걸?' 이런 생각으로 얌전히 멘토님께서 말씀하시는 걸 들은 뒤에 수정해야 할 부분을 수정했고, 거짓말처럼 다시 배포에 성공했다.
이번에 수정한 부분은 Qoddi와 관련된 문제였는데, Qoddi에 우리가 프로젝트에서 사용할 환경변수를 등록시킬 때 큰 따옴표(")를 발견하면 뭔가 종료 문자로 인식하는 문제가 있었다고 한다.
그래서 멘토님께서도 이전에 이런 문제를 겪고 큰 따옴표가 쓰일 일이 생기면 대략적으로 아래와 같은 순서로 동작하도록 하셨던 것 같다.
- 큰 따옴표가 포함된 환경변수에 대해 텍스트 에디터를 사용해서 큰 따옴표를 일괄적으로 다른 문자로 바꾼다.
- 여기서 다른 문자란 실제 값에 영향이 생기지 않고 한 번도 사용되지 않은 문자여야 한다.
- 바뀐 환경변수를 Qoddi에 저장한다.
- 프로젝트에 쉘 파일을 만들고 Qoddi에서 Procfile을 실행하면 해당 쉘 파일이 실행되도록 만든다.
- 쉘 파일 내용은 환경변수를 읽어와서 이전에 바꿔두었던 문자를 일괄적으로 전부 큰 따옴표(")로 바꾼 뒤에. json의 형태로 프로젝트 폴더에 저장한다.
- 저장된 환경변수. json을 프로젝트에서 읽어서 사용하게 한다.
역시 8년차 백엔드 개발자의 짬바란 어마 무시했다.
이렇게 문제를 해결하고 내가 갖고 있던 질문사항에 대해 멘토님께 질문하고 답변을 받는 시간을 가졌다.
- [질문 1] 시큐리티 Filter를 등록할 때 @Component 어노테이션을 통해 빈으로 먼저 등록하고 빈 컨테이너에서 가져와서 등록하면 안 되는 이유가 궁금합니다.
- 답변 : 스프링 컨테이너는 빈을 등록하는 과정에서 Filter 인터페이스를 상속받는 빈을 필터로 등록합니다. 우리가 시큐리티 Filter를 등록하는 것은 SecurityFilterChain에 등록하기 위함이고, 해당 필터 체인에 등록되어있어야만 스프링 시큐리티로써 동작할 수 있습니다. 만약 빈으로 등록된 Filter를 SecurityFilterChain에 등록했다면, 특정 URL요청에 대해서는 해당 Filter가 동작하지 않도록 스프링 시큐리티 Config에 등록했다고 하더라도 무조건 실행되는 문제를 야기할 수 있습니다.
- [질문 2] UserRepository에서 deleteByEmail()메소드에 @Transactional어노테이션이 존재해야 하는 이유
- 답변 : 기본적으로 지원하는 메소드들에는 @Transactional이 일반적으로 붙어있다. 하지만 사용자에 의해 커스텀으로 생성되는 메소드의 경우 해당 어노테이션이 붙어있지 않다. 따라서 JPA는 트랜잭션이 안되면 에러를 발생시키기 때문에 해당 어노테이션이 필요했던 것.
- [질문 3] FirebaseTokenFilter를 UsernamePasswordAuthenticationFilter의 직전에 넣는 이유 (아래 코드 참고)
- 답변 : addFilterBefore()는 특정 필터가 실행되기 전에 실행되는 필터를 등록하는 메소드가 아니다. 해당 메소드는 특정 필터를 대신해서 실행되도록 만들어진 메소드이고, 여기서 Before는 필터는 핸들러가 실행되기 전, 후에 각각 실행되는데 그중에 핸들러가 실행되기 전에 실행되도록 만들어진 것이다.
http.addFilterBefore(new ProdFirebaseTokenFilter(userDetailsService, firebaseAuth),
UsernamePasswordAuthenticationFilter.class);
마치며
전체적인 코드가 들어가면 내용이 너무 길어질 것 같아서 생략하면서 최대한 링크로 대체하도록 했는데, 그럼에도 불구하고 이번 주에 진행한 내용이 꽤 많은 것 같다.
진행한 내용이 많은 만큼 얻은 것도 많아서 좋았다.
모든 멘토링이 끝나고 4주차가 지나가기 전에 프론트엔드와 연동해서 로그인/회원가입 요청을 수신했을 때 정상적으로 로그인/회원가입이 되는 것과 로그아웃, 회원 탈퇴 기능도 확인했다.
이제 5주차가 시작되기 전까지 최대한 아래 항목들을 완수하고 싶다.
- 로그인/회원가입 요청을 제외한 다른 모든 요청에 대해서는 로그인 권한이 필요한 경우 필터를 통해 쿠키에 저장된 토큰 값을 가져와서 유효한지 체크할 수 있도록 수정
- 리뷰 생성/수정/삭제/조회 기능 구현
- 리뷰 생성/수정/삭제 시 편의점 summary가 업데이트될 수 있도록 로직 추가
감사합니다!!
'카우치코딩 > 주간 학습 및 개발사항' 카테고리의 다른 글
FUN편log 프로젝트 6주차 회고 (0) | 2022.12.26 |
---|---|
FUN편log 프로젝트 5주차 회고 (0) | 2022.12.19 |
FUN편log 프로젝트 3주차 회고 (0) | 2022.12.04 |
FUN편log 프로젝트 2주차 회고 (0) | 2022.11.28 |
FUN편log 프로젝트 1주차 회고 (0) | 2022.11.22 |