SOLID

SOLID는 객체 지향 프로그래밍 및 설계(OOP)에서 좋은 소프트웨어를 만들기 위해 지켜야 하는 5가지 핵심 설계 원칙의 앞 글자를 딴 약어입니다. 로버트 C. 마틴(Uncle Bob)이 2000년대 초반에 정립한 이 원칙들의 핵심 목적은, 시간이 지나도 유지보수가 쉽고, 확장성이 높으며, 변화에 유연하게 대처할 수 있는 코드를 만드는 것입니다. SOLID를 구성하는 원칙은 SRP, OCP, LIP, ISP, DIP입니다.

SRP

Single Responsibility Principle로 단일 책임 원칙입니다. 하나의 클래스는 단 하나의 책임(변경 이유)만을 가져야 한다는 것인데, 실무에서는 오직 하나의 액터(Actor, 기능 변경을 요청하는 집단)에 대해서만 책임을 져야 한다로 적용하는게 자연스럽습니다.

SRP가 적용된 예는 다음과 같습니다.

// 1. 로깅 전담 클래스 (변경 이유: 로그 포맷 변경, 저장소 변경 등)
class Logger {
    public log(message: string): void {
        const timestamp = new Date().toISOString();
        console.log(`[LOG] [${timestamp}] - ${message}`);
    }
}

// 2. 인증 전담 클래스 (변경 이유: JWT 도입, OAuth2 도입 등)
class AuthService {
    public authenticate(token: string): boolean {
        console.log(`토큰 유효성을 검증합니다: ${token}`);
        return true;
    }
}

// 3. 사용자 정보 관리 전담 클래스 (변경 이유: 프로필 비즈니스 로직 변경)
// 의존성 주입(DI)을 통해 외부에서 로거를 주입받아 사용합니다.
class UserService {
    private logger: Logger;

    constructor(logger: Logger) {
        this.logger = logger;
    }

    public updateProfile(userId: string, data: any): void {
        console.log(`사용자 ${userId}의 프로필을 업데이트합니다.`);
        
        // 자신의 책임이 아닌 로그 출력은 주입받은 Logger 객체에게 위임합니다.
        this.logger.log(`프로필 업데이트 완료 (UserId: ${userId})`);
    }
}

function run() {
    const logger = new Logger();
    const authService = new AuthService();
    const userService = new UserService(logger);

    // 각자 맡은 책임만 수행
    if (authService.authenticate("valid-token")) {
        userService.updateProfile("user_123", { name: "Gemini" });
    }
}

run();

SRP가 적용될 경우 잇점을 생각해 보면… <클래스가 작고 단순해져 유닛 테스트 작성이 매우 수월해 지고 각 클래스는 본연의 임무에만 집집하므로 코드가 단단해 집니다. 단단하다는 의미는 오류 발생이 적다는 것입니다.

OCP

Open-Closed Principle으로 개방-폐쇠 원칙입니다. 확장에는 개방되어 있고 변경에는 폐쇠되어 있어야 한다는 원칙인데요. 변경에는 폐쇠되어 있다(Closed for modi)라는 것은 신규 기능 추가 시 다른 기능에 대한 코드 수정은 금지한다이고 확장에는 개방되어 있다(Open for extension)는 신규 기능 추가 시 제약 없이 코드 작성 가능해야 한다는 것입니다. SRP 적용을 위한 구체적인 도구는 인터페이스와 추상 클래스입니다.

OCP가 적용된 예는 다음과 같습니다.

// 공통 인터페이스 정의 (이 틀은 변하지 않습니다)
interface PaymentMethod {
    pay(): void;
}

// 각 결제 수단은 인터페이스를 상속받아 구체적인 로직을 구현합니다.
class KakaoPay implements PaymentMethod {
    public pay(): void {
        console.log("카카오페이로 안전하게 결제합니다.");
    }
}

class NaverPay implements PaymentMethod {
    public pay(): void {
        console.log("네이버페이로 안전하게 결제합니다.");
    }
}

// 💡 새로운 결제 수단이 추가되어도 기존 코드를 건드릴 필요 없이 파일만 새로 만들면 됩니다 (확장에 열림)
class TossPay implements PaymentMethod {
    public pay(): void {
        console.log("토스페이로 안전하게 결제합니다.");
    }
}

// 결제 매니저는 구체적인 클래스가 아닌 인터페이스(PaymentMethod)에만 의존합니다.
class PaymentManager {
    // 새로운 결제 방식이 아무리 늘어나도 이 메서드는 단 한 줄도 수정할 필요가 없습니다 (변경에 닫힘).
    public processPayment(paymentMethod: PaymentMethod): void {
        paymentMethod.pay();
    }
}

function runPaymentSystem() {
    const manager = new PaymentManager();

    const kakao = new KakaoPay();
    const toss = new TossPay(); // 새롭게 추가된 결제 수단

    manager.processPayment(kakao);
    manager.processPayment(toss); // 기존 결제 매니저 코드 수정 없이 그대로 확장 적용 가능
}

