네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

'객체지향의 4대 특성'은 객체지향을 잘 사용하기 위한 도구이다.

'객체지향의 5대 원칙'은 이러한 도구를 올바르게 사용하는 원칙으로 볼 수 있다.

'디자인 패턴'은 레시피에 비유할 수 있다.

실제 개발현장에서 비즈니스 요구사항을 처리하면서 만들어진 다양한 해결책 중 많은 사람들이 인정한 Best Practice를 정리한 것이다.

-> 스프링 역시 다양한 디자인 패턴을 활용하고 있다.

스프링 공식 정의: "자바 엔터프라이즈 개발을 편하게 해주는 오픈소스 경량급 애플리케이션 프레임워크"

디자인 패턴은 객체 지향의 특성 중 '상속', '인터페이스', '합성'을 이용한다. (합성은 객체를 속성으로 사용하는 것)

 

어댑터 패턴 (Adapter Pattern)

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.
어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

OBDC/JDBC가 어댑터 패턴을 이용해 다양한 데이터베이스 시스템을 단일한 인터페이스로 조작할 수 있게 해준다.

'어플리케이션' - '어댑터' - '실제 구현체'

 

어댑터 패턴 사용

public class AdapterServiceA {
    ServiceA serviceA = new ServiceA();
    
    void runService() {
        serviceA.runServiceA();
    }
}

public class AdapterServiceB {
    ServiceB serviceB = new ServiceB();
    
    void runService() {
        serviceB.runServiceB();
    }
}

// 어댑터 패턴이 적용된 코드
public class ClientWithAdapter {
    public static void main(String[] args) {
        AdapterServiceA adpaterA = new AdapterServiceA();
        AdapterServiceB adpaterB = new AdapterServiceB();
        
        adpaterA.runService();
        adpaterB.runService()
    }
}

어댑터에서 동일한 이름의 runService()를 사용해서 각각의 runServiceX를 호출하고 있다.

어댑터가 특정 인터페이스를 구현하게 해서 하나의 인터페이스에서 의존관계 주입을 통해 똑같은 메서드로 각각 다른 구현체의 메서드를 호출할 수 있다.

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

 

프록시 패턴 (Proxy Pattern)

프록시는 대리자라는 의미이다. 다른 누군가를 대신해 그 역할을 수행하는 존재를 말한다.

 

프록시 패턴 사용

public interface IService {
    String runSomething();
}

public class Service implements IService {
    public String runSomething() {
        return "서비스 최고!!";
    }
}

public class Proxy implements IService {
    IService service;
    
    public String runSomething() {
        System.out.println("호출에 대한 흐름 제어가 주 목적, 변환 결과를 그대로 전달");
        
        service = new Service();
        return service.runSomething();
    }
}

public class ClientWithProxy {
    public static void main(String[] args) {
        IService proxy = new Proxy();
        System.out.println(proxy.runSomething());
    }
}

서비스 객체가 들어갈 자리에 대리자 객체를 대신 투입한다.

 

  • 프록시는 실제 서비스와 같은 이름의 메서드를 구현
  • 프록시는 실제 서비스에 대한 참조 변수를 갖는다. (합성)
  • 프록시는 실제 서비스의 메서드와 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 반환한다.
  • 프록스는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있다.
프록시 패턴은 실제 서비스의 반환값은 변경하지 않고 제어의 흐름을 변경하거나 다른 로직을 수행하기 위해 사용한다.

 

데코레이터 패턴 (Decorator Pattern)

원본에 장식을 더하는 패턴

프록시 패턴과 구현 방법이 같으나 최종적으로 반환값에 장식을 덧입힌다.

public class Decorator implements IService {

    IService service;
    
    public String runSomething() {
        System.out.println("호출에 대한 장식 주목적, 클라이언트에게 반환 결과에 장식을 더하여 전달");
        
        service = new Service();
        
        return "정말" + service.runSomething(); // 반환값에 다른 값을 추가한다.
    }
}

데코레이터 패턴의 중요 포인트는 프록시 패턴의 중요 포인트에 반환값에 변화를 줄 수 있다는 점이다.

 

싱글톤 패턴 (Singleton Pattern)

인스턴스를 하나만 만들어 사용하기 위한 패턴

'커넥션 풀', '스레드 풀', '디바이스 설정 객체' 등과 같은 경우 인스턴스를 여러 개 만들게 되면 불필요한 자원을 사용하게 되고, 프로그램이 예상하지 못한 결과를 만들 수 있다.

 

싱글톤 패턴을 적용하면 두 개의 객체가 존재할 수 없으므로 

1. 객체 생성을 위한 new에 제약을 걸어야 하고

2. 만들어진 단일 객체를 반환할 수 있는 메서드가 필요하다.

 

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

 

 

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

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

공통적으로 사용하는 메서드는 상위 클래스에서 구현

하위 클래스마다 달라지는 것은 추상 클래스로 구현 강제화

하위 클래스마다 달라질 수 있는 것은 오버라이드 가능한 훅 메서드로 만듦.

 

public abstract class Animal {

