[1] 디자인 패턴이란?

  • 디자인패턴을 습득하면 좋은 점
    • 상황에 맞는 올바른 설계를 더 빠르게 적용할 수 있다.
    • 각 패턴의 장단점을 통해서 설계를 선택하는데 도움을 얻을 수 있다.
    • 설계 패턴에 이름을 붙임으로써 시스템의 문서화, 이해, 유지 보수에 도움을 얻을 수 있다.

[2] 전략(Strategy) 패턴

💡 비슷한 코드를 실행하는 if-else 블록은 전략 패턴 적용 대상이다.

비슷한 코드를 실행하는 if-else 블록을 가진 기존 코드

  • 기존 코드
    public class Calculator {

        public int calculate(boolean firstGuest, List<Item> items) {
            int sum = 0;
            for (Item item: items) {
                if (firstGuest)
                    sum += (int)(item.getPrice() * 0.9);
                else if (!item.isFresh())
                    sum += (int)(item.getPrice() * 0.8);
                else
                    sum += item.getPrice();
            }
            return sum;
        }
    }
  • 기존 코드의 문제점
    • 서로 다른 계산 정책들이 한 코드에 섞여있음
      • 정책이 추가될수록 코드 분석이 어렵
    • 가격 정책이 추가될 때 마다 calculate 메서드를 수정하는 것이 점점 어려워짐
      • 새로운 가격 정책이 추가될경우 (ex. 마지막 손님 50% 할인), calculate 메서드에 lastGuest 파라미터가 추가되고 if 블록 하나가 더 추가돼야함.

전략 패턴 도입

  • 전략패턴이란?
    • 특정 콘텍스트에서 알고리즘(전략)을 별도로 분리하는 설계 방법

  • ex)
    • 전략(Strategy): 가격 할인 알고리즘을 추상화 한 DiscountStrategy
    • 콘텍스트(Context): 가격 계산 기능 자체의 책임을 갖고있는 Calculator
// 전략 패턴을 적용한 Calculator의 구현
public class Calculator {

	private DiscountStrategy discountStrategy;

	public Calculator(DiscountStrategy discountStrategy) {
		this.discountStrategy = discountStrategy;
	}

	public int calculate(List<Item> items) {
		int sum = 0;
		for (Item item: items) {
			sum += discountStrategy.getDiscountPrice(item);
		}
		return sum;
	}
}
// Calculator에서 사용하는 전략 인터페이스
public interface DiscountStrategy {
	int getDiscountPrice(Item item);
}
// DiscountStrategy 인터페이스를 구현한 콘크리트 클래스
public class FirstGuestDiscountStrategy implements DiscountStrategy {

	@Override
	public int getDiscountPrice(Item item) {
		return (int)(item.getPrice() * 0.9);
	}
}
  • 전략 객체는 콘텍스트를 사용하는 클라이언트에서 직접 생성한다.
public class Client {
	private DiscountStrategy strategy;

	public void onFirstGuestButtonClick() {
    	// 첫 손님 할인 버튼 누를 때 생성
		strategy = new FirstGuestDiscountStrategy();
	}

	public void onCalculationButtonClick() {
    	// 계산 버튼 누를 때 실행
		Calculator cal = new Calculator(strategy);
		int price = cal.calculate(items);
	}
}
  • 전략 패턴을 적용함으로써, 콘텍스트 코드의 변경 없이 새로운 전략을 추가할 수 있다.
  • 즉, 콘텍스트인 Calculator 클래스는 할인 정책 확장에는 열려있고 변경에는 닫혀있는, 개방 폐쇄 원칙을 따르는 구조를 가진다.
public class Client {
	private DiscountStrategy strategy;

	public void onFirstGuestButtonClick() {
		strategy = new FirstGuestDiscountStrategy();
	}

	public void onLastGuestButtonClick() {
		// 마지막 손님 대폭 할인 버튼 누를 때 생성
		strategy = new LastGuestDiscountStrategy();
	}

	public void onCalculationButtonClick() {
		Calculator cal = new Calculator(strategy);
		int price = cal.calculate(items);
	}
}

[3] 템플릿 메서드(Tempalte Method) 패턴

💡 실행 과정/단계는 동일한데 각 단계 중 일부의 구현이 다른 경우에 사용할 수 있는 패턴

  • 템플릿 메서드 패턴의 구성

    1. 실행 과정을 구현한 상위 클래스
    2. 실행 과정의 일부 단계를 구현한 하위 클래스
  • 상위 클래스: 실행 과정을 구현한 메서드 제공

    • 이 메서드는 기능을 구현하는데 필요한 각 단계 정의
    • 일부 단계는 추상 메서드를 호출하는 방식으로 구현

