객체 지향의 여섯번째 계는 숨기는 것이다. 흔히 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

프레임워크를 작성하는 데 있어 가장 필요한 능력은 무엇일까 ? 프레임워크의 사용자는 다른 프로그래머이기 때문에 프레임워크를 작성하는 사람은 아무래도 old한 경력이 좀 된 프로그래머여야 할테니 기본적으로 언어나 툴의 능숙한 사용같은건 제약요소가 되지 않는다.

개인적으로 가장 중요한 능력은 "눈" 이라고 생각한다. 물론 물리적인 눈을 말하는게 아니라 "관점"의 의미를 가지는 눈이다. 대부분의 프로그램은 스파게티 코드로 얽혀있고 꼬아져 있다. 와인버그는 일찌기 "건물을 짓는 사람들이 프로그래머가 소프트웨어를 제작하는 방식으로 건물을 짓는다면, 창공에서 날아온 첫번째 딱따구리는 전 인류의 문명을 파괴할 것이다"라고 말한바가 있다. (http://cjhgogox.egloos.com/1757251 프로그래머가 비행기를 만든다면..:) 불안정한 소프트웨어에서 거미줄처럼 얽혀 있는 이런 막연한 공간에서 존재하지 않았던 공통점을 발견하는 것은 프레임워크 개발에 가장 필요한 능력임과 동시에 가장 어려운 능력이기도 하다.

예컨데 여기의 Validation Framework는 예전에 루비의 선언적 프레임워크란 글을 읽고 반나절만에 작성하였다. 이전의 명령형이나 로직으로 처리하던 이전의 관점에 머물러 있을때는 보이지 않았었지만 새로운 관점을 익히니 문제가 떠올랐고 해결책은 더할나위없이 쉬웠다.

Code Analyzer같은 매트릭스 프로그램을 몇개 돌려보면 코드 중복을 찾는건 그다지 어려운 일이 아니다. 그러나 수십만줄의 코드 사이에서 구조적인 중복을 발견하는 것은 쉬운일이 아니다. 그나마 이 블로그의 소위 인프라 시스템 프레임워크라고 할 수 있는 DB, Configration, Message 등이 그나마 나은건 문제에 대한 인식이 쉽기 때문이기도 하다. 그 외의 프레임워크는 해결책을 찾기 전에 문제를 찾아야 하고 그 문제의 해결책은 그 문제를 바꾸지 않아야 한다. 그래서 더 어렵다.

다만 도움이 될 수 있는 한가지 말을 하자면 한사람이 2가지 관점을 가지는 것보다 다른 관점을 가진 두사람이 만나 협력하는 것이 좀더 쉽다.


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

Self-Signed Java Applets  (0) 2009.06.01
여섯번째 계 - Encapsulation  (0) 2009.04.14
Here is Dragon  (0) 2009.03.12
프로그래밍 패러다임  (0) 2009.03.12
아키텍쳐 패턴 - Broker 패턴  (0) 2009.03.12
Posted by bleujin


주석(코멘트)에 대한 대부분의 책은 단 한마디로 요약할 수 있다.
 "주식을 충실하게 달아라"

이는 다른말로 대부분의 프로젝트에는 주석이 충실하지 않다는 반증이고 주석 다는게 쉽지 않다는 말이기도 하다. 사실 좋은 코드와 나쁜 코드를 구분할 수 있는 것처럼 주석에도 좋은 주석과 나쁜 주석이 있다.

주석 안에는 무엇이 들어갈까 ? 주석을 달때 주의점은 아래와 같다 .

 - 어떻게가 아니라 왜를 기술하라 이를테면
    틀린예) GlbWLRegistry에 있는 WidgetList 구조를 업데이트 한다.
    라고 주석을 달아서는 안된다. 왜냐하면 그 내용은 코드안에 박혀 있기 때문이다. 따라서 "왜" 이렇게 코드를 작성했는지에 대해 주석을 달아야 한다. 
    바른예) 나중을 위해서 widget 정보를 캐시함 O


 - 코드를 묘사하지 마라
    틀린예) i++ ; // increment i
    DRY의 원칙은 단순히 코드끼리의 중복만을 의미하는 것은 아니다. 위 틀린예는 옆의 코드와 주석이 중복되기 때문에 DRY 원칙을 위배한 것이다. 코드와 중복되는 내용을 다시 코멘트로 쓰지 말아라. 


 - 코드를 대신하지 마라
    나쁜예) // 이 변수는 foo 클래스에 의해서만 접근해야 함. 
    코드의 주의사항을 달기보다는 코드를 다시 작성해라. 변수의 사용법을 설명하는 주석을 달기보다는 변수의 이름을 다시 지어라. 코드로 충분히 할 수있는 것을 주석으로 대체하지 말아라. 

    
 - 유용성을 유지하라
    주석은 당연한 것을 말하는 것이 아니라 당연하지 않다고 생각되는 의외의 것을 기록해야 한다. 주석으로 코드를 쉽게 설명할 생각은 하지 말고 그냥 코드를 쉽게 만들어라. 만약 더 이상 코드를 쉽고 명료하게 만들수가 없을 때에만 주석이 필요하다. 주석을 쓰레기 코드를 덮는 포장지로 사용해서는 안된다. 더이상 코드에 손댈게 없을때에야 진실되고 명료하고 알기 쉬운 주석을 달도록 노력해라. 


 - 주의를 흩트리지 마라
   코드가 주인이라는 사실을 잊지 말자. 따라서 주석을 달음으로서 불필요하게 코드 리딩과정에 주의를 흐트려서는 안된다. 쓸데 없는 아스키 기교로 주석의 네모 박스를 이쁘게 유지하는 등의 바보같은 노력을 하지 마라.
블록의 끝에  // end if ( a < 1 ) 같은 주석이 그럴듯 하게 보일지 모르지만 이 주석을 달 시간에 차라리 코드를 더 이쁘게 작성하고 지나친 중첩 block를 사용하지 말아라. 이런 주석은 코드가 수정될때 같이 수정이 되지 않아서 부패가 되고 썩어 버린다. 불필요한 코드만큼이나 불필요한 주석 그리고 공허한 주석을 달지 말아라.


주석에 대한 이런 관점은 주석은 필요악이라는 관점이다. - 비록 많은 사람들이 동의 하지 않을지라도 - More Comment, Better Code가 절대 아니다. 방법론에서 문서화들이 그러한 것처럼 Better Code를 추구하기 위한 과정일뿐 주석자체는 목적이 아니다. 그래서 좋은 코멘트를 작성하는 것보다는 좋은 코드를 작성하는것에 집중하는 것이 좋다.

Posted by bleujin


프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해 주고, 결정하는 역할을 한다. 예를 들어 객체지향 프로그래밍은 프로그래머들이 프로그램을 상호작용하는 객체들의 집합으로 볼 수 있게 하는 반면에, 함수형 프로그래밍은 상태값을 지니지 않는 함수값들의 연속으로 생각할 수 있게 해준다.

프로그래머가 어떤 언어를 사용하는 것과는 상관없이 프로그래밍 패러다임에 대해 알아두는 것은 새로운 관점의 세계관을 가진다는 것을 뜻한다. XP 켄트벡의 프로그래머들은 일년에 하나의 새로운 언어를 배워두는걸 권장하는 것은 아마도 이 때문이 아닌가 싶다. 예를 들어 객체지향 프로그래밍은 프로그래머들이 프로그램을 상호작용하는 객체들의 집합으로 볼 수 있게 하는 반면에, 함수형 프로그래밍은 상태값을 지니지 않는 함수값들의 연속으로 생각할 수 있게 해준다.