    // 템플릿 메서드
    public void playWithOwner() {
        System.out.println("귀염둥이 이리온...");
        play();
        runSomething();
        System.out.println("잘했어!");
    }
    
    // 추상 메서드
    abstract void play();
    
    // Hook(갈고리) 메서드
    void runSomething() {
        System.out.println("꼬리 살랑 살랑-");
    }
}

상위 클래스에서 템플릿을 제공하는 playWithOwner() 템플릿 메서드를 제공한다.

그리고 템플릿 메서드 안에 있는 play() 추상 메서드와 runSomething() 메서드가 있다.

-> 추상 메서드는 하위 클래스에서 구현을 강제한다.

-> Hook 메서드는 오버라이드가 자유롭다.

 

템플릿 메서드: 공통 로직 수행, 로직 수행 중 추상메서드 혹은 훅 메서드를 호출

템플릿 메서드에서 호출하는 추상 메서드: 반드시 하위 클래스가 오버라이딩 해야 한다.

템플릿 메서드에서 호출하는 훅 메서드: 하위 클래스가 선택적으로 오버라이딩한다.

 

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

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

팩터리 메서드는 객체를 생성/반환하는 메서드를 말한다.

팩터리 메서드 패턴은 하위 클래스에서 팩터리 메서드를 오버라이딩해서 객체를 반환하는 것을 의미한다.

// 추상 팩터리 메서드
public abstract class Animal {

    // 추상 팩터리 메서드
    abstract AnimalToy getToy();
}

// 추상 팩터리 메서드 오버라이딩
public class Dog extends Animal {

    @Override
    AnimalToy getToy() {
        return new DogToy();
    }
}

public class Driver {
    public static void main(String[] args) {
        Animal bolt = new Dog();
        Animal kitty = new Cat();
        
        AnimalToy boltBall = bolt.getToy();
        AnimalToy kittyTower = kitty.getToy();
        
        boltBall.identify();
        kittyTower.identify();
    }
}

전략 패턴 (Strategy Pattern)

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

전략 패턴 구성요소 3가지

  • 전략 메서드를 가진 전략 객체
  • 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
  • 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트 (전략 객체의 공급자)
// 전략 인터페이스
public interface Strategy {
    public abstract void runStrategy();
}

// 전략 인터페이스 구현
public class StrategyGun implements Strategy {
    
    @Override
    public void runStrategy() {
        System.out.println("탕, 타당, 타다당");
    }
}
public class StrategySword implements Strategy {
    
    @Override
    public void runStrategy() {
        System.out.println("챙! 챙챙! 챙챙챙!");
    }
}

// 전략을 사용하는 컨텍스트
public class Soldier {
    void runContext(Strategy strategy) {
        System.out.println("전투 시작!");
        strategy.runStrategy();
        System.out.println("전투 종료!");
    }
}

// 전략 패턴의 클라이언트
public class Client {
    public static void main(String[] args) {
        Strategy strategy = null;
        Soldier rambo = new Soldier();
        
        strategy = new StrategyGun();
        rambo.runContext(strategy);
        
        strategy = new StrategySword();
        rambo.runContext(strategy);
                
    }
}

클라이언트는 전략을 다양하게 변경하면서 컨텍스트를 실행할 수 있다.

 

전략 패턴에는 OCP, DIP가 적용된다.

 

템플릿 콜백 패턴 (Template Callback Pattern)

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

템플릿 콜백 패턴은 전략 패턴의 변형으로 스프링 3대 프로그래밍 모델 중 하나인 DI에서 사용하는 특별한 형태의 전략 패턴이다.

전략 패턴과 동일하지만 전략을 익명 내부 클래스로 정의해서 사용한다.

 

public class Client {
	public static void main(String[] args) {
		Soldier rambo = new Soldier();

		rambo.runContext(new Strategy() {
			@Override
			public void runStrategy() {
				System.out.println("총! 총초종총 총! 총!");
			}
		});

		System.out.println();

		rambo.runContext(new Strategy() {
			@Override
			public void runStrategy() {
				System.out.println("칼! 카가갈 칼! 칼!");
			}
		});

		System.out.println();

		rambo.runContext(new Strategy() {
			@Override
			public void runStrategy() {
				System.out.println("도끼! 독독..도도독 독끼!");
			}
		});
	}
}

 

위 코드는 중복이 있으므로 리팩토링이 가능하다.

public class Soldier {

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

public class Client {
    public static void main(String[] args) {
        Soldier rambo = new Soldier();
        
        rambo.runContext("총! 총초종총 총! 총!");
        System.out.println();
        
        rambo.runContext("칼! 카가갈 칼! 칼!");
        System.out.println();
        
        rambo.runContext("도끼! 독독..도도독 독끼!");
    }
}

중복되는 전략을 생성하는 코드가 컨텍스트 내부로 들어왔다. (중복되는 부분을 컨텍스트로 이관)

 

스프링은 이런식으로 리팩터링된 템플릿 콜백 패턴을 DI에 적극 활용하고 있다.

OCP, DIP 적용된 설계 패턴이다.

 

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !