서론
매칭 알고리즘 이후 나는 수동매칭이라는 새로운 API를 맡아서 작업을 시작하게 되었는데,
원래 초기 업무분담에서 내가 맡은 부분은 아니였지만, 프로젝트 진행중 변경사항으로 인해 내가 해당 작업을 맡게됐다
사실 프로젝트 최종발표가 2월 6일이였기에 일주일 ?? 정도의 매우 촉박한 기간에 작업을 맡게되어서 걱정이 많았지만, 프로젝트를 진행하면서 충분히 있을 수도 있는 일이라고 생각하기도 했었고, 이전 매칭 알고리즘 회고록에서도 작성했었듯이 난 내가 맡은 파트가 너무 적어져서 팀원들에게 미안한 마음이 있었어서, 해당 파트를 맡아달라는 주영이의 요청에 당연하게도 수락하게 되었다 !(+ 작업 볼륨은 중요한게 아니라고 걱정하지말라고 얘기해준 팀원들에게 너무 고마웠다🙏)
게다가, 프론트 분들의 작업도 생각을 해야해서 구현할 수 있는 기간이 길지 않은 만큼, 수동매칭 기능에 대한 기능 설계 에 대한 부분을 최대한 세세하게 설계해야되겠다 라는 생각을 하게 되었었고, 동시에 최대한 요구사항을 제대로 파악해서 정확하게 기능을 구현하는 것이 중요하겠다라는 생각 또한 했다
본론
구현 전 기능 설계
기존 자동매칭 플로우와 분리
먼저 기존에 존재하던 자동매칭 플로우와 분리해서 구현을 해야겠다고 생각했던 이유는 아래와 같았다.
먼저 첫번째로, 내가 구현했던 알고리즘 서비스는 자동매칭에서 사용될 알고리즘 서비스였기때문에
아래 내용과 같이 empty를 반환을 해줘서 Kafka 이벤트 발행을 통해 매칭 프로세스를 비동기적으로 처리하는 방식으로 구현이 되었었다
public interface MatchingAlgorithmService {
/**
* 방을 찾는 메서드
* 이미 방에 들어가있는 멤버가 다시 요청했을 때 Optional.empty()를 반환하도록 로직을 구성해야함
* @param userId 방에 들어가려는 사용자 ID
* @param startLongitude 시작 지점 경도
* @param startLatitude 시작 지점 위도
* @param destinationLongitude 도착 지점 경도
* @param destinationLatitude 도착 지점 위도
* @param criteria 방 검색에 필요한 기타 조건 (태그 등)
* @return Optional<FindRoomResult> - 매칭 가능한 방 정보가 있으면 값이 있고, 없으면 empty
*/
Optional<FindRoomResult> findRoom(Long userId, double startLongitude, double startLatitude, double destinationLongitude, double destinationLatitude, List<Tags> criteria);
}
따라서 자동 매칭 Kafka 이벤트 발행시 필요한 위도(latitude) 및 경도(longitude) 기반의 필터링 로직까지 포함하고 있었지만,
자동 매칭과 달리 수동 매칭은 위에 나와있는 조건들처럼 위도 경도를 받아 필터링을 해줄 필요가 없었기때문에, 기존에 로직을 재활용 하는것에 대한 필요성을 느끼지 못하였다
또한 두번째로,아래의 뷰를 보면 알 수 있듯이 수동매칭은 이를 통해 사용자가 특정 조건에 맞는 매칭 상대를 찾을 때 비동기적으로 매칭을 수행하는 구조를 가지고 있다.
따라서 수동 매칭의 경우 사용자가 직접 매칭 방을 선택하고 참여하는 방식이므로 별도의 이벤트(Kafka)를 발행해줄 필요성도 없었다.
요구사항 자체가 수동매칭 리스트를 조회해오고, 유저가 수동매칭 리스트를 확인하고, 원하는 매칭방에 참여 요청을 보낸뒤, 매칭방에 참여되는 로직이였기 때문에 Kafka로 이벤트를 발행해주는건 over engineering 이라고 판단했기 때문이다
마지막으로 Kafka 의존도를 줄여 서비스 안정성을 높일 수 있는 설계를 적용하였다
자동 매칭과 수동 매칭의 차이점은 방 생성 방식이고, 사용자가 직접 매칭방에 참여하는 형태지만 수동 매칭에서도 Kafka를 사용하게 되면 DB ~ Kafka 간의 불필요한 데이터 중복 및 일관성 문제가 발생할 수 있다고 생각했고,
수동 매칭에서도 Kafka를 사용하게 되면 DB의 동기적 데이터 처리 흐름 ~ Kafka의 비동기 이벤트 처리 간의 불필요한 충돌이 발생할 가능성이 있다고도 생각이 들었다
매칭 취소 및 방 삭제 플로우
추가로 고민했던 내용은 매칭 취소 및 방 삭제 로직이다.
수동 매칭 서비스에서 방이 언제, 어떻게 삭제되는지가 사용자 경험 및 데이터 관리에 중요한 요소이므로,
아래와 같은 방 삭제 플로우를 설계하여 적용하였다.
1. 방장이 나갈시
랜덤으로 남아있는 참여자 중 한 명을 새로운 방장으로 지정하여 매칭을 지속할 수 있도록 함
2. 방장이 아닌 참가자가 나갈시
그대로 방장과 방이 유지되며, 나간 참가자만 제외됨
3. 방에 남은 모든 참가자가 나갈시 (방에 다른 참여자가 남아있지않은 상황에서, 방에 한명 남은 참여자까지 퇴장하게 될시)
방의 상태를 CANCELLED 로 변경하여 Soft Delete가 된다
방장의 필요성
위 플로우로 구현한 이유는 먼저 방에 방장이 필요한 이유를 먼저 설명을 해야할거같다
기존에는 결제 시스템을 도입하여 정산을 자동화하려 했으나, 이슈로 인해 직접적인 결제 시스템 도입이 어려운 상황이되었다
이에 대한 대안으로 매칭방의 방장이 택시 API를 활용한 정산을 담당하는 구조로 변경되었기 때문에,
방장은 정산을 책임지는 역할을 하므로, 방을 유지하기 위해 꼭 필요했었다.....
Hard Delete 대신 Soft Delete 방식을 선택
하드 딜리트를 사용하면 삭제된 방 데이터가 완전히 사라지므로, 추후 문제가 발생했을 때 추적이 어려워진다.
하지만 우리 서비스는 금전적인 내용이 직접적으로 연결되는 서비스인데 매칭방에서 정산이 이루어질때,
만약 사용자 간의 분쟁이 발생했을때에 하드 딜리트를 적용하면 삭제된 데이터 복구가 불가능하여 증빙 자료를 제공하기 어렵다는 단점이 있어, Soft Delete 방식으로 구현하게 되었다
방장이 변경되는 방식으로 구현
방장이 나갈 때마다 방을 삭제하는 방식은 사용자 경험(UX) 측면에서도 불편하다고 생각했고, 유저 입장에서 방이 깨질때마다 다시 매칭을 시작해야했기에 매칭의 연속성을 깨뜨려 이용성 측면에서 떨어진다고 판단했었다. 그래서 참여자들이 계속 매칭을 이어갈 수 있도록 방장이 변경되는 방식이 필요했기에, 방장이 나가더라도 랜덤으로 새로운 방장을 지정하여 매칭이 지속될 수 있도록 설계하였다.
또한 위에서 얘기했듯이 매칭방이 삭제가 되는 방식이 하드 딜리트가 아닌 소프트 딜리트여서, 방장이 나갈때마다 방이 삭제가 되게되면 DB에 삭제된 방에 대한 데이터가 너무 많아지게 되어, 리소스 측면에서도 불필요한 데이터가 너무 많아진다고도 생각하여 방장이 나갈 때마다 방을 삭제하는 방식은 사용하지 않았다.
사업자 이슈 관련 내용은 아래의 게시글에 " 느낀점 " 에도 나와있다 😢
가치택시(매칭 알고리즘)
서론방학이 시작되고 Leets 4기에서 프로젝트를 시작하게 되었고, 내가 참여하여 시작한 프로젝트의 주제는 " 교내 학생들의 택시 이용시, 요금과 시간의 부담을 줄여줄 수 있는 택시 매칭 서비
huncozyboy.tistory.com
구현 중 고려사항
DTO의 책임
객체를 생성하는 방식은 크게 두 가지 접근 방식이 있다. 구현하기전, 그리고 해당 우석이형의 리뷰를 보고, 각 방식의 장점과 단점에 대해 고민해보았다
1. 정적 팩토리 메서드에 DTO를 직접 넘겨주는 방식
장점은 메서드가 간결해지고, 객체 생성의 책임이 DTO 내부로 이동하여 객체를 DTO 기반으로 직접 생성해줘서 일관된 생성 방식 유지가 있지만, DTO가 엔티티의 생성 과정까지 책임지는 구조가 되어 객체 간 의존성이 강해질 가능성이 있다는 단점이 있다
2. DTO를 넘기지 않고, 개별 파라미터를 전달하는 방식
엔티티가 DTO에 의존하지 않도록 유지할 수 있어 객체의 간 의존성 문제를 해결할 수 있고
DTO 변경이 엔티티의 생성 방식에 영향을 미치지 않으므로 코드 수정이 쉬울 수 있다. 하지만 단점은 객체를 생성할 때 전달해야 하는 매개변수가 많아지게 되면 코드의 간결성이 다소 떨어질 수 있다
이번 작업에서는 기존 메서드와의 일관성을 유지하기 위해 두 번째 방식을 사용했지만,
기존 객체 생성 시 DTO를 넘기는 것이 좋은가 ? 개별 필드만 넘기는 것이 좋은가 ? 에 대한 방식에 대한 내용은
추후 백엔드 회의에서 DTO의 책임과 엔티티 생성 방식을 명확하게 정의하여 가독성과 통일성을 위해 하나로 통합하기로 했다(아마 나도 정적 팩토리 메서드를 자주 사용하는 편이기때문에 해당 방식으로 통일할거같다)
수동매칭 -> 자동매칭 전환 로직
먼저 구현한 플로우는 아래와 같았다
@Transactional
public void convertToAutoMatching(Long roomId) {
MatchingRoom matchingRoom = this.matchingRoomRepository.findById(roomId)
.orElseThrow(NoSuchMatchingRoomException::new);
if (!matchingRoom.isActive()) {
throw new NotActiveMatchingRoomException();
}
if (LocalDateTime.now().isAfter(matchingRoom.getDepartureTime().minusMinutes(10))) {
int currentMembers = this.memberMatchingRoomChargingInfoRepository
.countByMatchingRoomAndPaymentStatus(matchingRoom, PaymentStatus.NOT_PAYED);
if (matchingRoom.isAutoConvertible(currentMembers)) {
matchingRoom.convertToAutoMatching();
matchingRoomRepository.save(matchingRoom);
}
}
}
1. 방이 ACTIVE 상태인지 확인하고, 삭제된 방이나 비활성화된 방은 예외처리
2. 출발 10분 전인지 확인하여 전환 시점을 결정
3. NOT_PAYED 상태의 참여자 수를 조회하여 현재 방에 남아 있는 멤버 수(정산하지 않은)를 확인
4. 최소 인원 미달 시 자동 전환 가능 여부를 판단한 뒤에 (matchingRoom.isAutoConvertible 조건 사용)
5. 자동 매칭으로 전환 가능한 상태 확인 후 matchingRoom.convertToAutoMatching()을 호출하여 자동 매칭으로 상태 변경
결론만 먼저 얘기하자면, 해당 내용은 MVP 제외 후 최종발표 후 리팩토링시 구현하도록 하였다
/* todo 수동 매칭 → 자동 매칭 전환 : 추후 고도화시, 10분전에 유저에게 알림을 주고 자동 매칭으로 전환 */
이유는 첫번째로 자동 전환이 강제적으로 이루어지는 방식 때문이였다
사용자가 수동 매칭을 선택한 상태에서 갑자기 자동 매칭으로 전환되면 혼란을 초래할 가능성이 클거 같다는 강혁이의 의견이 있었고, 요구사항에 맞춰 구현하던 나도 똑같이 생각했던 문제였기에 해당 의견에 동의하였다
두번째는 프론트엔드 분들의 추가적인 작업 필요
만약 첫번째 방식에서 제시된 문제를 해결하려면 별도의 모달 창 등의 인터페이스가 필요할 것이라고 생각을 했고, 그러면 프론트엔드 분들이 추가적인 UI/UX 설계가 필요한 부분이였기에, 최종발표때까지 남은 기간이 얼마 없었기에, 다른 MVP 들이 우선이라고 결론을 내리게 되었다
최종발표 후 리팩토링을 진행할때 구현하려고 생각했던 플로우는 일단 아래와 같다
1. 수동매칭 -> 자동매칭 전환전에, 알림을 통해 자동 매칭 전환 동의 여부(수락/거절) 를 유저에게 띄워준다
2. 모든 사용자가 수락하면 자동 매칭으로 전환하고, 한명의 사용자라도 거절시 방을 CANCELLED 처리한다
결론
느낀점
일단은 첫번째로 최종 발표를 앞두고 있는 시점에서 로직을 완성한 뒤에 테스트까지 무사히 완료해서 뿌듯하고 팀원들에게 도움이 됐다는 사실도 좋았다.
게시글의 내용을 보면 알 수 있겠지만 구현 전에 기능 설계를 하며 고민했던 부분도 많았고, 구현 중 고민했던 부분 또한 정말 많았는데,
팀원들이 리뷰로 세세한 내용들까지도 빠짐없이 남겨줘서 같이 고민할 수 있었고 그 결과로 요구사항에 맞는 API가 나올 수 있었다고 생각한다 🔥
계획
마지막으로 수동매칭의 리스트를 넘겨주는 API + 나의 매칭 리스트를 확인할 수 있는 API
이 두가지의 GET API의 내용은 게시글에 담지 않은 이유는 pageNumber와 pageSize를 기반으로 페이징 처리한 뒤에 무한 스크롤 방식으로 구현했기에, 해당 내용을 담은 별도의 회고로 추가적으로 게시글을 작성해보려고 한다 👍🏻
참고한 자료
https://bin-kkwon.tistory.com/entry/SQL-Hard-Delete-Soft-Delete
'회고록 > 매칭 로직' 카테고리의 다른 글
가치택시(매칭 알고리즘, Spring) (0) | 2025.01.25 |
---|