runPaymentSystem();

SRP의 장점을 생각해 보면… 새로운 기능을 추가할때 기존 코드 영향을 최소화 시키며 기능을 컴포넌트 단위로 개발하여 플러그인처럼 쉽게 추가하여 조립할 수 있다는 것입니다.

LSP

Liskov Substitution Principle으로 리스코프 치환 원칙입니다. OOP의 올바른 다형성 구현을 위한 상속 규칙을 정의한 것으로 서브 타입은 항상 슈퍼 타입을 대체할 수 있어야 한다와 자식 클래스는 부모 클래스의 추상 매서드만을 public으로 제공해야 한다는 것을 강조한 원칙입니다.

LSP가 적용된 예는 다음과 같습니다.

// 두 도형의 공통점은 '면적을 구할 수 있다'는 것뿐입니다.
interface Shape {
    getArea(): number;
}

// 직사각형은 자신의 성질에 맞게 구현합니다.
class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}

    public setWidth(width: number): void {
        this.width = width;
    }

    public setHeight(height: number): void {
        this.height = height;
    }

    public getArea(): number {
        return this.width * this.height;
    }
}

// 정사각형도 자신의 성질에 맞게 구현합니다. 직사각형을 상속받지 않습니다.
class Square implements Shape {
    constructor(private side: number) {}

    public setSide(side: number): void {
        this.side = side;
    }

    public getArea(): number {
        return this.side * this.side;
    }
}

// 클라이언트는 구체적인 가로/세로 수정에 의존하지 않고, 
// 오직 '추상화된 Shape 인터페이스의 getArea 규약'에만 의존합니다.
function printArea(shape: Shape) {
    console.log(`면적: ${shape.getArea()}`);
}

const myRect = new Rectangle(5, 4);
const mySquare = new Square(5);

printArea(myRect);   // 면적: 20 (의도대로 동작)
printArea(mySquare); // 면적: 25 (의도대로 동작)

LSP을 고려할 때 체크해야할 것을 고민해 보면… 먼저 서브 클래스가 슈퍼 클래스의 매서드를 오버라이딩할 때 의미를 왜곡하지 않는가? 그리고 서브 클래스가 슈퍼 클래스의 예외 규약(전제 조건)을 깨지 않는가? 입니다. 아울러 상속은 행동의 일치성이 보장될때 사용되는 것이지 언어적 직관이 아닙니다. 즉, 정사각형은 직사각형을 상속받을 수 있는가라는 질문에서 속성은 상속받는 대상이 아니므로 정사각형은 직사각형을 상속받기에는 적합하지 않다입니다.

ISP

Interface Segregation Principle로 인터페이스 분리 원칙입니다. 클라이언트는 자신이 사용하지 않는 매서드에 의존하도록 강제되어서는 안된다는 원칙으로 하나의 만능 인터페이스를 만들지 말고 목적에 따라 여러 개의 인터페이스로 쪼개라는 것입니다.

ISP가 적용된 예는 다음과 같습니다.

// 역할을 얇게 쪼갠 역할 인터페이스들
interface Printer {
    print(): void;
}

interface Scanner {
    scan(): void;
}

interface FaxMachine {
    fax(): void;
}

// 보급형 프린터는 오직 'Printer' 인터페이스만 구현합니다. (깔끔)
class EconomicPrinter implements Printer {
    public print(): void {
        console.log("흑백으로 문서를 출력합니다.");
    }
}

// 고급형 복합기는 필요한 인터페이스들을 다중 상속(구현)하여 조합합니다.
class AllInOnePrinter implements Printer, Scanner, FaxMachine {
    public print(): void { console.log("컬러로 문서를 출력합니다."); }
    public scan(): void { console.log("고화질로 스캔합니다."); }
    public fax(): void { console.log("팩스를 보냅니다."); }
}

// 클라이언트 함수도 거대한 인터페이스 전체에 의존하지 않고, 
// 자기가 딱 필요한 얇은 인터페이스에만 의존하게 됩니다.
class OfficeWorker {
    // 이 직원은 오직 스캔 기능만 필요로 합니다.
    public doScanJob(scanner: Scanner) {
        scanner.scan();
    }
}

const worker = new OfficeWorker();
const premiumMachine = new AllInOnePrinter();
const ecoMachine = new EconomicPrinter();

worker.doScanJob(premiumMachine); // 정상 작동

// 💡 애초에 스캔 기능이 없는 ecoMachine은 타입 검사 단계에서 걸러지므로 
// 컴파일 에러를 통해 잠재적 버그를 사전에 방지할 수 있습니다.
// worker.doScanJob(ecoMachine); // 컴파일 에러 발생!

