[Java] 디자인패턴 정리

devdo·2021년 12월 16일
3

Java

목록 보기
29/56

✨ 참고
어댑터(서로다른 객체들끼리 연결), 데코레이터(하나의 클래스에 확장해서 꾸며주기 위해), 프록시(대리로 위임역할, 접근 권한 체크용) 패턴은 아주 비슷한다. 구현방식이 아주 비슷하기 때문에 구현 예시, 사용하는 이유를 많이 보면서 구분할 줄 알아야 한다!


1. 어댑터 패턴 ( Adapter Pattern )

호출당하는 쪽의 메소드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴

호환성이 없는 기존 클래스의 인터페이스를 변환해서 재사용하기 위해 필요.

ex) JDBC, JRE

서로 다른 두 인터페이스 사이에 통신이 가능하게 하는 것
합성, 즉 객체의 속성으로 만들어서 참조하는 디자인 패턴

ex. 220V 전자제품을 110V 전자제품으로 사용할 수 있도록 하는 어댑터를 구현하는 예시

public interface Electronic110V {
    void powerOn();
}
public interface Electronic220V {
    void connect();
}

어댑터 클래스 AdaptorSocket

public class AdaptorSocket implements Electornic110V {

    private Electornic220V electornic220V;

    public AdaptorSocket(Electornic220V electornic220V) {
        this.electornic220V = electornic220V;
    }

    @Override
    public void powerOn() {
        electornic220V.connect();
    }

main

public class App {

    public static void main(String[] args) {
        // 220V 전자제품 생성
        Electronic220V electronic220V = new Electronic220V() {
            @Override
            public void connect() {
                System.out.println("220V 전자제품 연결");
            }

            @Override
            public void powerOn() {
                System.out.println("220V 전자제품 ON");
            }
        };

        // 어댑터를 통해 110V 전자제품처럼 사용
        Electronic110V adapter = new AdaptorSocket(electronic220V);
        adapter.powerOn(); // 220V 전자제품이 110V 전자제품처럼 동작
    }
}

2. 프록시 패턴 ( Proxy Pattern )

제어 흐름을 조정하기 위한 목적으로 중간에 대리자(proxy)를 두는 패턴

프록시 패턴은 개방 폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)이 적용된 설계 패턴이다.

객체에 접근을 제어하고 싶을 때, 다른 객체가 위임해서 대리로 해결하는 방식
1) 객체를 실제 사용하기 전까지 초기화를 미루고 싶을 때 (Lazy init)
2) 접근 권한 통제

사용된다.

스프링에서는 AOP에서는 이 프록시 패턴을 사용한다.
객체를 실제로 부르는 게 아닌, 프록시 객체를 부르고 나중에 프록시 객체가 실제 객체를 호출하는 것이다.

public interface CommandExecutor {
 
    public void runCommand(String cmd) throws Exception;
}
public class CommandExecutorImpl implements CommandExecutor {
 
    @Override
    public void runCommand(String cmd) throws IOException {
        //some heavy implementation
        Runtime.getRuntime().exec(cmd);
        System.out.println("'" + cmd + "' command executed.");
    }
}

이제 프록시 객체를 통해 권한 접근을 체크할 것이다.

// 프록시 사용
public class CommandExecutorProxy implements CommandExecutor {
    private boolean isAdmin;
    private CommandExecutor executor;
	
    public CommandExecutorProxy(String user, String pwd){
        if("ReadyKim".equals(user) && "correct_pwd".equals(pwd))
            isAdmin = true;
        executor = new CommandExecutorImpl();
    }
	
    @Override
    public void runCommand(String cmd) throws Exception {
        if(isAdmin){
            executor.runCommand(cmd);
        }else{
            if(cmd.trim().startsWith("rm")){
                throw new Exception("rm command is not allowed for non-admin users.");
            }else{
                executor.runCommand(cmd);
            }
        }
    }
}
// main 프록시 객체(CommandExecutorProxy)가 위임해서
// executor의 runCommand()를 실행한다.
public class ProxyPatternTest {
 
