본문 바로가기

객체지향

[POCU-OOP] 디자인 패턴

 이 포스팅은 POCU 아카데미의 '개체지향 프로그래밍 및 설계(Java)' 동영상 강의를 학습하고 정리한 내용입니다.

제 의견이 추가되어 강의 내용과 포스팅 내용이 일치하지 않을 수 있다는 점 미리 말씀드립니다.

 

 


 

 

 

 인간은 새로운 문제를 접할 때, 우선 장기기억에서 과거에 겪었던 비슷한 문제를 찾는다. 이후 그 문제를 성공적으로 해결했던 방법은 새로운 문제에 적용한다. 즉, 인간은 패턴인식 머신이라고 볼 수 있다. 인류는 반복을 통해 정형화된 문제해결 방법을 만들어 왔으며, 비슷한 문제들에 반복적으로 적용하였다. 프로그래밍 관점에서의 패턴들은 다음의 특성을 지닌다.

 

 

 디자인 패턴은 소프트웨어 설계에서 흔히 겪는 문제에 대한 해결책을 제시된 패턴이다. 하지만, 완성된 설계가 아님에 주목해야 한다. 디자인 패턴은 곧바로 코드로 바뀌지 않으며, 어떤 문제를 다양한 환경에서 해결하기 위한 가이드라인 수준으로 이해해야 한다.

 

 

디자인 패턴의 장단점

  • 장점(이라고 주장한 것) :
    • 이미 테스트를 마친 검증된 개발 방법을 사용해 개발 속도를 향상할 수 있다고 주장한다. 하지만 새 코드를 작성할 때 곧바로 알 수 없는 문제들이 존재한다.
    • 공통 용어 정립을 통해 개발자들 간의 빠른 의사소통을 촉진시킨다고 한다. 어떻게 설계할 것인지 장황하게 설명할 필요가 없어진다. 단, 모든 개발자가 해당 패턴을 알고 있어야 한다.
  • 단점
    • 잘못 적용하는 경우가 빈번하다. 이로 인해 프로그램이 더 복잡해지고 비효율적으로 동작하는 경우가 존재한다.

 여기서 다룰 디자인 패턴은 다음과 같다. 각 패턴은 목적에 따라 적합한 사용용도가 있으므로, 장단점에 관해서 언급하지 않겠다. (디자인 패턴이 오용된다면, 해당 상황에서 단점을 발견할 수 있다.)

  • 싱글턴
  • 팩토리 메서드
  • 빌더
  • 어댑터
  • 프록시
  • 책임 연쇄

 

 

싱글턴 패턴

 싱글턴 패턴은 클래스의 인스턴스 수를 하나로 제한하는 패턴이다. 프로그램 실행 중에 최대 하나만 있어야 하고, 다른 개체에서 전역적으로 접근 가능해야 하는 경우에 적합하다.(ex. 파일 시스템)

 Java의 static은 JVM에서 실체가 하나만 존재한다는 점에서, 싱글턴 패턴과 유사하게 동작한다. 그러나 static에 대해서는 다형성을 사용할 수 없으며, 멀티턴 패턴으로 바꿀 수 없다는 차이가 있다.

 

 

 

팩토리 메서드 패턴

 팩토리 메서드 패턴은 사용할 클래스에 대해 정확하게 몰라도 개체 생성을 가능하게 해주는 패턴이다. 실생활을 예로 들자면, 티셔츠를 구매할 때 M,L,XL 등과 같은 사이즈를 보고 구매를 하며, 티셔츠의 실제 수치(cm로 표현된)를 알지 못해도 구매가 가능하다. 물론, 개체 생성을 위한 패턴인 팩토리 메서드와는 조금 다르지만, 클라이언트에게 익숙한 인자를 통해 개체를 반환한다는 점에서는 동일하다.

 앞서 언급한 것처럼, 클라이언트가 본인에게 익숙한 인자를 통해 개체 생성이 가능하다. 또한, 사용자가 팩토리 메서드에 잘못된 인자를 넣었을 때, 예외를 던지지 않고 null값을 반환할 수 있다.

 그러나, 실제 어떻게 개체가 생성되는지를 모르기 때문에 복잡도가 올라갈 수 있다. 따라서 개체의 생성을 추상화할 필요성이 있을때에 팩토리 메서드를 사용해야 한다.

