프로토타입 (Prototype) 패턴

weekbelt·2022년 12월 8일
0

1. 패턴 소개

프로토타입 패턴은 기존 인스턴스를 복제하여 새로운 인스턴스를 생성하는 방법입니다. 이런방법을 쓰게되는 경우는 보통 기존의 객체를 응용해서 새로운 인스턴스를 만들때 유용합니다. 특히 기존의 인스턴스를 만들때 시간이 오래걸리는 작업인 예를들어 데이터베이스에서 어떤 데이터를 읽어와서 인스턴스를 생성해야 한다던가 또는 네트워크를 거쳐서 HTTP요청을 보내서 가져온 데이터를 기반으로 인스턴스를 만들어야 되는경우에 매번 DB에 접근하고 네트워크 요청을 통해서 가져오면 인스턴스를 생성할때 오래걸리고 리소스를 많이 사용하게 됩니다. 그래서 이미 기존의 네트워크나 DB호출을 통해서 만들어진 객체를 가지고 안에 있는 모든 데이터를 복사해서 새로운 인스턴스에서 원하는 값만 일부 변경해서 쓴다면 좀 더 효율적으로 인스턴스를 생성할 수 있습니다.

위의 그림에서 프로토타입이 생긴 모양을 보면 Prototype인터페이스에서 제공하는 추상메서드인 clone메서드가 있고 이 clone이라는 복제기능을 제공할 클래스들이 구현체로 ConcretePrototypeA, ConcretePrototypeB로 생성됩니다. 구현된 clone메서드에서 인스턴스를 어떤식으로 생성해야할지 정의를 하게 됩니다. Client입장에서는 Prototype의 clone메서드를 사용해서 인스턴스를 생성하기만 하면 됩니다.
실제로 코드를 보면서 살펴보겠습니다.

public class GithubRepository {

    private String user;

    private String name;
    
    // getter, setter....
}

위의 GithubRepository클래스에서 user, name을 필드로 가지고 있습니다.

public class GithubIssue {

    private int id;

    private String title;

    private GithubRepository repository;

    public GithubIssue(GithubRepository repository) {
        this.repository = repository;
    }
    
    // getter, setter

    public GithubRepository getRepository() {
        return repository;
    }

    public String getUrl() {
        return String.format("https://github.com/%s/%s/issues/%d",
                repository.getUser(),
                repository.getName(),
                this.getId());
    }
}

GithubIssue클래스는 이슈 id, title과 이슈가 속해져 있는 GitRepository를 가지고 있습니다.
이 상태에서 GitRepository와 GithubIssue를 가지고 아래와 같이 url을 생성할 수 있습니다.

url 생성

public class App {

    public static void main(String[] args) {
        GithubRepository repository = new GithubRepository();
        repository.setUser("whiteship");
        repository.setName("live-study");

        GithubIssue githubIssue = new GithubIssue(repository);
        githubIssue.setId(1);
        githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");

        String url = githubIssue.getUrl();
        System.out.println(url);
    }

}

실행 결과

https://github.com/whiteship/live-study/issues/1

이렇게 GithubIssue를 통해서 URL을 생성했는데 프로토타입 패턴을 사용하게되면 새로운 GithubIssue로 생성해서 만드는 것이 아닌 아래와 같이 clone메서드를 통해서 새로운 GithubIssue의 인스턴스를 만들 수 있습니다.

새로 정의