프로그래밍 패러다임과 프로그래밍 언어와의 관계는 프로그래밍 언어가 여러 프로그래밍 패러다임을 지원하기도 하기 때문에 복잡할 수도 있다. 어떤 언어들은 하나의 특정한 패러다임을 지원하기도 하는데, 스몰토크와 자바가 객체지향 프로그래밍을 지원하는 반면에, 헤스켈과 스킴은 함수형 프로그래밍을 지원한다.

여러가지 패러다임을 지원하는 언어들도 있는데, C++, 자바 스크립트, 커먼 리스프, 파이썬, 오즈가 이런 언어들이다. 예를 들어서 C++는 절차적 프로그래밍, 객체기반 프로그래밍, 객체지향 프로그래밍, 제네릭 프로그래밍의 요소들을 지원하도록 설계되었다. C++에서는 순수하게 절차적 프로그램을 작성할 수 있고, 순수하게 객체지향 프로그램을 작성할 수 있으며, 두 가지 패러다임 모두의 요소를 포함한 프로그램을 작성할 수도 있다. 자바 스크립트는 구조적 프로그래밍이고, 함수형 프로그래밍이고, 프로토타입 기빈 프로그래밍이며, 객체기반 프로그램밍 언어이지만 절차적 프로그래밍 방식도 사용한다.


구조적 프로그래밍과 비구조적 프로그래밍

구조적 프로그래밍(영어: structured programming)은 구조화 프로그래밍으로도 불리며 프로그래밍 패러다임의 일종인 절차적 프로그래밍의 하위 개념으로 볼 수 있다. GOTO문을 없애거나 GOTO문에 대한 의존성을 줄여주는 것으로 가장 유명하다.역사적으로 구조적 프로그램을 작성하기 위하여 몇가지 다른 구조화 기법과 방법론이 개발되어 왔다. 

데이크스트라의 구조적 프로그래밍은 프로그램의 논리 구조는 제한된 몇 가지 방법만을 이용하여 비슷한 서브 프로그램들로 구성된다. 프로그램에 있는 각각의 구조와 그 사이의 관계를 이해하면 프로그램 전체를 이해해야 하는 수고를 덜 수 있어, SoC에 유리하다. (데이크스트라의 관점에서 파생된 관점 : 하위 프로그램의 시작점은 한 군데이지만 끝점은 여러 개일 수 있다. )

저수준의 관점에서 구조적 프로그램은 간단하고, 계층적인 프로그램 제어 구조로 구성된다. 이 제어 구조들은 하나의 구문으로 간주되며, 동시에 더 간단한 구문들을 결합시키는 방법이다. 더 간단한 구문들은 또 다른 제어 구조일 수도 있고, 할당문이나 프로시저 호출과 같은 기본 구문일 수도 있다.(에츠허르 데이크스트라가 확인한 3가지 형태의 구조는 순차, 선택, 반복이다.)

설계에 있어서 구조적 프로그래밍이 항상 그런 것은 아니지만 하향식 설계와 관련이 있다. 하향식 설계를 할 때, 설계자는 큰 규모의 프로그램을 더 작은 공정으로 나누어 구현하고, 각각 검사한 다음에 전체 프로그램으로 합친다.

모든 절차적 프로그래밍 언어에서 구조적 프로그래밍을 할 수 있다. 1970년쯤부터 구조적 프로그래밍이 인기있는 기법이 되었기 때문에, 대부분의 새로 나온 절차적 프로그래밍 언어들이 구조적 프로그래밍을 고취시키기 위한 특징을 추가하였고 구조화되지 않은 프로그래밍을 쉽게 하기 위한 특징들은 남겨둔 것들도 있었다. 잘 알려진 구조적 프로그래밍 언어에는 파스칼(Pascal)과 에이다(Ada)가 있다.


비구조적 프로그래밍은 하나의 연속된 덩어리에 모든 코드를 넣는 프로그래밍 패러다임이다. 대비되는 개념으로는 구조적 프로그래밍이 있는데, 이는 프로그램의 작업이 (함수나 서브루틴으로 알려진) 더 작은 부분으로 나누어 필요할 때마다 호출하는 것이다. 비구조적 프로그래밍 언어는 코드의 특정부분으로 건너뛰는 GOTO문과 같은 흐름 제어문에 의존할 수 밖에 없다.

구조화되지 않은 원시 코드는 읽고 디버그하기가 매우 어렵고, 구조적인 작성을 지원하는 프로그래밍 언어에서는 추천하지 않는다. 그러나 프로그램 구조는 항상 조건문과 GOTO문을 조합하여 구현할 수 있기 때문에 구조가 모든 언어에서 필요한 것은 아니다. MS-DOS의 배치 파일과 같은 많은 스크립트 언어나 베이직이나 포트란 같이 오래된 언어에서는 여전히 사용되기도 한다. GOTO문을 쓰는 것에 수행 속도상의 이점은 없다. (실제로, 컴파일러가 최적화 할 수 있는 것들을 혼란시켜 오히려 불이익이 될 수도 있다.)

어셈블리어는 대체로 비구조적 언어인데, 기본이 되는 기계어 코드가 구조적이지 않기 때문이다. 어셈블리 언어에 있는 유일한 구조는 함수의 시작과 끝 같이 컴파일 도구에서 쓰는 것들이다.



명령행 프로그래밍과 선언형 프로그래밍

전산학에서 명령형 프로그래밍(Imperative programming)은 선언형 프로그래밍과 반대되는 개념으로, 프로그래밍의 상태와 상태를 변경시키는 구문의 관점에서 연산을 설명하는 프로그래밍 패러다임의 일종이다. 자연 언어에서의 명령법이 어떤 동작을 할 것인지를 명령으로 표현하듯이, 명령형 프로그램은 컴퓨터가 수행할 명령들을 순서대로 써 놓은 것이다.

명령형 프로그래밍 언어는 함수형 프로그래밍이나 논리형 프로그래밍언어와 같은 다른 형태의 언어와 다르다. 헤스켈 같은 함수형 프로그래밍 언어는 구문들을 순서대로 써 놓은 것이 아니며, 명령형 프로그래밍 언어와는 다르게 전역적인 상태가 없다. 프롤로그와 같은 논리 프로그래밍 언어는 "어떻게" 계산을 할지 보다는 "무엇"이 계산될 것인지를 정의한다는 생각으로 작성된다.

거의 대부분의 컴퓨터 하드웨어는 명령형으로 구현된다. 거의 모든 컴퓨터 하드웨어들이 컴퓨터의 고유 언어인 기계어를 실행하도록 설계되어 있는데, 이것이 명령형으로 씌어 있다. 낮은 수준의 관점에서 프로그램의 상태는 메모리의 내용으로 정의되고, 구문들은 기계어의 명령어로 정의된다. 높은 수준의 언어 구현은 변수와 더 복잡한 구문을 사용하지만, 여전히 같은 패러다임을 따른다. 요리법이나, 공정 점검표같은 것들은 컴퓨터 프로그램은 아니지만, 명령형 프로그래밍과 비슷한 형태의 이해하기 쉬운 개념이다. 각각의 단계의 지시 사항들이 있고, 상태라는 것은 현실 세계에 반영된다. 명령형 프로그래밍의 기본 생각이 개념적으로 친밀하고, 직접적으로 구체화되어 있어서, 대부분의 프로그래밍 언어들은 명령형이다.

