IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
한국 developerWorks   >   Open developerWorks  > developerworks

단일 접근 원칙(Uniform Access Principle)을 통한 캡슐화, Part 1: 요구사항 변경에 대응하는 기본 원칙, 캡슐화



조영호조영호 youngho.cho@nhncorp.com

LG-CNS 재직 시 다수의 공공 프로젝트에 아키텍트로 참여했고 현재는 NHN에서 네이버 카페 서비스를 개발하고 있다. 애자일 방법론과 객체 지향 분석•설계에 관심이 많으며 켄트 벡과 마틴 파울러를 존경하는 평범한 개발자다. 시간이 날 때마다 http://aeternum.egloos.com에 소프트웨어 개발과 관련된 다양한 이야기를 적고 있다.


난이도 : 중급
2009년 7월 28일


연재순서
1회(2009년 7월): 요구사항 변경에 대응하는 기본 원칙, 캡슐화


[오픈 developerWorks]는 여러분이 직접 필자로 참가하는 코너입니다.

속성과 함수, 그리고 캡슐화

은행 도메인에서 계좌(account)의 주된 용도는 고객의 잔액(balance)을 관리하는 것이다. 객체 지향 분석•설계의 핵심은 실세계의 개념과 비슷한(하지만 똑같지는 않은) 추상 모델을 구축하는 것이므로 유비쿼터스 언어(Ubiquitous Langauge: 모든 프로젝트 관련인 사이에서 동의하는 말)에 포함된 어휘인 account와 balance를 사용해 도메인 모델을 작성할 수 있다.

구현 언어로 자바를 사용할 경우 계좌의 개념을 구현하는 가장 간단한 방법은 balance를 public 속성으로 가지는 Account 클래스를 추가하는 것이다. 실제 운영 코드였다면 금액을 표현하기 위해 통화와 금액을 하나의 단위로 유지하는 퀀터티(Quantity) 패턴을 사용하겠지만 여기에서는 설명을 위해 간단히 long 타입을 사용한다.

Listing 1. Account.java

public class Account {
   public long balance;
	
   public Account() {		
   }
}


예금이란 계좌 잔액에 일정 금액을 더하는 것을, 인출이란 계좌 잔액에서 일정 금액을 차감하는 것을 의미한다(Listing 2, 3).

Listing 2. AccountTest.java(예금)

@Test
public void deposit() {
   Account account = new Account();
   account.balance += 3000;
   account.balance += 2000;
		
   assertEquals(5000, account.balance);
}



Listing 3. AccountTest.java(인출)

@Test
public void withdraw() {
   Account account = new Account();
   account.balance += 3000;
   account.balance -= 1000;

   assertEquals(2000, account.balance);
}


balance가 Account 클래스의 public 속성이므로 account.balance += 3000 또는 account.balance -= 1000과 같이 balabce 속성의 값을 직접 변경할 수 있으며 간단하게 account.balance를 호출해 잔액을 조회할 수 있다. 그러나 객체 지향의 기본 개념을 아는 사람이라면 다음과 같은 의문이 들 것이다. “캡슐화 원칙은 어디로 가버린 거지?”

객체 지향의 십계명 중 제1계명은 속성은 감추고 public 인터페이스를 통해서만 상태를 변경할 수 있도록 객체를 캡슐화하라는 것이다. “네 이웃의 것을 탐하지 말라.” 그렇다면 객체의 상태를 캡슐화해야 하는 이유는 무엇일까? account.balance와 같이 직관적이면서도 간단한 직접 접근(direct access) 방식에 비해 account.setBalance()나 account.getBalance()처럼 번거로운 메서드 호출을 통한 간접 접근(indirect access) 방식의 장점은 무엇일까?

모든 음모(?)의 배후에는 요구사항이 도사리고 있다. 정확하게 말하면 요구사항 변경이라는 소프트웨어의 본질적인 특징과 관련이 있다. 소프트웨어가 출시되고 시간이 얼마 흐르면 사용자들은 소프트웨어를 더 잘 이해하게 된다. 기본 기능에 익숙해진 사용자들은 자신의 작업 환경을 개선하기 위해 기능 개선을 요구하고, 높아진 사용자들의 눈높이를 맞추려면 소프트웨어 수정이 불가피하다.

