데브코스 2차 프로젝트가 다행스럽게도 잘 마무리되었다. 그리고 생각보다 내가 목표했던 부분에 대해서는 전부는 아니더라도 적당히 이뤄낼 부분은 이뤄내고, 아쉬웠던 부분은 다듬어서 다음 프로젝트에서, 그리고 안된다면 개인적인 공부로 이뤄내면 된다고 생각하며 이번 프로젝트 또한 좋은 결과를 얻어낸 것 같아서 기분이 좋았다.
그리고 팀내에서 좋은 평가를 받아서 기분이 좋았따! 내가 팀장은 아니였지만, 팀원들이 내 의견을 많이 한편으로는 부담이 되기도 했지만 한편으로는 굉장히 고마웠다.

피어리뷰로 받은 결과이다. 좋은 평가를 주셨으니 당당하게 올려도 되지않을까...?
그리고 항상 회고가 너무 길어져서 이번에는 목표에 대해서 한번 확인해보고 KPT 회고 방식으로 작성해보려고 한다. 내가 내 회고를 다시 읽으려고 해도 너무 길다.. 근데 아마도 쓰면 또 길어지지않을까 생각하면서 일단은 써보려고 한다.
나는 프로젝트를 진행하기 전에 세운 목표가 있었다. 이런저런 다양한걸 해보고 싶기도 했고, 목표없이 그냥 흘러가는대로 따라가다보면 챙길 수 있는게 많이 없었다. 내가 프로젝트 들어가기전에 세웠던 목표들과 함께 프로젝트하면서 겪었던 일들을 통해서 KPT를 통해서 프로젝트가 어땠는지 그리고 어떻게 나아갈 것인지 생각해 볼 수 있을 것 같다. KPT를 이용하여 회고를 작성하기 전 세웠던 목표에 대해 리마인드하면서 가려고 한다.

리뷰를 정말 열심히했다. 그리고 나도 팀원들로부터 리뷰를 되게 열심히 받았다. 애초에 브랜치 Rule을 걸어두었기 때문에 하기 싫어도 하게 만들었다. 이렇게 해야지 남의 코드를 보면서 공부하고, 내 코드에도 이런방식으로 써볼 순 없을까? 고민하는 과정도 생긴다고 생각했다.
그리고 모든 코드를 확인하다 보니, 각자의 코딩 스타일, 개성이 있기 때문에 아무리 팀 컨벤션이 정해져있다고 하더라도 모두 지켜나갈 순 없었을 것이다. 조금씩은 각자의 스타일로 가던 부분들에 대해서 컨벤션으로 지켜야 할 부분은 컨벤션으로 맞춰나가며 좀 더 협업하는 느낌이 들게 되었다.
그래서 점점 시간이 지날수록 컨벤션도 맞아가고 하다보니, 코드를 읽는 시간도 줄어들었다고 생각한다. 나중에 이슈가 생겨서 "저 여기 코드에서 이런게 안되는데, 어떻게 해야되나요?" 라는 질문이 나왔을 떄 코드리뷰를 하지 않았더라면 전체적인 맥락을 확인하기 위해서 처음부터 다 봐야 했을 수도 있다고 생각한다. 하지만 코드리뷰를 통해서 어느정도 인지하고, 코드스타일도 컨벤션에 맞추어가는 과정이었기 때문에 좀 더 고민해야 할 사안에 집중해 볼 수 있었다고 생각한다.
그리고 가장 중요한 것은 리뷰는 내가 생각하지 못한 부분에 대한 것을 생각하게 해준다. 그리고 나에게 리뷰가 달렸을 때도 다시한번 생각하는 계기가 된다. 정말이지 서비스를 급하게 내놔야 되는 상황이 아니라면 코드리뷰를 안할 이유가 있을까? 라는 생각이든다.
코드리뷰는 앞으로도 꾸준히 이어나가야 할 문화라고 생각한다. 그렇게 정착시키기 위해서는 내가 한발 앞장서서 코드리뷰를 달아줘야 하고, 의미있는 과정이라고 느낄 수 있도록 만들어줘야 한다고 생각한다. 앞으로 이런 문화를 꼭 지키며 나아가고싶다.
QueryDSL을 사용하려고 했던 이유를 생각해보면 그냥 간단하다. 직접 JPQL로 쿼리를 물론 짜도 되지만, 개발자의 실수를 방지해주는 QueryDSL을 사용하지 않을 이유가 있을까?
안타까운 소식이라면 querydsl이 업데이트가 없기 때문에.. 안쓸수도 있다는 사실..?
그래도 나는 개발자의 실수를 줄일 수 있는 querydsl을 쓰기로 했다.