ISP이 적용될 경우 장점을 생각해 보면… 인터페이스 별로 잘 나눠져 있다면 해당 인터페이스의 변경에 따른 구현 클래스 변경이 최소화된다는 것입니다. 참고로 SRP는 클래스에 대한 단일 책임을 강조한다면 ISP는 인터페이스의 단일 책임을 강조하는 원칙입니다.

이 원칙에 대한 체크 사항은 인터페이스를 구현하는 클래스에서 인터페이스의 매서드 중 빈칸 또는 예외로 처리하는 경우가 있다면 ISP 위반일 가능성이 높습니다.

DIP

Dependency Inversion Princlple로 의존 역전 원칙입니다. 고수준 모듈은 저수준 모듈으 구현에 의존해서는 안된다는 것으로 고수준이나 저수준 모두 별도의 추상화된 모듈에 의존해야 한다는 것입니다. 전통적인 모듈 방식의 개발은 고수준 모듈이 저수준 모듈에 의존했지만 DIP을 준수하면 저수준과 고수준 모듈 간의 의존은 제거되고 두 모듈이 별도의 추상화 모듈에 의존하게 됩니다. 즉 의존 방향 역전됩니다.

DIP가 적용된 예는 다음과 같습니다.

// 고수준 모듈과 저수준 모듈이 모두 의존할 중심 틀입니다.
interface MessageSender {
    sendMessage(message: string): void;
}

// [저수준 모듈] 새로운 알림 수단이 추가되어도 인터페이스만 맞춰서 만들면 됩니다.
class NaverSmsService implements MessageSender {
    public sendMessage(message: string): void {
        console.log(`[네이버 API] SMS 전송: ${message}`);
    }
}

// [저수준 모듈] 
class KakaoTalkService implements MessageSender {
    public sendMessage(message: string): void {
        console.log(`[카카오 API] 알림톡 전송: ${message}`);
    }
}

// [고수준 모듈]

class NotificationService {
    // 💡 구체적인 클래스가 아닌, 추상화된 인터페이스(MessageSender)에만 의존합니다.
    private messageSender: MessageSender;

    // 외부에서 의존성을 주입(Dependency Injection)받도록 설계합니다.
    constructor(messageSender: MessageSender) {
        this.messageSender = messageSender;
    }

    public sendNotification(message: string): void {
        this.messageSender.sendMessage(message);
    }
}

// 클라이언트 사용

function run() {
    // 1. 네이버 SMS를 사용하고 싶을 때
    const naverSms = new NaverSmsService();
    const service1 = new NotificationService(naverSms);
    service1.sendNotification("안녕하세요! 네이버 SMS입니다.");

    // 2. 카카오톡으로 수단을 바꾸고 싶을 때 (NotificationService의 코드는 전혀 변경 없음)
    const kakaoTalk = new KakaoTalkService();
    const service2 = new NotificationService(kakaoTalk);
    service2.sendNotification("안녕하세요! 카카오톡 알림입니다.");
}

run();

이 DIP 원칙의 장점을 생각해 보면… 비지니스 로직(고수준 모듈)의 변경 없이 저수준 모듈을 변경하기 용이해 집니다.

[GoF] Visitor 패턴

패턴명칭

Visitor

필요한 상황

데이터와 이 데이터의 처리를 분리하여 구현하고자 할때 사용되는 패턴입니다. 데이터는 Composite 패턴으로 구현되므로 집합을 구성하는 단일 요소 역시 집합으로 저장될 수 있습니다. 이러한 집합에 대한 집합으로 구성된 데이터를 처리하는 로직을 독립적으로 구현할 수 있습니다.

예제 코드

Visitor 인터페이스는 데이터를 처리하는 클래스가 구현해야할 공통 인터페이스입니다. 코드는 다음과 같습니다.

package tstThread;

public interface Visitor {
	void visit(Unit unit);
}

이 Visitor를 구현하는 클래스로는 SumVisitor, MaxVisitor과 위의 클래스 다이어그램에는 표시되어 있지 않지만 MinVisitor, AvgVisitor이 있습니다. 이 네 클래스는 각각 데이터의 총합 계산, 데이터 중 최대값 파악, 데이터 중 최소값 파악, 데이터의 평균값 계산입니다. 데이터는 Unit 인터페이스를 구현해야 하며 코드는 다음과 같습니다.

package tstThread;

public interface Unit {
	void accept(Visitor visitor);
}

이 Unit 인터페이스를 구현하는 Item에는 하나의 정수값이 저장되며 ItemList는 여러개의 Unit 객체를 담을 수 있습니다. 먼저 Item 클래스는 다음과 같습니다.

package tstThread;

public class Item implements Unit {
	private int value;
	
	public Item(int value) {
		this.value = value;
	}
	
	public int getValue() {
		return value;
	}
	
	@Override
	public void accept(Visitor visitor) {
		visitor.visit(this);
	}
}

ItemList 클래스는 다음과 같습니다.

package tstThread;

import java.util.ArrayList;
import java.util.Iterator;

public class ItemList implements Unit {
	private String name;
	
