컬렉션 구현 방법을 노출시키지 않으면서 그 집합체 안에 들어있는 모든 항목에 순차적으로 접근할 수 있게 해주는 방법을 제공 한다.
무궁화호 열차를 타면 열차 티켓을 확인하고 펀치로 찍는 승무원을 본 적이 있을 것이다. 무궁화호는 승무원이 있고, 발급된 티켓이 있다. 열차가 출발하면 승무원은 열차의 티켓을 확인한다. 이때 순서는 순차적(좌석번호 순)으로 접근하며 정보를 확인 한다.
무궁화호 열차 [MugungTrain.java]
무궁화호 승무원 [MugungTrainman.java]
티켓 [Ticket.java]
실행 프로그램 [StartApp.java]
지금까지 무궁화 호 열차는 아무 문제 없이 운행해 왔다. 그런데 이번에 새마을 호 열차 티켓 확인은 다른 방식으로 접근하게 되었다. 기존에 사용하던 티켓 배열 대신 java.util.List 인터페이스를 이용하는 컬렉션을 사용하기로 한 것이다. 해서 접근에 대한 문제점이 발견 되었다. 어떤 열차(비록 종류는 다르지만..)는 배열을 통해 티켓을 확인하고 어떤 열차는 컬렉션을 통해 확인하는 것의 문제점을 해결해야 한다.
사실 이부분은 어뎁터 패턴을 이용해도 된다. 예를들어 java.util.List의 경우 다음과 같은 코드가 작성 될 것이다.
이런식으로 원소에 접근하는 것 이다. 쉽게보면 이것은 이전에 살펴본 어뎁터 패턴을 통해서도 쉽게 사용 가능해 보인다. 하지만 이번엔 이터레이터 패턴을 이용해 적용 할 것이다. 이유는 이터레이터와 어뎁터는 '목적'이 전혀 다른 패턴이기 때문이다. 어뎁터는 서로 다른 클레스의 인터페이스를 호환시키는데 목적이 있고, 이터레이터는 '동일한 방법으로 순차적인 원소에 접근'하는데 목적이 있다.
열차의 티켓 확인은 순차적으로 모든 원소에 접근한다. 따라서 이터레이터가 목적에 더 맞을 것이다. 친절하게도 자바의 기본 패키지로 이터레이터 인터페이스를 제공해 준다. 어차피 오픈소스인 자바는 속 내용을 쉽게 알 수 있다. java.util.Iterator내용은 다음과 같다.
딱 3가지만 구현하면 된다.(JDK 5.0 기준이고, 1.4는 제너릭이 없을 것이다.) hasNext()는 다음원소가 있는지 여부를 확인(반복조건)하고 next()는 현재위치의 원소를 돌려주고 위치를 증가시키고, remove()는 현재 원소를 삭제 한다. remove()는 사용하지 않을것 이므로(열차 운행중 내쫓을수 없으니 ..)UnsupportedOperationException을 사용할 것 이다.
개선된 무궁화호 [MugungTrain.java]
개선된 승무원 [MugungTrainman.java]
새로 작성한 Iterator [IteratorMugung.java]
티켓(변화없음) [Ticket.java]
실행 프로그램 [StartApp.java]
java.util.Iterator를 상속받아 새로운 클래스를 만들었다. 그리고 승무원이 티켓을 확인할때 티켓을 넘겨주는 것이 아니라 작성한 Iterator를 넘겨 줬다. 이제 승무원은 Iterator를 통해 접근하면 될 것이다. remove()는 구현하지 않아서 throw했다.
이로써 얻는 이점은 동일안 인터페이스를 제공함으로써 지금까지의 방식과 앞으로 추가될 방식이 통합되는 점이 될 수 있다.
메소드에서 알고리즘의 골격을 정의한다. 알고리즘의 여러 단계 중 일부는 서브클래스에서 구현할 수 있고, 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의 할 수 있다.
PC방 (주)PUPUSTORY에서는 좀 더 나은 고객 서비스를 위해 게임의 종류에 따라 최적화된 PC를 제공하고 있으며, 이를 관리(엄밀히 따지면 재설치)하기 위해 프로그램을 제작 하였다. 처음 작성한 프로그램의 코드는 다음과 같다.
서브 pc클래스 (PointPC_01.java)
서브 pc클래스 (PointPC_02.java)
실행 프로그램(StartApp.java)
문제점은 바로 ‘공통되는 부분’이 많다는 점과 ‘할때마다 불필요 하게 구현하는 부분’이다. 모두 개별적이므로 하나의 코드를 수정할때 모든 코드를 일일이 수정해야 하며, 중복되는 코드도 많고, 새로운 게임이 출시하여 PC에 설치할때 프로그램의 배포시 새로 작성해야 하므로 여간 귀찮은 일이 아니다.
템플릿 메소드 패턴은 큰 의미로 ‘공통은 상위에서 구현하고, 서브클래스에선 구미에 맞게 알아서 구현(이를테면 추상화 된 메소드)한다.’라고 간단히 말할 수 있다. 여기서 좀 더 세밀하게 살펴보면 ‘공통은 상위에서 구현한다. 단, 많은 부분이 겹치고 몇몇의 서브클래스에서 변경사항이 있다면, 오버라이딩해서 사용한다.’고 할 수 있다. 이것은 템플릿 메소드 패턴이 간단하게 하나의 패턴 방식만 뜻하는 것이 아니라 ‘하나의 템플릿에 서브클레스가 템플릿을 그대로 사용할수도, 그것을 바꿀수도’있다는 큰 범위의 의미이다. 먼저 강제성(?)을 띄고 있는 추상화를 통해 구현해 보자.
템플릿 클래스 (PCInstaller.java)
서브 pc클래스 (PointPC_01.java)
서브 pc클래스 (PointPC_02.java)
실행 프로그램 (StartApp.java)
이 부분에서 접근자를 final로 적용한 메소드는 하위에서 오버라이딩 할 수 없으며, 특정 추상화된 메소드는 서브에서 반드시 구현해야 한다. 하지만 구현해야 할 내용이 많다면? 상위에서 해두는것이 더 수훨할 것이다. 만약 그부분이 ‘다른 서브 클래스’에서 바꿔야 한다면? 쉽게 오버라이딩을 이용하면 된다. 코드를 보자.
템플릿 클래스 (PCInstaller.java)
서브 pc클래스 (PointPC_01.java)
서브 pc클래스 (PointPC_02.java)
실행 프로그램 (StartApp.java)
일부는 추상화된 메소드를 사용하지 않았으므로 그냥 상위 클래스의 내용을 쓸 수도있고, 경우에 따라 오버라이딩(installOther())을 통해 재정의 할 수도 있다. 접근자가 final이라면 오버라이딩 할 수 없겠지만 여기선 public로 선언되어 있다. 하지만 상위 클레스에선 하는일이 없다. 그냥 {}만 있을 뿐이다. 이렇게 하는일이 없고(혹은 조건없이 같은값을 반환 한다던지 .. 즉 어떤 분기나 로직이 없는..), 재정의 가능한 메소드를 '후크(hook)’라 한다. 후크를 구현해 알고리즘에 추가 수행할 수도 있고, 이것을 내버려 두면 아무일도 하지 않을 것이다.
어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공. 퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할 수 있다.
필자는 거북이를 키운다. 근데 거북이 키우는게 보통일이 아니다. 그냥 관상용 물고기라면 .. 여과기 1개면 되는데 .. 거북이는 물이 엄청 빨리 더러워 진다.뭐 거북이가 아주 깨끗한 물에서만 서식하는것도 아니라지만 .. 그래도 보기에 좋지 않아 자주 청소해 준다. 근데 이 어항청소가 만만한게 아니다.
먼저 거북이 옮기고, 안에 악세사리 빼고, 물빼고 어항청소 하고, 악세사리 넣고, 물 채우고, 거북이 넣고 ..
갑자기 왜 이런얘기를 하는지 궁금하다면, 바로 퍼사드 패턴을 설명하기 위해서이다. 만약 자동으로 청소를 해주는 로봇이 있다고 한다면, 저런 명령을 하나하나 입력해 수행하도록 해야 할 것이다.별로 중간과정이 있는것도 아니고, 순서도 똑같다. 이런것을 일일이 수행한다면 다음과 같은 코드가 작성 될 것이다.
어항 (Aquarium.java)
어항 장식 (AquariumDecorate.java)
어항 물 (AquariumWater.java)
거북이 (Turtle.java)
실행 프로그램 (StartApp.java)
뭐 사용되는 클래스는 어려운 부분이 없다. 실행 프로그램 부분에선 청소의 일괄 과정을 수행하는데 하나하나 순차적으로 수행 한다. 좀 더 어렵게 말하자면, '작은 모듈들을 하나의 큰 비지니스로 만든' 형태'이다. 이번엔 퍼사드 패턴을 적용한 클레스를 이용해 코드를 작성해 보자.
실행 프로그램 (StartApp.java)
퍼사드 패턴 클래스 (AquariumFacade.java)
퍼사드 패턴을 적용한 클래스는 별다른 일이 없다. 단지 수행 과정을 method로 묶어둔 것 뿐이다. 사실 어찌보면 어댑터 패턴과의 차이를 이해할 수 없다. 엄밀히 따지면 어댑터 패턴은 '서로 다른 인터페이스를 이용할 수 있도록 연결'해주는 패턴이고, 퍼사드 패턴은 '부분 부분의 서브시스템을 한데 묶어 사용'하는 것 이라볼 수 있다. 형식은 비슷할 지언정 애초에 필요 조건이 틀린 것 이다.
한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 어댑터를 이용하면 인터페이스의 호환성 문제 때문에 같이 쓸 수 없는 클래스를 연결해 쓸 수 있다.
(주)푸푸스토리 토이즈에서는 이제 새로운 타입의 장난감을 생산하게 되었다. 기존의 장난감의 충전을 플러그로 했지만 이것은 국가별로 플러그를 별도 관리해야 하는 단점이 있었다.문제를 보완하고자 충전식 장난감인 Toy모델을 이제 PC의 USB케이블로 손쉽게 충전 가능하도록 만든 것이다.하지만 문제가 생겼다. 기존의 모델도 USB충전이 가능해야 하기 때문이다.(앞으로 출시모델이 아닌 지금부터 생산하는 모델을 기준으로 한다.)
때문에 대대적인 interface를 교체해야 하는 문제가 있어 새로운 디자인 패턴을 도입하기로 했다. 그것이 바로 어댑터 패턴(Adapter Pattern)이다. 어댑터 패턴의 정의는 위에서 설명한데로 '서로 다른 인터페이스를 사용하기위에 맞추는' 패턴이다.
여기서 살펴볼 코드는 기존의 PLUG방식을 USB방식으로 변경하는 것이다.
두개(신모델 구모델)은 서로 다른 interface를 갖고 있다. 따라서 이것을 쉽게 이용할 수 있는 중간의 매개체가 필요한 것이다.
구모델 인터페이스 (OldtypeToy.java)
구모델 장난감 (OldToy.java)
신모델 인터페이스 (NewtypeToy.java)
신모델 장난감 (NewToy.java)
구모델,신모델 어뎁트 (ToyAdater.java)
실행 프로그램 (StartApp.java)
구모델 에서 구현한 충전은 .onPlug(); 을 통해 충전했던 사항을 신모델에 맞춰 onUSBCharge();에서 사용하도록 했다. 큰 역할은 없고, 단지 onUSBCharge(); 를 호출하면 onPlug();를 내부에서 호출해주는 형식이다. 위의 코드에선 별다른 일이 없지만 만약 긴 코드에서 구모델과 신모델의 변경된 정도에 따라 새로운 코드가 삽입될 수 있다.
사실 이것은 우리가 너무 흔히 사용해온 방식이고, 쉽게 생각할 수 있는 부분이라 '이것도 패턴인가?'라고 말할 수 있다. 이제 막 디자인패턴을 보기 시작한 필자의 생각엔 이 '패턴'이라고 정의한것은 우리가 자주 사용하고 있단 개발방법에 대해 이름만 명명하고 '~~에서 유용하게 사용할 수 있다.'고 예시한것 뿐이지 그리 대단한건 아니라고 본다.
'어렵고 신기해야 패턴'이고 너무 쉽게 쓸 수 있고 이미 써왔다고 해서 '어째서 이런게 패턴인가?'라고 묻는다는건 억지라는 얘기다.
커맨드 패턴을 이용하면 요구 사항을 객체로 캡슐화 할 수 있으며 매개변수를 써서 여러가지 다른 요구 사항을 집어 넣을 수 있다. 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있으며 작업취소 기능도 가능 하다.
방송 스튜디오 조명회사 (주) pupustory에서는 이번에 2MB방송국 개국을 맞아 새로 만든 스튜디오의 조명공사를 들어갔다. 조명은 총 5개로 구성되어 있고, 5개의 조명은 기본적인 '켜짐, 꺼짐'기능을 갖고 있고, 5개의 조명은 개발적인 제어도 가능하고, 전체조명의 '켜짐, 꺼짐'도 가능해야 한다. 조명의 편리한 사용을 위해 '리모컨 하나로 모든 조작이 가능 하도록' 요구 하였다.
먼저 우리가 살펴볼 사항은 1. '조명' 5개와 그 조명을 관리하는 2. 모듈(Command), 사용에 편리한 기능의 3. 리모컨이 필요하다. 일단 각각의 조명은 기본적인 On,Off(켜짐, 꺼짐)기능이 동일하게 들어가므로 이부분을 통합할 수 있다. 즉 interface의 이용이 가능하단 이야기다. 그리고 리모컨에선 모듈들을 모아 관리하고, 경우에 따라 전체 On, 전체 Off, 개별적인 조명의 On,Off기능을 수행하도록 하면 될 것이다. 간추려 핵심만 지적하면 다음과 같다.
1. 5개의 조명이 있다.
2. 5개의 조명은 on,off의 기능이 있다.
3. 리모컨을 통해 5개의 조명을 모두, 혹은 개별적인 제어가 가능 해야 한다.
4. 경우에 따라선 on,off기능 외의것도 들어갈 수 있다.
우리에게 필요한건 '조명, 조명의 on,off모듈, 이것을 관리할 리모컨이렇게 필요하다. 여기서 공통점을 끄집어 내자면 '조명의 on off기능은 공통으로 적용'된다는 점이다. 그렇다면 이것을 '추상화'하는것이 가
능할 것이다. 그럼 이에맞게 코드를 작성해 보자.
조명 클레스 [Light.java]
커맨드(조명제어모듈) 상위 클레스 [Command.java]
구현된 off(커맨드) 클래스 [LightOffCommand.java]
구현된 on(커맨드) 클래스 [LightOnCommand.java]
리모컨 클레스 [RemoteControl.java]
실행 프로그램 [StartApp.java]
먼저 조명클레스는 큰 내용이 없다. on,off의 상태는 status를 통해 확인 할 것이다. 그리고 execute()가 정의되어 있는 커맨드(Command)는 '변경을 실행'한다고 생각하면 된다. 여기에서 변경은 on,off를 의미한다. 그것을 상속받아 구현된 on,off모듈인 LightOffCommand와 LightOnCommand은 Command를 상속받아 execute를 구현한다. 여기에 execute는 on인가? off인가? 를 판단하여 구현하면 될 것이다. 만약 새로운 명령어가 있다면? 여기에 추가하면된다. 어차피 레퍼런스로 사용할 Command에선 execute를 수행할 것이고, 객체에서 할 일에 대한 메소드를 정의하기 때문에 추가던 삭제던 코드의 큰 변화는 없다.
head first designpatterns에선 커맨드패턴에 undo기능까지 친절히 설명해 두었다. undo기능은 이전 상태로 복원한다는 의미로 봐도 되는데, 상태 변경은 어렵지 않다. 현재의 상태를 별도로 관리하고 복원하면 되기 때문이다. 만약 undo가 여러개 있다면 자료구조에서 필수코스로 거쳐간 '스택'을 이용하면 된다. 하지만 여기까지는 이 부분까진 다루지 않겠다. (사실 귀찮아서 --)
인터페이스를 이용하여 서로 연관되는,또는 의존하는 객체를 구상 클레스를 지정하지 않고도 생성할 수 있다
장난감 제조업체 (주)pupustory toys 에서 팩토리 메소드 패턴(이전 포스트를 먼저 참조하세요!)을 이용해 국가별로 장난감을 별도로 생산할 것을 생각해 이용했다. (만약 절대 확장이 없는 경우라면 복잡한 관계나 인터페이스 레퍼런스를 이용할 필요가 없을 수도 있다. 간단히 해결할 문제를 디자인 패턴을 적용한답시고 더 지저분 하게 할 수도 있기 때문이다. 뭐든 오버하면 안한만 못하니까 ..) 그런데 한가지 문제가 생겼다. 유럽 공장 클레스를 살펴보자
장난감을 만들고 메뉴얼을 넣고, 포장을 하는 부분은 해당 지역의 국가에 있는 공장에서 이루어 졌다. 그런데 이번에 회사 방침에 따라 '우리회사 대주주 2MB께서 "대한민국 국민이 '삽'에대해 안좋은 감정이 많은 것 같다. 모든 장난감에 '삽'을 추가해 땅파기의 진리를 아이부터 깨닳게 하자!" 는 취지로 한국은 물론 유럽 공장까지도 .. 앞으로 추가될 수백 국가의 공장에 추가하기로 했습니다. 그리고 '임의의 다른 삽'으로 추가하지 않고, '공식 공장 지정 삽'으로 넣도록 변경하라고 합니다.
하나씩 살펴보죠. 먼저 바뀌어야 할 부분은 어떤것일까요?
1. 상위 장난감 클레스(PupustoryToys)에 '삽'이 추가되어야 한다.
2. 국가마다 삽이 틀리다. 따라서 삽은 국가별 공장에서 '구현'하되, 삽을 추가하는 부분은 '임의 수정이 되지 않도록'해야한다.
3. 디자인 패턴에 맞게 '구현'보다는 '추상'에 중점을 둬야 한다.
먼저 상위 장난감 클레스에 '삽'을 추가하자.
장난감 최상위 클레스[PupustoryToys.java]
유럽형 장난감 클레스[Toy_eu.java]
한국형 장난감 클레스[Toy_ko.java]
다음은 좀 까다롭다. 구현한 공장에서 하는것이 아니라면 어떻게 해야할까..? 여기서 한가지 더 생각해 볼것은 '공식 공장 지정 삽'이라는 부분이다. 즉 공장에서 '삽'에 대해 지정은 했지만, 이것이 '공장 로직엔 제외'된다는 것이다. 그렇다면 어디서 하는게 좋을까?
공장에서 알 수 없도록 하기 위해 공장 로직에서 빠진다면 '공장이 아닌 다른 곳'에서 이것을 관리해야 할것이다. 따라서 '삽공장'클레스를 만든 후 이것을 '공장'이 갖고 있는 구조로 하면 쉽게 해결 된다. 공장은 삽공장 정보를 갖고 있고, 이것을 '장난감'에 넣어주기만 하면된다. 그럼 넣는 공정은어디에..? 이쯤되면 예상했겠지만 '장난감 클레스'에 '공장 클레스(가 가지고 있는 삽공장)'가 인자로 넘겨주고, '장난감 클레스'의 생성자에서 '공장 클레스에 있는 삽공장 레퍼런스'을 넣어주면 되는 것 이다.
생성자로 인자를 전달 하므로, 실제 장난감 객체가 생성되는 부분이 추가될 것 이고, 삽공장은 '공식 지정 삽'의 정보(엄밀히 따지면 삽의 객체를 반환하는 메소드)를 가지고 있을 것이다. 이제 해결책이 생겼다. 그럼 변경된 코드를 살펴보자.
삽공장 인터페이스 [IShvelFactory.java]
유럽형 삽공장 [ShvelFactory_eu.java]
한국형 삽공장[ShvelFactory_ko.java]
공장 최상위 클레스 [PupustoryFactory.java]
장난감 최상위 클레스 [PupustoryToys.java]
유럽형 장난감 [ToyFactory_eu.java]
한국형 장난감 [ToyFactory_ko.java]
유럽형 공장 [ShvelFactory_eu.java]
한국형 공장 [ShvelFactory_ko.java]
실행 프로그램 [StartApp.java]
코드의 주석을 살펴보면 보다 이해가 빠를 것 이다. 그렇다면 과연 이것은 어떤 의미에서 더 좋을까 ..? 일단 팩토리는 말 그대로 '공장'이므로 모두 같은 제품이 찍어저 나올 것 이다. (물론 그러해야만 하고..) 하지만 여기에 좀 더 추가되 생성되는 객체(제품)에 연관된 작업이 있다고 할 수도 있고, 무엇보다 '공장 클레스'로부터 어떤 작업(여기선 삽 설정)을 하는지 알 필요가 없어진다. 즉, 캡슐화가 가능한 것 이다.
그리고 생성된 객체(제품)은 공장으로 부터의 의존도가 없어진다.의존도가 사라지는 것은 매우 유용하다. 바로 구현된 클레스들에게 보다 정확한 역할을 부여하기 때문이다.
팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클레스의 인스턴스를 만들지는 서브클레스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클레스의 인스턴스를 만드는 일을 서브클레스에게 맡기는 것을 말함
장난감 제조업체 (주)pupustory toys 에서는 이번에 새로운 공장설비 프로그램을 작성하기로 했다. 기존의 낡은 방식에서 벗어나 이번엔 좀 더 유연하고 국가별로 서로다른 제품이 생산될 수 있도록 하는 프로그램으로 접근하기로 했다. 그렇게 나온 코드는 다음과 같다.
공장 상위 클레스[PupustoryFactory.java]
장난감 상위 클레스[PupustoryToys.java]
한국 공장 클레스 [ToyFactory_ko.java]
유럽 공장 클레스[ToyFactory_eu.java]
유럽에서 생산되는 장난감 Toy01[Toy_eu.java]
한국에서 생산되는 장난감 01호[Toy_ko.java]
실행 프로그램 [StartApp.java]
클레스가 여러가지 있지만 내부 내용들은 아주 단순하다. 간단히 설명하면 공장에 대한 상위 클레스(PupustoryFactory)가 존재 한다. 각 국가(ko, eu)에서는 상속받아 createToy()메소드를 구현 한다. (사실 이부분은 인터페이스로 해도 무관하다. 여러 코드를 더 넣었지만 보기에 가독성이 떨어질꺼같아 그냥 빼버렸다.) 그리고 createToy()에 인자로 넘어가는 toyType를 통해 생성하는 장난감을 확인 한다. 그리고 조건을 통해 타입에 대한 장난감을 생성하고 반환해 준다.
팩토리 패턴에서 사용된 팩토리는 어떠한 장난감이 생성되는지 알 수 없다. 그저 조건에 따라 레퍼런스에 장난감을 생성하고 그걸 반환해 주는 역할을 한다. 즉 장난감 내부에 어떤 로직이 있는지 알 수 없다는 것이다. 그저 생성 후엔 '메뉴얼을 넣고 포장을 한다.'정도만 알고 있다.
이렇게 정확히 역할을 분리함으로써 결합도가 낮아지기 때문에 보다 유연한 코드작성이 가능해 진다. 위 코드는 매우 단순하다. 국가별로 나눈 팩토리도 사실 하나로 묶어도 상관 없다. 하지만 나눈 이유는 무엇일까 ?
위 소스는 어차피 간단한 소스기 때문에 묶어도 상관 없지만 분류가 많아지고 내부로직이 바뀐다면? 예를들어 한국에서 생산된 장난감은 기본적으로 20%dc를 해준다고 가정 했을때 value필드에 가격을 내리는 로직이 추가되어야 할 것이다. 하나의 팩토리에 묶여있다면 이것은 모든부분을 일일이 찾아가 '한국 생산 공장 제품에 20%dc로직을 추가'해야 하는 제앙이 따르게 된다. 이러한 것을 방지할 수 있기 때문에 상위 클레스를 상속받아 이용하는것이 더 좋을 것이다.
싱글턴 패턴은 해당 클레스의 인스턴스가 하나만 만들어지고, 어디서든 그 인스턴스에 접근할 수 있도록 하기위한 패턴 입니다.
소프트웨어 전문 업체 (주)pupustory에서 이번에 새로 수주받은 프로그램은 '사원 출입 통제 프로그램'이다. 고객의 요구사항은 다음과 같다.
갑 : 우리는 권한이 있는 사람만 출입을 허용할 수 있도록 하는 프로그램이 필요합니다.
갑 : 우리 회사엔 동시에 수백명이 들어올 수도 있고, 너무 좋은회사라 한달정도 회사 전체 유급휴가가 있을 수도 있습니다.
갑 : 이왕이면 메모리를 적게 사용했으면 좋겠습니다. 이런 작은 프로그램에 자원을 많이 할당하고 싶지 않거든요. 그리고 시스템이 느린것은 용서할 수 없습니다.
갑 : 아차. 그리고 출입문은 차후 더 생길 것 입니다.
조건을 간단히 설명하면 회사 출입은 단 한개의 정문으로 들어올 수 있다. 차후 생길 여러개의 출입문(multi thread)에 대비해 만들어야 한다. 그리고 사원의 카드에 권한에 따라 출입여부가 결정 된다. 동시에 수백명이 한번에 몰릴 수도 있고, 어떨땐 한명도 접근하지 않을 수 있다.
이것과 비슷한것을 생각해보면 먼저 우리가 자주 사용한 DB POOL을 들 수 있다.DB에 대한 커넥션을 얻어오기 위해 POOL에 요청을 하고 미리 만들어진 인스턴스를 받아와 사용할 때 말이다. 이것을 풀어 얘기하면 결과적으로 우리가 만들려고 하는 프로그램과 같은 얘기가 된다. 출입 통제를 확인하는 인스턴스 하나를 두고, 사용자 권한을 확인해 출입 가능 여부를 확인하는 것이다. 코드를 살펴보자.
출입통제 프로그램 [GateApp.java]
사원 권한 [UserInfo.java]
실행 프로그램 [StartApp.java]
주석을 살펴보면 왜 저렇게 작성하게 된 것인지 알 수 있다. 생성자를 private로 함으로 써 외부에서 감히 객체를 생성할 수 없도록 했으며, 동기화 걸린 static 메소드인 getInstance()를 통해 생성된 인스턴스를 반환해 준다.(최초 호출시엔 당연히 인스턴스가 없으므로 null 여부를 확인 후 생성해 반환한다.)
다중 쓰레드에 의한 접근에도 본 프로그램은 문제가 없을 것이다. 왜냐하면 getInstance()부분에서 동기화가 되어 있으므로 동시에 접근해 instance를 두개 이상 생성할 수 없기 때문에 이정도로 프로젝트가 완료되어도 될 듯 하다.
그렇다면 이런 질문이 있을 수도 있다. '그냥 static로 이용해서 하면 안되나?'라고 .. 사실 static로 해도 문제가 안될 수도 있다. 위의 프로그램은 간단한 메소드 한개만 가지고 있다. 그런데 갑의 요구사항엔 다음과 같은 내용이 있었다.
갑 : 우리 회사엔 동시에 수백명이 들어올 수도 있고, 너무 좋은회사라 한달정도 회사 전체 유급휴가가 있을 수도 있습니다
즉, 어쩌면 인스턴스가 아예 생성되지 않을(사용을 전혀 안할수도) 있다는 얘기다. static로 할 경우 처음에 메모리에 올려버리기 때문에 한번도 사용하지 않는 인스턴스를 쓸대없이 올라와 있게 된다. 싱글턴 패턴은 이러한 낭비를 최대한 줄일수도 있고, 객체생성을 늦출수도 있다.또 static로 하게 되면 내부에서 사용되는 인스턴스는 반드시 static이여야 하므로 로직이 복잡하게 들어 간다면 모든 인스턴스를 static로 해주어야 하는데, 이것은 코드가 복잡해지고, 차후 수정시(혹은 일부분 재사용)제약을 받게 될 것이다.
싱글턴 패턴의 이용 분야는 여러가지 있겠지만 이정도면 충분히 설명이 되었다 생각된다. 이쯤에서 한번 살펴볼 사항이 있다. 바로 '성능'부분이다. 자바의 동기화는 '이왕이면 .. 이왕이면 ..'하며 반드시 필요한 부분에 사용하길 권장한다. 이유는 동기화가 걸리게 되면 성능이 엄청나게 저하되기 때문이다. 여기에 한가지 해결책이 있다.
향상된 출입 프로그램 [GateApp.java]
위 코드는 먼저 getInstance()부분에 동기화가 제거 되어있다. 그리고 조건을 확인 한 후 객체가 null일경우 동기화 블럭을 수행한다. 수행시 한번 더 확인하는 이유는 위에서 설명한데로 동기화 블럭을 진행중일때 동기화 블럭에 대기중인 객체가 있을 수 있기때문에 한번 더 확인하는 것이다.
여기서 눈의 띄는 부분은 volatile 키워드이다. 이것은'원자성을 보장'한다고 되어있다. 이에 대해 간단히 말하자면 우리가 int변수를 선언하면 32bit 의 영역이 생기게 된다. 만약 double를 선언하면..? 64bit가 선언되어야 하는데 이것은 32bit를 2개 붙혀둔 모양이다. 다시말해 [32bit][32bit]이런식으로 구성되어 있는데 만약 두개의 프로그램에서 여기에 동시에 접근했다고 가정 하자.
프로그램 a에선 앞의 [32bit]에 접근했고, b에선 뒤의 [32bit]에 접근 했다면 double의 값은 프로그램a의 값도 아니고 b의 값도 아닌 전혀 엉뚱한 값이 들어가 있게 된다. 이것을 보호하는 키워드가 바로 volatile인것이다. 이런 경우는 정말 java에서는 있기 매우 힘든경우고 없다고 해도 과언이 아닐듯 하다. 하지만 instance의 경우엔 생성자를 호출하고, 객체가 생성되는 시간을 생각해 본다면 정밀한(?) 멀티 쓰레딩 프로그램에선 보다 나은 안정성을 보장할 것으로 보인다.
지금까지 회사에 부품은 총 A~F까지 존재했지만 앞으로 추가될 부품은 계속 늘어날 것으로 예상 된다.
사실 그냥 사용하는것도 나쁘진 않다. 어차피 상위 클레스에 존재하므로, 그것을 상속받아 cost()에서 반환할때 값만 더해주면 문제가 없으니 말이다.
하지만 생각해보자. 우리는 기본적으로 OOP에 대해 공부할때 '확장의 용이성'에 대해 언급한 적이 있을 것이다.(기억 안나면 그냥 넘어가자..)
위 코드방식을 계속 고집한다면 상위 클레스의 덩치는 계속 커져가고, 결과적으로 이것은 확장성을 제로에 가깝다고 할 수 있다.
또 부속이 늘어나 1000가지가 되었다고 가정 하자. 하지만 제품에 들어가는 부속은 단 2~3가지라고 가정할때 나머지 부속에 대한 정보를 상속받아 하위클레스가 가지고 있는것은 쓸대없는 낭비일 뿐 아니라 보안상으로도 별로 보기좋은 모습은 아니다.
이럴때 생각해 볼 수 있는것이 '데코레이터 패턴'이다.
우리의 문제점을 정확히 해결해 줄 수 있는 패턴인듯 하다. 일단 주목할 것은 '추가적인 요건을 동적으로 첨가'한다는데 있다.
우리의 부속품을 첨가한다 가정할때 동적으로 첨가하므로 상위 클레스의 덩치가 커지는(혹은 변경되는)일을 막을 수 있다.
물론 '서브클레스에서 만드는것을 통해 기능을 유연하게 확장'할 수 있으므로 문제점에 정확한 해결책 일 것이다.
기본적 골격의 코드는 다음과 같다.
Mp3Player.java [Mp3제품 클레스(상위)]
Mp3Part_A.java [부속품A 클레스]
Mp3Part_B.java [부속품B 클레스]
Mp3Part_C.java [부속품C 클레스]
Mp3Mdl_A.java [부속품A 클레스]
Mp3Mdl_B.java [부속품B 클레스]
Mp3Mdl_C.java [부속품C 클레스]
위 코드를 보면 클레스가 많이 나뉘어 있어 보기 어려울 수도 있다. 하지만 최상위 클레스 Mp3Player와 그에대한 부품(Mp3Part..), 기본 제품(Mp3Mdl..)로만 나뉜 쉬운 구조다.
위 구조에서 보는바와 같이 먼저 Mp3Player에 cost() 메소드를 부품과 제품 모두 상속받아 가격을 책정했다.
여기서 '기본 제품'과 '부품'의 차이점은 생성자에서 볼 수 있다. 먼저 기본제품은 기본생성자가 생략되어 있다. 하지만 부품에선 상위 클레스 Mp3Player타입의 인자를 받는다.
이것은 '기본 제품을 생성자에서 인자로 받아 제품을 가지고 있는구조'라고 할 수 있다.
또다른 차이점은 바로 cost() 메소드이다. '기본 제품'은 단순한 자신의 값만 반환하지만 부품은 '자신이 생성자로 넘겨받은 (바로 기본제품!)의 가격에 자신의 가격을 더해서 반환한다.
이러한 구조로 되어있으므로 우리가 사용할땐 '기본 제품'을 '부품'클레스로 감싸서(생성자로 인자른 넘겨서)사용하는 것으로 부품이 추가될때마다 기존에 추가된 것을 가지고 있는 객체를 넘겨주면 된다.
내부적으로 이것은 부품이 넘어온 부품을 가지고 있고, 그 부품은 또다시 넘어온 부품을 가지고 있는식으로 되어있다.
즉, 최종적으로 외부에서 cost() 메소드를 호출하면 그 메소드는 '기본 제품'까지 내부적으로 계속 호출하며 가격을 추가하게 되고, 모두 추가된 값을 돌려받는 것이다.
어쩌면 '이것은 재귀호출인가..?'라고 생각할지 모르지만 전혀 틀리다. 재귀호출은 자신의 메소드를 계속 호출하여 깊이 들어가는것을 의미한다.
하지만 이것은 객체가 자신의 내부에 객체를 갖고있고,또 그 객체는 또다른 객체를 갖고있는 형식이다.
(말이 좀 어렵다. 하지만 코드를 계속 보고, 또는 직접 작성해보면 이해가 더 빠를 것이다.)
디자인 원칙
클레스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.
핵심정리
1. 상속을 통해 확장을 할 수도 있지만, 디자인의 유연성 면에서 보면 별로 좋지 않다.
2. 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다.
3. 구성과 위임을 통해 실행중에 새로운 행동을 추가할 수 있다.
4. 상속 대신 데코레이터 패턴을 통해서 행동을 확장할 수 있다.
5. 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터들을 사용한다.
6. 데코레이터 클레스의 형식은 그 클레스가 감싸고 있는 클레스의 형식을 반영 한다.(상속 또는 인터페이스를 통해 자신이 감쌀 클레스와 같은 형식을 가짐)
7. 데코레이터에서는 자기가 감싸고 있는 구성요소의 메소드를 호출한 결과에 새로운 기능을 더함으로써 행동을 확장 한다.
8. 구성 요소를 감싸는 데코레이터는 개수 제한이 없다.
9. 데코레이터패턴을 너무 많이 사용하면 코드가 필요 이상으로 복잡해질 수 있다.
먼저 우리는 interface INewsPaprCmpny (신문사)를 사용했다. 그리고 interface INewsReader (구독자)를 이용했다.
그것을 상속받은 클레스는 해당 메소드를 구현 했고,신문사의 뉴스가 추가발생하게 되면 setNewsUpdate() 메소드를 호출 한다.
메소드가 호출되면 'this.notifyReaders();' 구문이 실행되므로 구독하고 있는 모든이에게(ArrayList) 뉴스가 추가되었고,
구독자의 메소드에 뉴스 갯수와 날짜를 알려주므로 구독자는 그 정보를 갱신하게 된다. 그리고 그 결과를 display() 메소드로 출력 한다.
Q. 위와같은 코드로 작성한다면 변경이 있을때마다 모두에게 호출하게 된다. 어떤 객체는 단1개의 값만 요구할 때가 있을 수 있다. 그런거라면 getter를 이용해 해당 객체가 알아서 가져오는건 어떨까? A. 이것은 사용자가 임의로 경우에 맞게 사용할 수 있다. 바로 '푸시'와 '풀' 방식을 이용할 수 있기 때문이다.
간단히 설명하자면 푸시방식은 위의 방식대로 '신문사 -> 구독자에게 값을 밀어주는(푸시) 방식'이다.
주제의 값이 바뀌면 신문사는 메소드를 호출해 구독자에게 변경값을 통보해 준다.
하지만 모든 구독자가 이런 방식을 취하지 않아야 하는 경우도 있을수 있다. 예를 들어 신규뉴스건만 필요하고 갱신날짜는 필요 없을 경우도 있다.
바로 '신문사 -> 구독자에게 요청해 값을 가져가는(풀 방식) 방식'인 것이다.
이렇게 될 경우 단지 구독자 클레스는 신문사 클레스의 레퍼런스를 보유하고, 게터(getter)를 이용해 받아오는 방법을 적용하면 쉽게 될 것이다.
java에서는 친절하게도 이러한 옵저버 패턴을 손쉽게 사용할 수 있는 API를 제공한다.
바로 java.util패키지에 있는 Observer [interface]와 Observable [class]가 그것이다. 두가지를 살펴보면 ..
위에서 주목할 것은 setChanged() 메소드와 관련된 것 들이다. Obserable 소스를 살펴보면 알겠지만 내부적으로 변경여부에 대한 boolean changed 값이 존재하고, setChanged() 메소드를 통해 값을 'true'로 바꾼다. 이렇게 값이 바꾼 뒤 notifyObservers() 메소드를 진행하게 된다. 이때 if (!changed) 조건으로 조회를 하므로 반드시 setChanged()를 호출해야 하며, 호출 뒤 값은 자동으로 스위칭 된다. (스위칭을 강제로 하려면 clearChanged() 를 호출하고, changed값을 알고자 한다면 hasChanged()를 호출하면 된다. 기타 자세한 것은 javadoc 이용)
중요한것은 바로 '풀'과 '푸시'방식의 선택이다. 위에선 주석처리 되었지만 구독자 클레스의 public void update(Observable o, Object arg)를 보면 알 수 있다.
이쯤되면 이미 알고 있겠지만 푸시 방식은 신문사가 값을 구독자에게 '떠먹여 주는'방식이다. 따라서 신문사가 푸시 방식을 적용하게 되면 notifyObservers(Object arg) 메소드를 호출하게 되는데,
구독자 클레스의 public void update(Observable o, Object arg) 에서 arg로(신문사에서 NewsInfoBean로 넘겨 줌) 넘겨주게 되고, 구독자는 그것을 Object로 받아 케스팅 후 게터(getter)로 자신의 값을 변경 했다.
즉, 신문사가 'bean에 담아서 정보를 너에게 줄테니 넌 그것을 받아서 자신의 값을 변경하도록 해!' 하는 방식인 것이다.
풀 방식을 적용하게 되면 인자 없이 notifyObservers()를 호출하게 되는데 당연히 구독자의 void update(Observable o, Object arg) 중 arg는 null넘어가게 되고 o 객체에는 '신문사'객체가 넘어가게 된다.
신문사 객체는 자신의 값을 가져올 수 있는 게터(getter)를 보유하고 있으니 구독자는 그것을 받아와 사용할 것이다.
즉, 구독자가 '신문사가 자신의 객체를 넘겨주고, 그것에 게터(getter)가 있으니 그걸 받아서 내 값을 변경하면 되겠다!'는 방식인 것이다.
여기서 java.util.Observable의 단점은 다음과 같다.
1. Observable는 class이므로 내가 사용하는 class가 이미 상속받은 class가 있다면 사용할 수 없다.
(이것은 자바에서 말하는 class상속은 단 1개만 된다는 것으로 인해 발생 한다.
또 Observable 인터페이스가 제공되지 않는다.)
2. Observable의 핵심 메소드는 외부에서 호출될 수 없다.
setChanged() 메소드가 protected로 선언되어 있으므로 상속관계 등의 관계가 아니라면 아예 접근할 수 없다.
위의 단점을 극복하기 위해선 애초에 상위 클레스에 Observable를 상속받아 작성한 후 그것을 다시 상속하는 방법이 있을 수 있다. 아니면 아예 처음에 만들었던 방식으로 직접 구현하는 방식 등의 여러 해결책이 있을 수 있다.
디자인 원칙
애플리케이션에서 바뀌는 부분을 찾아내서 바뀌지 않는 부분으로부터 분리 시킨다.
(이 패턴에서는 주제와 옵저버 상태만 바뀌게 된다. 주제의 내용(코드)를 바꾸지 않고도 청취객체를 바꿀 수 있다.)
특정 구현이 아닌, 인터페이스에 맞춰서 프로그래밍 한다.
(주제와 청취객체들은 모두 interface를 이용하므로 느슨한 결합이 가능하다.)
상속보다는 구성을 활용한다.
(주제는 옵저버를 관리한다. 상속관계가 아닌 구성으로 이루어 졌으므로 유용하다.)
핵심정리
1. 옵저버 패턴에서는 객체들 사이에 1:n관계가 형성된다.
2. 주제 또는 Observable객체는 동일한 인터페이스를 사용해 청취객체에게 연락한다.
3. Observable에서는 청취객체들이 Obserable인터페이스를 구현하다는것을 제외하면 청취객체의 내용을 전혀 모른다.
(느슨한결합)
4. 옵저버 패턴엔 푸시방식과 풀방식이 있다.
5. java.util.Obserable에서는 청취객체들에게 연락하는 순서에 의존해선 안된다.