따라서 요구사항 변경 시 수정해야 하는 코드 영역을 최소화함으로써 파급 효과(ripple effect)를 줄이는 도구가 필요하다. 이를 해결하는 방법으로 다양한 정보 은닉(information hiding) 기법이 소개되어 왔다. 객체 지향의 경우 클래스라는 빌딩 블록을 언어 차원에서 지원함으로써 속성을 인터페이스 뒤로 감추는 장치를 제공한다. 이를 데이터 캡슐화(data encapsulation)라고 한다.

모듈은 서브 프로그램이라기보다는 책임의 할당이다. 모듈화는 개별적인 모듈에 대한 작업이 시작되기 전에 정해져야 하는 설계 결정들을 포함한다. … 분할된 모듈은 다른 모듈에 대해 감추어야 하는 설계 결정에 따라 특징지어진다. 해당 모듈 내부의 작업을 가능한 적게 노출하는 인터페이스 또는 정의를 선택한다. … 어려운 설계 결정이나 변화하기 쉬운 설계 결정들의 목록을 사용해서 설계를 시작할 것을 추천한다. 이러한 결정이 외부 모듈에 대해 숨겨지도록 각 모듈을 설계해야 한다.
- Davis Parnas, “On the Criteria To Be Used in Decomposing Systems Into Modules”

Account의 balance 속성을 public 으로 노출하는 설계는 다음과 같은 두 가지 변경에 대해 취약하다.

  • balance 증감 시 추가 작업이 필요한 경우
  • balance 값을 저장된 값(stored value) 방식에서 계산된 값(computed value) 방식으로 변경하고자 할 경우


위로


balance 증감 시 추가 작업이 필요한 경우

계좌의 최종 잔액뿐만 아니라 모든 예금/인출 이력을 조회할 수 있어야 한다는 요구사항이 추가되었다고 가정하자. 새로운 요구사항을 만족하려면 Account에 예금 목록과 인출 목록을 관리하는 List 타입의 withdraws, deposits 속성을 추가하고, deposit()과 withdraw() 메서드를 사용해 balance 값을 변경하도록 코드를 수정해야 한다. 이처럼 속성의 값에 접근할 때 함께 수행해야 하는 작업들을 캡슐화하는 가장 좋은 방법은 메서드를 사용하는 것이다.

Listing 4. Account.java

public class Account {
   public long balance;
   private List<Long> withdraws = new ArrayList<Long>();
   private List<Long> deposits = new ArrayList<Long>();
	
   public Account() {		
   }
	
   public void withdraw(long amount) {
      withdraws.add(amount);
	  balance -= amount;
   }
	
   public void deposit(long amount) {
	  deposits.add(amount);
	  balance += amount;
   }
}


deposit()과 withdraw() 메서드는 balance 값을 늘리거나 줄이는 작업 외에도 예금 List나 인출 List에 금액을 추가하는 작업도 함께 처리한다. account.balance에 직접 접근해 잔액을 변경하던 클라이언트 코드를 deposit()과 withdraw() 메서드를 사용하도록 수정하자.

Listing 5. deposit()과 withdraw() 메서드를 사용한 AccountTest.java

@Test
public void deposit() {
   Account account = new Account();
   account.deposit(3000);
   account.deposit(2000);
		
   assertEquals(5000, account.balance);
}
	
@Test
public void withdraw() {
   Account account = new Account();
   account.deposit(3000);
   account.withdraw(1000);
		
   assertEquals(2000, account.balance);
}


이제 수정된 Account 객체는 “모든 입금액의 합과 모든 출금액의 합을 더한 금액은 계좌 잔액과 같아야 한다”라는 불변식(invariant)을 만족시켜야 한다.

Listing 6. Account에 추가된 불변식

balance = sum(withdraws) – sum(deposits)


그러나 balance가 public 속성이므로 Account 객체 외부에서 deposit()과 withdraw()를 통하지 않고서도 balance의 값을 마음대로 변경할 수 있다. 따라서 Account 객체의 불변식은 쉽게 깨지고 만다.

Listing 7. AccountTest.java

@Test
public void encapsulateionBreak() {
   Account account = new Account();
   account.deposit(3000);
   account.withdraw(1000);
   account.balance = 5000; 
		
   assertEquals(2000, account.balance);
}


