대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 쓰레기를 줄이는 타입 설계 가이드 : 기본 타입으로부터 벗어나기
등록일 조회수 4081
첨부파일  

쓰레기를 줄이는 타입 설계 가이드

기본 타입으로부터 벗어나기



데이터 타입을 설계할 때 특별한 의미없이 기본 타입(Primitive type)을 흔히 쓴다. 기본 타입은 특정 비즈니스 목적을 표현할 수 없는 경우가 대부분이다. Contact 클래스의 EmailAddress, State, Zip 등도 모두 string 문자열형이다. 문자열형이 EmailAddress, State, Zip의 역할과 의도까지 표현하기에는 분명 한계가 있다. 타입에 역할과 의도까지 표현하는 필수 기본 데이터 타입 설계 방법을 함께 고민해 보자.



낮은 결합도와 높은 응집도를 위해 Contact 클래스를 리팩토링한 후 이전보다 더 많은 타입 클래스가 추가됐다(<그림 1> 참조).



Contact 클래스가 리팩토링되면서 Contact의 세부 정보를 클래스 단위로 확인할 수 있게 됐다. 과거에는 클래스 내부을 살펴보지 않으면 Contact 클래스가 어떤 정보로 구성됐는지 알 수 없었다. 그러나 지금은 이름(PersonalName), 이메일(EmailContactInfo), 주소(PostalContactInfo)로 바뀌면서 굳이 클래스 내부를 들여다보지 않아도 구성을 알 수 있게 됐다.



더 명시적이고 정확한 기본 데이터 타입 설계

10월호부터 계속된 연재에서 언급했듯 ‘string email;’에서 이메일 데이터는 프로그래밍 언어 관점으로 보면 문자열형이라는 점 외에 어떠한 의도도 알 수 없다. 변수명에서 알 수 있는 것은 문자열형이고 이메일 역할을 한다는 게 전부다. 정말 문자열형만으로 이메일의 역할과 의도까지 표현할 수 있을까? 그 답을 <리스트 1>의 EmailcontactInfo 클래스와 PostalAddress 클래스에 정의된 타입을 통해 함께 찾아보자.



<리스트 1> EmailAddress, State, Zip 타입 public class EmailContactInfo { public string EmailAddress { get; set; } … } public class PostalAddress { … public string State { get; set; } public string Zip { get; set; } }



<리스트 1>에는 EmailAddress, State, Zip가 문자열형으로 정의돼 있다. 정말 문자열형 하나로 EmailAddress, State, Zip의 각각의 역할과 의도를 표현할 수 있을까? 당연하지만 문자열형으로는 어렵다. 마틴 파울러가 쓴 리팩토링 책의 챕터8 ‘데이터 체계화’를 살펴보면 데이터 값을 객체로 전환(Replace Data Value with Object), 분류 부호를 클래스로 전환(Replace Type Code with Class), 분류 부호를 상태/전략 패턴으로 전환(Replace Type Code with State/Strategy) 등으로 코드 속 나쁜 냄새를 제거할 수 있다.

여기서 말하는 나쁜 냄새는 문자열형과 같은 기본 타입을 특별한 이유없이 남용하는 것을 뜻한다. 이를 ‘기본 타입에 대한 강박관념(Primitive Obsession)’이라 한다. 그의 조언대로 역할과 의도까지 표현하기 위해 개별 타입을 이용해 보자. 데이터 타입을 만들 때 가장 먼저 해야 할 것은 프로그래밍 언어 관점에서 값 타입 또는 참조 타입 중 하나를 결정하는 것이다. 값 타입인 경우 구조체(struct)로, 참조 타입인 경우 클래스(class)로 구현해야 한다.

구조체와 클래스의 차이는 이 글의 주제에서 벗어난 관계로 자세한 설명을 생략한다. 지금부터는 두 타입 중 참조 타입으로 진행한다.



<리스트 2> EmailAddress, StateCode, ZipCode 타입 정의 public class EmailContactInfo { public EmailAddress EmailAddress { get; set; } … } public class PostalAddress { … public StateCode State { get; set; } public ZipCode Zip { get; set; } } public class EmailAddress { public string Value { get; set; } } public class StateCode { public string Value { get; set; } } public class ZipCode { public string Value { get; set; } }



<리스트 2>는 EmailAddress, State, Zip라는 개별 타입을 만들고 문자열형에서 새로 추가한 타입으로 변경한 예다. 기존의 EmailAddress, State, Zip이 문자열형 타입에서 EmailAddress, StateCode, ZipCode라는 개별 타입으로 정의돼 있다. 타입 이름부터 더 명확한 의도를 전달하게 바뀐 것이다.

