[GoF] Facade 패턴

패턴명칭

Facade

필요한 상황

어떤 기능을 실행할때 다양한 타입의 객체들의 함수를 정확한 순서로 호출해야 할때 발생하는 복잡도를 단순화시켜주는 패턴이다.

예제 코드

데이터베이스부터 사용자의 정보를 가져오기 전에 먼저 캐쉬를 통해 정보를 가져오고, 만약 캐쉬에 없다면 데이터베이스로부터 사용자 정보를 가져와 캐쉬에 저장한다. 그 다음은 이 정보를 원하는 형태의 메세지로 출력하기 위한 기능을 개발한다고 하자. 이 기능에 대해서 사용자 정보는 Row 클래스, 데이터베이스는 DBMS 클래스, 캐쉬는 Cache 클래스, 메세지 출력을 위한 내용의 구성은 Message 클래스가 담당한다. 이러한 클래스들 간의 정확한 관계와 매서드들간의 복잡한 관계를 Facade 클래스를 도입하여 단순화 시킨다. 먼저 사용자 정보는 Row는 다음과 같다.

package pattern;

public class Row {
	private String name;
	private String birthday;
	private String email;
	
	public Row(String name, String birthday, String email) {
		this.name = name;
		this.birthday = birthday;
		this.email = email;
	}
	
	public String getName() {
		return name;
	}
	
	public String getBirthday() {
		return birthday;
	}
	
	public String getEmail() {
		return email;
	}
}

DBMS 클래스는 다음과 같다.

package pattern;

import java.util.HashMap;

public class DBMS {
	private HashMap<String, Row> db;
	
	public DBMS() {
		db = new HashMap<String, Row>();
		
		db.put("jane", new Row("Jane", "1990-02-14", "jane09@geosee.co.kr"));
		db.put("robert", new Row("Robert", "1979-11-05", "nice@googl.com"));
		db.put("dorosh", new Row("Dorosh", "1985-08-21", "doshdo@nave.net"));
	}
	
	public Row query(String name) {
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		return db.get(name.toLowerCase());
	}
}

Cache 클래스는 다음과 같다.

package pattern;

import java.util.HashMap;

public class Cache {
    private HashMap<String, Row> cache;
    
    public Cache() {
        cache = new HashMap<String, Row>();
    }
    
    public void put(Row row) {
        cache.put(row.getName(), row);
    }

    public Row get(String name) {
        return cache.get(name);
    }
}

Message 클래스는 다음과 같다.

package pattern;

public class Message {
	private Row row;
	
	public Message(Row row) {
		this.row = row;
	}
	
	public String makeName() {
		return "Name: \"" + row.getName() + "\"";
	}
	
	public String makeBirthday() {
		return "Birthday: " + row.getBirthday();
	}
	
	public String makeEmail() {
		return "Email: " + row.getEmail();
	}
}

이제 위의 클래스들에 대한 상호간의 API 호출을 단순화시킨 Facade 클래스는 다음과 같다.

package pattern;

public class Facade {
	public void run(DBMS dbms, Cache cache, String name) {
		Row row = cache.get(name);
		if(row == null) {
			row = dbms.query(name);
			if(row != null) {
				cache.put(row);
			}
		}
		
		if(row != null) {
			Message message = new Message(row);
			System.out.println(message.makeName());
			System.out.println(message.makeBirthday());
			System.out.println(message.makeEmail());
		} else {
			System.out.println(name + " is not exists.");
		}
	}
}

실제 사용 예제 코드는 다음과 같다.

package pattern;

public class Main {
	public static void main(String[] args) {
		DBMS dbms = new DBMS();
		Cache cache = new Cache();
		
		String name = "Dorosh";
		
		Facade facade = new Facade();
		facade.run(dbms, cache, name);
	}
}

실행 결과는 다음과 같다.

Name: "Dorosh"
Birthday: 1985-08-21
Email: doshdo@nave.net

Facade 패턴의 도입으로 복잡한 클래스간의 함수 호출이 단순화되고 재사용성이 높아진다.

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

[GoF] Prototype 패턴

패턴명칭

Prototype

필요한 상황

어떤 객체가 생성되고, 그 객체의 상태값이 변경된 후 그 객체와 동일한 상태의 또 다른 복사본을 생성하고자 할때 사용할 수 있는 패턴이다. 복사본의 상태의 변경으로인해 원본의 상태는 변경되지 않는다.

예제 코드

Point와 Circle 그리고 Line은 Prototype 인터페이스를 구현하는 클래스이다. Prototype을 구현한 클래스는 자신의 상태값을 완전하게 복사할 책임을 갖는다. Prototype의 코드는 다음과 같다.

