객체 지향의 여섯번째 계는 숨기는 것이다. 흔히 Encapsulation이라고 불리며 인터페이스등을 이용하여 메소드의 상세 구현여부를 숨기는 것으로 알려져 있다.

인터페이스를 통한 메소드의 구현 말고도 숨겨할 예는 몇가지 더 있다.
예컨데 Type이다.

User 객체를 만든다고 할때 new User(String userId) 와 같이 생성하는 건 좋지 못한 습관이다. 아마도 User.create(String userId) 같은 Factory Pattern 얘기를 떠올렸을지 모르지만 그 보다 좀더 근본적인 문제이다. 위 코드의 문제는 String userId 그 자체에 있다. DB repository를 생각한다면 아마도 userId는 varchar 타입에 저장될테고 varchar와 String은 별 문제가 없어보인다.


코드를 좀더 구현해서 getUser(String userId)를 구현해야 할때가 되면 슬슬 걱정이 되기 시작한다. 과연 인자로 넘어온 userId는 소문자일까?

getUser(String userId) {
     String _userId = userId.toLowerCase() ;l
      ......
}
와 같이 코드를 수정한다. 다시 두번째 걱정이 든다. 과연 인자로 넘어온 userId는 null이 아닐까?

getUser(String userId) {
     if (userId == null) throw new IllegalArgumentException("userId must be nullable") ;
     String _userId = userId.toLowerCase() ;l
      ......
}
와 같이 코드를 수정한다. 다시 세번째 걱정이 든다. 앞의 User를 생성하는 new User(String userId)의 인자는 소문자가 넘어갔을까?

// in User Class
public boolean equals(Object obj){
     if (obj instanceof User) return false ;
     User that = (User)obj ;
     this.getId().equalsIgnoreCase(that.getId()) ;
}
와 hashCode 메소드를 추가한다. 다시 네번째 걱정이 든다. 과연 DB에는 소문자가 저장이 될까?

// in User Class
public String getId(){
     return id.toLowerCase()  ;
}
와 같이 코드를 수정한다. 다시 다섯번째 걱정이 든다. 어라? 다른 메소드들은 모두 괜찮은건가?? .. .이하 반복



이러한 걱정이 드는 이유는 간단하다. 조금 더 생각해 보면 userId는 단순한 String 이상의 의미가 있다. 이를테면 length는 4byte - 20byte, 허용되는 문자는 AlpahNumUnderbar이며 '_'로 시작해서는 안된다 등의 세부 규칙이 있으며 User 객체를 구분하는 Key의 role을 가지고 있다. 그리고 이러한 정보들은 String에 담기지 않기 때문에 걱정이 들고 이것저것 잔뜩 체크하는 코드들을 중복해서 심어놓게 된다.


이 문제를 해결하기 위해서는 두가지 방법이 있다.

첫번째 방법은 지금보다 훨씬 더 단순하게 만들어서 길을 단 하나만 만들어놓고 입구에서만 체크하는 방법이다. 구조를 단순한게 만드는 작업이 쉽지 않지만 프로그램이 CRUD만을 하는 것이라면 그렇게까지 어렵지는 않다. 프리젠트 레이어에서 모델 레이어까지 거의 원방향 직통 터널을 뚫는 것과 같다. 이 방법을 사용한다면 Entity객체를 만들지 않는게 좋다. 단순한 게시판에 이방법을 사용할때 Board 객체니 Article 객체니 하는것은 단순하게 만드는데 방해가 되기 때문에 그래서 좋지 않다.

음 그러나 만들기는 어렵지 않으나 지속적인 유지보수하기에는 어려운 방법이다. 왜냐하면 엔트로피는 증가하기 마련이고 프로그램은 복잡해지기 마련이다. 일명 세컨드 시스템 이펙트로 프로젝트가 성장 할수록 누군가가 터널에 구멍을 뚫어 저것과 인터페이싱하면 그닥기능과 저닥기능도 될수 있다고 누군가가 주장하기 시작할테고 이것저것 통합하다보면 얼마 지나지 않아 뚫린 곳으로 검증되지 못한 데이타가 들어오고 그것을 체크하기 위한 코드로 프로그램은 누더기가 될것이다. (성장하지 않는 프로그램이라면 - 이를테면 파일럿 예제라든가 - 첫번째 방법은 좋은 방법이 된다.)



