VBA에서 문자열은 & 로 쉽게 이어 붙이는데, Byte 배열은 그런 연산자가 없습니다. 그런데 바이너리를 다루다 보면 배열을 합칠 일이 생각보다 자주 생깁니다. 대표적인 게 WinHttp로 파일을 업로드할 때입니다. multipart/form-data 본문은 "경계 문자열 + 헤더(텍스트) + 파일 내용(바이너리) + 끝 경계" 순서로 붙여야 하는데, 파일 내용을 문자열로 바꿨다 붙이면 바이트가 깨집니다. 그래서 각 조각을 Byte 배열로 만든 뒤 바이트 그대로 합치는 함수를 만들어 씁니다.
코드
32비트 Office에서만 쓰던 옛 선언은 주석으로 남겨 두고, 64비트에서도 동작하도록 PtrSafe 를 붙인 선언을 씁니다. 이 선언은 모듈 맨 위에 있어야 합니다.
'// Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDest As Any, pSource As Any, ByVal cbLength As Long)
Public Function JoinBytesArray(ParamArray ByteArrays() As Variant) As Byte()
Dim sizeArray As Variant, startIndex As Long, i As Integer
Dim Buffer() As Byte, ByteArray() As Byte
ReDim sizeArray(0 To UBound(ByteArrays))
For i = 0 To UBound(ByteArrays)
sizeArray(i) = UBound(ByteArrays(i))
Next
startIndex = 0
ReDim Buffer(WorksheetFunction.Sum(sizeArray) + UBound(sizeArray))
For i = 0 To UBound(ByteArrays)
ByteArray = ByteArrays(i)
CopyMemory Buffer(startIndex), ByteArray(0), UBound(ByteArray) + 1
startIndex = startIndex + UBound(ByteArray) + 1
Next
JoinBytesArray = Buffer
End Function
Private Function Sum(ByVal IntegerArray As Variant) As Long
Dim i As Integer
For i = 0 To UBound(IntegerArray)
Sum = Sum + Val(IntegerArray(i))
Next
'// Range.Application.WorksheetFunction.Sum
End Function
크기 계산과 복사
ParamArray 라서 배열을 몇 개든 쉼표로 이어 넘길 수 있습니다. 먼저 각 배열의 UBound 를 모아 둡니다. 배열이 0부터 시작하면 길이는 UBound + 1 이므로, 전체 길이는 "UBound 합계 + 배열 개수"가 되고, 결과 버퍼의 마지막 인덱스는 거기서 1을 뺀 "UBound 합계 + (배열 개수 - 1)" 입니다. 코드의 Sum(sizeArray) + UBound(sizeArray) 가 바로 그 값입니다.
복사는 For 로 한 바이트씩 옮기지 않고 RtlMoveMemory 로 배열 단위로 통째로 옮깁니다. Buffer(startIndex) 와 ByteArray(0) 을 As Any 인자에 넘기면 각 원소의 메모리 주소가 전달되기 때문에, 원본의 첫 바이트부터 길이만큼을 버퍼의 해당 위치에 그대로 복사합니다. 수 MB짜리 파일을 붙일 때 반복문과 속도 차이가 확실히 납니다.
쓰는 예
Dim head() As Byte, body() As Byte, tail() As Byte, all() As Byte
head = StrConv("--BOUNDARY" & vbCrLf, vbFromUnicode)
body = StrConv("hello", vbFromUnicode)
tail = StrConv(vbCrLf & "--BOUNDARY--", vbFromUnicode)
all = JoinBytesArray(head, body, tail)
실제 업로드라면 body 자리에 파일을 바이너리로 읽은 배열이 들어가고, 완성된 all 을 WinHttpRequest.Send 에 그대로 넘기면 됩니다.
주의할 점
함수 안에서 합계를 WorksheetFunction.Sum 으로 구하기 때문에 엑셀에서만 동작합니다. 아래에 같이 넣어 둔 Sum 함수가 그 대안인데, 지금 코드에서는 호출되지 않습니다. Access나 VB6에서 쓸 거라면 WorksheetFunction.Sum(sizeArray) 를 Sum(sizeArray) 로 바꾸면 됩니다.
넘기는 배열은 모두 0부터 시작해야 하고, 비어 있으면 안 됩니다. 길이 0인 배열(UBound 가 -1)이 섞이면 ByteArray(0) 에서 첨자 오류가 납니다. 그리고 CopyMemory 는 길이를 잘못 주면 엑셀이 그대로 꺼질 수 있는 API라, 크기 계산 부분은 손대지 않는 게 좋습니다. 수정했다면 저장하고 나서 시험해 보세요.
PtrSafe 는 VBA7(Office 2010 이후)에서만 알아듣는 키워드라서, VB6 나 Office 2007 이하에서는 이 선언 줄 자체가 컴파일 오류입니다. 그쪽에서는 주석으로 남겨 둔 옛 선언을 살리고 PtrSafe 선언을 지우면 됩니다. 두 환경 모두에서 열리는 파일이라면 #If VBA7 Then ... #Else ... #End If 로 감싸 두세요.
API 없이 합치는 방법은?
CopyMemory 가 부담스럽다면 ADODB.Stream 을 바이너리 모드로 열어 배열을 차례로 써 넣고 한 번에 읽어 오는 방법이 있습니다. 메모리를 직접 건드리지 않으니 크기를 잘못 계산해서 엑셀이 꺼질 일이 없고, WorksheetFunction 도 쓰지 않아 Access 에서도 그대로 동작합니다.
Function JoinBytesStream(ParamArray arr() As Variant) As Byte()
Dim i As Long
With CreateObject("ADODB.Stream")
.Type = 1
.Open
For i = 0 To UBound(arr)
.Write arr(i)
Next
.Position = 0
JoinBytesStream = .Read
.Close
End With
End Function
.Type = 1 이 바이너리 모드입니다. 다 쓴 뒤 .Position = 0 으로 처음으로 돌아가야 .Read 가 전체 내용을 돌려줍니다. 이걸 빼먹으면 끝에서부터 읽으려 해서 빈 결과가 나옵니다. 속도는 CopyMemory 쪽이 더 빠를 수 있지만, 업로드 본문 몇 개를 붙이는 정도라면 차이를 느끼기 어렵습니다.
한글이 들어간 헤더는 어떻게 만드나?
예제의 StrConv(..., vbFromUnicode) 는 시스템 코드페이지(한국어 Windows 는 CP949)로 바이트를 만듭니다. 영문 경계 문자열은 상관없지만, multipart 헤더의 filename="보고서.xlsx" 처럼 한글이 들어가면 UTF-8 을 기대하는 서버에서는 파일 이름이 깨집니다. 그 경우엔 헤더 문자열을 ADODB.Stream 에 Charset = "utf-8" 텍스트로 쓴 다음 바이너리로 바꿔 읽어서 배열을 만들어야 하고, 이때 맨 앞에 붙는 BOM 3바이트는 잘라 내야 합니다.