본문 바로가기
프로그래밍/Effective C#

Effective C# Item 34 : 함수를 매개변수로 사용하여 결합도를 낮추라

by bantomak 2023. 9. 27.

함수를 매개변수로 사용하여 결합도를 낮추라

클래스 내의 메서드를 정의하기 위해서 베이스 클래스(Base class)나 인터페이스(Interface)를 정의하고 이렇게 정의된 내용을 기반으로 코딩을 한다. 혹은 함수를 매개변수로 취하는 방식을 활용한다면 기존의 컴포넌트나 라이브러리와 함께 사용해야 하는 코드를 개발할 때 상당히 큰 도움이 된다. 함수를 매개변수로 취한다는 것은 개발자가 더 이상 구상 타입(concreate type)을 작성할 필요가 없으며, 오히려 추상화된 정의를 통해 종속성을 다루는 것을 의미한다.

 

인터페이스와 클래스를 구분하는 것에는 반드시 익숙해져야 한다. 하지만 때로는 인터페이스를 정의하고 구현하는 것조차 성가신 경우가 있으며 전통적인 객체지향 기법과는 다른 기법을 사용하여 API를 좀 더 단순하게 만들 수 있다. 실제로 델리게이트를 사용하여 컴포넌트의 계약을 기술하면 클라이언트 측에서 코드를 사용하기가 쉬워진다.

 

다른 개발자가 사용할 코드를 작성하는 경우 우리는 의도하지 않은 가정을 하게 되고 다양한 의존성 문제를 다룰 수밖에 없는데, 이러한 가정과 의존성 문제를 우리가 작성할 코드에서 분리하는 것은 상당히 까다로운 작업이다. 이 두 가지 문제가 코드에 미치는 영향은 실로 다양하다. 먼저 작성할 코드가 다른 부분에 의존하면 할수록 단위 테스트를 수행하기 어렵고, 다른 환경에서 코드를 재사용하기가 어려워진다. 한편 우리가 작성한 코드를 사용할 다른 개발자가 흔히 사용하는 구현 패턴과 유사하게 다가가면 갈수록 구현시의 제약 조건은 점점 더 많아진다.

 

함수를 매개변수로 사용하면 컴포넌트를 사용하는 측과 컴포넌트를 구현하는 측의 코드를 분리할 수 있다. 하지만 이러한 기법은 새롭게 적용하면 늘 그러하듯 추가 비용이 발생한다. 함께 사용하던 코드들을 분리하려면 추가 작업을 할 수밖에 없고 코드의 명확성도 떨어진다. 따라서 코드를 분리해서 사용자들이 얻을 수 있는 잠재적인 이득과 코드의 복잡도 증가라는 단점 사이에서 적절한 균형을 가져야 한다. 이와는 별도로 델리게이트나 여타의 통신 메커니즘을 사용하여 코드상의 결합도를 낮추면 낮출수록 컴파일러가 제공하는 다양한 검사 기능을 활용하기가 어려워진다. 이 또한 해결해야 할 과제로 남게 된다.

 

베이스 클래스에서 파생 클래스가 반드시 작성해야 하는 작업을 강제하는 것은 가장 제약이 많은 방법이다. 베이스 클래스가 필요로 하는 구성요소를 파생클래스에서 작성하도록 요구하는 것은 사용자 입장에서는 매우 제한적으로 느껴질 수 있다. 하지만 이 경우 클래스 상속방식을 이용하여 작업을 위임하기 때문에 다른 기법을 활용할 도리가 없다.

 

인터페이스를 작성하고 이를 구현하도록 구조를 가져가면 베이스 클래스에 의존하는 방식보다는 조금 더 느슨한 결합을 만들 수 있다. 하지만 이러한 방식 또한 베이스 클래스를 사용하는 경우와 크게 다르지 않으며 두 가지의 중요한 차이가 있을 뿐이다.

 

  • 첫 번째, 인터페이스를 사용하면 사용자에게 클래스의 계층 구조를 강제하지 않을 수 있다는 장점이 있다.
  • 두 번째, 인터페이스만을 제공하기 때문에 클라이언트 코드에서 활용할 수 있는 재사용 가능 코드를 제공하기가 어렵다는 단점이 있다.

 

사실 이 두 가지 방식 모두 우리가 달성하려는 목적에 비해서는 너무 많은 추가 작업을 요구한다. 정말 인터페이스를 정의해야 할까? 혹은 델리게이트를 정의하는 등의 작업을 통해서 느슨한 결합을 만드는 것이 정말 좋은 것일까?

 