    public static void main(String[] args){
        CommandExecutor executor = new CommandExecutorProxy("ReadyKim", "wrong_pwd");
        try {
            executor.runCommand("ls -ltr");
            executor.runCommand("rm -rf abc.pdf");
        } catch (Exception e) {
            System.out.println("Exception Message::"+e.getMessage());
        }	
    }
}

3. 데코레이터 패턴 ( Decorator Pattern )

메서드 호출의 반환값에 변화를 주기 위해 중간에 장식자를 두는 패턴

기존 뼈대(클래스)는 유지하되, 이후 필요한 형태를 꾸밀 때 사용.
extends, implement 을 통해 확장하는 형태로 활용.

예시로는, 기본 자동차(Audi)의 옵션이 추가되면서 장식(Decorator)을 하는 패턴을 보여주겠습니다. 아우디 자동차에 기본적인 기능 외 여러 옵션을 추가할 겁니다.

public interface ICar {
    int getPrice();
    void showPrice();
}
public class Audi implements ICar {

    @Override
    public int getPrice() {
        return 50000; // 아우디 기본 모델 가격
    }
    
        @Override
    public void showPrice() {
        System.out.println("Audi 기본 모델 가격은 " + getPrice() + "입니다.");
    }

}

AudiDecorator, NavigationSystemDecorator 데코테리어 클래스

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() + this.modelPrice;
    }

    @Override
    public void showPrice() {
        System.out.println("Audi " + this.modelPrice + " Price is " + getPrice());
    }
}


public class NavigationSystemDecorator extends AudiDecorator {
    public NavigationSystemDecorator(ICar audi, String modelName, int modelPrice) {
        super(audi, modelName, modelPrice);
    }

    @Override
    public int getPrice() {
        return audi.getPrice() + this.modelPrice;
    }

    @Override
    public void showPrice() {
        System.out.println("Audi " + modelName + " Price is " + getPrice() + " (Navigation System Included)");
    }
}

main

public class Main {
    public static void main(String[] args) {
        // 기본 아우디 모델
        ICar basicAudi = new Audi();
        basicAudi.showPrice(); // 출력: Audi 기본 모델 가격은 50000입니다.

        // 네비게이션 시스템이 추가된 아우디 모델
        ICar audiWithNavigation = new NavigationSystemDecorator(new Audi(), "A6", 2000);
        audiWithNavigation.showPrice(); // 출력: Audi A6 Price is 52000 (Navigation System Included)
    }
}

보통 A 클래스 = new B (new C(), "A6", 2000); 이런 형태로 감싸주는 구조이다.


4. 싱글턴 패턴 ( SingleTon Pattern )

클래스의 인스턴스, 즉 객체를 하나만 만들어 사용하는 패턴

인스턴스를 하나만 만들어 사용하기위한 패턴 (커넥션 풀, 스레드 풀, 디바이스 설정 등)

주로 서버와 Socket Connection, DB JDBC Connetion, 스프링의 Bean 등에 많이 활용!

< 싱슬턴 패턴 만들기 과정 >

  • new를 실행할 수 옶도록 생성자에 private 접근 제어자를 지정한다.
  • 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요하다.
  • 유일한 단일 객체를 참조할 정적 참조 변수가 필요하다.
public class Singleton {
	static Singleton singletonObject; // 정적 참조 변수
	
	private Singleton() {}; // private 생성자
	
	// 객체 반환 정적 메서드
	public static Singleton getInstance() {
		if(singletonObject == null) {
			singletonObject = new Singleton();
		}
		return singletonObject;
	}
}