보통 할당문은 메모리에 있는 정보에 연산을 수행하고, 결과값을 나중에 사용하기 위해 메모리에 저장한다. 추가로, 고급 명령형 언어는 산술 연산, 함수연산, 결과 값을 메모리에 할당하는 연산을 결합한 복잡한 수식을 계산한다. 반복문은 이런 연속된 구문을 여러번 실행하게 한다. 반복문은 미리 정의된 횟수만큼 반복하기도 하고, 어떤 조건이 바뀔때까지 반복하기도 한다. 조건 분기문은 구문의 덩어리를 어떤 조건이 만족하는 경우에만 실행하게 할 수 있다. 그렇지 않으면, 그 구문의 덩어리를 실행하지 않고 그 다음부터 실행한다. 비조건 분기문은 실행 순서를 프로그램의 다른 부분으로 옮기는 것이다. 여러 언어에서 제공하는 GOTO문, 서브프로그램, 프로시저, 호출문들이 비조건 분기문이다.

명령형 프로그래밍의 전형적인 예는 포트란과 알골이다. 파스칼, C, 에이다는 또 다른 예이다.


선언형 프로그래밍은 두 가지 구분되는 뜻이 있는데 두 가지 뜻 모두 통용되고 있다.

한 정의에 따르면, 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명하는 경우에 "선언형"이라고 한다. 예를 들어, 웹 페이지는 선언형인데 웹페이지는 제목, 글꼴, 본문, 그림과 같이 "무엇"이 나타나야하는지를 묘사하는 것이지 "어떤 방법으로" 컴퓨터 화면에 페이지를 나타내야 하는지를 묘사하는 것이 아니기 때문이다. 이것은 전통적인 포트란과 C, 자바와 같은 명령형 프로그래밍 언어와는 다른 접근방식인데, 명령형 프로그래밍 언어는 프로그래머가 실행될 알고리즘을 명시해주어야 하는 것이다. 간단히 말하여, 명령형 프로그램은 알고리즘을 명시하고 목표는 명시하지 않는데 반해 선언형 프로그램은 목표를 명시하고 알고리즘을 명시하지 않는 것이다.

또 다른 정의에 따르면, 프로그램이 함수형 프로그래밍 언어, 논리형 프로그래밍 언어, 혹은 제한형 프로그래밍 언어로 쓰여진 경우에 "선언형"이라고 한다. 여기서 "선언형 언어"라는 것은 명령형 언어와 대비되는 이런 프로그래밍 언어들을 통칭하는 것이다.

이 두가지 정의는 서로 겹치는 부분도 있다. 특히, 제한형 프로그래밍과 논리형 프로그래밍은 필요한 해의 특성을 설명하고(무엇) 그 해를 찾는데 사용하는 실제 알고리즘은 설명하지 않는다(어떤 방법). 그러나 대부분의 논리형과 제한형 언어들은 알고리즘을 설명할 수 있고, 상세한 부분을 구현할 수 있어서 첫 번째 정의를 따르는 엄밀한 의미의 선언형 프로그래밍 언어는 아니다.

마찬가지로, 명령형 프로그래밍 언어로 선언형으로 프로그램을 작성할 수도 있다. 라이브러리나 프레임워크 내부의 비선언형 부분을 캡슐화하여 이렇게 할 수 있다. 이런 형태의 예가 제이유닛 유닛 테스트 프레임워크에 반영되어 쓰이고 있는데, 이것은 정의만 되어 있으면 프레임워크로 등록하여 유닛을 테스트하는 것을 가능하게 한다.

선언형 프로그래밍은 특수 분야 언어(영어: Domain-specific language, DSL)의 형태로 자주 사용된다. 특수 분야 언어의 한 가지 결점은 튜링 완전성이 없다는 것이다. 그 말은 할 수 없는 일이 있다는 것이다. 스프레드시트에서는 전자메일을 보낼 수 없고 전자메일을 이용하여 은행 계좌를 계산할 수 없다는 것이다. 이러한 이유로 특수 분야 언어들은 때로 범용 언어에 내장된다. 이렇게 하면 프로그래머가 특수 분야 언어가 힘을 발휘하는 분야에서 이것을 이용할 수 있고, 특수 분야 언어로 하기 어렵거나 불가능한 문제는 범용 언어를 이용할 수 있다.

여기서 소개한 Validation Framework는 바로 이 선언형 프로그래밍 패러다임을 사용한것이다. 이 밖에도 잘 알려진 선언형 프로그래밍을 포함한 프레임워크 루비 온 레일와 jUnit등의 Unit 시리즈가 있다.



메시지 전달 프로그래밍과 명령행 프로그래밍

명령행 프로그래밍은 위에...

메시지 전달 프로그래밍은 주로 분산환경에서 독자적인 메모리 공간을 가지는 각각의 프로세스가 하나의 문제를 해결하기 위하여 프로세서들은 정보(메시지)들을 교환하는 방식을 말한다. 메시지 전달의 간단한 형태는 Point to Point 통신이다. 이는 한 프로세서가 다른 프로세서로 메시지를 보내는 것인데, 보내는 방식에 따라 동기와 비동기로 나눌 수 있다. 동기 방식은 메시지의 완료를 다른 프로세서에 보내주는 것이며, 비동기 방식은 단지 언제 메시지를 보냈는지만 알 수 있다. 

개념적으로 본다면 DB 프로세스는 프로그래밍 프로세스와 독립된 메모리 공간을 가지는 분산환경이며 프로그래밍 언어와 데이타베이스는 각각이 독립적인 서비스 이기 때문에 둘 중 어느 누구도 상대방에 의존하지 않는 중립적인 메시지를 교환하는게 맞다고 생각했기 때문에 이 블로그에서 소개하는 DB Framework는 이 메시지 전달 프로그래밍 패러다임을 사용한 것이다. 



절차적 프로그래밍과 함수형 프로그래밍

절차적 프로그래밍(procedural programming)은 절차지향 프로그래밍 혹은 절차지향적 프로그래밍이라고도 불리는 프로그래밍 패러다임의 일종으로서, 때때로 명령형 프로그래밍과 동의어로 쓰이기도 하지만, 프로시저 호출의 개념을 바탕으로 하고 있는 프로그래밍 패러다임을 의미하기도 한다. 프로시저는 루틴, 하위프로그램, 서브루틴, 메서드, 함수(수학적 함수와는 다르고 함수형 프로그래밍에 있는 함수와는 비슷한 의미이다.)라고도 하는데, 간단히 말하여 수행되어야 할 연속적인 계산 과정을 포함하고 있다. 프로그램의 아무 위치에서나 프로시저를 호출될 수 있는데, 다른 프로시저에서도 호출 가능하고 심지어는 자기 자신에서도 호출 가능하다.

절차적 프로그래밍 언어들은 절차적 프로그래밍 접근 방식을 따름으로써 프로그래머의 작업을 수월하게 한다. 알골과 같은 언어가 절차적 프로그래밍 언어의 표준적인 예이다. 그 밖에 포트란, PL/I, 모듈라-2, 에이다 등이 있다.

