이전 포스트 Dependency Inversion Principle 에서 의존하는 모듈, 레이어 사이에 추상 (interface 또는 abstract class)을 의존함으로써 구현의 상세(detail)를 의존할 때보다 느슨한 커플링을 만들 수 있고,  구조적 설계와 비교할 때 의존의 방향이 역전 되었다라고 하며, 의존의 전이를 끊었다라고 말했습니다.

Inversion of Control 은 프레임워크가 정의한 추상(interface 또는 abstract class)을 클라이언트 코드가 구현을 하고, 구현된 객체를 프레임워크에 전달(또는 주입) , 프레임워크가 제어를 가지게 함으로써 클라이언트 코드로 부터 제어의 수를 줄이게 하는 것이라고 말할 수 있습니다. 모든 제어를 클라이언트 코드가 가지고 있는 구조적 설계와 비교해 프레임워크가 제어를 가지는 것을 제어가 역전 되었다 라고 말합니다.

Invsersion of Control Container 란 무엇일까요? Invsersion of Control 을 담고 있는 상자 일까요?



용어의 개념을 왜곡하지 않고 가능한 원래의 의미를 알기 위해 마틴 파울러의 [ Inversion of Control Containers and the Dependency Injection pattern] 의 글을 번역 인용하여 IoC Container를 설명하고자 합니다.

콜론(:) 구분자로 저장된 영화의 목록에서 특정 감독의 영화를 조회하는 예제는 아래와 같습니다.

 public class MovieLister
 {
        private MovieFinder finder;
        public MovieLister()
        {
            finder = new ColonDelimitedMovieFinder("Movies1.txt");
        }
 
        public List MoviesDirectedBy(String arg)
        {
            List items = new List();
            List allMovies = finder.FindAll();
             
            foreach (Movie item in allMovies)
            {
                if(string.Equals(item.DirectorName, arg)){
                    items.Add(item);
                }
            }
 
            return items;
        }
    }
 
    public interface MovieFinder
    {
        List FindAll();
    }
 
    public class ColonDelimitedMovieFinder:MovieFinder
    {
        string sourceFilename = string.Empty;
 
        public ColonDelimitedMovieFinder(string arg)
        {
            this.sourceFilename = arg;
        }
 
        #region MovieFinder 멤버
 
        public List FindAll()
        {
            List items = new List();
 
            //Read From file : sourceFilename;
            //Split with Colon Delemiter
            //Add to items List
            string rawText = File.ReadAllText(sourceFilename);
            foreach (string row in rawText.Split(new char[]{':'}))
            {
                items.Add(new Movie(row.Split(new char[] { '|' })[0],

 row.Split(new char[] { '|' })[1])); 
            }
            return items;
        }
 
        #endregion
    }
 
    public class Movie
    {
        public string Name;
        public string DirectorName;
        public Movie(string name, string directorName)
        {
            this.Name = name;
            this.DirectorName = directorName;
        }
    }
}
 


이 클래스를 나만 사용한다면 깔금하고 멋져보입니다. 하지만, 나의 친구가 이 멋진 기능을 자신의 프로그램에 복사해서 넣고 싶다면 어떤 일이 벌어질까요? 그들이 영화 목록을 콜론으로 구분된 "movie1.txt"라는 이름의 텍스트파일로 정한다면 환상적일  것입니다. 다른 파일이름을 사용하고 싶다해도 속성값 만 변경하면 되니 괜찮습니다. 하지만 영화목록을 위한 완전히 다른 폼을 사용하길 원한다면 : SQL 데이터베이스, XML 파일, 다른 텍스트파일 포멧 , 이 경우에 데이터를 가져오는 다른 클래스가 필요 합니다. 나는 MovieFinder interface를 사용했기 때문에 MoviesDirectedBy 메소드는 변경하지 않아도 됩니다. 하지만 알맞는 finder 인터페이스 구현 인스턴스를 얻을 방법이 필요합니다.


이 상황을 P of EAA 에서는 Plugin 이라고 기술합니다. finder를 위한 구현 클래스는 컴파일타임에 링크되지 않습니다. 내 친구가 어떤 구현을 원할지 모르니 Lister 가 특정 구현과 일하는 대신, 내 손을 떠나 나중에 플러그 될 수 있도록 합니다. 문제는 어떻게 해야 Lister가 구현 클래스를 무시하면서도 여전히 인스턴스와 일할 수 있게 하느냐 입니다.

이것을 실제 시스템으로 확장하자면, 우리는 수십개의 이런 서비스와 컴포넌트를 가지고 있을 것입니다. 이런 경우에 interface 를 통해 이야기하는 방법으로 컴포넌트 사용 방법을 추상화 할 수 있습니다. ( 컴포넌트가 interface를 가지도록 설계되지 않았다면 Adapter를 사용할 수 있습니다.) 


중요한 문제는 이 플러그인들을 어떻게 어플리케이션에 조립하느냐 입니다. 이것이 경량 컨테이너 lightweight Container 가 부상하게 이유입니다. 일반적으로 Inversion of Control을 이용해 해결할 수 있습니다.
 


주: 여기서 경량 컨테이너란 Service Locator, PicoContainer, Spring, Avalon, Guice 등을 말합니다.

주: 여기서 Inversion of Control을 이용해 해결한다는 말은 구현을 선택하는 전략을 가진 객체에게 제어를 넘김으로써 구현을 찾도록 한다는 것을 의미합니다.

주 : 추상을 사용함으로써 커플링을 느슨하게 하는 아이디어는 로버트 C. 마틴의 Dependency Inversion Principle 이 도입된 것입니다.



Inversion of Control 

이 컨테이너들이 "Inversion of Control"을 구현하고 있기 때문에 유용하다고 말할 때 완전히 혼란 스러워졌습니다. Inversion of Control 은 프레임워크의 일반적인 특성입니다. 따라서 경량 컨테이너들이 특별하다라고 말하는 것은, 내 차는 바퀴를 가지고 있기 때문에 특별하다라고 말하는 것입니다.

그렇다면 과연 컨트롤의 어떤 관점이 역전 되었다는 것일까? 내가 처음 제어의 역전을 말했을 때는 사용자 인터페이스의 제어를 의미하는 것이었습니다. 초기의 사용자 인터페이스는 어플리케이션에 의해 제어 되었습니다. 여러분은 "Enter Name" , "Enter Address" 와 같은 순차적인 명령을 가지고 있고, 여러분의 프로그램은 프롬프트를 나타낸 후 각각에 대해 응답을 선택할 것입니다. main loop를 담고 있는 그래피컬 UI 프레임워크를 가지고 작업한다면, 스크린의 다양한 필드를 위한 이벤트 핸들러가 제공되고, 프로그램의 중앙 제어는 UI 프레임워크에게 역전됩니다. 제어는 여러분에게서 프레임워크로 이동한 것이죠.


컨테이너에게 있어서 역전 이라고 하는 것은 컨테이너가 플러그인 구현을 스스로 찾는 것을 의미 합니다. 나의 순진한 Lister는직접 인스턴스화된 finder 구현을 찾습니다. 이렇게 하면 플러그인으로 사용할 수 없게 됩니다. 컨테이너를 사용하면 Lister에 구현을 주입할 수 있습니다. 


심사숙고한 후 우리는 이 패턴을 위한 좀더 구체적인 이름이 필요하다는 결론에 도달했습니다.  제어의 역전 Inversion of Control은 너무 일반적인 용어여서 사람들이 매우 혼란스러어 했습니다. IoC 에 대한 다양한 토론을 거친 결과 우리는 IoC를 Dependency Injection 이라고 이름 붙이기로 했습니다.


마틴 파울러의 [ Inversion of Control Containers and the Dependency Injection pattern] 중 일부 


