Skip to content

Commit ace9e1a

Browse files
committed
feat: ChatSegment 소유권 이전 검증 및 IDisposable 패턴 강화
- IDisposable 인터페이스 구현으로 메모리 해제 시맨틱 강제 - WithAudioMemory 메서드에 소유권 이전 시 크기/수명 검증 추가 - ArgumentNullException: audioMemoryOwner null 체크 - ArgumentOutOfRangeException: audioDataSize 범위 검증 (0 ≤ size ≤ memory.Length) - 완전한 Dispose 패턴 구현 (Dispose(bool), Finalizer, GC.SuppressFinalize) - 소유권 이전 문서화: XML 주석으로 메모리 소유권 이전 명시 - 검증 로직 테스트 케이스 추가 및 기존 테스트 호환성 개선 메모리 누수 방지와 안전한 리소스 관리를 통한 시스템 안정성 강화
1 parent e032d5e commit ace9e1a

File tree

2 files changed

+111
-16
lines changed

2 files changed

+111
-16
lines changed

ProjectVG.Application/Models/Chat/ChatSegment.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace ProjectVG.Application.Models.Chat
55
{
6-
public record ChatSegment
6+
public record ChatSegment : IDisposable
77
{
88

99
public string Content { get; init; } = string.Empty;
@@ -84,9 +84,26 @@ public ChatSegment WithAudioData(byte[] audioData, string audioContentType, floa
8484

8585
/// <summary>
8686
/// 메모리 효율적인 방식으로 음성 데이터를 추가합니다 (LOH 방지)
87+
/// 소유권이 이전되므로 호출자는 더 이상 audioMemoryOwner를 해제하지 않아야 합니다.
8788
/// </summary>
89+
/// <param name="audioMemoryOwner">소유권이 이전될 메모리 소유자</param>
90+
/// <param name="audioDataSize">실제 오디오 데이터 크기 (메모리 크기 이하여야 함)</param>
91+
/// <param name="audioContentType">오디오 컨텐츠 타입</param>
92+
/// <param name="audioLength">오디오 길이 (초)</param>
93+
/// <returns>새로운 ChatSegment 인스턴스</returns>
94+
/// <exception cref="ArgumentNullException">audioMemoryOwner가 null인 경우</exception>
95+
/// <exception cref="ArgumentOutOfRangeException">audioDataSize가 유효하지 않은 경우</exception>
8896
public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
8997
{
98+
if (audioMemoryOwner is null)
99+
throw new ArgumentNullException(nameof(audioMemoryOwner));
100+
101+
if (audioDataSize < 0 || audioDataSize > audioMemoryOwner.Memory.Length)
102+
throw new ArgumentOutOfRangeException(
103+
nameof(audioDataSize),
104+
audioDataSize,
105+
$"audioDataSize는 0 이상 {audioMemoryOwner.Memory.Length} 이하여야 합니다.");
106+
90107
return this with
91108
{
92109
AudioMemoryOwner = audioMemoryOwner,
@@ -122,7 +139,28 @@ public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audi
122139
/// </summary>
123140
public void Dispose()
124141
{
125-
AudioMemoryOwner?.Dispose();
142+
Dispose(true);
143+
GC.SuppressFinalize(this);
144+
}
145+
146+
/// <summary>
147+
/// 보호된 Dispose 패턴 구현
148+
/// </summary>
149+
/// <param name="disposing">관리되는 리소스를 해제할지 여부</param>
150+
protected virtual void Dispose(bool disposing)
151+
{
152+
if (disposing && AudioMemoryOwner != null)
153+
{
154+
AudioMemoryOwner.Dispose();
155+
}
156+
}
157+
158+
/// <summary>
159+
/// Finalizer - 관리되지 않는 리소스 정리 (최후 안전장치)
160+
/// </summary>
161+
~ChatSegment()
162+
{
163+
Dispose(false);
126164
}
127165
}
128166
}

ProjectVG.Tests/Infrastructure/Integrations/MemoryPoolingPerformanceTests.cs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,41 +88,98 @@ public void ChatSegment_MemoryOwner_vs_ByteArray_Test()
8888
[Fact]
8989
public void ChatSegment_GetAudioSpan_SafetyBoundaryTest()
9090
{
91-
// 경계 조건 테스트: AudioDataSize가 실제 메모리보다 큰 경우
91+
// 경계 조건 테스트: 유효한 범위 내에서의 메모리 접근 안전성 검증
9292
var testData = GenerateTestAudioData(1000);
9393
using var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 더 작은 메모리 할당
9494
var actualMemorySize = memoryOwner.Memory.Length; // 실제 할당된 메모리 크기
9595
var copySize = Math.Min(500, actualMemorySize);
9696
testData.AsSpan(0, copySize).CopyTo(memoryOwner.Memory.Span);
9797

98-
// AudioDataSize를 실제 메모리보다 크게 설정 (위험한 상황 시뮬레이션)
99-
var oversizedRequest = actualMemorySize + 100;
98+
// 유효한 크기로 설정 (실제 메모리 크기 이하)
99+
var validSize = actualMemorySize - 10; // 안전한 크기
100100
var segment = ChatSegment.CreateText("Test content")
101-
.WithAudioMemory(memoryOwner, oversizedRequest, "audio/wav", 5.0f); // oversizedRequest > actualMemorySize
101+
.WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);
102102

103-
// GetAudioSpan이 예외 없이 안전하게 처리되어야
103+
// GetAudioSpan이 정확한 크기를 반환해야
104104
var span = segment.GetAudioSpan();
105105

106-
// 실제 메모리 크기만큼만 반환되어야 함 (Math.Min 적용됨)
107-
Assert.Equal(actualMemorySize, span.Length);
108-
_output.WriteLine($"요청 크기: {oversizedRequest}, 실제 메모리: {actualMemorySize}, 반환된 span 크기: {span.Length}");
106+
// 요청한 크기만큼 반환되어야 함
107+
Assert.Equal(validSize, span.Length);
108+
_output.WriteLine($"요청 크기: {validSize}, 실제 메모리: {actualMemorySize}, 반환된 span 크기: {span.Length}");
109109
}
110110

111111
[Fact]
112112
public void ChatSegment_GetAudioSpan_EmptyAndNullSafetyTest()
113113
{
114-
// null AudioMemoryOwner 테스트
115-
var segment1 = ChatSegment.CreateText("Test").WithAudioMemory(null!, 100, "audio/wav", 1.0f);
116-
var span1 = segment1.GetAudioSpan();
117-
Assert.True(span1.IsEmpty);
114+
// null AudioMemoryOwner는 이제 예외가 발생해야 함 (ArgumentNullException)
115+
var nullException = Assert.Throws<ArgumentNullException>(() =>
116+
ChatSegment.CreateText("Test").WithAudioMemory(null!, 100, "audio/wav", 1.0f));
117+
Assert.Equal("audioMemoryOwner", nullException.ParamName);
118118

119-
// AudioDataSize가 0인 경우
119+
// AudioDataSize가 0인 경우는 여전히 정상 작동해야 함
120120
using var memoryOwner = MemoryPool<byte>.Shared.Rent(100);
121121
var segment2 = ChatSegment.CreateText("Test").WithAudioMemory(memoryOwner, 0, "audio/wav", 1.0f);
122122
var span2 = segment2.GetAudioSpan();
123123
Assert.True(span2.IsEmpty);
124124

125-
_output.WriteLine("빈 케이스들이 모두 안전하게 처리됨");
125+
// 기존 AudioData 방식 (null 허용)
126+
var segment3 = ChatSegment.CreateText("Test").WithAudioData(null!, "audio/wav", 1.0f);
127+
var span3 = segment3.GetAudioSpan();
128+
Assert.True(span3.IsEmpty);
129+
130+
_output.WriteLine("null 검증과 빈 케이스가 모두 안전하게 처리됨");
131+
}
132+
133+
[Fact]
134+
public void ChatSegment_WithAudioMemory_ValidationTest()
135+
{
136+
var testData = GenerateTestAudioData(100);
137+
using var memoryOwner = MemoryPool<byte>.Shared.Rent(100);
138+
testData.CopyTo(memoryOwner.Memory.Span);
139+
140+
// 정상 케이스
141+
var validSegment = ChatSegment.CreateText("Test")
142+
.WithAudioMemory(memoryOwner, 50, "audio/wav", 1.0f);
143+
Assert.Equal(50, validSegment.AudioDataSize);
144+
145+
// null audioMemoryOwner 테스트
146+
var nullException = Assert.Throws<ArgumentNullException>(() =>
147+
ChatSegment.CreateText("Test").WithAudioMemory(null!, 100, "audio/wav", 1.0f));
148+
Assert.Equal("audioMemoryOwner", nullException.ParamName);
149+
150+
// audioDataSize < 0 테스트
151+
var negativeException = Assert.Throws<ArgumentOutOfRangeException>(() =>
152+
ChatSegment.CreateText("Test").WithAudioMemory(memoryOwner, -1, "audio/wav", 1.0f));
153+
Assert.Equal("audioDataSize", negativeException.ParamName);
154+
155+
// audioDataSize > memory.Length 테스트
156+
var oversizeException = Assert.Throws<ArgumentOutOfRangeException>(() =>
157+
ChatSegment.CreateText("Test").WithAudioMemory(memoryOwner, memoryOwner.Memory.Length + 1, "audio/wav", 1.0f));
158+
Assert.Equal("audioDataSize", oversizeException.ParamName);
159+
160+
_output.WriteLine("모든 소유권 이전 검증 테스트 통과");
161+
}
162+
163+
[Fact]
164+
public void ChatSegment_Dispose_MemoryOwnerReleaseTest()
165+
{
166+
var testData = GenerateTestAudioData(100);
167+
using var memoryOwner = MemoryPool<byte>.Shared.Rent(100);
168+
testData.CopyTo(memoryOwner.Memory.Span);
169+
170+
var segment = ChatSegment.CreateText("Test")
171+
.WithAudioMemory(memoryOwner, 100, "audio/wav", 1.0f);
172+
173+
// Dispose 호출 전에는 정상 접근 가능
174+
Assert.True(segment.HasAudio);
175+
Assert.Equal(100, segment.GetAudioSpan().Length);
176+
177+
// Dispose 호출
178+
segment.Dispose();
179+
180+
// 메모리가 해제되었으므로 ObjectDisposedException 발생할 수 있음
181+
// (실제 구현에 따라 다를 수 있음)
182+
_output.WriteLine("Dispose 호출 완료 - 메모리 소유자 해제됨");
126183
}
127184

128185
private byte[] GenerateTestAudioData(int size)

0 commit comments

Comments
 (0)