✔️ 개발자(설계자) 간의 원할한 소통
✔️ 소프트웨어 구조 파악에 용이
✔️ 재사용이 가능하여 개발 시간 단축 가능
✔️ 설계 변경 요청에 대한 유연한 대처 가능
✔️ 객체지향적 설계와 구현을 해야하므로 객체지향에 대해 잘 알고 있어야 함
✔️ 초기 설계 및 구현하는데 투자 비용이 많이 들어갈 수 있음
✔️ 객체를 생성하는 것과 관련된 패턴으로, 객체의 생성과 변경이 전체 시스템에 미치는 영향을 최소화하고 코드의 유연성을 높여줌
✔️ 프로그램 내의 자료구조나 인터페이스 구조 등 프로그램 구조를 설계하는데 활용될 수 있는 패턴
✔️ 클래스, 객체들의 구성을 통해서 더 큰 구조를 만들 수 있게 해줌
✔️ 큰 규모의 시스템에서는 많은 클래스들이 서로 의존성을 가지게 되는데, 이런 복잡한 구조를 개발하기 쉽게 만들어주고, 유지보수 하기 쉽게 만들어 줌
✔️ 반복적으로 사용되는 객체들의 상호작용을 패턴화한 것
✔️ 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 제공
✔️ 행위 관련 패턴을 사용하여 독립적으로 일을 처리하고자 할 때 사용
✔️ 어떤 클래스(객체)가 유일하게 1개만 존재할 때 사용
✔️ 서로 자원을 공유할 때 주로 사용 되며, 실물 세계에서의 대표적인 예는 프린터가 있음
✔️ 실제 프로그래밍에서는 TCP Socket 통신에서 서버와 연결된 connect 객체에 주로 사용됨
✔️ 스프링에서는 Spring Bean이 싱글톤으로 관리됨
✔️ 기본적으로 default 생성자는 Private이며, get method를 통해 생성된 인스턴스를 가져다 씀
✔️ 예제 코드
public class SocketClient {
private static SocketClient socketClient = null;
//기본 생성자를 private로 막음
private SocketClient() {}
//static 메소드를 통해서 인스턴스를 가져오도록 함
public static SocketClient getInstance() {
//객체가 null인 경우 새로 생성
if(socketClient == null) {
socketClient = new SocketClient();
}
return socketClient;
}
public void connect() {
System.out.println("connect");
}
}
장점
✔️ 고정된 메모리 영역을 얻으면서 하나의 객체만 사용하므로, 메모리 낭비를 방지
✔️ 싱글톤 객체의 인스턴스는 전역(Static)이므로 다른 인스턴스와 데이터를 공유할 수 있음
✔️ 처음 하나의 객체 생성 이후에는 객체를 그대로 가져다 사용하므로 성능이 좋아짐
단점
✔️ 싱글톤 패턴을 구현하는 코드 작성이 많아짐
✔️ 의존관계상 클라이언트가 구체 클래스에 의존함 -> DIP 위반
✔️ 클라이언트가 구체 클래스에 의존함으로 OCP 위반할 가능성이 높음
✔️ 테스트 하기 어려움 - 싱글톤은 생성 방식이 제한적이기 때문에 Mock 객체로 대체하기가 어려우며, 동적으로 객체를 주입하기도 힘듦
✔️ 내부 속성을 변경하거나 초기화 하기 어려움
✔️ Private 생성자 이므로 자식 클래스를 만들기 어려움
✔️ 유연성이 떨어짐
💡 스프링에서는 스프링 컨테이너에서 객체 인스턴스를 싱글톤으로 관리하는데 위의 기존 자바에서의 싱글톤 문제점들을 해결하여 사용한다. -> 스프링 컨테이너(싱글톤 컨테이너)는 따로 글 작성(링크)
✔️ 호환성이 없는 기존 클래스의 인터페이스를 변환하여 재사용 할 수 있도록 함
✔️ 실생활의 예시로는, 110v를 220v로 변환해주거나 또는 그 반대로 변환해주는 돼지코로 불리는 변환기가 있음
✔️ SOLID 원칙에서 개방폐쇄 원칙 (OCP)를 따름
✔️ 어댑터 패턴의 구성 요소
장점
✔️ 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 사용할 수 있음 - 개방 폐쇄 원칙
✔️ 기존코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있음 - 단일 책임 원칙에 가까움
단점
✔️ 다수의 새로운 인터페이스와 클래스를 생성해야 하므로 추가 코드가 많아지고 복잡해질 수 있음
✔️ 코드의 성능이 개선되지는 않는다. 오히려 어댑터를 통해야 하므로 속도가 저하
✔️ 예제 코드
//Electronic110v - Target
//Electronic220v - Adaptee
//SocketAdapter - Adapter
public class SocketAdapter implements Electronic110v{
private Electronic220v electronic220v;
public SocketAdapter(Electronic220v electronic220v) {
this.electronic220v = electronic220v;
}
@Override
public void powerOn() {
electronic220v.connect();
}
}
//cleaner - Client
Cleaner cleaner = new Cleaner();
Electronic110v adapter = new SocketAdapter(cleaner);
✔️ Proxy란, 대리인 이라는 뜻으로 뭔가를 대신해서 처리하는 것
✔️ Proxy Class를 통해서 대신 전달하는 형태로 설계되며, 실제 Client는 Proxy로부터 결과를 받는 형식
✔️ Cache의 기능으로도 활용 가능함
✔️ SOLID 원칙 중에서 개방폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)를 따름
✔️ 스프링에서는 AOP에서 프록시 패턴을 사용
✔️ 예제 코드
public class BrowserProxy implements IBrowser{
private String url;
private Html html;
public BrowserProxy(String url) {
this.url = url;
}
/**
* 처음 객체가 없으면 생성하고, 이후부터는 기존 객체를 가져와 Cache처럼 사용
* @return Html {@link Html}
*/
@Override
public Html show() {
if(html == null) {
html = new Html(url);
System.out.println("BrowserProxy loading from : " + url);
}
System.out.println("BrowserProxy use cache html : " + url);
return html;
}
}
장점
✔️ 기존 객체를 수정하지 않고 일련의 로직을 프록시 패턴을 통해 추가 가능
✔️ 실제 객체를 수행하기 이전에 전처리를 하거나, 기존 객체를 캐싱할 수 있음
✔️ 클라이언트는 객체를 신경쓰지 않고, 서비스 객체를 제어하거나 생명 주기를 관리
단점
✔️ 많은 프록시 클래스를 도입해야 하므로 코드의 복잡도가 증가
✔️ 프록시 클래스 자체에 들어가는 자원이 많다면 서비스로부터의 응답이 늦어질 수 있음
✔️ 객체 생성 시 프록시 클래스를 거치므로 객체를 빈번하게 생성 해야하는 경우 성능이 저하될 수 있음
✔️ 객체의 결합을 통해 기능을 동적으로 유연하게 확장 할 수 있게 해주는 패턴
✔️ 기존 뼈대 클래스는 유지하되, 이후 필요한 형태로 꾸밀 때 사용
✔️ 확장이 필요한 경우 상속 대신의 대안으로도 활용함
✔️ SOLID 원칙 중에서 개방폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)를 따름
✔️ 실생활의 예시로는, 커피를 들 수 있다. 커피 원액(에소프레소)이 있고 여기서 물을 타면 아메리카노, 우유를 타면 카페라떼가 되는 것처럼 기본에서 다른 것들을 추가하여 확장하는 형식
✔️ 예제 코드
//Decorator 정의
public class AudiDecorator implements ICar{
protected ICar audi;
protected String modelName;
protected int modelPrice;
public AudiDecorator(ICar audi, String modelName, int modelPrice) {
this.audi = audi;
this.modelName = modelName;
this.modelPrice = modelPrice;
}
@Override
public int getPrice() {
return audi.getPrice() + modelPrice;
}
@Override
public void showPrice() {
System.out.println(modelName + "의 가격은 " + getPrice() + "원 입니다.");
}
}
//AudiDecorator를 확장하여 사용
public class A3 extends AudiDecorator{
public A3(ICar audi, String modelName) {
super(audi, modelName, 1000);
}
}
장점
✔️ 데코레이터를 사용하면 서브클래스를 만들때보다 훨씬 더 유연하게 기능을 확장할 수 있다.
✔️ 객체를 여러 데코레이터로 래핑하여 여러 동작을 결합할 수 있다.
✔️ 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
✔️ 각 장식자 클래스마다 고유의 책임을 가져 단일 책임 원칙(SRP)을 준수
✔️ 클라이언트 코드 수정없이 기능 확장이 필요하면 장식자 클래스를 추가하면 되니 개방 폐쇄 원칙(OCP) 준수
✔️구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙(DIP) 준수
단점
✔️ 데코레이터 패턴을 사용하면 자잘한 객체들이 많이 추가될 수 있고, 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해 질 수 있음 - 가동성이 떨어짐
✔️ 변화가 일어났을 때, 미리 등록된 다른 클래스에 통보해주는 패턴
✔️ 보통 event listner가 옵저버 패턴을 사용함
✔️ 예제 코드
//Listener
public interface IButtonListener {
void clickEvent(String event);
}
//Listener 등록
public class Button {
private String name;
private IButtonListener buttonListener;
public Button(String name) {
this.name = name;
}
public void click(String message) {
buttonListener.clickEvent(message);
}
public void addListener(IButtonListener iButtonListener) {
this.buttonListener = iButtonListener;
}
}
//이벤트 발생 시 이벤트 전달
Button button = new Button("버튼");
button.addListener(new IButtonListener() {
@Override
public void clickEvent(String event) {
System.out.println(event);
}
});
button.click("메시지 전달 : click 1");
button.click("메시지 전달 : click 2");
button.click("메시지 전달 : click 3");
button.click("메시지 전달 : click 4");
✔️ Facade는 건물의 정면이라는 뜻으로, 여러 개의 객체와 실제 사용하는 서브 객체 사이에 복잡한 의존관계가 있을 때, 중간에 Facade라는 객체를 두고, 여기서 제공하는 인터페이스를 활용하여 기능을 처리하는 방식
✔️ Facade는 자신이 가지고 있는 각 클래스의 기능을 명확하게 알아야 함
✔️ 클래스 라이브러리 같은 어떤 소프트웨어의 다른 커다란 코드 부분에 대한 간략화된 인터페이스를 제공
✔️ 예제 코드
//필요한 정보들을 받아서 퍼사드 객체에서 관련 메소드들을 일괄적으로 수행해준다.
public class SftpClient {
private Ftp ftp;
private Reader reader;
private Writer writer;
public SftpClient(Ftp ftp, Reader reader, Writer writer) {
this.ftp = ftp;
this.reader = reader;
this.writer = writer;
}
public SftpClient(String host, int port, String path, String fileName) {
this.ftp = new Ftp(host, port, path);
this.reader = new Reader(fileName);
this.writer = new Writer(fileName);
}
public void connect() {
ftp.connect();
ftp.moveDirectory();
writer.fileConnect();
reader.fileConnect();
}
public void disconnect() {
writer.fileDisconnect();
reader.fileDisconnect();
ftp.disConnect();
}
public void read() {
reader.fileRead();
}
public void write() {
writer.write();
}
}
장점
✔️ 서브 객체 간의 의존 관계가 많을 경우 이를 감소시키고 의존성을 한 곳으로 모을 수 있음
✔️ 복잡한 코드를 감춤으로써, 클라이언트가 시스템의 코드를 모르더라도 Facade 클래스만 이해하고 사용 가능
✔️ 클라이언트 입장에서 서브 객체를 사용해야 할때 다루어야 할 객체의 수를 줄여줌
단점
✔️ 퍼사드 클래스 자체가 서브 객체에 대한 의존성을 가지게 되어 의존성을 완전히 제거할 수 없음
✔️ 서브 객체가 많아질수록 퍼사드 클래스의 복잡도가 증가함
✔️ 퍼사드 클래스가 모든 클래스에 결합된 God 객체가 될 수 있음
✔️ 객체지향의 꽃으로 불리며, 유사한 행위들을 캡슐화하여, 객체의 행위를 바꾸고 싶은 경우 직접 변경하는 것이 아닌 전략만 변경하여 유연하게 확장할 수 있는 패턴
✔️ SOLID 원칙 중에서 개방폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)를 따름
✔️ 예제 코드
//인코딩 전략 객체들을 위한 인터페이스
public interface EncodingStrategy {
String encode(String text);
}
//Normal 인코더 - 텍스트 그대로 출력
public class NormalStrategy implements EncodingStrategy{
@Override
public String encode(String text) {
return text;
}
}
//Base64 인코더 - Base64로 텍스트 인코딩
public class Base64Strategy implements EncodingStrategy{
@Override
public String encode(String text) {
return Base64.getEncoder().encodeToString(text.getBytes());
}
}
//인코딩 객체를 사용하는 컨텍스트
public class Encoder {
private EncodingStrategy encodingStrategy;
public String getMessage(String message) {
return this.encodingStrategy.encode(message);
}
public void setEncodingStrategy(EncodingStrategy encodingStrategy) {
this.encodingStrategy = encodingStrategy;
}
}
//인코딩 객체를 생성해서 컨텍스트에 주입하는 클라이언트
Encoder encoder = new Encoder();
//base64
EncodingStrategy base64 = new Base64Strategy();
//normal
EncodingStrategy normal = new NormalStrategy();
String message = "hello java";
encoder.setEncodingStrategy(base64);
String base64Result = encoder.getMessage(message);
System.out.println(base64Result);
encoder.setEncodingStrategy(normal);
String normalResult = encoder.getMessage(message);
System.out.println(normalResult);
참고 Reference