함수형 프로그래밍은 프로그래밍 패러다임의 하나로, 계산을 수학적 함수의 조합으로 생각하는 방식을 말한다. 이것은 일반적인 프로그래밍 언어에서 함수가 특정 동작을 수행하는 역할을 담당하는 것과는 반대되는 개념으로, 함수를 수행해도 함수 외부의 값이 변경될 수 없다.

알론조 처치가 1930년대에 개발한 람다 대수는 함수에 대한 이론적 기반을 세웠다. 이것은 프로그래밍 언어가 아니라 수학적 추상화였지만, 이것은 함수형 프로그래밍의 근간을 이루었다. 처음으로 만들어진 함수형 프로그래밍 언어는 IPL이었다. 존 매카티가 만든 리스프는 훨씬 향상된 함수형 프로그래밍 언어였고, 이것은 현대적 함수형 프로그래밍의 여러 특징을 가지고 있었다. 리스프를 발전시키고 간단하게 만든 언어로 스킴이라는 것도 나왔고1980년대에는 그동안의 함수형 프로그래밍에 대한 연구를 바탕으로 헤스켈이 만들어졌다.



 

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

중복을 볼 수 있는 눈  (0) 2009.03.13
Here is Dragon  (0) 2009.03.12
아키텍쳐 패턴 - Broker 패턴  (0) 2009.03.12
아키텍쳐 패턴 - Pipes and Filter 패턴  (0) 2009.03.11
아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
Posted by bleujin



만약 현재 개발중인 환경이 독립적인 협력 컴포넌트들로 구성된 이질적인 분산시스템이라면 Broker 패턴을 생각해 봐야 한다.

분산은 아래와 같은 장점이 있다.
  - 경제성
  - 성능과 범위성
  - 고유 분산성
  - 신뢰성

그해 비해 단점은 중앙집중적 시스템이 가지고 있는 것과는 근본적으로 다른 소프트웨어가 필요하다는 것이다. 보통의 경우 분산 시스템은 느리고(비효율적) 예외의 종류가 비약적으로 증가하기 때문에 만들기가 훨씬 더 어렵다.

간단히 말하자면 브로커 패턴은 중개인을 통해 원격 컴포넌트의 위치 투명성을 제공하는 것이다. 컴포넌트는 위치 투명한 서비스 호출을 통해서 다른 컴포넌트가 제공하는 서비스에 억세스 할수있고 런타임에 컴포넌트의 추가나 제거가 가능해야 한다. 그리고 아키텍쳐는 특정 시스템에 국한된 세부사항(system-specific details)과 특정 구현에 국한된 세부사항(implementation-specific details)을 숨겨야 한다. 

브로커패턴의 예에는 CORBA(common object request broker architecture)와 DCOM(OLE 2.x)이 있는데 현재는 두 사양 모두 많이 사용되지는 않고 EJB2도 위 패턴을 도입했지만 느린 속도등의 이유로 역시 활성화 되지는 못하였다. 

개인적인 의견이지만 분산환경에서 위치투명성이란 코드적 관점이지 프로그래머적 관점은 아니라고 생각한다. 무슨 말이냐면 브로커 패턴을 이용한 분산 시스템에서 코드는 위치투명한 컴포넌트의 서비스 코드를 사용하지만 그걸 작성하는 사람은 이게 원격호출인지 아닌지를 알고 있어야 한다는 뜻이다. 그렇지 않으면 최소한 지금의 네트워크 환경에서는 Hello World 정도의 프로그램이 아닌 보통의 실제 어플리케이션의 복잡함을 감당할 실행속도가 나오기가 힘들다고 생각한다. (비록 coarse-grained 패턴을 쓴다고 해도 말이다. ) 이 블로그의 AL Framework는 이런 관점의 브로커 패턴을 사용한 프레임워크이다.


Broker 패턴은 분산 어플리케이션을 개발하면서 어쩔 수 없이 야기되는 복잡성을 줄인다. Broker 패턴을 사용할 경우 개발자는 분산 구조를 투명하게, 다시 말해서 개발을 위해 필요한 부분과 필요하지 않은 부분을 명확하게 구별해 파악함으로서 분산의 전체 구조를 바라볼 수 있다. 확장된 이 분산 어플리케이션은 이기종 머신에서 실행될 수 있고 각기 다른 프로그래밍 언어로 작성될 수 있다.



브로커 패턴은 아래와 같은 장단점이 있다.
장점
1. 위치 투명성이 제공된다.
2. 컴포넌트의 가변성과 확장성이 보장된다.
3. 플랫폼간의 이식 가능성이 제공된다.
4. 서로 다른 Broker 시스템들간의 상호 운용성이 지원된다.
5. 재사용성이 확보된다.

 

단점
1. 효율이 낮다.
2. 장애 허용성이 낮다.
3. 테스트와 디버깅이 한편으론 용이하고 다른 한편으론 복잡하다.

Broker 패턴은 거대 규모의 인프라 패러다임으로 단일 어플리케이션을 구축할 때에는 잘 사용되지 않으며 여러 어플리케이션 전체를 위한 플랫폼 역할을 한다.
 

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

Here is Dragon  (0) 2009.03.12
프로그래밍 패러다임  (0) 2009.03.12
아키텍쳐 패턴 - Pipes and Filter 패턴  (0) 2009.03.11
아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
Class Design  (0) 2009.03.07
Posted by bleujin


앞글의 예외처리의 예외에서 소개한 코드는 아키텍쳐 레벨에서 보면 Pipes and Filter 패턴의 한 예이며 가장 많이 알려진 PAF 패턴의 예는 유닉스의 명령어이다. 유닉스의 모든 명령어는 char*가 입출력의 표준 메시지 포맷이기 때문에 ls -al | grep 'abc' | vi 식의 사용이 가능하다. 그리고 티스토리의 플러그인 방식도 PAF의 일례라고 볼 수 있다. 앞의 두 예 모두 순서에 상관없지만 PAF가 항상 순서에 관계없이 동작하는 것은 아니다.


PAF 패턴은 아래와 같은 특징이 있다.

1. 미래에 시스템의 기능을 향상시키기 위해서는 (심지어 사용자도) 프로세싱 단계를 변경하거나 혹은 재조합할 수 있어야 한다.
2. 작은 프로세싱 단계들은 서로 다른 상황에서 거대한 컴포넌트보다 쉽게 재사용된다.
3. 인접하지 않은 프로세싱 단계들 간에는 정보를 공유하지 않는다.
4. 입력데이타는 각기 다른 소스에서 얻는다.
5. 여러가지 방법으로 결과를 표시하거나 저장 할수 있어야 한다.
6. 단계를 멀티프로세싱 방식으로 처리하는 것도 수용할 수 있다. 예를 들어 병렬 혹은 준 병렬 방식을 체택할 수 있다.


절차는 아래의 단계를 밟는다.
1. 시스템의 테스크를 일련의 프로세싱 단계에 따라 구분한다.
2. 각 파이프 사이로 전달할 수 있도록 데이터 포맷을 정의한다.
3. 각 파이프를 어떻게 연결할지 결정한다. (Push? Pull?)
4. 필터를 설계하고 구현한다.
5. 오류 핸들링을 설계한다.
필요할 경우 단계의 중간에 파일에 추가적인 정보를 남기는 것도 고려할 수 있다.


PAF는 아래와 같은 장단점이 있다.

장점