	private ArrayList<Unit> list = new ArrayList<Unit>();
	
	public ItemList(String name) {
		this.name = name;
	}
	
	public String getName() {
		return this.name;
	}
	
	public void add(Unit unit) {
		list.add(unit);
	}
	
	@Override
	public void accept(Visitor visitor) {
		Iterator<Unit> iter = list.iterator();
		
		while(iter.hasNext()) {
			Unit unit = iter.next();
			visitor.visit(unit);
		}
	}
}

이제 이러한 데이터를 처리하는 Visitor 인터페이스의 구현 클래스를 살펴보겠습니다. 먼저 SumVisitor 클래스입니다.

package tstThread;

public class SumVisitor implements Visitor {
	private int sum = 0;
	
	public int getValue() {
		return sum;
	}
	
	@Override
	public void visit(Unit unit) {
		if(unit instanceof Item) {
			sum += ((Item)unit).getValue();
		} else {
			unit.accept(this);			
		}
	}
}

다음은 MaxVisitor 클래스입니다.

package tstThread;

public class MaxVisitor implements Visitor {
	private int max = Integer.MIN_VALUE;
	private String name = null;
	private String visitedName = null;
	
	public int getValue() {
		return max;
	}
	
	public String getName() {
		return name;
	}
	
	@Override
	public void visit(Unit unit) {
		if(unit instanceof Item) {
			int value = ((Item)unit).getValue();
			if(value > max) {
				max = value;
				name = visitedName;
			}
		} else {
			visitedName = ((ItemList)unit).getName();
			unit.accept(this);			
		}
	}
}

다음은 MinVisitor 클래스입니다.

package tstThread;

public class MinVisitor implements Visitor {
	private int min = Integer.MAX_VALUE;
	private String name = null;
	private String visitedName = null;
	
	public int getValue() {
		return min;
	}
	
	public String getName() {
		return name;
	}
	
	@Override
	public void visit(Unit unit) {
		if(unit instanceof Item) {
			int value = ((Item)unit).getValue();
			if(value < min) {
				name = visitedName;
				min = value;
			}
		} else {
			visitedName = ((ItemList)unit).getName();
			unit.accept(this);			
		}
	}
}

다음은 AvgVisitor 클래스입니다.

package tstThread;

public class AvgVisitor implements Visitor {
	private int sum = 0;
	private int count = 0;
	public double getValue() {
		return sum / count;
	}
	
	@Override
	public void visit(Unit unit) {
		if(unit instanceof Item) {
			sum += ((Item)unit ).getValue();
			count++;
		} else {
			unit.accept(this);			
		}
	}
}

지금까지의 클래스를 사용하는 예제 코드는 다음과 같습니다.

package tstThread;

public class Main {
	public static void main(String[] args) {
		ItemList root = new ItemList("root");
		root.add(new Item(10));
		root.add(new Item(20));
		root.add(new Item(40));
		
		ItemList subList1 = new ItemList("sub1");
		subList1.add(new Item(5));
		subList1.add(new Item(16));
		subList1.add(new Item(36));
		
		ItemList subList2 = new ItemList("sub2");
		subList2.add(new Item(50));
		subList2.add(new Item(70));
		
		ItemList subList3 = new ItemList("sub2-sub");
		subList3.add(new Item(8));
		subList3.add(new Item(21));
		subList3.add(new Item(37));
		
		root.add(subList1);
		root.add(subList2);
		subList2.add(subList3);
		
		SumVisitor sum = new SumVisitor();
		root.accept(sum);
		System.out.println("Sum: " + sum.getValue());
		
		MaxVisitor max = new MaxVisitor();
		root.accept(max);
		System.out.println("Max: " + max.getValue() + " @" + max.getName());
		
		MinVisitor min = new MinVisitor();
		root.accept(min);
		System.out.println("Min: " + min.getValue() + " @" + min.getName());
		
		AvgVisitor avg = new AvgVisitor();
		root.accept(avg);
		System.out.println("Avg: " + avg.getValue());
	}
}

실행 결과는 다음과 같습니다.

Sum: 313
Max: 70 @sub2
Min: 5 @sub1
Avg: 28.0
이 글은 소프트웨어 설계의 기반이 되는 GoF의 디자인패턴에 대한 강의자료입니다. 완전한 실습을 위해 이 글에서 소개하는 클래스 다이어그램과 예제 코드는 완전하게 실행되도록 제공되지만, 상대적으로 예제 코드와 관련된 설명이 함축적으로 제공되고 있습니다. 이 글에 대해 궁금한 점이 있으면 댓글을 통해 남겨주시기 바랍니다.

[GoF] Interpreter 패턴

패턴명칭

Interpreter

필요한 상황

프로그램의 실행 상황을 제어할 수 있는 스크립트 언어를 지원할 수 있는 패턴이다.

예제 코드

