10
27

Retrofit์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ž๋ฃŒ๊ฐ€ ๋งŽ์•„์„œ ์‹ ๊ฒฝ์•ˆ์ผ๋˜ ๋ถ€๋ถ„์ธ๋ฐ, Ktor๋ฅผ ์ด๋ฒˆ์— ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด์„œ ๊ฒช์€ ์ด์Šˆ๋“ค์„ ์ ์–ด๋ณด๊ฒ ๋‹ค.

๋จผ์ € ํด๋ฆฐ ์•„ํ‚คํ…์ณ์— ๋Œ€ํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์งš๊ณ  ๋„˜์–ด๊ฐ€๋ณด์ž.

# Clean Architecture

์›Œ๋‚™ ๊ฐœ์ธ๊ฐ„์˜ ์˜๊ฒฌ ์ฐจ์ด๊ฐ€ ์‹ฌํ•ด์„œ ๋…ผ๋ž€์ด ๋˜๋Š” ์ฃผ์ œ๊ธฐ๋„ ํ•˜์ง€๋งŒ, CleanArchitecture๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ ์–ป๋Š” ๊ฐ•์ ์€ SOLID๋ฅผ ์ง€ํ‚จ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์ด๊ฑธ ๋„์ž…ํ•ด์•ผ๋  ์ •๋„์˜ ๊ทœ๋ชจ๊ฐ€ ์•„๋‹Œ๋ฐ๋„ ๋„์ž…ํ•˜๋Š” ๊ฑด ์˜ค๋ฒ„์—”์ง€๋‹ˆ์–ด๋ง์ด๋ผ๊ณ ๋“ค ํ•˜์ง€๋งŒ, ์ผ๋‹จ ๋‚˜๋Š” ํ•™์Šต์šฉ์œผ๋กœ ๋„์ž…ํ–ˆ๋‹ค.

Domain์—๋Š” UseCase ์ธํ„ฐํŽ˜์ด์Šค์™€, ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•  Model(Entities)์ด ์กด์žฌํ•œ๋‹ค. 

์•„ํ‚คํ…์ณ๋ฅผ ์„ค๋ช…ํ•  ๋•Œ ๋ณดํ†ต domain, data, presenter ๋ ˆ์ด์–ด๋กœ ๋‚˜๋ˆ ์„œ ๋ณด๋Š”๋ฐ, CleanArchitecture์™€ ๊ตฌ๊ธ€์ด ๊ถŒ์žฅํ•˜๋Š” ์•ฑ ์•„ํ‚คํ…์ณ์˜ ์ฐจ์ด๋Š” domain์— ์žˆ๋‹ค.

์•„๋ž˜๊ฐ€ ๊ถŒ์žฅ ์•ฑ ์•„ํ‚คํ…์ณ,

flowchart LR
    B[Domain] --> C[Data]
    C --> D[Presentation]

๊ทธ๋ฆฌ๊ณ  ์ด๊ฒŒ ํด๋ฆฐ ์•„ํ‚คํ…์ณ๋‹ค

flowchart LR
    B[Data] --> C[Domain]
    D[Presentation]--> C

์ด์ „์—๋Š” ๋„๋ฉ”์ธ์ด data๋ฅผ ์•Œ๊ณ  ์žˆ๋Š”๋ฐ ๋ฐ˜ํ•ด, ํด๋ฆฐ์•„ํ‚คํ…์ณ์˜ ๊ฒฝ์šฐ ๋„๋ฉ”์ธ์€ ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค. ๊ทธ๋ž˜์„œ ๋ชจ๋“ˆ์„ ๋งŒ๋“ค๋•Œ๋„ ์•ˆ๋“œ๋กœ์ด๋“œ ์˜์กด์„ฑ๋„ ์ œ๊ฑฐํ•œ Kotlin, Java Library ๋ชจ๋“ˆ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค.

 

ํด๋ฆฐ์•„ํ‚คํ…์ณ๋ฅผ ์ ์šฉํ–ˆ์„ ๋•Œ MultiPart๊ฐ€ ์–ด๋ ค์›Œ์ง€๋Š” ์ ์€ ํ•˜๋‚˜๋‹ค.