public class Cup {
    private int sizeMl;

    private Cup(int sizeMl){
        this.sizeMl = sizeMl;
    }

    public static Cup createOrNull(CupSize size){
        switch (size){
            case SMALL:
                return new Cup(355);
            case MEDIUM:
                return new Cup(477);
            case LARGE:
                return new Cup(645);
            default:
                assert (false) : "Unhandled CupSize: " + size;
                return null;
        }
    }
}

public enum CupSize {
    SMALL,
    MEDIUM,
    LARGE
}

public class Run {
    public static void run(){
        CupSize cupSize = CupSize.SMALL;
        Cup cup = Cup.createOrNull(cupSize);
    }
}

 

 

 

 

빌더 패턴

 빌더 패턴은 개체의 생성과정을 그 개체의 클래스로부터 분리하는 방법이다. 개체의 부분부분을 먼저 만들고, 각 부분을 모아 개체 생성이 가능해지면 개체를 생성한다. 예를 들자면, 벽돌을 하나씩 쌓아 집을 만드는 것과 유사하다. Java에서는 StringBuilder에서 빌더 패턴을 사용하였다.

 개체 생성자에 어떤 인자를 넣어야 되는지 잘 모르거나 실수가 발생할 여지가 있을 때, 빌더 패턴을 사용하여 클라이언트가 개체 생성을 위한 인자를 넘겨주도록 강제할 수 있다. 그러나, C++ or Java의 언어적 한계(호출자가 넘겨주는 파라미터 들의 순서와 생성자의 아규먼트들의 순서가 일치해야함)를 해결하기 위한 패턴일 뿐이라는 지적을 받곤 한다.

public class Employ {
    private int id;
    private int age;
    private int startingYear;
    private String firstName;
    private String lastName;