어떤 객체를 상(Front), 하(Back), 좌(Left), 우(Right)로 이동시키는 명령어로써 각각 FRONT, BACK, LEFT, RIGHT를 사용하고 이러한 명령어들의 조합을 반복할 수 있는 LOOP 명령어를 사용할 수 있는 스크립트 언어를 해석하기 위한 예제이다. Context는 스크립트에 대한 문자열을 받아 처리하는 클래스이고, Expression은 명령어들의 해석하고 처리하기 위한 클래스들이 구현해야 하는 인터페이스이다. 먼저 Context 클래스는 다음과 같다.

package tstThread;

import java.util.StringTokenizer;

public class Context {
	private StringTokenizer tokenizer;
	private String currentKeyword;

	public Context(String script) {
		tokenizer = new StringTokenizer(script);
		readNextKeyword();
	}

	public String readNextKeyword() {
		if(tokenizer.hasMoreTokens()) {
			currentKeyword = tokenizer.nextToken();
		} else {
			currentKeyword = null;
		}
		
		return currentKeyword;
	}
	
	public String getCurrentKeyword() {
		return currentKeyword;
	}
}

Expression 인터페이스는 다음과 같다.

package tstThread;

public interface Expression {
	boolean parse(Context context);
	boolean run();
}

parse 매서드는 스크립트를 해석하고, run은 해석된 스크립트를 실제로 실행하는 매서드이다. 스크립트의 예제로 다음 문자열을 사용한다.

BEGIN FRONT LOOP 3 LOOP 2 RIGHT FRONT END LOOP 3 LEFT END BACK RIGHT END BACK END

스크립트는 BEGIN으로 시작해서 END로 끝나며, 반복문인 LOOP는 반복 회수로 시작해서 반복할 명령어들로 구성되고 END로 끝난다.

Expression 인터페이스를 구현하는 클래스들을 살펴보자. 먼저 스크립트의 시작을 해석하는 BeginExpression이다.

package tstThread;

public class BeginExpression implements Expression {
	private CommandListExpression expression;

	@Override
	public boolean parse(Context context) {
		if(checkValidKeyword(context.getCurrentKeyword())) {
			context.readNextKeyword();
			expression = new CommandListExpression();
			return expression.parse(context);
		} else {
			return false;
		}
	}

	public String toString() {
		return "BEGIN " + expression; 
	}

	@Override
	public boolean run() {
		return expression.run();
	}

	public static boolean checkValidKeyword(String keyword) {
		return keyword.equals("BEGIN");
	}
}

다음은 CommandListExpression 이다.

package tstThread;

import java.util.ArrayList;
import java.util.Iterator;

public class CommandListExpression implements Expression {
	private ArrayList<CommandExpression> commands = new ArrayList<CommandExpression>();
	
	@Override
	public boolean parse(Context context) {
		while(true) {
			String currentKeyword = context.getCurrentKeyword();
			if(currentKeyword == null) {
				return false;
			} else if(currentKeyword.equals("END")) {
				context.readNextKeyword();
				break;
			} else {
				CommandExpression command = null;
				
				if(LoopCommandExpression.checkValidKeyword(currentKeyword)) {
					command = new LoopCommandExpression(currentKeyword);
				} else if(ActionCommandExpression.checkValidKeyword(currentKeyword)) {
					command = new ActionCommandExpression(currentKeyword);
				}
				
				if(command != null) {
					if(command.parse(context)) {
						commands.add(command);
					} else {
						return false;
					}
				} else {
					return false;
				}
			}
		}
		
		return true;
	}

	public String toString() {
		return commands.toString();
	}

	@Override
	public boolean run() {
		Iterator<CommandExpression> iter = commands.iterator();
		
		while(iter.hasNext()) {
			boolean bOK = iter.next().run();
			if(!bOK) return false;
		}
		
		return true;
	}
}

CommandListExpression은 실제 실행이 가능한 LOOP나 FRONT, BACK, RIGHT, LEFT 명령어를 담을 담을 수 있는 CommandExpression의 파생클래스를 생성해 주는 책임을 진다. CommandExpression의 클래스는 다음과 같다.

package tstThread;

public abstract class CommandExpression implements Expression {
	protected String keyword;
	
	public CommandExpression(String keyword) {
		this.keyword = keyword;
	}
}

CommandExpression 추상 클래스를 상속받는 클래스들을 살펴보자. 먼저 LoopCommandExpression 클래스이다.

package tstThread;

public class LoopCommandExpression extends CommandExpression {
	private int count;
	private CommandListExpression expression;
	
	
	public LoopCommandExpression(String keyword) {
		super(keyword);
	}

	@Override
	public boolean parse(Context context) {
		if(!checkValidKeyword(keyword)) return false; 
		
		String countKeyword = context.readNextKeyword();
		if(countKeyword == null) return false;
		
		try {
			count = Integer.parseInt(countKeyword);
			expression = new CommandListExpression();
			
			if(context.readNextKeyword() == null) return false;
			
			return expression.parse(context);
		} catch(NumberFormatException e) {
			return false;
		}
	}

