A/B 프로젝트에서 토픽 생성 기능이 존재한다. 토픽은 선택지 A와 B를 가지고 있다. 선택지는 텍스트 타입 또는 이미지 타입으로 등록이 가능하다.
이미지 타입으로 토픽을 생성하기 위해서는, AWS에 프런트가 직접 이미지를 등록해야 한다. 과정은 아래와 같다
1 번과 2 번의 과정은 PresignedImageRepository의 upload 메서드 안에서 모두 처리할 것이다.
protocol PresignedImageRepository: Repository {
func upload(bucket: ImageBucket, request: UIImage) async throws -> String
}
우선 아래와 같이 틀을 작성했다.
images 데이터를 만든 후 반복문으로 이미지를 하나씩 처리할 수 있도록 구현했다. 이렇게 구현한 이유는 현재 기획 상으로는 업로드하는 사진이 2장이긴 하지만, 확장성을 고려했을 때 더 많은 선택지/이미지가 추가될 수 있다고 생각했다. 그래서 직접 변수를 하나씩 추가하는 것보다는 반복문을 활용하는 것이 맞다고 판단하게 되었다.
func makeChoicesDTO() async throws -> [TopicGenerateRequestDTO.ChoiceRequestDTO] {
let images = request.choices.map{ $0.image }
var url = [String?](repeating: nil, count: request.choices.count)
for (i,image) in images.enumerated() {
if let image = image {
//이미지 업로드 작업 구현
}
}
return request.choices.enumerated()
.map{ i, choice in
.init(
choiceOption: choice.option.toDTO(),
choiceContentRequest: .init(
type: "IMAGE_TEXT",
imageUrl: url[i],
text: choice.text
)
)
}
}
틀을 작성한 이후, 문제에 직면하였다. '여러 이미지를 업로드해야 할 때, 동시에 처리하려면 어떻게 코드를 구현해야 하는가?' 였다.
1. try await
for (i,image) in images.enumerated() {
if let image = image {
url[i] = try await presignedImageRepository.upload(bucket: .topic, request: image)
}
}
이 코드는 순차적으로 작업이 진행된다는 문제점이 있다. 0번 인덱스의 이미지 업로드 작업이 완료되면, 1번 인덱스 이미지에 대한 업로드 작업을 진행하게 된다. try await을 실행시킨 작업이 비동기로 수행되긴 하지만, 작업을 끝날 때까지 기다리기 때문이다. 즉, 비동기를 기대했지만, 실질적으로 비동기로 동작하지 않는 경우이다.
이미지 하나당 url을 받아오고, 다시 url로 이미지를 업로드하는 2 번의 API 과정을 거쳐야 하는데, 이를 여러 개의 이미지에 대해 진행할 경우 엄청난 시간이 소요될 것이다.
2. Task
접근 1에서 반복문 안의 작업이 비동기 작업을 수행되지 않았다. 그럼 비동기로 수행될 수 있게 Task 안에 넣어주면 어떨까? 생각했다.
for (i,image) in images.enumerated() {
if let image = image {
Task {
url[i] = try await presignedImageRepository.upload(bucket: .topic, request: image)
}
}
}
하지만 이 방식은 2가지의 문제점이 발생하며, 기능을 제대로 수행하지 못한다.
3. DispatchGroup
비동기 작업 여러 개를 순차적 실행이 아닌, 동시에 실행시킬 수 있는 방법이 무엇이 있을까 고민을 하다가, 문득 iOS 면접 공부를 하다가 간단하게 기억하고 넘어갔던 Dispatch Group 개념이 생각났다.
반복문 안의 작업을 그룹에 넣어주고, 모두 완료된 시점에 notify 메서드를 통해 알림을 받아 메서드 안에서 DTO를 반환하도록 구현하는 것이다.
Dispatch Group에 대한 설명은 아래 링크를 참고
https://sujinnaljin.medium.com/ios-차근차근-시작하는-gcd-7-4d9dbe901835
4. Task Group
3 번 접근 방식에 대한 연장선이다.
3번 방식의 경우 GCD 방식인데, 평소 Swift Concurrency 기술을 사용하면서 가독성과 에러 핸들링 측면에서 GCD 보다 많은 이점을 느꼈었기에 조금 더 선호하는 경향이 있었다. 그래서 Swift Concurrency로 Dispatch Group과 같은 기능을 구현할 수 있나 찾아보았고, 아래 메서드들을 발견하였다.
withTaskGroup(of:returning:body:) //에러를 발생시키지 않는 경우
withThrowingTaskGroup(of:returning:body:) //에러가 발생할 수 있는 경우
위의 메서드 중 하나를 활용하여 문제를 해결해 보도록 하겠다.
이미지 업로드가 순차적으로 진행되는 것이 아니라, 동시에 진행되는 것을 확인할 수 있었다.
구체적인 수치는 아직 presigned url로 이미지를 업로드할 때 접근 오류가 발생해 측정해보지는 않았지만, 로그 찍히는 순간을 확인해 보았을 때 크게 개선된 것을 느낄 수 있었다. 나중에 오류가 해결된 후 다시 1번 방식과 4번 방식의 성능을 측정해 비교해 보면 좋을 것 같다.
(성능 측정을 위해 이전 코드도 주석 처리하여 남겨 놓았다^^)
func makeChoicesDTO() async throws -> [TopicGenerateRequestDTO.ChoiceRequestDTO] {
let images = request.choices.map{ $0.image }
var url = [String?](repeating: nil, count: request.choices.count)
try await withThrowingTaskGroup(of: (Int, String).self) { group in
for (i, image) in images.enumerated() {
if let image = image {
group.addTask{
(i, try await self.presignedImageRepository.upload(bucket: .topic, request: image))
}
}
}
for try await (index, imageUrl) in group {
url[index] = imageUrl
}
}
return request.choices.enumerated()
.map{ i, choice in
.init(
choiceOption: choice.option.toDTO(),
choiceContentRequest: .init(
type: "IMAGE_TEXT",
imageUrl: url[i],
text: choice.text
)
)
}
}
아래 링크에 Concurrency에 대해 자세한 설명과 예시 코드가 적혀있다. 하나씩 차근차근 살펴보면 좋을 것 같다.
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency
비동기를 무작정 동시성이라고 생각하면 안된다는 것을 인지할 수 있었던 시간이었다.
동기와 비동기를 구분하고, concurrent와 serial을 구분하자.
https://green1229.tistory.com/336
재사용 가능한 ViewModel 구현하기 (0) | 2024.02.18 |
---|---|
텍스트 필드 디자인 시스템 구현하기 (0) | 2024.01.07 |
Timer 사용해보기 (0) | 2023.12.24 |
Repository의 DTO → Entity 변환 과정 개선하기 (1) | 2023.12.23 |
토스트 메시지 싱글톤으로 관리하기 (1) | 2023.12.19 |