    public Employ(int id, int age, int startingAge, String firstName, String lastName){
        this.id = id;
        this.age = age;
        this.startingYear = startingAge;
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

public class EmployeeBuilder {
    private final Integer id;
    private Integer age;
    private Integer startingYear;
    private String firstName;
    private String lastName;

    public EmployeeBuilder(int id){
        this.id = id;
    }

    public EmployeeBuilder withAge(int age){
        this.age = age;
        return this;
    }

    public EmployeeBuilder withStaringYear(int startingYear){
        this.startingYear = startingYear;
        return this;
    }

    public EmployeeBuilder withName(String firstName, String lastName){
        this.firstName = firstName;
        this.lastName = lastName;
        return this;
    }

    public Employ build(){
        if(age != null && startingYear != null && !firstName.isEmpty() && !lastName.isEmpty()){
            return new Employ(id,age,startingYear,firstName,lastName);
        }
        return null;
    }
}

 

위 예시에서는 생성자에 필요한 아규먼트를 넣도록 강제하는 목적으로 사용되었다. 이 목적 이외에, Builder가는 단어가 내포한 “만들어주는 녀석”이라는 의미에 초점을 맞춘 목적도 있다.

아래 예시는 csv 포맷의 파일을 읽어들여, MarkDown과 HTML 형태로 만들어주는 빌더이다.

 

// Run.java
public class Run{
    
    public void run() {
        Path path = Paths.get("resource","test.csv");
        CsvReader csvReader = new CsvReader(path.toString());
        {
            HtmlTableBuilder builder = new HtmlTableBuilder();

            csvReader.writeTo(builder);
            String html = builder.toHtmlDocument();
            System.out.println(html);
        }
        {
            MarkdownTableBuilder builder = new MarkdownTableBuilder();

            csvReader.writeTo(builder);
            String mdText = builder.toMarkDownText();
            System.out.println(mdText);
        }

    }
}
// CsvReader.java
public class CsvReader {
    private String csvText;

    public CsvReader(String csvPath){
        this.csvText = getTextFromPath(csvPath);
    }

    private String getTextFromPath(String path) {
        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            String st;
            while ((st = br.readLine()) != null) {
                sb.append(st).append(System.lineSeparator());
            }
            return sb.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void writeTo(TableBuilder tableBuilder){
        String[] lines = csvText.split(System.lineSeparator());
        String[] columns = lines[0].split(",");

        for (int i = 0; i < columns.length; i++){
            tableBuilder.addColumn(columns[i]);
        }

        tableBuilder.addHeadingRow();

        for (int i = 1; i < lines.length; i++){
            String[] tokens = lines[i].split(",");

            for(int j = 0; j < tokens.length; j++){
                tableBuilder.addColumn(tokens[j]);
            }

            tableBuilder.addRow();
        }
    }
}
// TableBuilder.java
public abstract class TableBuilder {
    protected static final String lineSeparator = System.lineSeparator();
    public abstract TableBuilder addHeadingRow();
    public abstract TableBuilder addRow();
    public abstract TableBuilder addColumn(String column);
}
// HtmlTableReader.java
public class HtmlTableBuilder extends TableBuilder{
    private final StringBuilder stringBuilder = new StringBuilder();
    private LinkedList<String> rowItems = new LinkedList<>();

    @Override
    public TableBuilder addHeadingRow() {
        if(rowItems.isEmpty()) return this;

        stringBuilder.append("<tr>");
        rowItems.forEach(item -> stringBuilder.append("<th>" + item + "</th>"));
        stringBuilder.append("</tr>")
                .append(lineSeparator);

        rowItems.clear();
        return this;
    }

    @Override
    public TableBuilder addRow() {
        if(rowItems.isEmpty()) return this;

        stringBuilder.append("<tr>");
        rowItems.forEach(item -> stringBuilder.append("<td>" + item + "</td>"));
        stringBuilder.append("</tr>")
                .append(lineSeparator);

        rowItems.clear();
        return this;
    }

    @Override
    public TableBuilder addColumn(String column) {
        if(rowItems == null){
            rowItems = new LinkedList<>();
        }
        rowItems.addLast(column);
        return this;
    }

    public String toHtmlDocument(){

        return "<table>" + lineSeparator + stringBuilder.toString() + "</table>";
    }

}
// MarkDownTableBuilder.java
public class MarkdownTableBuilder extends TableBuilder{
    private final StringBuilder stringBuilder = new StringBuilder();
    private LinkedList<String> rowItems = new LinkedList<>();

    @Override
    public TableBuilder addHeadingRow() {
        for (String item : rowItems){
            stringBuilder.append("|").append(item);
        }
        stringBuilder.append("|").append(lineSeparator);

        for(int i = 0; i < rowItems.size(); i++){
            stringBuilder.append("|").append("---");
        }
        stringBuilder.append("|").append(lineSeparator);
        rowItems.clear();
        return this;
    }

    @Override
    public TableBuilder addRow() {
        for (String item : rowItems){
            stringBuilder.append("|").append(item);
        }
        stringBuilder.append("|");

        stringBuilder.append(lineSeparator);
        rowItems.clear();
        return this;
    }

    @Override
    public TableBuilder addColumn(String column) {
        if (rowItems == null) rowItems = new LinkedList<>();

        rowItems.add(column);
        return this;
    }

    public String toMarkDownText(){
        return stringBuilder.toString();
    }
}

 

 

어댑터 패턴(== 래퍼 패턴)

 주로 업계에서는 래퍼 패턴이라고 말하고, GoF책에서는 어댑터 패턴이라고 말하였다. 래퍼 패턴은 어떤 클래스의 메서드 시그내처가 맘에 안 들 때 다른 걸로 바꾸기 위한 방법이다. 이때, 대상이 되는 클래스의 메서드 시그내처를 직접 변경하지 않는다. 해당 클래스의 소스코드를 변경할 수 없는 상황이거나, 다른 클래스들이 해당 클래스에 의존할 경우 이 패턴을 활용한다.

 아래 예시에서는, GraphicsRapper클래스에서 클라이언트에게 메서드를 제공하고, 실제 어떤 개체(OpenGL or DirectX)가 사용될지는 클라이언트가 아닌 Rapper 클래스가 결정한다. OpenGL과 DirectX는 서로 타입의 속성을 갖고 있으며, 메서드의 시그내처와 파라미터도 다른 타입이다. 래퍼 클래스는 동일한 메서드 시그내처로 두 클래스의 다른 메서드를 호출가능 하도록 한다.

 

public class Run  implements Runnable {
    @Override
    public void run() {
        GraphicsRapper rapper = new GraphicsRapper();
        rapper.clear(0.3f,0.1f,0.4f,0.2f);
    }
}
public class GraphicsRapper {
    private OpenGL gl = new OpenGL();

    public void clear(float r, float g, float b, float alpha){
        gl.clearScreen(alpha,r,g,b);
    }

    private DirectX dx = new DirectX();
//
//    public void clear(float r, float g, float b, float alpha){
//        dx.clear((int)(r * 100),(int)(g * 100),(int)(b * 100),(int)(alpha * 100));
//    }
}
public class OpenGL {
    private float r;
    private float g;
    private float b;
    private float alpha;

    public void clearScreen(float alpha, float r, float g, float b){
        this.alpha = alpha;
        this.r = r;
        this.g = g;
        this.b = b;
    }
}
public class DirectX {
    private int r;
    private int g;
    private int b;
    private int alpha;

    public void clear(int r, int g, int b, int alpha){
        this.r = r;
        this.g = g;
        this.b = b;
        this.alpha = alpha;
    }
}

 

 

프록시 패턴

 프록시란 무엇일까? WEB 상에는 실제 웹사이트와 사용자 사이에 위치하는 중간 서버인 프록시 서버가 존재한다. 이 서버는 마치 인터넷상의 캐시 메모리처럼 동작한다. 사용자가 프록시 서버를 통해 특정 리소스를 요청할 때, 프록시 서버에 리소스가 존재하면 그것을 반환하고, 아니라면 실제 웹서버에서 리소스를 불러온다.

 프록시 패턴이 이루려는 목적도 비슷하다. 클래스 안에서 어떤 상태를 유지하는 게 여의치 않은 경우가 존재한다. 데이터가 너무 커서 미리 읽어두면 메모리가 부족한 경우가 있으며, 개체 생성 시 데이터 로딩이 오래걸리는 경우가 존재한다. 또한, 개체를 생성했더라도 사용되지 않을 수 있다. 이럴 경우 불필요한 로딩을 방지하기 위해, 맨 처음 개체가 생성될 때는 데이터 로딩에 필요한 정보만(ex. 파일 경로)만 기억해 두고, 클라이언트가 실제로 데이터를 요청할 때 메모리에 로딩하도록 한다. 이러한 방식을 프록시 패턴이라고 한다. 예시는 아래와 같다.

 

프록시 패턴 사용 전

public class Image{ 
	private ImageData image;
	public Image(String filePath){ 
		this.image = ImageLoader.getInstance().load(filePath); 
	} 
	public void draw(Canvas canvas, float x, float y){
		canvas.draw(this.image,x,y); 
	} 
}

 

프록시 패턴 사용 후(Lazy Loading)

class Image{
	private String filePath; 
    private ImageData image; 
    public Image(String filePath){ 
    	this.filePath = filePath; 
    } 
    public void draw(Canvas canvas, float x, float y){ 
    	if(this.image == null){ 
    		this.image = ImageLoader.getInstance().load(this.filePath); 
    	} 
        canvas.draw(this.image,x,y); 
    } 
}

 

 

 

책임 연쇄 패턴

 말 그대로 책임을 연쇄적으로 갖는 패턴이다. 어떤 메시지를 처리할 수 있는 여러 개체가 존재하며, 이 개체들은 차레대로 메시지를 처리할 수 있는 기회를 얻는다. 만약 그 중 한 개체가 메시지를 처리하면 그거에 대한 책임을 지는 것이고, 다음 개체는 메시지를 처리할 기회를 얻지 못한다. 반대로 앞에 있는 개체가 메시지를 처리하지 못하면, 메시지는 다음 개체에게 책임의 기회가 전달된다.

public abstract class Logger {
    private EnumSet<LogLevel> logLevels;
    private Logger next;

    public Logger(LogLevel[] levels){
        this.logLevels = EnumSet.copyOf(Arrays.asList(levels));
    }

    public void setNext(Logger next){
        this.next = next;
    }

    public final void message(String msg, LogLevel severity){
        if(logLevels.contains(severity)){
            log(msg);
        } else if (this.next != null) {
            this.next.message(msg, severity);
        }
    }

    protected abstract void log(String msg);
}