package tstThread;

public interface Prototype {
	Prototype clone();
}

Point 클래스는 다음과 같다.

package tstThread;

public class Point implements Prototype {
	private int x;
	private int y;
	
	public Point setX(int x) {
		this.x = x;
		return this;
	}
	
	public Point setY(int y) {
		this.y = y;
		return this;
	}
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
	
	@Override
	public Prototype clone() {
		Point newPoint = new Point();
		newPoint.x = this.x;
		newPoint.y = this.y;
		
		return newPoint;
	}
	
	@Override
	public String toString() {
		return "POINT(" + x + " " + y + ")";
	}
}

Circle 클래스는 다음과 같다.

package tstThread;

public class Circle implements Prototype {
	private Point center;
	private int radius;
	
	public Circle setCenter(Point center) {
		this.center = (Point)center.clone();
		return this;
	}
	
	public Circle setRadius(int radius) {
		this.radius = radius;
		return this;
	}
	
	public Point getCenter() {
		return (Point)center.clone();
	}
	
	public int getRadius() {
		return radius;
	}
	
	@Override
	public Prototype clone() {
		Circle newCircle = new Circle();
		
		newCircle.center = (Point)center.clone();
		newCircle.radius = this.radius;
		
		return newCircle;
	}

	@Override
	public String toString() {
		return "CIRCLE(center: " + center + ", radius: " + radius + ")";
	}
}

Line 클래스는 다음과 같다.

package tstThread;

public class Line implements Prototype {
	private Point startPt;
	private Point endPt;
	
	public Line setStartPoint(Point pt) {
		this.startPt = (Point)pt.clone();
		return this;
	}
	
	public Point getStartPoint() {
		return (Point)startPt.clone();
	}
	
	public Line setEndPoint(Point pt) {
		this.endPt = (Point)pt.clone();
		return this;
	}
	
	public Point getEndPoint() {
		return (Point)endPt.clone();
	}
	
	@Override
	public Prototype clone() {
		Line newLine = new Line();
		
		newLine.startPt = (Point)startPt.clone();
		newLine.endPt = (Point)endPt.clone();
		
		return newLine;
	}
	
	@Override
	public String toString() {
		return "LINE(" + startPt + ", " + endPt + ")";
	}
}

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

package tstThread;

import java.io.UnsupportedEncodingException;

public class Main {
	public static void main(String[] args) throws UnsupportedEncodingException {
		Point center = new Point();
		center.setX(100);
		center.setY(200);
		
		Circle circle = new Circle();
		circle.setCenter(center);
		circle.setRadius(50);
		
		Point startPt = new Point();
		startPt.setX(10);
		startPt.setY(20);
		
		Point endPt = new Point();
		endPt.setX(40);
		endPt.setY(60);
		
		Line line = new Line();
		line.setStartPoint(startPt);
		line.setEndPoint(endPt);
		
		System.out.println(circle);
		System.out.println(line);
		
		Circle otherCircle = (Circle)circle.clone();
		Line otherLine = (Line)line.clone();
		
		System.out.println(otherCircle);
		System.out.println(otherLine);
	}
}

실행 결과는 다음과 같다.

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

[GoF] Chain of Responsibility 패턴

패턴명칭

Chain of Responsibility

필요한 상황

어떤 기능을 구성하는 순차적인 처리들을 클래스별로 나눠 구현하여 수행할 수 있는 패턴이다.

예제 코드

URL을 구성하는 각 부분에 대한 처리를 수행하는 예제이다. 예를 들어http://127.0.0.1:8080과 같은 경우 URL을 구성하는 Protocol, Port, Address를 출력하는 기능을 각각 ProtocolParser, PortParser, AddressParser 클래스가 처리한다. 연속된 처리를 위해 Parser 클래스를 부모 클래스로 둔다. 먼저 부모 클래스인 Parser는 다음과 같다.

package pattern;

public abstract class Parser {
	protected Parser nextParser = null;

	public Parser setNext(Parser nextParser) {
		this.nextParser = nextParser;
		return this.nextParser;
	}
	
	public void run(String url) {
		process(url);
		if(nextParser != null) nextParser.run(url); 
	}
	
	protected abstract void process(String url);
}

URL 문자열에 대한 구체적인 처리는 process 추상 함수에서 수행한다. 먼저 Protocol을 처리하는 ProtocolParser 클래스는 다음과 같다.

package pattern;

