모듈 간 계약 설계
모듈 간 계약 설계Design by Contract, DBC는 소프트웨어 모듈 간의 상호작용을 계약contract으로 정의하여, 각 모듈이 서로에게 제공하는 서비스와 그 서비스의 조건을 명확히 하는 설계 방법론입니다. 이 개념은 1980년대에 Bertrand Meyer가 제안한 이래로 모듈화 설계와 소프트웨어의 신뢰성을 높이는 중요한 설계 원칙으로 자리 잡았습니다. DBC는 특히 객체지향 프로그래밍과 결합하여 모듈 간의 상호작용을 구조화하고, 명확한 인터페이스와 계약을 통해 오류를 예방하는 데 도움을 줍니다.
DBC의 주요 개념
모듈 간 계약 설계에서는 세 가지 핵심 개념을 사용하여 모듈 간의 관계를 정의합니다:
전제 조건
전제 조건Preconditions은 모듈이나 함수가 실행되기 전에 반드시 충족되어야 할 조건을 의미합니다. 즉, 클라이언트가 호출하는 시점에 제공해야 하는 요구사항 입니다. 전제 조건은 주로 입력 값이나 상태에 대한 확인으로, 계약 위반 시 해당 모듈이 정상적으로 작동할 수 없음을 나타냅니다.
public class LibraryService
{
public void BorrowBook(User user, Book book)
{
if (user == null || book == null)
throw new ArgumentNullException("User or book cannot be null");
if (user.BooksBorrowed >= 5)
throw new InvalidOperationException("User cannot borrow more than 5 books");
if (book.IsBorrowed)
throw new InvalidOperationException("Book is already borrowed");
// 대출 로직 실행
book.Borrow();
}
}
BorrowBook
메서드 호출 전user
와book
이 null이 아니며, 사용자가 5권 이상의 도서를 대출하지 않았는지, 도서가 이미 대출되었는지 여부를 전제 조건으로 확인합니다.
사후 조건
사후 조건Postconditions은 모듈이나 함수가 실행된 후에 반드시 성립해야 할 조건을 의미합니다. 즉, 작업이 완료된 후 시스템의 상태가 어떻게 변해야 하는지를 정의합니다. 이는 계약에서 약속된 결과로, 클라이언트는 이 조건을 통해 모듈이 올바르게 작동했는지 확인할 수 있습니다. 예시:
public class LibraryService
{
public void BorrowBook(User user, Book book)
{
// 전제 조건 확인
if (user == null || book == null)
throw new ArgumentNullException("User or book cannot be null");
// 비즈니스 로직 처리
book.Borrow();
// 사후 조건 확인
if (!book.IsBorrowed)
throw new InvalidOperationException("Book should be marked as borrowed");
}
}
- 책이 대출된 후
book.IsBorrowed
상태가true
로 설정되었음을을 확인하여 대출 로직이 성공적으로 실행되었는지 확인할 수 있습니다.
불변 조건
불변 조건Invariants은 시스템의 상태가 항상 유지되어야 하는 속성을 정의합니다. 불변 조건은 객체의 일관성을 보장하는 데 사용되며, 메서드 실행 전후로 변하지 않는 조건을 나타냅니다. 이는 객체의 중요한 규칙을 표현하며, 모든 메서드 실행 중에도 일관성을 유지해야 합니다.
public class User
{
public int BooksBorrowed { get; private set; }
public bool HasOverdueBooks { get; private set; }
public void BorrowBook(Book book)
{
if (HasOverdueBooks)
throw new InvalidOperationException("User cannot borrow books with overdue books");
BooksBorrowed++;
}
public void ReturnBook(Book book)
{
BooksBorrowed--;
if (BooksBorrowed < 0)
throw new InvalidOperationException("BooksBorrowed cannot be negative");
}
}
User
클래스의 불변 조건은BooksBorrowed
가 음수로 내려가지 않는다는 것입니다.- 이 조건은 시스템이 올바르게 동작하는 동안 항상 유지되어야 하며, 비즈니스 로직이 이를 위반하지 않도록 보장합니다.
DBC의 장점
명확한 계약 정의: 모듈 간 계약을 통해 각 모듈이 기대하는 입력과 출력 조건을 명확하게 정의할 수 있으며, 이를 통해 모듈 간 상호작용의 오류를 줄일 수 있습니다.
버그 예방: 전제 조건, 사후 조건, 불변 조건을 명시적으로 정의함으로써 잘못된 입력이나 출력으로 인한 오류를 사전에 예방할 수 있습니다.
유지보수성 향상: 모듈 간의 계약이 명확하게 정의되면, 코드 변경 시에도 계약을 기반으로 시스템이 올바르게 동작하는지 쉽게 검증할 수 있습니다.
DBC의 한계
- 복잡성 증가: 전제 조건, 사후 조건, 불변 조건을 모두 정의하고 검증하는 과정은 코드의 복잡성을 증가시킬 수 있으며, 개발 초기 단계에서는 다소 부담이 될 수 있습니다.
- 성능 저하: 조건 검사가 많아질수록 런타임 성능에 영향을 미칠 수 있습니다. 특히, 불필요한 조건 검사를 반복적으로 수행하면 시스템 성능이 저하될 가능성이 있습니다.
맺음말
모듈 간 계약 설계는 소프트웨어 시스템의 신뢰성과 유지보수성을 향상시키는 데 중요한 역할을 합니다. 전제 조건, 사후 조건, 불변 조건을 통해 모듈 간 상호작용을 명확하게 정의하고, 각 모듈이 맡은 역할과 책임을 명확히 함으로써 오류를 예방할 수 있습니다. 비록 초기 설계 단계에서 다소 복잡하게 느껴질 수 있지만, 장기적으로는 코드 품질을 유지하고 버그를 줄이는 데 큰 도움이 됩니다.
심화 학습
도메인 특화된 계약 설계
심화 학습의 첫 번째 단계는 DBC를 도메인에 특화된 방식으로 설계하는 것입니다. 즉, 전제 조건이나 불변 조건을 비즈니스 로직과 도메인 규칙에 맞게 설정하여 도메인 문제를 해결하는 데 적합한 계약을 정의하는 것입니다.
예시: 도서관 관리 시스템의 도메인 특화된 계약
public class LibraryService
{
private const int MaxBooksAllowed = 5;
public void BorrowBook(User user, Book book)
{
if (user == null || book == null)
throw new ArgumentNullException("User or book cannot be null");
// 도메인 특화된 계약: 사용자가 최대 5권의 책만 빌릴 수 있음
if (user.BooksBorrowed >= MaxBooksAllowed)
throw new InvalidOperationException("User cannot borrow more than 5 books");
if (book.IsBorrowed)
throw new InvalidOperationException("Book is already borrowed");
// 비즈니스 로직 실행
book.Borrow();
}
}
- 이 예시는 도메인 특화된 계약 설계의 전형적인 예입니다.
- 도서관의 비즈니스 규칙에 맞춰 사용자가 최대 5권까지 대출할 수 있으며, 이를 전제 조건으로 명시하여 비즈니스 로직의 일관성을 유지합니다.
런타임 계약 검증
DBC는 종종 컴파일 타임에 확인할 수 없기 때문에, 런타임에서 전제 조건과 사후 조건을 검증하는 메커니즘을 사용해야 합니다. 심화 학습에서는 이러한 런타임 검증이 성능에 미치는 영향을 줄이고, 필요할 때만 계약을 검증할 수 있는 전략을 사용합니다.
예시: 런타임 조건 검증과 성능 최적화
public class LibraryService
{
private const int MaxBooksAllowed = 5;
public void BorrowBook(User user, Book book)
{
if (Debugger.IsAttached)
{
// 디버깅 모드에서만 조건 검증
if (user.BooksBorrowed >= MaxBooksAllowed)
throw new InvalidOperationException("User cannot borrow more than 5 books");
if (book.IsBorrowed)
throw new InvalidOperationException("Book is already borrowed");
}
// 비즈니스 로직 실행
book.Borrow();
}
}
- 이 코드는 디버깅 모드에서만 계약을 검증하는 방식입니다.
- 이를 통해 개발 환경에서 철저히 검증하되, 운영 환경에서는 성능 저하를 최소화할 수 있습니다.
계약 설계와 테스트 주도 개발
DBC와 테스트 주도 개발TDD를 결합하여 더욱 강력한 품질 보장 메커니즘을 만들 수 있습니다. TDD는 계약을 기반으로 테스트 케이스를 작성하고, 이를 통해 계약을 검증하는 방식입니다.
예시: DBC 기반의 테스트 케이스
[TestClass]
public class LibraryServiceTests
{
[TestMethod]
public void BorrowBook_WhenUserExceedsMaxBooks_ShouldThrowException()
{
var user = new User(booksBorrowed: 5); // 이미 5권을 대출한 상태
var book = new Book(1, "Clean Code");
var libraryService = new LibraryService();
Assert.ThrowsException<InvalidOperationException>(() =>
{
libraryService.BorrowBook(user, book);
});
}
[TestMethod]
public void BorrowBook_WhenBookIsAlreadyBorrowed_ShouldThrowException()
{
var user = new User(booksBorrowed: 2);
var book = new Book(1, "Refactoring");
book.Borrow(); // 이미 대출된 책
var libraryService = new LibraryService();
Assert.ThrowsException<InvalidOperationException>(() =>
{
libraryService.BorrowBook(user, book);
});
}
}
- 이 테스트 코드에서는 사용자가 최대 대출 권수를 초과하거나 이미 대출된 책을 대출하려고 할 때 예외가 발생하는지 확인합니다. 이는 DBC에 정의된 계약을 테스트하는 과정입니다.
계약 위반 처리 전략
심화 학습에서는 계약 위반이 발생했을 때의 처리 전략도 중요합니다. 계약 위반 시 즉시 예외를 발생시키는 전통적인 방식 외에도, 상황에 따라 계약 위반을 로그로 기록하거나 특정 작업을 시도하는 방식으로 처리할 수 있습니다.
예시: 계약 위반 시 로그 기록
public class LibraryService
{
private const int MaxBooksAllowed = 5;
private readonly ILogger _logger;
public LibraryService(ILogger logger)
{
_logger = logger;
}
public void BorrowBook(User user, Book book)
{
try
{
if (user.BooksBorrowed >= MaxBooksAllowed)
throw new InvalidOperationException("User cannot borrow more than 5 books");
if (book.IsBorrowed)
throw new InvalidOperationException("Book is already borrowed");
book.Borrow();
}
catch (Exception ex)
{
_logger.LogError($"Contract violation: {ex.Message}");
throw;
}
}
}
- 이 예시는 계약 위반 시 예외를 발생시키는 동시에, 로그를 통해 계약 위반 정보를 기록하여 문제를 추적할 수 있도록 합니다.
불변 조건의 동적 업데이트
불변 조건은 시스템의 특정 속성이 항상 일정하게 유지됨을 보장하지만, 때로는 도메인의 변화에 맞춰 동적으로 불변 조건을 업데이트해야 할 때가 있습니다. 심화 학습에서는 이러한 동적 불변 조건 관리를 다루며, 특정 상황에서 불변 조건을 수정하는 방법을 제시합니다.
예시: 불변 조건의 동적 관리
public class LibraryService
{
private int _maxBooksAllowed = 5;
public void SetMaxBooksAllowed(int maxBooks)
{
if (maxBooks <= 0)
throw new ArgumentOutOfRangeException("Max books allowed must be greater than 0");
_maxBooksAllowed = maxBooks;
}
public void BorrowBook(User user, Book book)
{
if (user.BooksBorrowed >= _maxBooksAllowed)
throw new InvalidOperationException($"User cannot borrow more than {_maxBooksAllowed} books");
book.Borrow();
}
}
SetMaxBooksAllowed
메서드는 도메인의 변화에 따라 동적으로 불변 조건을 업데이트할 수 있는 기능을 제공합니다. 이를 통해 도메인의 규칙이 바뀌었을 때 시스템이 유연하게 대응할 수 있습니다.