[ECC 2026 Summer Project] SplitOpt - 프로젝트 회고록
기간: 2026. 07. 13 – 08. 28 (7주)
파트: 백엔드 — 정산 최적화·상태 관리(23~29), 예산(38~40)
프로젝트 개요
SplitOpt는 모임 정산을 최적화하는 더치페이 서비스다. 이름은 Split과 Optimization을 합친 것이다.
여행이나 회식에서는 한 사람이 전부 결제하지 않고 여러 명이 번갈아 결제한다. 그러면 결제자와 부담자가 어긋나고, 인원이 N명일 때 주고받을 수 있는 관계는 최대 N(N−1)/2건까지 늘어난다. 6명이면 15건이다. 여기에 누가 아직 송금하지 않았는지 확인할 공용 기록이 없어 확인과 독촉이 반복된다.
SplitOpt는 사용자가 지출만 기록하면 나머지를 서버가 처리한다.
- 지출 기록 — 누가 결제했고 누가 부담하는지 입력
- 잔액 계산 — 참여자별로 (낸 돈 − 부담할 몫). 양수면 받을 사람, 음수면 줄 사람
- 정산 최적화 — 송금 관계를 그리디 네팅으로 압축
- 송금·확인 — 보냄 → 받음 확인까지 상태로 추적
4인 모임 기준 최대 6건이던 송금이 3건으로 줄어든다.
팀은 프론트엔드 1명, 백엔드 3명이다. 백엔드는 도메인별로 나눠 병렬 개발했고, 나는 정산 최적화·상태 관리와 예산을 맡았다. 최종적으로 명세의 API 40개를 전부 구현했고 자동화 테스트는 287개다.
1. 개인 관련
개인 역량 성장에 어떤 도움이 되었는지
동작하는 코드와 틀리지 않는 코드를 구분하게 됐다.
학교 과제에서는 결과가 나오면 끝이었다. 이번 프로젝트에서 고친 문제는 대부분 결과가 나오는 상태에서 값만 틀린 종류였다.
| 증상 | 원인 |
|---|---|
| 같은 청구가 두 벌 저장 | 정산 재실행이 조회–삭제–저장이라 동시 요청이 서로의 미커밋 결과를 보지 못함 |
| 정산에 구멍이 생김 | 송금 표시만 하고 상대 확인 전에 모임을 나갈 수 있었음 |
| 예산 초과 경고가 과함 | 여행 전에 결제한 항공권이 하루 평균에 섞여 남은 기간 예상액을 부풀림 |
| 날짜 집계가 하루 밀림 | 배포 서버 기준 시각이 UTC |
| 운영에서만 500 | 엔티티는 고쳤는데 마이그레이션 추가를 빠뜨림. 로컬 H2에서는 드러나지 않음 |
다섯 건 모두 예외가 발생하지 않는다. 요청은 200으로 처리되고 화면에도 값이 표시된다. 사용자가 신고할 근거도 없고, 개발자도 다른 경로로 확인하기 전까지는 알 수 없다.
이런 문제를 겪으면서 코드를 확인하는 기준이 바뀌었다. 이전에는 정상 경로가 동작하는지를 봤고, 이후에는 동시에 두 번 호출될 때, 값이 비어 있을 때, 서버 시각이 다를 때 어떻게 되는지를 함께 본다.
테스트를 쓰는 방식도 바뀌었다.
중반에 기존 테스트를 확인하다가, “동액일 때 id 순서로 정렬한다”를 검증한다고 작성해둔 테스트가 실제로는 그 규칙을 확인하지 않는 것을 발견했다. 테스트 데이터의 잔액이 30000 / 20000 / 10000이라 동액이 발생하지 않아, 정렬 규칙을 제거해도 통과하는 상태였다.
이후로는 버그를 고치면 고치기 전 구현으로 되돌려 그 테스트가 실제로 실패하는지 확인하고 넘어갔다. 예산 동시 설정을 고쳤을 때 옛 구현으로 되돌려 실행한 결과, 새로 작성한 테스트만 예상한 원인으로 실패했다.
1
Unique index or primary key violation: PUBLIC.BUDGETS(GROUP_ID) VALUES (3)
이 확인을 거친 뒤에야 그 테스트를 근거로 쓸 수 있었다. 테스트 개수보다 그 테스트가 무엇을 못 잡는지가 판단 기준이 된다.
서버 단독 개발과 실제 연동의 차이를 확인했다.
5주차에 프론트엔드 연동이 시작됐다. 첫날 브라우저에서 오는 요청이 전부 차단됐는데, CORS 설정이 없었기 때문이다. curl과 Postman은 CORS를 보지 않으므로 그때까지는 드러나지 않았다.
지출 등록도 500으로 실패했다. 결제자를 쿼리 파라미터로 요구했지만 프론트는 보내지 않았고, 날짜를 시각까지 요구했지만 화면 입력은 날짜뿐이었다. 두 경우 모두 요청 본문 검증 전에 실패해 프론트에는 “서버 오류가 발생했습니다”만 전달됐다. 요청이 잘못된 것인지 서버가 잘못된 것인지 구분할 수 없는 응답이다.
이후 새 엔드포인트를 만들 때는 화면에서 어떤 값을 가지고 호출하는지를 먼저 확인하고, 에러 응답도 계약의 일부로 보고 설계했다.
기술적으로 새로 익힌 것은 다음과 같다. Spring Data JPA의 비관적 락과 트랜잭션 경계, ON DUPLICATE KEY UPDATE를 이용한 원자적 upsert, N+1과 fetch join·EntityGraph, Flyway 마이그레이션, Testcontainers를 이용한 실제 MySQL 검증, GitHub Actions CI. 대부분 수업에서 이름만 접한 것들이었다.
스스로가 잘했다고 생각하는 점
커밋 메시지와 PR 본문에 판단 근거를 남겼다.
팀 컨벤션은 [Type(scope)] 설명 형식이었고, 형식을 맞추는 데서 그치지 않고 본문에 무엇을 왜 고쳤는지와 검토한 대안을 적었다.
예를 들어 지출 수정 시 부담 내역이 UNIQUE 제약에 걸리는 버그를 고칠 때, 벌크 삭제(JPQL delete)로 바꾸는 방법도 있었다. 그 방법은 영속성 컨텍스트를 우회해 같은 트랜잭션에 남은 객체가 이미 삭제된 행을 가리키게 만들고, 모임 삭제 경로를 깨뜨린다. 이 판단을 커밋 본문에 남겼다.
이렇게 적어두면 코드만으로는 알 수 없는 판단 근거가 저장소에 남는다. 같은 자리를 다시 고칠 때 당시 판단과 검토한 대안을 git log에서 확인할 수 있고, 리뷰하는 사람도 PR 본문만으로 의도를 파악할 수 있다.
초반 커밋과 후반 커밋을 비교하면 형태가 다르다. 처음에는 무엇을 만들었는지만 적혀 있고, 후반에는 왜 그렇게 했고 대안은 왜 아닌지가 함께 적혀 있다.
사람이 기억해야 하는 절차를 검증 장치로 옮겼다.
엔티티에 제약을 추가하면서 Flyway 마이그레이션을 빠뜨려 운영에서만 500이 나는 일이 두 번 있었다. 테스트와 로컬은 엔티티로 스키마를 만들고 운영만 마이그레이션을 쓰므로, 어긋나도 테스트에 걸리지 않는다.
주의해서 처리하는 대신, 실제 MySQL에 마이그레이션을 적용한 뒤 엔티티와 대조하는 테스트를 만들었다. 어긋나면 스프링 컨텍스트가 뜨지 않아 CI에서 잡힌다.
팀 결정을 적용할 때 그 근거를 확인했다.
회의에서 지출 수정·삭제를 결제자 본인으로 제한하기로 했는데, 일정 화면에서 지출을 연결하려면 다른 사람이 등록한 지출도 붙일 수 있어야 했다.
해당 규칙의 근거는 다른 사람이 지출을 고쳐 정산이 틀어지는 것을 막는 데 있었다. 일정 연결은 여기 해당하지 않는다. 정산과 통계는 일정을 참조하지 않고, 잘못 연결해도 참여자 누구나 다시 옮길 수 있다.
연결 변경만 별도 API로 분리해 그 부분만 권한을 넓히고, 제목·금액 수정과 삭제는 그대로 두었다. 혼자 판단할 종류가 아니라 근거를 커밋과 PR에 적어 팀 확인을 받았다.
담당 범위 밖의 문제도 함께 처리했다.
인가 가드를 붙이는 과정에서 통계와 일정 API가 참여 여부를 검증하지 않는 것을 확인했다. 로그인만 하면 groupId를 아는 누구나 다른 모임의 데이터를 조회할 수 있는 상태였다. 담당자에게 공유하고 같은 가드를 적용했다.
지출 수정 500 버그, 지출 응답의 일정 정보 누락, 일정 종료 시각 검증도 내 담당은 아니었지만 예산 예측이 그 값을 사용하고 있어 함께 고쳤다.
스스로에게 아쉬웠던 점
성능을 측정하지 않았다.
N+1은 몇 군데 제거했다. 정산 목록에서 참여자 이름을 만들 때마다 추가 쿼리가 나가던 것을 fetch join으로 고쳤고, 지출 목록에는 EntityGraph를 걸었다.
그러나 전부 느릴 것으로 추정해서 고친 것이다. 응답 시간을 재본 적이 없고, 데이터를 충분히 넣고 부하를 준 적도 없다. 쿼리 개수가 줄어든 것은 확인했지만 그것이 실제 성능 개선인지는 확인하지 않았다. 측정 없이 최적화한 셈이라 그 수정들이 의미가 있었는지도 정확히는 알 수 없다.
초반 구현을 급하게 처리했다.
3주차에 정산 API를 만들 때 요청자를 X-User-Id 헤더로 받았다. 인증이 붙기 전이라 임시로 둔 것인데, 헤더는 조작할 수 있어 다른 참여자로 위장해 송금 확인을 누를 수 있다. 코드 리뷰에서 IDOR로 지적도 받았다.
주석과 이슈에 임시 처리임을 남기고 5주차에 걷어냈지만, 그 사이 2주 동안 그 코드 위에 다른 코드가 쌓여 정리할 지점이 처음보다 늘어났다.
응답 필드명도 같은 경우다. 명세를 확인하지 않고 정하다가 연동 시점에 프론트에서 값이 undefined로 나오는 것을 보고 한꺼번에 고쳤다. 처음에 명세를 확인했으면 발생하지 않았을 작업이다.
막힌 상황을 공유하지 않았다.
prod 부팅 실패를 진단할 때 남은 로그가 순환 의존 한 줄뿐이라, jar를 직접 실행하며 설정을 하나씩 바꿔 원인을 찾았다. 원인 자체는 정확히 짚었다. 팀에서는 Spring Boot 버전 문제로 보고 다운그레이드가 거론됐지만, 실제로는 로컬 편의 설정 한 줄이 prod로 상속된 것이었다. 버전 문제로 결론 냈다면 원인은 그대로 남고 지원이 끝난 버전을 쓰게 됐을 것이다.
다만 진단 과정을 혼자 오래 끌었다. 중간에 증상을 공유했으면 더 빨리 좁혀졌을 가능성이 있고, 팀 프로젝트에서 좋은 방식은 아니었다.
프론트엔드 화면 구조를 늦게 파악했다.
연동 과정에서 필드명과 요청 형식을 여러 번 고쳤는데, 대부분 화면 구조를 몰라서 생긴 작업이었다. 지출 수정 화면이 제목·금액·부담자를 한 폼에서 다룬다는 것을 알았으면 API를 처음부터 그 형태로 설계했을 것이다.
2. 프로젝트를 하며 아쉬웠거나 보완됐으면 하는 지점
연동을 더 일찍 시작했어야 했다.
프론트–백엔드 연동이 5주차에 시작되면서 CORS 미설정, 필드명 불일치, 요청 형식 불일치, 500 에러가 한 주에 몰려 나왔다. 모두 서버 단독 호출로는 드러나지 않는 문제다.
기능을 전부 만든 뒤에 붙이는 대신, 3주차쯤 간단한 API 하나를 화면에서 실제로 호출해봤다면 같은 문제를 훨씬 이른 시점에 확인할 수 있었다. 가장 단순한 경로 하나를 끝에서 끝까지 먼저 연결하는 것을 초반 목표로 두는 편이 낫다.
명세 문서가 여러 세대로 갈라졌다.
노션의 API 명세서에서 회의 결정이 바뀔 때마다 일부 페이지만 갱신됐다. 정산의 from/to를 두고 세부 페이지는 fromUserId, 회의록 스키마와 모임 상세 각주는 participantId로 서로 다르게 적혀 있었다.
구현 전에 어느 문서가 최신인지부터 판단해야 했다. 문서에 마지막 수정일과 결정 근거를 함께 남기는 규칙이 있었으면 줄일 수 있는 작업이다.
파트 분담에서 공통 규칙에 공백이 생겼다.
도메인별로 나눠 병렬 개발한 방식 자체는 효과가 있었다. 서로 충돌하지 않고 각자 속도로 진행할 수 있었다.
다만 모든 파트에 공통으로 적용돼야 하는 것들, 즉 인가 규칙과 에러 응답 형식, 필드 이름 규칙이 파트마다 달랐다. 정산에 참여 검증 가드를 붙였는데 통계·일정에는 없다는 것을 6주차에 확인한 것이 그 예다.
공통 규칙은 파트를 나누기 전에 한 명이 뼈대를 잡고 시작했어야 했다. 나중에 붙이면 이미 각자 다른 방식으로 구현한 뒤라 맞추는 비용이 든다.
배포를 늦게 시작했다.
배포에서 만난 문제는 대부분 조용히 실패하는 종류였다. jar 경로가 틀리면 부팅 전에 종료돼 로그가 거의 남지 않고, prod 환경변수가 비면 예외 없이 H2로 떠서 재배포마다 데이터가 사라진다. 실패해야 할 상황이 정상처럼 보인다.
코드가 완성된 뒤에 이런 문제를 만나면 원인 후보가 많아진다. 프로젝트 초반에 최소 구성으로 한 번 배포해두고 시작했으면 진단이 쉬웠을 것이다.
3. 기타
시작 시점의 경험은 Spring Boot로 CRUD를 작성해본 정도였다. 7주 동안 API 40개를 구현하고 배포까지 마쳤다.
가장 기억에 남는 작업은 예산 초과 예측 수정이다. 항공권 100만원이 하루 평균에 섞여 예상 총액이 275만원으로 나오던 것이, 확정 지출과 기간 중 지출을 분리하자 125만원이 됐다. 화면이 달라지지 않고 사용자가 알아채기도 어려운 수정이지만, 이런 계산 하나가 서비스의 신뢰도를 결정한다.
팀 운영 방식도 기록해둔다. 도메인을 나눠 각자 진행하면서도 매주 토요일에 모여 결정할 것은 함께 결정했다. 필드명이 어긋났을 때는 문서를 먼저 고치고 양쪽 코드를 함께 맞췄고, 다른 파트에서 발견한 문제를 공유했을 때도 바로 반영됐다. 파트 간 충돌 없이 40개 API를 끝낼 수 있었던 이유다.
다음 프로젝트에서 지킬 것은 두 가지다. 추측이 아니라 측정하고 고치기, 그리고 막히면 일찍 공유하기.