이렇게 파일이 많아지는것을 통합 할 수 있는 방법이 있다고 하는데, 내가 제대로 못한 것인지 아직은 제대로 적용시키지 못했다. 하지만 그래도 최대한 QueryDSL을 쓰려고 노력했다.
"SELECT c.memberId FROM Choice c WHERE c.voteItemId = :voteItemId"
이렇게 쿼리를 쓰는게 물론 더 간단할 수는 있지만
public List<Long> findMemberIdsByVoteItemId(Long voteItemId) {
return queryFactory.select(choice.memberId)
.from(choice)
.where(choice.voteItemId.eq(voteItemId))
.fetch();
}
이렇게 작성하는게 크게 어려운일인가? 생각한다.
위의 JPQL은 그럴일은 거의 없겠지만 손이 미끌려서 오타를 냈다고 한다면 컴파일오류를 못찾는다. 하지만 QueryDSL은 어떤가, 바로 무섭게 빨간줄을 띄울 것이다.
아직 제대로 QueryDSL에 대해서 탐구해보지 못한 것은 아쉽지만, 가벼운 SQL을 작성하더라도 QueryDSL에 적응해나가는 노력을 하는게 맞다고 생각한다.
말은 거창하지만, 생각보다 지키지 못한 부분이 많다. 그래도 최대한 @Embeddable을 사용해보았고, 싱글테이블 전략이라는 것도 사용해보았다.
그리고 장점은 많았다. 그리고 @Embedabble에 대해서 조금 생각이 바뀌었다고 해야되려나 지금까지는 Position과 같이 위도 경도가 있는 경우를 묶어서 객체로 관리하기 위해서 사용하려고 했었다. 그런데 Email도 VO라고 생각하면 @Embeddable을 이용해서 검증의 책임을 넘겨서 사용해보는 것도 좋지 않을까? 라고 생각했다. 물론 요청이 들어올때 부터 검증이 되긴하겠지만..? 검증에 대해서는 항상 어렵고도 험난하기는 하다.
그리고 SingleTable 전략을 사용해보았는데 JPA의 고급매핑을 써볼 일도 없었고, 써볼 생각도 못해봤다. 그냥 단순히 예제들을 보고 지금 내가 하고있는 투표에도 한번 적용해보면 좋지 않을까? 이럴 때 사용하라고 있는 기술 아닐까? 라고 생각하면서 사용하게 되었다.
간단하게 사용하게 된 근거는 이렇다.
자세한 내용은 [데브코스] 2차 프로젝트 2주차 회고 에서 싱글 테이블 전략에 대해 작성한 것이 있으니 확인 할 수 있을 것이다.
그리고 나서 다른 팀원이 싱글테이블 전략을 또 사용하였다 !! 그리고 나서 코드를 곰곰히 살펴보면서 이런 리뷰를 남겼던 적이 있다.

