2023년은 20대 동안 개발
면에서 가장 활발하게 다양한 활동을 경험한 한 해이자, 휴식이 적은 한 해였다.
정말 순식간에 1년이 지나가버린 것 같다 🤔.
그동안 내가 무엇을 배웠고 어떤 성장을 했는지 하나씩 정리해보자. ✍🏻
2023년에 내가 공부하면서 배운 내용을 2023 Dev History에도 기록해두었다.
2023년에 참여한 모임
1월 ~ 5월: 신입 개발자를 위한 CS 스터디 모임
3월 ~ 11월: 히빗(팀 프로젝트)
(8월: 대학교 졸업)
4월 ~ 9월: 우리FISA '클라우드 서비스 개발자'
8월 ~ 9월, 12월: 굿프렌즈(팀 프로젝트)
11월 ~ 12월: 우아한스터디 2023 겨울시즌 '내 코드가 그렇게 이상한가요?'
12월: 글또 9기 '글 쓰는 또라이가 세상을 바꾼다! 글쓰는 개발자 모임'
2023년 1월에 시작한 신입 개발자를 위한 CS 스터디 5월 말까지 5개월 동안 지속적으로 운영해왔다. 그 당시에는 컴퓨터 기반 지식이 부족했고, 배워야 할 것이 너무 많아서 운영체제, 데이터베이스, 네트워크와 같은 컴퓨터 공학의 필수 과목을 매월 1개씩 스터디원들과 공부해나갔다.
스터디를 처음 운영해보는 경험이라 깊이 있는 학습과 철저한 기록을 목표로 삼았다.
그래서 우리 스터디의 목표
는 CS 기본 지식들을 깊이 있게 습득하고, 공부한 내용을 설명하여 죽은 지식이 아닌 살아있는 지식으로 바꾸고자 했다.
여러 시행착오를 거치면서 더 나은 개선 방향을 찾고, 이러한 경험을 Issues와 Wiki에 정리해놓았다.
Issues에는 주제와 내용을 매주 정하고, 각 스터디원이 주제를 정해 발표하는 내용을 기록했다.
Wiki에는 외부인도 쉽게 이해할 수 있도록 주제를 카테고리별로 정리하고, 주차별로 기록했다.
5개월 동안 진행한 뒤 6월부터는 다른 교육과 사이드 프로젝트로 인해 참석하지 못했지만, 남아있는 스터디원들이 적극적으로 활동하여 12월 31일 현재까지 총 591개의 Star를 받게 되었다. 이는 1-3월에 80개, 5월에 150개, 11월에 500개로 거의 매일 Star가 증가한 것을 보며 뿌듯함과 신기함을 느끼게 되었다.
이 스터디가 많은 Star를 받은 이유는 다른 CS 스터디 모임보다 CS 지식을 깊게 공부하고 정리한 점과 외부 사람이 이해하기 쉽도록 작성하려고 노력한 점이라고 생각한다. 현재까지도 스터디원분들끼리 매주 온라인 회의로 스터디를 진행하시는데, 이런 분들과 같이 스터디를 할 수 있어서 영광이라고 생각한다 👏🏻.
해당 스터디에서 배운 내용은 아래와 같다.
신입 개발자를 위한 CS 스터디 모임에서 진행했던 내용
- 데이터베이스 - 학교에서 배운 전공 서적, 데이터베이스 개론, 면접을 위한 CS 전공지식 노트 + 그 외에 각종 레퍼런스 자료들
- 운영체제 - 학교에서 배운 전공 서적, Operating Systems: Three Easy Pieces , 운영체제와 정보기술의 원리, 면접을 위한 CS 전공지식 노트 + 그 외에 각종 레퍼런스 자료들
- 네트워크 - 학교에서 배운 전공 서적, 면접을 위한 CS 전공지식 노트 + 그 외에 각종 레퍼런스 자료들
- 자료구조 - 학교에서 배운 전공 서적, 면접을 위한 CS 전공지식 노트 + 그 외에 각종 레퍼런스 자료들
Spring, JPA 부분은 김영한의 스프링 완전 정복에 있는 강의를 혼자서 들으면서, 관련 내용을 블로그에 정리했다.
메모리: 119 MB, 시간: 49.53 ms - Answer Code1
메모리: 98.7 MB, 시간: 4.72 ms - Answer Code2
import java.util.*;
class Solution {
public int solution(int[] queue1, int[] queue2) {
Queue<Integer> que1 = new LinkedList<>();
Queue<Integer> que2 = new LinkedList<>();
long sum1 = 0, sum2 = 0;
for(int i = 0; i < queue1.length; i++) {
sum1 += queue1[i];
que1.offer(queue1[i]);
}
for(int i = 0; i < queue2.length; i++) {
sum2 += queue2[i];
que2.offer(queue2[i]);
}
int count = 0;
while(sum1 != sum2) {
count++;
if(sum1 > sum2) {
int value = que1.poll();
sum1 -= value;
sum2 += value;
que2.offer(value);
} else {
int value = que2.poll();
sum1 += value;
sum2 -= value;
que1.offer(value);
}
if(count > (queue1.length + queue2.length) * 2) return -1;
}
return count;
}
}
각 큐의 합이 같을 때까지 반복문을 돌려줬다.
또한 원소의 합이 같지 않는 경우에는 return을 -1로 두는 특수 예외 조건이 존재한다.
분기 조건이 (queue1.length + queue2.length) * 2
인 이유는 양쪽 큐 길이를 전부 돌았을 횟수로 잡았기 때문이다.
즉, 각 큐에서 원소를 추출하고 집어넣는 작업이 최대로 반복될 수 있는 횟수를 고려한 것이다.
최악의 경우, 한 큐의 모든 원소를 다른 큐에 집어넣어야 할 수 있으므로, 이때 루프의 최대 횟수는 큐의 길이의 2배가 된다.
합계(sum)가 long 형으로 한 이유는 제한사항
에서 queue1, queue2 원소의 최대 범위가 10^9이므로, 합 계산 과정 중 산술 오버플로우 발생 가능성이 있어서이다.
import java.util.*;
class Solution {
public int solution(int[] queue1, int[] queue2) {
int[] totalQueue = new int[queue1.length + queue2.length];
long queue1Sum = 0;
long queue2Sum = 0;
for(int i = 0; i < queue1.length; i++) {
int val = queue1[i];
totalQueue[i] = val;
queue1Sum += val;
}
for(int i = queue1.length; i < queue1.length + queue2.length; i++) {
int val = queue2[i - queue1.length];
totalQueue[i] = val;
queue2Sum += val;
}
if((queue1Sum + queue2Sum) % 2 == 1) return -1;
int count = 0;
int left = 0;
int right = queue1.length;
long half = (queue1Sum + queue2Sum) / 2;
while(left < right && right < totalQueue.length) {
if(queue1Sum == half) {
return count;
} else if(queue1Sum > half) {
queue1Sum -= totalQueue[left++];
} else {
queue1Sum += totalQueue[right++];
}
count++;
}
return -1;
}
}
이 글은 제미니의 개발실무의 Git 형상 관리 + 작업 단위와 PR 코드 리뷰 + 협업을 잘하는 개발자 영상을 보면서 제 생각과 같이 정리한 글입니다.
(시청자) 질문. 큰 규모의 작업을 할 때 commit 해야 할 작업 task를 나누는 기준이 궁금합니다.
- commit 할 작업 단위를 먼저 생각해보고 -> 그 단위로 작업을 하면서 중간중간 commit한다.
- 그냥 한번에 기능 단위 개발을 하고 -> 이후에 작업을 쪼개면서 commit한다.
리뷰
시스템 전체를 개발한다고 가정을 해보자. 그러면 하나의 task로 담기에는 굉장히 클 수 있다.
그렇기 때문에 먼저 작업 단위 자체
를 잘 나누는게 가장 중요하다.
리뷰
라는 기능이 있다면,
리뷰 등록, 리뷰 삭제와 같은 각 세부 기능에 대해서 하나의 작업
이라 생각하고, 그러한 작업을 Issue & Branch & PR
로 가져가는게 좋다.
큰 규모의 작업 자체가 있으면, 이는 여러가지 어려움을 만드는 것 같다.
추가/수정된 파일과 커밋 갯수가 많을 수록 다른 사람이 코드 리뷰하는게 까다로울 수 있다.
제민님의 의견: 같이 일하는 동료가 내 코드를 쉽게 이해할 수 있도록 task를 작게 가져갈 것 같다.
작업 단위를 작게 나누고, 위에 질문주신 2가지를 고민할 것 같다.
작업 단위를 작게 엄청 잘 나누게 되면, 애초에 커밋에 대한 생각을 많이 안해도 되는 경우가 있는 것 같다.
흔히 많이 하는 실수가 신규 기능 개발과 리팩터링하는 것을 하나의 task
라고 생각하고 작업하는 경우가 있다.
이러한 실수를 예방하기 위해 한 가지 작업을 딱 정해서, 최대한 작게 가져가면 커밋에 대한 고민은 크게 하지 않아도 된다.
즉, 신규 기능 개발과 리팩터링을 각각의 작업 단위로 생각해야 한다. -> task1 : 신규 기능 개발 / task2 : 리팩터링
결론적으로 질문 주신 것에 대해 답변을 하면, 1번과 2번을 다 쓰긴 한다.
어떻게 하면 동료가 코드 리뷰하기 더 좋을까에 대해서 커밋과 PR를 나눈다고 보시면 될 것 같다.
개인적으로 2번을 많이 쓴다. 2번: “그냥 한번에 기능 단위 개발을 하고 -> 이후에 작업을 쪼개면서 commit한다.”
커밋하는 부분도 협업 스킬 중 하나라고 생각한다.
아래와 같이 커밋을 작성하면 리뷰하는 사람 입장에서 힘들 수 있다.
User 클래스에 대한 작업 <- 하나의 task
--- 아래부터는 커밋 내용
feat: User 클래스가 수정!
feat: User 클래스가 수정!
feat: User 클래스가 수정! (리팩토링 포함)
feat: User 클래스가 수정!
...
feat: User 클래스가 수정!
[PR]
수정 파일이 50개
커밋이 10개
만약 작업 단위가 작았다면, 위에서처럼 발생할 수 없다.
수정 파일이 많고, 같은 클래스에서 여러 번의 커밋을 작성하면, 리뷰할 사람 입장에서는 이해하기가 어려울 수 있다.
[PR 올릴 때 커밋 정리 기준 중 하나] - 한 클래스는 한 파일에서만 수정된다.
보통 커밋은 아래와 같이 진행한다.
하나의 작업에 대한 커밋을 여러번 작성한다. -> 본인을 위해
어느 정도 커밋을 작성하고 PR을 올리기 전에, 지금까지 작성한 커밋들을 정리한다.
PR을 올리면서 동료로부터 코드 리뷰를 받고, 추가적인 커밋을 한다. -> 동료를 위해
리뷰가 끝나면, 지금까지 작성한 커밋을 한번 더 정리를 한다. -> 회사 자산 관리 + 미래의 동료를 위해
리뷰
에 대한 기능을 개발한다고 가정했을 때, 작업 단위를 나눈다. -> 리뷰 등록, 리뷰 조회, 리뷰 수정, 리뷰 삭제
그런 다음, 작업에 대한 Branch를 나누고 PR도 여러개 올린다.
리뷰 등록에 대한 Branch명: review-add
리뷰 삭제에 대한 Branch명: review-remove
그리고 세부 기능에 대해서 Branch를 연계적으로 생성한다.
이전 굿프렌즈 팀 프로젝트에서는 우리만의 Git flow 전략을 만들면서, 형상 관리를 나름 신경썼다고 생각했다.
브랜치 면에서는 이 영상에서 다루는 거와 거의 비슷하게 작업을 진행했지만, 커밋 부분에 있어서는 ‘이 정도로 신경을 써야 하는구나’라는 생각이 많이 들었다.
최근에 주문
기능에 대해서 리팩터링 과정을 진행했는데, 작은 작업 단위 보다는 큰 작업으로 진행해서 아래와 같이 15개의 커밋 내용과 17개의 파일이 수정되었다.
(물론, 10월 이후부터는 팀원들을 제외한 나 혼자만의 리팩터링을 진행해서 더 신경을 안쓴것도 맞다..ㅎㅎ 😅)
다음에 팀 프로젝트를 진행한다면, 커밋
에 대해서도 신경쓰면서 작업을 진행하면 좋겠다는 생각이 들었다.
(해당 영상을 참고하시면 더 자세하게 설명해주시니 참고하면 좋을 것 같습니다!)
문제: PRODUCT
테이블과 OFFLINE_SALE
테이블에서
상품코드 별 매출액(판매가 * 판매량) 합계를 출력하는 SQL문을 작성해주세요.
결과는 매출액을 기준으로 내림차순 정렬해주시고
매출액이 같다면 상품코드를 기준으로 오름차순 정렬해주세요.
SELECT A.PRODUCT_CODE,
(A.PRICE * B.TOT_SALES_AMOUNT) AS SALES
FROM PRODUCT A
LEFT JOIN (
SELECT PRODUCT_ID
, SUM(SALES_AMOUNT) TOT_SALES_AMOUNT
FROM OFFLINE_SALE B
GROUP BY
PRODUCT_ID
) B
ON A.PRODUCT_ID = B.PRODUCT_ID
ORDER BY SALES DESC, PRODUCT_CODE ASC
문제를 풀기 위해서 해야할 작업들
첫 번째 단계는 OFFLINE_SALE 테이블에서 PRODUCT_ID 별 판매 수량을 집계한다.
이때 GROUP BY를 집계해서 각 상품별 수량을 산출한다.
SELECT PRODUCT_ID
, SUM(SALES_AMOUNT) TOT_SALES_AMOUNT
FROM OFFLINE_SALE B
GROUP BY
PRODUCT_ID
두 번째 단계는 앞에서 진행한 결과를 서브쿼리로 넣고, PRODUCT 테이블을 기준으로 LEFT JOIN을 해준다.
이 결과를 통해 각 상품별 총 판매개수와 상품별 금액을 알 수 있다.
SELECT *
FROM PRODUCT A
LEFT JOIN (
SELECT PRODUCT_ID
, SUM(SALES_AMOUNT) TOT_SALES_AMOUNT
FROM OFFLINE_SALE B
GROUP BY
PRODUCT_ID
) B
ON A.PRODUCT_ID = B.PRODUCT_ID
SELECT A.PRODUCT_CODE,
(A.PRICE * B.TOT_SALES_AMOUNT) AS SALES
FROM PRODUCT A
LEFT JOIN (
SELECT PRODUCT_ID
, SUM(SALES_AMOUNT) TOT_SALES_AMOUNT
FROM OFFLINE_SALE B
GROUP BY
PRODUCT_ID
) B
ON A.PRODUCT_ID = B.PRODUCT_ID
SELECT A.PRODUCT_CODE,
(A.PRICE * B.TOT_SALES_AMOUNT) AS SALES
FROM PRODUCT A
LEFT JOIN (
SELECT PRODUCT_ID
, SUM(SALES_AMOUNT) TOT_SALES_AMOUNT
FROM OFFLINE_SALE B
GROUP BY
PRODUCT_ID
) B
ON A.PRODUCT_ID = B.PRODUCT_ID
ORDER BY SALES DESC, PRODUCT_CODE ASC
8월 1일부터 9월 27일까지 팀 프로젝트를 진행했지만, 그 당시에는 ‘구현’에 초점이 맞춰졌기 때문에, 개개인의 코드의 품질에 제대로 신경쓰지 못하고 개발했다.
(초반에는 코드 리뷰를 열심히 해왔지만, 한정된 시간안에 구현해야 하는 압박감과 부담감에 후반으로 갈 수록 코드 리뷰를 제대로 못했다..)
그리고 좋은 코드가 무엇인지, 좋은 코드를 짜기 위해서 어떻게 리팩터링하는지에 대해서 제대로 알지 못했다.
그래서 팀 프로젝트가 끝난 이후에, ‘나중에 반드시 리팩터링 해야겠다!’ 고 다짐했다.
그렇게 한달이 지나고, 예상치 못한 좋은 기회로 11월부터 12월까지 우아한 스터디의 “내 코드가 그렇게 이상한가요?
” 주제에 참가하게 되어서, 좋은 코드가 무엇인지 배우게 되었다.
“내 코드가 그렇게 이상한가요?” 책을 읽으면서 스터디에 참가하는 분들과 토론하면서 좋은 코드로 개선하는 방법을 알게되었다.
(해당 책을 공부하면서 내 블로그: GoodGode에 따로 정리해두었다)
해당 스터디에서 배운 내용을 기반으로 기존에 구현했던 기능들, 특히 사용자 기반 기능(프로필, 상품, 주문, 신고)을 우선적으로 리팩터링을 진행했다.
주문
도메인에 대한 리팩터링 과정주요 기능 중 하나인
주문
에 대한 리팩터링 과정을 자세히 확인하고 싶다면, PR을 참고하자.
주문
도메인에서 본인이 판매 등록한 물품에 들어온 주문서 전체를 조회하는 API와 관련된 비즈니스 로직에 대한 리팩터링 과정을 정리하고자 한다.리팩터링 전)
주문
도메인에서 주문서 전체를 조회하는 API에 대한 비즈니스 로직(OrderService.class) - findAllOrder
# 목표
- [ ] 구조를 파악할 수 있도록 기존의 클래스/메서드/변수명 수정할 것 - 굿프렌즈 WIKI에 있는 [객체 및 메서드 생성 규칙](https://github.com/woorifisa-projects/GoodFriends/wiki/객체-및-메서드-생성-규칙)
- [ ] 불변 활용할 것
- [ ] 단일 책임 원칙을 지키기 위해 하나의 메서드에 한가지 일만 담당하도록 구현한다.
- [ ] 메서드 최대 길이 15줄 이내로 할 것
- [ ] 리팩터링 이후 불필요한 주석은 제거할 것
findAllOrder()
메서드명을 findAllMyProductOrders()
로 수정했다.[2] 그리고 findAllMyProductOrders()
메서드에서 유효성을 검사하는 로직을 validateOffenderAndMyProduct()
메서드를 생성해서 처리해주도록 수정했다.
추가적으로 예외 클래스명에 대해서도 직관적이고 이해할 수 있는 클래스명으로 수정했다. (예외 처리 클래스를 각 도메인의 비즈니스 규칙에 맞도록 커스터마이징했다)
기존) NotAccessProductException
-> 변경) InactiveUserAccessException
- 예외 메시지: ‘비활성화 상태인 유저로 해당 페이지에 접근이 불가능합니다’
기존) NotOwnProductException
-> 변경) InvalidProductOrderAccessException
- 예외 메시지: ‘본인이 등록하지 않은 상품의 주문서는 조회할 수 없습니다.’
[3] findAllMyProductOrders()
에는 여러 로직을 처리하고 있어서 단일 책임 원칙
에 위배되고 있다.
이를 해결하기 위해 첫 번째로 하나의 메서드에 한 가지 일만 담당하도록 로직을 수정했고, 동시에 중첩 if문을 제거했다.
handleNonSellProduct()
[4] 그런 다음, 두 번째로 map 부분에서 OrderProductResponse
클래스(이전 클래스명: OrderViewOneResponse
) 정적 팩토리 메서드
(of)를 추가하여 중복되는 부분을 최소화했다.
팩토리 메서드 패턴
(Factory Method Pattern)은 객체 생성을 처리하기 위한 디자인 패턴 중 하나다.
이 패턴을 사용함으로써 객체 생성과정이 클래스 외부로 추상화되어, 코드의 가독성이 높아지고 유지보수가 용이해진다.
또한 팩토리 메서드 패턴
은 객체 생성에 필요한 복잡한 로직을 한 곳에 모아둠으로써 중복을 줄일 수 있다.
of
메서드는 주로 정적(static) 팩터리 메서드로 사용되며, 해당 객체를 생성하고 초기화하는데 사용된다. 불변성(Immutability)을 강조하거나 특정한 시나리오에서 명확하게 사용할 수 있도록 하는데 주로 쓰인다.
findAllMyProductOrders()
, handleNonSellProduct()
두 메서드의 return 하는 부분에서 true/false 값은 판매 상태 여부를 나타내는 걸 의미하는데, 이를 private static final
로 상수화로 선언해서 아래와 같이 수정했다.findAllMyProductOrders()
메서드에서 주문 응답을 가져오는 로직을 getOrderProductResponses()
메서드로 추출했다.마지막으로 findAllMyProductOrders()
메서드에서 로직을 처리하는 순서에 맞게 조정했다.
그리고 handleNonSellProduct()
메서드 명에 대해서도 직관적으로 이해할 수 있게 beginDealForOrder()
으로 다시 수정했다.
이렇게 해서 주문
도메인에서 본인이 판매 등록한 물품에 들어온 주문서 전체를 조회하는 API와 관련된 비즈니스 로직에 대한 리팩터링을 진행했다.
# 목표 달성
- [x] 구조를 파악할 수 있도록 기존의 클래스/메서드/변수명 수정할 것 - 굿프렌즈 WIKI에 있는 [객체 및 메서드 생성 규칙](https://github.com/woorifisa-projects/GoodFriends/wiki/객체-및-메서드-생성-규칙)
- [x] 불변 활용할 것
- [x] 단일 책임 원칙을 지키기 위해 하나의 메서드에 한가지 일만 담당하도록 구현한다.
- [x] 메서드 최대 길이 15줄 이내로 할 것
- [x] 리팩터링 이후 불필요한 주석은 제거할 것
리팩터링 후)
주문
도메인에서 주문서 전체를 조회하는 API에 대한 비즈니스 로직(OrderService.class) -findAllMyProductOrders()
사실 주문
도메인에 대한 리팩터링 하기 이전에, 이미 프로필
, 신고
, 상품
에 대해 리팩터링을 진행했다.
스터디에서 배운 내용을 기반으로 기존 프로젝트(굿프렌즈)에서 사용자 기반의 기능들을 모두 리팩터링하면서 적용해봤다.
책에 있는 기술, 개념들을 적용하는 과정에서 시간이 많이 걸렸지만, 그 만큼 많은 경험을 얻게 되었다.
다음에는 이전에 테스트 코드 강의를 듣고 정리해둔 Practical Testing: 테스트 코드 작성 방법을 기반으로 굿프렌즈 프로젝트에 단위 및 통합 테스트 코드를 작성해보자!