서론
좋은 코드의 조건을 다양합니다. 하지만 "표현이 직관적이며 동작이 예상 가능한 코드"가 좋은 코드라는 것에는 이견이 없을 것이라 생각합니다. 동작이 예상 가능한 코드라는 표현을 달리 말하면 오용하기 어려운 코드라고 할 수 있습니다. 오늘 주제는 코드의 제약 조건에 의해 발생하는 오용 가능성과 이런 제약 조건을 핸들링해서 오용 가능성을 좁히는 내용입니다.
(이 내용은 좋은 코드, 나쁜 코드에 수록된 내용을 정리한 것임을 밝힙니다.)
제약 조건이 있는 코드
오용하기 쉬운 코드는 암묵적 제약 조건이 많은 코드라고 할 수 있습니다. 제약 조건에 대한 이해가 있어야 제대로 사용할 수 있는 코드라는 말은 자칫 잘못 사용되기 쉬운 코드라는 말이 됩니다. 다음의 예제를 통해 코드에서 발생하 수 있는 제약조건에 대해 알아보겠습니다.
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
/**
* 입금
* @param 임금액, 0보다 큰 금액을 입금한다.
*/
deposit(amount: number): void {
this.balance += amount;
}
/**
* 인출
* @param amount는 balance보다 클 수 없다.
*/
withdraw(amount: number): void {
this.balance -= amount;
}
/**
* 잔액 조회
* @return 0보다 크거나 같은 양수, 음수를 입금하거나 잔액보다 큰 금액을 인출할 수 없기 때문에 항상 양수를 반환해야 한다.
*/
getBalance(): number {
return this.balance;
}
}
// 100 예금
const account = new BankAccount(100);
account.deposit(50); // 50예금
account.withdraw(30); // 30인출
console.log(account.getBalance()); // 잔액 120
위 BankAccount 클래스에는 몇 가지 문제가 있습니다.
- 잔액(balance)를 음수로 설정할 수 있다.
- deposit(입금)에 음수가 들어올 수 있다.
- 인출에 잔액보다 더 큰 금액이 들어올 수 있다.
이런 암묵적 조건들을 선결 조건, 사후조건으로 나눠 볼 수 있습니다.
선결 조건
코드를 호출하기 전에 사실이어야 하는 조건, (eg. 함수의 매개변수 조건)
사후 조건
코드가 호출된 후에 사실어이야 하는 조건, (eg. 실행 결과에 대한 조건, 반환값이 데이터 형식 등)
위 코드에서는 deposit의 매개변수가 양수이어야 한다는 조건, withdraw의 매개변수는 balance보다 클 수 없다는 조건은 선결 조건에 해당하고 getBalance의 반환값이 양수이어야 한다는 사후 조건이 암묵적으로 설정되어 있습니다.
이런 제약 조건은 사용자가 각 메서드의 선결 조건과 사후 조건에 대한 충분한 이해가 필요하기 때문에 코드 사용자의 오용 가능성을 제시합니다. 제약 조건을 문서화를 통해 명시할 수 있지만 문서 또한 업데이트가 필요한 관리 대상이며 코드의 복잡도를 해결하는 방법은 아니기 때문에 이상적 해결책이 될 수 없습니다.
제약 조건 관리하기
좀 더 실생활에 가까운 예시로 새로운 브라우저가 있다고 생각해보겠습니다.
그런데 놀랍게도 이 브라우저에서 탭을 10개 이상 띄우면 "브라우저에 탭을 10개 이상 띄우면 메모리 관리를 위해 브라우저를 강제 종료합니다" 메세지와 함께 브라우저가 갑자기 종료됩니다. 그리고 이 상황을 개발자에게 제보하자 "이 부분은 저희 브라우저의 제약 사항이며 이 부분에 대해 공식 홈페이지와 설치시 약관에 안내하고 있습니다"라는 답변을 들었다고 생각해보겠습니다. 제약 조건이 많은 공통 모듈을 사용하는 동료의 마음이 이 상황과 크게 다르지 않을 것이라고 생각합니다.
컴파일타임 검사
제약 조건이 많은 모듈은 사용하기 어렵고 자칫 오용으로 인해 브라우저가 갑자기 종료되는 상황과 비슷한 상황을 목도해야합니다. 좀 더 좋은 사용성을 위해서 해야할 일은 제약 조건을 최대한 줄여서 오용을 어렵게 만드는 일입니다. 제약 조건을 줄이는 방법으로 컴파일러를 생각해볼 수 있습니다. 컴파일러를 통해 잘못 사용된 코드가 컴파일 에러로 노출된다면 오용 가능성을 사전 차단할 수 있습니다. 앞선 예시가 타입스크립트로 쓰여졌지만 메서드에 대한 타입 정의가 없는 상황을 상상해보면 타입스크립트의 컴파일러로 인해 오용가능성이 줄어들었다는 것을 알 수 있습니다.
런타임 검사
제약 조건을 노출하기 위한 또 다른 방법으로는 런타임 검사가 있습니다.
위 예제에서는 입출력에 대한 타입을 타입스크립트로 명시하면서 코드의 오용 범위를 좁혔지만 아직 제약 조건이 남아있는 상황입니다. 이를 런타임 검사로 개선할 수 있으며 이를 위해 몇 가지 수정이 필요합니다.
아래 코드에서는 명시적으로 입출력값에 대한 조건을 검사하면서 함수의 오용 가능성을 차단하고 있습니다.
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.validateNonNegative(initialBalance, "Initial balance");
this.balance = initialBalance;
}
deposit(amount: number): void {
this.validatePositive(amount, "Deposit amount");
this.balance += amount;
this.validateNonNegative(this.balance, "Balance after deposit");
}
withdraw(amount: number): void {
this.validatePositive(amount, "Withdrawal amount");
if (amount > this.balance) {
throw new Error("Insufficient funds");
}
this.balance -= amount;
this.validateNonNegative(this.balance, "Balance after withdrawal");
}
getBalance(): number {
return this.balance;
}
private validatePositive(value: number, name: string): void {
if (value <= 0) {
throw new Error(`${name} must be positive`);
}
}
private validateNonNegative(value: number, name: string): void {
if (value < 0) {
throw new Error(`${name} cannot be negative`);
}
}
}
try {
const account = new BankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance()); // 120
// 주석 해제하면 예외 발생: Insufficient funds
// account.withdraw(200);
} catch (error) {
console.error(error.message);
}
이제 계좌에 대한 입금, 출금의 오사용 가능성을 런타임 검사를 통해 검증했고 개발 단계나 테스트 단계에서 오류에 대한 조기 검증 가능성을 높일 수 있었습니다.
하지만 여전히 한계점이 존재하는데 검사가 런타임에 이뤄지기 때문에 조건이 위배되는 상황의 확인이 늦고 코드를 실행하는 환경에 따라 오류가 발견되지 않을 수 있기 때문이입니다. 그럼에도 불구하고 정적 타입 확인을 통한 검증이 제한적(잔액보다 큰 금액을 인출해도 컴파일 에러가 나지 않음)이기 때문에 구체적 조건을 명시할 수 없고 오류의 조기 검증 가능성을 높였다는 점에서 없는 것보다는 낫다고 생각합니다.
정리
로직에 제약 조건이 발생하는 상황과 그로 인한 모듈의 오용 사례를 막기 위한 방법으로 컴파일 타임확인과 런타임 확인을 통해 오용 가능성을 줄이는 개선에 대해 알아봤습니다.
하지만 특히 런타임 확인의 경우 자칫 성능 이슈로 연결될 수 있고 오류에 대한 확인이 늦어 질 수 있다는 점에서 한계점이 존재했습니다. 제약 조건에 대한 처리 방법을 고민하기에 앞서 애당초 제약 조건을 없애서 코드 사용자의 인지 부하를 낮추는 방법에 대해 먼저 고민해봐야겠습니다. 간단하게 오용하기 쉬운 코드에 대해서 알아봤는데 불변성, single source of truth등 신경써야 하는 세부 주제들이 남아있습니다. 이 주제에 대해서는 후속 포스팅으로 다뤄보도록 하겠습니다.
'programming' 카테고리의 다른 글
견고한 소프트웨어를 위한 오류 핸들링 (0) | 2024.07.01 |
---|---|
[설계] 단일 책임 원칙에 근거한 모듈 설계 (0) | 2024.06.28 |