Project/Monew

[Monew] 복기 - 일정·개발 측면 회고

승주우에요 2026. 5. 10. 14:48

뉴스 큐레이션 소셜 플랫폼 MoNew를 팀 프로젝트로 마무리했다. 댓글 도메인을 담당하면서 느꼈던 것들, 배운 것들, 그리고 다음엔 꼭 챙겨야겠다 싶은 것들을 정리해봤다.


1. 일정 측면

초반 세팅 — 컨벤션과 팀 규칙

프로젝트 초반에는 피그마 프로토타입과 기능 요구사항 정의서를 기반으로 방향을 잡고, 팀 컨벤션과 규칙을 수립하는 것부터 시작했다.

클래스명, 변수명 네이밍 컨벤션은 초반에 잘 정리됐다. 문제는 테스트 로그 컨벤션이었다. 이 부분을 정하지 못한 채로 넘어갔더니 중간에 팀원마다 다른 방식으로 로그를 남기게 됐고, 나중에 통일하는 게 생각보다 번거로웠다. 다음 프로젝트에서는 테스트 로그 형식도 초반 컨벤션에 반드시 포함시켜야겠다.

포맷팅 자동화(spotless, google-java-format 등)도 초반에 적용했어야 했다. 나중에 맞추려니 diff가 너무 많이 생겨서 PR 리뷰가 힘들어졌다.

CI 파이프라인에도 아쉬움이 남는다. 이번엔 테스트 커버리지 체크 수준에 그쳤는데, 앞으로는 아래 항목들을 CI에 통합하고 싶다.

  • CheckStyle — 코드 스타일 자동 검사
  • 정적 코드 분석 도구 — NullPointerException 등 위험 사전 탐지
  • ✅ JaCoCo — 테스트 커버리지 측정 및 리포트

이게 있었다면 팀원들이 각자 다른 방식으로 짜도 일정 수준 이상의 품질을 보장할 수 있었을 것 같다!


요구사항 정의서 — 러프하게 쓰면 나중에 반드시 수정된다

초반에 요구사항 정의서를 너무 러프하게 작성했다. 결국 중간에 몇 번 수정이 일어났고, 그때마다 이미 짜놓은 코드에 영향이 생겼다.

요구사항 정의서 수정은 어느 프로젝트든 일어난다고 생각한다. 그렇더라도 내가 담당하는 도메인만큼은 프로토타입을 충분히 보면서 기능을 완전히 파악한 다음, 나만의 텍스트로 재정의하는 게 맞는 것 같다. 다른 사람이 쓴 문서를 그대로 가져가는 게 아니라, 내가 구현할 관점에서 다시 써야 나중에 놓치는 게 없다.


도메인 의존 순서 — 댓글은 다른 도메인이 먼저 완성돼야 한다

내가 맡은 댓글 도메인은 User, NewsArticle 도메인에 의존이 있었다. 기능 개발 자체는 약 2일 만에 마쳤는데, 문제는 의존하는 도메인이 아직 완성이 안 된 상태라 테스트 코드 작성이 막혔다는 거다. 서비스 레이어 테스트를 짜려면 User, Article이 있어야 하는데 그게 없으니 Mocking을 과하게 해야 했고, 그게 또 까다로워서 결국 테스트 코드 작성이 늦어졌다.

다음에는 팀 전체 작업 순서를 조율할 때 의존 관계를 기준으로 개발 순서를 정하는 것이 필요하다. 먼저 완성돼야 하는 도메인이 뭔지 팀이 공유하고 있어야 한다.


전체 일정 플랜의 중요성

기능 개발이 빨리 끝난 건 좋았지만, 전체 기능 개발 일정, 리팩토링 일정, 테스트 코드 작성 등 구체적인 일자?를 정하지 않으니 다들 기간이 널널해 여유롭게 작업하느라 마지막에 급하게 문서화를 했다. 앞으로는 아래처럼 작업 단위로 플랜 일정을 구체적으로 세워야 겠다. 예를 들면, 기능 개발 (5/6 ~ 13), 테스트는 3일동안 등등 이런식으로 ......