	public String toString() {
		return "LOOP(" + count + ") " + expression;
	}

	@Override
	public boolean run() {
		for(int i=0; i<count; i++) {
			if(!expression.run()) {
				return false;
			}
		}
		
		return true;
	}
	
	public static boolean checkValidKeyword(String keyword) {
		return keyword.equals("LOOP");
	}
}

다음은 ActionCommandExpression 클래스이다.

package tstThread;

public class ActionCommandExpression extends CommandExpression {
	public ActionCommandExpression(String keyword) {
		super(keyword);
	}

	@Override
	public boolean parse(Context context) {
		if(!checkValidKeyword(keyword)) return false;
		if(context.readNextKeyword() == null) return false;
				
		return true;
	}
	
	public String toString() {
		return keyword;
	}

	@Override
	public boolean run() {
		System.out.println("cmd: " + keyword);
		return true;
	}

	public static boolean checkValidKeyword(String keyword) {
		boolean bKeywordOk = keyword.equals("FRONT") || 
				keyword.equals("BACK") || keyword.equals("LEFT") || 
				keyword.equals("RIGHT");
		
		return bKeywordOk;
	}
}

지금까지의 클래스들을 사용하는 예제는 다음과 같다.

package tstThread;

public class Main {
	public static void main(String[] args) {
		//String script = "BEGIN FRONT LOOP 2 RIGHT FRONT LEFT LEFT BACK RIGHT END BACK END";
		//String script = "BEGIN FRONT LOOP 2 RIGHT FRONT LOOP 3 LEFT LEFT END BACK RIGHT END BACK END";
		String script = "BEGIN FRONT LOOP 3 LOOP 2 RIGHT FRONT END LOOP 3 LEFT END BACK RIGHT END BACK END";
		Context context = new Context(script);
		Expression expression = new BeginExpression();
		
		System.out.println(script);
		if(expression.parse(context)) {
			System.out.println(expression);
			expression.run();
		} else {
			System.out.println("Parsing error");
		}
	}
}

실행 결과는 다음과 같다.


BEGIN FRONT LOOP 3 LOOP 2 RIGHT FRONT END LOOP 3 LEFT END BACK RIGHT END BACK END
BEGIN [FRONT, LOOP(3) [LOOP(2) [RIGHT, FRONT], LOOP(3) [LEFT], BACK, RIGHT], BACK]
cmd: FRONT
cmd: RIGHT
cmd: FRONT
cmd: RIGHT
cmd: FRONT
cmd: LEFT
cmd: LEFT
cmd: LEFT
cmd: BACK
cmd: RIGHT
cmd: RIGHT
cmd: FRONT
cmd: RIGHT
cmd: FRONT
cmd: LEFT
cmd: LEFT
cmd: LEFT
cmd: BACK
cmd: RIGHT
cmd: RIGHT
cmd: FRONT
cmd: RIGHT
cmd: FRONT
cmd: LEFT
cmd: LEFT
cmd: LEFT
cmd: BACK
cmd: RIGHT
cmd: BACK

이 글은 소프트웨어 설계의 기반이 되는 GoF의 디자인패턴에 대한 강의자료입니다. 완전한 실습을 위해 이 글에서 소개하는 클래스 다이어그램과 예제 코드는 완전하게 실행되도록 제공되지만, 상대적으로 예제 코드와 관련된 설명이 함축적으로 제공되고 있습니다. 이 글에 대해 궁금한 점이 있으면 댓글을 통해 남겨주시기 바랍니다.

[GoF] Abstract Factory 패턴

패턴명칭

Abstract Factory

필요한 상황

어떤 기능 또는 출력결과를 생성할때 그 기능 또는 출력결과를 구성하는 각각의 요소를 생성하는 방법을 추상화할 수 있는 패턴이다.

예제 코드

Factory는 어떤 기능이나 출력결과를 생성하기 위해 구성되는 각각의 요소를 생성하는 구체적인 클래슬르 생성하는 클래스이다. Member와 Club 그리고 Page가 각각의 요소에 대한 추상 클래스이다. Item 추상 클래스는 Member와 Club을 같은 개념으로 다루기 위해 존재한다. 실제 기능 또는 출력결과를 생성하는 구체 클래스는 HtmlMember, HtmlClub, htmlPage 그리고 HtmlFactory이다.

Factory 클래스를 먼저 보자

package tstThread;

public abstract class Factory {
	public abstract Member createMember(String name, String duty, String email);
	public abstract Club createClub(String title);
	public abstract Page createPage(String title, String footer);
}

Factory 클래스는 Member, Club, Page 객체를 생성한다. Member와 Club은 동일한 개념으로 취급되기 위해 Item이라는 추상클래스를 상속받는데 다음과 같다.

package tstThread;

public abstract class Item {
	protected String name;
	