File์„ parameter๋กœ ๋„˜๊ธฐ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, uri๋ฅผ ๋„˜๊ธฐ๋Š” ๊ฒƒ์ด๋ผ๋ฉด ์ธํ„ฐํŽ˜์ด์Šค์— Uri๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์„ ์–ธํ•ด์•ผ๋˜๋Š”๋ฐ ๋„๋ฉ”์ธ ๋ชจ๋“ˆ์€ android ์˜์กด์„ฑ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— Uri ํƒ€์ž…์„ ๊ฐ€์งˆ ์ˆ˜ ์—†๋‹ค. ์ด๊ฑธ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด์ œ Ktor๋กœ Multipart ์ ์šฉํ•˜๊ธฐ๋ฅผ ์‹œ์ž‘ํ•ด๋ณด๊ฒ ๋‹ค.


# Ktor์—์„œ ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ•˜๋Š” ๋ฐฉ๋ฒ•

Ktor์—์„œ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๋Š” ๋ฐฉ๋ฒ•์€ 2๊ฐ€์ง€๋‹ค. ๊ณตํ†ต์ ์€ formData๋กœ ๋‹น์—ฐํžˆ ๋ฌถ์–ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์ธ๋ฐ `submitFormWithBinaryData`์™€ post ์š”์ฒญ์„ `MultiPartFormDataContent`๋กœ ์„ค์ •ํ•˜๋Š” ๋ฐฉ์‹์˜ ์ฐจ์ด์ ์„  ํ•œ ๋ฒˆ ์‚ดํŽด๋ณด์ž. ์˜ˆ์ œ ์ฝ”๋“œ๋Š” ๊ณต์‹๋ฌธ์„œ์—์„œ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๋‹ค.

`submitFormWithBinaryData`

val client = HttpClient(CIO)

val response: HttpResponse = client.submitFormWithBinaryData(
    url = "http://localhost:8080/upload",
    formData = formData {
        append("description", "Ktor logo")
        append("image", File("ktor_logo.png").readBytes(), Headers.build {
            append(HttpHeaders.ContentType, "image/png")
            append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
        })
    }
)

์ด๋ฏธ์ง€๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฐ์ดํ„ฐ๋‹ค. ๊ทธ๋ž˜์„œ Ktor์—์„œ๋Š” ์ด๊ฑธ ๊ทธ๋ƒฅ ์‰ฝ๊ฒŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š”๋ฐ, ByteArray๋กœ ๋ณ€ํ™˜๋œ File์„ ํผ๋ฐ์ดํ„ฐ์— ๋„ฃ์–ด์ฃผ๊ณ , Mime Type๊ณผ ContentDisposition ํ—ค๋”๋ฅผ ์„ค์ •ํ•˜์—ฌ ํŒŒ์ผ๋กœ ์ธ์‹๋  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

๋‚ด๋ถ€๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ณด๋ฉด suspend ํ•จ์ˆ˜๋กœ ๋˜์–ด์žˆ๋‹ค.