기능 개발 → 리팩토링 → 테스트 코드 작성 → 문서화

문서화 시간이 항상 부족했고 정신없었다. 기능 개발하면서 머릿속에 있는 의사결정 맥락이 문서로 안 남으면, 나중에 팀원이 코드를 볼 때 이유를 알 수가 없다. 이번 프로젝트에서 다른 한 팀원 코드를 파악하려 했는데 주석이 없으니 너무 힘들었다. 다음 프로젝트에서는 상세 주석을 팀 컨벤션으로 강제화하고 싶다. (나는 아직 개발자가 아니기에....)


2. 개발 측면

ERD 설계 — 타입 선택이 DB 부하에 영향을 준다

ERD를 설계하면서 처음으로 컬럼 타입 하나하나가 DB 성능에 영향을 준다는 걸 체감했다. 지금까지는 그냥 생각 없이 VARCHAR, BIGINT 같은 걸 썼는데, 타입 선택이 인덱스 크기와 I/O 비용에 영향을 준다는 게 생각보다 흥미로웠다.

Long 타입을 ID에 쓰는 이유도 이번에 다시 짚게 됐다. PK 자동 증가 전략에서 BIGINT(Long)를 쓰는 건 범위 때문이다. INT(Integer)는 약 21억, BIGINT는 약 922경까지 커버하기 때문에 서비스 규모를 고려하면 처음부터 Long을 쓰는 게 맞다. 타입 별 DB 부하와 선택 기준은 따로 정리해봐야겠다. 

만약 INT 타입을 썻을 때 커버할 수 있는 범위를 넘어가게 되면 마이그레이션까지 생각을 해놔야하기에 애초에 넉넉하게 잡는 게 좋은 것 같다! 


글로벌 예외 처리 — 커스텀 예외·응답 설계

이번에 GlobalExceptionHandler, MonewException, ErrorCode를 직접 설계해봤다. 쓰면서는 돌아갔는데, 사실 왜 이렇게 설계하는지 원리가 아직 100% 잡히지 않은 느낌이다.

  • 커스텀 예외 클래스는 왜 RuntimeException을 상속하는가
  • @RestControllerAdvice가 동작하는 방식
  • 에러 응답 바디를 어떻게 일관되게 관리할 것인가

이 부분은 따로 글로 정리하면서 완전히 소화해야겠다.


서로 다른 DB 사용 시 정합성 문제 — Outbox 패턴

이번 프로젝트에서는 PostgreSQL과 MongoDB를 함께 사용했다. 그러다 보니 두 DB 간 정합성 문제가 현실적인 문제로 다가왔다. PostgreSQL에는 썼는데 MongoDB에는 못 쓴 경우, 혹은 반대 상황이 생기면 어떻게 처리할 것인가.

이 문제를 알아보다가 Outbox 패턴을 알게 됐다. 핵심은 이렇다.

1. 비즈니스 로직 + Outbox 이벤트를 동일 트랜잭션에서 DB에 저장
2. 별도 프로세스(혹은 배치)가 Outbox 테이블을 폴링해서 외부 시스템에 전달
3. 전달 완료 후 Outbox 레코드 삭제

트랜잭션 범위 안에서 이벤트를 함께 기록하기 때문에, 외부 시스템에 전달 실패해도 재시도가 가능하다. 정합성이 깨지는 상황을 원천적으로 줄여주는 방식이라 나중에 더 깊게 파봐야겠다.


스프링 배치 — 제대로 알고 쓰자

이번에 로그 S3 업로드, 기사 수집, 알림 정리 등 여러 배치를 작성했는데, 솔직히 Job / Step / Tasklet / Chunk 개념을 정확히 이해하고 쓴 게 아니었다. 동작은 했지만 왜 이렇게 쓰는지 근거가 약했다.

