woowacourse-teams / 2023-festa-go

🎪 페스타고, 대학 축제를 더욱 즐겁게!
71 stars 8 forks source link

[BE] feat: 이미지 업로드 기능 추가 (#929) #930

Closed seokjin8678 closed 4 months ago

seokjin8678 commented 4 months ago

📌 관련 이슈

✨ PR 세부 내용

본문이 꽤 깁니다. 주의!!!

아기다리고기다리던 이미지 업로드 기능을 추가했습니다.

이미지 업로드는 S3가 아닌 R2에 저장됩니다.

또한 R2를 사용해도 S3 API를 그대로 사용할 수 있기 때문에 S3 의존성을 추가했습니다.

이미지 업로드 기능이 추가될 때 발생하는 문제점은 기존 생성 API들이 모두 바뀐다는 점입니다. (학교, 아티스트, 축제 등)

때문에 출시를 앞둔 상황에서 모든 생성, 수정 API를 수정하기는 거의 불가능하고 시간도 무척 오래 소요됩니다. 😂 덤으로 지금까지 작성한 테스트 코드도 갈아 엎어야 하구요. (물론 컨트롤러 쪽이라 수정하는 곳은 적겠지만...)

따라서 기존 기능을 수정하지 않고 새롭게 이미지 업로드 기능을 추가할 방법이 필요했습니다.

따라서 조금 복잡하지만, 기존 기능을 수정하지 않고 이미지를 업로드 할 수 있는 기능을 만들었습니다.

사실 이미지를 업로드 하는 기능은 단순한데, 이미지 파일, 이미지를 추가할 대상의 식별자와 타입을 받으면 끝입니다.

하지만 이미지의 업로드와 대상(학교, 축제)의 생성 시점이 다른 게 문제입니다. 😂

이미지는 UploadFile이라고 이름을 정했고, Upload가 붙은 이유는 해당 이미지의 생성 시점은 이미지가 스토리지에 저장된 후에 생성되도록 의도했기 때문입니다. (Image가 아닌 File인 이유는 혹시 이미지 말고 다른 포맷도 추가할 수 있기에 추상적인 의미의 File을 사용했습니다)

UploadFileUploadStatus라는 상태를 가지는데 다음과 같습니다.

  1. UPLOADED (주인의 식별자와 타입이 정해지지 않은 상태, 하지만 주인은 해당 파일 경로를 가지고 있을 수 있음)
  2. ASSIGNED (주인의 식별자와 타입을 가지고 있는 상태, 하지만 주인이 파일을 가지고 있지 않을 수 있음)
  3. PRE_ATTACHED (ATTACHED가 되기 전 임시 상태, 자세한 설명은 후술)
  4. ATTACHED (주인의 식별자와 타입을 가지고 있고, 주인이 해당 파일을 가지고 있는 상태)
  5. ABANDONED (버려진 상태, 해당 상태는 추후 삭제 대상이 됨)

이 상태들이 필요한 이유는 생성, 수정, 삭제 때문인데, 밑에서 과정별로 설명하겠습니다.


생성

생성의 경우 흐름은 다음과 같습니다. (학교를 추가할 때 예시)

  1. 사용자가 학교 추가 폼에 정보를 입력
  2. 이미지 업로드 버튼을 눌러 이미지를 업로드 (UploadFile 엔티티 생성, 영속, 이때 상태는 UPLOADED)
  3. 학교 생성 버튼 클릭 (학교 엔티티 생성, 영속)

UploadFile은 생성되고 영속되었지만, UploadFile의 주인은 UploadFile보다 늦게 생성되기 때문에 UploadFile은 주인을 모릅니다. 따라서 UploadFile의 관리가 불가능하기 때문에 학교의 이미지가 다른 이미지로 수정됐을 때 추적이 불가능합니다.

따라서 주인이 생성될 때 이벤트를 발행하여, 주인이 가지고 있는 이미지 URI를 파싱해서 UploadFile의 식별자를 추출합니다.

그리고 해당 식별자에 대한 영속된 UploadFile을 찾아, 해당 주인을 설정합니다. (ATTACHED 상태로 변경)

이 과정에서 다음과 같은 흐름이 생길 수 있습니다.

  1. 사용자가 학교 추가 폼에 정보를 입력
  2. 이미지 업로드 버튼을 눌러 이미지를 업로드 (이미지 엔티티 생성, 영속)
  3. 사용자가 다른 마음이 생겨서 취소 또는 다른 이미지 업로드 (새로운 이미지 엔티티 생성, 영속)

그러면 취소 또는 새롭게 생성되기 전 이미지는 어디에도 사용되지 않는 쓰레기가 됩니다.

하지만 생성의 경우에는 처리가 쉬운 것이 UPLOADED 상태이고 createdAt 필드가 오래 지난 것을 대상으로 정리를 해주기만 하면 됩니다.


수정

수정의 경우 흐름은 다음과 같습니다.

  1. 사용자가 학교 수정 폼에 정보를 입력
  2. 이미지 업로드 버튼을 눌러 이미지를 업로드 (UploadFile 엔티티 생성, 영속, 이때 수정은 식별자가 있으니 ASSIGNED 상태)
  3. 학교 수정 버튼 클릭 (학교 엔티티 수정)

수정은 위와 같이 파일을 생성할 때, 주인이 누구인지 식별할 수 있으니 생성 시점에 주인을 설정할 수 있습니다.

하지만 기존 이미지를 대체해야 하는 경우가 발생하는데, 사용자가 이미지를 수정하면 기존 ATTACHED 상태의 UploadFile은 ABANDONED 상태로 변경되어야 합니다.

여기서 여러 분기가 생기는데, 사용자가 이미지를 수정하지 않고 이름과 같은 값을 변경하는 경우와 수정할 이미지가 2개 이상이면 어떤 이미지를 어떤 상태로 변경해야 하는지 모르는 문제가 생깁니다.

따라서 이를 해결하려면 다음과 같은 방법을 사용해야 합니다.

수정 시점에 주인이 가지고 있는 모든 이미지 URI를 파싱하여 UploadFile의 식별자 목록을 추출합니다. (이 식별자 목록이 최종적으로 ATTACHED 상태가 되어야 할 UploadFile 입니다)

그리고 주인의 식별자와 타입을 기준으로 UploadFile을 모두 조회합니다.

그리고 조회한 UploadFile이 가지고 있는 상태와 식별자 목록에 포함되어 있는지를 판단하여 ATTACHED, ABANDONED 상태로 변경합니다.

바로 여기서 PRE_ATTACHED 상태가 중요하게 사용됩니다.

  1. UploadFile이 식별자 목록에 포함되어 있으면 해당 UploadFile의 상태를 PRE_ATTACHED 상태로 변경
  2. ASSIGNED, ATTACHED 상태면 ABANDONED 상태로 변경
  3. PRE_ATTACHED 상태면 ATTACHED로 변경

이를 통해 최종적인 파일만 ATTACHED 상태로 유지하고, 나머지 파일들은 ABANDONED 상태로 변경시킬 수 있습니다.


삭제

삭제의 경우 단순하게 주인이 가진 UploadFile을 모두 찾아 ABANDONED 상태로 변경합니다.


조금 길긴 했지만, 대충 이미지가 저장되고 관리되는 흐름은 이렇습니다.

나중에 추가해야 할 사항으로, 기간이 오래 지난 UPLOADED 상태 또는 ABANDONED 상태의 UploadFile을 정리하는 로직이 필요합니다.

아마 이건 스케쥴러를 사용해서 해결할 수 있을 것 같기도 하네요. (어드민 API를 열어도 될 것 같습니다)

약간의 고려한 사항으로는, 특정 주인이 가지고 있는 업로드 된 이미지가 다른 주인에게도 사용될 수 있다는 점인데... 이렇게 되면 원래 주인이 가진 이미지가 수정되는 상황이 발생하면 이 이미지는 삭제 대상이 되므로 의도치 않은 사이드 이펙트가 발생할 수 있습니다.

따라서 영구적인 상태(PERMANENT)를 추가하여 ABANDONED 상태가 되지 않도록 할 수도 있을 것 같습니다.

github-actions[bot] commented 4 months ago

Test Results

226 files  226 suites   27s :stopwatch: 751 tests 751 :white_check_mark: 0 :zzz: 0 :x: 770 runs  770 :white_check_mark: 0 :zzz: 0 :x:

Results for commit f02296a6.

:recycle: This comment has been updated with latest results.

seokjin8678 commented 4 months ago

큰 기능이라 리뷰가 필요하지만, 현재 서비스가 운영 환경에 올라가 있는 상태라서 빠른 기능 추가가 필요합니다..! 따라서 기다리지 않고 바로 머지 처리 하도록 하겠습니다. 관리자에서 이미지를 업로드 하므로 버그가 생기더라도 큰 문제는 없을 것 같습니다. 추후 의견 있으시거나 버그 발견 시 이슈로 부탁드립니다!