Post

[둥지] 사용자 정보 수정: 단일 API vs 분리형 API

사용자 프로필 수정 기능을 구현하며 겪은 API 설계 고민을 공유합니다. 모든 정보를 한 번에 수정하는 단일 API 대신 닉네임, 비밀번호, 프로필 사진 엔드포인트를 분리한 이유와 S3 Presigned URL을 활용한 이미지 업로드 최적화 과정을 다룹니다.

[둥지] 사용자 정보 수정: 단일 API vs 분리형 API

사용자 프로필 수정 기능은 백엔드 개발자라면 누구나 한 번쯤 구현하게 되는 기능입니다. 하지만 ‘둥지’ 프로젝트에서 이 기능을 설계할 때, 생각보다 깊은 고민에 빠졌습니다.

“닉네임, 비밀번호, 프로필 사진을 하나의 PATCH /users/me API에서 일괄 처리할 것인가, 아니면 각각의 엔드포인트로 분리할 것인가?”

결론부터 말씀드리면, 우리는 엔드포인트를 3개로 완전히 분리하는 전략을 택했습니다. 이번 포스트에서는 그 아키텍처적 배경과 각 API의 구현 디테일, 그리고 S3 Presigned URL을 활용한 이미지 업로드 최적화 과정을 공유합니다.


통짜 API(일괄 수정)를 포기한 3가지 이유

처음에는 하나의 API로 모든 수정을 처리하는 것이 클라이언트 입장에서 호출 횟수를 줄일 수 있어 효율적일 것이라 생각했습니다. 하지만 다음과 같은 치명적인 문제들이 있었습니다.

  1. 데이터 형태의 불일치 (JSON vs Multipart): 닉네임과 비밀번호는 application/json으로 처리하는 것이 깔끔하지만, 프로필 사진은 이미지 파일이므로 multipart/form-data를 사용해야 합니다. 이를 하나의 API에서 억지로 묶어 처리하면 백엔드의 파싱 로직이 지저분해집니다.
  2. 복잡한 유효성 검증 (Validation): 사용자가 닉네임만 바꾸고 싶을 때도 있고, 비밀번호만 바꾸고 싶을 때도 있습니다. 단일 API에서는 어떤 필드가 들어왔는지 분기 처리(If-else)를 하며 Pydantic 검증을 선택적으로 적용해야 하므로 코드가 비대해집니다.
  3. 사용자 경험 (UX)의 차이: 보통 서비스에서 닉네임/사진 변경은 가벼운 프로필 탭에서 이루어지지만, 비밀번호 변경은 ‘보안/계정 설정’ 탭에서 기존 비밀번호를 입력하는 엄격한 과정을 거칩니다. UX 흐름이 다르므로 API도 분리하는 것이 자연스럽습니다.

이에 따라 책임을 명확히 나눈 3개의 엔드포인트를 설계했습니다.

텍스트 정보 수정: 닉네임과 비밀번호

텍스트를 다루는 두 API는 직관적인 JSON PATCH 요청으로 구성하되, 엣지 케이스를 방어하는 촘촘한 예외 처리에 집중했습니다.

닉네임 변경 (PATCH /api/v1/users/me/nickname)

닉네임은 서비스 내에서 사용자를 식별하는 중요한 수단이므로 중복 검증이 핵심입니다.

  • 유효성 검증: 1~20자 제한, 공백만으로 구성 불가
  • 핵심 예외 처리: 다른 사용자가 이미 사용 중인 닉네임일 경우 NicknameDuplicateException(409 Conflict)를 반환하여 프론트엔드가 적절한 경고를 띄울 수 있도록 합니다.

비밀번호 변경 (PATCH /api/v1/users/me/password)

가장 민감한 정보인 만큼 기존 비밀번호(current_password) 검증을 선행합니다.

  • 소셜 로그인 예외 처리: 구글, 카카오 등 OAuth로 가입한 유저는 DB에 비밀번호가 존재하지 않습니다(password == None). 이들이 비밀번호 변경을 시도하면 SocialUserPasswordChangeException(400 Bad Request)을 던져 명확히 차단합니다.
  • 비밀번호 규칙 검증: 영문+숫자+특수문자 조합 및 8자 이상의 강력한 규칙을 Pydantic으로 검증합니다.

S3 Presigned URL 도입

프로필 사진 변경은 API 분리 전략의 가장 큰 수혜를 본 기능입니다. 클라이언트가 백엔드 서버로 이미지를 보내고, 백엔드가 다시 S3로 업로드하는 기존 방식은 백엔드 서버의 네트워크 대역폭과 메모리를 이중으로 낭비합니다.

이를 해결하기 위해 클라이언트가 S3에 직접 이미지를 업로드하는 Presigned URL 아키텍처를 도입했습니다.

프로필 사진 변경 흐름

프로필 사진 변경 시퀀스 다이어그램 프로필 사진 변경 시퀀스 다이어그램

  1. URL 발급 요청: 클라이언트가 백엔드에 확장자 정보(content_type)를 담아 POST /api/v1/users/me/profile-image/presigned-url을 호출합니다.
  2. Presigned URL 생성: 백엔드는 AWS 권한을 이용해 10분간 유효한 S3 업로드용 임시 티켓(URL)을 발급하여 반환합니다.
    • S3 Key 규칙: profile/{user_id}/{uuid4}.{ext}
  3. Direct Upload: 클라이언트는 백엔드를 거치지 않고, 발급받은 URL을 향해 이미지를 직접 PUT 요청으로 보냅니다.
  4. DB 업데이트: 업로드가 완료되면, 클라이언트는 PATCH /api/v1/users/me/profile-image를 호출하여 성공적으로 올라간 이미지의 최종 S3 URL을 DB에 저장합니다.

이 구조를 통해 백엔드 API 서버는 무거운 이미지 트래픽을 전혀 감당할 필요 없이, 가벼운 JSON 통신과 권한 제어 역할만 수행하게 되어 서버 안정성이 극대화되었습니다. 보안을 위해 content_typeimage/jpeg, image/png, image/webp 3가지만 허용하도록 제한을 두었습니다.


마치며

통짜 API 하나를 만드는 것이 초기 개발 속도는 빠를 수 있습니다. 하지만 유지보수성, 프론트엔드와의 협업 효율, 그리고 인프라 트래픽 비용을 종합적으로 고려했을 때 기능별로 잘게 쪼개진 분리형 API와 Presigned URL의 조합은 그 수고로움을 상쇄하고도 남는 이점을 가져다주었습니다.

현재 회원가입(auth) 도메인과 이번 정보 수정(user) 도메인에 비밀번호 검증 규칙이 중복으로 작성되어 있습니다. 다음 스텝으로는 이 규칙을 공통 Validator 객체로 추출하여 중복 코드를 제거하는 리팩토링을 진행할 예정입니다.

This post is licensed under CC BY 4.0 by the author.