no more IoC , it`s Dependency Injection!



위의 내용을 요약하자면 :
 

이미 작성된 코드를 다른 어플리케이션에 재사용하거나, 의존하고 있는 객체의 행위를 컴파일타임 이후에 다른 행위로 플러그 하고자 한다면, 클라이언트 객체는 추상을 가지고 작업을 해야 합니다. 이 추상에 구현을 교체할 수 있는 패턴을 마틴파울러는 Plugin 패턴이라고 이름 붙였습니다. 그럼 추상을 구현한 다양한 플러그인 중에 알맞는 플러그인을 선택해야하는 문제가 남게 됩니다.

의존 그래프의 생성과 생성된 의존을 주입 하는 일이 코드의 여러곳에 반복적으로 난립하게 되는 문제이지요, 의존 그래프의 생성과 주입 전략을 가진 객체가 등장 했으니 이것을 경량 컨테이너 라고 하는 놈 입니다. 제어를 넘겨 받은 컨테이너 이므로 Inversion of Control Container 라는 이름을 얻게 되었습니다. 그런데, IoC Container 라는 이름이 너무 일반적인 용어라서 사람들이 너무 혼란스러워 하게 되었고, IoC Container 가 하는 일이 결국에는 의존을 주입하는 일이기 때문에 Dependency Injection 이라 부르기로 결정했습니다.




그럼 의존을 주입하는 방법이 궁금해 질 것입니다. 의존을 주입하는 방법으로는 
1. Constructor Injection
2. Setter Injection
3. Interface Injection 
의 크게 세가지가 있으며, 주입할 객체를 생성하는 설정을 코드에 내재하거나 설정파일(주로 XML 파일) 을 이용하기도 합니다.


이 포스트는 IoC Container 의 개념을 설명하는 목적으로 여기에서 줄이며, 주입하는 방법까지 설명은 마틴 파울러의 
Inversion of Control Containers and Dependency Injection pattern 글과 이 글을 번역 설명한 행복한 아빠 님의  IoC 와 DI 에 대한 블로그 포스트 시리즈 1, 2, 3 포스트를 읽어보시기 바랍니다.

토비님의 블로그와 저서 스프링 프레임워크 3.0 은 IoC  컨테이너의 자세한 내용을 담고 있습니다. 

마틴 파울러의 [Inversion of Control Containers and the Dependency Injection pattern] 글에 대한 여러 개발자님들의 번역이 있군요. 그 중 SKY-TIGER 님의 번역 글을 링크 합니다. 





참고 :
Inversion of Control Containers and Dependency Injection pattern by 마틴 파울

저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트
또 등장 했습니다. "역전 inversion" 이라는 말은 용어 자체가 굉장히 어려운 말입니다. 두가지 뜻을 담고 있으니까요. "역전" 은 두가지를 알아야 이해를 할 수 있는 용어 입니다. "기존"의 상태 와 "역전" 된 상태, 그리고 역전되어야 하는 이유를 알아야 합니다. 이 포스트는 바로 마틴 파울러 martin fowler 의 "제어의 역전 IoC Inversion of Control" 의 문서를 가지고 이야기 합니다.

* IoC 와 IoC Container는 조금 다른 개념으로 다음 포스트에 다룰 예정입니다. 


프로그래밍이란 제어를 순서화 하는 일련의 과정 입니다. 하나의 프로그램은 수천, 수만개의 제어를 가질 수 있습니다. 프로그래머가 다음에 일어날 모든 제어를 알고, 관리한다는 것은 불가능에 가깝습니다. 관련이 깊은 제어와 속성들을 하나로 묶어 객체를 만들고, 객체들이 상호작용 하도록 하자는 것이 객체-지향의 개념이라고 할 수 있습니다. 단위 제어를 객체에 묶었어도 여전히 객체들을 제어 해야 합니다. 어떻게면 프로그래머가 작성하는 코드가 제어의 숫자를 줄일 수 있게 할까?에 대한 고민이 바로 "제어의 역전" 입니다.

재사용 가능한 프로그램을 작성하고 싶다고? 콘트롤을 줄여!



제어의 역전 이란 어떠한 일을  하도록 만들어진 프레임워크에 제어의 권한을 넘김으로써 클라이언트 코드가 신경 써야 할 것을 줄이는 전략입니다. 이것을 제어가 역전 되었다 라고 합니다. 일반적으로 라이브러리는 프로그래머가 작성하는 클라이언트 코드가 라이브러리의 메소드를 호출해서 사용하는 것을 의미 합니다. 프레임워크를 규정하는 특성은 프레임워크의 메소드가 사용자의 코드를 호출 한다는데 있습니다.

여기까지는 이해가 쉽지만, 의문이 생깁니다. 대체 어떻게 프레임워크가 나의 메소드를 호출하지? 전통적인 사고 방식을 가진 우리 프로그래머들은 혼란이 발생합니다.

어떻게 하면 프레임워크가 나의 코드를 호출 할 수 있을까요? 프레임워크는 내가 작성한 코드를 모르잖아!. 

제어를 역전 시키는 (프레임워크가 나의 코드를 호출 할 수 있게 하는) 가장 쉽게 생각할 수 있는 접근 방법은 프레임워크의 event, delegate 에 나의 메소드를 등록 시키는 것입니다. 전달되는 인자와 반환 형식만 일치 한다면, 프레임워크 코드는 내가 작성한 객체와 타입을 고려하지 않습니다. 등록된 메소드만 감지하여 실행 invoke 하는 것입니다.

다른 방법은 프레임워크에 정의 되어 있는 인터페이스 interface, 추상타입 abstract 을 나의 코드에서 구현, 상속 한후 프레임워크에 넘겨주는 것입니다. 프레임워크는 인터페이스와 추상을 알고 있으므로  내가 하고자 하는 일련의 작업을 처리할 수 있습니다. 이는 객체를 프레임워크에 주입하는 것이고, 이를 의존을 주입 dependency injection 한다고 합니다.


마틴 파울러는 그의 글 Inversion of Control Containers and the Dependency Injection pattern 에서 IoC 라는 용어가 너무 일반적이며 모호하므로 앞으로는 Dependency Injection 이라는 용어를 사용하겠다 라고 말하고 있습니다. 만, 이 포스트에서는 IoC 의 원래의 개념 설명에 집중하도록 하겠습니다.


좀더 깊은 이해를 위한 마틴파울러 글의 요약 및 정리는 아래에 이어 집니다.

Inversion of Control , 2005 june 26, by martin fowler

 제어 역행은 여러분이 프레임워크를 확장할 때 종종 만나게 되며, 프레임워크의 특성을 정의 합니다. 사용자로 부터 몇가지 정보를 얻는 커멘드라인 프로그램을 작성한다고 상상해 보세요. 다음과 같을 것입니다.

#ruby
  puts 'What is your name?'
  name = gets
  process_name(name)
  puts 'What is your quest?'
  quest = gets
  process_quest(quest)



이 상호 작용에서 코드는 제어를 하고 있습니다. : 언제 질문을 할 것인가? 응답을 언제 읽을 것인가?  이 결과들을 언제 처리할 것인가? 를 결정합니다.


그러나, 윈도윙 시스템을 위한 작업을 한다면 다음과 같을 것입니다.

require 'tk'
  root = TkRoot.new()
  name_label = TkLabel.new() {text "What is Your Name?"}
  name_label.pack
  name = TkEntry.new(root).pack
  name.bind("FocusOut") {process_name(name)}
  quest_label = TkLabel.new() {text "What is Your Quest?"}
  quest_label.pack
  quest = TkEntry.new(root).pack
  quest.bind("FocusOut") {process_quest(quest)}
  Tk.mainloop()


이 두개의 프로그램은 "제어의 흐름 flow of control" 에서 매우 큰 차이가 있습니다. 커맨드 라인 프로그램에서는 
process_name 과 process_quest  메소드를 호출 합니다. 하지만 윈도우 시스템에서는 폼을 만들 때의 바인딩을 기반으로 나의 메소드를 호출합니다. 제어는 역전 inverted 되었습니다.  내가 아니라 프레임워크가 나를 호출하는 것 입니다. 이 현상을 제어의 역전 Inversion of Control 이라고 합니다.( A.K.A Hollywood Principle - "Don't call us, we'll call you").


프레임워크의 중요한 특성은 사용자가 정의한 메소드가 사용자의 어플리케이션 코드가 호출 하기보다 종종 프레임워크로 부터 호출 되어 진다는 것이다.  프레임워크는 종종 어플리케이션의 행위를 재배치 하고, 순서를 정하는 메인프로그램의 역할을 담당한다. 역전된 제어는 프레임워크가 확장된 뼈대를 제공하는 힘을 제공한다. 사용자는 특정 어플리케이션을 위한 커스터마이즈된 알고리즘 메소드를 프레임워크에 전달한다. 

One important characteristic of a framework is that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user's application code. The framework often plays the role of the main program in coordinating and sequencing application activity. This inversion of control gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application.

-- Ralph Johnson and Brian Foote 


역전된 제어는 프레임워크와 라이브러리를 구분하는 열쇄 입니다. 라이브러리는 여러분이 호출할 수 있는 기능의 집합 set of functions 니다. 오늘날에는 보통 클래스들로 구성하죠.  각각의 호출은 지정된 일을 한후 클라이언트에게 제어를 반환 합니다. 

 
프레임워크는 추상화된 설계와 많은 내장된 행위를 감싸고 있습니다.  그걸 이용하기 위해서는 여러분이 작성한 클래스를 서브클래싱 또는 플러그 함으로써  행위를 프레임워크의 여러장소에 삽입해야 합니다.  그후 프레임워크는 여러분의 코드를 호출 합니다.

여러분의 코드가 호출 되도록 플러그 하는 방법은 여러가지가 있습니다. 위의 Ruby 에서 처럼 이벤트 이름과 Closure 인자를 넘김으로써 메소드를 바인딩 할 수 있습니다. text entry box 가 이벤트를 감지 했을때 clossure는 코드를 호출 합니다. 이 방법은 매우 편리하지만 모든 언어들이 지원하는 것은 아닙니다.

다른 방법으로는 프레임워크가 이벤트를 정의하고, 클라이언트 코드가 이벤트를 구독 subscribe 하게 하는 것입니다. .NET 플렛폼은 사람들이 이벤트를 정의할 수 있도록 언어에서 지원하는 좋은 예 입니다. 여러분은 delegate를 이용하여 여러분의 메소드를 event 에 바인딩 할 수 있습니다.

위의 예는 한가지 경우에 작동하는  접근방법입니다. 하지만 여러분은 때때로 여러개의 메소드가 호출되기를 원할 것입니다. 이 경우에 클라이언트 코드가 관련된 호출을 받을 수 있는 인터페이스 interface를  구현할 수 있도록 
 프레임워크는 인터페이스 interface를 정의 합니다.


EJB는 이런 스타일의 역전된 제어를 제공하는 좋은 예 입니다. 세션빈을 작성할 때, 여러분은 EJB 컨테이이너가 라이프사이클의 여러곳에서 호출 할 수 있도록 다양한 메소드를  구현할 수 있습니다. 예를 들어 세션빈은 (두번째 저장소를 위한) 
ejbRemoveejbPassivate 와 (대기상태에서 리스토어를 위한) ejbActivate  인터페이스를 정의 합니다. 여러분은 이 메소드들을 호출하는 제어권을 가지지 않습니다.  프레임워크가 알아서 합니다. 우리가 호출 하는 것이 아니라, 컨테이너가 우리가 작성한 코드를 호출 합니다.


이는 역전된 제어의 복잡한 예시 입니다. 이 효과를 더욱 단순하게 얻는 방법이 있습니다. template method 가 좋은 방법입니다 : 슈퍼클래스는 제어의 흐름 flow of control 을 정의하고, 서브클래스는 메소드를 오버라이드 하거나 추상메소드를 구현함으로써 확장을 얻을 수 있습니다. JUnit 프레임워크는 setUp과 tearDown 메소드를 호출 함으로써 여러분의 단위테스트를 생성하고 제거 합니다. 프레임워크는 여러분의 코드를 호출하고 반응하게 합니다. - 또 다시 제어는 역전 되었군요. 


오늘날 IoC 컨테이너의 등장으로 역전된 제어 Inversion of Control 의 의미에 몇가지 혼동이 생겼습니다. 어떤 사람들은 역전된 제어 스타일의 일반적인  원칙( 의존 주입과 같은) 에 혼란 스러워 합니다. IoC 컨테이너가 일반적으로 EJB의 경쟁자로 인식되기 때문에 그 이름이 혼란의 원인이 됩니다. EJB는 여전히 역전된 제어를 매우 많이 사용 합니다.


어원 :
내가 아는  역전된 제어 Inversion of Control  용어는 1998, Object-Oriented Programming 저널에 출판된 Johnson 과 Foote's의 논문 Designing Reusable Classes 에서 입니다. 이 논문은 15년이 지난 지금도 읽어볼 만한 가치가 있습니다. 이 용어가 무엇으로 부터 영감을 받았는지 그들도 기억하지 못하지만, 객체-지향 커뮤니티와 GoF의 책에서 살아남아 등장합니다.  더 섹시한 용어인 'Hollywood Principle' 은  1993, Mesa 의  Richard Sweet 의 논문으로 부터 기인합니다. 그가 쓴 "Don't call us, we'll call you (Hollywood's Law)"의 디자인 목적은 : 사용자가 도구에 대해 희망하는 커뮤니케이션을 도구가 고지 notify 해야 한다는 것입니다. 모델에 대한 명령 command 과 실행 execute 을 사용자가 직접 하는 대신에 말이죠. John Vlissides는 column for C++ report에 'Hollywood Principle'에 대한 매우 좋은 설명을 제공합니다. (어원에 대해 도움을 준 Brian Foote와 Ralph Johnson에게 감사합니다.)
 

### 끝.

관련 포스트 :  

 IoC 의 기반 개념을 가진 : 의존 관계 역전의 법칙(DIP) 

참조
Inversion of Control , 2005 june 26, by martin fowler
- Johnson 과 Foote's의 논문 Designing Reusable Classes 은 읽어볼 만한 굉장한 가치가 있습니다. 
저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트
이 포스트의 주제는 이전의 S.O.L.I.D 의  LSP에 이어 의존 역전의 원칙 Dependency Inversion Principle (DIP) 입니다. 포스팅의 동기 또한 LSP의 동기와 같습니다. 개념이해를 돕기  위해 뽑아낸 간략화된 핵심 정의가, 오히려 상세 이해를 방해함을 느꼈습니다. 원칙의 정의만으로는 그 원칙이 의미하는 바를 정확히 이해하기 어렵습니다. 이 포스트는 DIP 원칙이 의미를 설명하는 것입니다. 원칙의 상세를 이해하고, 핵심 정의 구문은 기억을 끄집어 내기위한 실마리로 삼는 것이 가장 좋을 것입니다.

이제 의존 관계 역전의 원칙 DIP :  Dependency Inversion Principle 을 시작하도록 합시다.

먼저 원칙의 이름으로 부터 어떤 원칙인지 힌트를 얻어 보도록 하겠습니다. "의존 관계를 역전 하라는 이야기군, 가만 의존이 뭐지?"


의존이란 무엇인가?

객체-지향 프로그램은 관계를 가지고 있는 객체의 집합 그 자체 입니다. 그 관계를 UML의하면 Multiplicity, Aggregation, Composition, Dependency  4가지의 형태로 일반화 할 수 있습니다. Inheritance 는 종류가 다른 관계라고 생각하도록 하죠.  단순화 하면 객체A 가 객체B를 포함하고 있는데, 그 형태가 위에 언급한 형태 중 하나라는 것이며, 이때 객체A가 객체B를 의존하고 있다 라고 일반화 시켜 말합니다.



의존을 알았으니 추측을 넘어, DIP 의 정의를 알아 보도록 하겠습니다.

A. 고차원의 모듈은 저차원의 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존 해야 한다.

B. 추상화 된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.


더 쉽게 말할 수도 있다. 자주 변경되는 컨크리트 클래스에 의존하지 마라. 만약 어떤 클래스의 참조를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만들어라. 만약 어떤 함수를 호출해야 한다면, 호출 되는 함수를 추상 함수로 만들어라.

일반적으로, 추상 클래스와 인터페이스는 자신에게서 유도된 구체적인 클래스 보다 훨씬 덜 변한다.

Java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p140, 141 


음... 가능한 추상 클래스나 인터페이스를 참조 함으로써 의존을 좀더 느슨하게 만들어라는 얘기군. 알겠어, 앞으로는 최대한 추상 클래스를 만들고, 인터페이스를 끄집어 내서 DIP 를 지키는 개발자가 되겠어!  근데 관계의 역전은 뭘 의미하는 거지? 위의 정의에 역전이라는 말은 언급되지도 안잖아, 에이 머리 아파~ 이 정도만 이해하면 되겠지.

관계의 역전의 뜻에 대해 의문점을 남긴채 찜찜한 상태로 공부를 마무리 하게 됩니다.




자! 이제부터 시작입니다. 역전은 뭘 말하는 걸까요?. DIP 의 성배를 찾으러 갑시다.


DIP를 제시한 로버트 C. 마틴보다 더 잘 설명할 능력이 부족하기도 하고, 지금까지 읽은 문서중 가장 잘 설명하고 있다고 생각하는 마틴의 DIP 문서의 요약 및 해석으로 글을 쓰도록 하겠습니다.



의존 관계 역전의 원칙 The Dependency Inversion Principle 


이전 글에서 Liskov Substitution Principle LSP 에 대해 이야기 했습니다. LSP는 C++ 에서 상속을 사용할 때 일반적인 가이드를 제공하는 것입니다.  파생되는 클래스는 기반 클래스의 가상 멤버 함수의 약속이 파생 클래스에서도 제공되어 져야 한다는 것이고, 이를 위반할 경우 객체의 적절한 작동을 보장하기 위해서는  실제 객체의 타입을 확인해야 절차가 유발되며, 이 행위는 OCP를 위반하게 된다 였습니다.

우리는 OCP 와 LSP를 논했고, 이 원칙들이 엄격하게 지켜져서 얻어지는 구조를 일반화 시킬수가 있는데, 이 구조를 "의존 관계 역전의 원칙 The Dependency Inversion Principle" 이라고 이름 붙이겠습니다.


소프트웨어에 뭐가 잘못 되어가고 있는가? What goes wrong with software?

우리 대부분은 "나쁜 디자인"의 소프트웨어 조각으로 일을 해본 유쾌하지 않은 경험을 가지고 있습니다. 우리중 일부는 그 "나쁜 디자인"의 주인이 자기 자신이었다는 것을 발견하는 더욱 나쁜 경험을 가지고 있을지도 모릅니다. 도대체 디자인을 나쁘게 만드는 것은 무엇일까요?


"나쁜 디자인"의 정의 The Definition of a "Bad Design"

여러분은 다른 사람들에게 자신이 특별히 자랑스러워하는 소프트웨어 디자인을  발표해 본적이 있습니까?  그때 "왜 그런 방법으로 하셨나요?" 라는 말을 들어 본적이 있습니까? 이건 저에게 일어난 일이고, 또 많은 엔지니어들에도 벌이지고 있는 일입니다. 디자인에 동의하지 않는 엔니지어는 "나쁜 디자인"에 대한 정의에 대해 나와 동일한 기준을 가지고 있지 않았습니다. 엔지니어들에게서 발견한 가장 일반적인 기준은 "나는 그런 방법으로 하지 않아" 였습니다.

하지만, 모든 엔지니어들이 동의하는 나쁜 디자인에 대한 기준이 있었으니 다음과 같습니다.

1. 모든 변경마다 많은 다른 부분에 영향을 미쳐 변경 자체가 어렵다. (Rigidity)
2. 변경 작업을 할때 예상치 못한 다른 부분이 망가진다. (Fragility)
3. 현재의 어플리케이션에서 분리할 수 없기 때문에 다른 어플리케이션에서 재사용 하기가 매우 어렵다. (Immobility)

그 외에도 유연성, 견고성 과 같은 설명하기 어려운 요소들이 있었습니다. 따라서 위의 세가지 항목을 "좋고, 나쁨의" 기준으로 삼기로 결정 했습니다.


 "나쁜 디자인"의 원인 The Cause of "Bad Design"

무엇이 디자인을 딱딱하고 rigid, 깨지기 쉽고 fragile, 이동할 수 없게 immobile 만드는 것일까? 그건 모듈 디자인의 상호의존 interdependence 때문입니다. 

변경이 어려운 rigidity 디자인 원인은 강하게 상호의존하고 있는 부분의 단일 변경이 그가 의존하는 모듈에게 연쇄변경을 요구하기 때문입니다. 이러한 일련의 변경이 확장되면 설계자와 유지관리자는 그를 예측, 추정하지 못합니다. 이는 변경 승인을 주저하게 만들고, 디자인은 공식적으로 경화 되었다라고 선언 됩니다.

깨지기 쉬움 fragility 이란 단일 변경의 영향으로 매우 많은 부분이 고장남을 뜻합니다. 개념적으로 관련이 없는 영역에서도 종종 새로운 문제가 발생합니다. 이런 깨지기 쉬움은 설계와 관리조직의 신뢰성을 급격히  떨어트립니다. 사용자와 관리자는 상품의 품질을 예상할 수 없게 됩니다. 한 부분의 간단한 변경이 그와 명백히 관련이 없는 부분에서 실패를 만들어 냅니다. 이런 문제를 해결하는 일 자체는 더욱 문제로 부상합니다. 유지보수 프로세스가 자신의 꼬리를 쫒는 개와 닮았거든요.

이동 불가능한 immobile 디자인이란 이동을 희망하는 어떠한 부분이 원치 않는 다른 부분의 상세를 강력하게 의존하고  있는 것을 말합니다. 디자이너는 새로운 어플리이션에 재사용 될 수 있는지 기존 어플리케이션을 조사 합니다. 이동시키고 싶은 부분을 분리하기 위해 매우 많은 부분으로 부터 분리해야 한다면, 대부분 그러한 디자인은 새로 개발하는 것보다 비용이 높기 때문에 재사용될 수 없습니다.



의존 관계 역전의 원칙 The Dependency Inversion Principle


A. 하이 레벨 모듈은 로우레벨 모듈에 의존해서는 안된다. 둘다 추상에 의존해야 한다.

B. 추상은 상세를 의존해서는 안된다. 상세는 추상을 의존해야 한다.

A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.

B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.


Copy 모듈은 Keyboard 와 Printer 디바이스의 상세를 사용합니다. - 구조적 디자인의 예



내가 왜 "역전 inversion" 이라는 단어를 사용했는지 의문이 생길 겁니다. 바로 구조적 분석, 구조적 디자인으로 대표되는 전통적인 개발 방법 때문입니다.  하이 레벨 모듈이 로우 레벨 모듈을 의존하는 구조, 추상이 상세를 의존하는 구조적 디자인 말이죠. 전통적 설계의 목적은 하이 레벨 모듈이 로우 레벨 모듈을 어떻게 호출할 것인지의 서브프로그램 구조를 정의하는 것 입니다.  [그림 1 ]은 구조적 디자인 구조의 좋은 예 입니다. 그래서 잘 설계된 객체-지향 프로그램의 의존 구조는 전통적 절차적 메소드의 결과로 만들어지는 의존 구조를 "역전 invert"한 형태 입니다.


 하이 레벨 모듈이 로우 레벨 모듈을 의존하고 있는 경우를 고려해 봅시다. 하이 레벨 모듈은 어플리케이션의 중요한 정책 결정과 비즈니스 모델을 담고 있습니다. 모델은 어플리케이션의 정체성을 담고 있죠. 이 모듈들이 로우 레벨 모듈을 의존하고 있고, 로우 레벨 모듈에서 발생하는 변경은 하이 레벨 모듈들에 직접적인 영향을 미치며 마침내 변경을 강요합니다.


이건 완전히 말이 안되는 상황입니다. 변경을 강요해야 하는 것은 하이 레벨 모듈이어야 하며, 하이 레벨 모듈이 로우 레벨의 상급자가 되어야 합니다. 어떤 상황이건 하이 레벨 모듈이 로우 레벨 모듈을 의존 해서는 안됩니다.

더 나아가, 하이 레벨 모듈은 재사용 가능해야 합니다. 우리는 이미 서브루틴 라이브러리와 같은 형태로 로우 레벨 모듈을 재사용하는데 정통해 있습니다. 하이 레벨 모듈이 로우 레벨 모듈을 의존하고 있으면, 하이 레벨 모듈은 문맥이 다른 곳에 재사용되기 어려워 집니다. 그러나, 하이 레벨 모듈이 로우 레벨 모듈에 독립적 이라면, 하이 레벨 모듈은 매우 간단히 재사용 될 수 있습니다. 이것이 바로 프레임워크 디자인의 핵심 입니다.
 



레이어링 Layering

Booch 에 따르면 " 잘 구조화된 객체-지향 아키텍처는 명확하게 정의된 레이어를 가지고 있다. 개개의 레이어는 잘-정의 되고, 통제되는 인터페이스를 통해 응집성 있는 서비스의 집합을 제공한다." 이 문장을 순진하게 해석한 디자이너가 만들어내는 구조는 [그림 3] 과 같을 것입니다. 이 다이어그램에서 하이 레벨 정책 클래스는 로워 레벨 메커니즘을 사용합니다. 이는 상세 레벨의 유틸리티 클래스를 사용하는 형태로 변합니다.  정책 레이어가 변경에 민감해서 유틸리티 레이어에 변경이 전파되는 교활한 특성을 보이더라도 괜찮지만, 의존은 전이적 transitive 입니다. 정책 레이어는 유틸리티 레이어를 의존하는 그 어떤 것(여기서는 메커니즘 레이어)에 의존하고 있습니다.  그래서 정책 레이어는 유틸리티 레이어를 의존하는 불행한 형태가 됩니다.




[그림 4] 는 보다 좋은 모델입니다. 각각의 로워 레벨 레이어들이 추상 클래스로 표현되고 있습니다. 실체 레이어는 추상레이어로 부터 파생 됩니다. 각각의 하이 레벨 레이어는 추상 인터페이스를 이용해 다음 단계의 로워 레벨 레이어를 사용합니다. 따라서 레이어들은 다른 레이어들을 의존하는 대신에 추상 클래스를 의존합니다. 정책 레이어의 의존이 전이되면 유틸리티 레이어만을 망가뜨리는 것이 아닙니다. 정책 레이어가  직접적으로 의존하고 있는 메커니즘 레이어 조차도 망가뜨립니다. 


이 모델을 사용하면 정책 레이어는 메커니즘 레이어나 유틸리티 레이어의 변경으로 부터 영향을 받지   않습니다. 더 나아가,  정책 레이어는 메커니즘 레이어의 인터페이스를 준수하는 로워 레벨 모듈의 정의를 가진 다른 문맥에서 재사용 될 수 있습니다. 의존을 역전시킴으로써 우리는 좀더 유연하고, 견고하고, 이동성 있는 구조를 만들 수 있습니다.


단순한 예제 Simple Example

의존 역전은 한 클래스가 다른 클래스에 메시지를 보낼때 적용할 수 있습니다. button 객체와 lamp 객체를 생각해 보세요. 

button 객체는 외부 환경을 감지하고, 사용자가 버튼을 눌렀는지 여부를 결정합니다. lamp 객체는 외부 환경으로 부터 영향을 받습니다. TurnOn 메시지를 받으면 불을 밝히고, TurnOff 메시지를 받으면 불을 끕니다.

 button 객체가 lamp 객체를 제어하는 시스템을 어떻게 설계할까요?

Figure 5는 이를 구현한 순진한 모델입니다. 

button 객체는 lamp 객체에게 TurnOn, TurnOff 메시지를 보냅니다.  button 클래스는 lamp 클래스 인스턴스와 관계를 가지기 위해서 lamp를 "포함" 합니다. 

 


위 모델의 구현은 아래와 같습니다.

Figure 5 는 의존 관계 역전의 원칙을 위반합니다. 어플리케이션의 하이-레벨 정책이 로우-레벨 모듈과 분리되지 않았습니다. : 추상과 상세가 분리되지 않았다는 말입니다. 분리하지 않으면 하이-레벨 정책이 자동적으로 로우-레벨 모듈을 의존합니다. 추상이 상세를 자동적으로 의존하는 것입니다.


[전통적인 button/lamp model 구현 코드]

namespace TradButtonModel
{
    class Lamp{
        public void TurnOn()
        {
            Console.WriteLine("Trad Lamp 램프를 켭니다.");
        }
        public void TurnOff()
        {
            Console.WriteLine("Trad Lamp 램프를 끕니다.");
        }
    }

    class Button {
        private Lamp itsLamp;

        public Button(Lamp lamp)
        {
            this.itsLamp = lamp;
        }

        public void Detect()
        {
            bool buttonOn = GetPhysicalState();
            if (buttonOn)
            {
                itsLamp.TurnOn();
            }
            else
            {
                itsLamp.TurnOff();
            }
        }

        private bool GetPhysicalState()
        {
            bool isPressed = false;
            //물리적 장치가 눌려저 있으면 true 를 반환
            return isPressed;
        }
    }



}


의존 하는 추상 찾기 Finding the Underlying Abstraction

하이 레벨 정책이란 무엇일까요? 이것은 어플리케이션이 의존하는 추상입니다. 상세가 변한다 해도 변경되지 않는 것을 의미하죠. Button/Lamp 예제에서 의존하는 추상이란 사용자의 on/off 제스처를 감지하고 타겟 객체에게 전달하는 것을 의미합니다. 말도 안됩니다!, 타겟 객체는 또 뭐죠? 타겟 객체는 추상에 영향을 주지 않는 상세 입니다.

의존 관계 역전의 원칙을 적용하려면 문제의 상세로부터 추상을 분리해야 합니다. 그런 다음, 추상이 상세를 의존하는 Figure 5 를 Figure 6 와 같이 변경할 수 있습니다.



Figure 6, 우리는 button 클래스의 상세 구현으로 부터 추상을 격리하였습니다. 


상위 레벨 정책이 완전히 추상 button 클래스만을 가지고 있음을 주목하세요. Button 클래스는 사용자   제스처를 감지하는 물리적 메커니즘을 전혀 모릅니다. 그리고, Lamp 에 대해서도 전혀 모릅니다. 

이 상세들은 추상을 구현한 ButtonImplementation, Lamp 클래스에 의해 추상으로 부터 격리됩니다. 


[DIP를 적용한 button/lamp model 구현 코드] 

namespace InvertedButtonModel
{
    abstract class ButtonClient
    {
        public abstract void TurnOn();
        public abstract void TurnOff();
    }


    class Lamp : ButtonClient
    {
        public override void TurnOn()
        {
            Console.WriteLine("Lamp 램프를 켭니다.");
        }

        public override void TurnOff()
        {
            Console.WriteLine("Lamp 램프를 끕니다.");
        }
    }


    abstract class Button
    {
        private ButtonClient buttonClient;

        public Button(ButtonClient client)
        {
            this.buttonClient = client;
        }
        public void Detect()
        {
            bool buttonOn = GetState();
            if (buttonOn)
            {
                buttonClient.TurnOn();
            }
            else
            {
                buttonClient.TurnOff();
            }
        }
        public abstract bool GetState();


    }

    class ButtonImplementation : Button
    {
        public ButtonImplementation(ButtonClient client):base(client){}

        public override bool GetState(){
            bool isPressed = false;
            //물리적 장치를 감지하여 반환
            return isPressed;
        }

    }
}


결론 Conclusion

의존 관계 역전의 원칙은 객체-지향 기술이 주장하는 장점의 근원입니다. 좋은 어플리케이션은 재사용 가능한 프레임워크를 만들길 요구합니다. 변경에 강한 코드의 구조를 만드는 것은 매우 중요 합니다. 그리고, 추상과 상세를 서로 격리시키기 때문에 코드 관리는 더욱 쉬워 집니다.

의존 관계의 역전 dependency inversion  이란 구조적 디자인에서 발생하던 로워 레벨 모듈의 변경이 하이 레벨 모듈의 변경을 요구는 위계의 관계를 끊자 라는 의미로 쓰여진 역전입니다. 실제의 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만들어야 한다는 원칙입니다.

구현 관점에서 추상 클래스, 인터페이스를 사용하는 것부터 시작하여, 아키텍처 관점까지 확장할 수 있습니다. 예를 들어,  데이터베이스를 연결하는 레이어로 OLEDB를 사용한다면, OLEDB의 인터페이스를 제공하는 물리 DB가  MSSQL 에서 ORACLE 로의 변경되더라도 그 변경이 상위로 전파되지 않는다 라고 이해 할 수 있습니다.
 


하이 레벨과 로워레벨 사이에 추상을 삽입함으로써 로워 레벨의 변경이 하이 레벨로 전파되는 것을 막는다.



끝으로, 


"A. 하이 레벨 모듈은 로우레벨 모듈에 의존해서는 안된다. 둘다 추상에 의존해야 한다." 라는 정의는  "A. 하이 레벨 모듈은 로우레벨 모듈에 의존해서는 안된다 의존 하되, 둘다 추상에 의존해야 한다." 로 해석하는 것이 혼란을 피하는 것이라 생각됩니다.


### 끝.


참고자료 : 
로버트 C. 마틴 The Dependency Inversion Principle.pdf [Martin96] Robert C. Martin, Engineering Notebook, C++ Report
 


책 : java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p140,p141 

 




저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트
리스코프 치환 원칙 LSP Liskov Substitution Principle 은 객체-디자인의 S.O.L.I.D 5원칙 중의 가장 이해하기 어려운 원칙이라고 할 수 있습니다. 다른 원칙들은 이름에서 힌트를 얻을 수 있는데 LSP만은 이름으로 부터 힌트를 얻을 수가 없습니다.

이 글을 읽고 있다면 이미 S.O.L.I.D 를 공부해 본적이 있을 겁니다. 독일어나 러시아어 이름 같은  리스코프 라는 어감은 이 원칙을 이해하면 뭔가 대단한 것을 알고 있다는 느낌이 들것도 같습니다. 이해하기 위해 정의를 수십번 읽어 봐도 대부분의 원칙이란걸 대할때 그러하듯 "그래서 이거 가지고 뭘 하라는 건데" 라는 자조섞인 답변이 들리기도 하죠.  

수 많은 문서와 책,  인터넷의 블로그 글들은 리스코프 치환 (교체) 원칙에 대해 다음과 같이 기술하고 있습니다.

서브타입은 언제나 자신이 기반타입 (base type)으로 교체할 수 있어야 한다.

유도된 클래스의 메소드를 퇴화시키거나 불법으로 만드는 일을 피하라. 기반 클래스의 사용자는 그 기반 클래스에서 유도된 클래스에 대해 아무것도 알 필요가 없어야 한다.
java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p137,p144  에 있는 정의 입니다. 


"서브타입을 기반타입으로 교체할 수 있어야 한다고?  음... 다음 말은 무슨 뜻인지 잘 모르겠고, 상속 받고, 메소드를 빠짐없이 구현하면 되잖아, 기반 타입에 파생 타입을 대입할 수도 있고, 봐~~ 컴파일도 되고, 작동하잖아, LSP 다 됐어!" 라고 많은 개발자들이 넘어 갔지만, 화장실 갔다가 마무리를 짖지 않은 것 처럼 뭔가 찜찜한 기분이 들었다면, 아래의 내용을 한줄도 빠짐 없이 읽어주시길 바랍니다. 



리스코프 치환 원칙이란?

1998년 Barbara Liskov (MIT 컴퓨터 사이언스 교수, 2008 튜링상 수상) 가 제안한 원칙으로 :

타입 S의 객체 o1 과 타입 T의 인스턴스 o2가 있을 때, 어떤 프로그램에서 타입 T의 객체로 P가 사용된다고 하자. S가 T의 서브타입이라면 P에 대입된 o1이 o2로 치환 된다고 해도 P의 행위는 바뀌지 않는다. 

Barbara Liskov wrote LSP in 1988:
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T." - BarbaraLiskov, Data Abstraction and Hierarchy, SIGPLAN Notices, 23,5 (May, 1988).

로버트 C. 마틴의 LSP 정의  :

참조되는 기반클래스의 함수는 파생클래스 객체의 상세를 알지 않고서도 사용될 수 있어야 한다. 

FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE
CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES
WITHOUT KNOWING IT.


리스코프 치환 원칙이 왜 중요한가?

1. 이 원칙을 지키지 않으면 클래스 계층이 지저분하게 될 꺼에요. 서브클래스 인스턴스를 파라미터로 전달 했을 때 메소드가 이상하게 작동 할 수 있습니다.


2. 슈퍼클래스에 대해 작성된 단위테스트가 서브클래스에 대해서는 작동되지 않을 것입니다.




로버트 C. 마틴 Liskov Substitution Principle

로버트 마틴은 LSP를 다음과 같이 소개 하고 있습니다.
개방 폐쇄 원칙 OCP 은 관리가능하고 maintainable, 재사용 reusable 가능한 코드를 만드는 기반이다. 잘 설계된 코드는 기존 코드의 변경 없이 확장 가능하다. OCP 를 가능케 하는 중요 메커니즘은 추상화와 다형성이다. 추상화와 다형성을 가능케 하는 키 메커니즘이 상속이다. 추상 기반 클래스의 순수 가상 함수로 부터 클래스를 상속 파생 시킴으로써 추상화된 다형 인터페이스를 만들어 낼 수 있다.

상속을 적용함에 있어서 디자인 룰은 무엇일까? 최고의 상속 구조의 특성은 무엇일까? 계층 구조를 만듦에 있어 OCP를 위반하게 하는 덧은 무엇일까?


LSP가 바로 상속의 룰 이며, 최고의 상속 구조 특성을 갖추게 하며, OCP를 위반하지 않도록 인도하는 원칙 이라고 말하고 있습니다.  



정사각형과 사각형, 그리고 심각한 위반, Square and Rectangle, a More Subtle Violation.

LSP를 위반하는 사례를 살펴봅시다. 로버트 C. 마틴의 LSP 문서의 사례 입니다. C++ 로 작성된 예제를 C#으로 바꾸었습니다.

아래와 같이 정의된 Rectangle 클래스를 사용하는 어플리케이션이 있습니다.
 public class Rectangle
    {
        protected double itsWidth;
        protected double itsHeight;
    
        public void SetWidth(double w)
        {
            this.itsWidth = w;
        }

        public void SetHeight(double h)
        {
            this.itsHeight = h;
        }

        public double GetWidth()
        {
            return this.itsWidth;
        }
        
        public double GetHeight()
        {
            return this.itsHeight;
        }
    }


이 어플리케이션은 많은 사이트에 설치 되었고, 매우 잘 작동하고 있다고 상상해 봅시다. 매우 성공한 소프트웨어라고 할 수 있습니다. 어느날 사용자가 정사각형 square 요소를 추가해 달라고 요구해 왔습니다. 상속은 ISA 관계이고, 새로 추가할 객체가 기존 객체의 ISA 관계를 만족 시킨다면, 새로운 객체는 기존 객체로 부터 파생 시킬수 있습니다.


명확하게 정사각형은 사각형의 일반적인 의도를 만족 시켰습니다. ISA 관계가 성립되기 때문에, Square 클래스를 Rectangle 클래스로 부터 파생시키는 것은 논리적으로 완벽합니다.

ISA 관계를 사용하는 것은 객체-지향의 기반 기법을 사용하는 것으로 사료됩니다. Square 클래스를 Rectangle로 부터 파생시켜면 문제가 있을 수 있지만, 이런 문제는 실제로 코드에 적용해 보지 않고서는 알 수 없습니다.
 

Rectangle, Square 상속 다이어 그램



우리가  눈치 챈 첫번째 문제는 Square는 itsHeight, itsWidth 의 두개의 멤버 변수가 필요하지 않지만 상속된다는 것 입니다. 이건 명확하게 쓸모 없는 것이고, CAD와 같은 프로그램에서 수백 추천개의 정사각형을 그리게 된다면 문제가 발생할 건 확실합니다.


메모리 효율을 문제 삼지 않는다고 가정합시다. 다른 문제가 있을 까요?  이런! Square 는 SetWidth 와 SetHeight 함수도 상속 받습니다. Square는 가로와 세로가 같아야 하기 때문에 설계의 문제가 발생한 겁니다. 이 문제를 해결하기 위해 아래와 같이 오버라이드 override 할 수 있습니다.
 


  public class Square : Rectangle
    {
        /// 
        /// 정사각형이기 때문에 너비를 할당하더라도 높이도 같이 할당해 줌.
        /// 
        /// The w.
        /// 기반 타입의 SetWidth를 호출 하면 
        /// 기반 타입의 메소드가 호출 됨.
        /// 
        public new void SetWidth(double w)
        {
            base.itsWidth = w;
            base.itsHeight = w;
        }

        /// 
        /// 정사각형이기 때문에 높이를 할당하더라도 너비도 같이 할당해 줌.
        /// 
        /// The h.
        /// 
        /// 
        public new void SetHeight(double h)
        {
            base.itsWidth = h;
            base.itsHeight = h;
        }
    }



누군가 Square 객체에 가로값을 적용한다 해도 높이가 자동으로 적용되고, 정사각형이라는 논리를 위반하지 않습니다.



하지만 아래의 함수를 고려해 보면 문제가 있습니다.


Square 객체의 Height가 변경되지 않을 것이기 때문에 지금까지 쌓아 놓은 것은 무너 집니다. Rectangle의 SetWitdth , SetHeight가 virtual 로 선언 되지 않았기 때문에 발생한 문제 입니다.

 


     /// 
        /// Fs the specified rect.
        /// 
        /// The rect.
        /// 
        /// 
        static void f(Rectangle rect)
        {
            rect.SetWidth(32);
        }




진짜 문제 The Real Problem

이제 우리는 Rectangle, Square 두개의 클래스를 가지고 있고, Square 객체로 어떠한 일을 하더라도 문제가 없어 보입니다. Square 는 수학적으로도 일관성이 있고, Rectangle 도 문제가 없습니다. 

아래의 함수 g를 생각해 봅시다.
 


     /// 
        /// Gs the specified rect.
        /// 
        /// The rect.
        /// 
        /// 
        static void g(Rectangle rect)
        {
            rect.SetWidth(5);
            rect.SetHeight(4);

            //파라미터로 Square 타입이 전달되면 Assertion 에러를 발생 시킵니다.
            System.Diagnostics.Debug.Assert(rect.GetWidth() * rect.GetHeight() == 20);
        }



Rectangle의 메소드를 실행시키는 함수 입니다. Rectangle 객체에 대해서 올바르게 동작하고, Square 객체가 전달 될 때는 assertion 에러를 발생하도록 한겁니다.

함수 g를 만든 프로그래머는 매우 합리적인 가정을 했습니다. 다른 프로그래머가 함수 g에 Square 객체를 전달하면 assertion  에러를 보고 받을 테니깐요.

하지만, 부모인 Rectangle 객체에서 작동하는 행위가 Square 객체에 대해서 작동하지 않는다는 것은 LSP를 위반하는 것이고, 파생클래스 행위의 내부 상세를 안다는 것은 OCP open-close principle 또한 위반 하는 것입니다. 


public class Rectangle
    {
        protected double itsWidth;
        protected double itsHeight;
    
        public virtual void SetWidth(double w)
        {
            this.itsWidth = w;
        }

        public virtual void SetHeight(double h)
        {
            this.itsHeight = h;
        }

        public double GetHeight()
        {
            return this.itsHeight;
        }

        public double GetWidth()
        {
            return this.itsWidth;
        }
    }

    public class Square : Rectangle
    {
        /// 
        /// 정사각형이기 때문에 너비를 할당하더라도 높이도 같이 할당해 줌.
        /// 
        /// The w.
        /// 기반 타입의 SetWidth 메소드를 호출하더라도
        ///  파생 타입의 메소드가 호출됨.
        /// 
        public override void SetWidth(double w)
        {
            base.itsWidth = w;
            base.itsHeight = w;
        }

        /// 
        /// 정사각형이기 때문에 높이를 할당하더라도 너비도 같이 할당해 줌.
        /// 
        /// The h.
        /// 
        /// 
        public override void SetHeight(double h)
        {
            base.itsWidth = h;
            base.itsHeight = h;
        }
    }



무엇이 잘 못 되었는가? What Went Wrong (W3)


무슨 일이 일어 난거야? Rectangle과 Square라는 논리적인 모델이 뭐가 잘 못 된거지?  무엇보다도 정사각형은 사각형이다. ISA 관계도 만족 시키잖아!!!.


정사각형은 사각형이지만, Square 객체는 Rectangle 객체가 아닙니다. 왜 일까?  Square 객체의 행위는 Rectangle 객체의 행위와 일관성이 없습니다. 행위적으로 Square 는 Rectangle 이 아니고, 행위는 소프트웨어의 거의 모든 것입니다.

LSP는 OOD의 행위적 ISA 관계를 명확하게 해줍니다. : 본질적인 행위보다 클라이언트가 의존하는 비본질적인 행위를 더욱 중요시 합니다. 함수 g를 작성한 프로그래머는 사각형의 너비와 높이는 독립적으로 설정될 수 있다는 사실에 근거하였습니다. 두 변수의 독립성은 다른 프로그래머들이 의존하는 외적으로 공개적인 행위 입니다. 



계약에 의해 설계하기 Design by Contract

Bertrand Meyer (Eiffel programming language 창시자, Object Oriented Software Construction 저자) 에 따르면  LSP와 Design by Contract 는 매우 깊은 관계가 있습니다. 클래스 메소드의 선행 조건 precondition 과 후행조건 postcondition 에 이런 구조를 적용한다면, 메소드를 실행하기 위해선 선행조건이 만족해야 하고, 그렇다면, 메소드는 후행조건이 참일 것이라는 것을 보장 할 수 있습니다.


Meyer 는 파생클래스의 선행조건과 후행조건에 대한 규칙을 정의 했습니다.: 선행조건이 약하고, 후행조건이 강할 때에만 루틴을 재정의가  가능하다.

...when redefining a routine [in a derivative], you may only replace its
precondition by a weaker one, and its postcondition by a stronger one.

Object Oriented Software Construction, Bertrand Meyer, Prentice Hall, 1988, p256


다시말해서, 기반클래스의 인터페이스를 사용할 때, 사용자는 오로지 기반클래스의 선행조건과 후행조건만을 알고 있기 때문에, 파생 객체는 기반클래스 보다 더 강한 요구사항을 사용자가 지키라고 강요할 수 없다.  또한 파생 클래스는 기반 클래스의 모든 후행 조건을 만족 시켜야만 한다.  이것이 파생클래스의 행위에 대한 결과가 기반 클래스의 제약 사항을 위반하지 않는 것이고, 기반 클래스의 사용자는 파생클래스의 결과물에 혼란스럽지 않게 될 것이다.



맺음말

오리처럼 생겼고, 오리 울음 소리를 내지만 배터리가 필요하다면 - 아마도 잘못된 추상화를 한것이다. LSP



객체-지향 디자인에서 상속에 대한 룰을 어느정도로 엄격하게 지키냐는 가치판단의 문제 입니다. LSP 원칙은 상속에 대한 룰을 가장 엄격함을 적용하는 것이라 할 수 있습니다. 상속을 무분별하게 적용하고 있는 현재의 관행은 상속을 적용함에 있어 더욱 엄격해야 한다는 것이 커뮤니티의 주류 의견되고 있습니다. 객체-지향 디자인이 이루고자 하는 목적이 작동하기만 하는 소프트웨어가 아닌 관리가능한, 재사용가능한, 신뢰할 수 있는, 유연한 소프트웨어의 개발이기 때문입니다.

### 끝.

참고자료 :
로버트 C. 마틴 Liskov Substitution Principle.pdf [Martin96] Robert C. Martin, Engineering Notebook, C++ Report, Nov-Dec, 1996.
 

리스코프 치환 원칙 잘 정리된 wiki 문서


책 : java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p137,p144 
저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트
객체-지향 디자인은 위기에 빠진 소프트웨어를 구할 수 있는 메시아 처럼 받아들여 집니다. 진짜 메시아처럼 그 정체를 정확히 아는 사람도 얼마 없다라는 것도 닮았군요. 소프트웨어 개발의 어려운 점은 개념 자체 이해가 어렵다는 것이 크다고 생각합니다. 이 블로그에서 개념에 집착하는 이유가 거기에 있습니다. 

객체-지향 언어, 객체-지향 디자인, 객체-지향 프로그래밍 이란 용어는 아마도 가장 이해하기 어려운념이며, 구원자를 앞에 두고 알아보지 못하고 있는 것 같은 상황입니다. 객체-지향을 설명하는 수많은 책과 글이 있음에도, 정확히 이해하지 못하는 상황에 빠져 있다는 것은 무엇인가 문제가 있음을 증명하고 있습니다.

주된 이유는 객체-지향이라는 개념이 모호한 용어의 조합으로 설명하기 때문이며, 사람에게 익숙한것은 객체-지향이 아니라 절차적 프로그래밍이라는 생각이 들기도 합니다. 기본적으로 사람의 사고 체계의 기본 단위는 절차적입니다. 한 발짝 뒤로 물러 서서 보면 절차를 감싸고 있는 객체가 있음에도 희안하게 그게 잘 보이지 않습니다.

객체-지향을 좀더 쉽고 정확하게 설명하자, 그리고, 객체-지향이 만들어 내는 현상에 현혹되지 말고 핵심을 설명하자는 것이 객체-지향을 수차례에 걸쳐 포스팅 하는 이유입니다.

이전 포스트 : 객체-지향 프로그래밍 이란 무엇인가 (OOP)? 는 객체-지향의 각론에 대한 이야기입니다.  

간단한 프로그램을 작성한다면 구조적 프로그래밍으로도 충분합니다. 뉴튼의 만유인력의 법칙 만으로도 태양계 행성들과 자연계의 움직임을 근사값에 가깝게 설명할 수 있는 것 처럼 말이죠. 하지만, 복잡한 프로그램을 관리가능하게 작성하려면 아인슈타인의 상대성이론을 적용해야 합니다. 그게 문제 인거죠. 만유인력의 법칙이 구조적 프로그래밍 이라면 상대성 이론이 객체-지향 프로그래밍 입니다.

객체-지향을 설명하는 많은 글들 중에 저의 생각과 일치하는 책이 있어, 객체-지향 프로그래밍 개념에한 단락을 번역 포스트 합니다. 98년에 출판된 책인데 번역서는 없더군요.  바로 Stephen Gilbert 와 Bill McCarty의 저서 Object-Oriented Design in Java, published by Sams, 1998 입니다. 


객체-지향 프로그램이란 상호작용 하는 객체의 집합 입니다.




객체-지향을 정확하기 이해하기 위한 5개의 개념 정의와 중요한 관점에 대해 정리해 보자면, 

1. 객체란 무엇인가?
데이터, 행위, 아이덴티티를 가지고 있는 것.

2. 클래스란 무엇인가? 그리고, 객체와 클래스의 차이는 무엇인가?
클래스란 객체를 생성하는 청사진 이다.

3. 캡슐화란 무엇인가?
행위와 상태를 포장하고 외부에 노출할 것과 감출 것을 결정하는 것. 외부에 노출되는 모든 것을 인터페이스 interface 라고 할 수 있다.

4. 상속이란 무엇인가?
가족 관계를 나타내는 것이다. 자식은 부모의 자산을 이용할 수 있으며, 코드 재사용이 첫번째 달성되는 것이며,  상속은 클래스의 계층 구조를 표현 할 수 있게 되는데, 계층 구조는 그 구조 자체만으로 수 많은 정보를 포함 할 수 있다. 계층 구조를 표현하는 것이 두번째 핵심이다.

많은 문서에서 상속을 코드 재사용의 관점에서 기술하고 있지만, 상속의 핵심은 계층구조를 표현 하는 것입니다. 재사용만을 목적으로 상속을 이용한다면 가장 강력하게 커플링된 재사용방법을 쓰고 있는 것입니다.  공통 라이브러리를 사용하는 것이 훨씬 좋은 방법이죠. 계층구조를 포함할 목적이 아니라면 상속을 사용해서는 안되며, 코드 재사용 보다 계층구조의 표현이 상속에서 더욱 중요한 개념이다 라는 것입니다. 

 
5. 다형성이란 무엇인가? 
캡슐화, 상속과 함께 동작 함으로써 객체-지향 프로그램의 흐름 제어를 단순화 하는 것 입니다.

다형성을 설명하는 많은 문서는  
서브클래스의 오버라이드된 메소드가  다른 작업을 할 수 있어서 다양한 형태라는 다형성이라고 설명하는데 포커스가 맞춰 있지만 다형성의 핵심은 흐름 제어 Flow of Control 를 객체로 처리 하도록 단순화 하는 것 입니다.



아래는 
Stephen Gilbert 와 Bill McCarty의 저서 Object-Oriented Design in Java, 1998 의 객체-지향 프로그래밍 설명 부분의 번역 입니다.


번역 : 

객체-지향 프로그램 Object-Oriented Programs

객체-지향 프로그래밍은 오늘날 프로그램의 복잡성을 극복하고 있는 듯 합니다. 프로시저를 object라고 하는 유닛으로 그룹화 함으로써 프로그램은 더 적은 블록을 요구하고 결과로 단순해 졌습니다.


OOP 새로운 것이 아니다? : Is Nothing New?

객체-지향 프로그래밍을 공부하다 보면, OOP에 대한 매우 다른 관점의 말들이 오간다는 것을 쉽게 알아 차릴 수 있습니다. OOP의 다양한 책과 문서들을 읽어보면 사람들이 전혀 다른 것에 대해 이야기 하는 것처럼 보인다. 모든 잡설들을 제고하고 나면 '혁명적 revolutionary' 과 '혁신적 evolutionary' 이라는 두가지 관점이 있다는 것을 알 수 있다. 혁명적 관점을 변론하자면 OOP는 맨손으로 프로그램을 만들던 전통적인 방법과 전혀 다르다는 것이다. 반면에 혁신가의 입장에서 보면, OOP는 전통적인 개념의 재포장 일 뿐이라고 말합니다. 아마도 각각의 견해는 맞는 것도 있고 틀린 것도 있습니다.

절차적 언어로 명확하고, 이해하기 쉬운 프로그램을 작성하는 것이 가능하다고 가정하면 혁신가의 말이 옳습니다. 그리고 객체지향 언어로 작성하더라도 이해할 수도, 관리할 수도 없는 코드가 작성될 가능성이 있습니다. 그러나 혁신가들은 OOP 프로그램이 절차적인 프로그램과는 기반적으로 다른 방법으로 구성된다는 것을 인지하지 못했습니다.


혁명가들은 OOP 디자인 프로세스가 다른 도구와 다른 추상 타입을 사용하며, OOP 프로그램은 함수적 분해를 하지 않는다는 것을 지적합니다. 혁명가들은 OOP 디자인의 명확성과 이해용이성을 과대평가 하는 듯합니다. 잘 설계되고 구현된 절차적 프로그램은 개판으로 짜여진 프로그램 보다 절대적으로 낫습니다. OOP 와 객체지향 언어는 아이디어를 명확하게 표현할 수 있는 도구를 제공하는 것이지, 즉각적으로 효과가 나타나는 만병통치약이 아닙니다. 


주: 
혁명가 : 패러다임이 완전히 다르다는 견해를 가지고 있는 사람. 
혁신가 : 이전의 기술이 개선 되었을 뿐이라는 견해를 가지고 있는 사람.



객체지향 프로그램의 다섯개의 기반 개념.

1. Objects
2. Classes
3. Encapsulation
4. Inheritance
5. Polymorphism

"객체지향 프로그램은 어떠한 목적을 이루기 위해 협업하도록 구성되어지는 객체들의 집합이다."

객체란 무엇인가? What Are Objects?

프로시저를 구조적 프로그램을 작성하는데 사용하다고 단정하면, 객체란 객체지향 프로그램을 작성하는데 사용 되는 것 입니다. 객체지향 프로그램은 어떠한 목적을 이루기 위해 협업하도록 구성되어지는 객체들의 집합입니다. 

모든 객체는 : 

데이터를 가지고 있습니다. - 데이터는 객체의 상태를 기술하는 정보를 저장합니다.
행위의 집합을 가지고 있습니다. - 이 행위들은 메세지를 받았을 때 객체가 어떻개 해야하는지 알고 있는 것 입니다.
개체를 구분하는 아이덴티티를 가지고 있습니다. - 어떠한 객체를 다른 객체와 구분하는 것을 가능케 합니다. 

구조적 프로그램에서 사용되는 레코드,구조체 처럼 객체도 데이터를 담고 있습니다. 이처럼 생각하면 객체는 이전 섹션에서 보았던 급여관리 프로그램에서 보았던 하나의 직원 레코드 처럼 보입니다. 객체의 데이터는 객체의 상태를 표현하기 위해 사용됩니다. 예를 들어, 객체의 데이터를 가지고 그 직원이 정규직원인지 파트타임인지 감지할 수 있습니다.

직원 객체는 작업 행위를 가지고 있다는 데서, 절차적 프로그램의 직원 레코드와는 다릅니다. 이 작업들은 아마도 객체의 데이터를 읽거나 변경하는데 사용될 겁니다. 객체는 자신의 등에 데이터를 메고 있는 작은 프로그램 처럼 동작합니다.


여러분이 어떤 것을 알고 싶거나, 어떤 일을 하기를 원한다면 객체에게 작업을 수행하도록 요청하면 됩니다. 이것을 객체지향 세계의 말로 "메세지" 보낸다 라고 합니다. 응답은 객체의 두번째 특성입니다.  직원 객체는 내장된 행위로 급여에 대해 어떻게 말해야 하는지, 우편물 주소 라벨을 어떻게 인쇄해야 하는지 알고 있을 것입니다.

객체의 세번째 특성은 유일한 아이덴티티를 가지고 있다는 것 입니다. 이것이 모든 객체가 관계형 데이터베이스에서 말하는 ID 값이나 Primary Key를 가져야 함을 의미하는 것은 아닙니다. 객체는 구조적 언어의 '변수'와 매우 닮았다고 할 수 있습니다. 정수 변수 i 와 j는 같은 값을 가질 수 있지만 여전히 구분 될 수 있습니다. 


클래스란 무었인가? What Are Classes?

자바 프로그램이 객체의 집합으로 이루어진다면, 대체 클래스란 뭘까? "학생" 객체의 집합인가? 아닙니다.
클래스는 객체 생성의 청사진 blueprint 입니다. 여러분은 실제 객체에 대한 코드를 절대로 작성할 수 없으며, 객체를 만드는데 사용되는 패턴을 작성할 수 있을 뿐입니다. 클래스와 객체의 구별하는 것은 미묘하지만 객체지향 디자인을 이해하기 위한 기본입니다.
 
클래스와 객체 사이의 관계를 이해하는 좋은 방법은 구조적 프로그래밍 언어의 '타입 Type' 과 '변수 Variable'를 떠올리면 됩니다. 변수의 타입을 이야기 할때 그 변수로 어떤작업을 할수 있는지, 저장할 수 있는 범위가 어떻게 되는지로 기술합니다. 예를 들어, 정수 변수는 복소수를 저장 할수 없습니다. 같은 방법으로 정수와 소수의 곱하기 연산은 가능하지만, 맞춤법 검사는 오로지 문자열에 대해서만 가능합니다.

변수는 값을 저장한다는데 주목하라 : 타입이 하는 일이 아닙니다. 타입은 추상이라고 말할 수 있습니다. 실제로 정수 값을 저장하려면 정수 변수를 생성해야만 합니다. 정수 타입으로 지정된 청사진으로 변수가 생성되면 그 변수는 정수입니다. 정수 타입에 결정된 규칙에 의해 저장되고, 행동합니다. 각각의 i,j,k 정수를 정수 타입의 '인스턴스'라고 말합니다.

객체와 클래스는 비슷한 관계를 가지고 있습니다. 클래스는 객체가 소유하게 될 속성 attributes 과 행위 behaviors 를 정의합니다. 클래스는 객체 생성의 청사진 blueprint 입니다. 자바 프로그램을 작성할 때 남이 작성한 클래스를 사용하기도 하고, 자신의 장치에 맞는 새로운 클래스를 정의하기도 합니다. 새로운 클래스를 생성하는 일은 두 부분으로 나누어져 있습니다. 

1. 객체의 상태를 저장하는데 사용될 속성을 정의 합니다.
2. 객체가 이해할 수 있는 메세지와 메세지에 응답하는 과정을 정의 합니다. 각각의 메세지에 대해 메소드 method 라고 불리우는 프로시저를 만들고, 이것을 구현합니다.


캡슐화란 무엇인가? What is Encapsulation?

객체,클래스와는 다르게 캡슐화는 자바 언어의 요소가 아닙니다. 캡슐화는 잘-설계된 클래스를 만드는데 사용되는 기법입니다. 잘-설계된 객체-지향 클래스는 캡슐화 기법을 요구합니다.

"좋아, 캡슐화라는 것에 기꺼이 주목할께, 하지만 마치 우주인이 가지고 있을 만한 물건처럼 들리는데, 도대체 그게 뭐야." 캡슐화는 프로그램을 포장 packaging 하는 작업입니다. 클래스는 두 부분으로 나눌 수 있습니다: 인터페이스 interface 와 구현 implementation.

"잠깐, 당신은 이미 클래스는 속성 attributes 과 메소드 methods 의 두-부분으로 구성되어 있다고 말했잖아요, 인터페이스와 구현은 속성과 메소드를 부르는 새로운 용어 인가요?" 라고 반응할 수 있습니다.

여러분의 객체는 속성과 메소드로 만들어져 있습니다. 일부 속성과 메소드는 객체의 외부에서 접근할 수 있고 이것을 인터페이스 interface 라고 합니다. 다른 속성,메소드는 객체 자신만의 사적인 용도로 예약되어 있고 이것을 구현 implement 이라고 합니다. 구현으로 부터 인터페이스를 분리하는 것은 객체-지향 프로그램을 설계할 때 가장 중요한 결정 입니다. 

구현으로 부터 인터페이스를 나누는 일의 가치를 살펴보자면, 자동차를 연상하면 쉬울 것입니다. 자동차의 인터페이스는  핸들, 가속 페달, 브레이크로 간단하고 규격화 되어 있습니다. 운전방법을 한번 배우기만 하면 됩니다. 반면에 자동차의 내부 동작은 점화, 실린더, 연료 분사 등등 매년 역동적으로 변경됩니다. 여러분이 각기 다른 타입의 자동차의 점화 시스템을 직접 통제해야 한다면, 새차를 운전하기란 매우 어렵다는 걸 알게 될겁니다. 

잘-설계된 클래스 Well-Designed Class 는 이런 특성을 가지고 있습니다. 인터페이스는 여러분의 클래스와 어떻게 상호작용 해야 하는지 완벽하게 묘사합니다. 그리고 클래스의 대부분의 속성은 감추어 진다는 것을 의미합니다. 사용자는 데이터를 수정하기 위해서 메소드를 사용할 것입니다.



상속이란 무엇인가? What Is Inheritance?

상속 계층을 구조화 하는 것은 클래스 설계의 두번째 결정입니다. 캡슐화는 변경으로 부터 클래스를 견고하게 만드는데 필요합니다. 상속은 클래스 관계에서 "가족 famillis" 개념과 연관이 있습니다.  

"상속의 진가는 강력한 추상 구조화 이다."

클래스의 상속관계를 정의하면 두가지의 큰 잇점을 얻을 수 있습니다. 새로운 종류의 서브클래스를 작성하면, 여려분은 부모 super class 에 이미 내장된 기능들을 사용할 수 있습니다. 이는 상속을 사용하는 가장 일반적인 잇점입니다. 그러나 이게 전부는 아닙니다. 프로그래밍 언어에서 프로시저가 처음 사용 될 때, 중복 코드를 줄이라고 요구합니다. 그리고, 복잡한 컴퓨터를 구성하고 정복하는 엄청난 힘이라는 것이 증명되었습니다.  상속의 진가는 추상화를 구조화 한다는데 있습니다. 프로시저는 복잡한 문제를 단순한 부분으로 나눌 수 있게 합니다. 상속을 사용하면 공통적인 요소를 슈퍼클래스에 정의 함으로써 일반화 할 수 있습니다. 

구조적 프로그래밍은 분해 decomposition (프로시저 형태로의 분해) 에 의한 추상화에 기반하고 있습니다. 객체-지향 프로그래밍은 추상 메커니즘을 이용해 추상화를 포함합니다. 클래스화 classification 에 의한 추상화를 기반으로 상속을 적용하는 메커니즘을 이용합니다. 자연과학에서 처럼 계층구조를 묘사할 수 있음이 계층적 클래스화의 힘입니다. 계층구조는 우리 행성의  수백만 종에 달하는 동,식물의 정보를 구조화하는 강력한 도구 입니다.


다형성이란 무엇인가? What Is Polymorphism?

마지막으로, 다형성은 객체-지향 프로그래밍의 기반 원칙입니다. "많은 형태 many shapes" 라는 뜻을 가진 그리스어에서 유래한 말로,
다형성은 캡슐화, 상속과 함께 작동해서 객체-지향 프로그램의 흐름 제어 flow of control 를 단순화 합니다.

"다형성은 캡슐화, 상속과 함께 동작 함으로써 객체-지향 프로그램의 흐름 제어를 단순화 합니다."

흐름 제어, 다음에 일어날 일을 알아햐 한다는 것은 컴퓨터 프로그램의 아킬레스 건 입니다. 이게 바로 프로그램이 복잡하게 되는 주요 원인입니다. 모든 경로를 추적하지 않고서는 객체가 가질 상태의 가능성을 검사 할 방법이 없습니다. 따라서 여러분은 프로그램을 테스트 하다가 지쳐 버릴 것입니다. 구조적 프로그래밍에서 프로그램의 흐름 제어를 단순화 하는 초기 시도는 조건 없는 분기로 규범화된 순서 구조, 선택, 반복을 제공하는 것 이었습니다. 같은 방법으로 다형성은 상속 계층의 연관된 객체에 메세지를 보냄으로써 단순화 합니다.

객체에 메세지를 보낼 때, 그 객체는 메세지에 응답할 메소드를 가지고 있어야 합니다. 클래스가 상속 계층에 연결되어 있다면, 모든 서브클래스는 부모의 인터페이스를 자동으로 상속 받습니다. 어떤 일이라도 슈퍼클래스 객체가 할 수 있는 것이라면 서브클래스 객체도 할 수 있습니다. 예를 들어, 클래스 A가 클래스 B의 서브클래스 라면, A 객체는 B객체가 할 수 있는 모든 일을 할 수 있습니다. A객체는 B객체라고 말할 수 있고, 이런 경우의 상속 관계를 ISA 관계 라고 부릅니다. 비록 서브클래스 객체가 슈퍼클래스 객체가 하는 것 처럼 동일한 메시지에 응답할 책임이 있다 하더라도, 메세지는 동일한 동작을 하도록 강제하지 않습니다. 이것을 이해할 필요가 있습니다. 각각의 서브클래스는 슈퍼클래스가 정의하고 있는 적절한 응답 또는 새로 정의된 특화된 응답에 의존하고 있습니다. 그래서, 각각의 서브클래스는 같은 메세지에 다른 응답을 할 수 있습니다. 이게 "많은 형태"라고 말하는 의미 입니다.


주 : 다음 동작에 대한 조건의 선택을 객체의 계층구조로 추상화 해 흐름 제어를 다형성으로 처리하는 구현 방법은 마틴 파울러의 리팩토링의 Replace Conditional with Polymorphism(293) 에 기술되어 있습니다.

다형성으로 조건문을 제거하라 Replace Conditional with Polymorphism(293)

객체 전문용어 가운데 가장 멋지게 들리는 말 중의 하나가 다형성(polymorphism)이다. 다형성의 진가는 동작이 그 타입에 따라 변하는 객체를 가지고 있을 때, 명시적으로 조건문을 사용하지 않아도 되도록 한다는 데 있다. 

리팩토링, 마틴 파울러 지음/윤성준,조재박 옮김, 대청, p.293 

Object-Oriented Design in Java, by Stephen Gilbert and Bill McCarty, Sams, 1998 p.32~43
 
### 끝.



저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트
구름 사이로 비추는 햇볕에 이따금씩 반짝이는 사금파리 처럼 수년동안 머릿속에 남아있는 구절이 있다면, 개발자로 밥을 먹고 있으면서 수년동안 읽은 개발서 중에 머리가 아닌 가슴을 에이게 하는 책이 있다면 그중 하나가 바로 이책입니다. 개발서 치고는 얊은 데다가 타이포그래피가 구닥다리 냄새를 풍기고 있기는 하죠. [버그 안녕] 이라는 부제는 텔레토비를 연상케 하는군요. 원서로는 93년도에 나왔으며, 번역으로는 2001년에 출판된 책을 온라인 서점에서 지금도 구매할 수 있습니다.




Writing Solid Code.(부제: 버그 안녕) STEVE MAGUIRE 저, 나윤석. 이을재 공역, 높이깊이 ISBN 89-7588-020-6

많은 좋은 내용이 있지만 그중 머릿속에 멤도는 몇가지는 아래와 같습니다.

에러를 리턴하는 함수는 사용하지 말라 - 만일 어떤 함수가 에러값을 리턴한다면, 그 함수를 호출한 모든 곳에서 에러를 처리해야 한다. 
p.168

C 언어를 처음 배울때 실패시에는 -1 또는 0을 성공시에는 1을 리턴하라는 가르침은 그 후에 어떤 언어를 배우더라도 성공, 실패를 값으로 리턴하려는 습관이 생겨 버립니다. 이 습관은 exception을 이해해고 적절하게 사용하는데 가장 걸림돌이 된다고 생각하며, C#이나 자바와 같은 고급언어를 공부한 후, C 나 C++ 언어를 공부하는 것이 더 좋다 라고 생각하는 이유 중 하나 입니다. 에러값을 리턴하는 함수를 사용하지 말자는 철학은 고급언어에 exception 메커니즘이 적용된 이유입니다.

보충 설명 : 고급언어에서의 exception 메커니즘

Unix와 C에 기초한 시스템들은 전통적으로 루틴의 성공 또는 실패를 나타내는 리턴 코드를 사용한다.
자바는 예외(exception)라는 더 좋은 수단을 가지고 있다. 예외는 일반적인 처리 과정과 예외 처리 과정을 명확하게 분리하기 때문에 더 좋다. 이는 프로그램을 이해하기 쉽게 만든다. 프로그램을 쉽게 이해할 수 있도록 작성하는 것이 매우 중요하다는 사실을 이제는 믿어 주기 바란다.

리팩토링, 마틴 파울러 지음/윤성준,조재박 옮김, 대청, p.358




효율성과 이상적인 설계 사이

프로그래머들 가운데에는, 아무리 사소하 것이라도 효율을 향상시킬 수 있는 부분을 포기하는 것을 마치 죄악처럼 여기는 사람들도 있다. 가장 이상적인 것은 효율을 약간 희생하더 라도 안전한 설계와 구현 방법을 선택하여 위험을 줄이는 것이다. 
p.214

개발조직에서 논쟁을 하다 보면 효율성을 위해서 이상적인 설계와 구현 방법, 도구를 포기하자는 목소리가 클때가 많습니다. 효율과 이상적인 설계중 선택하라면, 저는 이상적인 설계를 선택합니다. 효율성 자체로서 크리티컬한 도메인 이라면 그 의견에 동의 하겠지만, 그러한 경우는 극히 드뭅니다. 효율을 약간 희생하더라도 안전한 설계와 구현 방법을 선택하는 것이 대부분의 경우에 적합하다 라는 것은 매우 동의하는 구절중의 하나 입니다.


페인트 젓는 막대

페인트 젓는 막대, 잘 저어질수 있도록 넓으며, 일회성이란 성격으로 버릴 수 있는 소재로 되어 있다.


페인트 젓는 막대

페인트 깡통의 뚜껑을 열고 페인트를 휘젓는 데에 드라이버를 사용하는 것은, 아주 오래 전부터 집집마다 통상적으로 써 오던 방법이다. 나도 그 방법을 즐겨 사용했었다. 그 결과, 우리 집에는 여러 색의 페인트로 겹겹이 덮인 드라이버거 여러 개 있다. 사람들은 그래서는 안된다는 것을 알면서도, 왜 드라이버로 페인트를 젓는 것일까? 이유는 자명하다. 그 당시에는 드라이버가 편리하게 생각되었고, 실제로 페인트도 잘 저어졌기 때문이다. 페인트 젓는 막대가 된 드라이버 처럼, 편리하게 사용할 수도 있고 프로그램도 제대로 작동하지만 원래의 목적에서는 벗어나는 방법들이 코딩 과정에도 많이 사용된다.  ~ 이 기준은, 마치 법률을 피하면서 세금을 적게 내는 방법을 가르쳐 주는 납세 지침서와 마찬가지이기 때문이다. 납세 지침서는 모두, 의도의 불순성을 무시하고 목적의 달성만을 추구하고 있는 것이다. 앞 페이지 예의 진짜 문제점은 코드에 있는 것이 아니라, 프로그래머의 자세에 있다. 수치적 연산을 위하여 논리적 연산을 사용하고도 마음이 편한 프로그래머라면, 다음엔 어떤 기상천외한 방법을 동원할까? 그 방법들을 어떻게 믿을 수 있을까?
p.252,253

페인트까지 저을 수 있는 드라이버를 개발에서는 재사용이 가능하며 유연하며 활용도가 높은 객체라고 생각하기 쉬울수 있습니다. 엄밀히 말하자면 원래의 용도가 아닌 객체를 동작한다는 이유로 다른 용도로 사용하게 되는 것이죠. 

때로는 실용적이라는 변명으로 목적이 수단을 합리화하는 일은 우리의 일상 뿐만이 아니라 개발에서도 발생합니다. 어떤 때는 적절한 행동처럼 보이기도 하구요. 거기에 동조하기도 하고, 눈을 살짝 감기도 합니다.  결과가 좋았더라도 가슴한 구석이 편치 않았던 때가 많았었죠. 살면서 그렇게 행했던 때, 개발하면서 그런 선택을 했을때, 페인트 통을 또한 번 드라이버로 저었다라는 생각을 합니다. 그러한 선택이 많아 질 수록 그 회사, 조직, 서비스, 나 자신이 어떠한 모습으로 변해갈지. 그러한 방법들을 어떻게 신뢰할 수 있을지.



증상만 치료하지 말고, 원인을 제거하라.

앤토니 로빈스(Anthony Robbins)는 그의 책 [내 안의 거인을 일깨우자]에서 어떤 의사의 이야기를 하고 있다. 한 의사가 물살이 빠른 강의 제방에 서 있다가, 물에 떠내려가며 살려 달라고 외치는 비명소리를 들었다. 자기 이외에는 도와줄 사람이 없다는 것을 안 그 의사는, 주저하지 않고 물 속으로 뛰어들었다. 물에 빠진 사람을 강가로 끌어 낸 뒤, 의사는 인공호흡으로 그를 소생시키려 했다. 그런데 그가 소생되기도 전에, 강에서 두사람의 비명 소리가 또 들려왔다. 의사가 두 사람을 구조하여 소생시키기 무섭게, 강에서는 또다시 네 사람의 비명 소리가 들려 왔다. 어어서, 여덟명의 비명이 들려 왔다... 안타깝게도, 이 의사는 사람들을 구조하기에 너무 바쁜 나머지, 상류로 올라가서 도대체 누가 사람을 물에 빠뜨리는가 찾아 볼 시간이 없었다.
p.273

증상만 치료하다가 정작 중요한 일을 하지 못하는 조직, 서비스. 자신의 조직이 아니라고 말 할 수 있는 개발자가 몇이나 될까요? 시간이 소요되더라도 원인을 제거해야 진짜 중요한 일을 할 수  있으며, 개발자도 발전 할 수 있습니다. 



융통성이 크다는 것

융통성이 크다 는 것이  '사용하기 쉽다'는 뜻이 아니다. 함수나 기능을 설계할 때는, 사용상의 용이성에 초점을 맞추어라. 함수나 기능이 융통성이 크다면, 아무리 애를 써도 그것들을 사용하기 쉽게 만들기는 어려울 것이다. 그 대신, 버그 찾기만 더욱 어렵게 만들 것이다. 
p.299

커다란 유혹입니다."지금 필요하진 않지만 나중에 필요할 지 모르니 그걸 붙일수 있게 개발해! 그렇게 하면 나중에 붙이기만 하면 되잖아!" 내가 개발하는 프로그램이 융통성이 있게 하자. 이 유혹과 소프트웨어를 단순 레고블록으로 취급하는 마인드는 오버 엔지니어링을 유발하고, 유연하기는 하지만 용이성을 잃어 버리기 쉽습니다. 그리고 버그의 온상이 되기 쉽죠. 이 균형을 맞추는 일은 성장이냐 분배냐의 질문에 대답하는 것 만큼이나 어렵습니다. 이럴 때마다 머리속으로 생각하는 답이 있습니다. 

 YAGNI  You ain't gonna need it! KISS Keep it simple, stupid! 단순하게 해 멍청아!

저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트
근래의 거의 모든 프로그래밍 객체지향이란 개념으로 구현합니다. 또한, 새롭게 제시되는 방법론 들도 모두 객체지향을 기반으로 제시됩니다. 실세계의 사물을 추상화(Abstraction) 하고, 캡슐화(Encapsulation) 하며, 계층구조는 상속(Inheritance)시키며, 부모와 다른 자식의 특성, 행위는 다형성(Polymorphism) 으로
구현된 그것, 바로 객체의 구성으로 프로그램을 만들어 나가는 것을 객체지향 프로그래밍 이라 하는 것이죠.

객체지향 프로그래밍



이렇게 정의가 되어 있어도 다시 나오는 질문이 있습니다 : "그래 알겠는데, 도대체 객체지향이 뭐야?, 뭐가 특별하다는 거지?, 왜 객체지향 방법론을 사용해야 하는데?, 어떻게 하는 거지?"

이와 같은 질문에는 다시 객체지향의 개념이 반복되죠. 끝이 없습니다. 도대체 어떻게 하면 객체지향을 제대로 이해 할 수 있을까요?  이 질문에 대한 잘 기술된 글이 있어 번역 포스트 합니다. 무엇이 객체지향이고, 무엇이 객체지향이 아니며, 객체지향을 만족시키려면 어떻게 해야 하는가? 어떤 이득이 있는 것인가? 를 다룬 글 입니다. 대부분의 관점에 동의 하며 특히 아래의 객체지향의 핵심을 집어내는 정의로써 에릭 에반스의 DDD 에서 이 장점을 취하기 위해서는 어떻게 해야 하는가에 대한 내용을 다루고 있습니다.

객체지향 프로그래밍 이란 캡슐화, 다형성, 상속 을 이용하여 코드 재사용을 증가시키고, 유지보수를 감소시키는 장점을 얻기 위해서 객체들을 연결 시켜 프로그래밍 하는 것 입니다.



번역 :

제목 :  What is Object Oriented Programming (OOP)?
http://www.tonymarston.net/php-mysql/what-is-oop.html


들어가는 말

뉴스그룹이나 포럼에서 나는 다음과 같은 질문을 매우 빈번히 접합니다. : 무엇을 객체지향(OOP) 라고 부르나요?, 뭐가 특별한가요? 왜 해야 하죠? 어떻게 하는 거죠?. 이런 종류의 질문을 하는 사람은 보통 비객체지향(non-OO) 프로그래밍에 경험이 있으며, 방법론의 전환을 위해서 잇점이 무엇인지 알고 싶어 하는 것입니다. 짧던 길던 불행하게도 대부분의 답변은 동화같고 애매모호하며, 의미없는 것들 뿐입니다.

비객체지향 언어로 1,000개 이상의 프로그램과 500개가 넘는 객체지향 기능의 PHP 프로그램을 작성해본 경험으로 이 논쟁에 기여를 해보려 합니다. 몇몇 객체지향 순혈주의자들은 내가 그들의 방법을 따르지 않는다고 생각하고, 나또한 그들의 방법론을 따르는 것을 거부합니다. 이것에 대한 나의 대답은 "세상에는 객체지향을 유일한 방법이란 없다" 입니다. 사람들은 나의 방법이 틀렸다지만, 그들은 전통적인 실수를 하고 있습니다. 내 방법이 틀리지 않은 간단한 이유는 "작동한다" 입니다. 길 막고 물어 봐도 "작동 하는 것이 틀릴 수는 없습니다. 작동하지 않는 것이 옳을 수는 없습니다."  내 방법이 틀린 것이 아니라, 약간 다를 뿐 입니다.

사람들이 전혀 쓸모 없는 답변을 하는 하나의 이유는 그들이 그렇게 배웠으며, 배운 것 이상으로 생각할 능력이 없기 때문입니다. 또다른 이유는 객체지향에 대한 설명들이 막연하고, 여러가지 의미로 해설될 수 있으며, 어떤 것이 해석 될 때는 '잘못 해석' 될 가능성이 있습니다.  심지어 추상화 Abstraction, 갭슐화 Encapsulation, 정보은닉 Information-Hiding 의  어떤 기본 용어들은 사람들 마다 다른 뜻으로 정의하고 있습니다. 사람들이 OOP의 기본개념들에 동의하지 않는 데, 어떻게 구현에 동의 할 수 있겠습니까.


OOP가 아닌 것은 무엇인가?

내가 반박할 첫번째는 non-OOP 에도 이미 존재 하고 있음에도 OOP 에서만 가능한 것처럼 말해지는 것들입니다.

OOP는 실세계 real world 를 모델링 하는 것이다.

OOP는 실세계를 모델링 하기 위해 추상화 Abstraction 을 사용하는 프로그래밍 패러다임이다. 더 나은 도메인 분석과 시스템 디자인의 통합을 제공 함으로써 실세계 모델링을 더 잘 지원한다. 

OOP 가 다른 방법론들 보다 실세계 모델링을 더 잘 지원한다는 것은 쓰레기 같은 말입니다. 모든 컴퓨터 프로그램은 프로세스를 모델링하는 방법을 찾는 것 입니다. 모델이 잘 못 됐다면 소프트웨어도 잘 못 됩니다. 개념적 모델은 실세계의 분석으로 만들어 집니다. 그리고 컴퓨터 소프트웨어는 전적으로 개념적 모델에  기반합니다. OOP는 더 잘 모델링하는 것을 보장하지 않습니다. 모델의 구현방법이 약간 다를 뿐입니다.

"추상화 Abstraction" 또한 해석해야 하는 용어 이고, 잘 못 해석되는 용어 입니다. "추상화의 진정한 의미 이해하기" 의 논의 에서 처럼, 추상화라는 말은  미켈란젤로의 작품을 보아야 할  때, 피카소의 작품을 떠올리게 합니다.

고객에게 필요한 X,Y,Z 기능이 없고, A,B,C 의 기능을 가진 소프트웨어가 만들어지는데는 이유가 있습니다. 고객이 요구사항 분석서에  X,Y,Z 를 언급하지 않았으며, 분석가가 이것을 집어내는 것을 실패했기 때문입니다. 내 오랜 경력에 비추어 이런 일은 빈번하게 벌어집니다.

OOP 에 의해 실세계가 모델로 바로 매핑된다는 것에 모든 사람들이 동의하지는 않습니다. 더 심하게는 프로그램은 세상을 모델링 하는 것이 아니라 세상의 부분만을 모델링 할 수 있다 입니다.


OOP는 코드 재 사용을 위한 것이다.

객체지향시스템은 코드를 재사용 함으로써 생산성을 증대시키고, 품질을 향상시키며, 비용을 축소 시키는 것을 약속한다.

쓰레기같은 말이다. 이것은 코드 재사용이 non-OOP 에서는 불가능하고 OOP 에서만 가능하다는 의미를 내포하고 있습니다. 코드 재사용은 코드를 어떻게 작성 하느냐에 달려 있는 것이지, OOP가 코드 재사용을 보장하는 것이 아닙니다. non-OOP 언어로도 재사용 가능한 라이브러리를 작성하는 것이 가능하며,  OO 언어로도 재사용 불가 코드가 만들어 집니다.

어떤 언어에서 같은 코드 블록이 100군데 이상 중복 되는 것은 언어 능력의 문제가 아닙니다. 또한, 어떤 언어로도 코드 블록을 재사용 가능한 라이브러리로 만들고 100군데에서 호출하게 할 수 있습니다.

OOP에 대해 수년동안 들어온 약속중에 하나는 , 소프트웨어 벤더들이 이미 작성해 놓은 라이브러리를 사용 가능하게 한다는 것입니다. 프로그래머들에게는 클래스를 작성하는 대신에 이미 작성해 놓은 클래스를 사용하라고 말했습니다. 그것은 '바퀴를 재발명하는 것'이라 면서 말이죠. 꿈은 실현되지 않았습니다. OOP 는 많이 약속 하고 조금 배달 한다는 것을 증명했을 뿐이죠.


OOP는 모듈화에 대한 것이다.

어떤 오브젝트에 대한 소스 코드는 다른 오브젝트 코드와 별개로 작성되고 관리되어질 수 있다. 한번 만들어지면 시스템 안으로 쉽게 전달 할 수 있다.

모듈화 프로그래밍은 non-OO 언어에도 수년동안 존재해 왔습니다. 그래서 이 논재는 객체지향이 비객체지향보다 낮다는 설명으로 쓰일 수 없습니다. 모든 언어는 하나의 어플리케이션 소스코드를 한개의 파일에 집어넣을 수 있습니다. 또한 여러개의 작은 모듈로 나누어서 모듈 별로 파일에 넣고, 유지보수하고, 컴파일 할  수 있습니다.

게다가, 여러개의 클래스로 구성된 어떤 소프트웨어는 자동적으로 모듈화 됩니다. 중요한 요소는 모듈 또는 클래스를 잘 디자인 하는 것입니다.


OOP는 플러그를 가능하게 하는 것이다.

특정 오브젝트가 문제가 있다고 밝혀지면, 여러분은 어플리케이션에서 그 오브젝트를 간단하게 제거하고 다른 오브젝트를 플러그 하듯이 대체할 수 있다. 이는 실세계와 유사하다, 볼트가 부러지면 전체 머신이 아니라 볼트만 대체하면 된다.

이것은 다른 모듈을 손볼 필요없이 개개의 모듈이 독립적으로 수정되고 컴파일 되고 삽입될 수 있다고 말하는 모듈화의 경우와 같다.


OOP는 정보 은닉에 대한 것이다.

객체의 인터페이스와만 상호작용함으로써 내부 구현은 바깥 세상으로 부터 감추어 질수 있다.

첫 부분, 여기서 설명 하는 것은 '구현' 의 은닉이지 '정보'의 은닉이 아니다. 어떤 사람들은 무의식적으로 객체의 데이터가 포함된다고 가정합니다. 다시 말하면, API 뒤로 코드를 감춘다는 것은 뷰로 부터 감춘다는 것이지 객체로 부터 데이터를 감춘다는 말이 아닙니다.

두번째 부분 , 구현 은닉은 OOP의 목적이 아닙니다. 이것은 캡슐화의 부산물 입니다. 바깥 세상은 오브젝트의 메소드 이름만을 볼 수 있습니다. 메소드 뒤의 코드를 볼 수 있는 것이 아닙니다.

세번째 부분, 구현 은닉은  OOP만의 특별한 것이 아닙니다. 인터페이스를 가진 80년대에 작성된 상업용 COBOL 패키지를 기억합니다. 이 패키지의 벤더는 우리의 코드에서 호출해서 사용할 수 있도록 컴파일된 패키지를 제공 했었습니다. 우리는 그 서브루틴의 소스코드를 볼수 없었습니다. 우리는 단지  API 목록과 입력과 출력의 기술서를 받았습니다.



OOP는 메시지 전달에 대한 것이다.

메시지 전달이란 한 객체에서 다른 객체로 데이터를 전달하거나, 다른 객체의 메소드를 실행 시키는 것이다.

객체지향 언어에서 객체가 메소드를 실행시키는 것은 비객체지향 언어에서 함수나 프로시저를 실행시키는 것과 구별되는 것입니다. 비객체지향 언어에서의 함수나 객체 메소드를 실행 시키는 것은 "메시지 전달 Message Passing" 이 아니라 "호출 calling" 이라고 부릅니다. 사실 몇몇 언어에서는 서브루틴을 실행시키기 위하여 "호출" 이라고 부르는 단어가 필요합니다.

non-OO : $result = function(arg1, arg2, ...)
OO :        $result = $object->function(arg1, arg2, ...)

각각의 실행의 결과는 정확하게 동일합니다. - 통제권은 피호출자로 넘어가고 호출자는 기다립니다. 통제권은 피호출자가 종료 될때까지 반환되지 않습니다.

나는 과거에 메시징 스프트웨어를 작업해 보았습니다. 그것이 완벽하게 다르다고 말할 수 있습니다.

첫번째로 메시징 소프트웨어는 한 프로세스에서 다른 프로세스로 메시지를 전달하는 것을 허용합니다. 같은 프로세스에서 다른 모듈로의 메시지 전달이 아닙니다. 같은 어플리케이션의 한 non-Modal 폼에서 다른 non-Modal 폼으로 메시지를 전달 하는 것이었죠. 이것은 오로지 분리된 sendMessage() 함수와 수신 모듈에서의 수신되는 메시지를 처리하기 위한 receiveMessage 트리거 로써 가능합니다. 수신자가 송신자에게 응답하는 방법은 메시지를 전송하는 방법 이외에는 없습니다.

두번째로 행위( Behavior )가 전혀 다릅니다.

그것들은 비동기 입니다. 이것은 호출자는 메시지를 큐에 전송하고 다른 프로세스를 처리합니다. 피호출자가 통제권을 반환 하는 것을 기다릴 필요가 없습니다. E-mail이 전통적인 메시징시스템의 예 입니다.

메시지큐는 프로세스의 개수와 메시지의 갯수에 제약받지 않습니다. 수신 프로세스는 메시지 큐의 첫번째 메시지를 꺼내고, 또 다음 것을 꺼냅니다.

수신자는 메시지를 받자마자 송신자에게 acknowledgement 를 전송합니다. 또는, 처리된 메시지를 반환 합니다.

메시지 시스템은 acknowledgement 또는 결과를 전송하는 코드를 포함하고 있어야 합니다.

보다시피 객체가 메소드를 확성화 시키는 메커니즘은 비객체지향 함수가 활성화 시키는 것과 정확하게 동일합니다. 메시징 시스템의 메시지 전송과는 무관합니다.



OOP 는 책임의 분리에 대한 것이다.

각각의 객체는 구별되는 역할과 책임을 가지고 있는 작은 머신으로 보여질 수 있다.

언어의 문제가 아니라 전적으로 모듈이 어떻게 작성되느냐에 달려 있습니다. COBOL과 같은 절차적 언어로도 독립적인 모듈을 작성할 수 있습니다. 반대로 객체지향 언어로도 지극히 종속적인 모듈이 작성될 수 있습니다.


책임분리의 문제는 그 뜻이 정확히 무엇이든지 상관없이 해석하는 사람마다 다릅니다. 어떤 사람은 SELECT, INSERT, UPDATE, DELETE 와 같은 데이터베이스 오퍼레이션이 그를 필요로 하는 객체에 존재해야 한다고 생각하고, 어떤 사람은 그것들을 한데 모은 별도의 객체에(Data Access Object , DAO) 넣어야 한다고 생각합니다. 어떤 프로그래머는 테이블 마다의 DAO를 작성해야 한다고 생각하고  어떤 프로그래머는 모든 데이터베이스 테이블을 취급하는 한개의 DAO를 작성해야 한다고 생각합니다. 여러분은 책임들을 분리하기 전에 언어와의 별도의 디자인 결정에 의해 책임을 구별해야만 합니다.

내 모든 경험을 통틀어 "기술의 난이도"에 의해 실패한 단 하나의 프로젝트는 "책임의 분리"에대한 모든 것을 알고 있다고 생각하는 객체지향 설계 전문가와 함께 한 것이었습니다. 그들은 각각의 책임을 가지는 모듈로 나누고, 디자인 패턴을 이용하여 설계하였습니다. 이 결과로 UI 에서 데이터베이스 까지는 적어도 10개의 레이어를 가지도록 설계 되었습니다. 이로써 컴포넌트는 필요 이상으로 복잡해 졌고 테스트와 디버깅은 완전히 악몽이었습니다. 그 결과 고객의 시간과 비용은 엄청 증가하였습니다. 그 고객은 플러그를 뽑고 전체 프로젝트를 중단하여 손실을 제한하였습니다. 구식의 비객체 방법론으로 한시간도 채 안걸리는 컴포넌트를 최신 유행하는 객체지향 기술로는 10일이 걸렸습니다.

덧붙여서, "관심이 분리된 Seperation of Concern" 여러개의 클래스/모듈 로 자동으로 구성된 소프트웨어는 특정 엔터티에서는 "걱정거리 Concern"로 고려 되어 질 수 있습니다. 중요한 요소는 클래스 /모듈이 엔터티의 요구사항을 어떻게 잘 취급하는가 입니다.



OOP는 배우기가 쉽다.

OOP는 이전의 접근방법보다 배우기가 매우 쉽다. 이 접근방법은 개발과 유지보수를 단순하게 하며,  다른 절차적 방법론 보다  분석, 코딩, 복잡한 상황의 이해도를 직관적으로 만들어 준다.

마케팅 전략일 뿐입니다. 모든 언어/도구/패러다임은 그 가치보다 모든 것이 좋다고 제안됩니다.  무엇을 사용하느냐가 아니라 어떻게 사용하느냐가 중요합니다. 유능한 프로그래머에 의해 사용되는 '구식' 언어는 '신식'언어의 광고되는 생산성보다 몇배는 더 큽니다.

한 사람의 학습 능력은 종종 가르치는 사람과 가르치는 도구에 의해 제한 됩니다. 나는 프로젝트를 성공 시키는 것 보다 너무 비효율적이고, 복잡하게 하는 것을 배우는 것을 두려워 합니다. OOP를 가능하게 하는 방법은 "단 한가지" 뿐이라고 주장하는 선생님이 너무 많습니다. 나는 이것에 가장 동의하지 않습니다. 나는 소위 전문가 라고 하는 사람들의 방법을 무시하고도  비객체 지향 언어로 작성된 것을 성공적으로 객체지향언어로 마이그레이트한 경험이 많습니다.

어떤 사람은 프로시저 함수를 클래스로 감싸는 것 만이 OOP가 아니라고  나에게 말 했습니다. 나는 동의 하지 않습니다. 그것은 간단 합니다. 유일한 트릭은 연결된 함수를 한곳에 모으고( 캡슐화 라고 말하는) , 객체로 관리될 수 있도록 조정 합니다. 여러분이 클래스로 할 수 있는 정말 똑똑한 것은 부모 또는 추상 클래스를 상속(Inheritance)을 통하여 수개의 서브클래스로 확장하는 것 입니다. 다형성(Polymorphism)을 이용하여 서로 다른 클래스의 메소드/함수를 사용 가능하게 할 수도 있습니다. 나는 함수들이 잘못 혼합된 매우 많은 예제들을 접해 봤습니다. 관계 없는 함수가 한 클래스에 있으면 안됩니다. 나는 상속의 과잉 사용으로 매우 복잡한 계층구조를 가져서 유지보수와 진보가 거의 힘든 경우를 보았습니다.  나는 다른 프로그래머에게 인상적으로 보일 목적 이외로는 볼 수 없는 객체지향의 모든 기능을 사용하여 코드를 더욱 불분명하게 만드는 프로그래머를 본적이 있습니다. 그들은 너무 단순하면 틀렸다고 생각하는 듯 합니다. 아마도 KISS( Keep-It-Simple, Stupid. 단순하게 하란 말야. 멍청아!) 원칙을 들어본적이 없어 보입니다.


OOP는 행위자(actors) 와 행위(actions) 에 대한 것이다.

객체지향 프로그래밍이란 소프트웨어 개발을 행위자와 행위를 코드를 이용하여 분해하고 모듈화하는 것이다.

이것은 매우 모호하고, 전혀 쓸모 없는 말입니다.



OOP 는 늦은 바인딩( late binding )에 대한 모든 것이다.

'늦은' 이라는 것은 (어떤 바이너리를 로드할 지 , 어떤 함수를 호출 할지) 바인딩을 가능한한 늦추는 결정이다. 컴파일 타임에 바인딩 (early) 하기 보다는 호출 될 때 바인딩 하는 것이다.

빠르거나 늦게 바인딩 되는 것은 객체지향과 비객체지향 차이와 무관합니다. 비객체지향 언어도 늦은 바인딩을 지원합니다. 비객체지향 언어가 늦은 바인딩을 지원한다고 해서 마술같이 객체지향으로 바뀌지 않는 것 처럼, 객체지향 언어가 빠른 바인딩을 한다고 해서 비객체지향 언어가 되는 것이 아닙니다.


알겠지만, 위는 너무 불명확하거나 OOP에 한정된 것이 아니어서 구별되는 기능으로 사용될 수 없는 것들을 기술한 것입니다.



객체지향 언어란 무엇인가?

아래의 것들을 제공한다면 그 언어는 객체지향언어 목록에 추가 될 수 있습니다.

객체-지향 언어의 5가지 특성
1. Object
2. Class

3. Encapsulation
4. Inheritance
5. Polymorphism

5가지 특성이 무엇인지에 대한 설명 포스트: 객체-지향 프로그래밍이란 무엇인가? 다섯개의 기반 개념 참조 하세요.


클래스는 엔터티의 프라퍼티(데이터) 와 그 프라퍼티들에 작동하는 메소드( 함수 또는 오퍼레이션) 들을 정의(캡슐화 Encapsulation) 합디다. 엔터티의 프라퍼티나 메소드는 클래스 바깥에 정의 되어서는 안됩니다.


객체지향 이란 무엇인가?

여러분이 믿고 싶어하는 것보다 훨씬 단순 합니다. 사람들은 실제의 그들 자신보다 더 영리해 보이기 위해 더욱 복잡한 정의를 사용합니다. 여기에 진짜 정의가 있습니다.

객체지향 프로그래밍 이란 캡슐화, 다형성, 상속 을 이용하여 코드 재사용을 증가시키고, 유지보수를 감소시키는 장점을 얻기 위해서 객체들을 연결 시켜 프로그래밍 하는 것 입니다.


객체지향 프로그래밍을 하기 위해서는 객체지향 언어가 필요합니다. 캡슐화, 상속, 다형성을 지원한다면 객체지향 언어라고 말 할 수 있습니다. 다른 기능들도 지원 할 수 있지만 그것들은 그닥 중요하지 않습니다.  이는 개인적인 의견이 아니라 객체지향 용어를 발명한 사람의 의견 입니다. Bjarne Stroustrup 은 그의 문서 Why C++ is not just an Object Oriented Programming Language: 섹션 3 에서 지금의 널리 사용되는 "객체지향" 이라는 용어를 제시하였습니다.

언어 또는 기술은 다음을 직접 지원 한다면 객체지향 이다.
1. 추상화 - 클래스나 객체를 제공한다.
2. 상속 - 이미 존재하는 것으로 부터 새로운 추상화를 만들어 낼 능력을 제공한다.
3. 런타임 다형성 - 수행시간에 바인딩 할수 있는 어떠한 폼을 제공한다.


많은 객체지향 언어들은 나중에 더 많은 기능들을 추가 하였고, 몇몇 사람들은 이 부가 기능들을 객체지향을 결정하는 요소라고 생각하지만 나는 전혀 동의하지 않습니다. 이는 온도조절기나 네비게이션이 없다고 자동차를 자동차가 아니다라고 말하는 것과 같습니다. 이것들은 구별되는 기능이 아니라 선택 사양 입니다.

바퀴가 있다고 해서 자동차라고 말 하는 것 또한 틀린 것입니다. 바퀴를 가졌다고 모든 것이 차가 되는 것은 아닙니다. 손수레에 바퀴가 있다고 해서 자동차가 되는 것은 아닙니다. 이것은 구별되는 기능이 아닙니다. 비객체지향 언어에 이미 존재하는 것이기 때문에 "모듈성", "재사용성", "메시징"을 비객체와 객체를 구별하는 기능이 아니라고 말하는 간단한 이유입니다.


OOP 와 non-OOP 의 차이점



비객체와 객체의 차이점을 설명하는 가장 좋은 방법은 실제 예제를 보는 것입니다.

다르게 정의 됩니다.

함수는 필요한 것을 가진 코드의 블록으로 정의 됩니다. 각각의 함수 이름은 어플리케이션에서 유일해야 합니다.

function fName ($arg1, $arg2)
// function description
{
    ....
   
    return $result;
   
} // fName


클래스 메소드는 클래스 정의의 경계로써 규정됩니다. 각각의 클래스 이름은 어플리케이션에서 유일 해야 합니다. 각각의 클래스는 클래스 내부에서 유일한 이름을 가지는 다수의 함수(메소드라고 알려진)들을 가질수 있습니다. 어플리케이션에서 유일할 필요는 없습니다. 사실 공통의 함수/메소드 이름을 가지는 것은 다형성에서 필요로 하는 공유의 능력을 위해서 입니다.

class cName
{
    function fName ($arg1, $arg2)
    // function description
    {
        ....
       
        return $result;
       
    } // fName
   
} // cName



접근 방법이 다릅니다.

함수/클래스는 그 정의가 로드되기 전에 접근될 수 있음을 주목해야 합니다. 

함수를 호출하는 것은 직접적입니다.

$result = fName($arg1, $arg2);


클래스의 메소드를 호출하는 것은 직접적이지 않습니다. 먼저 클래스의 인스턴스를 생성해야 합니다. 그리고  오브젝트의 함수 이름을 통해서 접근합니다.  오브젝트 이름은 어플리케이션에서 유일 해야 합니다.

$object = new cName;
$result = $object->fName($arg1, $arg2);


다수의 다른 복제본을 가지고 있습니다.

함수는 접근되기 전에 인스턴스화 될 필요가 없습니다. 단 한개의 카피본 만이 존재하기 때문이죠.

클래스 메소드는 오브젝트로 인스턴스화 된 후에만 접근될 수 있습니다. (정적 메소드로 정의된 경우를 제외하고) . 같은 클래스로 서로 이름이 다른 다중의 오브젝트로 인스턴스화 시킬수 있습니다.
 
$object1 = new cName;
$object2 = new cName;
$object3 = new cName; 

 
인스턴스화 시키지 않고 정적메소드에 접근할 수 있다 하더라도, non-Class 함수의 접근보다 나을 것이 없습니다. 오브젝트에 의해 실제로 사용되지 않으며, 객체지향 프로그래밍의 한 부분으로 고려되는 사항이 아닙니다.


다수의 엔트리 포인트를 가집니다.

함수는 단일의 진입점을 가집니다. 그것은 함수 이름 그 차체 입니다.
오브젝트는 여러개의 진입점을 가집니다. 각각의 메소드 이름 입니다.


상태 관리를 위한 다수의 메소드를 가집니다.

함수는 기본적으로 상태를 가지지 않습니다. 이것은 호출 될 때마다 새로운 실행으로 취급된다는 것을 의미합니다. 이전 실행과는 아무 연결이 없습니다.

오브젝트는 상태를 가집니다. 각각의 오브젝트의 메소드가 호출될 때 마다 오브젝트의 상태가 변경됩니다.

이것은 함수와 클래스 메소드 모두 로컬 변수를 사용한다는 점에서 같은 방식으로 동작합니다. 로컬 변수란 함수나 클래스 메소드의 범위를 넘지 않는다는 것을 뜻합니다. 그리고, 실행들 사이에서 저장되지 않는다는 것이죠.


함수가 서로 다른 수행에서 값을 기억할 수 있는 방법은 정적 변수를 사용하는 것입니다.

function count () {
    static $count = 0;
    $count++;
    return $count;
}

이 함수는 호출 될 때마다 이전 호출 값보다 하나더 증가된 값을 반환 합니다.  static 키워드가 없다면 항상 '1' 이라는 값을 반환 합니다.

클래스 레벨에서 선언된 변수를 함수(메소드) 범위 밖으로 저장시키려면 다음과 같이 하면됩니다.


class calculator{    // define class properties (member variables)    var $value;        // define class methods    function setValue ($value)     {        $this->value = $value;                return;            } // setValue        function getValue ()     // function description    {        return $this->value;            } // setValue        function add ($value)     // function description    {        $this->value = $this->value + $value;                return $this->value;            } // setValue        function subtract ($value)     // function description    {        $this->value = $this->value - $value;                return $this->value;            } // setValue    } // cName

클래스/오브젝트의 참조는 $this->varname. 과 같은  $this->  프리픽스 에 의해 참조 된다는 것에 주목하세요.
this 키워드로 참조되지 않은 $varname 변수는 로컬 변수로 취급됩니다.

각각 인스턴스는 자신의 변수를 가지고 있습니다. 같은 클래스로 부터 나왔을 지라도 하나의 오브젝트의 컨텐츠는 다른 오브젝트와 독립적입니다.


(실제 예제 - 캡슐화, 상속, 다형성의 예제는 번역에서 생략합니다. 예제는 원문을 참조하세요.)


결론.


많은 사람들이 OOP의 의미를  서로 다른 단어로 묘사합니다. 문제는 그 단어들이 오역되기 쉽다는 것이죠. 루이스 캐롤의 험프티-덤프티가 자칭한 유리창 너머로 볼때 처럼.

내가 이 단어를 사용할 때는 내가 선택한 뜻을 의미해, 더도 덜도 아니야


OOP의 창작자가 사용한 단어를 사용할 때, 그 단어에 다른 뜻을 적용한다면, 다른 사람은 여러분의 단어를 또 다른 뜻으로 적용합니다. 그것은 원래와 전혀 관계 없는 것으로 끝이 납니다.

객체지향과 비객체지향을 구별하는 데에느 단지 세가지 기능이 있습니다. 이것들은 캡슐화, 상속, 다형성 입니다. 이거 이외에는 헛소리 입니다. 객체지향 프로그래밍은 프로그래밍 언어에서 이 기능들을 이용하는 것입니다. 높은 재사용성과 낮은 유지보수 비용은 보장될 수 없습니다. 전적으로 이 기능들을 어떻게 구현하느냐에 달려 있습니다.

몇몇 사람들은 내가 OOP에 대해 너무 단순한 관점을 가졌다고 비난합니다. "필요이상으로 단순화 시켰다거나", "필요이상으로 복잡화 했다 거나" 라고 하는 대신에 말이죠. KISS 원칙의 오래된 추종자로써 나는 다른 사람들을 가르치기 쉬운 더욱 적절한 관점을 알고 있습니다.

### 끝.


더 읽을 꺼리 :
제목 : 구조적 프로그래밍 이란?
http://user.chol.com/~hurcb01/study5_sub7.html
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트
코딩의 시작은 객체의 생성 부터 입니다. 객체의 생성은 디자인패턴에서 생성패턴, 구조패턴, 행위패턴(Creational, Structural, Behavioral Patterns) 중 첫번째에 해당하는 것입니다. 생성패턴에 해당합니다. 개발 과정에서 객체의 생성은 매우 빈번하게 발생하며, 상태를 지정하고 변경하고 사용하는 방법을 디자인 하는 것은 매우 중요합니다. 객체지향 5원칙을 준수하는 생성자를 디자인하고 사용하는 방법에 대해서 다룹니다.

 

생성자를 디자인하다.

모든 시작은 생성자의 디자인으로 부터... 출처: 위키피디아, 천지창조 Creation Of Adam, 미켈란젤로


생성자의 책임

생성자는 최대한 가용상태임을 유지해야 하는데, 아래의 2가지 책임을 가집니다. 
1. 인스턴스를 생성한다. 
2. 인스턴스가 잘 못된 상태에 있지 않도록 초기화 되어야 할 항목을 사용자에게 알린다.


 

생성자 Constructor


생성자는 타입(type) 생성자 와 인스턴스(instance) 생성자 두가지가 있습니다.
public class Customer {
public Customer() { ... } // instance constructor
static Customer() { ... } // type constructor
}
타입 생성자는 static 이며 타입이 사용되기 전에 CLR(Common Language Runtime) 에서 실행 되며, 인스턴스 생성자는 타입의 인스턴스가 만들어 질때 실행 됩니다. 타입 생성자는 파라미터를 가질 수 없고 인스턴스 생성자는 가질 수 있습니다.


 

생성자의 디자인


1. 가능한 단순하게 하라. 이상적으로는 default 생성자를 제공한다. 
2. static factory method 형태를 제공하라. 이는 Loose Coupling을 지원한다. 
3. 메인 속성(Property)는 생성자 파라미터로 전달하라. 
4. 파라미터 이름은 속성이름과 동일하게 하라. 
5. 가장 최소한의 일을 하도록 하라. 가능한 파라미터를 속성에 할당하는 일만 하라.
6. 초기화에 문제가 있다면 생성자에서 예외를 발생시켜라. 생성자 범위를 벗어나면 잘 못 생성된 인스턴스가 사용될수 있는 상태에 놓여진다. 
7. default 생성자가 필요하다면 명시적으로 선언하라. default 생성자는 명시적으로 선언하지 않더라도 컴파일러가 암시적으로 만들어 놓는다. 파라미터가 있는 생성자를 나중에 추가 한다면 컴파일러는 암시적 default 생성자를 추가하지 않게되고, 이는 잠재적인 문제를 발생시킬 수 있다. 
8. 생성자 내부에서 virtual 멤버를 호출하지 마라. base 클래스의 생성자가 먼저 호출 되기 때문에 잘못된 초기화가 일어날 수 있다. virtual 메소드의 호출은 override 메소드가 담당하도록 해야 한다.


 

타입 생성자 디자인


타입 생성자란 타입을 초기화 하기 위한 static 생성자를 말합니다. 타입이 최초 사용되어 질때 CLR에 의해 실행됩니다. 
1. static 생성자는 private 으로 선언해야 한다. 타입 생성자는 CLR 이외의 곳에서 사용되지 않도록 설계 되어 있다. 
2. static 생성자에서는 예외를 던져서는 안된다. 현재 어플리케이션 도메인 내에서 의미가 없다. 
3. static fields 에 대한 초기화는 inline에서 수행하라. 생성자 내부에서 초기화 하는 field는 런타임이 최적화를 수행하지 못한다.


 

의존 주입 Dependancy Injection 관점으로 인스턴스 초기화 설명하기

(여기서 말하는 초기화는 일반적인 초기화 및 사용시의 가용상태를 만드는 것을 포함합니다.)

 

 

생성자에서 주입하기 Constructor Injection


생성자에서 파라미터를 전달하여 값(또는 객체, 이하 의존 dependency 이라고 칭함.)을 전달하는 것을 Constructor Injection 이라 정의한다. setter 메소드를 이용하는 방법을 Setter Injection 이라 정의 한다.
public class Emailer {
private SpellChecker spellChecker;
public Emailer(SpellChecker spellChecker) {
this.spellChecker = spellChecker;
}
}
[Constructor Injection]
public class Emailer {
private SpellChecker spellChecker;
public void setSpellChecker(SpellChecker spellChecker) {
this.spellChecker = spellChecker;
}
}
[Setter Injection] 

 생성자는 인스턴스 초기화의 책임이 있습니다. 초기화 할때는 속성에 의존을 할당하여 적절한 상태를 만들어야 하는데, 의존을 할당하는 방법은 생성자 파라미터, Setter 메소드 2가지가 있습니다. 

생성자와 일반 메소드와의 차이 : 
* 아무것도 반환하지 않는다.
 * 완전히 생성되기 전에 실행완료 되어야 한다. 
* final 필드를 최기화 할 수 있다. 메소드는 못한다. 
* 인스턴스 당 단 한번 호출된다. 
* 다른 이름을 지정할 수 없다. 클래스 이름과 같아야만 한다. 
* vitual 로 지정할 수 없다. 선언했으면 구현해야 한다.

 

 

Setter 메소드로 주입하기 Setter Injection


인스턴스가 완전히 생성된 후에 의존을 주입하는 것이 Constructor Injection과 다른 점입니다. Setter 메소드는 일반적인 메소드와 같은 방법으로 동작하면서 생성자의 제한을 극복하는 유연성(flexibility)을 가지고 있습니다. 하지만 유연성은 잇점을 가지고 있는 동시에 때때로 문제를 일으키는 원인이 되기도 합니다. Setter Injection은 일반적으로 매우 쉽게 사용됩니다. 많은 예제와 튜토리얼이 왜 사용되어야 하는지 아무 설명 없이 Setter Injection을 사용합니다. 사실 Setter Injection은 매우 유연하고, 편리합니다. 하지만 의존성 주입(Dependancy Injection) 관점에서 봤을때 완벽하지 않습니다. 

Setter Injection의 단점 : 
1. 인스턴스 초기화에 많은 setter가 필요할때 우리의 통제를 쉽게 벗어납니다. 
2. 클라이언트 코드 작성자는 setter 메소드가 사용되어야 할 적절한 시기에 대해서 알고 있어야 합니다. Setter Injection은 Information Hiding을 위반합니다. 클라이언트 코드 작성자는 사용하려는 타입에 대한 정보를 너무 많이 알아야 합니다.

 

인터페이스 주입 Interface Injection


Interface Injection은 인스턴스의 책임(role)을 주입해서 속성(property)에 대한 정보를 아는 것을 회피하자는 아이디어 입니다.
public class Rocket implements EngineMountable, Loadable, Startable,
Stoppable {
private Engine engine;
private Cargo cargo;
public void mount(Engine engine) {

this.engine = engine;
}
public void load(Cargo cargo) {
this.cargo = cargo;
}
public void start() {
//start this rocket's engine...
}
public void stop() {
//stop this rocket...
}
}
[Interface Injection] Interface Injection 의 장점: 
1. 목적에 맞는 이름을 붙일수 있다. setCargo(Cargo cargo) 보다는 load(Cargo cargo)라고 붙이는 것이 목적에 더욱 부합하며 이를 가능케 한다. 
2. 속성 정보에 대한 접근을 제한할 수 있다. 

단점: 
1. 모든 의존 마다 인터페이스를 정의하는 것은 낭비다. 
2. 많은 인터페이스를 나열하는 것은 class 정의를 깔끔하지 못하게 만든다. 
3. class signiture 를 지저분하게 만들며, 더 중요하게 처리해야 할 인터페이스로 부터 주의를 분산시킨다.

 

Method decoration (or AOP injection)


Method decoration은 의존 주입 방법의 변경에 흥미로워 합니다. 목적된 주입보다는 메소드 관점으로 접근합니다. 다시 설명하면, 원래 값이 아닌 injector-provided value를 반환하는 것입니다. 이 과정을 decorating 이라고 부르며 Decorator design pattern 으로 부터 이름을 따왔습니다.
public class Dispenser {
private Sugar sugar;
public Pez dispense() {
return new Pez(sugar);
}
}
[Method decoration]

 

 

Constructor vs. setter injection


Constructor 와 Setter injection 중에 선택하는 것은 인스턴스의 초기화에서 state 와 mutablility 두가지 요소에 의해서 좌우 됩니다. 우리는 대부분의 경우 필드가 딱 한번 할당되고, 변경되지 않길 바랍니다. 객체 생애 동안 의존이 변하지 않기를 바라는 것을 말하는 것이 아니라, 적시에 사용가능한 상태이어야 한다는 말입니다. Constructor Injection은 의존 불변을 제공해 줍니다. 고정된 의존 상태는 언제든지 사용가능하다는 것을 보증합니다. 프로그래밍에서 불변성(immutablility)은 매우 중요합니다. Setter Injection은 유연성을 가지는 대신에 immutablility를 훼손합니다. 그렇다면 Constructor Injection 만을 사용 해야 하느냐? 모든 의존이 결정되어 있고 불변하다면 Construction Injection 으로는 해결이 안되는 문제가 있습니다. 그것은 Reinjection Problem 이라고 합니다. 

 Reinjection problem : 의존하고 있는 인스턴스의 생애가 길고(long-lived) 의존 당하는 인스턴스의 생애가 짧으며(short-lived) 의존이 재 할당 되어야 하는 경우. 


의존이 예측되어 있고 불변한 것에는 Constructor Injection을 사용하고, Reinjection 이 요구되는 것에는 Setter Injection을 사용하는 것이 방어적이며 견고한 프로그래밍이라고 할 수 있습니다.

 

 

참고

Framework Design Guide Lines 2nd, 
Dependency Injection, Manning, by Dhanji R. Prasanna
저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트

OOD:데이터 접근 루틴 작성 가이드 :  이번 포스트의 주제는 데이터 접근 루틴을 어떻게 작성하는 것이 좋은가 입니다.

Low Coupling 포스트에서 언급 했듯이 하이-레벨 모듈에서, 커플링을 완전히 제거할 수 없습니다. 커플링을 제거하는 유일한 방법은 소프트웨어 전체를 한 덩어리의 모듈로 만드는 것 뿐이죠. 이렇게 해도 내부적으로는 더욱 많은 커플링이 발생하게 될 뿐이죠.

그래서 여러개의 모듈로 나누게 되는데, 나누어진 모듈은 서로 통신을 해야 합니다. 상대방의 데이터 또는 인터페이스를 사용하는 것이죠. 노출 방법의 결정은 객체 설계 및 클래스 작성 초기 단계에 요구되어 지는데, 어떻게 노출 할 것이냐, 데이터 접근 방법을 설명하는 글 Martin Fowler 의 Data Access Routines 의 번역 입니다.


기본값은 Private !!!



요약 :


클래스 또는 객체가 노출하는 데이터의 종류는 두가지가 있습니다. 하나는 단일값(single-value) 다른 하나는 콜렉션(collection) 입니다. 단일값은 접근자(accessor)를 이용해서 노출 하는 것이 좋습니다. 접근의 일관성을 유지해 주며, 행위를 부가할 수 있는 장점이 있기 때문이죠. 콜렉션의 경우 복잡한데, 외부로부터 콜렉션 요소(element)가 변경 되는 것을 막아야 하기 때문이죠. 그 방법 으로는 세가지가 있으며 복사본(copy), 보호 프록시(protection proxy), 반복자(iterator) 순으로 추천합니다. 비어있는 객체(empty-object)를 생성하고 멤버에 값을 대입하는 것이 좋은지, 모든 인자를 받아들이는 객체 생성자를 제공하는 것이 좋은지에 대해 후자를 추천합니다. 주의사항으로 접근자(accessor)는 꼭 필요한 경에만 사용해야 하며, 데이터 접근이라는 행위가 다른 오브젝트가 해야 할 행위를 하지 않도록 각별히 신경 써야 합니다.

 

번역 :

Data Access Routines


November/December 2003 IEEE SOFTWARE
Martin Fowler

좋은 설계의 가장 중요한 요소중 하나는 모듈성(modularity) 입니다-시스템을 조각으로 분리 하는 것이며, 모듈의 수정이 전체 시스템에 영향을 주지 않도록 할 목적을 가집니다.
일찍이, David Parnas 는 모듈들은 서로 비밀을 감추어야 함을 관찰했습니다. 그렇게 하면 비밀이 변경되어도 파급을 피할 수 있습니다.

오늘날 비밀을 감추는 가장 일반적인 것은 자료구조(data structure)입니다. 데이터는 언제나 비공개이어야 한다는 것은  객체지향설계의 격언입니다. 하지만, 데이터를 감추는 아이디어는 객체의 범위를 넘어섭니다. 저는 기본적인 데이터 은닉의 규칙을 이야기 하려 합니다.
저의 모든 예제는 객체를 이용합니다.(저는 거의 객체 맹신도 이죠). 하지만, 이 논의는 non-OO 모듈에도 잘 들어맞습니다.

데이터 접근 절차(data access routines)에 대해 생각할 때, 저는 두개의 주요 경우를 발견했습니다:
단일값을 캡슐화 하는 경우(person's name 같은 것) 또는 컬렉션( 주문의 상품 목록 같은 것). 이 두개가 약간 다릅니다. 컬렉션의 경우가 더 복잡합니다. 그래서 단일 값 부터 시작하도록 하시죠.

그림1. Encapsulating a single-valued field with accessors (in Java).
class Person {
private String name;
public String getName() {return name;}
public void setName(String arg)
{name = arg;}
}


그림2. Altering the data structure in Figure 1 while (almost) preserving the interface (in Java).
class Person {
private String lastName;
private String firstName;
public String getName() {
return firstName + “ “ + lastName;
}
public void setName(String arg) {
String[] words = arg.split(“ “);
if (words.length != 2)
throw new IllegalArgumentException
(“name must have two words”);
firstName = words[0];
lastName = words[1];
}


그림3. Encapsulating a single-valued field with a property (in C#).
class Person {
public String Name;
public static void Main() {
Person martin = new Person();
martin.Name = “Martin Fowler”;
System.Console.WriteLine(martin.Name);
}
}


그림4. Altering the data structure in Figure 3 while (almost) preserving the interface (in C#).
class Person {
public String Name {
get {
return _firstName + “ “ + _lastName;
}
set {
String[] words = value.Split(‘ ‘);
if (words.Length != 2)
throw new Exception (“name can only
have two words”);
_firstName = words[0];
_lastName = words[1];
}
}
private String _firstName;
private String _lastName;


그림5. A class that doesn’t fully encapsulate its collection field (in Java).
class Album {
private List tracks =new ArrayList();
public List getTracks() {
return tracks;
}
}


그림6. Preserving collection encapsulation by copying (in Ruby).
class Album
def initialize
@tracks = []
end
def tracks
return @tracks.clone
end
end
그림7. Using a protection proxy to encapsulate a collection (in Java).
class Album {
private List tracks = new ArrayList();
public List getTracks() {
return Collections.unmodifiableList
(tracks);
}
}


그림8. Using an iterator to encapsulate a collection (in C#).
class Album {
private IList tracks = new ArrayList();
public IEnumerator TrackEnumerator {
get {return tracks.GetEnumerator();}
}


 

단일값 접근하기 Accessing single values

가장 단순한 데이터 캡슐화 접근법은 모듈 바깥에서 데이터를 접근 불가능하게 하는 것 입니다. 그리고, 클라이언트에게 데이터를 얻을 수 있는 접근자(accessor)를 제공하는 것입니다.
그래서 Java 에서 그림1 과 같은 코드를 발견할 수 있습니다. 우리는 다음과 같은 코드로 데이터를 관리 할 수 있습니다.
aPerson.setName("Martin Fowler");
어떤 것을 이렇게 캡슐화 하는 가치는 접근자 함수에 행위(behavior)를 추가하는 것에 있습니다. 성 과 이름을 따로 저장하는 클래스를 수정한다면 이 방법을 통하여 전체 이름을 한번에 얻을 수 있습니다. 그림1 의 인터페이스를 예약해 놓고도 그림2 와 같은 방법으로 처리 할 수있습니다.
클라이언트의 인터페이스를 수정하지 않고도 내부 데이터 구조를 변경할 수 있는 능력은 데이터 캡슐화의 엑기스 입니다.(인터페이스가 정말로 예약 되진 않습니다.  기존의 코드는 '이름'을 두개의 단어가 아닌 것을 수용하고, 새로운 코드는 거절합니다. 저는 예제를 위해 이 코드를 남겨 두었습니다.)

이것이 일반적인 접근법 일지라도, 이렇게 작업하는 것은 편리하지 않습니다. 캡슐화된 데이터가 공개 데이터인 것처럼 보이는 것이 더욱 좋습니다. 그러면 우리는 다음과 같이 코드를 작성할 수 있습니다.
aCustomer.name = "Martin Fowler".

어떤 언어에서는 이 방법을 쓸 수 없지만, 속성(properties) 개념(notion) 으로 동일하게 처리할 수있습니다. 예를 들어 그림3 처럼 간단한 접근자를 C# 코드로 작성할 수 있습니다. 행위 추가가 필요해지면 그림4 처럼 하면 됩니다. 클라이언트는 속성(property)을 여전히 같은 방법으로 접근할 수 있습니다.(리플렉션을 사용한다면 .NET 에서 약간 다릅니다.) 결과는 훨씬 더 자연스럽게 느껴집니다. 적어도 저에게는 말이죠.

 

컬렉션의 값 접근하기 Accessing a collection of values


단일값을 캡슐화 하는 것은 매우 쉽게 이해 되지만, 다중값은 동일하게 이해되지 않습니다. 거기에는 미묘함이 있으며, 언어차원의 지원이 단일값 때 보다  적어 보입니다.

단일값과 다중값에 같은 인터페이스 스타일을 사용하는 것은 초보자의 가장 일반적인 실수 입니다-그렇게 하는 것이 종종 캡슐화를 깨뜨린다는 것을 알아차리지 못합니다.
그림5 의 코드를 보자면, track 은 완전하게 캡슐화 되지 않았습니다: 클라이언트는 track의 목록에 접근할 수 있고, album을 모르고도 track을 추가 , 삭제 할 수 있습니다. 콜렉션에 대한 접근을 캡슐화 라고 한다면, 누군가 아이템을 추가,제거 하려는 것을 우리는 항상 알아야 합니다. 고로, 무방비의 자료구조를 전달 해서는 안됩니다.

저는 캡슐화 하면서도 값을 읽는 능력을 지원하는 세가지 방법을 알고 있습니다: 복사 (copying), 보호 프록시(protection proxy), 이터레이터 사용(iterator).

셋중 가장 간단한 방법은 자료구조에 기반한 복사값을 전달하는 것입니다, 그림6. 업데이트를 방지하도록 감싸진 콜렉션을 전달하는 보호 프록시(protection proxy) 입니다(그림 7). 이 두가지 기술은 클라이언트에 콜렉션을 전달 합니다. 복사의 경우 클라이언트는 복사본을 변경할 수도 있지만, album 저장소에는 영향을 미치지 못합니다. 보호 프록시의 경우 , 콜렉션을 변경하련느 시도는 런타임 에러를 발생 시킵니다(C++ 의 경우 const 키워드는 같은 기능을 합니다.)

클라이언트는 종종 콜렉션을 반복하고, 그 엘리먼트로 어떤 작업을 하고 싶어 합니다. 그래서 다른 좋은 전략은 itorator를 제공하는 것입니다(그림 8을 보세요). 가장 흔한 것이 외부 iterator 입니다. 사용자가 콜렉션의 끝을 테스트 할 수 있고, 현재 객체를 접근해서 향상된 작업을 할 수 있도록 말이죠.

세가지 방법은 각기 장단점을 가지고 있습니다. 저는 무슨 일이 벌어지는지 명확한 신호를 주는 보호 프록시(protection proxy)를 좋아합니다. 이게 쉽지 않다면 복사본 방법을 사용합니다. 대부분의 사람들은 빈번한 복사에 의한 성능 문제에 대해 걱정합니다. 하지만 실무에서는 사람들이 생각하는 것보다 훨신 작은 문제 입니다(참조를 복사하는 것이지 객체를 참조하는 것이 아니기 때문이죠).
iterator는 반복만을 허용합니다-콜렉션의 다른 기능은 제공하지 않죠(size, contains, 등등). 저는 C# iterator의 접근방법을 선택하지 않을 겁니다. foreach 구문을 사용할 수 없게 되어 있기 때문이죠.

저는 종종 콜력션을 전혀 보호하지 않는 사람들을 보게됩니다. 오로지 콜렉션을 기반한 수정을 회피하라는 관례에 의존하는 사람들이죠. 전에 말했다시피 이것은 진정한 캡슐화가 아닙니다. 콜렉션을 직접 수정하려는 사람들을 곤란한 버그로 이끌 뿐이죠. 그러나 대안은 그 가치보다 곤란한 상황을 만듭니다.

iterator를 사용한다면 멤버가 콜렉션을 호출하는 것에 대해 걱정하지 않아도 됩니다. 콜렉션의 캡슐화는 멤버가 콜렉션을 수정하지 못하게 하는 것이 아닙니다. 콜렉션의 멤버쉽 변경(역주:콜렉션이 가지고 있는 의미)을 보호하는 것이지요.
반면에 add 와 remove 와 같은 modify 메소드는 쉽게 지원됩니다. 아마 addAll 메소드를 본적이 있을 겁니다. 대입(Assignment)는 드물게 발생합니다; 객체를 생성 할때 또는 추가, 수정 행위가 없다면 좋은 방법이죠. 그런 행위가 있다면, 대입은 매우 복잡해 집니다.

이런 종류의 콜렉션 캡슐화가  발생하기 쉬운 지역은 직접 접근을 기대하는 framework를 사용할 때 입니다. GUI 프레임워크가 가장 일반적인 경우 입니다. 콜렉션 인터페이스에 데이터방인딩을 하는 것이죠. 여기에서 불쾌한 두가지 방법이 발생합니다 :
캡슐화를 유지하고 편리한 바인딩을 포기한다. 또는 바인딩을 사용한다. 바인딩을 사용한다면 여러분은 class 의 캡슐화를 깨뜨리거나 콜렉션에 특화된 하부 클래스나 이벤트 같은 지저분한 기술을 사용하게 됩니다. 이것을 일반화 하는 것도 매우 어렵습니다. GUI 플랫폼과 도구 와 트레이드-오프 하려는 경향이 발생하기 때문이죠. 다음번에는 이것에 대해 다룰 생각입니다.: 다음에 다룬다면 저의 결론을 http://martinfowler.com 게시 하겠습니다.

 

자체 캡슐화 Self-encapsulation

여러분이 클래스에 접근자를 사용하는 것과 관계 없이 발생하는 공통적인 이슈가 있습니다. 접근자를 사용하는 것은 원리적으로 클래스에서 지속적으로 부가적인 행위를 한다는 것이죠. 여러분이 하위클래스(subclass) 와 접근자의 행위를 오버라이드 하고 싶다면 일을 매우 쉽게 만들어 줍니다. 안좋은 점은 코드를 좀더 뒤죽박죽으로 만듭니다. 저는 어느쪽을 사용하라고 강하게 주장하지는 않습니다.


객체 생성  Object construction

사람들은 종종 빈 객체를 만들고 setter를 사용하는 것이 나은지, 여러개의 argument를 받아 들이는 생성자를 사용하는 것이 나은지 묻습니다. 일반적으로 완전하게 형식을 갖춘 객체를 추천합니다. 그래서 항상 여러개의 인자를 받는 생성자를 추천합니다. 이는 특히 불변(immutable) 데이터에 유용합니다. 여러분은 setter를 포함하지 않더라도 명확한 신호를 전달 할 수 있습니다. 어떤 경우에는 빈-객체(empty-object) 방법이 좋을 수도 있습니다. 이것은 그래서 철벽같은 규칙은 아닙니다.

객체 생성시에 콜렉션 필드를 비어있는 콜렉션으로 초기화 한다면 삶이 더욱 편해 진다는 것을 항상 느낍니다. 이 방법은 이후의 코드에서 null 검사를 하지 않아도 된다는 것을 뜻하지요.

 

접근자에 세심하라 Be wary of accessors

이제 가장 중요한 경고를 해야겠죠 : 꼭 필요할 때만 접근자를 제공하세요. 접근자는 한객체가 다른 객체로 부터 데이터를 꺼내고, 어떠한 행위를 하게 되는데, 원래 소유자가 직접 해야하는 일을 할때가 종종 발생합니다. 접근자만을 가지고 있는 모듈은 대부분 나쁩니다. 진정으로 데이터를 감추고 접근자를 전혀 제공하지 않는 모듈이 좋은 모듈입니다. 이는 거의 불가능하고, 특히 data-oriented 어플리케이션의 경우가 그렇습니다. 하지만, 접근자를 호출할 때면 데이터와 함께 행위도 이동되지 않았는지 자문(ask yourself)해야 합니다. 최고의 규칙은 어떤 상황이든 한 객체가 다른 객체의 접근자를 여러개 호출하지 않는지 의문을 갖는 것입니다.

 ## 끝.

저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트

Protected Variation : '보호된 변수?, 변경? 변화? 으로 부터의 보호', 어렵습니다. 설계에 대한 용어는 매번 모호함으로 시작되는 듯합니다. 대충 private 으로 변수나 메소드의 접근을 외부로 부터 숨기는 것. 이라는 감이 옵니다. Variation을 어떤 단어로 대응해야 하는지는 Protected Variation 의 진짜 의미를 이해한 후에나 가능할 것 같습 합니다. Protected Variation 보다  정보은닉( Information Hiding) 이라는 용어가 훨씬 많이 사용됩니다. 정보은닉도 대표적으로 잘 못 해석되고 있는 용어이죠. 소프트웨어 설계에서 제대로 해석되는 용어가 존재하는지 의문이 들 정도입니다.

PV 는 David Parnas 가 소개한 용어로써, GRASP 의 아홉번째 항목으로 등장합니다. 변화로 부터 보호하는 패턴 이라는 것으로 말이죠.  Craig Larman 이 Protected Variation: The Importance of Being Closed (May/June 2001 IEEE SOFTWARE) 의 제목으로 PV를 설명하는 글을 기고 하였는데요, PV의 개념을 잘 이해시키는 글 입니다.

요약하자면 :
Protected Variation , Information Hiding, OCP는 같은 개념이다. 외부 모듈로 부터 '설계 결정' 을 감추어서 변화의 충격을 최소화 하는 것이 PV 이며, Data Encapsulation 과는 다른 의미이다. 유연성(flexibility)은 PV 로 얻어지는 산물중 하나이다.

Protected Variation 또는 Information Hiding 은  field 와 메소드를 private 으로 설정한다 정도의 지엽적이기 보다는 , 설계 결정 또는 설계 정보를 감추어서 외부의 변화로 부터 내부가 변경되지 않도록 보호한다 는 큰 개념. 으로 생각하면 이해하기 쉬울 듯 합니다.

보호는 그냥 되는 것이 아니다. 사소한 필드 부터 적절한 전략을 취해야 한다.



번역 :

변화로 부터의 보호 : 닫힘의 중요성.

Protected Variation: The Importance of Being Closed

by Craig Larman , May/June 2001 IEEE SOFTWARE

The Pattern Almanac 2000 (Addison-Wesley, 2000) 에는 소프트웨어와 관련된 패턴이 500여개 넘게 담겨져 있습니다. 호기심 많은 개발자는 프로그램을 작업할 시간이 없을 정도죠!
물론 이렇게 많은 패턴들은 개발자들이 오랜 시간동안 고민하고 논쟁해오던 주제와 원칙들을 단순화 할 목적을 가지고 있습니다.


Larry Constantine 의 coupling 과 cohesion 가이드라인 ("Structured Design," IBM Systems J., vol. 13, no. 2, 1974) 을 예로 들수 있듯이, 이 원칙들은 새로운 세대의 개발자와 아키텍트들이 무수히 많은 디자인 아이디어와 통합을 달성할 수 있도록  지속적으로 재 탐구 되어야 함이 당연 합니다.

Bertrand Meyer 가 Object-Oriented Software Construction (IEEE Press, 1988)에 기술한  Open–Closed Principle : 모듈은 확장(extention)과 적응(adaptation) 에는 열려(open) 있어야 하고, 클라이언트에 의한 변경 에는 닫혀 있으야 한다.
, 이 바로 그런 것입니다.

OCP는 본질적으로 Protected Variation pattern 과 동일 합니다. : 변경이 예측되는 지점을 판별하고 , 그것들을 위한 안정된 인터페이스를 생성하는 것.

Alistair Cockburn 은 PV("Prioritizing Forces in Software Design," Patterns Languages of Program Design, vol. 2, Addison-Wesley,1996)를 최초 기술할 때 OCP에 대해 알지 못했습니다.
David Parnas 의 OCP는 information hiding 을 뜻 하는 것이었습니다.("On the Criteria to Be Used in Decomposing Systems into Modules" Comm. ACM, vol.12, no. 2, Dec. 1972).

OCP 와 PV 는 많은 형태로 묘사되는 기반 설계 원칙을 형식화, 일반화 합니다. OCP 와 PV 는 작은 차이를 강조하는 것이 다를 뿐, 같은 원칙을 다르게 표현 한 것 이죠.-변화와 진화로 부터 기존의 코드와 디자인의 변경을 보호 하는 것.


저는 protected variation을 일반적 용어로 사용 하기를 제안합니다.개념을 짧고 명확하게 만드는 용어로 말이죠.

OCP 에서는 모듈에 포함된 소트프웨어의 모든 추상적인 요소를 칭하는 용어입니다. 메소드 , 클래스, 서브시스템, 어플리케이션, 기타 등등 말이죠.
또한 "X에 대해서 닫혀있다"라는 문구는 X의 변경이 클라이언트에 영향을 주지 않는다는 뜻입니다. 예를 들어 "클래스는 인스턴스 필드 정의에 대해 닫혀있다." PV 에서는 접근 관점으로의 광의적 표현으로 interface (java 나 COM의 인터페이스를 특정 하는 것은 아닙니다.) 라는 용어를 사용합니다.


정보은닉(information hiding)은 PV 이지 data encapsulation 이 아닙니다.


고전인 "On the Criteria To Be Used in Decomposing Systems Into Modules" 는 많이 참조 되지만 많이 읽혀지지는 않았습니다.
Parnas 는 거기에서 information hiding 을 소개 했습니다. 많은 사람들은 이 용어를 data encapsulation으로 잘 못 해석하고 있고, 몇몇 책에서는 유사어 개념으로 잘못 정의하고 있습니다.
Parnas 는 다른 모듈 내부의 어려운 것 또는 변경되기 쉬운 것들의 설계로 부터 정보은닉을 의미하려고 의도하였습니다.


정보은닉에 대한 설계 원칙 가이드로서 그의 논의를 빌자면 : 우리는 어려운 설계 결정의 목록으로써의 시작점, 변경되기 쉬운 설계 결정  대신에 제안하였습니다. 각각의 모듈은 다른 것에 대하여 그러한 결정을 감추도록 설계 되어야 합니다. Parnas의 정보은닉은 PV 나 OCP의 표현과 같은 원칙입니다. - 단순히 data encapsulation 이 아니라 설계 정보를 감추는 많은 기술중의 하나 입니다.
어쨋든, 이 용어는 data encapsulation 의 유사어 인 것 처럼 너무 광범위 하게 사용되어져서, 이제는 원래 의미로 사용 될 수 조차 없습니다.
Parnas의 PV 원칙에 대한 존경을 표시하자면 이 글을 "정보은닉의 중요성"이라고 불러야 마땅 합니다.
Dijkstra는 일찍이 이 원칙을 "THE" 프로젝트에서 암시했습니다. 하지만 Parnas 는 이것을 중요하게 생각하고 형태를 만들었죠. (Dijkstra, "The Structure of the 'THE' Multiprogramming System," Comm. ACM, 1968).


PV에 의해 동기를 얻은 메커니즘들 Mechanisms motivated by PV


PV는 수많은 프로그래밍 패턴 과 메커니즘들이 변화로 부터 설계의 유연성과 보호성을 제공하도록 동기를 부여한 근원 원칙 입니다.
여기에 몇가지 예가 있습니다. 데이터 캡슐화, 인터페이스, 다형성, 간접성, 표준성은 PV 메커니즘과 유사하며 PV가 동기를 부여한 것들 입니다.
브로커(broker)와 가상머신(virtual machine)과 같은 컴포넌트는 간접성의 복잡한 예 입니다.


통일된 접근 Uniform access

Ada, Eiffel, C# 과 같은 언어는 메소드와 필드에 대해 동일한 접근을 지원합니다. 예를 들어 aCircle.radius 는 radius() 를 실행합니다 :
클래스 정의에 기반한 public 필드에 대해 유동적인 메소드나 직접적인 참조를 의미.
여러분은 클라이언트 코드의 변경없이 public 필드가 메소드에 접근하도록 바꿀수 있습니다.


데이터-드리븐 디자인 Data-driven designs

데이터-드리븐 디자인은 코드, 값, 클래스 파일 경로, 클래스 이름 등 넓은 기술을 커버합니다. 다른 종류로는 외부 코드의 행위를 변경, 런타임에 "파라미터화" 를 위한 것, 객체-관계 매핑을 위한 meta-data, style sheet, 속성 파일 같은 것, 창 레이아웃을 읽는 것을 포함해서 말이죠.
시스템은 데이터, 메타데이터, 외부화된 변수의 선언을 읽는 행동 영향으로 부터 보호되어야 합니다.


서비스 룩업 Service lookup

서비스 룩업은 네이밍 서비스(예:JNDI), 서비스를 찾기 위한 트레이더(예: Jini) 기술을 포함합니다. 이 접근 방식은 클라이언트에게 안정적인 인터페이스를 제공함으로써 지역서비스에서의 변경으로부터 서비스를 찾는 것을 보호합니다. 이것은 데이터-드리븐 디자인의 특수한 경우 입니다.


인터프리터-드리븐 디자인 Interpreter-driven designs

인터프리터-드리븐 디자인은 외부의 소스,스크립트 또는 프로그램을 읽고 실행하는 규칙 자체를 해석하는 규칙을 포함합니다.
넷을 실행시키는 신경망 엔진. 이러한 접근은 외부 로직을 통하여 시스템을 변경시키는 파라미터화된 시스템을 가능케 합니다.
시스템은 외부 로직 표현의 변화로 부터 보호됩니다.

 

Reflective or metalevel designs

reflective, metalevel design 은 자바에서 bean 객체의 정보를 조회하기 위한 beans.Introspector 를 포함합니다.
bean 속성 X 를 얻기 위해 getter 메소드를 이용합니다(getXXX). 그리고 Method.invoke를 호출 합니다. introspection 과 metalanguage 서비스를 이용하는 Reflective는 외부 코드 변화의 충격으로 부터 시스템을 보호합니다.
우리는 이것 또한 데이터-드리븐 디자인이 포함하고 있는 특별한 경우라고 생각합니다.

Pick your battles

PV 어플리케이션의 예로써, 클라이언트에게 항공운항 논리를 설명하는 것은 머리아픈 일 입니다. 물류 비즈니스 로직을 지원하기 위한  변경이 매우 빈번하게  발생하기 때문이죠.
이런 변화에 대해 어떻게 시스템을 보호 할까요? PV를 지원하는 메커니즘(데이터 캡슐화, 인터페이스, 간접성)을 이용한 룰-기반 디자인(Rule-Based design)이 선택됩니다  :
룰 엔진은 시스템 소스 코드의 변경을 요구하지 않고도 룰을 업데이트 할 수 있게 합니다. 

Low coupling 과 protection

변화에 대항하는 것은 그냥 되는 것이 아닙니다. macro-architectural 레벨과 사소한 인스턴스 필드에서 부터 여러분은 설계에 적절한 전략을 취해야 합니다.
좋은 설계자는 변화와 불안전성이 존재하는 곳에는 PV 를 적용합니다. 그렇지 않으면 고생은 수포로 돌아가고, 복잡성은 증가할 것입니다. 또는 결함이 발생할 가능성이 높아지죠.
예를 들어 자바의 static public final field의 빈번한 사용으로 인해 놀랐던 경험을 회상합니다. 어떤 사람들은 잘 상상하지 못할 것입니다.
red, black, white등의 Color 로서의 static field 를 사용하는 것은 매우 안정합니다. 비슷하게 객체 순혈주의 입장에서 그것들을  private 또는 접근하는 메소드를 별도로 만들면 불안정하게 됩니다.
반대되는 경우로 유연성을 제공하기 위해 예쁜 스크립트 언어와 인터프리터를 추가한  pager-message-handling 시스템 아키텍트를 알고 있는데, 버전 향상 작업 동안에  복잡성과 비효율성을 이유로 스크립팅 부분은 제거 돼야만 했습니다.

 

현명한 PV 와 금강반야바라밀경 Judicious PV and the Diamond Sutra

low coupling 에 대한 Constantine의 설계 가이드라인은 설계의 핵심 원칙 이며, PV 는 여기에서 출발 했다고 말할 수 있습니다.
우리는 다음의 목표와 전락에 따라 우선순위를 정할 수 있습니다:
1. 우리는 시간과 비용을 줄이길 바랍니다. 새로운 결함을 줄이고, 중노동의 고통으로 부터 개발자를 구하길 희망합니다.
2. 이를 달성하기 위해 변경의 충격을 최소화 하도록 설계 해야 합니다.
3. 변경의 충격을 최소화 하기 위해 low coupling 이 달성되도록 목표해야 합니다.
4. low coupling을 달성하기 위해 PV로 설계 해야 합니다.

 Low coupling 과 PV 는 시간, 비용을 줄이기 위한 융합된 메커니즘 입니다. 이 목표를 달성하기 위한 불확실한 미래 방어 비용은 매우 높습니다.
highly couple 된 "깨지기 쉬운" 설계는 언제나 변화에 의한 압박으로 재작업이 요구됩니다. 진화 단계에서의 보호 엔지니어링 작업 비용은 단순한 설계를 재작업 하는 것보다 높습니다.


제 주장은 재작업과 깨지기 쉬운 설계를 변호할 목적이 아닙니다. 유연성이 필요하고, PV가 즉시 적용 가능하다면 PV를 적용하는 것이 옳습니다. 그러나, 불확실한 미래를 방어 하려거나, 재사용을 위해 이 전략을 사용하려는 것은 현명한 행동이 아닙니다. 풋내기 개발자는 깨지기 쉬운 설계를 하려는 경향이 있습니다. 예쁘고 일반화 시킨 유연성을 중간에 삽입하려 하죠(전혀 사용되지 않을 텐데도 말이죠).
전문가는 통찰력을 이용하여 단순하고, 깨지기 쉬운 디자인의 변경 비용 사이의 균형점을 찾습니다. 이는 잘 알려진 금강반야바라밀경을 수도하는 것과 같습니다:
선(zen, 禪) 을 수행하기 전에는 산은 산이요, 강물은 강물이더라.
선을 수행하는 동안에는 산은 더이상 산이 아니오, 강물은 더이상 강물이 아니다.
깨우친 후에는 또다시 산은 산이오, 강물은 강물이로다.

### 끝.


더 읽을 꺼리 :
Information Hiding : 이터너티 님의 블로그 포스트

The Difference Between Encapsulation and Information Hiding by stefanoricciardi

신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트


티스토리 툴바