public class ProtocolParser extends Parser {
	@Override
	public void process(String url) {
		int index = url.indexOf("://");
		if(index != -1) {
			System.out.println("PROTOCOL: " + url.substring(0, index));
		} else {
			System.out.println("NO PROTOCOL");
		}
	}
}

PortParser 클래스는 다음과 같다.

package pattern;

public class PortParser extends Parser {
	@Override
	public void process(String url) {
		int index = url.lastIndexOf(":");
		if(index != -1) {
			String strPort = url.substring(index+1);
			try {
				int port = Integer.parseInt(strPort);
				System.out.println("PORT: " + port);
				return;
			} catch(NumberFormatException e) {
				
			}
		}
		
		System.out.println("NO PORT");
	}
}

AddressParser 클래스는 다음과 같다.

package pattern;

public class AddressParser extends Parser {
	@Override
	public void process(String url) {
		int startIndex = url.indexOf("://");
		int lastIndex = url.lastIndexOf(":");
		
		if(startIndex == -1) {
			if(lastIndex == -1) {
				System.out.println("ADDRESS: " + url);
			} else {
				System.out.println("ADDRESS: " + url.substring(0, lastIndex));
			}
		} else if(startIndex != lastIndex) {
			System.out.println("ADDRESS: " + url.substring(startIndex+3, lastIndex));
		} else if(startIndex == lastIndex) {
			System.out.println("ADDRESS: " + url.substring(startIndex+3));
		} else {
			System.out.println("NO ADDRESS");
		}
	}
}

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

package pattern;

public class Main {
	public static void main(String[] args) {
		Parser parser = new ProtocolParser();
		Parser parser1 = new PortParser();
		Parser parser2 = new AddressParser();
		
		parser.setNext(parser1).setNext(parser2);

		String url = "http://127.0.0.1:8080";
		System.out.println("INPUT: " + url);
		parser.run(url);
		
		url = "127.0.0.1:8080";
		System.out.println("\nINPUT: " + url);
		parser.run(url);
		
		url = "http://127.0.0.1";
		System.out.println("\nINPUT: " + url);
		parser.run(url);
		
		url = "127.0.0.1";
		System.out.println("\nINPUT: " + url);
		parser.run(url);
		
	}
}

출력 결과는 다음과 같다.

INPUT: http://127.0.0.1:8080
PROTOCOL: http
PORT: 8080
ADDRESS: 127.0.0.1

INPUT: 127.0.0.1:8080
NO PROTOCOL
PORT: 8080
ADDRESS: 127.0.0.1

INPUT: http://127.0.0.1
PROTOCOL: http
NO PORT
ADDRESS: 127.0.0.1

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

[GoF] Decorator 패턴

패턴명칭

Decorator

필요한 상황

어떤 기능을 기준으로 다른 여러가지 종속적인 기능들을 효과적으로 추가할 수 있는 패턴이다.

예제 코드

Item은 기준이 되는 기능과 장식처럼 추가할 수 있는 종속적인 기능을 동일한 개념으로 간주하기 위한 클래스이다. StringItem과 MultiStringItem이 기준이 되는 기능이고, Decorator가 종속적인 기능이다. 종속적인 기능은 이 Decorator를 상속받아 효율적으로 추가할 수 있다. 먼저 Item 클래스는 다음과 같다.

package tstThread;

import java.io.UnsupportedEncodingException;

public abstract class Item {
	public abstract int getHeight();
	public abstract int getMaxWidth() throws UnsupportedEncodingException;
	public abstract int getWidth(int index) throws UnsupportedEncodingException;
	public abstract String getContent(int index) throws UnsupportedEncodingException;
	
	public void print() throws UnsupportedEncodingException {
		int height = getHeight();
		for(int i=0; i<height; i++) {
			String content = getContent(i);
			System.out.println(content);
		}
	}
}

기준이 되는 기본 기능인 StringItem은 하나의 문자열을 처리하며 다음과 같다.

package tstThread;

import java.io.UnsupportedEncodingException;

public class StringItem extends Item {
	private String content;
	
	public StringItem(String content) {
		this.content = content;
	}
	
	@Override
	public int getHeight() {
		return 1;
	}

	@Override
	public int getMaxWidth() throws UnsupportedEncodingException {
		return content.getBytes("euc-kr").length;
	}

	@Override
	public String getContent(int index) {
		return content;
	}

	@Override
	public int getWidth(int index) throws UnsupportedEncodingException {
		return getMaxWidth();
	}
}

마찬가지로 기본이 되는 기능인 MultiStringItem은 StringItem을 여러개 가질 수 있어서 여러개의 문자열을 처리할 수 있으며 코드는 다음과 같다.

package tstThread;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;

