사용자 정의 타입을 값처럼 편하게 다루고 싶다면 암시적/명시적 변환 연산자를 고려합니다. 안전한 변환은 implicit, 주의가 필요한 변환은 explicit로 제공하면 코드 가독성과 안정성이 동시에 올라갑니다.
1. 언제 implicit, 언제 explicit인가요?
암시적(implicit)은 정보 손실이나 예외 가능성이 사실상 없고, 개발자가 놀라지 않을 변환에만 사용합니다. 예: Guid 래퍼로의 포장, 단위의 확장 변환(좁은 개념에서 넓은 개념). 명시적(explicit)은 범위 손실, 정밀도 이슈, 실패/예외 가능성이 있거나 의미가 애매한 변환에 사용합니다. 예: 문자열 파싱, 단위 환산(반올림 포함), 내부 값 노출.
2. 기본 문법과 규칙
변환 연산자는 public static으로 선언하며, 두 타입 중 하나는 반드시 해당 연산자를 선언하는 타입이어야 합니다. 변환 연산자는 부작용 없이 빠르게 동작해야 합니다.
// 시그니처 형태
public static implicit operator TargetType(SourceType value) { ... }
public static explicit operator TargetType(SourceType value) { ... }
3. 값 객체 예시: Meter <-> double
double에서 Meter로는 안전하게 포장할 수 있으므로 implicit를, Meter에서 double로는 내부 값 노출이므로 explicit를 사용합니다. 유효성 검사도 함께 넣습니다.
using System;
public readonly struct Meter
{
public double Value { get; }
public Meter(double value)
{
if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
Value = value;
}
// 안전한 포장: double -> Meter
public static implicit operator Meter(double value) => new Meter(value);
// 내부 값 노출: Meter -> double (명시적으로만 허용)
public static explicit operator double(Meter m) => m.Value;
public override string ToString() => $"{Value} m";
}
class Demo
{
static void Main()
{
Meter m = 12.5; // implicit: OK
double raw = (double)m; // explicit: 캐스트 필요
Console.WriteLine(m); // 12.5 m
}
}
참고: struct 변환은 nullable로도 자동 승격(lifting)됩니다. Meter?에서 double?로의 explicit 변환 등도 지원됩니다(null은 null로 변환).
4. 식별자 래핑: Guid <-> OrderId
도메인 식별자를 값 객체로 감싸면 타입 안정성이 올라갑니다. Guid -> OrderId는 안전하니 implicit, 반대로 원시 값 노출은 explicit로 제한합니다.
using System;
public readonly record struct OrderId(Guid Value)
{
public static implicit operator OrderId(Guid value) => new OrderId(value);
public static explicit operator Guid(OrderId id) => id.Value;
public override string ToString() => Value.ToString();
}
class Demo
{
static void Main()
{
Guid g = Guid.NewGuid();
OrderId id = g; // implicit
Guid raw = (Guid)id; // explicit
Console.WriteLine(id);
}
}
5. 단위 변환: Celsius ↔ Fahrenheit (양방향 explicit)
정밀도 손실이나 반올림 이슈가 있을 수 있는 환산은 explicit로 두 방향 모두 구현합니다.
public readonly struct Celsius
{
public double Value { get; }
public Celsius(double value) => Value = value;
public static explicit operator Fahrenheit(Celsius c)
=> new Fahrenheit(c.Value * 9 / 5 + 32);
public override string ToString() => $"{Value:0.##} °C";
}
public readonly struct Fahrenheit
{
public double Value { get; }
public Fahrenheit(double value) => Value = value;
public static explicit operator Celsius(Fahrenheit f)
=> new Celsius((f.Value - 32) * 5 / 9);
public override string ToString() => $"{Value:0.##} °F";
}
class Demo
{
static void Main()
{
var c = new Celsius(25);
var f = (Fahrenheit)c; // explicit
var c2 = (Celsius)f; // explicit
}
}
6. 베스트 프랙티스
첫째, implicit은 놀람이 없어야 합니다. 예외를 던지거나 정보 손실이 발생할 가능성이 있으면 explicit로 전환합니다.
둘째, 변환은 빠르고 순수해야 합니다. I/O, 로깅, 서비스 호출을 넣지 않습니다.
셋째, 문자열 변환은 Parse/TryParse, ToString을 우선합니다. string에 대한 implicit은 혼동을 일으키기 쉽습니다.
넷째, 한쪽만 의미가 명확하면 단방향만 제공합니다. 양방향을 강요할 필요는 없습니다.
다섯째, 오버로드 해석 이슈를 피하려면 모호한 경우 explicit로 강제하거나, 명확한 팩토리 메서드를 병행합니다.
7. 자주 겪는 함정
모든 타입에 대해 정의할 수는 없습니다. 최소 한쪽 타입은 해당 연산자를 선언하는 사용자 정의 타입이어야 합니다.
숫자 타입 간 내장 변환이 우선될 수 있습니다. 의도한 연산이 모호하면 캐스트를 명시하세요.
컬처 의존 파싱을 변환 연산자에 넣지 마세요. API 가장자리에 Parse/TryParse를 제공합니다.
8. 체크리스트
변환이 항상 안전한가요? 그렇다면 implicit. 아니라면 explicit입니다.
예외를 던질 수 있나요? explicit 또는 TryParse 스타일 대안을 제공합니다.
Round-trip이 필요한가요? A→B→A 테스트를 작성해 정확도를 확인합니다.
문서화했나요? 변환 의미와 제약을 Summary에 명확히 남깁니다.
변환 연산자는 작지만 강력합니다. 규칙만 지키면 도메인 모델을 더 타입 안전하고 읽기 쉽게 만들 수 있습니다.
'C#' 카테고리의 다른 글
| C# 고급 이벤트 패턴: EventArgs 상속과 데이터 전달 (0) | 2026.05.26 |
|---|---|
| C# ThreadLocal<T>로 스레드별 데이터 관리 (0) | 2026.05.26 |
| C# 구조적 비교와 EqualityComparer<T> 커스터마이징 (0) | 2026.05.25 |
| C# 인터페이스의 기본 구현(Default Interface Implementation) 활용 (0) | 2026.05.23 |
| C# 디버깅 심화: Conditional Attribute와 DebuggerDisplay (0) | 2026.05.22 |