.NET Framework를 설계한 개발자들은 이 메서드를 활용할 델리게이트가 아닌 인터페이스를 이용해서도 사용할 수 있도록 구현해 두었다.

// 추가적으로 부적절한 연관 관계가 생김
public interface IPredicate<T>
{
    bool Match(T soughtObject);
}

public class List<T>
{
    public void RemoveAll(IPredicate<T> match)
    {
        // 생략
    }
    
    // 이하 생략
}

// 이 인터페이스를 사용하려면 추가적인 작업이 필요하다.
public class MyPredicate : IPredicate<int>
{
    public bool Match(int target) =>
        target < 100;
}

 

위 내용을 보면 델리게이트를 사용해서 RemoveAll() 메서드를 수행하는 것이 훨씬 간단하다는 것을 알 수 있다. 즉, 인터페이스를 사용하는 것보다 델리게이트나 느슨한 결합을 위한 여타의 메커니즘을 사용하는 편이 훨씬 낫다.

 

인터페이스 대신 델리게이트를 사용하는 이유는 델리게이트가 타입을 구성하는 핵심 구성요소가 아니기 때문이다. 델리게이트는 메서드 중 하나로 간주되지도 않는다.

 

시퀀스 내의 모든 요소에 대해 특정 알고리즘을 수행한 후 단일의 스칼라 값을 결과로 얻고 싶은 경우가 있다. 예를 들어 다음은 정수 시퀀스로부터 합을 구하는 메서드다.

 

public static in Sum(IEnumerable<int> nums)
{
    var total = 0;
    
    foreach (int num in nums)
    {
        total += num;
    }
    
    return total;
}

 

이제 합을 구하는 코드를 분리해서 델리게이트로 치환하면 이 메서드를 범용 누적기로 바꿀 수 있다.

 

public static T Sum<T>(IEnumerable<T> sequence, T total, Func<T, T, T> accumulator)
{
    foreach (var i in sequence)
    {
        total = accumulator(total, i);
    }
    
    return total;
}

 

이 메서드는 다음과 같이 사용할 수 있다.

 

var total = 0;
total = Sum(sequence, total, (sum, num) => sum += num);

 

Sum 메서드는 여전히 제약이 많은 것 같다. 이 메서드는 여전히 초깃값을 타입과 반환값의 타입으로 시퀀스 내의 개별 요소와 동일한 타입을 사용해야 한다. 다음과 같이 이를 다르게 사용하고 싶을 수도 있다.

 

var peeps = new List<Employee>();

var totalSalary = Sum(peeps, 0M, (sum, person) => sum + person.Salary);

 

이 경우 Sum 메서드를 약간 수정하여 시퀀스 내의 개별 요소와 누적 합이 서로 다른 타입을 갖도록 할 수 있따. 더욱 범용적인 메서드로 개선된 것 같으니 이름도 Fold로 변경하자.

 

public static TResult Fold<T, TResult>(IEnumerable<T> sequence,
TResult total, Func<TResult, T, TResult> accumulator)
{
    foreach(var i in sequence)
    {
        total = accumulator(total, i);
    }
    
    return total;
}

 

함수를 매개변수로 사용하면 알고리즘 자체와 알고리즘을 적용할 타입을 분리하는 데 도움이 된다. 하지만 이렇게 결합도를 느슨하게 구성하려면 분리된 컴포넌트를 사용할 때 발생할 수 있는 오류를 처리하기 위해 추가적인 작업을 해야 한다.

예를 들어 이벤트를 정의하는 코드를 작성했다고 가정해 보자. 사용자가 이벤트 핸들러를 구성하지 않았을 수도 있으므로 이벤트를 발생시킬 때마다 해당 이벤트가 null인지를 확인해야 한다. 델리게이트를 사용할 때도 마찬가지다. 클라이언트가 델리게이트로 null을 전달하면 어떻게 해야 할까? 이것은 예외적인 상황인가? 아니면 이 경우를 위한 기본 동작이 있는가? 만약 델리게이트가 예외를 일으키면 어떻게 되는가? 이를 복구할 수 있는가? 만일 그렇다면 어떻게 복구할 것인가?

 

델리게이트를 쓰면 작업은 늘어나지만 더 많은 유연성을 얻을 수 있다.

 

출처

 

이펙티브 C# - 예스24

더 나은 C# 코드를 작성하는 새로운 방법 50가지 C#은 전통적인 .NET 기반 개발에서 유니티 게임 엔진으로 개발 영역을 확대하면서 더욱 주목받고 있다. 또한 자마린으로 다양한 모바일 플랫폼에

www.yes24.com

댓글