Articket 서비스 소개
Articket은 이미지 생성 AI 모델인 Stable Diffusion을 사용해 사용자의 사진을 유명 화가(피카소, 르누아르, 고흐, 리히텐슈타인)의 스타일로 변환하고, 이를 티켓 형태로 출력해주는 서비스다.
2024년 대구 EXCO에서 열린 CO-SHOW 경진대회에 참가해 이 서비스로 3박 4일 동안 약 200명을 대상으로 부스를 운영했고, 협의회장상을 수상했다.
이번에는 서비스를 개선해 서울시립대 신입생 환영회 동아리 홍보제 부스를 운영했다.
CO-SHOW 경진대회에서 운영했던 Articket 1.0은 8문항의 성격 테스트를 통해 매칭된 화가를 기준으로, 잘 맞는 화가 유형과 잘 안 맞는 화가 유형을 포함한 티켓을 출력해주는 방식이었다.
하지만 이 방식에는 몇 가지 문제가 있었다.
1. 한 사람당 체험 시간이 길다.
2. 서비스 체험 방법을 설명해 줄 인력이 필요하다.
3. 티켓을 출력해 줄 추가적인 인력이 필요하다.
부스 운영의 목적은 기술력을 보여주고, 흥미를 유도하는 것이기 때문에, 이번에는 성격 테스트를 통한 화가 매칭 기능을 삭제하고 "사진 변환"이라는 핵심 기술만 남겼다.
사용자는 사진을 업로드하면 즉시 화가의 스타일로 변환된 이미지를 볼 수 있고, 변환된 이미지가 포함된 티켓을 휴대폰으로 확인하고 저장할 수 있도록 변경했다.
과정도 더 단순화했다.
기존에는 생성된 이미지를 백엔드에서 base64로 변환해 프론트엔드로 전달했는데, 생성된 이미지가 1024x1024 크기라 부하가 컸다.
이 과정 대신, 이미지를 백엔드에 저장하고 이미지 경로만 프론트엔드에 반환하도록 변경했다.
프론트엔드는 이 경로를 활용해 티켓을 생성하고, 사용자가 "티켓 발급" 버튼을 누르면 티켓이 캡처되어 휴대폰에 저장되도록 했다.
(quipu_say.jpeg라는 이미지를 하나 더 저장되도록 하여 인스타 태그해달라는 메세지를 전달했다..ㅎㅎ)
이렇게 기능을 단순화하여, 많은 인력을 들이지 않고도 편리하고 예쁜 티켓을 제공할 수 있게 되었다.
여기까지가 바뀐 기능에 대한 설명이고, 이제부터는 지난 부스 운영 시 불편했던 점과 이를 어떻게 개선했는지에 대해 이야기하려 한다.
부스 운영을 위한 리팩토링: 무엇을 개선했나?
기존에는 모든 코드가 app.py 하나에 몰려있었다. 하지만 유지보수를 쉽게 하기 위해 핵심 뼈대를 담당하는 함수와 세부 기능을 담당하는 함수를 분리하는 것이 필요했다. 따라서 용도에 따라 파일을 구분하여 관리하도록 변경했다.
백엔드에서 이미지를 처리하는 함수와 Stable Diffusion을 이용해 이미지를 생성하는 함수 이 두 가지를 각각 utils에 정리했다.
그리고, 전체적인 API 플로우를 담당하는 코드는 routes에 배치하여 분리했다.
Stable Diffusion을 실행할 때는 RunPod에서 GPU를 대여해 사용량만큼 비용을 지불하는 방식을 사용한다. 하지만 GPU 사용 비용이 커서 개발하는 동안 계속 실행할 수 없다는 문제가 있었다.
이 때문에 기존에는 개발 중 AI 모델을 실행하지 않을 때마다 AI 코드 주석 처리하고, 임시 이미지 데이터를 수동으로 삽입하여 테스트하여 번거롭게 작업해야 했다.
이를 해결하기 위해 config 파일을 추가했다.
config 파일에서 AI 사용 여부를 True/False로 설정할 수 있도록 만들었고, 이를 통해 AI를 실행할지, 아니면 더미 데이터를 활용할지 자동으로 선택되도록 했다.
또한, Stable Diffusion을 실행할 때의 여러 설정을 편리하게 관리할 수 있도록 다음과 같은 옵션을 추가했다.
지난 부스를 운영할 때, 원래는 로컬에서 서버를 실행하여 서비스를 제공했다. 방문자는 PC 화면에 표시된 QR 코드를 스캔해 모바일로 접속했는데, 여기서 문제가 발생했다.
로컬 서버를 사용할 경우, 사용자의 휴대폰이 서버와 동일한 네트워크(와이파이) 에 접속해야만 내부 IP (예: 192.168.x.x) 를 통해 페이지에 접근할 수 있었다. 즉, 서버가 연결된 네트워크에 방문자의 휴대폰도 연결해야 했으며, 그렇지 않으면 접속이 불가능했다. 이 과정이 번거로웠다.
이 문제를 해결하기 위해 서버를 배포하기로 했다. 서버를 외부에 배포하면 공인 IP 또는 도메인을 통해 접속할 수 있어, 네트워크 환경과 관계없이 어디서든 접속할 수 있다. 즉, 사용자는 같은 와이파이에 접속할 필요없이 QR 코드만 스캔하면 서버에 접속할 수 있어, 네트워크 설정을 안내할 필요 없이 서비스 이용이 가능해진다.
프론트엔드는 처음엔 vercel로 배포했었다.
근데 간헐적으로 접속이 안되는 문제가 있었고, 찾아보니 한국에서 커스텀 도메인을 연결하지 않은 기본 vercel.app 도메인을 사용하면 한 번씩 들어가지지 않는 문제가 발생한다고 한다.
그래서 vercel와 비슷한 무료 호스팅 플랫폼인 netlify로 프론트엔드를 배포하였다.
백엔드는 Flask를 간단하게 배포할 수 있고, $5.00을 제공해주는 Railway를 사용하였다.
기존에는 사용자 이름, 성별, 업로드한 이미지 경로, 생성된 이미지 경로를 모두 전역 변수로 관리하고 있었다. 또한, 소켓을 통해 연결된 모든 클라이언트에게 데이터를 브로드캐스트하는 방식으로 구현되어 있었다.
처음에는 부스 운영 시 한 번에 한 명의 사용자만 참여하도록 설계했기 때문에 이러한 방식이 큰 문제가 되지 않았다. 동시에 여러 명이 참여해야 할 경우에도, 여러 대의 컴퓨터에서 로컬 서버를 실행하는 방식으로 대응할 계획이었기 때문에 전역 변수를 사용해도 데이터 충돌이 발생할 가능성이 낮았다.
그러나 배포 환경으로 전환하면서 문제가 발생했다. 부스 운영 시 같은 네트워크(Wi-Fi)에서 다수의 사용자가 접속하는 것을 피하기 위해 외부 서버에 배포해야 했고, 이렇게 되면 모든 사용자가 같은 서버에 접속하게 되어 데이터가 서로 섞이는 문제가 발생했다. 즉, 동시에 여러 명이 참여하면 서로의 데이터가 공유되는 심각한 이슈가 있었다.
이를 해결하기 위해 사용자 데이터를 전역 변수에서 세션(Session)으로 관리하는 방식으로 변경했다.
클라이언트는 쿠키를 이용해 본인의 세션을 식별하고, 서버는 해당 세션에서 필요한 데이터를 불러와 사용하도록 수정했다.
또한, QR 코드를 통해 PC 화면에서 모바일로 접속할 때 room ID를 전달하여 특정 사용자만 같은 방(Room)에 속하도록 설정했다. 이렇게 하면 해당 Room 내에서만 데이터를 주고받고, 다른 Room과는 데이터를 공유하지 않도록 구현할 수 있었다.
하지만 배포 환경에서는 세션에서 값을 찾지 못하는 문제가 발생했다.
이는 프론트엔드 도메인과 백엔드 도메인이 서로 다를 경우, 브라우저가 크로스 도메인 환경에서 쿠키 전송을 차단하기 때문이었다.
이를 해결하기 위해 아래와 같은 설정을 추가했다.
app.config["SESSION_COOKIE_SAMESITE"] = "None" # 크로스 도메인에서 쿠키 허용
app.config["SESSION_COOKIE_SECURE"] = True # HTTPS에서만 쿠키 전송 가능하도록 설정
이 설정을 적용하자 크롬과 파이어폭스에서는 정상적으로 세션을 찾을 수 있었다.
하지만 사파리에서는 여전히 세션을 찾지 못하는 문제가 발생했다.
사파리는 제3자 쿠키(Third-party Cookies)를 완전히 차단하기 때문에 위와 같은 설정만으로는 해결되지 않았다.
이 문제를 해결하려면 같은 도메인을 사용하거나 JWT 방식으로 전환하는 방법이 필요했다.
혹시나 iOS에서 크롬을 사용하면 문제가 해결될까? 싶어 테스트해보았지만,
똑같이 세션을 찾지 못하는 오류가 발생했다.
조사해보니 iOS에서는 모든 브라우저(Chrome, Firefox, Edge 등)가 WebKit(= 사파리의 엔진)을 사용하기 때문에 iOS의 크롬은 사실상 "사파리 껍데기를 씌운 크롬"이었다.
즉, iOS 환경에서는 어떤 브라우저를 사용하더라도 사파리의 쿠키 정책을 그대로 따르게 되어 문제를 해결할 수 없었다.
결국, 같은 도메인에서 프론트엔드와 백엔드를 운영하거나, JWT 인증 방식으로 전환하는 것이 불가피했다.
시간 관계상 당장은 기존 방식으로 부스를 운영했지만, 추후에는 서드파티 쿠키 차단 문제를 해결하는 방향으로 개선할 계획이다.
기존에는 이미지 전처리 없이 원본을 그대로 base64로 변환하여 AI 서버로 전송하고 있었다.
그러나 고용량 이미지가 그대로 전송되면서 메모리 사용량이 급격히 증가했고, 이로 인해 간헐적으로 타임아웃(Timeout) 에러가 발생했다.
또한, 네 명의 화가 스타일로 이미지를 생성하는 과정에서 세 명의 화가 스타일 변환이 성공하더라도, 한 명의 화가 스타일 변환이 실패하면 전체 요청이 실패하는 구조였다.
이를 완화하기 위해, 실패한 화가 스타일에 대해 최대 3회까지 재시도하는 방식을 적용하여 전체 실패율을 낮추려 했다.
하지만 이는 근본적인 해결책이 아니었기 때문에, AI 서버의 메모리 사용량 자체를 줄이는 방향으로 개선하고자 했다.
이미지의 크기를 가로·세로 512px 이하로 제한하여, 퀄리티에 영향을 주지 않는 선에서 파일 크기를 줄였다.
기존에는 PNG 형식으로 저장했지만, JPG로 변환하여 파일 크기를 줄였다.
PNG는 비손실 압축 방식이라 파일 크기가 크고, AI 모델이 손실 압축을 감안한 학습을 진행했을 가능성이 높아 JPG 변환이 성능에 영향을 주지 않는다고 판단했다.
이러한 전처리 과정을 통해 AI 서버의 메모리 사용량을 줄이고, 타임아웃 에러 발생 가능성을 낮출 수 있었다.
이번 리팩토링에 대한 회고
이번 작업을 통해 배포 환경에서의 네트워크 설정과 안정적인 서비스 설계의 중요성을 다시 한번 깨달았다.
프론트엔드와 백엔드를 다른 도메인에서 운영할 경우, CORS나 서드파티 쿠키 차단 등 다양한 보안 제약 사항이 발생할 수 있다.
→ 앞으로는 같은 도메인을 사용하는 방식을 선택하는 것이 안정적인 배포를 위해 중요하다고 느꼈다.
로컬에서는 잘 작동하는 서비스가 배포 환경에서도 문제없이 작동한다는 보장이 없다.
→ 같은 네트워크에서 정상적으로 동작하는 서비스라도, 외부 네트워크에서 접근할 때는 쿠키 설정, 세션 관리, 도메인 정책 등 추가적인 고려 사항이 많다.
처음 개발 환경을 설정할 때 배포까지 고려한 구조를 미리 설계하는 것이 매우 중요하다.
→ 배포 후 여러 문제를 수정하는 것이 아니라, 처음부터 배포 환경에서도 원활하게 동작할 수 있도록 환경을 설정해야 한다는 점을 다시 한 번 느꼈다.
실제 운영될 서비스를 고려한 개발 환경 설정과 최적화를 신경 써야겠다.