☁️ Proxy
프록시는 대리자, 대리인이라는 뜻을 가진 단어이다. 대리자는 누군가를 대신해서 역할을 수행하는 존재이다.
제어 흐름을 조정하기 위한 목적으로 중간에 대리자를 두는 패턴
객체를 바로 사용하지 않고 대리자를 세움으로써 객체의 로직을 조정할 수 있는 패턴이다. 로깅, 캐싱, 보안 등에서 사용된다. 코드로 바로 살펴보자.
아래와 같이 Drivable 인터페이스를 구현하는 독립적인 Car, Bike 클래스가 있다.
interface Drivable {
void drive();
}
class Car implements Drivable {
@Override
public void drive() {
System.out.println("부릉부릉");
}
}
class Bike implements Drivable {
@Override
public void drive() {
System.out.println("따릉따릉");
}
}
public class Client {
public static void main(String[] args) {
Client client = new Client();
Drivable Bike = new Bike();
client.drive(proxy);
}
private void drive(Drivable driveable) {
driveable.drive(); // 따릉따릉
}
}
두 객체의 흐름은 “부릉부릉” 혹은 “따릉따릉”을 호출하는 것이다. 그런데 이 흐름 전에 로깅하는 작업을 추가하고 싶어졌다. 어떻게 해야할까?
Client 클래스의 drive 메서드에서 로깅 작업을 추가해야할까? 그럼 Client 클래스 외의 Drivable과 의존하는 모든 클래스한테도 로깅 코드를 추가해야 할 것이다. 이런 방법은 좋지 않다.
이렇게 객체의 정해진 흐름에 특정 작업을 추가하고 싶을 때, proxy 패턴을 사용한다. 아래의 코드를 보자.
class Proxy implements Drivable {
private final Drivable drivable;
public Proxy(Drivable drivable) {
this.drivable = drivable;
}
@Override
public void drive() {
System.out.print("Log: drive 호출됨");
drivable.drive();
}
}
Car, Bike와 동일한 Drivable 인터페이스를 구현하는 Proxy 클래스를 선언했다. 동일한 인터페이스를 구현하고 동일한 메서드를 호출하는 것이 프록시 패턴의 핵심이다.
Drivable 를 컴포지션으로 가지고, drivable 의 dirve 메서드를 실행한다. 그 전에 우리가 추가하고 싶었던 로깅 작업을 추가해줬다. 자, 이제 로깅 작업을 수행한 뒤 drive 메서드를 실행하는 Drivable 구현체를 선언하고 싶으면 이 Proxy를 통해 생성하면 된다.
interface Drivable {
void drive();
}
class Car implements Drivable {
@Override
public void drive() {
System.out.println("부릉부릉");
}
}
class Bike implements Drivable {
@Override
public void drive() {
System.out.println("따릉따릉");
}
}
// 동일한 인터페이스를 구현하는 프록시 객체 생성!!
class Proxy implements Drivable {
private final Drivable drivable;
public Proxy(Drivable drivable) {
this.drivable = drivable;
}
@Override
public void drive() {
System.out.print("Log: drive 호출됨 ");
drivable.drive();
}
}
public class Client {
public static void main(String[] args) {
Client client = new Client();
// Drivable Bike = new Bike();
// 프록시 객체에 바이크 객체 담아서 전달하기
Drivable proxy = new Proxy(new Bike());
client.drive(proxy);
}
private void drive(Drivable driveable) {
driveable.drive(); // Log: drive 호출됨 따릉따릉
}
}
이렇게 기존 객체의 흐름을 그대로 가져가 되, 그 전에 특정 공통 작업을 추가하고 싶은 경우 Proxy 패턴을 사용할 수 있다. 로그 작업 외에도 캐싱, 지연로딩과 보안에서도 사용된다.
Proxy 패턴의 특징이 있다. 실제 서비스가 수행한 결과, 반환하는 결과에 대해 아무런 영향을 주지 않는다는 것이다. Proxy와 거의 동일하고 수행 결과, 반환 결과에 영향을 주는 패턴은 Decorator 패턴이다.
☁️ Spring에서의 Proxy 패턴 - AOP, Transactional
☁️ Decorator
Decorator 패턴은 객체의 반환값에 장식을 더한다.
앞서 참고했던 코드를 반환값이 존재하도록 살짝만 수정해보자.
interface Drivable {
void drive();
}
class Car implements Drivable {
@Override
public String drive() {
return "부릉부릉";
}
}
class Bike implements Drivable {
@Override
public String drive() {
return "따릉따릉";
}
}
// 동일한 인터페이스를 구현하는
class Decolator implements Drivable {
private final Drivable drivable;
public Proxy(Drivable drivable) {
this.drivable = drivable;
}
@Override
public String drive() {
return "**" + drivable.drive() + "**";
}
}
public class Client {
public static void main(String[] args) {
Client client = new Client();
// Drivable Bike = new Bike();
// 프록시 객체에 바이크 객체 담아서 전달하기
Drivable decolator = new Decolator(new Bike());
client.drive(decolator);
}
private void drive(Drivable driveable) {
System.out.println(driveable.drive()); // ** 따릉따릉 **
}
}
이렇게 서비스의 반환값에 변화를 주기 위해 중간에 장식자를 두는 패턴을 Decolator 패턴이라 한다.
Proxy와 Decolator 두 패턴 모두 OCP와 DIP가 적용된 패턴임을 알 수있다. 추상화에 의존하며 확장에는 열려있고 변경에는 닫혀있다. 또한 Client 와 Decolator, Proxy 모두 추상화에 의존하며 외부에서 의존성을 주입받는다.
GPT가 알려준 데코레이터 패턴 예시
// Component
interface Beverage {
String getDescription();
double cost();
}
// ConcreteComponent
class BasicCoffee implements Beverage {
@Override
public String getDescription() {
return "Basic Coffee";
}
@Override
public double cost() {
return 2.0;
}
}
// Decorator
abstract class ToppingDecorator implements Beverage {
private Beverage beverage;
public ToppingDecorator(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDescription();
}
@Override
public double cost() {
return beverage.cost();
}
}
// ConcreteDecorator - Milk
class MilkDecorator extends ToppingDecorator {
public MilkDecorator(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return super.getDescription() + " with Milk";
}
@Override
public double cost() {
return super.cost() + 0.5; // Milk 추가 비용
}
}
// ConcreteDecorator - Sugar
class SugarDecorator extends ToppingDecorator {
public SugarDecorator(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return super.getDescription() + " with Sugar";
}
@Override
public double cost() {
return super.cost() + 0.2; // Sugar 추가 비용
}
}
// Client
public class DecoratorPatternExample {
public static void main(String[] args) {
// 기본 커피
Beverage basicCoffee = new BasicCoffee();
System.out.println("Description: " + basicCoffee.getDescription());
System.out.println("Cost: $" + basicCoffee.cost());
// 우유 추가
Beverage coffeeWithMilk = new MilkDecorator(basicCoffee);
System.out.println("\nDescription: " + coffeeWithMilk.getDescription());
System.out.println("Cost: $" + coffeeWithMilk.cost());
// 설탕 추가
Beverage coffeeWithSugar = new SugarDecorator(basicCoffee);
System.out.println("\nDescription: " + coffeeWithSugar.getDescription());
System.out.println("Cost: $" + coffeeWithSugar.cost());
// 우유와 설탕 추가
Beverage coffeeWithMilkAndSugar = new SugarDecorator(new MilkDecorator(basicCoffee));
System.out.println("\nDescription: " + coffeeWithMilkAndSugar.getDescription());
System.out.println("Cost: $" + coffeeWithMilkAndSugar.cost());
}
}
'JAVA' 카테고리의 다른 글
디자인 패턴 - Strategy, Template Callback (0) | 2024.01.20 |
---|---|
디자인 패턴 - Template Method, Factory Mehtod (0) | 2024.01.20 |
디자인 패턴 - Adapter (0) | 2024.01.19 |
SOLID (0) | 2024.01.18 |
BigDecimal 생성, 비교시 유의사항 (0) | 2024.01.17 |