GithubIssue githubIssue2 = new GithubIssue(repository);
githubIssue.setId(12;
githubIssue.setTitle("2주차 과제:");

복제를 통한 인스턴스 생성

GithubIssue githubIssue2 = githubIssue.clone();

복제를 통한 인스턴스 생성이어도 githubIssue == githubIssue2이 연산은 거짓이지만 githubIssue1.equals(githubIssue2)연산은 true가 나와야 합니다. 결국 clone메서드도 새로운 인스턴스를 생성해야하지만 안의 내용은 같아야 합니다.

2. 패턴 적용하기

clone메서드를 적용하도록 클래스를 구현해보도록 하겠습니다. 기존에 있던 다른 디자인패턴과는 다르게 이번 디자인패턴은 자바가 제공하는 기능을 그대로 사용하겠습니다. Java가 인스턴스를 복제해주는 기본 메커니즘을 제공해 줍니다. 그런다음에 custom한게 구현하는 방법을 살펴보도록 하겠습니다.

현재 githubIssue객체에 clone메서드를 호출하면 컴파일 에러가 뜹니다.

컴파일 에러

githubIssue.clone();

그 이유는 clone메서드의 접근제한자가 public이 아니라 protected이기 때문입니다. clone메서드는 최상위 객체인 Object클래스에 정의되어 있습니다.

Object클래스의 clone메서드

    @HotSpotIntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;

접근제한자가 protected이기 때문에 아무 인스턴스나 clone메서드를 호출할 수는 없고 명시적으로 clone이 가능하도록 만들어줘야 합니다. clone메서드를 사용하도록 하려면 해당클래스에 Clonable인터페이스를 구현해야합니다.

Cloneable을 implements한 Github클래스

public class GithubIssue implements Cloneable {

    private int id;

    private String title;

    private GithubRepository repository;

    public GithubIssue(GithubRepository repository) {
        this.repository = repository;
    }
    
    // getter, setter

    public GithubRepository getRepository() {
        return repository;
    }

    public String getUrl() {
        return String.format("https://github.com/%s/%s/issues/%d",
                repository.getUser(),
                repository.getName(),
                this.getId());
    }
}

먼저 기존의 GithubIssue에 Cloneable을 implements로 설정합니다. 그리고 clone메서드를 재정의 합니다.

clone메서드 재정의

@Override
protected Object clone() throws CloneNotSupportedException {
    return super.clone();
}

이안에서 clone메서드를 커스텀하게 구현해서 Object를 리턴하도록 할 수 있지만 Object가 기본으로 제공해주는 clone기능을 사용해서 제공할 수 있습니다.

ithubIssue clone = (GithubIssue) githubIssue.clone();
System.out.println(clone.getUrl());

clone메서드의 리턴타입이 Object이므로 GithubIssue로 타입캐스팅을 해서 인스턴스를 생성합니다. 실행을 해보면 기존의 url과 같은 값이 나오는 것을 확인할 수 있습니다.

실행결과

https://github.com/whiteship/live-study/issues/1

그렇다면 이제 clone이라는 인스턴스는 프로토타입으로 사용한 githubIssue와 다릅니다. 하지만 clone과 githubIssue1는 equals연산에서 true가 나와야합니다. 그러려면 GithubIssue클래스에 equals메서드를 재정의 해야합니다.

equals메서드 재정의

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    GithubIssue that = (GithubIssue) o;
    return id == that.id 
    			 && Objects.equals(title, that.title) 
                 && Objects.equals(repository, that.repository);
}

아래 코드를 실행하면 모두 true가 나와야합니다.

System.out.println(clone != githubIssue);		// true
System.out.println(clone.equals(githubIssue));	// true
System.out.println(clone.getClass() == githubIssue.getClass());		// true

결국 제대로 clone이 되었다는 것을 알 수 있습니다. 이렇게 clone을 해서 인스턴스를 생성하게 되면 아래와 같은 동일한 작업을 반복하지 않아도 됩니다. 특히나 DB나 네트워크를 통해서 받아오는 정보인 경우에는 더더욱 편리하게 객체를 생성할 수 있습니다.

GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");

하지만 한가의 주의해야 할 점은 기본적으로 자바가 제공해주는 clone메서드는 주석을 읽어보시면 얉은(shallow copy)복사를 지원하다고 나와있습니다.

this method performs a "shallow copy" of this object, not a "deep copy" operation.

