오랜만에 질문 게시판을 보다가 이런 코드를 봤습니다. 콤보박스에서 고른 형식에 따라 이미지를 저장하는 부분입니다.
Select Case m_cmbSaveAs.Text
Case "*.bmp"
m_bitmap.Save(strFileName, ImageFormat.Jpeg)
Case "*.jpg"
m_bitmap.Save(strFileName, ImageFormat.Jpeg)
Case "*.gif"
m_bitmap.Save(strFileName, ImageFormat.Gif)
Case "*.tif"
m_bitmap.Save(strFileName, ImageFormat.Tiff)
End Select
동작에는 문제가 없어 보이지만, 형식이 하나 늘 때마다 Case 를 하나씩 추가해야 합니다. 이렇게 경우의 수를 코드에 직접 박아 두는 걸 흔히 하드코딩이라고 부르죠. 실제로 이 코드도 복사해서 붙여 넣다 생긴 실수가 하나 있습니다. "*.bmp" 를 골라도 ImageFormat.Jpeg 로 저장됩니다. 이름은 .bmp 인데 내용은 JPEG 인 파일이 만들어지는 거죠. 줄이 반복되는 구조에서는 이런 실수가 눈에 잘 안 띕니다.
확장자 이름으로 속성을 찾게 하면
ImageFormat 클래스에는 Bmp, Jpeg, Gif, Tiff, Png 같은 형식이 Shared 속성으로 들어 있습니다. 확장자 문자열과 속성 이름이 거의 같으니, 리플렉션으로 이름을 넣어 속성을 찾으면 Select Case 없이 한 번에 해결됩니다.
Dim extension As String = IO.Path.GetExtension(strFileName).ToLower.Replace("jpg", "jpeg")
Dim imgFormat As ImageFormat = CType(GetType(ImageFormat).GetProperty(extension, BindingFlags.[Public] Or BindingFlags.[Static] Or BindingFlags.IgnoreCase).GetValue(Nothing, Nothing), ImageFormat)
m_bitmap.Save(strFileName, imgFormat)
GetProperty 에 Public·Static·IgnoreCase 를 주면 대소문자 구분 없이 공개 정적 속성을 찾습니다. 정적 속성이라 인스턴스 없이 GetValue(Nothing, Nothing) 으로 값을 꺼내면 해당 ImageFormat 객체가 나옵니다. 확장자 jpg 만 속성 이름(Jpeg)과 달라서 Replace 로 맞춰 줬습니다. Imports System.Reflection 과 Imports System.Drawing.Imaging 이 필요합니다.
둘 다 장단점이 있지만 저는 이쪽을 선호합니다. 새 형식을 지원하려면 콤보박스 목록에 항목만 추가하면 되고, 저장 코드는 손댈 필요가 없습니다. 위의 bmp 실수 같은 것도 구조적으로 생길 수가 없고요.
그대로 쓰면 막히는 곳
위 코드는 이 상태로는 동작하지 않습니다. Path.GetExtension 은 ".jpg" 처럼 점이 붙은 값을 돌려주기 때문에 ".jpeg" 라는 이름의 속성을 찾게 되고, 그런 속성은 없으니 GetProperty 가 Nothing 을 돌려줘 GetValue 에서 NullReferenceException 이 납니다. 또 .tif 확장자는 속성 이름이 Tiff 라서 따로 맞춰 줘야 합니다. 이 두 가지를 고치고, 모르는 확장자가 들어왔을 때 PNG 로 저장하도록 한 코드입니다(원 코드를 고쳐 추가한 것입니다).
Dim extension As String = IO.Path.GetExtension(strFileName).TrimStart("."c).ToLower()
If extension = "jpg" Then extension = "jpeg"
If extension = "tif" Then extension = "tiff"
Dim prop As PropertyInfo = GetType(ImageFormat).GetProperty(extension, BindingFlags.[Public] Or BindingFlags.[Static] Or BindingFlags.IgnoreCase)
Dim imgFormat As ImageFormat = If(prop Is Nothing, ImageFormat.Png, CType(prop.GetValue(Nothing, Nothing), ImageFormat))
m_bitmap.Save(strFileName, imgFormat)
Replace("jpg", "jpeg") 대신 If 로 비교한 것도 이유가 있습니다. Replace 는 문자열 중간에 있는 글자도 바꾸기 때문에, 확장자가 이미 jpeg 인 경우엔 괜찮지만 tif 를 같은 방식으로 Replace("tif", "tiff") 하면 tiff 가 tifff 가 됩니다.
리플렉션은 편하지만 문자열로 이름을 찾는 만큼 오타나 예상 못 한 입력에 약합니다. 지원 형식이 몇 개 안 되고 바뀔 일도 없다면 Dictionary(Of String, ImageFormat) 에 확장자와 형식을 미리 넣어 두고 찾는 방법도 괜찮습니다. 목록 한 곳만 고치면 된다는 장점은 그대로 가져가면서, 어떤 형식을 지원하는지가 코드에 분명히 드러납니다.