public class MultiStringItem extends Item {
	private ArrayList<StringItem> strings = new ArrayList<StringItem>();
	
	public MultiStringItem() {}
	
	@Override
	public int getHeight() {
		return strings.size();
	}

	@Override
	public int getMaxWidth() throws UnsupportedEncodingException {
		Iterator<StringItem> iter = strings.iterator();
		int maxWidth = 0;
		
		while(iter.hasNext()) {
			Item stringItem = iter.next();
			int width = stringItem.getMaxWidth();
			
			if(width > maxWidth) maxWidth = width;
		}
		
		return maxWidth;
	}

	@Override
	public String getContent(int index) throws UnsupportedEncodingException {
		StringItem item = strings.get(index);
		return item.getContent(index);
	}
	
	public void addItem(StringItem item) {
		strings.add(item);
	}

	@Override
	public int getWidth(int index) throws UnsupportedEncodingException {
		return strings.get(index).getMaxWidth();
	}
}

기본 기능에 장식처럼 또 다른 기능을 위한 Decorator 클래스는 다음과 같다.

package tstThread;

public abstract class Decorator extends Item {
	protected Item item;
	public Decorator(Item item) {
		this.item = item;
	}
}

Decorator의 item 필드가 장식할 대상이 되는 객체이다. 즉 Decorator는 기본 기능 뿐만 아니라 Decorator에 대한 보조 기능도 장식할 수 있다. 이제 이 Decorator에 대한 구체적인 파생 클래스들을 보자. 먼저 SideDecorator이다.

package tstThread;

import java.io.UnsupportedEncodingException;

public class SideDecorator extends Decorator {
	private Character ch;
	
	public SideDecorator(Item item, Character ch) {
		super(item);
		this.ch = ch;
	}

	@Override
	public int getMaxWidth() throws UnsupportedEncodingException {
		return item.getMaxWidth() + 2;
	}

	@Override
	public String getContent(int index) throws UnsupportedEncodingException {
		return ch + item.getContent(index) + ch;
	}

	@Override
	public int getHeight() {
		return item.getHeight();
	}

	@Override
	public int getWidth(int index) throws UnsupportedEncodingException {
		return item.getWidth(index) + 2;
	}
}

다음은 LineNumberDecorator이다.

package tstThread;

import java.io.UnsupportedEncodingException;

public class LineNumberDecorator extends Decorator {

	public LineNumberDecorator(Item item) {
		super(item);
	}

	@Override
	public int getHeight() {
		return item.getHeight();
	}

	@Override
	public int getMaxWidth() throws UnsupportedEncodingException {
		return item.getMaxWidth() + 4;
	}

	@Override
	public int getWidth(int index) throws UnsupportedEncodingException {
		return item.getWidth(index) + 4;
	}

	@Override
	public String getContent(int index) throws UnsupportedEncodingException {
		return String.format("%02d", index) + ": " + item.getContent(index);
	}
}

BoxDecorator는 다음과 같다.

package tstThread;

import java.io.UnsupportedEncodingException;

public class BoxDecorator extends Decorator {
	public BoxDecorator(Item item) {
		super(item);
	}

	@Override
	public int getHeight() {
		return item.getHeight()+2;
	}

	@Override
	public int getMaxWidth() throws UnsupportedEncodingException {
		return item.getMaxWidth() + 2;
	}

	@Override
	public int getWidth(int index) throws UnsupportedEncodingException {
		return item.getWidth(index) + 2;
	}

	@Override
	public String getContent(int index) throws UnsupportedEncodingException {
		int maxWidth = this.getMaxWidth();
		if(index == 0 || index == getHeight()-1) {
			StringBuilder sb = new StringBuilder();
			sb.append('+');
			for(int i=0; i<maxWidth-2; i++) {
				sb.append('-');
			}
			sb.append('+');
			return sb.toString();
		} else {
			return '|' + item.getContent(index-1) + makeTailString(maxWidth - getWidth(index-1));
		}
	}
	
	private String makeTailString(int count) {
		StringBuilder sb = new StringBuilder();
		for(int i=0; i<count; i++) {
			sb.append(' ');
		}
		sb.append('|');
		
		return sb.toString();
	}
}

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

package tstThread;

import java.io.UnsupportedEncodingException;

