sgcWebSockets의 AMQP 1.0 프로토콜 구현이 OASIS AMQP 1.0 사양에 대비해 종합적인 검토를 거쳤어요. 이 글은 8개의 소스 파일에 걸쳐 적용된 30가지 수정 사항을 다루며, 치명적인 버그, 메모리 누수, 사양 준수, 상태 기계 정확성, 하트비트 처리, 스레드 안전성 개선을 포함해요.
목차
1. 개요
AMQP 1.0 구현에서 6개의 소스 파일과 2개의 인터페이스 파일에 걸쳐 총 30개의 수정이 적용되었어요. 수정 사항은 다음과 같이 분류돼요:
| 분류 | 수량 | 심각도 |
|---|---|---|
| 치명적 버그 수정 | 6 | 치명적 |
| 메모리 누수 수정 | 4 | 치명적 |
| 사양 준수 | 10 | 높음 |
| 상태 기계 & 연결 | 5 | 높음 |
| 하트비트 & 유휴 타임아웃 | 3 | 높음 |
| 스레드 안전성 | 2 | 높음 |
2. 치명적 버그 수정
이 수정은 AMQP 1.0 통신 중 즉각적인 런타임 오류, 프로토콜 손상, 또는 잘못된 동작을 유발하는 버그를 해결해요.
2.1 예외에서 raise 키워드 누락
파일: sgcAMQP1_Classes.pas
잘못된 프레임 타입이 발생했을 때 예외 객체가 생성되었지만 발생(raise)되지 않았어요. 예외가 자동으로 무시되어 손상된 프레임이 감지 없이 처리되었어요.
수정 전 (오류)
TsgcAMQP1Exception.CreateFmt(S_AMQP1_INVALID_FRAME_TYPE, [vByte]);
수정 후 (올바름)
raise TsgcAMQP1Exception.CreateFmt(S_AMQP1_INVALID_FRAME_TYPE, [vByte]);
2.2 WriteMap Map32 데이터 누락 및 잘못된 크기
파일: sgcAMQP1_Classes.pas
Map32 인코딩 경로에서 실제 맵 데이터에 대한 WriteBytes 호출이 누락되었고, 크기 필드에 잘못된 오프셋이 사용되었어요. Map32는 4바이트 카운트 필드를 사용하므로(1바이트를 사용하는 Map8과 달리) 크기에 4바이트가 추가로 포함되어야 해요.
수정 전 (오류)
else
begin
WriteUByte(Ord(amqp1DataMap32));
_WriteUInt32(vSize + 1);
_WriteUInt32(oJSON.Count * 2);
// Missing: WriteBytes(oArray.Bytes);
end;
수정 후 (올바름)
else
begin
WriteUByte(Ord(amqp1DataMap32));
_WriteUInt32(vSize + 4);
_WriteUInt32(oJSON.Count * 2);
WriteBytes(oArray.Bytes);
end;
2.3 ContainsError 로직 반전
파일: sgcAMQP1_Frames.pas
TsgcAMQP1FrameRejected와 TsgcAMQP1DescribedListError의 ContainsError 메서드가 오류가 없을 때 True를, 오류가 있을 때 False를 반환했어요. 이로 인해 오류 정보가 자동으로 삭제되고, 실제 오류가 직렬화되어야 할 때 null 바이트가 작성되었어요. DoWrite와 DoWriteError 분기도 수정된 로직에 맞게 교체되었어요.
수정 전 (오류)
function TsgcAMQP1FrameRejected.ContainsError: Boolean;
begin
if not Assigned(FError) then
Result := True // Wrong: True when no error
else
Result := (Error.Condition = '') and (Error.Description = '') and
(Error.Info = ''); // Wrong: True when all empty
end;
수정 후 (올바름)
function TsgcAMQP1FrameRejected.ContainsError: Boolean;
begin
if not Assigned(FError) then
Result := False // Correct: False when no error
else
Result := (Error.Condition <> '') or (Error.Description <> '') or
(Error.Info <> ''); // Correct: True when any field set
end;
2.4 SASL PLAIN Null 바이트 구분자
파일: sgcAMQP1_Frames.pas
SASL PLAIN 메커니즘은 null 바이트($00) 구분자와 함께 \0username\0password 형식을 요구해요. 구현이 바이트 배열을 0으로 초기화하지 않아 구분자 위치에 쓰레기 데이터가 포함되었어요. 모든 표준 준수 AMQP 1.0 브로커에서 인증이 실패했어요.
수정 후 (올바름)
SetLength(FInitialResponse, 2 + Length(oUser) + Length(oPassword));
FillChar(FInitialResponse[0], Length(FInitialResponse), 0); // Zero-fill for null separators
2.5 TsgcAMQP1Message에서 inherited Create 누락
파일: sgcAMQP1_Message.pas
TsgcAMQP1Message의 매개변수화된 생성자가 inherited Create를 호출하지 않아 기본 클래스가 초기화되지 않았어요. 편의 생성자를 사용할 때 접근 위반이나 손상된 상태가 발생했어요.
수정 후 (올바름)
constructor TsgcAMQP1Message.Create(const aValue: string);
begin
inherited Create;
ApplicationData.ValueType := amqp1adtAmqpValue;
ApplicationData.AMQPValue.Value := aValue;
end;
2.6 AmqpValue.DoRead에서 세미콜론 누락
파일: sgcAMQP1_Frames.pas
TsgcAMQP1FrameAmqpValue.DoRead에서 세미콜론 누락으로 컴파일이 불가능했어요.
3. 메모리 누수 수정
이 수정은 시간이 지남에 따라 메모리가 누적되는 객체 수명 관리 문제를 해결해요. 특히 많은 메시지 교환이 있는 장기 실행 AMQP 1.0 연결에서 문제가 발생했어요.
| 수정 | 파일 | 설명 |
|---|---|---|
| 3.1 | sgcAMQP1_Frames.pas |
Source 디스크립터를 읽을 때 재할당 전에 FDefaultOutcome이 해제되지 않았어요. 새 기본 결과가 수신될 때마다 이전 객체가 누수되었어요. |
| 3.2 | sgcAMQP1_Session.pas |
소멸자에서 sgcFree(FCreditConsumed) 중복 호출로 잠재적 이중 해제가 발생했어요. 중복 줄을 제거했어요. |
| 3.3 | sgcAMQP1_Session.pas |
세션 소멸자에서 FOutgoingDeliveries가 누락되었어요. 세션이 소멸될 때 배달 추적 목록이 해제되지 않았어요. |
| 3.4 | sgcAMQP1_Message.pas |
FFreeMessageOnDestroy가 활성화되었을 때 SetMessage와 SetMessageAndFreeOnDestroy가 이전 메시지를 해제하지 않았어요. 반복적인 메시지 할당으로 메모리가 누수되었어요. |
수정 3.1 – 재할당 전 FDefaultOutcome 해제
sgcFree(FDefaultOutcome); // Free previous instance before reassignment
if oDescriptor.Code = amqp1dcptReleased then
FDefaultOutcome := TsgcAMQP1FrameReleased.Create
else if oDescriptor.Code = amqp1dcptAccepted then
FDefaultOutcome := TsgcAMQP1FrameAccepted.Create
else if oDescriptor.Code = amqp1dcptRejected then
FDefaultOutcome := TsgcAMQP1FrameRejected.Create
수정 3.4 – SetMessage가 이전 메시지 해제
procedure TsgcAMQP1Delivery.SetMessage(const aMessage: TsgcAMQP1Message);
begin
if FFreeMessageOnDestroy and Assigned(F_Message) and (F_Message <> aMessage) then
sgcFree(F_Message);
FFreeMessageOnDestroy := False;
F_Message := aMessage;
end;
4. 사양 준수 수정
이 수정은 AMQP 1.0 Transport, Types, Messaging 사양에서 벗어난 부분을 수정해요.
4.1 Begin 프레임 필드 7이 잘못된 속성에 읽힘
파일: sgcAMQP1_Frames.pas
begin performative의 필드 인덱스 7이 Properties 대신 DesiredCapabilities에 읽히고 있었어요. 사양에 따르면 begin 필드는 remote-channel(0), next-outgoing-id(1), incoming-window(2), outgoing-window(3), handle-max(4), offered-capabilities(5), desired-capabilities(6), properties(7)이에요.
4.2 DoWrite에서 Source 및 Target 필드 누락
파일: sgcAMQP1_Frames.pas
Source 디스크립터의 DoWrite 메서드가 default-outcome, outcomes, capabilities 필드를 직렬화하지 않았어요. Target 디스크립터에 capabilities가 누락되었어요. 이로 인해 브로커가 협상된 값 대신 기본값을 사용하여 잘못된 배달 상태 처리로 이어질 수 있었어요.
4.3 AmqpSequence가 잘못된 속성에 읽힘
파일: sgcAMQP1_Message.pas
메시지 본문을 읽을 때 amqp-sequence 데이터가 ApplicationData.AMQPSequence 대신 ApplicationData.AMQPValue에 읽혔어요. 이로 인해 amqp-sequence 인코딩을 사용하는 모든 메시지의 본문이 손상되었어요.
4.4 TransactionalState Outcome이 작성되지 않음
파일: sgcAMQP1_Frames.pas
transactional-state 배달 상태가 트랜잭션 내에서 배달을 처리할 때 필요한 outcome 필드를 직렬화하지 않았어요.
4.5 Disposition Last 필드가 0과 미설정을 구분할 수 없음
Files: sgcAMQP1_Frames.pas,
sgcAMQP1_Frames.intf,
sgcAMQP1_Session.pas
disposition performative에는 선택적인 last 필드(delivery-id)가 있어요. Cardinal이므로 값 0은 유효하며 “미설정”의 sentinel로 사용할 수 없어요. 필드가 명시적으로 설정되었는지 올바르게 추적하기 위해 새 FLastAssigned boolean 플래그와 SetLast setter가 추가되었어요.
procedure TsgcAMQP1FrameDisposition.SetLast(const Value: Cardinal);
begin
FLast := Value;
FLastAssigned := True;
end;
4.6 AmqpSequence에 Value 속성과 Read/Write 누락
Files: sgcAMQP1_Frames.pas,
sgcAMQP1_Frames.intf
TsgcAMQP1FrameAmqpSequence 클래스에 Value 속성이 없고 DoRead/DoWrite 메서드가 비어 있었어요. amqp-sequence 본문 타입이 완전히 작동하지 않았어요.
4.7 Error Info 필드가 Map 대신 String으로 읽힘
파일: sgcAMQP1_Frames.pas
AMQP 1.0 사양은 error 타입의 info 필드를 map으로 정의해요. ReadMap 대신 ReadString으로 읽혀서 브로커가 구조화된 오류 정보를 보낼 때 파싱 오류가 발생했어요.
4.8 Capabilities와 Locales가 Symbol 대신 String으로 작성됨
파일: sgcAMQP1_Frames.pas
AMQP 1.0 사양은 offered-capabilities, desired-capabilities, outgoing-locales, incoming-locales를 symbol 배열로 정의해요. open, begin, attach performative에서 WriteSymbol 대신 WriteString으로 작성되었어요. 표준 준수 브로커는 이 프레임을 잘못된 필드 타입으로 거부했어요.
4.9 DefaultOutcome 핸들러에서 Accepted 디스크립터 누락
파일: sgcAMQP1_Frames.pas
Source 디스크립터의 default-outcome 리더가 released와 rejected만 처리했어요. 가장 일반적인 결과인 accepted가 처리되지 않았어요. 브로커가 accepted를 기본 결과로 보내면 자동으로 무시되었어요.
5. 상태 기계 & 연결 수정
이 수정은 AMQP 1.0 연결 상태 기계와 프레임 처리 로직을 해결해요.
| 수정 | 파일 | 설명 |
|---|---|---|
| 5.1 | sgcAMQP1.pas |
amqp1csOpenReceived 상태 전이에서 다른 모든 상태에 있는 else DoRaiseInvalidState가 누락되었어요. 유효하지 않은 전이가 오류 발생 대신 자동으로 무시되었어요. |
| 5.2 | sgcAMQP1.pas |
프레임 크기 유효성 검사 오류 메시지에 로컬 제한을 확인하는 것이었으므로 LocalMaxFrameSize를 표시해야 했으나 RemoteMaxFrameSize가 표시되었어요. |
| 5.3 | sgcAMQP1.pas |
FLastTimeRead가 Now 대신 0(Delphi epoch: 1899-12-30)으로 초기화되어 시작 시 즉시 잘못된 유휴 타임아웃이 감지되었어요. |
| 5.4 | sgcAMQP1.pas |
Read의 TBytes 오버로드가 TMemoryStream 오버로드와 달리 FLastTimeRead := Now를 업데이트하지 않아 일관성 없는 하트비트 추적이 발생했어요. |
| 5.5 | sgcAMQP1.pas |
헤더 수신 상태 전이가 항상 트리거되어야 하는데 조건부였어요. AMQP 1.0 사양에 따라 상태 기계는 유효한 모든 헤더 교환에서 전이해야 해요. |
수정 5.1 – OpenReceived 누락된 오류 분기
amqp1csOpenReceived:
begin
if aState = amqp1csOpenSent then
FConnectionState := amqp1csOpened
else
DoRaiseInvalidState; // Added: was missing
end;
6. 하트비트 & 유휴 타임아웃 수정
AMQP 1.0 사양에 따르면 피어가 open performative에서 idle-timeout을 알릴 때 다른 피어는 알려진 간격의 절반으로 하트비트 프레임을 보내야 해요. 이 수정으로 하트비트 메커니즘이 실제로 작동해요.
| 수정 | 파일 | 설명 |
|---|---|---|
| 6.1 | sgcAMQP1_Client.pas |
HeartBeat가 활성화되지 않았어요. 유휴 타임아웃 확인의 두 분기 모두 HeartBeat.Enabled := False를 설정했어요. IdleTimeout > 0일 때 True로 변경했어요. |
| 6.2 | sgcAMQP1_Client.pas |
Disconnect가 하트비트를 비활성화하거나 FConnected := False를 충분히 일찍 설정하지 않았어요. 종료 중 하트비트 실행을 방지하기 위해 순서를 변경했어요. |
| 6.3 | sgcAMQP1.pas |
TBytes Read 오버로드에서 FLastTimeRead가 업데이트되지 않았어요(상태 기계 섹션에도 나열됨). |
수정 6.1 – HeartBeat 활성화
if oOpen.IdleTimeout > 0 then
begin
HeartBeat.Interval := Trunc(oOpen.IdleTimeout / 2);
HeartBeat.Enabled := True; // Was: False (heartbeat never started)
end
else
HeartBeat.Enabled := False;
수정 6.2 – Disconnect 순서 변경
procedure TsgcAMQP1_Client.Disconnect;
begin
FConnected := False; // Moved first: prevents heartbeat race
DoStopIdleTimeout;
HeartBeat.Enabled := False; // Added: stop heartbeat during teardown
Clear;
DoConnectionState(amqp1csEnd);
end;
7. 스레드 안전성 수정
이 수정은 공유 데이터 구조에 대한 동시 접근에서 경쟁 조건을 해결해요.
7.1 TsgcAMQP1Deliveries.First() 범위 확인 및 잠금
파일: sgcAMQP1_Message.pas
First() 메서드가 목록이 비어 있는지 확인하지 않고 스레드 안전 잠금을 획득하지 않은 채 Items[0]에 접근했어요. 멀티스레드 환경에서 다른 스레드가 카운트 확인과 접근 사이에 모든 항목을 제거하여 인덱스 범위 초과 예외가 발생할 수 있었어요.
수정 후 (올바름)
function TsgcAMQP1Deliveries.First: TsgcAMQP1Delivery;
var
oList: TList;
begin
result := nil;
oList := LockList;
Try
if oList.Count > 0 then
result := TsgcAMQP1Delivery(oList[0]);
Finally
UnLockList;
End;
end;
7.2 SetMessage 안전한 객체 교체
파일: sgcAMQP1_Message.pas
SetMessage 메서드가 이제 해제 전에 새 메시지가 현재 메시지와 다른지 확인하여, 동일한 메시지 객체를 할당할 때 해제 후 사용을 방지해요.
8. 수정된 파일
| 파일 | 수정 수 | 분류 |
|---|---|---|
Source\sgcAMQP1_Classes.pas |
2 | 치명적 버그 |
Source\sgcAMQP1_Frames.pas |
16 | 치명적 버그, 메모리 누수, 사양 준수 |
Interfaces\sgcAMQP1_Frames.intf |
2 | 사양 준수 (Disposition LastAssigned, AmqpSequence Value) |
Source\sgcAMQP1_Message.pas |
4 | 치명적 버그, 메모리 누수, 스레드 안전성 |
Source\sgcAMQP1_Session.pas |
3 | 메모리 누수, 사양 준수 |
Source\sgcAMQP1.pas |
5 | 상태 기계, 연결, 하트비트 |
Source\sgcAMQP1_Client.pas |
3 | 하트비트, 연결 해제 안전성 |
총 30개의 수정이 8개의 파일에 걸쳐 적용되어 OASIS 사양에 대한 AMQP 1.0 구현의 프로토콜 정확성, 메모리 안전성, 신뢰성이 향상되었어요.
