Design Pattern
1) Singleton
- 인스턴스를 1개만 만들 수 있도록 클래스를 디자인 하는 것
- 관리자 클래스(Controller, Manager)나 애플리케이션 전체가 공유해야 하는 공유 데이터를 갖는 클래스 또는 서버에서 클라이언트의 요청을 처리하는 클래스 등은 일반적으로 인스턴스를 1개만 만들어서 사용
- java.lang.Runtime 클래스가 이 패턴으로 디자인
2) Decorator Pattern
- 외부에서 데이터를 주입받아 인스턴스를 생성하는 구조
- 하는 일은 유사한데 일을 수행해야 하는 대상이 여러가지 종류일 때 사용하는 패턴
- 작업을 수행하는 대상을 클래스 내부에서 직접 생성하지 않고 외부에서 생성한 후 주입받는 형태의 구조
java.io.BufferedReader 클래스가 Decorator Pattern의 대표적인 예
- BufferedReader 는 문자 스트림에서 문자열을 읽어오는 스트림
- new BufferedReader(new InputStreamReader(new FileInputStream(String filepath))): 파일에서 읽어오기
- new BufferedReader(new InputStreamReader(Socket.getInputStream())): 소켓에서 읽어오기
- new BufferedReader(new InputStreamReader(HttpURLConnection.getInputStream())): 웹에서 읽어오기
매개변수가 여러 개가 될 수 있습니다.
- C++ 이나 Java 는 생성자 오버로딩을 이용해서 구현하거나 매개변수로 대입되는 클래스의 다형성을 이용해서 구현
2.구조와 관련된 패턴
1) 템플릿 메소드 패턴
- 메소드의 원형을 인터페이스(추상 클래스)에 선언하고 인터페이스를 구현한 클래스에 메소드를 구현하는 방식
c나 c++(추상 클래스)는 인터페이스 대신에 헤더파일을 이용해서 구현
- 최근에는 이런 방식은 잘 사용되지 않음
최근에 등장하는 GUI를 구현하는 언어들 중에도 파일을 2개로 나누어서 구현하는 경우가 있는데 이 경우는 디자인 파일과 구현 파일을 분리하는 형태이지 헤더파일을 이용하는 형태가 아닙니다.
프로그래밍을 할 때 사용자의 요청을 처리하는 클래스는 이 패턴으로 디자인 합니다.
- 이렇게 사용자의 요청을 처리하는 메소드를 소유한 클래스를 Service 클래스라고 합니다.
- 데이터베이스 연동 프로그램에서 트랜잭션을 적용하는 지점은 Service 클래스의 메소드입니다.
어떤 Unit 이라는 클래스를 설계하는데 사용자가 공격과 이동이라는 동작을 수행해야 합니다.
- 인터페이스 생성
public interface Unit {
//공격을 수행하는 메소드
public void attack();
//이동을 수행하는 메소드
public void move();
}
- 인터페이스를 구현한 클래스를 생성
public class UnitImpl implements Unit {
@Override
public void attack() {
System.out.println("공격");
}
@Override
public void move() {
System.out.println("이동");
}
}
- main 메소드를 만들어서 사용
public class UnitMain {
public static void main(String[] args) {
//템플릿 메소드 패턴이 적용된 클래스의 인스턴스 만들기
//변수는 인터페이스 이름을 사용하고 생성자는 클래스 이름을 이용하는 형태로 많이 작성합니다.
Unit unit = new UnitImpl();
unit.attack();
unit.move();
//List<String> list = new ArrayList<String>();
}
}
2) Adapter Pattern
- 구현된 인터페이스를 이용해서 상속받은 클래스의 메소드를 호출하는 패턴
- 상속받은 클래스의 메소드를 직접 호출하기가 어려운 경우 이용
- 상속을 받았기 때문에 상위 클래스의 메소드를 호출해서 사용하는 것이 가능한데 상위 클래스의 메소드 이름을 다른 용도로 사용하는 경우 - 상위 클래스의 메소드의 내용을 변경해서 오버라이딩 하는 경우
원래 오버라이딩은 상위 클래스의 메소드 기능을 확장하기 위해서 사용하는데 확장이 아니라 변경한 경우
Base 클래스 생성
public class OldSystem {
public void process() {
System.out.println("예전 처리");
}
}
- Target 인터페이스 생성
public interface Target {
//OldSystem 의 process 를 호출하기 위한 메소드
public void legacyProcess();
}
- Derived 클래스 생성
public class NewSystem extends OldSystem implements Target{
@Override
public void process() {
//구현된 메소드의 오버라이딩: 상위 클래스의 메소드를 호출하고 기능을 추가
/*
super.process();
System.out.println("기능 추가");
*/
System.out.println("새로운 기능");
}
@Override
public void legacyProcess() {
super.process();
}
}
- main 메소드를 만들어서 실행
public class AdaterMain {
public static void main(String[] args) {
NewSystem newSystem = new NewSystem();
//새로 만들어진 메소드
newSystem.process();
//이전에 만들어진 메소드
newSystem.legacyProcess();
}
}
3) Composite Pattern
- 재귀적 구조를 쉽게 처리하기 위한 패턴
- Recursive Call(Recursion - 함수가 내부에서 함수 자신을 다신 호출하는 경우)
- 파일 시스템에는 파일과 디렉토리가 있는데 디렉토리를 삭제할려고 하는 경우 디렉토리 안의 내용을 확인해서 디렉토리이면 다시 그 안에 있는 내용들을 삭제해야 합니다.
하나의 인터페이스를 만들고 인터페이스에 공통된 메소드 이름을 만들어주고 파일과 디렉토리처리를 위한 클래스를 별도로 만들어서 처리하는 메소드를 구현합니다.
- 다형성을 구현하는 방식과 유사합니다.
인터페이스는 Entry - add 와 remove 그리고 rename
클래스는 File 과 Directory 로 생성
인터페이스 생성
//파일과 디렉토리 클래스에서 공통으로 사용할 메소드를 소유한 인터페이스
public interface Entry {
public void add(Entry entry);
void remove();
void rename(String name);
}
- File 클래스 생성
public class File implements Entry {
//파일 이름을 저장할 변수
private String name;
public File(String name) {
this.name = name;
}
@Override
public void add(Entry entry) {
System.err.println("파일에는 파일이나 디렉토리를 추가할 수 없습니다.");
}
@Override
public void remove() {
System.out.println(name + "가 삭제되었습니다.");
}
@Override
public void rename(String name) {
this.name = name;
}
}
- Directory 클래스
public class Directory implements Entry {
private String name;
//File 이나 Directory를 소유할 수 있기 때문에 Entry를 여러 개 저장할 수 있는 자료구조를 소유
List <Entry> list;
public Directory(String name) {
this.name = name;
list = new ArrayList<Entry>();
}
@Override
public void add(Entry entry) {
list.add(entry);
}
@Override
public void remove() {
//Iterator를 이용해서 데이터 접근
Iterator <Entry> iter = list.iterator();
while(iter.hasNext()) {
Entry entry = iter.next();
//디렉토리인지 확인해서 작업을 수행을 하지 않을 수 도 있습니다.
//File이 있으면 File의 remove를 호출하고 Directory이면 Directory의 remove를 호출 - 다형성(Polymorphism)
entry.remove();
}
System.out.println("내부 데이터는 전부 삭제되었습니다.");
}
@Override
public void rename(String name) {
this.name = name;
}
}
- main 메소드를 만들어서 테스트
public class CompositeMain {
public static void main(String[] args) {
File f1 = new File("파일1");
File f2 = new File("파일2");
File f3 = new File("파일3");
Directory subDirectory = new Directory("하위 디렉토리");
subDirectory.add(f1);
subDirectory.add(f2);
Directory superDirectory = new Directory("상위 디렉토리");
superDirectory.add(subDirectory);
superDirectory.add(f3);
superDirectory.remove();
}
}
3.행동에 관련된 패턴
1) Command Pattern
처리 내용이 비슷한 명령을 모아서 실행하는 처리가 필요할 때 명령을 인스턴스로 취급해서 처리하는 패턴
데이터를 삽입하는 처리와 수정하는 처리가 필요한 경우
interface Action{
public void execute(DTO dto);
}
class InsertAction implement Action{
public void execute(DTO dto){
삽입하는 코드
}
}
class UpdateAction implement Action{
public void execute(DTO dto){
수정하는 코드
}
}
//action에 대입되는 인스턴스 자체가 명령어와 유사한 역할을 수행
Action action = null;
if(command == 삽입){
action = new InsertAction();
}else if(command == 삭제){
action = new UpdateAction();
}
action.execute(dto);
- 이러한 패턴은 웹 서버의 Controller 클래스를 만들어서 요청에 따라 처리를 할 때 많이 사용
- 웹 서버 프로그래밍을 하다보면 URL에 따른 Routing 구조를 만들 때도 이 구조를 이용합니다.
2) Observer Pattern
- 어떤 인스턴스의 내부 상태가 자주 변경되는 경우 내부 상태가 변경되는지를 감시하고 있다가 변경이 되면 알려줘서 다른 처리를 할 수 있도록 해주는 패턴
- 알려준다고 해서 Notification 이라는 표현을 많이 사용합니다.
- 이 패턴을 사용하는 대표적인 예가 스마트 폰의 뷰에서 키보드가 화면에서 보여지고 사라지는 것을 감시해서 뷰의 컴포넌트들을 재배치하는 형태
3) Strategy Pattern
- 어떤 클래스가 공통된 부분이 있고 서로 다른 부분이 있는 경우 공통된 부분은 클래스 안에서 만들어 사용하고 서로 다른 부분은 외부에서 주입(Injection)받아 사용하는 패턴
public class Injection {
private String common; //모든 인스턴스들이 "Java"라고 저장해서 사용
private String diff1; //인스턴스들 마다 다름
private String diff2; //인스턴스들 마다 다름
public Injection(String diff1) {
common = "Java";
//생성자를 이용한 주입
this.diff1 = diff1;
}
//diff2 에 대한 setter 메소드 - setter 나 getter를 소유한 인스턴스 변수를 property라고 합니다.
public void setDiff2(String diff2) {
this.diff2 = diff2;
}
//common 과 diff1 은 null 일 가능성이 없지만
//diff2는 setter를 이용해서 대입받기 때문에 null일 가능성이 존재
//서버 프로그래밍을 할 때는 메모리 부담이 되더라도 처음부터 만들어두고 사용하는 것이 좋고
//클라이언트 프로그래밍을 할 때는 속도가 느리더라도 필요할 때 생성하는 것이 좋습니다.
public void disp() {
System.out.println(common.toUpperCase());
System.out.println(diff1.toUpperCase());
System.out.println(diff2.toUpperCase());
}
}
public class InjectMain {
public static void main(String[] args) {
Injection injection = new Injection("JavaScript");
//다른 메소드를 호출
injection.setDiff2("Spring");
injection.disp();
//setDiff2를 호출하지 않았기 때문에 diff2가 null 인 상태에서 toUpperCase를 호출해서 예외
injection = new Injection("FrontEnd");
injection.disp();
}
}
- Design Pattern은 하나의 클래스에 여러가지를 적용하기도 합니다.
- Design Pattern의 개념은 절대적인 개념이 아니라서 개발자마다 약간씩 다르게 설명하기도 합니다. Singleton, Template Method, Command Pattern 은 모두 동일하게 설명합니다.
- 이외에도 인스턴스 생성을 대신해주는 Factory Method Pattern 이나 개발자가 만든 클래스에 코드를 더해서 실제로는 개발자가 만든 클래스와 다른 형태의 인스턴스를 만들어내는 Proxy Pattern 등도 있습니다.
객체지향 프로그래밍에서는 디자인 패턴이 중요합니다.
객체지향 프로그래밍 언어 문법 -> 자료구조&알고리즘 -> 디자인패턴 -> SW 공학(개발 방법론 ...)
외부로부터 데이터를 받아서 사용하는 경우
- 주입을 받는 방법은 생성자를 이용하는 방법이 있고 프로퍼티(setter)를 이용하는 방법
- 생성자를 이용하는 방법은 처음부터 가지고 있기 때문에 NullPointerException이 발생할 가능성이 적지만 메모리에 부담이 되고 setter를 이용하는 방식은 사용하기 전에 주입받아서 사용하기 때문에 메모리를 절약할 수 있지만 NullPointerException에 대비해야 합니다.
데이터 저장 및 읽기
1.로컬이나 원격에 flat file 형태로 저장
- text 형식: txt, csv 형식 - 용량이 작다는 장점이 있지만 전체가 아닌 일부 검색은 어려움
- xml, json 형식: 규격화 된 포맷 형식 - 인간이 알아보기 쉬움, 인간이 직접 작성도 가능, 사이즈가 커지면 용량도 커지고 알아보기도 어려워집니다.
- 설정 내용을 저장할 때 나 실시간으로 작은 용량의 데이터를 주고받을 때 이용
2.별도의 저장 프로그램 이용
- Database: 앞의 방법 들보다 보안이 우수하고 검색 조건을 다양하게 설정할 수 있음, 데이터 이외의 많은 것들을 저장해야 하기 때문에 오버헤드가 큼