	public Item(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
	
	public abstract String getResult();
}

이 Item 클래스를 상속받는 Member 클래스는 다음과 같다.

package tstThread;

public abstract class Member extends Item {
	private String duty;
	private String email;
	
	public Member(String name, String duty, String email) {
		super(name);
		this.duty = duty;
		this.email = email;
	}
	
	public String getDuty() {
		return duty;
	}
	
	public String getEmail() {
		return email;
	}
}

Item을 상속받는 Club 클래스는 다음과 같다.

package tstThread;

import java.util.ArrayList;
import java.util.Iterator;

public abstract class Club extends Item {
	private ArrayList<Item> items = new ArrayList<Item>();
	public Club(String name) {
		super(name);
	}
	
	public void add(Item item) {
		items.add(item);
	}

	public Iterator getIterator() {
		return items.iterator();
	}
}

Page 클래스는 다음과 같다.

package tstThread;

import java.util.ArrayList;
import java.util.Iterator;

public abstract class Page {
	private String title;
	private String footer;	
	private ArrayList<Item> items = new ArrayList<Item>();
	
	public Page(String title, String footer) {
		this.title = title;
		this.footer = footer;
	}
	
	public void add(Item item) {
		items.add(item);
	}
	
	public String getTitle() {
		return this.title;
	}
	
	public String getFooter() {
		return this.footer;
	}
	
	public Iterator getIterator() {
		return items.iterator();
	}
	
	public abstract String getResult();
}

이제 구체적인 부품에 해당하는 클래스들을 살펴보자. 먼저 Member의 파생 클래스인 HtmlMember이다.

package tstThread;

public class HtmlMember extends Member {

	public HtmlMember(String name, String duty, String email) {
		super(name, duty, email);
	}