[기존 코드]
DB/LDAP에서 사용자 정보를 가져오는 부분의 구현만 다르고, 인증을 처리하는 과정은 완전히 동일

public class DbAuthenticator {
	public Auth authenticate(String id, String pw) {
		// 사용자 정보로 인증 확인
		User user = userDao.selectById(id);
		boolean auth = user.equalPassword(pw);

		// 인증 실패시 익셉션 발생
		if (!auth)
			throw createException();

		// 인증 성공시, 인증 정보 제공
		return new Auth(id, user.getName());
	}

	private AuthException createException() {
		return new AuthException();
	}
}
public class DbAuthenticator {
	public Auth authenticate(String id, String pw) {
		// 사용자 정보로 인증 확인
		User user = userDao.selectById(id);
		boolean auth = user.equalPassword(pw);

		// 인증 실패시 익셉션 발생
		if (!auth)
			throw createException();

		// 인증 성공시, 인증 정보 제공
		return new Auth(id, user.getName());
	}

	private AuthException createException() {
		return new AuthException();
	}
}

[템플릿 메서드를 사용해 리팩토링]
코드 중복 문제를 제거하면서, 동시에 코드를 재사용 가능하게 만들어준다.

  • authenticate(): 모든 하위 타입에 동일하게 적용되는 실행 과정을 제공 => 템플릿 메서드
public abstract class Authenticator {
	// 템플릿 메서드
	public Auth authenticate(String id, String pw) {
		if (!doAuthenticate(id, pw))
			throw createException();

		return createAuth(id);
	}

	protected abstract boolean doAuthenticate(String id, String pw);

	private RuntimeException createException() {
		throw new AuthException();
	}

	protected abstract Auth createAuth(String id);
}

Authenticator 클래스를 상속받는 하위클래스는 authenticate() 메서드에서 호출하는 메서드만 알맞게 재정의해주면 된다.

public class LdapAuthenticator extends Authenticator {

	@Override
	protected boolean doAuthenticate(String id, String pw) {
		return ldapClient.authenticate(id, pw);
	}

	@Override
	protected Auth createAuth(String id) {
		LdapContext ctx = ldapClient.find(id);
		return new Auth(id, ctx.getAttributes("name"));
	}
}

상위 클래스가 흐름 제어 주체

  • 하위 클래스가 아닌 상위 클래스에서 흐름을 제어한다.

일반적인 경우, 하위타입이 흐름을 제어한다.
즉, 하위 타입이 상위 타입의 기능을 재사용할지 여부를 결정한다

// 일반적인 경우
// turnOn 메서드는 상위 클래스의 turnOn 메서드 재사용 여부를 자신이 결정
public class SuperCar extends ZetEngine {
	@Override
	public void turnOn() {
		// 하위 클래스에서 흐름 제어
		if (notReady)
			beep();
		else
			super.turnOn();
	}
}

반면, 템플릿 메서드 패턴에선 상위 타입의 템플릿 메서드가 모든 실행 흐름을 제어함.
하위 타입의 메서드는 템플릿 메서드에서 호출되는 구조

  • 템플릿 메서드: public
    • ex) authenticate()
    • 외부에 제공하는 기능에 해당하기 때문.
  • 템플릿 메서드에서 호출하는 메서드: protected
    • ex) doAuthenticate() / createAuth()
    • 템플릿 메서드에서만 호출되기 때문에 public일 필요 X
    • 하위 타입에서 재정의할 수 있어야하기 때문에 private X

위의 예시에선 템플릿 메서드에서 호출하는 메서드를 추상 메서드로 정의했는데, 기본 구현을 제공하고 하위 클래스에서 알맞게 재정의하도록 구현할 수도 있음: 훅(hook) 메서드
이 경우 해당 메서드는 기능의 확장 지점으로 사용됨.

템플릿 메서드와 전략 패턴의 조합

  • 템플릿 메서드 + 전략 패턴: 조립의 방식으로 템플릿 메서드 패턴 활용
    • ex) 스프링 프레임워크의 Template으로 끝나는 클래스들
  • 변경되는 부분을 실행할 객체를 파라미터를 통해서 전달받는 방식
  • 상속을 기반에 둔 템플릿 메서드 구현보다 유연함

0개의 댓글