1. 필터 교환에 유연하다.
2. 재조합에 유연하다.
3. 필터 컴포넌트를 재 사용할 수 있다.
4. 파이프라인의 프로토 타입을 빠르게 만들 수 있다.
5. 병렬 프로세싱의 효율성

단점

1. 상태정보를 공유하게 되면 비용이 많이 들며 유연성을 저하시키는 요인이 된다.
2. 병렬 프로세싱을 사용해 효율성을 얻을 것이라는 기대는 그저 망상일 뿐인 경우가 종종있다.
3. 데이타 변환에 과부하가 발생한다.
4. 오류 핸들링을 구현하기 힘들다.


 

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

프로그래밍 패러다임  (0) 2009.03.12
아키텍쳐 패턴 - Broker 패턴  (0) 2009.03.12
아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Posted by bleujin


GOF의 객체 패턴을 확대하면 아키텍쳐도 몇가지의 패턴을 가진다는 걸 알수 있다.

가장 대표적으로 Layer 패턴을 들 수 있는데 시스템의 규모가 커서 분해(decomposition) 할 필요가 있고 하위 레벨과 상위 레벨의 이슈가 서로 혼재해 있을때 나온다. 앞글의 실험실과 비실험실은 결국 레이어 패턴과 비슷하지만 굳이 실험실과 비실험실이라고 부른 것은 기계적인 효율성이 필요한 곳과 아닌곳을 구분하려고 실험실이라는 용어를 사용하였다. 레이어 패턴의 대표적인 예는 많이 알려진 OSI 7 Layer가 있고 Java의 VM자체가 하나의 Layer이다.

레이어 패턴은 아래와 같은 특징이 있다.

1. 나중에 소스 코드가 변경하다고 하더라도 그것이 시스템 전체에 파문을 일으켜서는 안된다. 그런 변경이 있더라도 오직 하나의 컴포넌트 내에만 국한되어야 하며 다른 컴포넌트에는 영향을 미쳐선 안된다.

2. 인터페이스는 안정성(stability)을 갖추어야 하며, 표준 구현부에 미리 지정되어 있는 것이 좋다.

3. 시스템의 각 부분들은 교환가능해야 한다. 일반적으로 변경에 대비해 설계하면, 시스템을 발전시키도록 촉진할 수 있다.

4. 현재 설계하고 있는 시스템과 동일한 하위 레별의 이슈 때문에 추후에 다른 시스템을 구축해야 할 필요가 있을지 모른다.

5. 각 컴포넌트는 이슈 하나만을 담당하도록 구현되어야 한다. 만약 한 컴포넌트가 다른 이슈를 구현하면 일관성을 잃게 된다.

6. 컴포넌트 경계를 넘나드는 처리가 발생할수록 성능을 하락시킬 가능성이 커진다.


"전산학의 모든 문제는 또 다른 수준의 간접층으로 해결할 수 있다."
라는 유명한 말을 통해 레이어의 개념을 알린 이는 램슨이라고 알려져 있는데 램슨은 사실 서브루틴의 개념을 만든 휠러에게서 따왔다고 밝힌바 있다. 휠러는 그 말에 이어서
 "그러나 그러면 또 다른 문제가 생기는 것이 일반적이다."
라며 간접과 계층화는 공간과 시간의 부담을 추가하고 코드의 가독성을 해칠수 있는 문제점도 같이 언급하였다.

보통의 경우에는 공간과 공간상의 추가 부담은 그리 크지 않기 때문에 일반적으로 큰 관심사가 되지 못한다. 대부분의 경우 추가적인 포인터 참조나 서브루틴 호출에 의한 시간 지연은 전반적인 구조 개선에 비할때 사소한 수준에 그친다. 사실 요즘의 현대적인 프로그래밍 언어들은 추가적인 유연성을 얻기 위한 목적으로 일부 연산들의 경우 항상 하나의 간접층을 거치도록 하는 경향을 보이고 있다. 예를 들어 java나 C#의 경우 객체에 대한 모든 접근이 하나의 포인터 간접을 거치게 하는데, 이는 쓰레기 수거를 위한 것이다. 또한 Java에서는 인스턴스 메서드에 대한 거의 모든 호출이 하나의 조회 테이블을 통해서 분배되는데, 이는 다른 클래스를 상속하는 클래스들이 실행시점에서 메서드를 재 정의 할 수 있도록 하기 위한 것이다.

모든 객체 접근과 메서드 호출에 부가되는 이러한 추가부담에도 불구하고 두 플랫폼은 시장에 선전을 펼치고 있다. 어떤 경우에서는 개발자가 코드에 집어넣은 간접을 컴파일러가 최적화를 통해서 제거하기도 한다. 대부분의 컴파일러들은 함수를 호출하는 것이 함수의 코드를 호출지점에 직접 삽입하는 것(소위 인라인 함수_보다 비싼 경우 자동적으로 그러한 인라인 처리를 수행한다.

반면 코드의 가독성에 대한 간접의 영향은 아주 중요한 문제이다. 지난 50년간 CPU의 속도는 엄청나게 빨라진 반면 코드를 이해하는 사람의 능력은 별로 발전하지 않았다는 점을 감안한다면 충분히 이해할 수 잇을 것이다. 그래서 애자일 프로세스 옹호자들은 오늘이 구체적인 요구가 아니라 미래에 생길 수도 있는 애매하고 명시되지 않는 요구사항들을 처리하기 위해 계층들을 도입할 때에는 아주 신중해야 한다고 조언한다. 스몰더스는 이에 대해 성능 안티패턴을 논의하면서 계층은 케이크를 위한 것이지 소프트웨어를 위한것이 아니다 라고 비꼰 바 있다.


레이어 패턴은 다은과 같은 단계를 거친다.

# 테스크를 레이러로 묶는 추상기준을 정의해야 한다. 이 추상기준을 플랫폼으로부터 개념적 거리(conceptual distance)로 삼는 경우가 많은데 실제 소프트웨어 개발에서는 하드웨어로부터의 거리와 개념적 복잡성의 추상적 기준을 혼합하여 사용한다.

1. 사용자를 위한 시각적 요소
2. 특수하게 지정된 애플리케이션 모듈
3. 공통 서비스 레벨
4. 운영체제 인터페이스 레별
5. 운영체제
6. 하드웨어

# 추상 기준에 따라 얼마나 많은 추상 레별로 나눌지를 결정하고 레이어마다 역할을 부여하고 테스크를 할당한다. ( 상당한 경험과 통찰력이 필요하다.)

# 서비스를 상세히 정의한다.
  레이어 패턴을 구현할때 가장 중요한 원칙은 레이어들을 서로 엄밀히 구분해야 한다는 점이다.  기반레이어는 가볍게 유지하는 대신 상위 레이어는 광범위한 적용석을 확보하도록 확장하는 것이 좋다. 이런 현상을 재사용의 역 피라미드 구조라고 한다.

# 레이어에 대한 정의를 개선해 간다.

# 각 레이어마다 인터페이스 하나씩을 정의한다.

# 개별 레이어를 구조화한다.

# 인접한 레이어들 간의 통신 방식을 정의한다.

# 인접한 레이어들을 서로 분리시키고 오류 핸들링 전략을 설계한다.

레이어 패턴을 사용하지만 부분적으로 터널을 사용하는 경우가 있는 일종의 변형코드를 사용하는 완화된 레이어 시스템을 사용하기도 하는데 유지보수성을 포기하는 대가로 유연성과 성능을 확보하게 된다. 어플리케이션보다 인프라 시스템에서 종종 이런 편법을 찾아볼수 있다. 이러한 편법이 종종 허용되는 이유는 인프라 시스템이 어플리케이션 시스템보다 변경이 빈번하지 않으며 대체로 성능을 유지보수성보다 중요하게 여기기 때문이다. 그리고 앞서 소개한 DB Framework에도 일종의 터널링이 존재하지만 일종의 예외이기 때문에 주의해서 사용해야 한다.


레이어 패턴은 아래와 같은 장단점이 있다.

장점
1. 레이어를 재사용할 수 있다.
2. 표준을 지원한다.
3. 종속성을 국지적으로 최소화한다.
4. 교환가능성이 확보된다.


단점
1. 동작이 변경될 경우 단계별로 재작업이 필요하다.
2. 효율이 낮다.
3. 불필요한 작업이 수행될 수 있다.
4. 레이어의 적절한 개수나 규모를 결정하는 것이 어렵다.



 

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

아키텍쳐 패턴 - Broker 패턴  (0) 2009.03.12
아키텍쳐 패턴 - Pipes and Filter 패턴  (0) 2009.03.11
Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Design Principle - SRP  (0) 2009.02.22
Posted by bleujin



클래스와 멤버에 대한 접근은 최소화하라

정보은폐는 시스템을 구성하는 모듈 사이의 의존성을 줄여, 각 모듈을 따로 개발하고, 테스트 하고, 최적화하고, 쓰고, 이해하고, 수정할 수 있게 해주기 때문에 매우 중요한 개념이다.


private
package-private
protected
public
[published] - SRP


public 클래스는 public 인스턴스 변수를 가지지 않아야 한다. (쓰레드에 대해 안전하지 않다.) 접근성은 최대한 줄여야 한다. 아주 조심스럽게 최소의 public API를 설계한 다음, 쓸데없는 클래스, 인터페이스, 멤버가 API의 일부가 되지 않도록 해야 한다. public static final 필드가 아니라면 클래스에 public 필드가 있어선 안된다. 또, public static final 필드라 해도 이 필드는 기본타입이거나 불변 객체만 참조해야 한다.

public static final String[] VALUES = {"1", "2", "3"} ;
이를테면 위와 같은 코드는 실제로 final 하지 않다. 배열은 레퍼런스 참조이므로 실제 값이 변동되는걸 막을 수는 없다.

private static final String[] PRIVATE_VALUE = {"1", "2", "3"} ;
public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUE)) ;
만약 정말로 final 하게 유지하려면 위와 같이 unmodifiableList 를 사용해야 한다.