그러나 새로이 추가된 EmailAddress, StateCode, ZipCode는 모두 이전에 정의했던 문자열형 타입을 public string Value { get; set; }과 같이 개별 타입 클래스의 내부로 이동시킨 것에 불과하다. 역할이라는 측면에서는 여전히 부족 표현이다.



<리스트 3> 데이터 유효성 검사를 데이터 타입 외부에서 처리 Test(fun@developer.co.kr); Test(“developer.co.kr”); void Test(string value) { if (System.Text.RegularExpressions.Regex.IsMatch (value, @"^w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)*$")) // 데이터 타입 유효성 검사 EmailAddress = new EmailAddress { Value = value }; }



<리스트 2>의 EmailAddress 데이터 타입을 사용하기 위해서는 유효성 검사를 EmailAddress 데이터 타입에서 처리하는 것이 아니라 <리스트 3>처럼 외부에서 처리해야 한다. 유효성 검사는 데이터 타입의 대표적인 역할이다. 이를 외부에서 처리하는 것은 데이터 타입 클래스가 필수적인 책임을 회피하는 것과 마찬가지다.

<리스트 4>는 유효성 검사를 데이터 타입 생성자에서 처리하도록 개선한 결과다.



<리스트 4> 데이터 유효성 검사를 생성자에서 처리 public class EmailAddress { private string _value; public EmailAddress(string value) { // 생성자 if (System.Text.RegularExpressions.Regex.IsMatch (value, @"^w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)*$")) { // 데이터 유효성 검사 _value = value; } else { // 예외 처리? }}}



<리스트 4>에서는 private으로 외부에서 데이터 유효성 검사가 생성자에서 처리된다.

타입 클래스가 책임을 다하지만 수용할 수 없는 데이터의 처리 방안이 모호하다. 예외 처리를 할지 아니면 무시하고 데이터 타입을 생성한 후 처리할지 애매한 것이다.

이를 해결하는 대표적인 방법은 객체 생성 전용 정적 메소드를 이용하는 것이다(<리스트 5> 참조).



<리스트 5> 데이터 유효성 검사를 객체 생성 전용 정적 메소드에서 처리 public class EmailAddress { private string _value; private EmailAddress(string value) { // 생성자 _value = value; } public static EmailAddress Create(string value) { if (System.Text.RegularExpressions.Regex.IsMatch (value, @"^w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)*$")) { return new EmailAddress(value); } else { return null; }}}



<리스트 5>는 올바른 데이터가 입력되면 Create 정적 메소드에서 객체를 생성하지 않는다고 명확하게 처리한다. 그러나 null을 전달하므로 이후 EmailAddress 객체를 사용하는 곳에서는 NullReferenceException 예외 발생을 방지하기 위해 null 유무를 확인하는 불필요한 제어 흐름(if)을 써야 한다.

‘Null Object’ 패턴을 이용하면 null과 관련된 불필요한 쓰레기 코드를 제거할 수 있다. 그러나 이 패턴을 적용하려면 모든 인터페이스로부터 나올 수 있는 데이터 타입에 null을 처리할 클래스를 추가해야 한다. 그러므로 이보다는 더 일반적인 제네릭(Generics) 기반 Option 값 타입을 추가하는 게 더 좋다(<그림 6> 참조). OptionType 열거형을 통해 데이터가 있을 때에는 Some으로, 데이터가 없을 때는 None으로 구분하는 것이다. 또 None일 때에는 내부적으로 void을 표현하기 위한 Unit 클래스로, Null을 대체하게 된다.



<리스트 6> Option 클래스로 null 가능성 표현 public enum OptionType { None = 0, Some = 1 } public struct Option< T> { private readonly OptionType _tag; private readonly T _value; internal Option(T value) { _value = value; _tag = value != null ? OptionType.Some : OptionType.None; } internal Option(Unit unit) { _value = default(T); _tag = OptionType.None; }} public static class Option { public static Option< T> None< T>() { return new Option< T>(Unit.Value); } public static Option< T> Some< T>(T value) { return new Option< T>(value); }} public class Unit { // void을 표현한다 private Unit() { } public static readonly Unit Value = new Unit(); public override string ToString() { return "unit"; }} public class EmailAddress { private string _value; private EmailAddress(string value) { _value = value; } public static Option < EmailAddress> Create(string value) { if (System.Text.RegularExpressions.Regex.IsMatch (value, @"^w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)*$")) { return Option.Some(new EmailAddress(value)); } else { return Option.None< EmailAddress>(); }}}



public static Option< EmailAddress> Create(string value) 함수 반환 타입인 Option< EmailAddress>로 null 가능성을 표현했다. 그러나 여전히 Create 함수에서 수용할 수 없는 EmailAddress에서 처리할 수 없는 데이터가 입력될 때 왜 return Option.None < EmailAddress>();으로 처리했는지에 대한 어떠한 정보도 없다.



<리스트 7> EmailAddress 객체 생성 실패 처리 개선과 연속 작업 제공 public class EmailAddress { private string _value; private EmailAddress(string value) { _value = value; } public static Option< EmailAddress> CreateWithContinuations(string value, Func< EmailAddress, Option< EmailAddress>> success, Func< string, Option< EmailAddress>> failure) { if (System.Text.RegularExpressions.Regex.IsMatch(value, @"^w+([-+.']w+) *@w+([-.]w+)*.w+([-.]w+)*$")) { return success(new EmailAddress(value)); } else { return failure(string.Format("error creating email: {0}", value)); }}}



<리스트 7>은 EmailAddress 객체 생성 실패를 처리할 때 발생되는 부가 정보를 CreateWithContinuations 함수로 전달된 failure로 전달하게 바뀌었다. 또한 <리스트 7>에서 변경된 EmailAddress는 <리스트 8>처럼 성공과 실패 처리를 Create WithContinuations 함수 매개변수로 전달하므로 Create WithContinuations 함수 내부의 성공과 실패에 상관없이 다음 작업을 수행할 수 있다.



<리스트 8> 함수 성공과 실패 구분 없이 다음 작업 연속으로 수행 Func< EmailAddress, Option< EmailAddress>> success = (emailAddress => { return Option.Some(emailAddress); }); Func< string, Option< EmailAddress>> failure = (message => { Trace.WriteLine(string.Format("Failure: {0}", message)); return Option.None< EmailAddress>(); }); Option< EmailAddress> emailAddress = EmailAddress.CreateWithContinuations ("fun@developer.co.kr", success, failure); // if (emailAddress == null) 불필요



<리스트 7>에서 수정한 EmailAddress 클래스를 통해 <리스트 8>은 함수 내부 수행 결과에 의존하지 않고 연속으로 다음 작업을 수행할 수 있게 됐다. 그러나 매번 성공과 실패를 처리하는 함수를 전달하는 것은 번거롭다. 일반적인 성공과 실패 처리할 함수를 정의한다면 그 외 경우에만 명시적으로 성공과 실패를 처리하는 함수를 전달함으로써 코드를 더 줄일 수 있을 것이다.



<리스트 9> 함수 성공과 실패 작업 일반화 public class EmailAddress { private string _value; private EmailAddress(string value) { _value = value; } public static Option< EmailAddress> CreateWithContinuations (string value, Func< EmailAddress, Option< EmailAddress>> success, Func< string, Option< EmailAddress>> failure) { if (System.Text.RegularExpressions.Regex.IsMatch(value, @"^w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)*$")) { return success(new EmailAddress(value)); } else { return failure(string.Format("error creating email: {0}", value)); }} public static Option < EmailAddress> CreateWithContinuations(string value) { Func< EmailAddress, Option< EmailAddress>> success = (emailAddress => { return Option.Some(emailAddress); }); Func< string, Option< EmailAddress>> failure = (message => { Trace.WriteLine(string.Format("Failure: {0}", message)); return Option.None< EmailAddress>(); }); return CreateWithContinuations(value, success, failure); }} Option< EmailAddress> emailAddress = EmailAddress.CreateWithContinuations("fun@developer.co.kr");



<리스트 9>는 CreateWithContinuations 함수의 성공과 실패 처리를 기본적으로 제공하는 함수와 직접 지정할 수 있는 함수 두 가지를 추가적으로 제공하도록 수정한 예다.

지금까지 문자열형으로 처리했던 데이터 타입을 그 역할과 의도를 더 명시적으로 표현하기 위해 개별 데이터 타입(EmailAddress, StateCode, ZipCode)을 추가해 봤다. 그리고 데이터 유효성 검사를 통해 더 정확한 데이터 타입을 위한 설계 과정을 EmailAddress 예제로 살펴봤다. 또한 메소드 결과의 성공과 실패 처리를 메소드 안으로 이동시킴으로써 메소드 결과에 매번 의존하지 않고 다음 작업을 연속으로 처리할 수 있게도 해 봤다.

지면 관계상 StateCode와 ZipCode는 생략했다. 그러나 EmailAddress 클래스 개선 과정을 이해했다면 StateCode와 ZipCode 클래스도 충분히 스스로 구현할 수 있을 것이다.



출처 : 마이크로소프트웨어 12월호

제공 : 데이터 전문가 지식포털 DBguide.net