inline suspend fun HttpClient.submitFormWithBinaryData(
    url: String, 
    formData: List<PartData>, 
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse

๊ธฐ๋ณธ์ ์œผ๋กœ post๋กœ ๋™์ž‘ํ•œ๋‹ค. ํผ ๋ฐ์ดํ„ฐ์˜ ๊ฒฝ๊ณ„๋ฅผ Ktor๊ฐ€ ์•Œ์•„์„œ ๊ด€๋ฆฌ ํ•ด์ฃผ๊ณ , ๋”ฐ๋กœ ์ง„ํ–‰์ƒํ™ฉ ๋ชจ๋‹ˆํ„ฐ๋ง์€ ์ง€์›ํ•˜์ง€์•Š๋Š”๋‹ค. ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๋Š” ์—ญํ• ์„ ํ•˜๊ธฐ๋•Œ๋ฌธ์—, ์ด๋ฏธ์ง€๊ฐ€ ์•„๋‹ˆ๋”๋ผ๋„ ์ผ๋‹จ ๋ฐ”์ด๋„ˆ๋ฆฌ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ํŒŒ์ผ์„ ์ „์†กํ•  ์ˆ˜ ์žˆ๋‹ค.

 

## Boundary? ๊ฒฝ๊ณ„?

๋ฐฉ๊ธˆ ์„ค๋ช…์—์„œ ํผ๋ฐ์ดํ„ฐ์˜ ๊ฒฝ๊ณ„๋ผ๋Š” ๊ฒŒ ์žˆ์—ˆ๋‹ค. ์ด๊ฒŒ ๋ญ”์ง€ ํ•œ ๋ฒˆ ์‚ดํŽด๋ณด์ž.

๋ฉ€ํ‹ฐํŒŒํŠธ ํผ ๋ฐ์ดํ„ฐ์—์„œ ๋ฐ”์šด๋”๋ฆฌ๋Š” ๊ฐ ํŒŒํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ์—ญํ• ์„ ํ•˜๋Š” ๊ณ ์œ ํ•œ ๋ฌธ์ž์—ด์ด๋‹ค. ๋ฉ€ํ‹ฐํŒŒํŠธ ํ˜•์‹์€ ํ•˜๋‚˜์˜ HTTP ์š”์ฒญ ๋ณธ๋ฌธ ์•ˆ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŒŒ์ผ์ด๋‚˜ ํผ ํ•„๋“œ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์œผ๋ฉฐ, ๊ฐ ํŒŒํŠธ๋ฅผ ์„œ๋กœ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ๊ฒฝ๊ณ„๊ฐ€ ํ•„์š”ํ•œ๋ฐ, ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

Content-Type: multipart/form-data; boundary=----WebAppBoundary

HTTP ์š”์ฒญ ๋ณธ๋ฌธ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŒŒ์ผ์ด๋‚˜ ํ…์ŠคํŠธ ํŒŒํŠธ๊ฐ€ ํฌํ•จ๋  ๋•Œ, ๋ฐ”์šด๋”๋ฆฌ ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ํŒŒํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ…์ŠคํŠธ ์„ค๋ช…๊ณผ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ๋™์‹œ์— ๋ณด๋‚ผ ๋•Œ ๊ฐ๊ฐ์˜ ํŒŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๊ฒฝ๊ณ„๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๋ ˆํŠธ๋กœํ•์—์„œ `@Part`๋กœ ๊ตฌ๋ถ„ํ•˜๋Š”๊ฒŒ ์ด๋Ÿฐ ์—ญํ• ์„ ํ•œ๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค. 

์—ฌ๋Ÿฌ๊ฐœ๋กœ ์ชผ๊ฐœ๊ฒŒ ๋˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์š”์ฒญ์ด ๊ฐ„๋‹ค.

------WebAppBoundary
Content-Disposition: form-data; name="description"

Ktor logo
------WebAppBoundary
Content-Disposition: form-data; name="image"; filename="ktor_logo.png"
Content-Type: image/png

๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฐ”์šด๋”๋ฆฌ๋Š” ์ž๋™์ƒ์„ฑ์ด๊ธด ํ•˜์ง€๋งŒ, `submitFormWithBinaryData`๋Š” ์ž๋™์„ค์ •๋งŒ ์ง€์›ํ•œ๋‹ค. ์ด์ œ ํ•œ๋ฒˆ MultiPartFormDataContent๋ฅผ ๋ด๋ณด์ž

`MultiPartFormDataContent`

val client = HttpClient(CIO)

val response: HttpResponse = client.post("http://localhost:8080/upload") {
    setBody(MultiPartFormDataContent(
        formData {
            append("description", "Ktor logo")
            append("image", File("ktor_logo.png").readBytes(), Headers.build {
                append(HttpHeaders.ContentType, "image/png")
                append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
            })
        },
        boundary = "WebAppBoundary"
    ))
    onUpload { bytesSentTotal, contentLength ->
        println("Sent $bytesSentTotal bytes from $contentLength")
    }
}

์ด๊ฑด post์š”์ฒญ์— Body๋กœ ๋„ฃ๋Š”, ์ด์ „์— ์•Œ๊ณ ์žˆ๋˜ ๋ฐฉ์‹์— ๊ฐ€๊น๋‹ค. post ์š”์ฒญ์— MultiPartFormDataContent๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ€ํ‹ฐํŒŒํŠธ ํผ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์„ค์ •ํ•˜๊ณ  setBody ํ•จ์ˆ˜๋กœ ์„œ๋ฒ„์— ์ „์†กํ•œ๋‹ค.

๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด ๋‹ค๋ฅธ ์ ์ด ๋”ฑ ๋ณด์ธ๋‹ค.

class MultiPartFormDataContent(
    parts: List<PartData>, 
    val boundary: String = generateBoundary(), 
    val contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
) : OutgoingContent.WriteChannelContent

boundary๋ฅผ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฌผ๋ก  ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๊ฐ€ ์žˆ์–ด์„œ ๋”ฐ๋กœ ๋งŒ๋“ค๊ฒŒ ์•„๋‹ˆ๋ฉด ๋†”๋‘ฌ๋„ ๋œ๋‹ค.

์ด์ œ ์ค‘์š”ํ•œ ์ฐจ์ด์ ์ด ๋“ฑ์žฅํ•œ๋‹ค. onUpload์ธ๋ฐ, MultiPartFormDataContent๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ํŒŒ์ผ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒญํฌ๋‹จ์œ„๋กœ ๋ถ„ํ• ํ•˜์—ฌ ์ „์†กํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•˜๊ณ , ์ด ๊ณผ์ •์—์„œ ๊ฐ ์ฒญํฌ๊ฐ€ ์ „์†ก๋  ๋•Œ๋งˆ๋‹ค onUpload ์ฝœ๋ฐฑ์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค. submitFormWithBinaryData๋Š” ์ฒญํฌ๋‹จ์œ„๋กœ ์ „์†กํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— post + MultiPartFormDataContent์ผ๋•Œ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค. 

 

์ข€ ๋” ์ž์„ธํžˆ ๋ณด๋ฉด MultiPartFormDataContent๋Š” OutgoingContent.WriteChannelContent๋ฅผ ์ƒ์†ํ•˜๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ด ์ƒ์†์„ ํ†ตํ•ด ๋น„๋™๊ธฐ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๊บผ๋ฒˆ์— ๋ฉ”๋ชจ๋ฆฌ์— ๋กœ๋“œํ•˜์ง€ ์•Š๊ณ , ์ฒญํฌ ๋‹จ์œ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜์—ฌ ์„œ๋ฒ„๋กœ ์ „์†กํ•œ๋‹ค.

 

onUpload๋งŒ ๋˜‘ ๋–ผ์„œ ๋ณด์ž.

onUpload { bytesSentTotal, contentLength ->
    println("Sent $bytesSentTotal bytes from $contentLength")
}

bytesSentTotal(์ง€๊ธˆ๊นŒ์ง€ ์ „์†ก๋œ ํฌ๊ธฐ), contentLength(์ „์†กํ•  ํŒŒ์ผ ์ „์ฒด ํฌ๊ธฐ)๋กœ ์ง„ํ–‰์ƒํ™ฉ์„ ๋ณด๊ณ ์žˆ์„ ์ˆ˜ ์žˆ์–ด์„œ, UI์— ์ง„ํ–‰์ƒํ™ฉ์„ ๋„์šธ ์ˆ˜ ๋„ ์žˆ์„ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

suspend fun uploadImage(
    image: Image,
    file: ByteArray,
    onProgress: (bytesSentTotal: Long, contentLength: Long) -> Unit
): CommonResponse<FileResponse> {
    return client.post("your_url") {
        setBody(MultiPartFormDataContent(
            formData {
                append("image", file, Headers.build {
                    append(HttpHeaders.ContentType, ContentType.Image.PNG)
                })
            }
        ))

        onUpload { bytesSentTotal, contentLength ->
            onProgress(bytesSentTotal, contentLength)
        }
    }.body()
}

์ด๋ ‡๊ฒŒ ์“ฐ๋ฉด onProgress๋กœ ์ƒํƒœ๊ฐ€ ๊ณ„์† ์œ„์ชฝ์œผ๋กœ ์ด๋™ํ•ด์„œ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

์ด์ œ ๋‘ ๋ฐฉ์‹์„ ๋ชจ๋‘ ์‚ดํŽด๋ดค์œผ๋‹ˆ, ์‹ค์ œ๋กœ ํ•œ๋ฒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๋ณด๊ฒ ๋‹ค.


Ktor๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ํ•ด๋ณด์ž

presenteter์—์„œ ๋‹จ์ˆœํžˆ uri๋งŒ ๋˜์ง€๊ณ  ๋‚˜๋จธ์ง€๋ฅผ ๋‹ค data ๋ ˆ์ด์–ด์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ๋กœ ์ง„ํ–‰ํ•œ๋‹ค. uri๊ฐ€ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์„ ์–ธ๋  ์ˆ˜ ์—†์œผ๋‹ˆ๊นŒ `setProfileImageUseCase(contentUri = contentUri.toString())` ์ด๋ ‡๊ฒŒ String์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ๋„˜๊ธด๋‹ค.

3๋‹จ๊ณ„๋กœ ๋™์ž‘ํ•œ๋‹ค.

  1. ๊ฐ€์ ธ์˜จ contentUri๋กœ ์‹ค๊ธฐ๊ธฐ ์•ˆ์— ์žˆ๋Š” ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค์Œ
  2. ์ด๊ฑธ InputStream์œผ๋กœ ๋ฐ”๊ฟ” ByteArray๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณ์„œ
  3. client์— formData๋กœ ๋„˜๊ธด๋‹ค.

๋จผ์ € ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด contentResolver๋ฅผ ์‚ฌ์šฉํ•ด์•ผ๋œ๋‹ค. 

val uri = Uri.parse(contentUri)
val projection = arrayOf(
    MediaStore.Images.Media._ID,
    MediaStore.Images.Media.DISPLAY_NAME,
    MediaStore.Images.Media.SIZE,
    MediaStore.Images.Media.MIME_TYPE
)
val cursor = context.contentResolver.query(
    uri,
    projection,
    null,
    null,
    null
)

return cursor?.use { c->
    c.moveToNext() // ์ปค์„œ๋ฅผ ํ•œ์นธ ๋‚ด๋ ค์„œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
    val idIdx = c.getColumnIndexOrThrow(projection[0])
    val nameIdx = c.getColumnIndexOrThrow(projection[1])
    val sizeIdx = c.getColumnIndexOrThrow(projection[2])
    val typeIdx =  c.getColumnIndexOrThrow(projection[3])
    // idx๋ฅผ ์•Œ์•„์•ผ ์ปค์„œ๊ฐ€ ์ปฌ๋Ÿผ์— ๋Œ€ํ•œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค
    val id = c.getLong(idIdx)
    val name = c.getString(nameIdx)
    val size = c.getLong(sizeIdx)
    val type = c.getString(typeIdx)
    Image(contentUri, name, size, type)
}

์ด ๊ณผ์ •์„ ๊ฑฐ์น˜๋ฉด cursor๋ฅผ ํ†ตํ•ด ํŒŒ์ผ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋ชจ๋‘ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค. ํŒŒ์ผ ๊ธธ์ด, ํŒŒ์ผ ์ด๋ฆ„, mimetype๊นŒ์ง€ ๋ชจ๋‘ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ๋งค๋ฒˆ mimeType์„ ์ˆ˜๋™์„ ์ง€์ •ํ•ด์ค˜์•ผํ•˜๋Š” ๋ถˆํŽธํ•จ์„ ๋œ ์ˆ˜ ์žˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๊ฐ€์ ธ์˜จ ์ด๋ฏธ์ง€ ์ •๋ณด๋“ค์€ multipart ์š”์ฒญ ๋•Œ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๊ณ , contentUri๋กœ ๊ฐ€์ ธ์˜จ Uri๋ฅผ InputStream์œผ๋กœ ByteArray ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์€ ๋˜ ๋”ฐ๋กœ ํ•ด์ค€๋‹ค.

val uri = Uri.parse(contentUri)
context.contentResolver.openInputStream(uri) ?: throw IllegalStateException("๋น„์–ด์žˆ์Œ")

์ฝ˜ํ…ํŠธ ๋ฆฌ์กธ๋ฒ„๋กœ inputStream์„ ์—ด์–ด์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๊ทผ๋ฐ ์ด๊ฑด ๋ฆฌ์†Œ์Šค๋ฅผ ์—ด๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ๊ผญ use๋ฅผ ์จ์„œ memory leak์•ˆ๋‚˜๊ฒŒ auto close ํ•ด์ค˜์•ผํ•œ๋‹ค.

val byteArray = getInputStreamUseCase(image.uri).getOrElse {
    Log.e("UploadImageUseCase", "Failed to get InputStream: ${it.message}")
    throw it
}.use { inputStream ->
    inputStream.readBytes()
}

// readBytes ๋‚ด๋ถ€
public fun InputStream.readBytes(): ByteArray {
    val buffer = ByteArrayOutputStream(maxOf(DEFAULT_BUFFER_SIZE, this.available()))
    copyTo(buffer)
    return buffer.toByteArray()
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด๊ฐ€์ ธ์˜จ inputStream์„ ByteArray๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค.

 

์ด์ œ ktor ํด๋ผ์ด์–ธํŠธ ํ˜ธ์ถœ๋ถ€๋ฅผ ๋ณด์ž.

@Serializable // Data ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ง๋ ฌํ™”๋ฅผ ํ•ด์คฌ์Œ
data class Image(
    val uri: String,
    val name: String,
    val size: Long,
    val mimeType: String
)

suspend fun uploadImage(imageFile: Image, file: ByteArray): CommonResponse<FileResponse> {
    return client.post("files") {
        setBody(
            MultiPartFormDataContent(
                formData {
                    append("fileName", imageFile.name)
                    append("file", file, Headers.build {
                        append(HttpHeaders.ContentType, imageFile.mimeType)
                        append(HttpHeaders.ContentDisposition, "filename=\"${imageFile.name}\"")
                    })
                },
            )
        )
        onUpload { bytesSentTotal, contentLength ->
            println("Sent $bytesSentTotal bytes from $contentLength")
        }
    }.body()
}

์ด ์ฝ”๋“œ์ž์ฒด๋Š” ์˜ˆ์‹œ์ฝ”๋“œ์™€ ์ •๋ง ๋‹ค๋ฅผ ๊ฒŒ ์—†๋‹ค. mimeType๊ณผ contentDisposition์„ contentResolver๋กœ ๊ฐ€์ ธ์˜จ๊ฑธ ์ œ์™ธํ•˜๋ฉด ๋˜‘๊ฐ™๋‹ค.

onUpload๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ธฐ ์œ„ํ•ด์„œ ๋‚˜๋Š” MultiPartFormDataContent๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ๋กœ๊ทธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ์ฐํžŒ๋‹ค.

Sent 4096 bytes from 233983
Sent 8192 bytes from 233983
Sent 12288 bytes from 233983
Sent 16384 bytes from 233983
Sent 20480 bytes from 233983
Sent 24576 bytes from 233983
Sent 28672 bytes from 233983
Sent 32768 bytes from 233983
Sent 36864 bytes from 233983
Sent 40960 bytes from 233983
Sent 45056 bytes from 233983
Sent 49152 bytes from 233983
Sent 53248 bytes from 233983
... ์ƒ๋žตํ•˜๊ฒ ๋‹ค

4096Bytes ๋‹จ์œ„ ์ฒญํฌ๋กœ ์ „์†ก๋˜๊ณ ์žˆ๋Š” ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค!

 

๋„์›€์ด ๋๋‹ค๋ฉด ๋Œ“๊ธ€์ด๋‚˜ ๊ณต๊ฐ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์”ฉ ๋ˆ„๋ฅด๊ณ  ๊ฐ€์ฃผ์„ธ์š”!

 

๋ฐ˜์‘ํ˜•
COMMENT