내가 당시에 싱글테이블 전략을 사용하게 된 계기도 생각하면서 어떨 때 사용해야 할 지 다시한번 생각해보게 된 계기가 되었지 않나? 라고 생각한다.
모든 기술을 사용할 때는 Trade-Off가 존재한다. 항상 기술을 선택 할 때 잘 고려하고 선택해야된다고 다시금 느꼈다.
JPA를 사용하면서 단순히 '쿼리안짜도되니까 편하네~' 라는 마인드보다는 ORM이 데이터베이스와 코드의 간극을 메꾸고 객체중심으로 설계 할 수 있도록 만들어주었기 때문에 좀 더 객체지향스럽게, 더 JPA스럽게를 고민하며 개발해나갈 것이다.
사실 나는 처음 팀원들과 같은 팀이 되었을 때 조금은 걱정이 있었다. 이전 1차 프로젝트 때는 팀원들이 내가 만든 테스트코드에서 몇개 추가해주지 않았다. 그래서 처음에는 테스트코드를 안짜고도 충분히 다른 공부해야 할 일도 많고, 힘드니까 어쩔 수 없으면 혼자서 테스트를 짜고 말지라고 생각했다.
그런데 다행스럽게도 팀원들이 나랑 팀이 되어서 테스트를 공부해볼 수 있겠다며 좋아했다(?? 난 그렇게 느꼈다. 머쓱) 그런데 생각보다 테스트를 짜면서 진행하는 팀원들이 많지는 않았고, 테스트를 짜더라도 서투른 부분이 많이 보였다. 물론 나도 TDD를 한다거나, 모든 메서드에 대해서 완벽하게 커버하는 테스트를 짠다거나 하지는 못하지만, 초반에는 팀원들이 어려워하는 모습을 많이 보였다.
리뷰에도 열심히 테스트에 대해서 공유하고, 의견을 주고받았지만 아무리 생각해도 이런방식으로는 안되겠다고 생각했다.
그래서 그냥 아예 발표자료를 만들어서 발표를 해버렸다.

물론 발표 덕분일 수도 있고 아닐수도 있지만, 하여튼 결론은 팀원들이 나에게 많이 물어보기도 하며, 리뷰하는 과정을 통해서 결국 총 테스트코드 235개를 만들었고, 마냥 좋다고는 할 수 없지만 꽤나 높은 커버리지를 가졌다.

살짝의 피드백을 해보자면, 내가 담당했던 Vote는 동적 스케쥴링에 대해서 테스트하기가 조금 어려웠다.. 그래서 조금 아쉬운 커버리지를 가졌던 것 같다. 그리고 chat이나 member는 웹소켓이나, oauth때문에 테스트하기 어려운 부분이 있었을 것이라고 예상되고, reservation은 너무 급하게 만드느라... 그렇다.
그래도 테스트를 이렇게 많이 작성해본 프로젝트는 나한테는 처음이었다. 그리고 테스트를 만들었기 때문에 리팩토링도 무섭지 않게 진행 할 수 있었다.
그리고 결국에 팀원들이 만든 모든 API들을 REST Docs 문서화를 진행하였다 !
나름대로 팀원들도 테스트를 열심히 해주고, 열심히 따라와줘서 좋은 결과물을 만들어 낼 수 있었다.
테스트에 대한 마음가짐은 항상 작성하지만 항상 똑같다. 테스트는 해야한다. 이번에는 발표를 통해서 테스트에 좀 더 쉽게 다가가게 만들었고, 그에 따라 좋은 결과를 얻었다고 생각한다. 테스트는 앞으로의 프로젝트들에서도 꼭 해야한다. 남이 안하더라도 나는 꼭해야한다.
회고는 사실 지금도 쓰고있고 1주차, 2주차 모두했다. 3주간 진행됐기 때문에 최종회고로 3주차 회고를 퉁친다해야되나? 하기 때문에 앞선 회고들을 확인하려면 이곳에서 확인하면 된다.
[데브코스] 2차 프로젝트 1주차 회고
[데브코스] 2차 프로젝트 2주차 회고
그리고 생각보다? 하면서 글은 많이 못쓴거 같은데 그래도 회고 내에 모두 담겨져 있긴하다. 그리고 하면서 궁금해서 더 공부해봤던 내용도 블로그에 포스팅했었다.

