디자인 패턴(Design Pattern)
소프트웨어 설계에서 자주 발생하는 문제들을 해결하기 위해 반복적으로 사용되는, 검증된 설계 방법을 정형화한 것. 다시 말해, 소프트웨어 개발 과정에서 직면하는 공통적인 문제를 해결하기 위한 최선의 사례(Best Practices)를 문서화한 것
어댑터 패턴(Adaptoer Pattern)
"호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴"
어댑터 패턴은 소프트웨어 디자인 패턴 중 하나로, 서로 다른 인터페이스를 가진 두 개체가 함께 작동할 수 있도록 중간에 "어댑터" 역할을 하는 클래스를 두어 호환성을 제공하는 패턴이다.
어댑터 패턴이 적용되지 않은 코드
package adapterPattern;
public class ServiceA {
void runServiceA() {
System.out.println("ServiceA");
}
}
package adapterPattern;
public class ServiceB {
void runServiceB() {
System.out.println("ServiceB");
}
}
package adapterPattern;
public class ClientWithNoAdapter {
public static void main(String[] args) {
ServiceA sa1 = new ServiceA();
ServiceB sb1 = new ServiceB();
sa1.runServiceA();
sb1.runServiceB();
}
}
위 코드에서는 ClientWithNoAdapter가 ServiceA와 ServiceB를 직접 사용한다. 하지만, 만약 클라이언트가 다른 인터페이스(runService())를 요구한다면, 기존 서비스 클래스들을 수정해야 한다. 이는 유지보수성과 확장성을 저해할 수 있다.
어댑터 패턴이 적용된 코드
package adapterPattern;
public class AdapterServiceA {
ServiceA sa1 = new ServiceA();
void runService() {
sa1.runServiceA();
}
}
package adapterPattern;
public class AdapterServiceB {
ServiceB sb1 = new ServiceB();
void runService() {
sb1.runServiceB();
}
}
package adapterPattern;
public class ClientWithAdapter {
public static void main(String[] args) {
AdapterServiceA asa1 = new AdapterServiceA();
AdapterServiceB asb1 = new AdapterServiceB();
asa1.runService();
asb1.runService();
}
}
이 코드에서는 ClientWithAdapter가 AdapterServiceA와 AdapterServiceB를 사용한다. 어댑터 클래스인 AdapterServiceA와 AdapterServiceB는 각각 ServiceA와 ServiceB를 내부적으로 사용하면서, 클라이언트가 요구하는 인터페이스(runService())를 구현한다.
어댑터 패턴을 사용함으로써, 클라이언트 코드는 특정 서비스 클래스에 직접 의존하지 않게 된다. 따라서 기존 서비스 클래스를 변경하지 않고도 클라이언트가 요구하는 인터페이스에 맞추어 코드를 작성할 수 있다. 이로 인해 코드의 유연성과 확장성이 크게 향상되게된다.
프록시 패턴(Proxy Pattern)
"제어 흐름을 조정하기 위한 목적으로 중간에 대리자를 두는 패턴"
프록시는 대리자, 대변인이라는 뜻을 가진 단어이다.
- 대리자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다.
- 대리자는 실제 서비스에 대한 참조 변수를 갖는다.(합성)
- 대리자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 돌려준다.
- 대리자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.
IService.java - 인터페이스 정의
package proxyPattern;
public interface IService {
String runSomething();
}
IService는 프록시와 실제 서비스가 구현해야 하는 공통 인터페이스. 이 인터페이스는 클라이언트가 기대하는 메서드를 정의한다.
Service.java - 실제 서비스 클래스
package proxyPattern;
public class Service implements IService {
public String runSomething() {
return "서비스 짱!!!";
}
}
Service 클래스는 IService 인터페이스를 구현하는 실제 서비스. 이 클래스는 클라이언트가 최종적으로 수행하고자 하는 작업을 처리한다.
Proxy.java - 프록시 클래스
package proxyPattern;
public class Proxy implements IService {
IService service1;
public String runSomething() {
System.out.println("호출에 대한 흐름 제어가 주목적, 반환 결과를 그대로 전달");
// 실제 서비스 객체 생성 및 호출
service1 = new Service();
return service1.runSomething();
}
}
Proxy 클래스는 IService 인터페이스를 구현하며, 클라이언트의 요청을 받아 실제 서비스(Service) 객체에 전달하는 역할을 한다. 프록시는 요청을 처리하기 전에 별도의 로직(여기서는 간단히 출력)을 수행할 수 있다.
ClientWithProxy.java - 클라이언트 코드
package proxyPattern;
public class ClientWithProxy {
public static void main(String[] args) {
// 프록시를 이용한 호출
IService proxy = new Proxy();
System.out.println(proxy.runSomething());
}
}
ClientWithProxy 클래스는 클라이언트 코드로, 프록시 객체를 통해 실제 서비스를 호출한다. 클라이언트는 프록시를 사용함으로써 실제 서비스에 직접 접근하는 대신, 프록시를 통해 간접적으로 접근합니다.
프록시 패턴은 실제 객체에 대한 접근을 제어하는 데 매우 유용한 패턴이다. 클라이언트는 실제 서비스에 직접 접근하는 대신 프록시를 통해 간접적으로 접근하며, 이를 통해 다양한 추가 기능(로깅, 캐싱, 접근 제어 등)을 프록시에서 처리할 수 있다.
데코레이터 패턴(Decorator Pattern)
"메서드 호출의 반환값에 변화를 주기 위해 중간에 장식자를 두는 패턴"
데코레이터 패턴은 클라이언트가 받는 반환값에 장식을 더한다는 점만 빼면 프록시 패턴과 동일하다.
- 메서드 호출의 반환값에 변화를 주기 위해 중간에 장식자를 두는 패턴
- 장식자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다.
- 장식자는 실제 서비스에 대한 참조 변수를 갖는다(합성).
- 장식자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고, 그 반환값에 장식을 더해 클라이언트에게 돌려준다.
- 장식자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있다.
IService.java - 인터페이스 정의
package decoratorPattern;
public interface IService {
String runSomething();
}
IService는 데코레이터와 실제 서비스가 구현해야 하는 공통 인터페이스. 이 인터페이스는 클라이언트가 기대하는 메서드를 정의한다.
Service.java - 실제 서비스 클래스
package decoratorPattern;
public class Service implements IService {
public String runSomething() {
return "서비스 짱!!!";
}
}
Service 클래스는 IService 인터페이스를 구현하는 실제 서비스. 이 클래스는 기본적인 기능을 제공하며, 클라이언트가 원래 호출하려는 객체이다.
Decorator.java - 데코레이터 클래스
package decoratorPattern;
public class Decorator implements IService {
IService service;
public String runSomething() {
System.out.println("호출에 대한 장식 주목적, 클라이언트에게 반환 결과에 장식을 더하여 전달");
service = new Service();
return "정말 " + service.runSomething();
}
}
Decorator 클래스는 IService 인터페이스를 구현하며, 실제 서비스 객체(Service)를 감싸는 역할을 한다. 이 클래스는 클라이언트의 요청을 받아 실제 서비스의 메서드를 호출하고, 그 반환값에 추가적인 장식을 더해 클라이언트에게 반환한다. 여기서는 "정말 "이라는 문자열을 반환값 앞에 덧붙여 장식하고 있다.
ClientWithDecorator.java - 클라이언트 코드
package decoratorPattern;
public class ClientWithDecorator {
public static void main(String[] args) {
// 데코레이터를 이용한 호출
IService decorator = new Decorator();
System.out.println(decorator.runSomething());
}
}
ClientWithDecorator 클래스는 클라이언트 코드로, 데코레이터 객체를 통해 실제 서비스에 접근한다. 이 코드는 데코레이터를 사용함으로써, 기본 서비스에 추가적인 기능(여기서는 문자열 장식)을 적용한 결과를 얻을 수 있다.
프록시 패턴과의 비교
데코레이터 패턴은 프록시 패턴과 구조가 매우 유사하다. 두 패턴 모두 실제 객체와 동일한 인터페이스를 구현하는 대리 객체를 사용하지만, 프록시 패턴은 주로 접근 제어 및 흐름 제어를 위해 사용되는 반면, 데코레이터 패턴은 반환값에 변화를 주거나 추가 기능을 동적으로 부여하기 위해 사용된다.
싱글턴 패턴(Singleton Pattern)
"클래스의 인스턴스, 즉 객체를 하나만 만들어 사용하는 패턴"
싱글턴 패턴(Singleton Pattern)은 객체지향 설계에서 하나의 클래스가 오직 하나의 인스턴스만을 생성하고, 이를 전역적으로 접근할 수 있도록 하는 패턴. 싱글턴 패턴을 사용하면 프로그램 전역에서 단일 객체를 공유할 수 있어 메모리 낭비를 줄이고, 객체 간 상태를 공유할 수 있다.
- Private 생성자: 싱글턴 패턴은 외부에서 객체를 직접 생성할 수 없도록 생성자를 private으로 선언한다. 이를 통해 클래스 외부에서 new 키워드를 사용하여 객체를 생성하는 것을 막을 수 있다.
- 정적 참조 변수: 싱글턴 클래스는 자기 자신의 인스턴스를 가리키는 정적 참조 변수를 하나 갖는다. 이 변수는 클래스 로딩 시 초기화되지 않으며, null로 초기화된 상태로 있다가 getInstance() 메서드에서 초기화된다.
- 정적 메서드: 싱글턴 패턴에서는 getInstance()라는 정적 메서드를 통해 인스턴스를 제공한다. 이 메서드는 객체가 생성되지 않았을 경우에만 새로운 객체를 생성하고, 이미 생성된 경우에는 기존 객체를 반환하게된다.
- 단일 객체의 속성: 단일 객체는 공유 객체이므로, 보통 쓰기 가능한 속성을 가지지 않는 것이 권장된다. 만약 속성을 갖는다면, 하나의 참조 변수가 해당 속성을 변경했을 때, 다른 참조 변수에도 영향을 미치기 때문. 하지만 읽기 전용 속성이나 다른 단일 객체에 대한 참조를 갖는 것은 문제가 되지 않음.
Singleton.java - 싱글턴 클래스
package singletonPattern;
public class Singleton {
static Singleton singletonObject; // 정적 참조 변수
private Singleton() {
// private 생성자
}
// 객체 반환 정적 메서드
public static Singleton getInstance() {
if (singletonObject == null) {
singletonObject = new Singleton(); // 객체가 없는 경우에만 생성
}
return singletonObject;
}
}
이 Singleton 클래스는 오직 하나의 인스턴스만 생성될 수 있도록 설계되었다. private 생성자를 통해 외부에서의 직접적인 객체 생성을 막고, getInstance() 메서드를 통해 클래스의 유일한 인스턴스를 반환한다.
Client.java - 싱글턴 사용 예제
package singletonPattern;
public class Client {
public static void main(String[] args) {
// Singleton s = new Singleton(); // 불가능: 생성자가 private이므로
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
s1 = null;
s2 = null;
s3 = null;
}
}
//위 코드를 실행했을때 나오는 결과
SingletonPattern.Singleton@263c8db9
SingletonPattern.Singleton@263c8db9
SingletonPattern.Singleton@263c8db9
이 Client 클래스에서는 Singleton.getInstance() 메서드를 통해 여러 개의 참조 변수를 사용해 동일한 Singleton 객체를 참조한다. 결과적으로 s1, s2, s3는 모두 동일한 인스턴스를 참조하게 된다. 이 예제에서는 new 키워드를 통해 객체를 생성하려고 하면 컴파일 에러가 발생하게 된다.
이 글은 스프링 입문을 위한 자바 객체 지향의 원리와 이해를 참고하여 작성하였습니다.
스프링 입문을 위한 자바 객체 지향의 원리와 이해 | 김종민 - 교보문고
스프링 입문을 위한 자바 객체 지향의 원리와 이해 | 이 책은 자바에서 스프링으로 나아가기 위한 연결 고리를 제공한다.
product.kyobobook.co.kr
'종합' 카테고리의 다른 글
[OOP] 객체지향 설계 5원칙 SOLID에 대하여 (0) | 2024.08.07 |
---|