두번째 방법은 구조를 단순하게 만들기가 힘들다면 더 복잡하게 만들어서 String을 사용하지 않고 userId가 가지는 규칙과 role을 가지는 새로운 타입을 만드는 방법이 있다. 예컨대 

 private IDString(String id) {
     this.id = id ;
 }

 private static IDString create(String id, int minLength, int maxLength){
        if (id == null) throw new IllegalArgumentException("....") ;

       if (StringUtil.isAlphaNumUnderbar(lowerId, minLength, maxLength)) {
            return new IDString(lowerId) ;
       }
        throw new IllegalArgumentException("......") ;
 }

 public static IDString userId(String userId){
    return create(userId, 2, 12) ;
 }

IDString class를 만들어서 

public User(IDString userId){ // 생성자
   ...
}

public User getUser(IDString userId) {
   .....
}

와 같이 인터페이스를 하는 방법을 사용할 수 있다. 만약 이런 식으로 사용하는 ID 형식의 Type이 많다면 공통점을 묶을 수도 있고 UserId Class 로 별도의 클래스로 만들수도 있다. (균형을 찾는 것은 언제나 어렵다.)

위와 같이 별도의 클래스를 사용하면 인자로 넘어오는게 소문자인지 허용할 수 없는 문자가 들어오는지 일일이 체크하지 않아도 된다.(이를 확실히 보장하려면 해당 클래스를 final로 만들수도 있다.) IDString이 null 로 넘어오는 것을 막을 수는 없지만 불필요한 toLowerCase() 같은 함수를 쓰지 않음으로서 가능성을 대폭 줄여준다. (null의 문제는 설계로 줄여줄수는 있어서 이론적으로 완벽히 막을 수는 없다. )


그래서 인캡슐은 단순히 숨기기만 하는게 아니다. 코드는 숨기는 대신 의도는 드러내야 한다. int, Sring 자체는 아무 의미없는 스칼라 값이기 때문에 의미를 부여하기 위해 첫번째 변수명을 잘 지어야 하고 규칙과 롤이 있어서 충분하지 않다면 그 의도를 반영해주는 포장을 해야 한다. 

어쩌면 이런 질문이 떠오를지도 모른다. 이봐~ 그래봐야 결국에는 String을 사용할거잖아? 언제까지 포장 객체를 쓸수는 없는 노릇이니까 말야. 물론 맨 마지막에는 이런 포장을 뜯어야 한다. 포장은 객체 매개 변수로 전달이 되는 운송과정에서 의미가 있다. String 객체와 달리 IDString으로 포장된 String은 컴파일러와 다른 누군가 혹은 자신에게 그 값이 어떤 값이며 왜 쓰이고 있는 지를 전달해준다. 그리고 마지막 포장을 풀고 String이 필요해질때 getter 메소드가 있는 Bean의 위치를 통해 그 값의 의미를 알게 된다.


Encapsulation에는 단순히 다형성만을 뜻하는 것이 아닌 하나의 패러다임에 가깝다. 아마도 varchar로 저장되겠지만 varchar에 대응되는 String형을 사용하지 말아야 한다는 것은 몰라도 되는 것을 숨겨야 할 뿐 아니라 이미 알고 있는 것도 숨기는게 좋다는 의미이다. (@TODO : 브룩스-맨먼쓰미신-나중에 추가)



'Framework > 아키텍쳐 일반' 카테고리의 다른 글

AOP  (0) 2009.06.08
Self-Signed Java Applets  (0) 2009.06.01
중복을 볼 수 있는 눈  (0) 2009.03.13
Here is Dragon  (0) 2009.03.12
프로그래밍 패러다임  (0) 2009.03.12
Posted by bleujin