2022년을 돌아보며
숨가쁘게 달려온 2022년도 벌써 끝이라는게 믿기지가 않는다.
2022년을 요약하자면 SSAFY 그자체다. SSAFY 내에서 경험한 것들을 정리해보자 한다.
알고리즘 스터디
1년동안 꾸준히 알고리즘 스터디를 진행했다. 알고리즘 스터디를 통해 여러 문제를 풀어보고, 한 문제에 대해 다양한 풀이를 접하면서 실력이 많이 늘었다. 하지만 알고리즘 공부에 시간 투자를 많이 하지 않아 아직도 기업 코딩 테스트를 통과하지 못한다는 점이 스스로에게 실망스러운 부분이다.
스터디장으로서 스터디를 어떻게 운영해야 스터디원들에게 도움이 될지 많이 고민했다. 전체적으로 스터디원들이 해이해진다 싶을 땐 쓴소리도 마다하지 않았고, 월간 회고를 통해 스터디원들이 배운 내용을 정리하고 코드 발표에 대한 피드백을 주고받을 수 있도록 했다. 자투리 시간에 찾아본 개발 관련 정보도 스터디원들에게 많이 공유했다. 이런 점이 스터디원들의 만족도가 가장 높은 부분 중 하나였던 것 같다.
잡페어 기간동안 1일 1알고리즘 인증제로 바꿔서 스터디를 진행했는데, 이때 나에게 필요한 공부를 집중적으로 할 수 있었던 것 같아 좋았다. 이후 프로젝트를 시작하면서 스터디를 빡세게 진행하지 않은 점이 아쉬움으로 남는다.
공통 프로젝트
공통 프로젝트를 시작할 때는 열정이 활활 불타오르던 시기여서 프로젝트 시작 2주 전부터 팀원들을 불러서 Git부터 GitWorkflow, Code Convention, JPA와 Spring Boot를 학습했다. 각자 학습한 내용을 정리해서 제출하고 무작위로 선정된 팀원이 공부한 내용을 설명한 후 서로 질의응답을 하는 방식으로 사전학습을 진행했다. 팀원들이 정리한 내용을 전부 보고 중요한 내용들을 요약해서 질의응답 후에 요약 정리를 해주면서 스스로 공부가 많이 됐다.
공통 프로젝트의 목표는 총 3가지였다.
1. 스크럼 적용
2. 코드 리뷰
3. 문서화
스크럼 적용
QR 체크인을 이용한 매장 내 혼잡도 제공 서비스 QRna 프로젝트에서 진행했던 프로세스를 기반으로 스크럼을 제대로 적용해보고자 했다. 스프린트를 정해서 주간 작업량을 정하고 데일리 스크럼을 진행하며 팀원 간 작업현황을 파악했다. 이런 방식으로 프로젝트를 진행하니 이전 프로젝트에서 팀원들의 진행현황을 알지 못해 한 파트만 작업이 끝나 다른 파트를 기다리던 일이 일어나지 않았다.
코드 리뷰
이전 프로젝트에서 코드 리뷰를 하면서 코드를 개선하거나 질의응답을 하면서 새로운 것을 알기도 하고, 잘못 알고 있던 것을 정정할 수 있었다. 그래서 프로젝트에 코드 리뷰 문화를 꼭 도입하고 싶었다. 다행스럽게도 팀원들이 모두 동의를 해주었고, 코드 리뷰 문화를 도입하게 되었다.
우리 팀의 전체적인 개발 프로세스는 아래와 같이 진행됐다.
1. 매주 월요일 스프린트 회의를 통해 Jira 이슈 생성
2. GitLab에 작업할 Issue 생성
3. 생성된 Issue 번호로 branch 생성 후 작업
4. 작업이 완료된 branch를 develop branch에 Merge Request 발행
5. 파트원들과 코드 리뷰 진행
6. 파트원들이 모두 approve 시 develop branch에 Merge
이러한 과정을 거쳐야해서 코드 리뷰를 하기 위해서는 작업 시간이 길어졌다. 하지만 열정 넘치는 팀원들을 만나 코드 리뷰 요청 시 10분 내로 코드 리뷰가 완료되었고, 우리 팀의 작업 속도는 내가 속한 반에서 가장 빨랐다.
코드 리뷰를 진행하기 위해서 다른 팀원들이 보기 좋은 코드를 작성해야 한다고 생각했고, 그렇기에 Code Convention과 Commit Convention을 정했다. 또한 백엔드 코드 가독성 향상을 위해 Stream API와 Optional API를 학습했다.
우리 팀의 개발 프로세스는 컨설턴트님과 코치님들도 인정해주셨고, Git과 Jira를 잘 활용하는 모습에 베스트 팀으로 선정되기도 하였다. 이렇게 팀이 잘 굴러간 데는 팀장님의 역할이 크다며 베스트 멤버로 선정되어 정말 뿌듯했다.
코드 리뷰를 하면서 의미 전달이 잘 되지 않아 불필요한 코드 수정 작업이 발생해 개발 속도가 더뎌지는 문제가 생겨 코드 리뷰 in 뱅크샐러드 개발 문화 에서 접한 "Pn룰"을 도입하게 되었다. 이후 소통이 훨씬 원활해졌다. 이후 프로젝트에서도 Pn룰을 도입해 코드 리뷰를 진행해 커뮤니케이션 비용을 줄일 수 있었다.
문서화
개발을 하다보면 기술을 적용하거나 에러를 해결하는 상황이 빈번하게 일어난다. 그러나 이를 기록해놓지 않으니 시간이 지나서 내가 어떤 기술을 어떻게 적용했는지, 어떤 에러 상황을 만났고 이를 어떻게 해결했는지 알 수 없었다. 그래서 프로젝트를 하면서 사소한 내용이라도 문서화를 하는 것을 목표삼았다.
아래는 내가 작성한 내용은 아니지만... 팀원이 이메일 인증을 구현한 것을 문서화한 내용의 일부다.
이 문서는 이후 프로젝트에서 이메일 인증을 구현할 때 계속 참고해서 도움이 굉장히 많이 됐다. 나 또한 서버 배포에 대해 정리해놨는데 이런저런 시도를 하면서 작성하다보니 정확하지 않은 부분도 있었다. (이 부분은 이후 프로젝트를 진행하면서 수정해나갔다.)
처음 생각했던 것보다 기술 적용이나 트러블 슈팅에 대한 문서가 2개 밖에 없어 다소 아쉬웠다. 또한 해당 문서를 "참고자료" 섹션에 올렸는데, 진짜 참고자료와 기술 문서나 트러블 슈팅 문서가 섞여 보기 좋지 않았다.
회고 및 팀원 피드백
프로젝트를 진행하면서 두 번의 팀원 피드백과 한 번의 회고를 진행했다. 특히 팀원 피드백이 나뿐만 아니라 팀원들에게 많은 도움이 됐다. 다른 팀원들한테 도움이 됐는지 내가 어떻게 아느냐고 생각할 것 같아 덧붙이자면 수료 후 공통 팀원을 만난 자리에서 "공통 프로젝트 팀원 피드백에 '처음에는 소극적인 모습이었는데, 시간이 지나고 적극적인 모습을 보여 좋았다. 처음부터 적극적으로 프로젝트에 참여하면 더 좋을 것 같다.'는 내용이 있어 다음 프로젝트부터는 초반에도 적극적으로 의견을 내려고 노력했다. 팀원 피드백이 도움이 많이 되더라."는 이야기를 들었다. 나 또한 팀원 피드백을 통해서 내가 부족한 점을 인지하고 개선할 수 있어 정말 좋았다.
회고 방식에는 5F를 사용했는데, 아래는 한 팀원의 Feelings에 적혀있던 내용이다.
해당 내용을 읽었을 때 누군가 잘했다고 말해주는 것보다 이루 말할 수 없이 기쁘고 뿌듯했다. 팀을 위해 고민했던 시간들이 헛되지 않았음을 증명받는 순간같아서 해당 내용을 공유받았을 때를 잊지 못할 것 같다.
공통 프로젝트는 수상이 목표가 아니었지만 이렇게 열정적인 팀원들과 함께 재밌게 프로젝트를 하다보니 우수상 수상이라는 영광까지 누릴 수 있었다. 개발을 시작하고 이렇게까지 열정적이었던 적이 있을까 싶을 정도로 열심히 했던 것 같아 앞으로 개발 인생의 원동력이 되지 않을까 싶다.
특화 프로젝트
특화 프로젝트는 가장 후회가 많이 남는 프로젝트인 것 같다. 이 시기에 하반기 공채가 쏟아지면서 프로젝트 초반에 온전히 프로젝트에 집중하지 못했다. 게다가 추석연휴가 끼면서 아이디어 회의 기간이 2주차까지 딜레이되어 마감일이 닥쳐서까지 개발하기 급급했던 기억이 있다. 팀장으로서 일정 관리에 소흘했기 때문에 좀 더 좋을 결과를 낼 수 있었는데 그러지 못한 것 같아 프로젝트 후 많이 반성했다.
아쉬웠던 것 만큼 좋았던 점도 많았다.
특화 프로젝트에서 좋았던 점을 꼽으라면 아래 4 가지다.
1. 적극적인 문서화
2. 발표
3. Spring REST Docs 적용
4. Enum 클래스 활용
5. Nginx URL 서버 이미지
적극적인 문서화
공통 프로젝트에서 기술 문서와 트러블 슈팅 문서가 다른 문서와 섞여있어 문서를 찾기 어려웠다. 그래서 기술문서와 트러블슈팅 섹션을 분리해 가독성이 매우 좋아졌다.
또한 공통 프로젝트에서 문서화를 많이 하지 못한 아쉬움이 남아 더욱 적극적으로 문서화를 했다. 특히 팀원들이 적극적으로 참여해주어 문서가 더욱 풍부해졌다. 이렇게 정리해둔 내용은 이후 프로젝트와 자기소개서 작성에 유용하게 쓰였다.
발표
다른 사람들은 잘 안믿지만 사실 난 발표 공포증이 있다. 그래서 공통 프로젝트 중간 발표를 내가 했었지만, 팀원들이 만족할 만한 발표를 보이지 못해 최종 발표를 다른 팀원이 맡았었다.
특화 프로젝트에서도 중간 발표를 내가 맡아서 진행했는데, 스스로도 만족할 만큼 이전보다 많이 발전한 모습을 보였다고 생각한다. 발표를 성공적으로 하기 위해서 대본을 작성해서 큰 소리로 말하는 연습을 했다. 반복적으로 말하면서 부정확한 발음을 고치고, 내용을 강조하기 위해 발화 속도를 조절했다. 이러한 연습이 발표에 도움이 많이 됐다. 이 발표 이후로 발표에 대한 자신감이 좀 생겼다. 그래서 최종 발표는 더욱 매끄럽게 진행했던 것 같다.
다만, 발표자료 준비에 시간을 많이 쏟아서 발표 전 시간이 부족해 팀원들의 피드백을 받지 않은 채로 발표를 진행한 점이 아쉬웠다. 팀원들의 피드백을 받아 발표 내용을 개선했으면 더 좋았을 것 같다.
Spring REST Docs 적용
TDD를 학습과 새로운 기술에 도전에 해보기 위해 Spring REST Docs를 적용해보았다. 이전 프로젝트에서 사용해본 경험이 있기 때문에 다른 파트원들에게 나만 믿으라고 큰 소리를 떵떵 쳤는데, 사용해 본 지도 오래돼서 그 때 당시에 Spring REST Docs를 완전히 이해하고 사용한 것이 아니었기 때문에 이를 학습하고, 프로젝트에 직접 적용하는 데 시간이 많이 소요됐다. 이는 개발이 지체된 이유 중 하나였다.
하지만 이를 이용해 API 명세서를 작성함으로써 API 명세서에 변경사항이 있을 때 코드 수정만으로도 변경사항이 즉각적으로 반영되어 클라이언트에서의 통신 이슈가 이전보다 훨씬 줄었다.
또한 테스트 코드를 작성함으로써 배포 전 에러 상황을 1차적으로 예방했다. 로컬에서 build 시 에러가 발생하면 배포 전에 수정해서 좀 더 안정적인 서비스를 운영할 수 있었던 것 같다.
Enum 클래스 활용
스터디에서 딱 한 번 Enum 클래스 활용해본 이후 잊고 있었는데, 이번 프로젝트에서는 조건문이 많이 쓰여 코드 가독성을 향상시킬 방법을 고민하다가 Enum 클래스가 생각나 코드에 적용해봤다. 우아한 형제들 기술 블로그 - Java Enum 활용기 및 이전 스터디 과제를 참고해 직선 경로와 비직선 경로를 처리하는 "PathUtil"을 사용했다.
| PathUtil.java
import com.hanssarang.backend.common.domain.ErrorMessage;
import com.hanssarang.backend.common.exception.BadRequestException;
import com.hanssarang.backend.hiking.controller.dto.PathResponse;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public enum PathUtil {
LINESTRING("LINESTRING") {
@Override
public boolean isCompleted(String path, double latitude, double longitude) {
String[] coordinates = split(path, PARENTHESIS, COORDINATE_DELIMITER);
return isInEndPoint(coordinates[coordinates.length - 1], latitude, longitude);
}
@Override
public double[] getCentralCoordinate(String path) {
String[] coordinates = split(path, PARENTHESIS, COORDINATE_DELIMITER);
return toCentralCoordinate(coordinates[coordinates.length / 2]);
}
},
MULTILINESTRING("MULTILINESTRING") {
@Override
public boolean isCompleted(String path, double latitude, double longitude) {
String[] lines = split(path, MULTI_PARENTHESIS, MULTILINESTRING_COORDINATE_DELIMITER);
return Arrays.stream(lines)
.map(line -> line.split(COORDINATE_DELIMITER))
.anyMatch(coordinates -> isInEndPoint(coordinates[coordinates.length - 1], latitude, longitude));
}
@Override
public double[] getCentralCoordinate(String path) {
String[] lines = split(path, MULTI_PARENTHESIS, MULTILINESTRING_COORDINATE_DELIMITER);
String[] coordinates = lines[0].split(COORDINATE_DELIMITER);
return toCentralCoordinate(coordinates[coordinates.length / 2]);
}
};
private static final String PATH_DELIMITER = " ";
private static final String COORDINATE_DELIMITER = ", ";
private static final String MULTILINESTRING_COORDINATE_DELIMITER = "\\), \\(";
private static final String DELETE = "";
private static final String OPENING_PARENTHESIS = "(";
private static final String CLOSING_PARENTHESIS = ")";
private static final int TYPE = 0;
private static final int LATITUDE = 0;
private static final int LONGITUDE = 1;
private static final Pattern PARENTHESIS = Pattern.compile("[()]");
private static final Pattern MULTI_PARENTHESIS = Pattern.compile("[()]{2,}");
private static final Pattern PATH_TYPE = Pattern.compile("(LINESTRING)|(MULTILINESTRING)[ ]");
private final String type;
public abstract boolean isCompleted(String path, double latitude, double longitude);
public abstract double[] getCentralCoordinate(String path);
public static PathUtil find(String path) {
return Arrays.stream(values())
.filter(value -> value.type.equals(path.split(PATH_DELIMITER)[TYPE]))
.findAny()
.orElseThrow(() -> new BadRequestException(ErrorMessage.WRONG_PATH));
}
public static String toLineStringForm(List<PathResponse> path) {
return LINESTRING.type + PATH_DELIMITER + OPENING_PARENTHESIS + path
.stream()
.map(point -> point.getLatitude() + PATH_DELIMITER + point.getLongitude())
.collect(Collectors.joining(COORDINATE_DELIMITER)) + CLOSING_PARENTHESIS;
}
protected boolean isInEndPoint(String path, double latitude2, double longitude2) {
double latitude1 = getCoordinate(path, LATITUDE);
double longitude1 = getCoordinate(path, LONGITUDE);
double theta = longitude1 - longitude2;
double distance = Math.sin(toRadians(latitude1)) * Math.sin(toRadians(latitude2)) + Math.cos(toRadians(latitude1)) * Math.cos(toRadians(latitude2)) * Math.cos(toRadians(theta));
return (toDegrees(Math.acos(distance)) * 60 * 1.1515) * 1609.344 < 10;
}
protected String[] split(String path, Pattern parenthesis, String delimiter) {
return parenthesis.matcher(PATH_TYPE.matcher(path).replaceAll(DELETE))
.replaceAll(DELETE)
.split(delimiter);
}
protected double[] toCentralCoordinate(String coordinate) {
return Arrays.stream(coordinate.split(PATH_DELIMITER)).mapToDouble(Double::parseDouble).toArray();
}
private double getCoordinate(String path, int coordinateInfo) {
return Double.parseDouble(path.split(PATH_DELIMITER)[coordinateInfo]);
}
private double toRadians(double degree) {
return (degree * Math.PI / 180.0);
}
private double toDegrees(double radian) {
return (radian * 180 / Math.PI);
}
}
위 Enum 클래스를 아래와 같이 사용하여 조건문의 사용을 줄여 코드 가도성을 높일 수 있었다.
double[] centralCoordinate = PathUtil.find(path).getCentralCoordinate(path);
Nginx URL 서버 이미지
클라이언트에서 이미지를 띄우기 위해 항상 이미지를 바이트 배열로 변환해서 반환했었는데, 이 방법을 사용하니 몇 천개의 이미지를 한 번에 반환하는데 시간이 너무 오래 걸렸다.
그래서 Nginx 설정을 통해 서버 이미지를 URL로 직접 접근함으로써 로딩 시간을 단축할 수 있었다.
location /images/mountain {
alias /home/ubuntu/img/mountain;
}
위와 같은 설정으로 '서비스 도메인/images/mountain/이미지파일명'으로 요청 시 지정된 경로에서 이미지 파일을 찾아 반환해주도록 하였다.
특화 프로젝트는 끝나고 바로 자율 프로젝트가 시작되어서 팀원 피드백조차 하지 못했다. 이건 곧 죽어도 했어야되는데 SSAFY 프로젝트 통틀어서 가장 잘못한 일이다.
아쉬운 점도 많고, 힘들었던 프로젝트지만 가장 많이 웃으며 개발했다. 특히 분위기 메이커 오빠가 있어서 밤새 개발하면서도 힘들다는 생각을 해본 적이 없는 것 같다. 동료의 중요성을 여실히 느낀 프로젝트다.
시간이 많이 부족했던 만큼 개선하고 싶은 코드가 많다. 찬찬히 들여다보며 리팩토링을 하자.
자율 프로젝트
자율 프로젝트는 굉장히 빠르게 진행되어 아이디어 회의에도, 개발에도 시간이 많이 부족했다. 특히 도메인이 없어지니 오히려 아이디어 회의가 정말 힘들었다. 게다가 컨설턴트님이 원하는 방향과 우리가 원하는 방향이 달랐는데, 팀장으로서 뚝심있게 팀의 방향성을 지키지 못해 갈팡질팡하느라 더욱 지체된 게 아닌가 싶다.
자율 프로젝트의 목표는 "실사용자" 받아보기였다.
트래픽 관리를 해보기 위해 사용하기 쉬운 간단한 서비스를 기획했는데, 생각보다 볼륨이 커지는 바람에 프론트엔드의 작업이 오래 걸려 실제 사용자를 받아본 기간이 매우 짧았다. 또한 실제 트래픽을 관리하기 위한 지식이 전무해서 로그를 찍는 것 외에는 트래픽에 대한 관리가 전혀 안됐다.
하지만 자율 프로젝트를 하면서 이전 프로젝트에서는 한 번도 해보지 못했던 Refresh Token을 적용해본 경험은 굉장히 좋았다.
Refresh Token 적용
이전 프로젝트에서 모두 Access Token만을 사용해 로그인 관리를 했었는데, 해당 프로젝트에서 Refresh Token을 처음으로 적용해봤다. 이때 Cookie를 사용해 Refresh Token을 클라이언트에 노출시키지 않아 XSS공격을 막을 수 있다는 사실을 처음 알게 되었다.
원래 목표는 Spring Security를 적용해 Token을 관리하는 것이었지만, Spring Security 적용에 실패했다. 이후에 꼭 Spring Security에 대해서도 학습해서 Spring Security로 Token 관리하는 것을 구현해봐야겠다.
팀원들의 취준 이슈로 인해 프로젝트에 가장 힘을 빼고 임했다. 그래서 가볍게 트래픽 관리를 해보고자 했는데, 그러지 못해서 아쉽다. 꼭 관련 내용을 공부해서 작은 서비스를 만들어서 배포하고 관리해볼 것이다.
다른 프로젝트에 비해 회고할 내용이 적다는 것이 해당 프로젝트에 열심히 임하지 않았음을 의미하는 것 같아 반성하게 된다.
무수히 많은 입사 지원
테크 기업 공채와 인턴은 대체로 다 지원한 것 같다. 서류에 결격사유만 없으면 코딩 테스트는 볼 수 있기 때문에 자기소개서 쓰는 연습할 겸, 코딩 테스트 연습할 겸 많은 기업에 지원했다. 하지만 코딩 테스트의 벽을 단 한 군데도 넘지 못했다. 이건 내가 노력하지 않았다는 증거겠지..
테크 기업 외에도 삼성전자, SK하이닉스, LG CNS 등에 지원했다. 하지만 삼성을 제외하고는 서류 탈락이라는 쓰라린 경험을 했다. 확실히 3일 밤새서 쓴 자소서의 퀄리티는 무시하지 못하는 것일까.. 이 때 써놓은 문항을 재탕의 재탕의 재탕 중인데 이제는 새롭게 작성할 때가 다시 돌아온 것 같아서 벌써 머리가 아프다.
많은 기업에 지원하고 떨어지기를 반복하고, 주변에 취업한 지인들이 생겨 얘기를 들어보며 앞으로 취업 활동에 대해 깊은 고민을 했었다. 코딩 테스트가 가장 큰 걸림돌인 만큼 기출 유형이 정해져있어 가장 취업 가능성이 큰 삼성 준비에 올인을 할 지, 내가 가고 싶은 테크 기업 준비를 해야할 지 고민을 정말 많이 했다. 이 때 친한 선배한테 고민 상담을 했는데 상담 후 내가 원하는 일을 하기 위해 준비하기로 마음을 굳혔다.
난..아직 어리니까!! 해보는거야!!!!!!
SSAFY 8기 실습코치 지원
코딩 테스트에 발목을 제대로 잡혀 올해 목표였던 취업을 하지 못했다. 아쉬움을 뒤로하고 프로젝트를 진행하며 쌓은 나의 프로젝트 노하우와 기술적 지식을 8기와 공유하고자 8기 실습코치에 지원했다.
자기소개서가 부족했는지, 나의 이력이 실습코치를 할 만큼 만족스럽지 못 했는지 서류 탈락이라는 결과를 받았다. 사실 서류는 무조건 통과할 것 같다고 생각했던 터라 적잖이 충격을 받았었다.
다소 아쉬웠지만, 좋은 기회로 8기 싸피셜과 인터뷰를 하게 되어 인터뷰를 통해 간략하게나마 프로젝트에서 꼭 했으면 하는 것들을 전달할 수 있었다.
2023년에는..
나무가 아닌 숲을 보자
당장 취업에 급급하고 싶지 않다. 난 아직 어리니까(?) 내가 원하는 기업을 가기 위한 노력을 해보고 싶다.
그동안은 학교 때문에, SSAFY 때문에 온전히 내게 필요한 공부를 할 수 있는 시간은 없었다. 사람이 하고 싶은 것만 하고 살 수 는 없지만 완전 백수가 된 지금, 열심히 공부해서 하고 싶은 일 근처까지 가보고 싶다ㅎㅎ
(그렇다고 여유있게 공부하겠다는 말은 아니다!)
난 내가 할 수 있다고 믿는다.
서비스를 운영해보자
여러 프로젝트를 진행하며 배운 것들을 토대로 실제 서비스를 운영해보고 싶다.
올해는 마음 맞는 사람들을 모아 프로젝트를 위한 서비스 개발이 아닌 실제 사용자를 위한 서비스를 개발하고 운영해보자.
꾸준하자
'내가 가는 길에 맞는가'에 대한 의구심이 들 때도 있겠지만 나를 믿고 꾸준히 내 갈 길을 가자.
나는 뭐든 할 수 있는 사람이고, 그런 사람들과 함께니까!
'etc > Diary' 카테고리의 다른 글
SW 마에스트로(Software Maestro) 14기 합격 후기 (2) | 2023.04.03 |
---|---|
2021 회고록 (1) | 2022.01.14 |