ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 단위 테스트 정리 5 - 가치 있는 단위 테스트를 위한 리팩터링
    책책책 책을 읽읍시다/프로그래밍 2022. 8. 3. 10:36

    리팩터링할 코드 식별하기


    코드의 네 가지 유형

    모든 제품 코드는 2차원으로 분류할 수 있다.

    • 복잡도 또는 도메인 유의성
    • 협력자 수

    코드 복잡도(cod complexity)는 코드 내 의사 결정(분기) 지점 수로 정의한다. 이 숫자가 클수록 복잡도는 더 높아진다.

    도메인 유의성(domain significance)은 코드가 프로젝트의 문제 도메인에 대해 얼마나 의미있는지를 나타낸다. 일반적으로 도메인 계층의 모든 코드는 최종 사용자의 목표와 직접적인 연관성이 있으므로 도메인 유의성이 높다. 반면에 유틸리티 코드는 그런 연관성이 없다.

    복잡한 코드와 도메인 유의성을 갖는 코드가 단위 테스트에서 가장 이롭다. 해당 테스트가 회귀 방지에 뛰어나기 때문이다. 도메인 코드는 복잡할 필요가 없으며, 복잡한 코드는 도메인 유의성이 나타나지 않아도 테스트할 만하다.

    두 번째 차원은 클래스 또는 메서드가 갖니 협력자 수다. 협력자는 가변 의존성이거나 프로세스 외부 의존성(또는 둘 다)이다. 협력자가 많은 코드는 테스트 비용이 많이 든다. 테스트 크기에 따라 달라지는 유지 보수성 지표 때문이다. 협력자를 예상되는 조건으로 두고 상태나 상호 작용을 확인하게끔 코드를 작성해야 한다.

     

    • 도메인 모델과 알고리즘 : 보통 복잡한 코드는 도메인 모델이지만, 100%는 아니다. 문제 도메인과 직접적으로 관련이 없는 복잡한 알고리즘이 있을 수 있다
    • 간단한 코드 : C#에서는 매개변수가 없는 생성자와 한 줄 속성 등이 있다. 협력자가 있는 경우가 거의 없고 복잡도나 도메인 유의성도 거의 없다.
    • 컨트롤러 : 복잡하거나 비즈니스에 중요한 작업을 하는 것이 아니라 도메인 클래스와 외부 애플리케이션 같은 다른 구성 요소의 작업을 조정한다.
    • 지나치게 복잡한 코드 : 두 가지 지표 모두 높다. 협력자가 많으며 복잡하거나 중요하다. 한 가지 예로 덩치가 큰 컨트롤러(복잡한 작업을 어디에도 위임하지 않고 모든 것을 스스로 하는 컨트롤러)가 있다.

    좌착 상단 사분면(도메인 모델 및 알고리즘)을 단위 테스트하면 노력 대비 가장 이롭다. 이러한 단위 테스트는 매우 가치있고 저렴하다. 해당 코드가 복잡하거나 중요한 로직을 수행해서 테스트의 회귀 방지가 향상되기 떄문에 가치 있다. 또한 코드에 협력자가 거의 없어서(이상적으로는 완전히 없음) 테스트 유지비를 낮추기 때문에 저렴하다.

    간단한 코드는 테스트할 필요가 전혀 없다. 이러한 테스트는 가치가 0에 가깝다. 컨트롤러의 경우, 포괄적인 통합 테스트의 일부로서 간단히 테스트해야 한다.

    가장 문제가 되는 코드 유형은 지나치게 복잡한 코드다. 단위 테스트가 어렵겠지만, 테스트 커버리지 없이 내버려두는 것은 너무 위험하다. 이러한 코드는 많은 사람이 단위 테스트로 어려움을 겪는 주요 원인 중 하나다. 이 딜레마를 우회할 수 있는지에 대해 알아보자. 때때로 실제 구현이 까다로울 수 있지만, 지나치게 복잡한 코드를 알고리즘과 컨트롤러 라는 두 부분으로 나누는 것이 일반적이다.

     

    헙블 객체 패턴

    테스트 대상 코드의 로직을 테스트하려면, 테스트가 가능한 부분을 추출해야 한다. 결과적으로 코드는 테스트 가능한 부분을 둘러싼 얇은 험블 래퍼(humble wrapper)가 된다. 이 험블 래퍼가 테스트하기 어려운 의존성과 새로 추출된 구성 요소를 붙이지만, 자체적인 로직이 거의 없거나 전혀 없으므로 테스트할 필요가 없다.

    육각형 아키텍처와 함수형 아키텍처 모두 정확히 이 패턴을 구현한다.

    험블 객체 패턴을 보는 또 다른 방법은 단일 책임 원칙(Single Responsibility Principle)을 지키는 것이다. 이 패턴을 적용하면 비즈니스 로직을 거의 모든 것과 분리할 수 있다.

     

    비즈니스 로직과 오케스트레이션을 계속 분리해야 하는 이유는 테스트 용이성이 좋아져서만이 아니다. 이렇게 분리하면 코드 복잡도를 해결할 수 있으며, (특히 장기적으로) 프로젝트 성장에도 중요한 역할을 한다. 개인적으로는 항상 테스트 가능한 설계가 어떻게 테스트를 용이하게 할 뿐만 아니라 유지 보수도 쉽게 해주는지를 흥미롭게 생각한다.

     

    가치있는 단위 테스트를 위한 리팩터링


    이번 샘플 프로젝트는 사용자 등록을 처리하는 고객 관리 시스템이며, 모든 사용자가 데이터베이스에 저장된다. 현재 시스템은 사용자 이메일 변경이라는 단 하나의 유스케이스만 지원한다. 이 연산에는 세 가지 비즈니스 규칙이 있다.

    • 사용자 이메일이 회사 도메인에 속한 경우 해당 사용자는 직원으로 표시된다. 그렇지 않으면 고객으로 간주한다.
    • 시스템은 회사의 직원 수를 추적해야 한다. 사용자 유형이 직원에서 고객으로, 또는 그 반대로 변경되면 이 숫자도 변경해야 한다.
    • 이메일이 변경되면 시스템은 메시지 버스로 메세지를 보내 외부 시스템에 알려야 한다.
    public class User
    {
       public int UserId { get; private set; }
     
       public string Email { get; private set; }
       public UserType Type { get; private set; }
     
       public void ChangeEmail(int userId, string newEmail)
       {
           object[] data = Database.GetUserById(userId); // 데이터베이스에서 사용자의 현재 이메일과 유형 검색
           UserId = userId;
           Email = (string) data[1];
           Type = (string) data[2];
     
           if (Email == newEmail)
               return;
     
           object[] companyData = Database.GetCompany(); // 데이터베이스에서 조직의 도메인 이름과 직원 수 검색
           string companyDomainName = (string) companyData[0];
           int numberOfEmployees = (int) companyData[1];
     
           string emailDomain = Database.GetCompany();
           bool isEmailCorporate = emailDomain == companyDomainName;
           UserType newType = isEmailCorporate ? UserType.Employee : UserType.Customer; // 새 이메일의 도메인 이름에 따라 사용자 유형 설정
     
           if (Type != newType)
           {
               int delta = newType == UserType.Employee ? 1 : -1;
               int newNumber = numberOfEmployees + delta;
               Database.SaveCompany(newNumber); // 필요한 경우 조직의 직원 수 업데이트
           }
     
           Email = newEmail;
           Type = newType;
     
           Database.SaveUser(this); // 데이터베이스에 사용자 저장
           MessageBus.SendEmailChangedMessage(UserId, newEmail); // 메세지 버스에 알림 전송
       }
    }
     
    public enum UserType
    {
       Customer = 1,
       Employee = 2
    }

    User 클래스는 사용자 이메일을 변경한다. 코드 복잡도는 그리 높지 않다. ChangeEmail 메서드에는 사용자를 직원으로 식별할지 또는 고객으로 식별할지와 회사의 직원 수를 어떻게 업데이트할 지 등 두 가지의 명시적 의사 결정 지점만 포함돼 있다. 간단하지만 이러한 결정은 중요하다. 애플리케이션의 핵심 비즈니스 로직이므로, 이 클래스는 복잡도와 도메인 유의성 측면에서 점수가 높다.

    반면에 User 클래스에는 네 개의 의존성이 있으며. 그중 두 개는 명시적이고 나머지 두개는 암시적이다. 명시적 의존성은 userId와 newEmail 인수다. 그러나 이 둘은 값이므로 클래스의 협력자 수에는 포함되지 않는다. 암시적인 것은 Database와 MessageBusdlek. 이 둘은 프로세스 외부 협력자다. 도메인 유의성이 높은 코드에서 프로세스 외부 협력자는 사용하면 안 된다. 따라서 User 클래스는 협력자 측면에서도 점수가 높으므로 이 클래스는 지나치게 복잡한 코드로 분류된다.

    도메인 클래스가 스스로 데이터베이스를 검색하고 다시 저장하는 이러한 방식을 활성 레코드(Active Record) 패턴이라고 한다. 단순한 프로젝트나 단기 프로젝트에서는 잘 작동하지만 코드베이스가 커지면 확장하지 못하는 경우가 많다. 그 이유는 정확히 두 가지 책임, 즉 비즈니스 로직과 프로세스 외부 의존성과의 통신 사이에 분리가 없기 때문이다.

     

    1단계: 암시적 의존성을 명시적으로 만들기

    테스트 용이성을 개선하는 일반적인 방법은 암시적 의존성을 명시적으로 만드는 것이다. 즉, 데이터베이스와 메시지 버스에 대한 인터페이스를 두고, 이 인터페이스를 User에 주입한 후 테스트에서 목으로 처리한다. 하지만 복잡한 목 체계가 필요한데, 여기서 테스트 유지비가 증가한다. 결국 도메인 모델은 직접적으로든 간접적으로든(인터페이스를 통해) 프로세스 외부 협력자에게 의존하지 않는 것이  훨씬 더 깔끔하다.

     

    2단계: 애플리케이션 서비스 계층 도입

    도메인 모델이 외부 시스템과 직접 통신하는 문제를 극복하려면 다른 클래스인 험블 컨트롤러(humble controller)로 책임을 옮겨야 한다.

    public class UserController
    {
       private readonly Database _database = new Database();
       private readonly MessageBus _messageBus = new MessageBus();
     
       public void ChangeEmail(int userId, string newEmail)
       {
           object[] userData = _database.GetUserById(userId);
           string email = (string)data[1];
           UserType type = (UserType)data[2];
           var user = new User(userId, email, type);
     
           object[] companyData = _database.GetCompany();
           string companyDomainName = (string)companyData[0];
           int numberOfEmployees = (string)companyData[1];
     
           int newNumberOfEmployees = user.ChangeEmail(newEmail, companyDomainName, numberOfEmployees);
     
           _database.SaveCompany(newNumberOfEmployees);
           _database.SaveUser(user);
           _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail);
       }
    }

    괜찮은 첫 시도다. User 클래스로부터 프로세스 외부 의존성과의 작업을 줄이는 데 애플리케이션 서비스가 도움이 됐다. 그러나 이 구현에는 몇 가지 문제가 있다.

    • 프로세스 외부 의존성(Database와 MessageBus)이 주입되지 않고 직접 인스턴스화된다. 이는 이 클래스를 위해 작성할 통합 테스트에서 문제가 될 것이다.
    • 컨트롤러는 데이터베이스에서 받은 원시 데이터를 User 인스턴스로 재구성한다. 이는 복잡한 로직이므로 애플리케이션 서비스에 속하면 안된다. 애플리케이션 서비스의 역할은 복잡도나 도메인 유의성의 로직이 아니라 오케스트레이션만 해당한다.
    • 회사 데이터도 마찬가지다. User는 이제 업데이트된 직원 수를 반환하는데, 회사 직원 수는 특정 사용자와 관련이 없다. 이 책임은 다른곳에 있어야 한다.
    • 컨트롤러는 새로운 이메일이 전과 다른지 여부와 관계없이 무조건 데이터를 수정해서 저장하고 메세지 버스에 알림을 보낸다.

     

    User 클래스는 더 이상 프로세스 외부 의존성과 통신할 필요가 없으므로 테스트하기가 매우 쉬워졌다. 실제로 프로세스 외부든 내부든 어떤 협력자도 없다. User의 ChangeEmail 메서드의 새로운 버전은 다음과 같다.

       public int ChangeEmail(string newEmail, string companyDomainName, int numberOfEmployees)
       {
           if (Email == newEmail)
               return newNumberOfEmployees;
     
           string emailDomain = newEmail.Split('@')[1];
           bool isEmailCorporate = emailDomain == companyDomainName;
           UserType newType = isEmailCorporate ? UserType.Employee : UserType.Customer;
          
           object[] data = Database.GetUserById(userId);
           UserId = userId;
           Email = (string) data[1];
           Type = (string) data[2];
     
           if (Type != newType)
           {
               int delta = newType == UserType.Employee ? 1 : -1;
               int newNumber = numberOfEmployees + delta;
               numberOfEmployees = newNumber;
           }
     
           Email = newEmail;
           Type = newType;
     
           return numberOfEmployees;
       }

    User는 더 이상 협력자를 처리할 필요가 없기 때문에 도메인 모델 사분면으로 수직축에 가깝게 이동했다. 하지만 UserController가 문제다. 컨트롤러 사분면에 들어갔지만, 아직 로직이 꽤 복잡하므로 지나치게 복잡한 코드의 경계에 걸쳐 있다.

    3단계: 애플리케이션 서비스 복잡도 낮추기

    UserController에서 재구성 로직을 추출해야 한다. ORM(Object-Relational  Mapping) 라이브러리를 사용해 데이터베이스를 도메인 모델에 매핑하면, 재구성 로직을 옮기기에 적절한 위치가 될 수 있다. 또는 도메인 모델에 원시 데이터베이스 데이터로 도메인 클래스를 인스턴스화하는 팩토리 클래스를 작성하라.

    public class UserFactory
    {
       public static User Create(object[] data)
       {
           Precondition.Requires(data.Length >= 3);
     
           int id = (int)data[0];
           string email = (string)data[1];
           UserType type = (UserType)data[2];
     
           return new User(id, email, type);
       }
    }

    이 코드는 이제 모든 협력자와 완전히 격리돼 있으므로 테스트가 쉬워졌다.

     

    4단계: 새 Company 클래스

    컨트롤러 코드를 다시 한 번 살펴보자.

    object[] companyData = _database.GetCompany();
    string companyDomainName = (string)companyData[0];
    int numberOfEmployees = (string)companyData[1];
     
    int newNumberOfEmployees = user.ChangeEmail(newEmail, companyDomainName, numberOfEmployees);

    User에서 업데이트된 직원 수를 반환하는 부분이 어색하다. 이는 책임을 잘못 뒀다는 시놓이자 추상화가 없다는 신호다. 이 문제를 해결하려면, 다음과 같이 회사 관련 로직과 데이터를 함께 묶는 또 다른 도메인 클래스인 Company를 만들어야 한다.

    public class Company
    {
       public string DomainName { get; private set; }
       public int numberOfEmployees { get; private set; }
     
       public void ChangeNumberOfEmployees(int delta)
       {
           Precondition.Requires(numberOfEmployees + delta >= 0);
           numberOfEmployees += delta;
       }
     
       public bool isEmailCorporate(string email)
       {
           string emailDomain = email.Split('@')[1];
           return emailDomain == DomainName;
       }
    }

    이 클래스에는 ChangeNumberOfEmployees()와 IsEmailCorporate()라는 두 가지 메서드가 있다 이러한 메서드는 ‘묻지 말고 말하라(tell-don’t-ask)라는 원칙을 준수하는데 도움이 된다. 이 원칙을 따르면 데이터와 해당 데이터에 대한 작업을 묶는다. User 인스턴스는 직원 수를 변경하거나 특정 이메일이 회사 이메일인지 여부를 파악하도록 회사에 말하며, 원시 데이터를 묻지 않고 모든 작업을 자체적으로 수행한다.

     

    컨트롤러는 이제 다음과 같다.

    public class UserController
    {
       private readonly Database _database = new Database();
       private readonly MessageBus PmessageBus = new MessageBus();
     
       public void ChangeEmail(int userId, string newEmail)
       {
           object[] userData = _database.GetUserById(userId);
           User user = UserFactory.Create(userData);
     
           object[] companyData = _database.GetCompany();
           Company company = CompanyFactory.Create(companyData);
     
           user.ChangeEmail(newEmail, company);
     
           _database.SaveCompany(company);
           _database.SaveUser(user);
           _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail);
       }
    }

    User 클래스는 다음과 같다.

    public class User
    {
       public int UserId { get; private set; }
     
       public string Email { get; private set; }
       public UserType Type { get; private set; }
     
       public void ChangeEmail(string newEmail, Company company)
       {
           if (Email == newEmail)
               return;
           UserType newType = isEmailCorporate ? UserType.Employee : UserType.Customer;
     
           if (Type != newType)
           {
               int delta = newType == UserType.Employee ? 1 : -1;
               int newNumber = numberOfEmployees + delta;
               numberOfEmployees = newNumber;
           }
          
           Email = newEmail;
           Type = newType;
       }
    }

    잘못 둔 책임을 제거하니 User가 훨씬 더 깔끔해졌다. 회사 데이터를 처리하는 대신 Company 인스턴스를 받아, 이메일이 회사 이메일인지 결정하는 것과 회사의 직원 수를 변경하는 것, 이 두 가지 중요한 작업을 해당 인스턴스에 위임한다.

    팩토리와 두 도메인 클래스는 모두 도메인 모델과 알고리즘 사분면에 속한다. User에 협력자가 이전에는 없었지만 지금은 하나(Company) 있기 때문에 User가 오른쪽으로 이동했다. 이로써 User를 테스트하기 어려워졌지만, 많이 어려워진 것은 아니다. 이제 모든 복잡도가 팩토리로 이동했기 때문에 UserController는 확실히 컨트롤러 사분면에 속한다. 이 클래스가 담당하는 것은 모든 협력자를 한데 모으는 것이다.

     

      협력자가 거의 없음 협력자 많음
    복잡도와 도메인 유의성이 높음 User의 ChangeEmail(newEmail, company),
    Company의 ChangeNumberOfEmployees(delta)와 isEmailCorporate(email)
     
    복잡도와 도메인 유의성이 낮음 User와 Company의 생성자 UserController의 ChangeEmail(userId, newMail)

    좌측 상단 테스트 메서드는 비용 편익 측면에서 최상의 결과를 가져다준다. 코드의 복잡도나 도메인 유의성이 높으면 회귀 방지가 뛰어나고 협력자가 거의 없어 유지비도 가장 낮다. 다음은 User를 어떻게 테스트하는지에 대한 예이다.

    [Fact]
    public void Changing_email_from_non_corporate_to_corporate()
    {
       var company = new Company("mycorp.com", 1);
       var sut = new User(1, "user@gmail.com", UserType.Customer);
     
       sut.ChangeEmail("new@mycorp.com", company);
     
       Assert.Equal(2, company.numberOfEmployees);
       Assert.Equal("new@mycorp.com", sut.Email);
       Assert.Equal(UserType.Employee, sut.Type);
    }

    전체 커버리지를 달성하려면, 다음과 같이 테스트 세 개가 더 필요하다.

    public void Changing_email_from_corporate_to_non_corporate()

    public void Changing_email_without_changing_user_type()

    public void Changing_email_to_the_same_one()

     

    전제 조건을 테스트해야 하는가?

    특별한 종류의 분기점(전제 조건)을 살펴보고 이를 테스트해야 하는지 확인해보자. 예를 들어 Company에 있는 이 메서드를 다시 한번 살펴본다.

    public void ChangeNumberOfEmployees(int delta)
    {
           Precondition.Requires(numberOfEmployees + delta >= 0);
           numberOfEmployees += delta;
    }

    회사의 직원 수가 음수가 돼서는 안된다는 전제 조건이 있다. 이 전제 조건은 예외 상황에서만 활성화되는 보호 장치다. 이러한 예외 상황은 보통 버그의 결과다. 직원 수가 0 미만으로 내려가는 까닭은 코드에 오류가 있는 경우뿐이다. 이 전제 조건을 테스트할 가치가 있는가?

    여기에 어려운 규칙은 없지만, 일반적으로 권장하는 지침은 도메인 유의성이 있는 모든 전제 조건을 테스트하라는 것이다. 직원 수가 음수가 되면 안 된다는 요구 사항이 이러한 전제 조건에 해당한다. 이는 Company 클래스의 불변성(항상 true여야 하는 조건)에 해당한다. 그러나 도메인 유의성이 없는 전제 조건을 테스트하는 데 시간을 들이지 말라. 예를 들어 UserFactory의 Create 메서드에 다음과 같은 보호 장치가 있다.

    Precondition.Requires(data.Length >= 3);

    이 전제 조건에 도메인 의미가 없으므로 테스트하기에는 별 가치가 없다.

     

    컨트롤러에서 조건부 로직 처리


    비즈니스 로직과 오케스트레이션의 분리는 다음과 같이 비즈니스 연산이 세 단계로 있을 때 가장 효과적이다.

    • 저장소에서 데이터 검색
    • 비즈니스 로직 실행
    • 데이터를 다시 저장소에 저장

    그러나 이렇게 단계가 명확하지 않은 경우가 많다. 의사 결정 프로세스의 중간 결과를 기반으로 프로세스 외부 의존성에서 추가 데이터를 조회해야 할 수도 있다. 이러한 상황에서는 다음과 같이 세 가지 방법이 있다.

    • 어쨋든 외부에 대한 모든 읽기와 쓰기를 가장자리로 밀어낸다. 이 방법은 필요 없는 경우에도 컨트롤러가 프로세스 외부 의존성을 호출하기 때문에 성능이 저하된다.
    • 도메인 모델에 프로세스 외부 의존성을 주입하고 비즈니스 로직이 해당 의존성을 호출할 시점을 직접 결정할 수 있게 한다.
    • 의사 결정 프로세스 단계를 더 세분화하고, 각 단계별로 컨트롤러를 실행하도록한다.

    문제는 다음 세 가지 특성의 균형을 맞추는 것이다.

    • 도메인 모델 테스트 유의성: 도메인 클래스의 협력자 수와 유형에 따른 함수
    • 컨트롤러 단순성: 의사 결정(분기) 지점이 있는지에 따라 다름
    • 성능: 프로세스 외부 의존성에 대한 호출 수로 정의

    위에서 언급한 방법은 세 가지 특성 중 두 가지 특성만 갖는다.

    대부분의 소프트웨어 프로젝트에서는 성능이 매우 중요하므로 첫 번째 방법(외부에 대한 읽기와 쓰기를 비즈니스 연산 가장자리로 밀어내기)은 고려할 필요가 없다. 두 번째 옵션(도메인 모델에 프로세스 외부 의존성 주입하기)은 대부분 코드를 지나치게 복잡한 사분면에 넣는다. 그러면 세 번째 옵션(의사 결정 프로세스 단계를 더 세분화하기)만 남게 된다. 이 방식을 쓰면 컨트롤러를 더 복잡하게 만들기 때문에 지나치게 복잡한 사분면에 더 가까워지게 된다. 그러나 이 문제를 CanExecute/Execute 패턴을 사용하면 완화할 수 있다. 

    Do() 메서드에 대해 CanDo()를 두고, CanDo()가 성공적으로 실행되는 것을 Do()의 전제 조건으로 한다. 이 패턴은 Do() 전에 CanDo()를 호출하지 않을 수 없기 때문에 컨트롤러의 의사 결정을 근본적으로 제거한다.

     

    샘플 프로젝트를 확장해 이메일은 사용자가 확인할 때까지만 변경할 수 있다고 하자. 사용자가 확인한 후에 이메일을 변경하려고 하면 오류 메세지가 표시돼야 한다. 이 새로운 요구사항을 담고자 User 클래스에 새 속성을 추가한다.

    public class User
    {
       public int UserId { get; private set; }
     
       public string Email { get; private set; }
       public UserType Type { get; private set; }
       public bool IsEmailConfirmed { get; private set; } // 새 속성
       /* ChangeEmail(newEmail, company) 메서드 */
    }

    IsEmailConfirmed 확인을 User에서 컨트롤러로 옮기자.

    public class UserController
    {
       private readonly Database _database = new Database();
       private readonly MessageBus PmessageBus = new MessageBus();
     
       public string ChangeEmail(int userId, string newEmail)
       {
           object[] userData = _database.GetUserById(userId);
           User user = UserFactory.Create(userData);
     
           string error = user.CanChangeEmail();
           if (error != null)
               return error;
     
           object[] companyData = _database.GetCompany();
           Company company = CompanyFactory.Create(companyData);
     
           user.ChangeEmail(newEmail, company);
     
           _database.SaveCompany(company);
           _database.SaveUser(user);
           _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail);
          
           return "OK";
       }
    }

    이러한 구현으로 성능은 그대로 유지된다. Company 인스턴스는 확실히 이메일 변경이 가능한 후에만 데이터베이스에서 검색된다. User에 새 메서드(CanChangeEmail() 메서드)를 둬서, 이 메서드가 잘 실행되는 것을 이메일 변경의 전제 조건으로 한다.

    public class User
    {
       public int UserId { get; private set; }
     
       public string Email { get; private set; }
       public UserType Type { get; private set; }
       public bool IsEmailConfirmed { get; private set; } // 새 속성
       /* ChangeEmail(newEmail, company) 메서드 */
     
       public void ChangeEmail(string newEmail, Company company)
       {
           Precondition.Requires(CanChangeEmail() == null);
          
           if (Email == newEmail)
               return;
           UserType newType = isEmailCorporate ? UserType.Employee : UserType.Customer;
     
           if (Type != newType)
           {
               int delta = newType == UserType.Employee ? 1 : -1;
               int newNumber = numberOfEmployees + delta;
               numberOfEmployees = newNumber;
           }
          
           Email = newEmail;
           Type = newType;
       }
     
       public string CanChangeEmail()
       {
           if (IsEmailConfirmed)
               return "Can't change a confirmed email";
           return null;
       }
    }

    컨트롤러는 CanChangeEmail() 메서드를 호출해서 연산을 수행할 수 있는지 확인하기만 하면 된다. 이 메서드에 여러가지 유효성 검사가 있을 수 있고, 유효성 검사 모두 컨트롤러로부터 캡슐화되어 있다. 또한, ChangeEmail()의 전제 조건이 추가돼도 먼저 확인하지 않으면 이메일을 변경할 수 없도록 보장한다.

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    댓글

Designed by Tistory.