ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Factory Method pattern & Abstract Factory Pattern
    CS/Design Pattern 2022. 8. 27. 18:03

    팩토리 메서드 패턴과 추상 팩토리 패턴의 구조와 두 패턴에 대해 정리합니다.

     

     

    Factory Method?

    어떤 인스턴스를 생성하는 책임을 구체적인 클래스가 아닌 추상적인 인터페이스의 메서드로 감싸는 방식

     

    필요성

    객체를 생성할 때, 동일한 객체에 대한 요구사항이 추가되어도 기본 로직이 변화하지 않도록 방지 (OCP)

     

     

     

    Factory Method 적용 전

     

    Ship.java (생성할 객체에 대한 정보)

    public class Ship {
    
    	private String name;
    
    	private String color;
    
    	private String logo;
    
    	// getter, setter
    }

     

    ShipFactory.java (주문에 맞춰 생성)

    public class ShipFactory {
    
    	public static Ship orderShip(String name, String email) {
    		if(name == null || name.isEmpty()) {				// 1
    			throw new IllegalArgumentException("배 이름을 지어주세요.");
    		}
    
    		if(email == null || email.isEmpty()) {
    			throw new IllegalArgumentException("연락처를 남겨주세요.");
    		}
    
    		prepareFor(name);
    
    		Ship ship = new Ship();						// 4
    		ship.setName(name);
    
    		if(name.equalsIgnoreCase("whiteShip")) {
    			ship.setLogo("!!");
    		} else if(name.equalsIgnoreCase("blackShip")) {
    			ship.setLogo("??");
    		}
    
    		if(name.equalsIgnoreCase("whiteShip")) {
    			ship.setColor("white");
    		} else if(name.equalsIgnoreCase("blackShip")) {
    			ship.setColor("black");
    		}
    
    		sendEmailTo(email, ship);
    
    		return ship;
    	}
    
    	private static void sendEmailTo(String email, Ship ship) {		// 3
    		System.out.println(ship.getName() + " 다 만들었습니다.");
    	}
    
    	private static void prepareFor(String name) {				// 2
    		System.out.println(name + " 만들 준비 중");
    	}
    
    }

     

    Client.java (사용자 주문)

    public class Client {
    
    	public static void main(String[] args) {
    
    		Client client = new Client();
    
    		Ship whiteShip = ShipFactory.orderShip("whiteShip", "gmail");
    		System.out.println(whiteShip);
    
    		Ship blackShip = ShipFactory.orderShip("blackShip", "naver");
    		System.out.println(blackShip);
    	}
    }

     

     

    사용자에 주문에 따라 해당 요소를 생성하고 제공하는 클래스를 이와 같이 만들 수 있다.

    ShipFactory를 보면 배의 종류에 따라 코드가 한 줄씩 추가되어있는 것을 볼 수 있는데, 이처럼 종류가 증가면 계속해서 기존 코드가 변경된다.

    즉, 기능이 추가됨에 따라 기존 코드를 계속 건들기 때문에 변경 과정에서 잘 작동하던 기존 기능까지 문제가 발생할 여지가 있어 OCP를 지키지 않는 코드가 된다.

     

    따라서, 배를 생성하는 interface를 하나 선언해 공통 모듈을 구성하고, 각각의 배를 생성하는 경우 해당 인터페이스를 사용하도록 코드를 변경한다. (ShipFactory 코드에 주석으로 써둔 번호와 변경된 코드 비교)

     

    -> 기존 ShipFactory는 WhiteShipFactory로 이름 변경

     

     

    ShipFactory.java (interface, default: java8 이상, private: java 9 이상)

    public interface ShipFactory {
    
    	default Ship orderShip(String name, String email) {
    		 validate(name, email);
    		 prepareFor(name);
    
    		 Ship ship = createShip();
    		 sendEmailTo(email, ship);
    		 
    		 return ship;
    	}
    
    	private void validate(String name, String email) {			// 1
    		if(name == null || name.isEmpty()) {
    			throw new IllegalArgumentException("배 이름을 지어주세요.");
    		}
    
    		if(email == null || email.isEmpty()) {
    			throw new IllegalArgumentException("연락처를 남겨주세요.");
    		}
    	}
    
    	Ship createShip();							// 4
    
    	private void prepareFor(String name) {					// 2
    		System.out.println(name + " 만들 준비 중");
    	}
    
    	private void sendEmailTo(String email, Ship ship) {			// 3
    		System.out.println(ship.getName() + " 다 만들었습니다.");
    	}
    }

     

     

    인터페이스를 이렇게 구성할 수 있고, 이를 WhiteShipFactory에서 구현한다.

     

    WhiteShipFactory.java

    public class WhiteShipFactory implements ShipFactory {
    
    	@Override
    	public Ship createShip() {
    		return new WhiteShip();
    	}
    }

     

    추가로, Ship을 상속받는 WhiteShip 객체를 생성해 WhiteShip 만의 데이터를 셋팅하고 리턴하면 배를 생성하는 과정이 끝난다.

     

    WhiteShip.java

    public class WhiteShip extends Ship{
    	public WhiteShip() {
    
    		setName("whiteShip");
    		setLogo("!!");
    		setColor("white");
    
    	}
    }

     

    Client.java

    public class Client {
    
    	public static void main(String[] args) {
    
    		Client client = new Client();
    
    		Ship whiteShip = new WhiteShipFactory().orderShip("whiteShip", "gmail");
    		System.out.println(whiteShip);
    
    //		Ship blackShip = WhiteShipFactory.orderShip("blackShip", "naver");
    //		System.out.println(blackShip);
    	}
    }

     

    결과

    그림 1. Whiteship 생성 결과

     

    기존과 동일하게 잘 만들어졌고, 이전에 코드 변경의 문제점을 방지하며 Blackship도 생성이 가능한지 확인한다.

    제품이 추가되므로 BlackShip 객체를 생성해 팩토리에서 만든 후 반환해주면 된다.

     

    BlackShip.java

    public class BlackShip extends Ship{
    
    	public BlackShip() {
    
    		setName("blackShip");
    		setLogo("??");
    		setColor("black");
    
    	}
    
    }

     

    BlackShipFactory.java

    public class BlackShipFactory implements ShipFactory{
    
    	@Override
    	public Ship createShip() {
    		return new BlackShip();
    	}
    }

     

    Client.java

    public class Client {
    
    	public static void main(String[] args) {
    
    		Client client = new Client();
    
    		Ship whiteShip = new WhiteShipFactory().orderShip("whiteShip", "gmail");
    		System.out.println(whiteShip);
    
    		Ship blackShip = new BlackShipFactory().orderShip("blackShip", "naver");
    		System.out.println(blackShip);
    	}
    }

     

    결과

    그림 2. Blackship 추가 결과

     

     

    조금 아쉬운 점은 Client 코드 또한 변경되고 있는데, 이를 막기 위해 의존성 주입과 같은 코드를 통해 수정해보자.

     

    Client.java

    public class Client {
    
    	public static void main(String[] args) {
    
    		Client client = new Client();
    
    		client.print(new WhiteShipFactory(), "whiteShip", "gmail");
    		client.print(new BlackShipFactory(), "blackShip", "naver");
    	}
    
    	private void print(ShipFactory shipFactory, String name, String email) {
    		System.out.println(shipFactory.orderShip(name, email));
    	}
    }

     

    결과

    그림 3. Client 코드 변경을 최소화하는 방식 결과

     

     

     

    Abstract Factory?

    팩토리에서 인터페이스를 만들어 사용하는 코드(클라이언트 쪽 코드)에 초점을 둔 패턴

     

    필요성

    객체를 생성 할 때, 내부 구성의 요구사항에 맞춘 다양성을 구현시 기존 코드의 변경을 방지 (OCP)

     

     

    우선 구체적인 구성을 확장해보기 위해 기존 구성에서 아래의 클래스와 인터페이스를 추가한다.

     

    DefaultShipFactory.java

    public abstract class DefaultShipFactory implements ShipFactory {
    	@Override
    	public void sendEmailTo(String email, Ship ship) {
    		System.out.println(ship.getName() + " 다 만들었습니다.");
    	}
    }

     

    WhiteWheel.java

    public class WhiteWheel {
    }

     

    WhiteAnchor.java

    public class WhiteAnchor {
    }

     

     

    그리고 기존 Ship과 WhiteShipFactory를 아래와 같이 변경한다.

     

    Ship.java

    public class Ship {
    
    	private String name;
    
    	private String color;
    
    	private String logo;
    
    	private WhiteWheel wheel;
    
    	private WhiteAnchor anchor;
    
    	public void setWheel(WhiteWheel wheel) {
    		this.wheel = wheel;
    	}
    
    	public void setAnchor(WhiteAnchor anchor) {
    		this.anchor = anchor;
    	}
    
    	public WhiteWheel getWheel() {
    		return wheel;
    	}
    
    	public WhiteAnchor getAnchor() {
    		return anchor;
    	}
        
        ... 생략
    }

     

    WhiteShipFactory.java

    public class WhiteShipFactory extends DefaultShipFactory {
    
    	@Override
    	public Ship createShip() {
    		Ship ship = new Ship();
    
    		ship.setAnchor(new WhiteAnchor());
    		ship.setWheel(new WhiteWheel());
    
    		return ship;
    	}
    }

     

    여기서 각 부품(anchor, wheel)의 기능적 확장이 필요한 경우를 처리하기 위한 인터페이스를 선언한다.

     

    Anchor.java

    public interface Anchor {
    }

     

    Wheel.java

    public interface Wheel {
    }

     

     

    WhiteAnchor, WhiteWheel은 각 제품 군에 맞는 인터페이스를 구현하도록 코드를 변경한다.

     

    WhiteWheel.java

    public class WhiteWheel implements Wheel {
    }

     

    WhiteAnchor.java

    public class WhiteAnchor implements Anchor {
    }

     

     

    그리고 이를 부품으로써 한데 모아 처리 할 인터페이스도 선언하고, 실제로 사용할 클래스를 생성해 아래 인터페이스로 구현한다.

     

    ShipPartsFactory.java

    public interface ShipPartsFactory {
    
    	Anchor createAnchor();
    
    	Wheel createWheel();
    
    }

     

    WhiteshipPartsFactory.java

    public class WhiteShipPartsFactory implements ShipPartsFactory {
    	@Override
    	public Anchor createAnchor() {
    		return new WhiteAnchor();
    	}
    
    	@Override
    	public Wheel createWheel() {
    		return new WhiteWheel();
    	}
    }

     

    각 부품이 추상화 되었으니, Ship 클래스에서 각 부품도 추상화된 데이터를 가져오도록 변경한다.

     

    Ship.java

    public class Ship {
    
    	private String name;
    
    	private String color;
    
    	private String logo;
    
    	private Wheel wheel;
    
    	private Anchor anchor;
    
    	public void setWheel(Wheel wheel) {
    		this.wheel = wheel;
    	}
    
    	public void setAnchor(Anchor anchor) {
    		this.anchor = anchor;
    	}
    
    	public Wheel getWheel() {
    		return wheel;
    	}
    
    	public Anchor getAnchor() {
    		return anchor;
    	}
    
    	... 생략
    }

     

     

    추상화된 부품 의존성을 가져와 배를 생성하면 다음과 같다.

     

    WhiteShipFactory.java

    public class WhiteShipFactory extends DefaultShipFactory {
    
    	private ShipPartsFactory shipPartsFactory;
    
    	public WhiteShipFactory(ShipPartsFactory shipPartsFactory) {
    		this.shipPartsFactory = shipPartsFactory;
    	}
    
    	@Override
    	public Ship createShip() {
    		Ship ship = new Ship();
    
    		ship.setAnchor(shipPartsFactory.createAnchor());
    		ship.setWheel(shipPartsFactory.createWheel());
    
    		return ship;
    	}
    }

     

     

    이제 부품의 기능 확장을 위한 준비를 끝냈으니 향상된 부품을 적용하는 코드를 구성해보자. (확장)

     

    WhiteAnchorPro.java

    public class WhiteAnchorPro implements Anchor {
    }

     

    WhiteWheelPro.java

    public class WhiteWheelPro implements Wheel {
    }

     

    WhitePartProFactory.java

    public class WhitePartsProFactory implements ShipPartsFactory {
    	@Override
    	public Anchor createAnchor() {
    		return new WhiteAnchorPro();
    	}
    
    	@Override
    	public Wheel createWheel() {
    		return new WhiteWheelPro();
    	}
    }

     

     

    실제로 아래와 같이 사용한다면, 생성자 파라미터에 따라 구분해 부품을 가져올 수 있다.

     

    ShipInventory.java - WhiteShipPartsFactory

    public class ShipInventory {
    
    	public static void main(String[] args) {
    //		ShipFactory shipFactory = new WhiteShipFactory(new WhitePartsProFactory());
    		ShipFactory shipFactory = new WhiteShipFactory(new WhiteShipPartsFactory());
    
    		Ship ship = shipFactory.createShip();
    		System.out.println(ship.getWheel().getClass());		// WhiteWheel
    		System.out.println(ship.getAnchor().getClass());	// WhiteAnchor
    	}
    }

     

    ShipInventory.java - WhitePartsProFactory

    public class ShipInventory {
    
    	public static void main(String[] args) {
    		ShipFactory shipFactory = new WhiteShipFactory(new WhitePartsProFactory());
    //		ShipFactory shipFactory = new WhiteShipFactory(new WhiteShipPartsFactory());
    
    		Ship ship = shipFactory.createShip();
    		System.out.println(ship.getWheel().getClass());		// WhiteWheelPro
    		System.out.println(ship.getAnchor().getClass());	// WhiteAnchorPro
    	}
    }

     

     

     

    두 방식 모두 기능적 확장을 객체지향적으로 구현하는 방식이지만, 어떤 부분을 어떻게 하느냐에 달린 문제라 볼 수 있다.

    어떠 것이 초점을 맞춰 코드의 방향성을 잡는지 기억하자.

     

     

     

     

    참고 자료

    GoF 디자인 패턴 - 에릭 감마, 존 블리사이드스, 랄프 존슨, 리하르트 헬름

    코딩으로 학습하는 GoF의 디자인 패턴 - 백기선님

     

    반응형

    'CS > Design Pattern' 카테고리의 다른 글

    Singleton  (0) 2022.08.06

    댓글

Designed by minchoba.