public class Client {
	public static void main(String[] args) {
		// private 생성자이므로 new를 통해 인스턴스를 생성할 수 없음
		// Singleton s = new Singleton();
		
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		Singleton s3 = Singleton.getInstance();
		
		System.out.println(s1);
		System.out.println(s2);
		System.out.println(s3);		
		/**
		 com.grace.oop.singleton.Singleton@26f0a63f
		 com.grace.oop.singleton.Singleton@26f0a63f
		 com.grace.oop.singleton.Singleton@26f0a63f
		 */
	}
}

그외 더 좋은 성능의 싱글톤 패턴 방식은 다음 블로그를 참조해주기를 바란다.
https://velog.io/@mooh2jj/멀티스레드환경에서의-싱글톤-객체-그리고-enum


5. 옵저버 패턴(Observer Pattern)

event가 일어났을 때,(그리고 event가 언제 일어날지 모를 때) 미리 등록된 다른 클래스에 통보해주는 패턴을 구현한 것.
ex) event listner

💡 옵저버 패턴은 구독만 하면 자동으로 알림을 받을 수 있어 편리합니다!

구현 예시

    private String name;
    private IButtonlistner buttonlistner;

    public Button(String name) {
        this.name = name;
    }

    public void click(String message) {
        buttonlistner.clickEvent(message);
    }

    public void addListner(IButtonlistner buttonlistner) {
        this.buttonlistner = buttonlistner;
    }
}

// Interface
public interface IButtonlistner {

    void clickEvent(String event);

}

// Main
public class ButtonMain {

    public static void main(String[] args) {

        Button button = new Button("Button");
        button.addListner(new IButtonlistner() {
            @Override
            public void clickEvent(String event) {
                System.out.println(event);
            }
        });
        button.click("click 1 send a message.");
        button.click("click 2 send a message.");
        button.click("click 3 send a message.");
    }
}

6. 파사드 패턴(Facade Pattern)

파사드(Facade)는 건물의 앞쪽 정면이라는 뜻
: 여러개의 객체와 실제 사용하는 서브 객체 사이에 복잡한 의존관계가 있을 때, 중간에 facade라는 객체를 두고, 여기서 제공하는 interface만을 활용하여 기능을 사용하는 방식.

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() {
        reader.fileConnect();
        writer.fileDisconnect();
        ftp.disConnect();
    }

    public void read() {
        reader.fileRead();
    }

    public void write() {
        writer.fileWrite();
    }

7. 전략 패턴 ( Strategy Pattern )

클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴

< 필요한 요소 >

  • 전략 메서드를 가진 전략 객체(람다를 사용할려면 전략 객체에 하나의 메서드만 존재해야 한다.)
  • 전략 객체(인터페이스 - 추상적인 객체)를 변하지 않는 컨텍스트 (전략 객체의 사용자/소비자)에 주입(DI) : 전략 패턴을 여러 형식이 있는데, 파라미터로 객체를 주입하는 형태컨텍스트에 바로 실행되는 전략패턴 방식으로 많이 사용됨.
  • 전략 객체(Strategy)를 생성해 컨텍스트(ContextV2)에 전략 객체를 주입하여 해당 인터페이스를 구현하도록 한다. 상속이 아니라 위임을 하는 것이다.

객체지향의 꽃
: 유사한 행위들을 캡슐화하여, 객체의 행위를 바꾸고 싶은 경우 직접 변경하는 것이 아닌 전략(interface)만 변경할 수가 있어 상호교체(interfaceImpl1, interfaceImpl2 서로 교체 가능)가 가능해 유연하게 확장가능한 패턴
: SOLID 중에서 개방폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)을 따른다.
: 스프링의 DI 의존성기술은 이 전략패턴을 사용한 것이다.

1)

    // Strategy : 전략 객체
    public interface Strategy {
    	void call();		// 전략 메서드
	}

    /**
     * 전략객체를 '파라미터'로 전달 받는 방식 : 가장 대표적인 방식
     */
    @Slf4j
    public class ContextV2 {
        public void execute(Strategy strategy) {		// 피라미타로 strategy 전달
            long startTime = System.currentTimeMillis();
            //비즈니스 로직 실행
            strategy.call(); //위임
            //비즈니스 로직 종료
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("resultTime={}", resultTime);
        }
    }