불변식을 유지하는 유일한 방법은 balance 속성의 가시성을 private로 설정해 deposit()와 withdraw() 메서드를 통하지 않고서는 속성의 값을 변경할 수 없게 만드는 것이다. 그러나 클라이언트는 계좌의 잔액을 참조할 수 있어야 하므로 balance의 값을 외부로 제공하는 메서드를 추가해야 한다.

Listing 8. balance의 값을 외부로 제공하는 메서드를 추가한 Account.java

public class Account {
   private long balance;
    
   public long getBalance() {
     return balance;
   }
    ……


맙소사! 모니터 여기 저기에 빨간색 메시지가 출력된다. balance 속성의 가시성이 private으로 변경되었기 때문에 balance 속성에 직접 접근하던 모든 클라이언트 코드에서 에러가 발생하고만 것이다. 전체 코드에 대한 소유권을 가지고 있다면 account.balance를 참조하는 모든 부분을 account.getBalance()로 수정하기만 하면 된다. 그러나 만약 Account가 프레임워크에 포함된 클래스이거나 수정 권한이 없는 외부 프로젝트에서 balance 속성을 직접 참조하고 있다면 balance 속성의 가시성을 자유롭게 낮출 수 있는 방법은 없다.

이처럼 요구사항 변경 때문에 속성을 사용할 때 별도 작업(여기에서는 금액을 List에 추가하는 작업)을 더 해야 한다면 기존 public 속성에 직접 접근하는 방식은 변경에 취약할 수밖에 없다.



위로


balance를 저장된 값에서 계산된 값으로 변경할 경우

balance는 Account 클래스의 속성이다. 기계적인 측면에서 말하자면 long 형인 balance는 메모리 상의 일정 크기(자바의 경우 4바이트)를 할당 받아 값을 저장한다. 이처럼 실제로 일정 크기의 메모리를 할당 받아 값을 저장하고 이를 참조하는 방식을 저장된 값 방식이라고 한다. 그러나 필요에 따라 실제 메모리를 할당 받지 않고 실행 중에 값을 계산한 후 그 결과를 참조할 수도 있다. 이를 계산된 값 방식이라고 한다. 예를 들어 고객의 나이를 참조해야 할 경우 Customer 객체에 실제로 존재하는 age 속성을 사용한다면 저장된 값 방식을 사용하는 것이고 생년월일을 나타내는 속성인 birthDate와 현재 날짜 간의 차이를 구한 후 이 값을 사용한다면 계산된 값 방식을 사용하는 것이다.

계산된 값 방식과 참조된 값 방식의 선택은 시간과 공간의 절충(tradeoff) 결과다. Customer 객체에 age 속성을 포함시키는 방식은 시간을 절약하는 대신 공간을 좀 더 소비하고, 생일을 표현하는 birthDate 속성과 현재 날짜 간의 차이를 사용해 나이를 계산하면 공간을 절약하는 대신 실행 시간이 더 오래 걸린다.

Account 클래스의 balance 속성은 모든 입금액의 합에서 출금액의 합을 뺀 값과 같아야 한다는 불변식을 만족시켜야 한다. 저장된 값 방식을 사용한 앞의 예제에서는 불변식을 보장하려고 withdraw()와 deposit() 메서드 내에서 입금액과 출금액을 추가할 때마다 속성인 balance의 값을 함께 증감했다. 계산된 값 방식을 사용하는 경우에는 balance 값이 필요한 시점에 입금액 목록과 출금액 목록에 저장된 금액의 차이를 사용해 balance 값을 계산할 수 있으므로 balance 속성을 유지할 필요가 없다.

Listing 9. Account.java

public class Account {
   private List<Long> withdraws = new ArrayList<Long>();
   private List<Long> deposits = new ArrayList<Long>();
	
   public Account() {		
   }
	
   public void withdraw(long amount) {
	  withdraws.add(amount);
   }
	
   public void deposit(long amount) {
	  deposits.add(amount);
   }
	
