단일 접근 원칙(Uniform Access Principle)을 통한 캡슐화, Part 2: C#과 루비로 구현하기
|
 |


연재순서
1회(2009년 7월): 요구사항 변경에 대응하는 기본 원칙, 캡슐화
2회(2009년 8월): C#과 루비로 구현하기
[오픈 developerWorks]는 여러분이 직접 필자로 참가하는 코너입니다.
단일 접근 원칙을 사용하자
은행 계좌 예제가 변경에 취약한 이유는 Account의 balance 속성을 외부에서 직접 변경할 수 있었기 때문이다. 따라서 balance와 관련된 설계 결정을 변경할 경우 public 속성에 의존하는 많은 코드가 연쇄적으로 영향을 받는다. 이를 방지하는 일반적인 방법은 public 메서드를 통해 private 속성을 캡슐화함으로써 파급 효과의 범위를 제한하고, 외부에서 속성의 값에 접근할 필요가 있을 때 값을 반환하는 함수를 추가하는 것이다. 그러나 이러한 방식은 코드에 불필요한 잡음을 추가한다.
Listing 1. Account.java
public class Account {
private long balance;
public Account() {
}
public long getBalance() {
return balance;
}
}
|
언어 차원에서는 public 속성을 사용할 수 있도록 허용하는 반면 프로그래머들에게는 사용하지 않도록 금지하는 것은 언어 설계에 문제점이 있음을 드러내는 것이다. 버트랜드 메이어(Bertrand Meyer)의 말을 인용하자면 오용을 막으려면 주의(care)보다는 금지(prevention)가 더 좋은 방법이다.
그렇다면 자바(Java)에서 속성을 public으로 노출하면 안 되는 이유는 무엇일까? 자바에서 public 가시성을 사용해 속성을 외부로 노출한다는 의미는 속성의 읽기와 쓰기 모두에 있어 제약을 두지 않음을 의미한다. 속성을 읽기 전용으로 설정해 외부에서 이를 변경할 수 없게 할 수 있다면 속성을 외부로 노출한다고 해서 문제가 되지는 않는다. 불행히도 자바에서는 속성에 대한 읽기, 쓰기 특성을 선택적으로 제어할 수 없기 때문에 변경의 영향을 최소화하려면 속성의 가시성을 private로 설정한 후 읽기를 위한 쿼리(query)를 제공하거나 변경을 위한 커맨드(command)를 제공해야 한다.
속성을 직접 노출하는 것은 변경될 여지가 있는 비밀을 감춰야 한다는 정보 은닉 개념을 위반한다. Part 1에서 살펴본 것처럼 외부에서 속성에 직접 접근할 수 있는 경우 함께 수행해야 하는 작업을 추가하거나 저장된 값 방식을 계산된 값 방식으로 변경하기 쉽지 않다. 하지만 속성 대신 커맨드/쿼리를 사용하면 코드의 가독성이 떨어지고 복잡도가 올라간다. customer.mileage += 10에 비해 customer.setMileage(customer.getMileage() + 10)을 이해하는 데 필요한 인지 과부하가 더 크다.
속성의 직관성과 메서드에 의한 캡슐화라는 장점을 함께 취할 수 있는 방법이 없을까? 단일 접근 원칙에 그 해답이 존재한다. 단일 접근 원칙은 Eiffel 언어의 창시자인 버트랜드 메이어가 제안한 설계 원칙 중 하나로 내부 설계 결정에 무관하게 객체의 특징(feature)에 접근할 수 있도록 단일 표기법을 제공하자는 것이다. 특징이란 객체의 속성(attribute)과 함수(function) 모두를 아우르는 포괄적인 개념이다.
모듈에서 제공되는 모든 서비스는 단일 표기법(uniform notation)을 통해 접근 가능해야 하며, 기억 장치에 저장된 값을 사용해 구현되는지 아니면 값을 계산하는지 여부를 누설해서는 안 된다.
-- Bertrand Meyer, Object-Oriented Software Construction 2nd Edition
|
단일 접근 원칙은 단순히 표기법과 관련된 원칙이 아니다. 소프트웨어 설계와 관련된, 그 중에서도 정보 은닉과 캡슐화에 관련된 원칙이다.
단일 접근 원칙을 적용하면 account.balance 같은 직관적인 표기법을 사용해 balance의 값을 참조할 수 있다. 클라이언트 입장에서는 balance가 속성인지(즉 저장된 값인지), 쿼리인지(단순하게 저장된 값을 반환하는 함수인지 아니면 런타임에 값을 계산해 반환하는 함수인지) 여부를 구분할 필요가 없다.
물론 Account를 구현하는 입장에서는 balance를 저장된 값으로 구현할 것인지, 계산된 값으로 구현할 것인지 여부를 결정해야 한다. 그러나 어떤 방식을 선택하든 account.balance와 비슷한 방식의 단일 표기법을 사용할 수 있으므로 설계 결정을 변경하더라도 Account를 사용하는 클라이언트는 영향을 받지 않는다. 클라이언트는 계속해서 계좌 잔액의 값을 참조하기 위해 account.balance 형식을 사용할 수 있다.
불행히도 자바는 단일 접근 원칙을 지원하는 메커니즘을 제공하지 않지만 자바 이후에 출현한 C#이나 최근 주목을 받는 루비(Ruby)는 단일 접근 원칙을 만족시키는 언어적인 특징을 보유하고 있다.
C# 예제
C#의 경우 프로퍼티(property)를 통해 언어 차원에서 단일 접근 원칙을 지원한다. 프로퍼티를 사용하는 클라이언트는 자바의 public 속성과 비슷한 문법을 사용해 Account의 balance 속성에 접근할 수 있다. 그러나 내부적으로는 커맨드나 쿼리를 통해 속성을 캡슐화하기 때문에 설계 변경 시 파급 효과를 최소화할 수 있다.
입출금 목록을 유지하고 저장된 값 방식으로 잔액을 구현하는 C# 코드를 작성해 보자. C#을 사용한 Account 클래스 역시 자바 버전과 마찬가지로 balance의 가시성을 private로 만들어 캡슐화하지만, 함수 대신 프로퍼티를 사용해 계좌 잔액을 노출한다. C# 프로퍼티의 첫 문자는 대문자를 사용하는 것이 관례다. 따라서 C# 버전의 클라이언트는 account.balance가 아니라 account.Balance를 사용해 프로퍼티에 접근한다.
Listing 2. Account.cs
class Account
{
private long balance;
private List withdraws = new List();
private List deposits = new List();
public long Balance
{
get { return balance; }
}
public void withdraw(long amount)
{
withdraws.Add(amount);
balance -= amount;
}
public void deposit(long amount)
{
deposits.Add(amount);
balance += amount;
}
}
|
여기에서 눈여겨 봐야 할 부분은 balance의 값을 얻기 위한 쿼리는 제공하지만 커맨드는 제공하지 않는다는 점이다. 따라서 자바의 속성과 비슷한 방식으로 계좌 잔액을 참조할 수 있도록 하면서도 선택적으로 값을 변경하지 못하게 방지할 수 있다. 따라서 Account 클래스의 상태를 변경하는 유일한 방법은 deposit()와 withdraw() 메서드를 사용하는 것뿐이다. 이처럼 C#의 프로퍼티를 사용하면 속성의 직관적인 표기법과 메서드를 통한 캡슐화의 장점을 동시에 얻을 수 있다.
Listing 3. AccountTest.cs
[Test]
public void deposit() {
Account account = new Account();
account.deposit(3000);
account.deposit(2000);
Assert.AreEqual(5000, account.Balance);
}
[Test]
public void withdraw() {
Account account = new Account();
account.deposit(3000);
account.withdraw(1000);
Assert.AreEqual(2000, account.Balance);
}
|
이제 저장된 값 방식으로 구현된 Balance 프로퍼티를 계산된 값 방식으로 변경해보자. 자바의 경우 balance 속성을 getBalance() 메서드 내부로 캡슐화한 후 balance 속성을 참조하는 클라이언트 코드를 수정해야 했다. 그러나 C#의 경우 이미 프로퍼티를 통해 변경의 파급 효과를 Account 내부로 고립시켜 놓았기 때문에 Balance 프로퍼티에 의존하는 클라이언트 코드를 수정할 필요가 없다.
Listing 4. 계산된 값 방식으로 변경한 Account.cs
class Account
{
private List withdraws = new List();
private List deposits = new List();
public long Balance
{
get
{
long result = 0;
foreach(long withdrawAmount in withdraws) {
result -= withdrawAmount;
}
foreach(long depositAmount in deposits) {
result += depositAmount;
}
return result;
}
}
public void withdraw(long amount)
{
withdraws.Add(amount);
}
public void deposit(long amount)
{
deposits.Add(amount);
}
}
|
이처럼 C#은 프로퍼티라는 언어 차원의 장치를 제공함으로써 단일 접근 원칙을 지원한다. 그러나 이를 위해 get/set/value라는 키워드를 문법에 추가해 언어 자체의 복잡도가 높아지는 결과를 초래했다. 또한 여전히 public속성을 노출하는 것도 가능하다. 따라서 C#의 프로퍼티 방식 역시 언어 차원에서 금지보다는 프로그래머의 주의에 모든 것을 맡기는 소극적인 방식이라고 할 수 있다.
그럼에도 불구하고 정적 타이핑을 지원하는 C#과 같은 주류 언어에서 단일 접근 원칙을 위한 장치를 제공한다는 점은 주목할 만한 발전이라고 할 수 있다. 그러나 진정한 단일 접근 원칙의 신봉자라면 루비를 간과해서는 안 된다.
루비 예제
단일 접근 원칙을 위해 프로퍼티라는 특별한 빌딩 블록을 추가함으로써 복잡도의 길을 선택한 C#과 달리 루비는 언어 자체의 간결함과 메타프로그래밍 기법을 통해 단일 접근 원칙을 만족시킨다.
루비에서 모든 인스턴스 변수는 private다. 루비의 원칙은 자바나 C#과 달리 프로그래머의 주의에 맡기기보다는 언어 차원에서 금지를 통해 코드의 안전성을 추구하는 것이다. 루비의 경우 알골(Algol) 계열 언어에서 함수를 표현하려고 사용하는 필수 요소인 ()를 생략할 수 있다. 따라서 메서드인 account.balance()를 account.balance라고 표기할 수 있다. 또한 =() 함수의 경우에도 ()가 생략 가능하기 때문에 account.balance=(10)을 account.balance = 10으로 표기할 수 있다.
루비로 작성된 모든 문장은 실행문이다. 클래스 선언 역시 실행문의 일종으로 이러한 특징은 루비의 메타프로그래밍 기능을 사용해 실행 시에 클래스를 확장하는 다양한 방법을 제공한다. 메타프로그래밍의 대표적인 예가 attr_reader와 attr_writer, attr_accessor를 사용하는 것으로 속성의 이름을 심볼(symbol)로 전달할 경우 자동으로 private 속성과 커맨드나 쿼리를 클래스 정의에 추가한다.
함수 호출 시에 ()가 생략 가능하다는 특징과 커맨드와 쿼리를 자동 생성할 수 있는 메타프로그래밍 기법을 적용하면 언어 차원에서 간단하게 단일 접근 원칙을 만족시킬 수 있다. 저장된 값 방식을 사용해 계좌 잔액을 관리하는 Account의 루비 버전부터 살펴 보자.
Listing 5. account.rb
class Account
attr_reader :balance
def initialize
@withdraws = []
@deposits = []
@balance = 0
end
def withdraw(amount)
@withdraws.push amount
@balance -= amount
end
def deposit(amount)
@deposits.push amount
@balance += amount
end
end
|
Account를 사용하는 클라이언트는 속성처럼 보이는 account.balance 쿼리를 통해 계좌 잔액에 접근할 수 있다.
Listing 6. account_test.rb
def test_deposit()
account = Account.new
account.deposit(3000);
account.deposit(2000);
assert_equal(5000, account.balance);
end
def test_withdraw()
account = Account.new
account.deposit(3000);
account.withdraw(1000);
assert_equal(2000, account.balance);
end
|
Account의 balance를 저장된 값 방식에서 계산된 값 방식으로 변경할 경우에도 쿼리에 의해 변경 범위가 Account 내부로 고립되므로 클라이언트로 변경이 전파되지 않는다.
Listing 7. 계산된 값 방식으로 바꾼 account.rb
class Account
def initialize
@withdraws = []
@deposits = []
@balance = 0
end
def withdraw(amount)
@withdraws.push amount
end
def deposit(amount)
@deposits.push amount
end
def balance
balance = @deposits.inject(0) {|sum, amount| sum += amount}
@withdraws.inject(balance) {|sum, amount| sum -= amount}
end
end
|
Account의 클라이언트는 변경 후에도 여전히 account.balance를 사용해 계좌 잔액에 접근할 수 있다. 따라서 단일 접근 원칙의 장점을 누릴 수 있다.
마치며
객체 지향의 개념적 기반은 실세계의 추상화지만 그 이면에는 클래스라는 빌딩 블록을 통해 제공되는 캡슐화라는 강력한 설계 원칙이 존재한다. 객체의 속성을 메서드 뒤로 감추는 것은 캡슐화다. 복잡한 클래스 계층 구조를 단일 인터페이스 배후로 감추는 것 역시 캡슐화다. 객체 그룹을 aggregate로 묶거나 협력 관계를 단순화하기 위해 facade를 사용한 것 역시 캡슐화다.
초기의 설계 결정이 미래에도 유효할 것이라고 예상한다면 몇 달 후 어마어마한 후폭풍에 시달릴 위험성이 높다. 전문가와 초보자의 차이는 변경에 대비하고 그에 따른 파급 효과를 최소화하기 위해 다양한 캡슐화 기법을 사용하고 장단점을 절충(tradeoff)하는 능력에 달려 있다. 단일 접근 원칙은 객체 속성에 대한 설계 결정이 변경될 때 변경에 의한 파급 효과를 최소화하기 위해 적용할 수 있는 유용한 캡슐화 기법의 하나다.
버트랜드 메이어가 Eiffel 언어를 통해 소개한 단일 접근 원칙은 현재 C#과 루비 같은 언어에까지 그 영역을 확장하고 있다. C#은 프로퍼티라는 빌딩 블록을 통해, 루비는 언어 차원의 간결함과 메타프로그래밍 기법을 통해 단일 접근 원칙을 제공한다.
단일 접근 원칙의 모토는 단순하다. 객체 속성에 대한 설계 결정을 변경해도 영향을 최소화할 수 있는 방어막을 구축하는 동시에 직관적인 표기법을 제공하자. 그러면 자연스럽게 설계가 유연해지고 코드는 직관적인 모습을 띨 것이다.
이 문서 북마킹 하기

|
| 이제 전문가의 글을 단순히 ‘보는 것’에서, 직접 여러분이 developerWorks의 필자가 될 수 있습니다. IBM developerWorks를 통해 공유하고 싶은 지식이 있으신 분들은 원고 기획안을 접수해주세요. 채택되신 분께는 소정의 원고료를 드립니다. |
|
|
|
[지난 Open dW 보기] |
|