2)


public abstract class Animal {

//    private String masterName;
    
    public void eat() {
        System.out.println("먹는다.");
    }

    //    public String getMasterName() {
//        return "dsg";
//    }
    public abstract void bark();

    public abstract String getName();

}


public class Cat extends Animal {

    private final String name = "고양이";


    @Override
    public void bark() {
        System.out.println("야옹");
    }

    @Override
    public String getName() {
        return name;
    }


}


public class Mouse extends Animal {

    private final String name = "쥐";


    @Override
    public void bark() {
        System.out.println("찍찍");
    }

    @Override
    public String getName() {
        return name;
    }


}

public class DoorMan {

    public void chaseOut(Animal animal) {
        System.out.println(animal.getName() + "을 쫒아낸다");
    }
}


public class App {
    public static void main(String[] args) {
    
     // Animal(Mouse, Cat) , DoorMan 객체를 App context 에 주입!
        Animal m = new Mouse();     // Mouse m = new Mouse(); 와 같은 의미
        Animal cat = new Cat();     // Cat cat = new Cat(); 와 같은 의미

        DoorMan doorMan = new DoorMan();
        doorMan.chaseOut(m);
        doorMan.chaseOut(cat);
    }
}

이 Strategy(추상객체)를 구현하는 각종전략(하위클래스)들을 불러와 Context에 주입하는 방식!


8. 그외 패턴들


템플릿 메서드 패턴 ( Template Method Pattern )

상위 클래스의 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴

  • 간단하게 다형성을 이용한 디자인패턴이라 생각하면 된다.
  • 단일 책임 원칙(SRP)을 지키고 있다
  • 템플릿 메서드 패턴은 상속을 사용한다. 따라서 상속에서 오는 단점, 자식 클래스가 부모클래스에 의존하는 문제를 그대로 안고간다.
/**
* 템플릿 메서드 패턴 적용
*/
@Test
void templateMethodV1() {
	AbstractTemplate template1 = new SubClassLogic1();
	template1.execute();
	AbstractTemplate template2 = new SubClassLogic2();
	template2.execute();
}

템플릿 메서드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴이 바로 전략 패턴(Strategy Pattern)이다.


팩터리 메서드 패턴 ( Factory Mrthod Pattern )

오버라이드된 메서드가 객체를 반환하는 패턴

  • 팩터리 메서드는 객체를 생성 반환하는 메서드이다.
  • 팩터리 클래스는 하위 클래스에서 팩터리 메서드를 오버라이딩해서 객체를 반환하게 하여 상위 클래스와 하위클래스가 분리되어 느슨한 결합이 되어진다.
  • 의존 역전 원칙(DIP)을 활용하고 있다
  • 처음 클래스를 배울 때 클래스를 붕어빵틀, 객체를 붕어빵이라고 배웠는데 사실, 붕어빵틀은 팩토리 메서드였다!
  1. 요구사항을 담은 interface 생성
public interface Language {
 
    public void compile();
 
    public String getLanguageType();
}
  1. 상속받은 클래스를 구분해 줄 클래스 생성(Enum도 가능)
public class LanguageType {		
 
    public static final String JAVA = "java";
 
    public static final String PYTHON = "python";
 
}
  1. interface를 상속받아 구현할 하위 클래스 생성
public class Java implements Language{
 
    @Override
    public void compile() {
        System.out.println("Java Compile");
    }
 
    @Override
    public String getLanguageType() {
        return LanguageType.JAVA;
    }
}
 
public class Python implements Language{
 
    @Override
    public void compile() {
        System.out.println("Python Compile");
    }
 
