참치김밥은 최고의 한식이다

[Effective C# 2판] Item 11 : API에는 변환 연산자를 작성하지 말라 본문

책/C#

[Effective C# 2판] Item 11 : API에는 변환 연산자를 작성하지 말라

l__j__h 2024. 2. 21. 16:05

변환 연산자(conversion operator)는 클래스 간 대체 가능성을 지원하기 위한 기능이다. 대체 가능성이란 하나의 클래스를 다른 클래스로 대체할 수 있음을 말한다.

 

클래스를 만들다 보면 어떤 변환은 자동으로 허용된다. 어떤 객체든 .NET 클래스 계층의 최상위 타입인 System.Object 타입으로 대체할 수 있는 것처럼, 클래스의 어떤 객체든 해당 클래스가 구현한 인터페이스, 그 인터페이스의 베이스 인터페이스 혹은 베이스 클래스로 대체가 가능하다.

 

하지만, 종종 대상 타입으로 완벽하게 변환을 수행하지 못할 경우 오류가 발생한다. 예를 들어, "대상 타입으로 변환된 객체의 상태를 변경하는 것"과 "변환되기 이전 객체의 상태를 변경하는 것"의 결과가 서로 다를 수 있다. 또는 변환 연산자가 임시 객체를 반환해버려서, 이 객체의 상태를 변경하더라도 이전 객체에 아무런 영향을 미치지 못하는 경우다. 이 임시 객체는 가비지 컬렉터에 의해서 제거되어 버린다.

 

객체를 다른 타입으로 변환하고 싶을 때는 생성자를 사용하라. 생성자는 새로운 객체를 만든다는 사실을 명확히 알려준다. 변환 연산자만 사용하면, 코드의 오류 지점을 찾기가 어려워진다.

예를 들어보자. 최상위 클래스로 도형 클래스가 있고, 그 밑에 각각 원(Circle) 클래스와 타원(Ellipse) 클래스가 존재한다. 두 클래스는 도형 클래스를 상속받는다. 그런데, 원은 타원에 포함된다. 따라서, 원을 타원으로 변환하는 변환 연산자를 직접 작성하고자 한다.

 

public class Circle : Shape{
	private Point center;
	private double radius;

	public Circle() : this(new Point(), 0){ }
	
	public Circle(Point c, double r){
		center = c;
		radius = r;
}

	public override void Draw() { ... }

	public static implicit operator Ellipse(Circle c){
		return new Ellipse(c.center, c.center, c.radius, c.radius);
}
}

*여기서 잠깐~~! 명시적(explicit) 형변환 연산자와 암시적(implicit) 형변환 연산자의 차이는?

아래 블로그가 잘 정리해두셨다. (감사)

https://loveme-do.tistory.com/4

 

[C#] 사용자 정의 형변환 - explicit, implicit

C# 사용자 정의 형변환 - explicit, implicit 두 기능 모두 C#4.0부터 지원해주고 있는 기능입니다. - explicit 명시적 사용자 정의 형변화 연산자. 1234567891011121314151617181920212223242526272829303132333435363738394041424

loveme-do.tistory.com

 

아무튼 위처럼 Circle 클래스를 작성했을 때, 아래 두 함수에 넣어보자

public static double ComputeArea(Ellipse e) => e.R1 * e.R2 * Math.PI;
//객체의 값을 변경하지 않는 메서드

public static Flatten(Ellipse e){
	e.R1 /= 2;
	e.R2 *= 2;
}

Circle c1 = new Circle(new Point(3f, 0f), 5f);
ComputeArea(c1); //객체의 값을 변경하지 않고, 객체의 값을 이용해서 원하는 값을 계산하기만 하면 되므로 잘 작동한다

Circle c2 = new Circle(new Point(3f, 0f), 5f);
Flatten(c2); //객체의 값을 변경하는 메서드이다. 
//언뜻 보기엔 Circle c2가 Ellipse로 변환된 후 c2의 값을 잘 변경해줄 것 같지만,
//실제로는 c2를 Ellipse로 변환한 "임시 객체"가 메서드 안에 들어간다.
//따라서 메서드 안의 임시 객체의 값만 바꾸고 끝나기 때문에, c2 객체에는 아무런 영향을 주지 않는다.

이게 암시적(암묵적) 형변환시의 주의사항이다.

그런데 그렇다고 명시적 형변환을 한다고 해서 변경이 되는 것도 아니다.

Flatten(new Ellipse(c2));
//이것도 임시 객체만 수정한다.

근데 사실 어찌보면 당연한 결과이다.

왜냐고???

형변환한 객체를 어딘가에 따로 저장해주지 않았잖아~!!

Ellipse e = new Ellipse(c2);
Flatten(e);

이제서야 값이 변경된 e를 얻을 수 있다.

이처럼 변환 연산자는 여러 문제를 일으킨다. 변환 연산자를 사용하면 현재 클래스 대신 다른 클래스도 끌어 쓸 수 있을 것 같지만, 형변환 과정에서 임시 객체가 생성되기 때문에, 이 임시 객체에 접근하여 작업하는 꼴이 된다. 임시 객체를 수정하는 것은 의미가 없다. 게다가 객체를 변환하는 코드는 컴파일러가 생성할 것이기 때문에, 코드에 명시적으로 드러나지도 않는다. 이처럼 미묘한 버그는 발견하기가 어렵다. API에는 변환 연산자를 작성하지 말아라.

 

 

 

728x90