	@Override
	public String getResult() {
		StringBuilder sb = new StringBuilder();
		
		sb.append("
    "); sb.append("
  • Name: "); sb.append(this.getName()); sb.append("
  • "); sb.append("
      "); sb.append("
    • Duty: "); sb.append(this.getDuty()); sb.append("
    • "); sb.append("
    • Email: "); sb.append(this.getEmail()); sb.append("
    • "); sb.append("
    "); sb.append("
"); return sb.toString(); } }

다음은 Club의 파생 클래스인 HtmlClub이다.

package tstThread;

import java.util.Iterator;

public class HtmlClub extends Club {
	public HtmlClub(String name) {
		super(name);
	}

	@Override
	public String getResult() {
		StringBuilder sb = new StringBuilder();
		
		sb.append("
    "); sb.append("
  • Club Name: "); sb.append(getName()); sb.append("
  • "); Iterator iter = getIterator(); while(iter.hasNext()) { Item item = iter.next(); sb.append(item.getResult()); } sb.append("
"); return sb.toString(); } }

다음은 Page의 파생 클래스인 HtmlPage이다.

package tstThread;

import java.util.Iterator;

public class HtmlPage extends Page {
	public HtmlPage(String title, String footer) {
		super(title, footer);
	}

	@Override
	public String getResult() {
		StringBuilder sb = new StringBuilder();
		
		sb.append("");
		sb.append("

"); sb.append(getTitle()); sb.append("

"); Iterator iter = getIterator(); while(iter.hasNext()) { Item item = iter.next(); sb.append(item.getResult()); } sb.append("
"); sb.append(""); sb.append(getFooter()); sb.append(""); sb.append(""); return sb.toString(); } }

다음은 Factory의 파생 클래스인 HtmlFactory이다.

package tstThread;

public class HtmlFactory extends Factory {

	@Override
	public Member createMember(String name, String duty, String email) {
		return new HtmlMember(name, duty, email);
	}

	@Override
	public Club createClub(String title) {
		return new HtmlClub(title);
	}

	@Override
	public Page createPage(String title, String footer) {
		return new HtmlPage(title, footer);
	}
}

지금까지의 클래스들을 사용하는 예제 코드는 다음과 같다.

package tstThread;

public class Main {
	public static void main(String[] args) {
		Page page = new HtmlPage("OrangeTree", "since 2021");
		Factory factory = new HtmlFactory();
		
		Member ceo = factory.createMember("Jany", "CEO", "jany@orangetree.com");
		page.add(ceo);
		
		Club club1 = factory.createClub("GAME");
		Member mem1_club1 = factory.createMember("Toms", "Manager", "toms@orangetree.com");
		Member mem2_club1 = factory.createMember("Sujin", "Assist", "sujin@orangetree.com");
		
		club1.add(mem1_club1);
		club1.add(mem2_club1);
		page.add(club1);
		
		Club club2 = factory.createClub("STUDY");
		Member mem1_club2 = factory.createMember("Jack", "Manager", "jack@orangetree.com");
		Member mem2_club2 = factory.createMember("Robert", "Assist", "robert@orangetree.com");
		club2.add(mem1_club2);
		club2.add(mem2_club2);
		page.add(club2);

		System.out.println(page.getResult());
	}
}

실행 결과는 HTML 출력인데, 이를 웹브라우저에서 보면 다음과 같다.

이 글은 소프트웨어 설계의 기반이 되는 GoF의 디자인패턴에 대한 강의자료입니다. 완전한 실습을 위해 이 글에서 소개하는 클래스 다이어그램과 예제 코드는 완전하게 실행되도록 제공되지만, 상대적으로 예제 코드와 관련된 설명이 함축적으로 제공되고 있습니다. 이 글에 대해 궁금한 점이 있으면 댓글을 통해 남겨주시기 바랍니다.

[GoF] State 패턴

패턴명칭

State

필요한 상황

상태에 따라 실행되는 기능이 달라질때 유연하게 대응할 수 있는 패턴이다.

예제 코드

7일간 하루 하루에 대한 일정을 주사위를 던져 짝수날에는 놀고 홀수날에는 공부를 하는 시스템이다. 여기서 상태는 주사위가 짝수인지 홀수인지이다. 다양한 상태를 동일한 인터페이스로 다룰 수 있도록 State 인터페이스를 두고 이 State를 PlayState와 StudyState가 구현한다. 각각은 상태에 따른 놀기와 공부하기이다. Schedule는 상태를 변경하는 클래스이다. 먼저 State 인터페이스는 다음과 같다.

package tstThread;

public interface State {
	void morning();
	void afternoon();
	void night();
}

다음은 공부하기 상태에 대한 StudyState 클래스이다.

package tstThread;

public class StudyState implements State {
	@Override
	public void morning() {
		System.out.println("I'm studying the math.");
	}

	@Override
	public void afternoon() {
		System.out.println("I'm studying the programming.");
	}

	@Override
	public void night() {
		System.out.println("I'm studying the physics.");
	}
}

다음은 놀기 상태에 대한 PlayState 클래스이다.

package tstThread;

public class PlayState implements State {

	@Override
	public void morning() {
		System.out.println("I am playing the piano.");
	}

	@Override
	public void afternoon() {
		System.out.println("I am playing the starcraft game.");
	}

	@Override
	public void night() {
		System.out.println("I am listening the pop song.");
	}
}

다음은 일주일에 대한 시간 흐름을 제어하고 주사위를 던저 상태를 변경하는 Schedule 클래스이다.

package tstThread;

public class Schedule {
	private State state;
	
	public void setState(State state) {
		this.state = state;
	}
	
	public void doInTheMorning() {
		if(state != null) {
			System.out.print("[Morning] ");
			state.morning();
		}
	}
	
	public void doInTheAfternoon() {
		if(state != null) {
			System.out.print("[Afternoon] ");
			state.afternoon();
		}
	}
	
	public void doInTheNight() {
		if(state != null) {
			System.out.print("[Night] ");
			state.night();
		}
	}
}

지금까지의 클래스를 실행하는 코드는 다음과 같다.

package tstThread;

import java.util.Random;

public class Main {
	private static Random dice = new Random();
	public static void main(String[] args) {
		Schedule schedule = new Schedule();
		
		String days[] = { "Sunday", "Monday", "Tuesday", "Thursday", "Friday", "Saturday" };
		for(int nDay=0; nDay<6; nDay++) {
			int nDice = dice.nextInt(7);
			
			if(nDice % 2 == 0) {
				schedule.setState(new PlayState());
			} else {
				schedule.setState(new StudyState());
			}
			
			System.out.println();
			System.out.println("# " + days[nDay]);
			schedule.doInTheMorning();
			schedule.doInTheAfternoon();
			schedule.doInTheNight();
		}
	}
}

실행결과는 다음과 같다.

# Sunday
[Morning] I am playing the piano.
[Afternoon] I am playing the starcraft game.
[Night] I am listening the pop song.

# Monday
[Morning] I am playing the piano.
[Afternoon] I am playing the starcraft game.
[Night] I am listening the pop song.

# Tuesday
[Morning] I am playing the piano.
[Afternoon] I am playing the starcraft game.
[Night] I am listening the pop song.

# Thursday
[Morning] I am playing the piano.
[Afternoon] I am playing the starcraft game.
[Night] I am listening the pop song.

# Friday
[Morning] I'm studying the math.
[Afternoon] I'm studying the programming.
[Night] I'm studying the physics.

# Saturday
[Morning] I'm studying the math.
[Afternoon] I'm studying the programming.
[Night] I'm studying the physics.
이 글은 소프트웨어 설계의 기반이 되는 GoF의 디자인패턴에 대한 강의자료입니다. 완전한 실습을 위해 이 글에서 소개하는 클래스 다이어그램과 예제 코드는 완전하게 실행되도록 제공되지만, 상대적으로 예제 코드와 관련된 설명이 함축적으로 제공되고 있습니다. 이 글에 대해 궁금한 점이 있으면 댓글을 통해 남겨주시기 바랍니다.