스프링 배치 테스트도 어려웠다. 메타 테이블이 H2에서 자동으로 생성되는 방식이나, 배치 컨텍스트를 어떻게 모킹할지 감이 안 잡혀서 결국 통합 테스트만 가볍게 작성했다. 다음 프로젝트 전에 배치를 처음부터 제대로 공부하고 들어가야겠다.


테스트 코드 — 의미를 알아야 제대로 쓴다

이번에 새로 알게 된 테스트 도구들이 있었다.

  • Test Faker / Instancio — 테스트용 더미 데이터 자동 생성
  • TestContainers — 실제 DB 컨테이너를 띄워서 하는 통합 테스트

Repository 테스트를 굉장히 빡빡하게 작성하는 걸 팀원 코드에서 봤는데, 솔직히 어려웠다. 하지만 쿼리문이 몇 번 flush 되는지, N+1은 안터지는지 까지 검사하는 걸 보고 많이 배웠다. 테스트 코드를 왜 작성할까라는 의문이 들었는데, 그 사람 껄 보면서 자신의 로직 검토, 코드의 품질 보장을 할 수 있다는 점을 배웠다.

그럼에도 아직 테스트 코드의 의미가 완전히 잡히지 않았다. "기능 개발 후 테스트"가 아니라 "테스트를 먼저 쓰고 기능 개발"하는 TDD 방식이 과연 좋은 방법일지도 잘 모르겠다. 시간이 매우 많이 걸릴 것 같다. 그렇다고 기능 개발만 먼저하고 테스트 코드를 작성하는 것도 뭔가 이질적으로 느껴진다. 그래서 TDD의 중간 단계에 있는 방법론은 없을 까 찾아봐야 겠다.


QueryDSL + 커서 페이지네이션

커서 페이지네이션을 QueryDSL로 직접 구현해봤다. 오프셋 방식 대비 왜 커서가 더 나은지는 이제 이해했다. 다음 페이지를 WHERE id < cursor 조건으로 가져오기 때문에 페이지가 깊어져도 성능이 일정하게 유지된다는 장점이 있다.

근데 QueryDSL 사용법 자체가 아직 익숙하지 않다. 특히 동적 조건 처리(BooleanBuilder, BooleanExpression)나 fetchJoin vs 별도 쿼리 선택 기준 같은 부분이 약하다. QueryDSL만 따로 시간 내서 정리해봐야겠다.


인덱스 — 도입 전후 수치 비교가 필수다

인덱스를 적용하면서 "이걸 달면 빨라지겠지" 하고 넣었는데, 사실 도입 전 수치와 도입 후 수치를 비교하지 않으면 의미가 없다는 걸 깨달았다.

수치 비교 방법으로는 아래를 공부해봐야겠다.

  • EXPLAIN ANALYZE — 실행 계획과 실제 수행 시간 확인
  • 부하 테스트 — nGrinder, Gatling, k6 등으로 TPS 측정
  • Grafana + Prometheus — 메트릭 시각화

특히 Grafana로 비즈니스 커스텀 메트릭을 직접 정의하고 시각화해보고 싶다. 예를 들어 "분당 댓글 등록 수", "기사 조회 실패율" 같은 도메인 레벨의 메트릭을 대시보드로 보는 것.


객체지향 설계 — 공통 로직을 언제 메서드로 분리할까

이번 프로젝트에서 가장 아쉬운 부분이 객체지향적 설계다. 특히 댓글과 좋아요를 하나의 Controller·Service에 묶어버린 것이 계속 마음에 걸렸다. (나중에 수정하긴 했지만..)

Comment와 CommentLike는 별개 도메인인데, 하나의 Service에 다 넣으니 클래스가 비대해졌고 책임이 불명확해졌다. 이걸 어떻게 분리하는 게 맞는지, 그 경계를 어떻게 잡는지는 DDD(Domain-Driven Design) 관점에서 더 공부해야 할 것 같다.

공통 로직을 언제 메서드로 분리해야 하는지도 아직 기준이 명확하지 않다. 단순히 중복이 보이면 빼는 게 아니라, 응집도와 결합도를 기준으로 판단하는 법을 익혀야겠다.


