[메타코딩] 디자인패턴 강의
- object는 class 설계도 상 하나의 책임만을 가진다.
- 그러나, 무조건적인 것은 아니고 권장사항이다.
- ex.
Add class가 있을 때, 이 클래스는 더하기 기능만을 수행해야한다. 곱하기 기능, 나누기 기능 x
- 개방(확장)에는 열려있고, 수정(변경)에는 닫혀있다.
- 요구사항의 변경이나 추가사항이 발생하더라도 기존 구성 요소는 수정이 일어나면 안된다.
- 확장, 재사용
- OCP를 위해선 추상화와 다형성이 중요.
- 구체적인 것에 의존하지 않고 추상적인 것에 의존한다.
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.
ex. 자동차 인터페이스에서의 엑셀은 앞으로 가는 기능. 뒤로 가게 되면 LSP위반이 된다.
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 구체적인 것에 의존하지 말고 추상적인 것에 의존한다.
같은 문제를 해결하는 여러 알고리즘이 클래스별로 캡슐화되어 있고 이들이 필요할 때
교체할 수 있도록 함으로써 동일한 문제를 다른 알고리즘으로 해결할 수 있게 하는 디자인 패턴
<처음 문지기의 임무>
- 쥐를 쫓아내
- 호랑이를 쫓아내
처음은 쉽다. but, 소, 말, 다람쥐 등등이 추가될 때마다 일일히 코드를 수정해야할까?
타겟을 구체적인 것에 의존하지 않고 추상화해야함.
따라서, "동물을 쫓아내"로 바꾼다.
(경로 계획 전략들)
- 전략 패턴을 사용함으로써 해결되는 문제점
- 기존 코드의 내용을 수정하여 위배되는 OCP
- 시스템 확장 시 메서드 중복 문제
-
프록시(Proxy) : 대리자, 대변인
-
어떤 객체를 사용하고자 할 때, 객체를 직접 참조하는 것이 아닌 해당 객체와
대응하는 객체를 통해 접근하는 것. -
메소드를 직접 호출하지 않고 왜 Proxy를 사용하는 것일까?
- 결국은 "흐름제어"
- 데이터 사이즈가 큰 이미지 같은 경우, 로드될 때, 시간이 걸리게 됨.
따라서 제어흐름을 통해 데이터가 로딩될 때까지 현재까지 완료된 것을
우선적으로 보여주는 것. - 객체의 은닉화.
<정의>
- 호환성이 없는 기존 클래스의 인터페이스를 변환 -> 사용자가 기대하는 인터페이스 형태로 변환
- 코드의 재활용성을 증가하고 기존의 코드를 수정하지 않는다는 장점. (OCP 원칙을 지키게됨)
<사용시점>
- 외부 요소를 기존 시스템에 재사용하고 싶지만 아직 만들어지지 않은 경우
- 외부 요소(애플리케이션)가 기대하는 인터페이스와 호환되지 않는 경우
<예제>
OuterTiger.java
public class OuterTiger {
private String fullName = "호랑이";
public String getFullName() {
return fullName;
}
}App.java
public class App {
public static void main(String[] args) {
Mouse m = new Mouse();
Cat c = new Cat();
OuterTiger ot = new OuterTiger();
DoorManProxy dm = new DoorManProxy(new DoorMan());
dm.쫓아내(m);
dm.쫓아내(c);
dm.쫓아내(ot);
}
}위와 같이 설계하면
OuterTiger객체에서 오류가 발생한다. Why?OuterTiger는 Animal 타입이 아니기 때문에
그렇다면 어떻게 수정하면 좋을까?
OuterTiger.java
public class OuterTiger extends Animal {
private String fullName = "호랑이";
public String getFullName() {
return fullName;
}
@Override
public String getName() {
return null;
}
}위 코드의 문제점은 뭘까? OCP 원칙을 위배. (확장에는 열려있고, 수정에는 닫혀있다.)
public class TigerAdapter extends Animal{
private OuterTiger outerTiger;
public TigerAdapter(OuterTiger outerTiger) {
this.outerTiger = outerTiger;
}
@Override
public String getName() {
return outerTiger.getFullName();
}
}
Adapter를 생성함으로써 Animal 객체를 상속받을수 있게 되었다. (재사용)
또한, 기존 시스템을 건드리지 않고 해결하게 되었다.
App.java
public class App {
public static void main(String[] args) {
Mouse m = new Mouse();
Cat c = new Cat();
TigerAdapter ot = new TigerAdapter(new OuterTiger());
// 어댑터를
DoorManProxy dm = new DoorManProxy(new DoorMan());
dm.쫓아내(m);
dm.쫓아내(c);
dm.쫓아내(ot);
}
}- 어플리케이션이 시작될 때 어떠한 클래스가 최초 한 번만 메모리를 할당하고 메모리에 인스턴스를
만들어 사용하는 패턴
DoorMan.java
public class DoorMan {
// 1. static 영역에 객체를 1개 생성
// (자바에서 static은 main 메소드를 호출하기 전에 JVM이 먼저 읽어서 메모리에 올라옴.)
private static DoorMan doorMan = new DoorMan();
// 2. public으로 오픈하여 객체 인스턴스가 필요하면 이 static 메소드를 통해서만 조회하도록 허용.
public static DoorMan getInstance() {
return doorMan;
}
// 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막음.
private DoorMan() {
}
// 로직 호출
public void 쫓아내(Animal a) {
System.out.println(a.getName() + "쫓아내");
}
}- 고객의 요청이 올 때마다 객체를 생성하는 것이 아닌 만들어진 객체를 공유하는 것이기 때문에
메모리를 효율적으로 사용가능 - 싱글톤으로 만들어진 클래스의 인스턴스는 전역이기 때문에 다른 클래스의 인스턴스들이
데이터를 공유하기 쉬움
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어감
- 의존관계상 클라이언트가 구체 클래스에 의존 -> DIP 위반
- 싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우,
다른 클래스의 인스턴스들과의 결합도가 높아져 OCP를 위반할 수 있음. - 내부 속성을 변경하거나 초기화 하기 어려움.
- 결론적으로 유연성이 떨어짐.
- 특정 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체적인 구조는 바꾸지 않으면서
특정 단계에서 수행하는 내용을 바꾸는 패턴 - 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 대표적인 방법.
(변하지 않는 기능은 슈퍼클래스에, 자주 변경되며 확장할 기능은 서브클래스에)
<예제>
강의를 하는 선생님(Teacher) 클래스가 있다고 가정.
Teacher 클래스에는
Java를 가르쳐주는 JavaTeacher 클래스,
Python을 가르쳐주는 PythonTeacher 클래스가 있음.
public class JavaTeacher{
private void 입장하기() {
System.out.println("입장하기");
}
private void 출석부르기() {
System.out.println("출석부르기");
}
private void 퇴장하기() {
System.out.println("퇴장하기");
}
private void 강의하기() {
System.out.println("Java 강의하기");
}
public void 수업시작() {
입장하기();
출석부르기();
강의하기();
퇴장하기();
}
} public class PythonTeacher{
private void 입장하기() {
System.out.println("입장하기");
}
private void 출석부르기() {
System.out.println("출석부르기");
}
private void 퇴장하기() {
System.out.println("퇴장하기");
}
private void 강의하기() {
System.out.println("Python 강의하기");
}
public void 수업시작() {
입장하기();
출석부르기();
강의하기();
퇴장하기();
}
}- 두 메서드는
강의하기의 내용만 다르고 중복되는 코드들이 존재.
-> 입장하기, 출석부르기, 퇴장하기, 수업시작
중복된 코드들을 없애는 방법이 없을까?
-> 추상 클래스를 사용하자
Teacher 라는 부모 클래스를 통해 공통된 기능들을 템플릿화.
public abstract class Teacher {
private void 입장하기() {
System.out.println("입장하기");
}
private void 출석부르기() {
System.out.println("출석부르기");
}
// 오버라이드(부모의 메서드를 무효화하는 것)
abstract void 강의하기();
private void 퇴장하기() {
System.out.println("퇴장하기");
}
public void 수업시작() {
입장하기();
출석부르기();
강의하기();
퇴장하기();
}
}
JavaTeacher클래스와PythonTeacher클래스는 다음과 같이
Teacher 클래스를 상속받아 전체적인 틀은 고정하면서
강의하기와 같이 본인의 기능마다 다른 부분을 오버라이딩하여 사용할 수도 있음.(훅 메서드)
즉, 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현하게 됨.
JavaTeacher
public class JavaTeacher extends Teacher {
// 재정의
void 강의하기() { // 동적바인딩 (C#에서 동적 결)
System.out.println("Java 강의하기");
}
}PythonTeacher
public class PythonTeacher extends Teacher {
void 강의하기() {
System.out.println("Python 강의하기");
}
}- 주어진 상황 및 용도에 따라 어떤 객체에 기능을 동적으로 추가하는 패턴.
- 기본 기능을 수행하는 클래스를 만들고 기능들을 장식처럼 점점 추가한다하여 decorator
<예제>
알림을 보내는 기능을 구현하고 있다고 가정.
알림은 총 3가지로 기본알림, 문자알림, 이메일 알림이 있음.
❗가장먼저, Notifier 객체를 구현. 각 클래스는 추상화된 객체인 Notifier 상속받음.
Notifier.java
public interface Notifier {
void send();
}다음으로, 기본알림 기능 구현.
BasicNotifier.java
// wrapper가 없는 Decorator
public class BasicNotifier implements Notifier{
// 이 친구는 뭔가를 의존하면 안된다.
@Override
public void send() {
System.out.println("기본 알림");
}
}이메일 알림
EmailNotifier.java
public class EmailNotifier implements Notifier {
private Notifier notifier;
public EmailNotifier(Notifier notifier) {
this.notifier = notifier;
}
public EmailNotifier() {
}
@Override
public void send() {
if (notifier != null)
notifier.send();
System.out.println("이메일 알림");
}
}문자 알림
SmsNotifier.java
public class SmsNotifier implements Notifier {
private Notifier notifier;
public SmsNotifier(Notifier notifier) {
this.notifier = notifier;
}
@Override
public void send() {
notifier.send();
System.out.println("문자 알림");
}
}❓ 만약, 기본 알림에 문자알림, 이메일 알림을 추가하여 한 번에 사용하고 싶다면?
각 기능들을 모두 클래스로 구현? -> No. 알림을 사용할때마다 클래스를 생성하면 비효율적.
이제, decorator 패턴을 통해 다음과 같이 한 번에 사용이 가능.
App.java
/*
* 데코레이터 패턴 (장식)
* (A) -> B(A) -> C(B(A))
*/
public class App {
public static void main(String[] args) {
// 전체 알림 (기본알림 -> 문자알림 -> 이메일 알림)
Notifier allNotifier = new SmsNotifier(new EmailNotifier(new BasicNotifier()));
allNotifier.send();
System.out.println("__end");
}
}