    @Override
    public String getLanguageType() {
        return LanguageType.PYTHON;
    }
}
  1. 파라미터에 따라 객체를 반환해줄 Factory 클래스 생성
// 기본 Factory 클래스
@Component
public class LanguageFactory {
    
    public Language getLanguage(String languageType) {
 
        if(languageType.equal(LanguageType.JAVA)){
            return new Java();
        } else if(languageType.equal(LanguageType.Python)) {
            return new Python();
        } else {
            return null;
        }//end else
 
    }//getLanguage
 
}//class
 
// 개선된 Factory 클래스
@Component
public class LanguageFactory {
 
    @Autowired
    private List<Language> languageList;
 
    public Language getLanguage(String languageType) {
        for(Language language : languageList) {
            if(languageType.equals(language.getLanguageType()))
                return language;
        }//end for
        return null;
    }//getLanguage
 
}//class
  1. Factory 클래스를 사용하여 객체를 받아 사용하는 클래스 생성
@Service
public class UseFactory {
 
    @Autowired
    private LanguageFactory factory;
 
    @Override
    public void useLanguageFactory() {
        Language java = factory.getLanguage(LanguageType.JAVA);
        Language python = factory.getLanguage(LanguageType.PYTHON);
 
        java.compile();
        System.out.println(java.getClass());
 
        python.compile();
        System.out.println(python.getClass());
 
    }//useLanguageFactory
 
}//class

탬플릿 콜백 패턴 ( Template Callback Pattern - 견본/회신 패턴 )

전략 패턴에서 템플릿콜백 부분이 강조된 패턴이라 생각하면 된다.

전략을 익명 내부 클래스로 구연한 전략 패턴

콜백 정의
프로그래밍에서 콜백(callback) 또는 콜애프터 함수(call-after function)는 다른 코드의 인수로서
넘겨주는 실행 가능한 코드를 말한다. 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도
있고, 아니면 나중에 실행할 수도 있다. (위키백과 참고)

쉽게 말하면 callback은 코드가 호출(call)은 되는데 코드를 넘겨준 곳의 뒤(back)에서 실행된다는 뜻이다.

자바 언어에서 콜백
자바 언어에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다. 자바8부터는 람다를 사용할 수 있다.
자바 8 이전에는 보통 하나의 메소드를 가진 인터페이스를 구현하고, 주로 익명 내부 클래스를 사용했다.
최근에는 주로 람다를 사용한다.

  • 전략 패턴의 변형으로 스프링에서만 부르는 특별한 형태의 전략 패턴
  • 스프링에서 jdbcTemplate, RestTemplate, RedisTemplateXxxTemplate가 있다면 템플릿 콜백 패턴을 쓴 것이다.
  • 개방 폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)이 적용된 설계 패턴이다.
public interface Strategy {
	public abstract void runStrategy();
}

public class Soldier {
	void runContext(String weapon) {
		System.out.println("전투 시작");
		executeWeapon(weapon).runStrategy();
		System.out.println("전투 종료");
	}
	
	private Strategy executeWeapon(final String weapon) {
		return new Strategy() {
			@Override
			public void runStrategy() {
				System.out.println(weapon);
			}
		};
	}
}

public class Client {
	public static void main(String[] args) {
		Soldier rambo = new Soldier();
		rambo.runContext("총 총 총 총 총");	
		System.out.println();
		rambo.runContext("활 활 활 활 활");

	}
}
/** 출력값
전투 시작
총 총 총 총 총
전투 종료

전투 시작
활 활 활 활 활
전투 종료
*/

템플릿 콜백 패턴 - 람다

/**
* 템플릿 콜백 패턴 - 람다
*/
@Test
void callbackV2() {
	TimeLogTemplate template = new TimeLogTemplate();
	template.execute(() -> log.info("비즈니스 로직1 실행"));
	template.execute(() -> log.info("비즈니스 로직2 실행"));
}


참고

profile
배운 것을 기록합니다.

0개의 댓글