얉은복사는 깊은복사(deep copy)와 반대되는 개념인데 자바가 기본적으로 제공해주는 기능은 얇은 복사입니다. 얉은복사가 무엇일까요? 아래와 같이 new연산자로 생성한 githubIssue인스턴스를 통해 clone메서드를 호출해서 복제된 인스턴스를 생성했는데 복제된 clone 인스턴스가 참조하고 있는 repository는 인스턴스를 생성할때 새로 GithubRepository를 생성해서 clone인스턴스에 참조하도록 했을까요 아니면 기존의 githubIssue인스턴스가 참조하고 있는 repository를 참조하고 있을까요? 이 차이에 따라서 얇은복사인지 깊은복사인지를 판가름 할 수 있습니다.

GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");

GithubIssue clone = (GithubIssue) githubIssue.clone();
System.out.println(clone.getUrl());

자바에서 기본적으로 제공하는 clone메서드는 얇은복사(shallow copy)라고 했는데 clone이 가리키고 있는 repository와 githubIssue가 가리키고 있는 repository가 같습니다.

얉은복사(shallow copy)인 경우 실행결과

System.out.println(clone.getRepository() == githubIssue.getRepository()); // true

githubIsssue와 clone이 같은 repository를 참조하고 있습니다. 이렇게 되면 만약에 clone을 생성하고 나서 repository의 이름이 변경된다면 clone이 참조하고있는 repository도 같은 참조기 때문에 영향이 미칩니다. 기존의 프로토타입인 githubIssue에 영향을 받지않게 repository또한 새로 생성해서 참조하도록 하고 싶다면 깊은복사(deep copy)를 통해서 구현할 수 있습니다.

깊은복사(deep copy)를 통한 clone메서드 재정의

@Override
protected Object clone() throws CloneNotSupportedException {
    GithubRepository repository = new GithubRepository();
    repository.setUser(this.repository.getUser());
    repository.setName(this.repository.getName());

    GithubIssue githubIssue = new GithubIssue(repository);
    githubIssue.setId(this.id);
    githubIssue.setTitle(this.title);

    return githubIssue;
}

clone메서드를 재정의하여 GithubRepository를 new연산자로 새로 생성하고 값만 기존의 repository의 값을 채워 넣으면 새로운 GithubRepository인스턴스를 참조한 clone을 리턴할 수 있습니다. 이렇게 깊은복사(deep copy)를 통해서 구현한 clone메서드 호출로 새로우 인스턴스를 생성하면 기존의 프로토타입이 참조하고 있는 repository의 값이 변경되어도 전혀 영향을 받지 않게 됩니다.

3. 정리

간단히 다시 프로토타입패턴을 정리하면 기존에 있는 인스턴스를 프로토타입으로 사용해서 새로운 인스턴스를 만드는 방법입니다. Prototype이라는 인터페이스에 clone이라는 추상메서드를 선언하고 하위타입에서 그 clone메서드를 커스텀하게 구현해야하지만 자바에서 기본적으로 Object객체에서 제공을 해주고 있기 때문에 직접 구현할 필요는 없고 Cloneable인터페이스의 구현체를 생성해서 clone메서드를 재정의하면 됩니다.

프로토타입패턴을 사용하게 되면서 얻는 장점은 복잡한 객체를 만드는 과정을 숨길 수 있습니다. 앞서 설명했듯이 DB나 네트워크를 통해서 인스턴스를 생성하게되면 시간과 리소스를 많이 잡아 먹게 되는데 이렇게 새 인스턴스를 생성하는 것보다 복제하는 과정이 더 효율적일 수 있습니다. 또한 추상적인 타입을 리턴할 수 있습니다. 단점은 복제한 객체를 만드는 과정 자체가 아무래도 코드가 추가되다보니 순환참조가 있는경우 복잡해질 수 있습니다.

참고

profile
백엔드 개발자 입니다

0개의 댓글