   public long getBalance() {
	  long result = 0;
	  for(long withdrawAmount : withdraws) {
	     result += withdrawAmount; 
	  }
		
	  for(long depositAmount : deposits) {
	     result -= depositAmount;
	  }
	
	  return result;
   }
}


이 경우 account.balance 속성을 제거했기 때문에 속성에 직접 접근하던 모든 클라이언트 코드에서 컴파일 에러가 발생한다. 즉, 저장된 값 방식으로 구현된 public 속성을 계산된 값 방식으로 변경할 경우 속성에 직접 접근하는 모든 코드를 메서드를 사용해 접근하도록 수정해야 하므로 변경에 취약할 수밖에 없다.



위로


일반적인 캡슐화 지침

요구사항은 변경된다. 그리고 변경되는 요구사항을 포용하는 능력은 소프트웨어 설계자가 갖춰야 할 가장 중요한 미덕 중 하나다. 앞에서 살펴본 경우처럼 public 속성에 직접 접근하는 방식은 변경에 취약할 수밖에 없다. 변경에 의한 파급 효과를 줄이려면 변경될 확률이 높은 public 속성을 안정적인 인터페이스 뒤로 숨겨야 한다. 따라서 자바의 경우 클래스의 모든 속성을 private로 만들어 외부에서 직접 접근하지 못하도록 금지하고 필요한 경우 메서드를 사용해 상태를 변경할 수 있도록 해야 한다. 이것이 C++, 자바 같은 주류 객체 지향 언어로 프로그램을 작성할 때 따라야 하는 캡슐화 지침이다.

Account 클래스는 balance를 public 속성으로 노출하고 있으므로 캡슐화 원칙을 위반했다. 그 결과 변경에 취약한 설계라는 사생아를 낳았으며 하위 호환성을 무시한 채 설계를 변경해야 하는 최악의 상황으로 치달았다. Account 클래스를 처음 작성하기 시작하던 시점부터 balance의 가시성을 private로 부여하고 getBalance() 메서드를 통해서만 balance에 접근할 수 있도록 했다면 요구 사항 변경 시 파급 효과를 최소화할 수 있었을 것이다.

Listing 10. Account.java

public class Account {
   private long balance;
	
   public Account() {		
   }
	
   public long getBalance() {
      return balance;
   }
}


이제 계좌 잔액을 필요로 하는 모든 클라이언트는 balance 속성에 직접 접근할 수 없고 getBalance()를 통해야만 속성값을 참조할 수 있다. 따라서 클라이언트에 대한 파급 효과를 염두에 두지 않고도 balance에 대한 설계 결정을 변경할 수 있다. 입출금 이력을 추가하거나 계좌 잔액을 저장된 값 방식에서 계산된 값 방식으로 변경하는 경우에도 getBalance() 메서드를 변경하지 않는 한 Account를 사용하는 클라이언트는 영향을 받지 않는다. 이것이 정보 은닉과 캡슐화의 힘이다.

하지만 balance를 직접 참조하는 대신 getBalance()처럼 메서드를 사용하는 방식은 코드를 복잡하고 이해하기 어렵게 만든다. customer.setMileage(customer.getMileage() + 10)와 customer.mileage += 10을 비교해 보자(단순한 getter/setter를 노출하는 것이 올바른 객체 지향 설계가 아니라는 점은 일단 논외로 하자.).

단일 접근 원칙은 간단하고 직관적인 직접 접근 방식과 캡슐화를 보장하는 간접 접근 방식의 장점을 결합할 수 있는 방법이 없을까라는 의문에서 출발한다.

Part 2에서는 버트랜드 메이어(Bertrand Meyer)의 단일 접근 원칙에 관해 설명하고, 객체의 속성을 안전하게 캡슐화하기 위해 C#, 루비를 사용해 단일 접근 원칙을 적용하는 방법에 관해 살펴 보기로 한다. 예제를 통해 단일 접근 원칙을 통해 캡슐화와 직관적인 코드라는 두 가지 장점 모두를 취할 수 있음을 알게 될 것이다.




이 문서 북마킹 하기

mar.gar.in mar.gar.in naver naver eolin eolin del.icio.us del.icio.us

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



[지난 Open dW 보기]
사이트 여행

dW 커뮤니티
포럼 | 블로그 | Spaces
dW Student Community

로컬 컨텐츠

행사 및 세미나

기획 기사

개발자 입문

튜토리얼 및 교육

TOP 10 인기자료

SW 다운로드

RSS 피드

뉴스레터
 
  
자바스크립트가 작동이 중지되었습니다. 이 기능을 수행하시려면 브라우저에서 자바스크립스트를 작동시켜 주시거나 이곳을 클릭해주세요.

Special offers
Screencast
IBM SOA Sandbox 시험판
dW Student Community
로보코드
코드 트레이닝


    IBM 소개 개인정보 보호정책 문의