MongoDB — NoSQL을 쓰는 이유와 설정

이번에 UserActivityDocument를 MongoDB로 관리했다. 단순히 "MongoDB도 써보자" 수준으로 도입했는데, 사실 왜 이 데이터를 NoSQL에 저장하는 게 맞는지 근거가 약했다.

MongoDB를 선택하는 기준은 보통 이렇다.

  • 스키마가 자주 바뀌거나 유연해야 할 때
  • 중첩 구조(Nested Document)가 자연스러운 데이터일 때
  • 읽기 성능이 중요하고 조인이 거의 없는 경우

UserActivity는 사용자별로 다른 활동 내역을 유연하게 담아야 하고, 조인 없이 단건 조회가 대부분이라 MongoDB가 맞는 선택이었다. 하지만 이 이유를 처음부터 명확히 하고 들어갔어야 했다.

MongoDB도 커넥션 풀 설정이 필요하다. PostgreSQL이랑 완전히 별개로 설정해줘야 하고, Spring Data MongoDB 기준으로는 MongoClientSettings에서 ConnectionPoolSettings를 별도로 잡아줘야 한다. 이 부분도 따로 정리해봐야겠다.


동시성 문제 — 정확히 뭐고 어떻게 해결하나

like_count를 업데이트할 때 @Modifying + clearAutomatically = true로 bulk update를 썼는데, 이게 사실 동시성 문제와 연결된다는 걸 뒤늦게 깨달았다.

동시성 문제란 두 개 이상의 트랜잭션이 같은 데이터를 동시에 수정할 때 의도치 않은 결과가 나오는 것이다. 해결 방법에는 크게 세 가지가 있다.

  • 낙관적 락 (Optimistic Lock) — 충돌이 드물다고 가정, 버전 번호로 체크
  • 비관적 락 (Pessimistic Lock) — 충돌이 잦다고 가정, 먼저 락을 걸고 진행
  • Atomic 연산 — DB 레벨에서 UPDATE count = count + 1처럼 단일 연산으로 처리

쓰레드 풀, 커넥션 풀과도 연결되는 개념이라 같이 공부해야 전체 그림이 잡힐 것 같다.


마치며

프로젝트를 할 때 마다 설계 결정의 무게, 팀원과의 의존 관계, 문서화의 중요성을 느끼게 되는 것 같다. 이와 관련해서 항상 아쉬움이 느껴진다. 의견을 더 세게 말해야 했었을까 싶기도 하다. 

이번에는 개발적인 측면도 배웠지만, 특히 팀원들 간의 트러블이 일어났을 때 어떻게 해야했을까를 많이 돌아보는 계기가 되었다. 팀장으로서 어떤 식으로 했어야 했을 까 되짚어 보는 시간이 됐다! (내가 많이 희생을 한 것 같다....으)

배운 것도 많고, 부족한 것도 명확하게 보였다. 그게 이번 프로젝트의 가장 큰 수확인 것 같다.

다음 프로젝트 전에 챙겨야 할 것들:

  • [ ] 커스텀 예외·응답 설계 원리 정리
  • [ ] QueryDSL 동적 쿼리 + 커서 페이지네이션 정리
  • [ ] 스프링 배치 Job / Step / Chunk 개념 정리
  • [ ] TestContainers + Repository 통합 테스트
  • [ ] 동시성 문제 (낙관적 락 / 비관적 락 / Atomic 연산)
  • [ ] DB 타입 선택 기준 + 인덱스 도입 수치 비교 방법
  • [ ] MongoDB 커넥션 풀 설정
  • [ ] Outbox 패턴 깊게 파기
  • [ ] Grafana 커스텀 메트릭 시각화

'Project > Monew' 카테고리의 다른 글

[Monew] 개인 개발 리포트  (3) 2026.05.06
[Monew] 개인 회고록 - 기술보다 어려웠던 것들  (0) 2026.04.27