상속보다 컴포지션을 써라

상속은 비록 강력한 기능이긴 하지만 캡슐화를 위반하기 때문에 문제를 발생시킬 수 있다. 상위클래스와 하위 클래스가 정말 서브타입 관계가 있을 때만 상속을 쓸 수 있다. 하지만 이런 경우라도 하위 클래스와 상위 클래스가 다른 패키지에 있거나 상위 클래스가 상속을 위해 설계되지 않았다면 상속받은 하위 클래스에 문제가 생기기 쉽다. 컴포지션과 포워딩을 쓰면 상속의 폐해를 막을 수 있다. 특히 적절한 인터페이스가 있어서 Wrapper 클래스를 구현할 수 있다면 금상첨화이다.

Case1)

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

public class InstrumentHashSet extends HashSet {
  private int addCount = 0;

  public InstrumentHashSet() {
  }

  public InstrumentHashSet(Collection c) {
    super(c);
  }

  public InstrumentHashSet(int initCap, float loadFactor) {
    super(initCap, loadFactor);
  }

  public boolean add(Object o) {
    addCount++;
    return super.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return super.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }
  
  public static void main(String[] args) {
    InstrumentHashSet s = new InstrumentHashSet() ;
    s.addAll(Arrays.asList(new String[]{"A""B""C"})) ;
    System.out.println(s.getAddCount()) ;
  }
}

위의 main함수를 실행시키면 기대와는 달리 3이 아니라 6이 나온다. 왜냐하면 Hashset의 addAll은 Collection의 수만큼 반복하여 add함수를 실행시키기 때문에 count가 2씩 올라가기 때문이다. 이렇게 인터페이스가 아닌 컨크리트 클래스의 상속을 사용하면 슈퍼클래스의 구현에 대해 알고 있지 않으면 이처럼 실수를 하기 쉽다.


Case2) Wrapper Class(Decorator Pattern)

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class InstrumentHashSet implements Set {

  private final Set s;
  private int addCount = 0;

  public InstrumentHashSet() {
    s = new HashSet();
  }

  public InstrumentHashSet(Set s) {
    this.s = s;
  }

  public boolean add(Object o) {
    addCount++;
    return s.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return s.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }

  // foward method
  public void clear() {
    s.clear();
  }
  
  ......
  

  public static void main(String[] args) {
    InstrumentHashSet s = new InstrumentHashSet();
    s.addAll(Arrays.asList(new String[] { "A""B""C" }));
    System.out.println(s.getAddCount());
  }
}

좀더 좋은 구현은 위와 같다. Case2)는 인터페이스를 구현해서 자신이 구현할 메소드는 직접 구현하고 나머지 메소드는 컴포짓을 사용해서 위임을 사용한다.

정말 모든 B가 A인가?는 LSP 이야기에서 나오겠지만 지키기기 생각만큼 쉽지는 않다. Java API에도 이런 실수가 보이는데 이를테면 Properties는 Hashtable을 상속받고 있다. Properties의 경우 String만 key와 value로 받아들인다는 불변규칙이 있지만 상위 클래스인 hashtable에는 이런 조건이 없다.

이럴경우....
  Hashtable hs = new Properties();
  hs.put(new Integer(1), Calendar.getInstance()) ;

와 같은 코드 작성시 혼란이 올수 있다. 이때  hs는 Properties 임에도 불구하고 key와 value로 Object를 받아버린다.


컨크리트 클래스보다는 추상클래스를, 추상클래스보다는 인터페이스를 써라

이미 존재하는 클래스가 새로운 인터페이스를 구현하도록 고치는 것은 어려운 일이 아니다. 하지만 이미 존재하는 클래스가 새로운 추상 클래스를 상속받도록 고치는 것은 이미 배포된 프레임워크에서는 거의 불가능한 일이다. Sun도 Properties가 Hashtable을 상속받는건 잘못이라는 걸 알았지만 이미 고칠수가 없다.

