2022. 4. 16. 02:09ㆍ회고
지금 보고 계시는 글은 코드스쿼드 마스터즈 과정에 참여하며 느끼고 학습한 내용을 정리한 글입니다.
과정에 참여한 지 벌써 100일이 넘었고, 이제는 이곳에서 배울 날보다 배운 날이 더 많아졌습니다. 기대고 배울 곳이 곧 사라진다니 두렵기도 아쉽기도 하지만 야생형으로 양육 받았으니 걱정은 없습니다.
지난 2주간 Android 멤버들과 협업 프로젝트를 수행했습니다. 간단한 TodoList 어플리케이션을 만드는 과제였습니다. 간단할 것이라 생각했지만 착각이었습니다. 동료, 리뷰어와의 커뮤니케이션 방법, 기술적 부족함, 체력적 한계와 야생 학습의 콜라보로 밤을 새가며 아주 바쁜 2주를 보낸 것 같습니다. 덕분에 어플리케이션 개발지식에 대한 성장 뿐 아닌, 소프트스킬의 성장 또한 누릴 수 있었습니다. 이제야 첫 발을 떼본 초보 개발자지만, 무엇을 모르는지에 대한 감은 서서히 잡히고 있는 것 같아 조급하지 않습니다 ㅎㅎ!
# 학습 회고
- Mock API
이번 과제는 이전과 다르게 클라이언트 개발자와의 협업이 이루어져야 했습니다. 그래서 최대한 빠르게 Mock Api를 제공해주는 것이 필요했습니다. 당연히 아무것도 몰랐던 Back-End 파트의 저와 Nori는 Mock Api가 무엇인지부터 알아가야 했습니다.
일단 Mock Api가 '클라이언트에게 당장 필요한 최소한의 데이터를 제공해주는 가짜 api' 라는 개념을 어찌어찌 우여곡절 끝에 정립했습니다. 그리고 난 후의 생각의 흐름은 이렇습니다.
- 일단 빨리 뭐라도 데이터를 제공해야 하겠구나
- 근데 뭘 내려줘야 하지?
- 데이터 포맷이나 어떤 데이터가 필요한지를 클라이언트 멤버들과 함께 협의하는 과정이 필요하겠다!
- 이것을 한 마디로 API 명세라고 하는구나, API 명세 예제를 찾아보고 구글 스프레드 시트에 명세를 정리하자!
- 명세를 정리했으니 클라이언트에게 이 형식대로 데이터를 내려줘야 한다!
- 최대한 빨리 MockController를 개발해서 AWS EC2에 올려야겠구나!
- DTO는 어떤 식으로 구성해야 하지..? - EC2는 왜 잘 안되지...?
- 밤샘...
Mock API를 제공하기까지의 과정이 참 험난했습니다. 좋은 툴과 방법론이 있는지 알지도 못한 채 바닥부터 삽질을 시작해서 우여곡절 끝에 제공에 성공은 했습니다. (추후 그룹 리뷰시간에 보니 Swagger, Postman 등 더 좋은 방법이 있었다는 걸 깨달았습니다. 이 글을 보시는 여러분은 좋은 툴로 편하게 작업하시길...)
아래는 노리와 제가 머리를 쥐어뜯으며 작성한 API 명세서입니다. 궁금하신 분들은 눌러보세요..ㅎㅎ (눈 건강을 위해 보시지 않는 것을 추천합니다)
https://docs.google.com/spreadsheets/d/1bPJdE0i-rjL-GX4rrXt58XGROvmyIbmn6gMUzgfNFlQ/edit?usp=sharing
- JDBC Template
요구사항에 적혀있는 기술 스택입니다. JPA로 정해지지 않은 이유는 아직 현업에서도 많이 쓰이고, 추상화된 개념을 조금 더 Low한 레벨에서 경험해보라는 의도인 것 같습니다. 물론 JPA가 JDBC template의 상위호환 개념이라는 것은 아닙니다.
순수 JDBC와는 다르게 커넥션이나 statement를 만들어주지 않아도 간편하게 작업할 수 있어서 좋았습니다. 중복된 코드가 줄어드니 가독성이 올라가고 그에 따라 실수는 줄고 능률이 상승했습니다. 누군지 모를 선배 개발자분들께 감사합니다...ㅎㅎ
아직 JPA를 깊이 학습하지 못해서인지 JDBC Template도 충분히 좋은 솔루션인 것 같습니다. ORM에 발을 들이면 또 생각이 달라지려나요? 추후 이 글을 보고 과거의 저를 비웃을지도 모르겠네요 ㅎㅎ
queryForObject() 메서드로 수행하는 select 문의 결과가 비어있을 때 EmptyResultDataAccessException 이 발생하는데, 이 예외처리를 깔끔하게 하는 것에 조금 애를 먹었습니다. 평범하게 try - catch로 예외만 잡고 다음 코드를 수행하면 될 것이라 생각했지만 '좋은 코드가 아니다' 라는 리뷰를 받고 해결책을 찾아봤습니다. 아직도 학습이 많이 필요한 부분이지만, 일반 query()로 빈 리스트를 반환하거나 DataAccessUtils의 singleResult() 메서드를 활용하는 방법도 있었습니다. 잠깐 맛만 봤지만 배울 것이 너무 많다는 것을 느꼈네요!
- 첫 번째 기술적 도전, Linked List !!
이번 투두리스트 미션에서는 까다로운 요구사항이 있습니다. 작성한 TODO 리스트의 순서와 위치를 변경하는 것이 가능해야 하고, 이것이 서버에 반영되어 어플리케이션이 껐다 켜져도 유지되어야 합니다.
처음에는 Card를 저장하는 테이블의 Column에 Position 속성을 하나 추가해서 관리하는 방법이 떠올랐습니다. Server 측에서도 위치를 까다롭게 관리할 필요 없이 그대로 내려주고 Client도 Position 정보를 보고 카드를 정렬하면 되니까요. 하지만, 이 방법은 너무 비효율적이었습니다.
만약 1번 인덱스부터 100만번째 인덱스의 카드가 존재하는 TODO에서, 100만번째 카드를 1번 인덱스로 옮긴다면?
-> 포지션의 update가 100만번 일어나야 합니다. 사용자가 단순히 카드를 맨 처음으로 보냈을 뿐인데 DB가 불탑니다.
두 번째 방법은, Position 속성을 부여하는 것이 아니라 카드의 id로 정렬하는 방법입니다. 간략한 방법은 다음과 같습니다.
처음부터 카드의 범위를 넓게 부여하고 이동이 일어날 때마다 그 사잇값으로 이동시킵니다. 배치라는 개념을 통해 사용자가 작업하지 않을때나 카드의 사이 값이 임계점에 다다랐을 때 다시 카드의 범위를 재정렬합니다.
예를 들어, 첫번째 카드가 삽입될 때 부여되는 id가 1000이라면, 다음에 삽입되는 카드의 id는 2000입니다.
id가 3000번인 카드를 1000과 2000사이에 이동시키려 한다면, (1000 + 2000) / 2의 값으로 id를 업데이트 시킵니다.
만약 이동 작업이 반복되어 카드 사이에 간격이 없게 된다면 ( ex, 1000번과 1002번 사이로 이동을 시도하는 경우) 테이블을 순회하며 다시 카드의 id값을 1000 단위로 재정렬합니다.
아주 좋은 아이디어라고 생각합니다. 마스터 호눅스 말로는 실제로 현업에서도 많이 사용하는 방법이라고 합니다!!
그런데 무언가 제 맘에는 들지 않았습니다. 저희 팀원들도 다행(?)히 동의해주었습니다. 그래서, 저희 팀은 마지막 방법을 채택했습니다. 카드 저장 형태를 연결 리스트 형식으로 순서를 유지하게 하는 것입니다.
Card 테이블에 next_card_id 속성을 추가하고 데이터의 삽입, 삭제, 이동이 일어날 때 마다 자료구조 연결리스트를 유지하는 방법과 동일하게 로직을 수행했습니다. 1번 카드는 2번을 바라보고, 2번은 3번을 바라보고 ..... X번은 null을 바라봅니다. 이론상 O(1) 연산으로 이동을 구현할 수 있습니다.
문제는, 구현의 난이도였습니다... 단순히 Card 데이터를 하나 추가하려고 해도 일반 삽입과는 다르게 next_card_id 를 확인해야 하므로 쿼리를 몇개는 더 날려야 했습니다. (카드 이동을 담당하는 Service에서는 쿼리를 5개나 날려야 했습니다 -_-) edge-case도 문제였습니다. 데이터를 마지막으로 이동시킬때, 데이터가 없는 곳에 이동시킬 때 등등... NPE와의 전쟁이었습니다. 그래도 다른 백엔드 팀원들이 참신하다고 칭찬해줄 때는 뿌듯해서 몸둘바를 몰랐습니다 ㅎㅎㅎㅎ
물론 MySQL로 한 컬럼이 다른 컬럼을 참조하는 식의 정확한 개념의 연결리스트를 구현한 것은 아닙니다. 단순히 next_card_id를 갖고있고 이걸 그대로 클라이언트에게 내려주면 순서를 조합해서 사용합니다. 그래도 이동 연산을 O(1)에 해낼 수 있는 것에 기뻤습니다. !!
- 두 번째 기술적 도전, Spring Interceptor !!!
이번 미션에서 또 하나의 화두는 사용자의 Action Log를 남기는 것이었습니다. Client는 이동, 삭제, 수정, 등록 등 API를 자유롭게 호출하여 사용하고 Server는 API 요청을 받아와 알아서 적절한 Log를 테이블에 저장해야 합니다. 물론 Log를 요청하면 잘 응답해줘야 하죠.. 이 기능을 구현하기까지의 의식의 흐름은 이렇습니다.
- 테이블에 저장하는 로그를 남긴다고? 서비스단에서 중복코드가 많아지겠다!
- 로그는 공통 처리니까 당연히 Interceptor에서 처리하면 중복을 많이 줄일 수 있겠군!
- 서비스에서 HttpServletRequest에 Log데이터를 담아서 인터셉터에 넘기면 알아서 해주겠다!
- 컨트롤러가 정상적으로 호출이 종료되었을 때만 Log를 남겨야 하니 postHandle() 메서드에 로직을 구현하면 되겠군
- 기능 완성! 역시 개발은 즐거워~
......인 줄 알았습니다.. Dion 선배님의 매운 맛 리뷰를 맛보기 전까지는.........
위 의식의 흐름 결과물의 문제점은 크게 두 가지 입니다.
1. 컨트롤러에서 HttpServletRequest를 직접 핸들링합니다.
-> 이런 low한 데이터는 최대한 건드리지 않는 것이 신상에 좋습니다. 뭐 아직까지는 괜찮습니다.
2. 컨트롤러에서 요청을 잘 처리한 후 Log를 남기기 전에 서버가 죽는 경우의 대비책이 없다.
-> 아주 크리티컬한 문제점입니다. 뭐 이런 것 까지 대비해야 하는가 생각하실 수 있겠지만..(제가 그랬습니다) 데이터의 무결성이 훼손되는 것은 개발자 입장에서 아주 치욕스러운 실수입니다.
결론부터 말씀드리자면, 순수 텍스트 로깅이 아닌 Log 데이터를 테이블에 남기는 로깅은 Service 단에서 처리하거나 AOP를 적용해야 합니다. 롤백의 실마리가 조금이라도 있기 때문입니다.
Spring에서는 단순히 메서드 위에 @Transactional 어노테이션을 붙이는 것만으로도 알아서 트랜잭션을 지원하고 롤백 기능도 개발자가 명시적으로 정할 수 있습니다.
그래서, 결국.... 힘들게 작성한 애정이 가득한 제 Interceptor에 있던 코드들을 Service 단으로 다 옮기게 되었습니다. 뼈아프지만 새로운 지식을 얻게 되어 감사하네요.
아직 AOP와 Transaction에 대해서는 공부가 많이 필요하지만, 적어도 Interceptor의 한계를 체감할 수 있는 좋은 삽질의 시간을 보내서 참 감사했습니다.
- AWS EC2
배포를 위해 EC2를 통해 jar 파일을 실행하려 했습니다.
그래서, EC2에 리눅스 인스턴스를 생성하고 git clone으로 코드를 복사해왔습니다.
./gradlew build
그런데, 빌드를 진행하니 30분이 넘는 시간동안 0%에서 빌드가 진행이 되지 않습니다.
인스턴스 재부팅 -> 빌드실패
테스트코드를 disable하는 설정으로 다시 build 시도 -> 빌드실패
아, 프리티어 인스턴스는 정말 쓸게 못되구나 하며 슬랙 질문방에 질문을 올렸습니다.
테스트 스킵은 이미 시도해 보았습니다.
로컬에서 빌드해서 scp로 jar를 전송하는 방법은 뭔가 지는 느낌(-_-) 이라 다른 방법을 찾아봤습니다.
결론부터 말씀드리면... MySQL 서버가 백그라운드에 돌아가면서 메모리를 너무 많이 차지하고 있던 원인이었습니다. 인스턴스의 메모리가 1기가인데, 혼자 600MB정도를 점유하는 바람에 빌드가 제대로 되지 않았습니다..
허무하기도 하고 뿌듯하기도 하고 제 마음도 모르고 build progress bar는 성실히, 빠르게 잘 올라갔습니다. ㅎㅎ
이제 서버는 떴는데, 접속이 되지 않습니다.
' 아, 무언가 구현이나 설정을 잘못 했구나... 뭐가 문제지...? ' 하며 해결책을 찾아봅니다.
보안그룹 설정을 닳도록 수정하고, 인바운드 아웃바운드 규칙을 열나게 편집합니다. 그런데도 접속이 되지 않습니다. 심지어 터미널에 로그도 남지 않습니다....
결론은, 접속 포트 문제였습니다. ㅎㅎ
HTTP에서 사용하는 기본 요청 포트는 80번인데, 서버에서 기본적으로 8080번 포트로 요청을 받도록 되어있었습니다.
너무 기초적이고 귀여운 실수처럼 보이시죠..? 맞습니다.... 그래도 이렇게 크게 실수 한 번 하면 다시는 잊지 않습니다. 저는 이제 포트를 헷갈리지 않는 개발자입니다. (농담입니다)
8080포트를 명시해서 요청을 보내거나,
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
위 명령을 수행하면 80포트로도 정상 접속이 가능합니다.
이 외에도 표준시간 이슈 등 여러가지가 있기는 한데, 말 줄이겠습니다.....ㅎㅎ..
- 브랜치 전략, github project
깃 덕분에 머리가 조금 더 빨리 빠진 2주입니다.
안드로이드와 백엔드가 같은 저장소에서 브랜치를 나눠 커밋을 진행하고, upstream 같은 브랜치에 PR을 보내고, merge 후 다시 받아와서 또 따로 작업하고 다시 PR을 보내고..
아주 간단한 작업처럼 보이지만 수많은 conflict 해결과 rebase, cherry-pick, 잘못된 merge를 선택한 과거의 나를 욕하는 현재의 나.... 등등 참 많이 삽질했던 것 같습니다.
솔직히 다시 해도 자신이 없습니다. ㅎㅎㅎㅎㅎ 꾸준히 공부해야 할 부분입니다. 과정이 끝난다면 프로깃을 한번 떼야할 것 같습니다. 많이 도와준 jerry 감사하고 사랑합니다
그리고, 처음으로 github project, 리드미 문서화를 통한 개발 효율 상승..? 을 시도해봤습니다.
개발하랴 해야할일 정리하랴,, 리드미 신경쓰랴 발표자료 만들랴.... 정신이 아주 없었습니다. 개발자는 개발만 잘하면 되는줄 알지는 않았지만 이정도 일줄은,,, 몰랐습니다.... 그래도 소프트 스킬이 점점 늘고 있는 것 같아 감사한 2주였습니다.
- Java와 Spring
곧 취직하게 된다면 누군가에게 ' 나 Java 개발자야! ' 혹은 ' 나 Spring 개발자야 ' 라고 할텐데, 아직은 많이 부족합니다.
@RequestBody와 @ModelAttribute의 차이점을 이번에서야 정확하게 알게되었고, validation, interceptor, aop, 예외처리, Service 테스트, Enum 등 많은 개념들을 조금씩 배워가고 있습니다. 앞으로 30년은 백엔드 개발자로 살아갈텐데 너무 조급해지는 면이 있는 것 같습니다. 아직은 영한주도개발(인프런최고)이 유효한 초보 코더니까 꾸준히 차근차근하려 합니다.
'회고' 카테고리의 다른 글
말하는 감자의 13개월 개발자 취준 회고 (14) | 2023.02.09 |
---|---|
코드스쿼드 마스터즈 백엔드[JAVA] 코스 마무리 회고 [2022.06.10] (16) | 2022.06.14 |
코드스쿼드 마스터즈 백엔드[JAVA] 코스 중간 회고 [2022.04.03] (2) | 2022.04.04 |
코드스쿼드 마스터즈 백엔드[JAVA] 코스 2주차 회고 [2022.01.16] (0) | 2022.01.17 |
코드스쿼드 CS02 - 가상머신 리눅스, 쉘 스크립트 (0) | 2022.01.06 |