노션에다가 생각 날 때마다 정리하는 부분도 있었고, 대부분은 회고에 정리가 되었다. 왜 다 회고에 때려박게 되었는지는 나도 모르겠지만..! 하여튼 정리도 열심히하면서 진행했다. 그리고 개발하면서 한 내용들을 다 잘라서 깃허브 레포지토리에 올려두었다.
이벤트 기반 동적 스케줄링
싱글테이블 전략 더 효과적으로 사용해보기
Soft Delete 사용해보기
동적 스케줄링 도전기
기록하고 회고는 해야한다. 내가 좋아하는 책인 함께 자라기에서 피드백과 회고의 힘에 대해서 이야기하고있다. 회고는 결국 나의 A작업인 개발을 더 잘하게 하기 위한 과정이기 때문에 앞으로도 꾸준히 할 예정이다.
Record는 Java14에서 프리뷰로 나오고 Java16에서 정식으로 출시된 기능이다. 많은 보일러플레이트 코드를 줄여주고, 불변객체처럼 사용 할 수 있기 때문에 DTO에서 많이들 사용한다. 그래서 나도 그렇게 좋으니까 DTO에서 써보자~ 라는 생각으로 시작했다. 그런데 그냥 맘에 안드는게 많았다 물론 실제로 DTO기 때문에 new를 할 일은 많이 없었지만, 결국 생성자를 호출해야했다.
우리가 생성자 대신 빌더 패턴을 사용하는 이유는 개발자의 실수를 줄이기 위함도 포함되어있다. 물론 요즘은 cmd+p를 하면 파라미터를 다 보여주긴 하지만, 난 빌더가 더 보기 좋다고 생각하고 실수를 안하는거 아닐까? 라고 생각한다.
물론 실제로 DTO에 new 해서 데이터를 넣을 일은 굉장히 드물지만... (대부분 정적팩토리 메서드를 통해서 넣었다.) 그래도 내 맘에는 안들었다!
사실 record에 대해서는 problem까지는 아니고 그냥 record써본 나의 소감? 정도라고 생각하면 될 것 같다.
이거는 코드를 짤 때도 그렇고, 작업을 할 때도 포함되는 것 같다. 나는 동적 스케줄링을 구현하기 위해서 되게 급하게(?)라고 해야될까? 다온것 같은데 다온것 같은데 하면서 문제를 제대로 파악하지 못하고 빨리 끝내려고만 했다.
이 때 문제는 Instant 타입에 대해서 내가 잘 몰랐고, 시간을 잘 못다뤘다. 그렇기 때문에 문제가 발생했다. 근데 나는 그냥 무작정 닥치는 대로 다 해봤다. 그래도 해결이 안됐다. 저녁을 먹고나서 다시 앉아서 차근차근 살펴보니 원하는 인자인 Instant 타입에 대해서 다시 공부해보게 되었다. 그리고 곧바로 해결했다.
가끔은 잘 안풀리는 문제가 있을 때는 잠시 쉬었다가 다시 생각해보는 시간도 필요해보인다.
그리고 전체적으로 프로젝트하면서 마음이 조금 급했다. 팀원들을 닥달한 부분도 없지않아 존재했을 것이라고 생각한다. 내가 팀장도 아니였고, 우리팀이 그렇게 느리지도 않았다. 1주차 회고때도 적었던 부분이지만, 내가 고쳐나가야할 문제라고 생각한다.
2차 프로젝트 발표기간이 코앞으로 다가왔는데 예약관련해서 기능이 만들어지지 않았다. 나는 클린코드를 지키려고 노력하고, 팀원들에게도 클린코드에 대해서 알리고, 리뷰를 많이했다. 하지만 내가 시간이 없자 만들어낸 코드는 끔직하리만큼 안좋은 코드가나왔다.
@Transactional
@Override
public void createReservation(Long memberId, Long courtId, Long teamId, LocalDateTime matchDate, List<Long> memberIds) {
Court court = courtRepository.findActiveById(courtId).orElseThrow(
() -> new IllegalArgumentException("해당하는 구장이 없습니다.")
);
Member member = memberRepository.findActiveById(memberId).orElseThrow(
() -> new IllegalArgumentException("해당하는 회원이 없습니다.")
);
Team team = teamRepository.findById(teamId).orElseThrow(
() -> new IllegalArgumentException("해당하는 팀이 없습니다.")
);
List<Member> participantMembers = memberRepository.findAllById(memberIds);
boolean allMale = participantMembers.stream()
.map(Member::getGender)
.allMatch(Gender.MALE::equals);
boolean allFemale = participantMembers.stream()
.map(Member::getGender)
.allMatch(Gender.FEMALE::equals);
Reservation reservation;
if (memberIds.size() >= 6) {
// 예약 만들기
if (allMale) {
reservation = Reservation.createMaleReadyReservation(court, member, team, matchDate);
} else if (allFemale) {
reservation = Reservation.createFemaleReadyReservation(court, member, team, matchDate);
} else {
reservation = Reservation.createMixedReadyReservation(court, member, team, matchDate);
}
Reservation savedReservation = reservationRepository.save(reservation);
List<Participant> participants = participantMembers.stream()
.map(participantMember -> Participant.create(reservation, participantMember, ParticipantRole.MEMBER))
.toList();
participantRepository.saveAll(participants);
eventPublisher.publishEvent(new ReservationPublishedEvent("예약 채팅방", savedReservation.getReservationId()));
eventPublisher.publishEvent(new ReservationMembersJoinEvent(participants, savedReservation.getReservationId()));
} else {
// 용병 만들기
if (allMale) {
reservation = Reservation.createMaleRecruitReservation(court, member, team, matchDate);
} else if (allFemale) {
reservation = Reservation.createFemaleRecruitReservation(court, member, team, matchDate);
} else {
reservation = Reservation.createMixedRecruitReservation(court, member, team, matchDate);
}
Reservation savedReservation = reservationRepository.save(reservation);
List<Participant> participants = participantMembers.stream()
.map(participantMember -> Participant.create(reservation, participantMember, ParticipantRole.MEMBER))
.toList();
participantRepository.saveAll(participants);
Mercenary mercenary = Mercenary.createDefault(reservation);
mercenaryRepository.save(mercenary);
eventPublisher.publishEvent(new ReservationPublishedEvent("예약 채팅방", savedReservation.getReservationId()));
eventPublisher.publishEvent(new ReservationMembersJoinEvent(participants, savedReservation.getReservationId()));
}
}
얼리리턴? 그런거 없다. 반복적으로 나오는코드 개선? 그런 것도 없다. 그냥 에라 모르겠다 정적팩토리 메서드만들어서 일단 만들어 이렇게 갔다.
일정을 맞추기 위해서 기능을 개발하는 것도 좋지만, 조금만 신경썼더라면 하는 생각이든다.
우리팀은 Github Project와 Issue, PR 통해서 일정관리를 통틀어서 했다. 그런데 Github Project는 너무 설정이 막혀있었고, 할 수 있는 부분이 많이 없었다. 그래서 사실상 그냥 보여주기용 일정관리, 로드맵이 되어버렸다고 생각이 든다.
나름 친해졌다고는 생각하지만, 아직도 나는 생성자 통해서 하는게 빌더보다 안좋은거아니야 ? 라는 생각을 가지고있긴 하다. 좀 더 친해져보자! 그리고 결국 코틀린에가면 Record와 비슷하게 사용하게된다고 하니 한번 노력해보자
일정을 맞추는 것도 중요하고, 개발하면서 몰입해서 빠져들어서 하는 것도 좋다고 생각한다. 하지만 가끔은 때론 잠시 여유를 가지고 돌아와서 작업하는게 더 좋을 때도 있다는 것을 경험했으니, 잘 안되거나, 힘들 때는 여유를 가져보려고 한다.
정말이지 내가 마지막에 짰던 코드는 최악이었다. 타임어택하듯이 만들어낸 코드라고 해야될까, 그리고 내가 만드는 코드는 대부분 리팩토링 할 시간이 주어진다. 물론 누구도 관심을 안가지더라도 할 수 있다. 그리고 웹이기 때문에 어디 한번 배포되면 수정할 수 없는게 아니다.
주어진 시간내에서 최선을 다해서 클린코드, 컨벤션, 성능문제 등을 생각하며 기능을 구현 하는게 1번이다.
그리고 만약 내가 정말 시간이 부족했다면, 다시 되돌아가서 리팩토링을 하면 된다.
그래서 지금도 하고있다...
마음의 짐처럼 남아있다면 리팩토링 꼭하자
지라를 써보면 가장 좋지 않을까 생각하면서도 사실은 어디 회사에서 프로젝트를 진행하는 것도 아니고, 일단 가장 중요한 것은 PM 또는 팀장이 업무를 배분해주거나 할 때 지라, 깃허브 프로젝트 등이 가장 큰 효과를 가지지않을까 라고 생각하긴 하지만, 그래도 지금 사용하던 툴은 너무 별로였다.
그리고 실제로 회사들에서는 지라를 많이 사용하기도 하니까, 나중에 취업을 하게 된다면 적응하기 쉬우려면 한번쯤은 써보면 좋을 것 같다고 생각한다.
다음 프로젝트를 진행하게 되면 지라를 한번 써보자고 제안해봐야겠다 !
생각보다 목표한 바에서 못이룬 부분이 몇개 있었다. 특히 DB쪽은 많이 못해봤다. 백엔드 개발자라면 데이터베이스를 더 잘 다루어야 된다고 생각하는데, 이러한 부분은 아쉽다.
그리고 이번 프로젝트를 통해서 또 한번 성장할 수 있었다고 생각한다. 언제나 프로젝트를 하면서 얻어 갈 수 있는 부분이 있다는게 기쁘다. 내가 목표한 것을 이룬것도 기쁘고 팀원들이 테스트를 짜준 것도 너무 고마웠고, 의견 교류했던 시간들도 모두 소중하게 느껴졌다.
스스로에게 부족하다고 생각했던 부분은 개인 공부, 그리고 앞으로의 프로젝트 등에서 개인적인 목표로 달성해나가면 된다고 생각한다.
이번 2차 프로젝트 정말 최선을 다해서 했다. 그리고 정말 즐거웠다. 앞으로도 행복한 프로젝트를 진행 할 수 있으면 좋겠다 ㅎㅎ
요즘 이래저래 관심이 생기는게 많다. 아키텍처나, DB도 관심이 많이 생기다보니 유튜브에서 컨퍼런스도 많이보고, 많이 기록해두고 언젠가 해봐야지! 라며 마음을 다지고 있다.
가장 눈앞에 보이는 목표로는 데이터베이스 공부해보기이다. 인덱스에 대해서 공부 제대로 못하고 적용 못시켜본게 너무 아쉬웠다. 그래서 앞으로의 목표는 인덱스 공부해보고 적용해보려고 한다.
멀리 내다본다면 일반적인 아키텍처를 가져가는게 아닌 CQRS도 적용해보고, 평범한 구조가 아닌 NoSQL과 RDB를 섞어서 사용하는 구조도 언젠가 해보고싶다. 당장에는 부족할지라도 언젠가 꼭 해볼 것이다.
그리고 항상 되새기는 테스트 열심히하기, 관성적으로 사용하지말고 생각하기
2차 프로젝트를 통해서 공부해본 것도 많고, 아쉬웠던 내용은 많았지만, 앞으로 차근차근 보완해나가면 된다.
다음 회고에는 인덱스를 공부한 내용을 쓸 수 있도록, 그리고 테스트에 대한 내용도 다시한번 쓸 수 있도록, 그리고 '왜?' 라는 키워드로 공부한 포스팅을 링크걸 수 있도록 노력해야겠다.