다양한 구현이 가능한 타입을 정의할때 인터페이스를 쓰는 것이 가장 좋다. 단, 쉽게 기능을 추가할 수 잇는 것이 유연성과 강력함보다 더 중요한 경우에는 추상 클래스를 쓰는 것이 좋다. 하지만 그 한계를 이해하고 수용할 수 있을때만 추상 클래스를 사용해야 한다. 외부에 제공하는 중요한 인터페이스에 대한 기본 뼈대 구현을 제공하는 것이 좋다. public 인터페이스는 신중하게 설계해야 하고, 다양하게 구현을 통한 철저한 시험을 거친후 발표해야 한다. 인터페이스와 추상 클래스의 장점만 결합해서, 외부에 제공하는 중요한 인터페이스들에 대해 기본 뼈대 구현(Skeletal implementation_을 해놓은 추상 클래스를 제공할 수 있다.(AbstractCollection, AbsgtractSet, AbstractList....)

여기서 본질적인 질문을 해보자.
추상클래스와 인터페이스는 무슨 차이가 있는가 ? 문법상으로 추상 메소드만 있다면 인터페이스와 비슷한 역할을 할 수 있다. 인터페이스를 쓰면 Mixin 타입을 정의할 수 있고 계층 구조가 없는 타입 프레임 워크를 만들 수 있지만 그런 차이를 넘어서 본질적인 차이는 무엇일까? 인터페이스를 써야할까 추상 클래스를 써야 할까 를 고민할때 선택하는 기준은 무엇인가? 극히 개인적인 의견이긴 하지만 클래스 이름이 명사는 추상클래스를 동사와 형용사는 인터페이스를 사용한다. 명사는 보통 속성을 가지고 있고 실체가 있는 경우가 보통이지만 동사와 형용사는 보통 실체가 없는 동작이나 관점이기 때문에 변할 소지가 다분하다.


인터페이스 타입으로 객체를 참조하고 리턴해라

// better
public List newSubscriber(){
    return new Vector() ;
}

// bad
public Vector newSubscriber(){
    return new Vector() ;
}

적절한 인터페이스가 없다면, 인터페이스 타입이 아닌 클래스 타입으로 객체를 참조해도 괜찮다. 예를 들어 String이나 BigInteger와 같은 값 클래스가 다양한 구현체를 가진다는 것은 상상하기 어렵다. 이 클래스들은 대부분 final이고, 해당하는 인터페이스를 가진 경우는 겨의 없다. 따라서 값 클래스는 인자, 변수, 리턴타입으로 쓸수 있다.

어떤 프레임워크의 기반타입이 인터페이스가 아니라 클래스라면 클래스 타입으로 객체를 선언할 수 밖에 없을 것이다. 하지만 이런 클래스 기반 프레임워크에 속하는 객체라도 구체적인 클래스 타입이 아닌 추상 클래스인 기반 클래스 타입으로 참조하는 것이 좋다. 


클래스의 상태를 요약하는 toString()을 작성해주면 편하다.

public String toString(){
   return "a:" + a + ", b:" + b + ", c:" + c;
}
대부분의 JVM은 최적화 작업을 해주기 때문에 위의 String 연산은 보이는만큼 많이 일어나지 않는다.

# Bad
public String toString(){
   String s = "a:" + a ;
             s += ", b:" + b ;
             s += ", c:" + c ;
   return s ;
}
그러나 위 코드는 JVM이 최적화 작업을 해주기 사실상 어렵다.



 

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

아키텍쳐 패턴 - Pipes and Filter 패턴  (0) 2009.03.11
아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
나쁜 디자인의 징후  (0) 2009.02.22
Design Principle - SRP  (0) 2009.02.22
Method Design  (0) 2009.02.11
Posted by bleujin


Rigidity ? 변경하기 어렵다.

Fragility ? 깨지기 쉽다.

Immobility ? 재사용하기 어렵다.

Viscosity ? 올바른 일을 하기 어렵다.

Needless Complexity ? 과도한 디자인

Needless Repetition ? Copy&Paste?

Shotgun surgery 수정을 하려 보니 여러 클래스를 수정해야 하더라. 하나의 책임이 여기저기 흩어져 있는 경우

Divergent Change 한 클래스가 이런 저런 이유로 자주 수정되어야 하더라. SRP의 위반

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

아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
Class Design  (0) 2009.03.07
Design Principle - SRP  (0) 2009.02.22
Method Design  (0) 2009.02.11
몇가지 프로그래밍 조언  (0) 2009.02.10
Posted by bleujin


객체지향에서 디자인이란 객체(클래스)들에게 적절히 책임(responsibility)을 분배한 뒤 객체들 간의 의존 관계를 관리(dependency management)하는 작업이다. 이때 객체지향 원리는 책임 분배와 의존관계 관리의 일종의 가이드 라인 역할을 하게 된다.

객체지향에 잘 알려진 원리는 SRP, DIP, LSP, OCP, ISP가 있으며 이 원리는 결국 DRY 성배를 추구하게 된다.
DRY는 객체지향뿐 아니라 모든 프로그래밍 분야에 적용될 수 있는데.

- DRY는 하나의 요구사항은 한곳에 두어야 한다는 원리이다.
- 사실 각 기능과 요구사항을 한 번만 구현하려고 노력하는것이다.
- DRY는 시스템의 각 정보와 기능을 말이 되는 하나의 장소에 두는 것을 의미한다.
- 어떤 지식 한 조각도 하나의 시스템 안에서는 모호하지 않고, 권위 있고, 단 하나뿐인 표현을 가져야 한다.

DRY 이야기는 많이 했으니 원리 얘기로 다시 돌아가서 그중에 SRP을 먼저 보자.

SRP는 Single Responsibility Principle의 약자로 클래스(혹음 메소드 혹은 패키지 혹은 컴포넌트)는 하나의 책임만을 맡아야 한다는 원리다. 이때 책임(responsibility)이란 뜻은 변경의 원인(reason to change)을 말하며 결국에 "클래스 변경의 원인은 하나이어야 한다."라고 할 수 있다.

시스템의 모든 객체는 하나의 책임만을 가지며, 객체가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 집중되어 있어야 한다. DRY는 하나의 기능을 한 곳에 두자는 내용이고 SRP는 클래스가 한가지 일만 잘하게 하자는 내용이며 응집도는 사실 SRP의 다른 이름이다.

SRP의 키워드는 책임으로 요약되는데, 그렇다면 책임이란 무엇일까? 책임이란 ‘변경을 위한 이유’이다. 만약 하나의 클래스에 변경을 위한 두 가지 이상의 이유가 있다면 그 클래스는 한 가지 이상의 책임을 갖고 있는 것이다. 그러나 아까 말했듯이 클래스 뿐 아니라 패키지 혹은 컴포넌트에도 같은 원리를 적용할 수 있다. 그러면 두개의 클래스를 가지고 있는 패키지는 이 원리를 위반하기 때문에 하나의 클래스만 가져야 한다는 것인가? 아니다 그렇지 않다. 동등한 추상화 레벨의 책임을 2개 이상 가져서는 안된다는 뜻이다.

SRP는 하나의 클래스에 한 가지 책임을 가르치는 원칙이다. 우리는 설계 관점에서 우리가 인식하지 못하는 SRP 위반을 자주 하게 된다. 이 위반을 경계하기 위해 깊은 통찰력이 필요하지도 않다. 단지 머리에 ‘책임’이란 단어를 상기하는 습관이면 된다

위반 사항에는 대가가 따른다. SRP를 위반할 경우 따르는 재앙은 첫 번째로 ‘왕따’가 발생한다는 것이다. 잔고 클래스가 이율 관리 애플리케이션과 배포됐을 때 확실히 ‘환율 계산’ 메쏘드는 소외된다. 즉 만약 A라는 책임과 B라는 책임을 갖고 있는 클래스가 있을 경우 A만 필요로 하는 애플리케이션은 항상 B를 들고 다녀야 한다.

문제는 여기서 그치지 않는다. 두 번째 재앙은 무관한 메쏘드에 변경이 발생할 경우 불필요한 변경 임팩트가 전달된다. 만약 ‘환율 계산’ 메쏘드가 변경됐을 경우 이율 관리 애플리케이션은 사용하지도 않는 ‘환율 계산’ 메쏘드 때문에 다시 컴파일해야 하고 리테스트해야 하며 재배포해야 한다. 이율 관리와 전혀 무관한데도 불구하고... 사실은 이 임팩트의 영향은 더 심각한데 다음에서 살펴보겠다.

하지만 무조건 책임을 분리한다고 SRP가 적용되는 건 아니다. 가령 데이터 맵퍼 클래스의 메쏘드들이 각각의 insert, delete, update, load 클래스로 분리됐을 경우를 생각해 보자. 마치 절차적 언어에서와 같은 함수 단위의 클래스가 될 것이다. 각 메쏘드 역할에 따른 책임들이 분리되었지만 설계는 장황해지고 관계는 복잡해진다. 그래서 이 문장은 틀린 문장이다. 동일한 책임을 갖는 여러 메쏘드들이 분리된 것이다. 즉 분리의 기준은 책임이며 분리의 목적은 복잡도 감소에 있다.



‘높은 응집도, 낮은 결합도(High Cohesion, Loose Coupling)’의 원리는 1970년대 Larry Constantine과 Edward Yourdon이 정의했던 아주 고전적인 원리이다. 이것은 현재 모든 소프트웨어 시스템 고유의 유지보수성과 적응성을 측정하는 가장 좋은 방법으로 사용되고 있다. 소프트웨어 디자인뿐만 아니라 아키텍처 평가에도 이 원리가 기준이 되는데, 그 이유는 이 원리의 적용 효과가 아주 명백하기 때문이다.

이 원리의 예외는 거의 찾아보기 힘들만큼 보편성을 가지고 있어서 마치 물리학의 엔트로피 법칙처럼 절대적인 기반원리를 제시한다. 낮은 응집도를 갖는 구조는 변경이나, 확장 단계에서 많은 비용을 지불해야 하며 높은 결합도의 경우도 마찬가지이다.

응집도는 ‘하나의 클래스가 하나의 기능(책임)을 온전히 순도 높게 담당하고 있는 정도’를 의미하며 이들은 서로 조화될수록 그 구조는 단순해진다. 응집도가 높은 동네에서 내부 개체가 변했을 때 다른 개체에 충격을 주는 것은 오히려 당연한 징후이다. 이들은 하나의 책임아래 서로 유기적인 관계를 갖고 있기 때문에 내부 개체가 변했을 때 다른 개체의 변경 확률이 높아진다. 마치 예쁜 부츠를 사면 부츠에 어울리는 치마를 입어야 하듯이…


이와 반해 결합도는 ‘클래스간의 서로 다른 책임들이 얽혀 있어서 상호의존도가 높은 정도’를 의미하며 이들이 조합될수록 코드를 보기가 괴로워진다. 이유는 서로 다른 책임이 산만하고 복잡하게 얽혀있기 때문에 가독성이 떨어지고 유지보수가 곤란해지기 때문이다. 이유는 필요 없는 의존성에 있다. 마치 키보드의 자판 하나가 고장나도 키보드 전체를 바꿔야 하는 것처럼. 하나의 변경이 엄청난 민폐를 야기하는 관계이다.



SRP 위반의 악취는 다움과 같다.

1. 여러 원인에 의한 변경
여러 원인에 의한 변경은 한 클래스를 여러 가지 다른 이유로 고칠 필요가 있을 때 발생한다. 즉, 하나의 클래스에 여러 책임이 혼재하고 있어서 하나의 책임의 변화가 다른 책임에게 영향을 준다. 그리고 이 책임이 두 개보다 훨씬 많은 여러 개로 혼재된다면 이 클래스는 심각한 고문관이 된다. 더욱이 이 구조는 더 괴로운 경우로 심화될 수 있다.

2. 산탄총 수술
산탄총을 발사하면 하나의 탄환이 부서지면서 여러 개의 탄환으로 확산되어 발사된다. 따라서 (상상하기도 싫지만) 산탄총을 맞은 대상의 총상은 온몸 전체에 퍼지게 된다. 만약 이런 환자를 수술하는 의사는 마치 수십 발의 총을 맞은 환자를 수술하는 것처럼 힘들 것이다.

‘산탄총 수술(shotgun surgery)’은 ‘여러 원인에 의한 변경’과 비슷한 듯 하면서도 정 반대의 내용을 갖는다. ‘여러 원인에 의한 변경’이 하나의 클래스가 여러 변경 원인(책임)을 지니는 반면, 산탄총 수술은 어떤 변경이 있을 때 여러 클래스를 수정해야 하는 증상이다. 즉 어떤 변경의 대상이 여러 곳에 분포되어 마치 산탄총 총상 환자를 수술해야 하는 것 같은 많은 노동비용이 따른다.

‘산탄총 수술’이 괴로운 이유는 단지 수술 부위가 많다는 것만이 아니다. 이 수술을 했음에도 불구하고 혹시 치료하지 못한 상처가 존재할 수 있다는 가능성이 ‘산탄총 수술’의 더 큰 위험성이다. 가령 하나의 테이블을 조작하는 DB 처리문이 애플리케이션 전역에 퍼져 있는 상황에서 DB 테이블의 구조가 바뀌게 됐을 경우에 발생하는 재앙과 같다. 수술도 고되지만 모든 환부를 찾아야 하는 집중력과 긴장감이 개발자를 더욱 힘들게 한다.


산탄총 수술의 냄새는 특히 설정 정보(configuration information), 로깅(logging), DB 처리에서 발생하기 쉬운데 이들을 다룰 때는 항상 산탄총 수술의 악취를 경계해야 한다. 예를 들어 한 곳에서 관리할 필요가 있는 설정 정보를 여러 클래스에서 나누어 처리하고 있다면 이는 산탄총 수술을 할 수 있는 좋은 본보기가 된다.

이를테면 쓰레드, 커넥션, 오브젝트 풀의 크기 값이나 DB, 서버의 주소 정보들을 각각의 클래스에 자체적으로 관리하고 있다면 이들을 설정 파일이나 설정 관리자에게 Move Field하는 것이 바람직하다. 더 나아가 플러그인을 도입해 설정 정보를 통해 동적으로 행위 변화를 통제(Enable Configurable Behavior with Plugin)하는 것도 생각해 볼만하다. 또한 XML 처리나 프로토콜 해석을 담당하는 메쏘드가 여러 곳에 분포되었다면 각각의 유틸성 클래스로 Move Method하는 것이 바람직하다.



다만 주의해야 할 것은
1. 하지만 너무 무리해서 책임을 나누지 말아야 한다. 책임은 변화의 축이며, 하나의 요구사항 변경은 하나의 책임의 변경을 적시하는 경우가 많다. 책임의 입자도가 매우 세밀하다면 변경에 대한 영역이 그만큼 커지게 된다. 변화가 예측되는 곳, 변화에 효율적으로 대응할 수 있는 크기에서 책임을 할당하는 것이 좋다.

2. 하나는 온전한 하나이어야 한다. 하나의 클래스가 여러 개의 책임을 맡는 것도 곤란하지만, 하나의 책임을 여러 클래스로 분할하여 할당하는 것도 곤란하다.





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

Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Method Design  (0) 2009.02.11
몇가지 프로그래밍 조언  (0) 2009.02.10
Bleujin Framework 활용 : HTML Parsing  (0) 2009.01.16
Posted by bleujin