public class Main {
	public static void main(String[] args) throws UnsupportedEncodingException {
		StringItem item1 = new StringItem("안녕하십니까?");
		StringItem item2 = new StringItem("제 이름은 김형준입니다.");
		StringItem item3 = new StringItem("반갑습니다.");
		StringItem item4 = new StringItem("디자인패턴의 세계에 푹 빠져보시죠.");

		MultiStringItem multiStringItem = new MultiStringItem();
		multiStringItem.addItem(item1);
		multiStringItem.addItem(item2);
		multiStringItem.addItem(item3);
		multiStringItem.addItem(item4);

		Decorator sideDecorator = new SideDecorator(multiStringItem, '\"');
		Decorator lineNumberDecorator = new LineNumberDecorator(sideDecorator);
		Decorator boxDecorator = new BoxDecorator(lineNumberDecorator);
						
		boxDecorator.print();
	}
}

기본 기능인 multiStringItem에 3개의 장식인 sideDecorator, lineNumberDecorator, boxDecorator을 적용한 예로 실행 결과는 다음과 같다.

다음은 기본 기능인 multiStringItem은 그대로 사용하고 3개의 장식의 순서를 달리한 예이다.

package tstThread;

import java.io.UnsupportedEncodingException;

public class Main {
	public static void main(String[] args) throws UnsupportedEncodingException {
		StringItem item1 = new StringItem("안녕하십니까?");
		StringItem item2 = new StringItem("제 이름은 김형준입니다.");
		StringItem item3 = new StringItem("반갑습니다.");
		StringItem item4 = new StringItem("디자인패턴의 세계에 푹 빠져보시죠.");

		MultiStringItem multiStringItem = new MultiStringItem();
		multiStringItem.addItem(item1);
		multiStringItem.addItem(item2);
		multiStringItem.addItem(item3);
		multiStringItem.addItem(item4);

		Decorator boxDecorator = new BoxDecorator(multiStringItem);
		Decorator lineNumberDecorator = new LineNumberDecorator(boxDecorator);		
		Decorator sideDecorator = new SideDecorator(lineNumberDecorator, '\"');
		
		sideDecorator.print();
	}
}

실행 결과는 다음과 같다.

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

[GoF] Proxy 패턴

패턴명칭

Proxy

필요한 상황

어떤 기능을 바로 실행하기보다는 모았다가 한번에 실행하도록 하는 것이 효율적인 경우에 사용할 수 있는 패턴이다. 또는 어떤 입력값에 대한 결과를 별도로 저장해 뒀다가 동일한 입력값에 대한 결과를 다시 요청받았을때 미리 저장해둔 결과를 전달해 속도 향상을 꽤할 수 있는 패턴이다.

예제 코드

ScreenDisplay 클래스는 입력 문자열을 화면(Screen)에 출력한다. BufferDisplay는 입력 문자열을 바로 출력하지 않고 일단 모은 후에 정해진 개수만큼 모이면 ScreenDisplay 클래스의 객체를 이용해 화면에 출력한다. Display는 출력한라는 인터페이스를 제공하며 이를 구현하는 BufferDisplay와 ScreenDisplay는 사용자의 입장에서 Display로 동일하게 다룰 수 있다.

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

package pattern;

public interface Display {
	void print(String content);
}

ScreenDisplay 클래스는 다음과 같다.

package pattern;

public class ScreenDisplay implements Display {
	@Override
	public void print(String content) {
		System.out.println(content);
	}
}

BufferDisplay 클래스는 다음과 같다.

package pattern;

import java.util.Iterator;
import java.util.LinkedList;

public class BufferDisplay implements Display {
	private LinkedList<String> contents = new LinkedList<String>();
	private ScreenDisplay screen = new ScreenDisplay();
	private int bufferSize;
	
	public BufferDisplay(int bufferSize) {
		this.bufferSize = bufferSize;
	}
	
	@Override
	public void print(String content) {
		contents.add(content);
		
		if(contents.size() == bufferSize) {
			Iterator<String> iter = contents.iterator();
			while(iter.hasNext()) {
				String bufferedContent = iter.next();
				screen.print(bufferedContent);
			}
		}
	}
}

지금까지 정의된 클래스를 사용하는 코드가 정의된 User 클래스는 다음과 같다.

package pattern;

public class User {
	public static void main(String[] args) {
		Display display = new BufferDisplay(3);
		
		display.print("Hello.");
		display.print("My name is Endru.");
		display.print("What's you name?");
		display.print("I am 10 years old.");
		display.print("How old are you?");
	}
}

실행 결과는 다음과 같다.

Hello.
My name is Endru.
What's you name?

User 클래스의 코드를 보면 총 5개의 문자열을 출력하라고 했으나, 실제로는 3개의 문자열만 출력된다. 이는 BufferDisplay가 5개의 문자열이 모아지는 경우에만 실제로 화면에 출력하기 때문이다.

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