본문 바로가기

CS ﹒ Algorithm/DesignPatterns

디자인 패턴 (5) Prototype Pattern : 프로토타입 패턴 _ 애매한 녀석

 

 

 

Prototype Pattern?

 

프로토타입 패턴은 원형이 되는 인스턴스를 사용해 새롭게 생성할 객체의 종류를 명시하여 새로운 객체가 생성될 시점에 인스턴스의 타입이 결정되도록 하는 패턴이다.

 

그렇다고 한다.

쉽게 설명하자면 인스턴스를 그대로 복사해서 사용하는 것이 프로토타입 패턴이며, 자바에는 이미 Cloneable이 구현되어 있기 때문에 그대로 사용해도 되고, 그게 아니더라도 구현이 그다지 어려운 패턴이 아니기 때문에 직접 구현해서 사용해도 된다.

Clonable을 implements해서 구현하는 경우와 직접 구현하는 경우를 둘 다 만들어보겠다.

 

"Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다. 기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고'라는 것이다. 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다." - JavaEffective 86p.

 

그래도 일단 배워보자.

예제는 최대한 단순하고 짧은 것으로 만들어왔다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CloneMain {
    public static void main(String[] args) {
        Order order = new Order();
        order.setId(0L);
        order.setPrice(10000);
 
        User user = User.builder()
                .id(0L)
                .name("배고프다")
                .address("평양")
                .order(order)
                .build();
 
        User copyUser = new User();
        copyUser.setId(user.getId());
        copyUser.setName(user.getName());
        copyUser.setAddress(user.getAddress());
        copyUser.setOrder(user.getOrder());
    }
}
 
cs

 

 

일반적으로 어떤 객체의 값을 새롭게 복사해서 생성하려면 이런 방법을 사용할 것이다.

혹은 생성자에 일일히 값을 넣어서 새로 만들어줘도 되겠다.

 

그러나 이런 식으로 구현한다면 불필요하게 반복되는 코드가 여러 로직에 쌓일 수 있고, 비즈니스 로직 자체가 특정 클래스에 의존하게 된다.

따라서 해당 클래스를 간편하게 복사할 수 있도록 변경하겠다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@Builder
public class User implements Cloneable {
    private Long id;
    private String name;
    private String address;
    private Order order;
 
    public User() {
    }
 
    @Override
    public User clone() throws CloneNotSupportedException {
            User clone = (User) super.clone();
            return clone;
    }
}
cs

 

 

User 클래스로 들어가서 Cloneable을 상속하여 clone() 메서드를 구현해주면 된다. 정말 이게 끝이다.

Order 클래스를 굳이 보여주지 않는 이유는 안에 변수 하나 들어있는 사실상 빈 깡통이기 때문이다.

 

그리고 기본 clone() 메서드에서 제공하는 예외는 참 특이한데, CloneNotSupportedException이다.

이것은 클론을 구현하지 않은 객체에서 clone()을 호출하면 나타나는 메세지다.

 

그런데 우리는 이 메서드를 직접 구현했기 때문에 절대 예외가 생길 수가 없음에도 불구하고 이 예외는 CheckedException이기 때문에 계속해서 던지던지, 아니면 어디서 받아주던지(catch) 해야 한다.

쩝.. 계속 알아보자

 

 

 

1
2
3
4
5
6
7
8
9
        CloneableUser user2 = CloneableUser.builder().id(0L)
                .address("주소")
                .name("창식이")
                .order(order)
                .build();
 
        CloneableUser copyUser2 = (CloneableUser) user2.clone();
        System.out.println(user2==copyUser2); // false
        System.out.println(user2.equals(copyUser2)); // true
cs

 

우선 clone()메서드로 잘 복사되고 있음을 확인할 수 있다.

user2와 copyUser2는 별도의 객체이기 때문에 동등 연산자(==)로 비교하면 false가 나오고, 둘의 값은 같기 때문에 equals비교는 true가 나온다.

문제는 이 equals도 참이 나올 수도 아닐 수도 있다. (ㅋㅋ) 궁금하면 API 문서를 찾아보자.

 

그리고 한 가지 더 문제가 있는데, 이 안에 있는 Order 객체에 대한 것이다.

 

 

 

 

원본 객체인 user2에서 Order를 가져온 다음 setPrice로 50000을 할당했다. (초기값은 10000이였다.)

copyUser2의 order의 price 값은 ?

10000이다.

 

clone은 기존 객체를 new로 새로 생성하는 것과 다를 바 없어 clone 객체와 원본 객체는 구별되지만, 이 안에 들어있는 객체는 그대로 가져오기 때문에 주소를 공유한다.

 

이렇게 만들어졌다고 생각하면 쉽다.

 

new User(user.getId, user.getName, user.getOrder)

이제 왜 내부 객체가 동일한 주소를 바라보는지 이해가 될 것이다.

 

그러면 이번에는 직접 구현해서 깊은 복사를 하도록 코드를 작성해보자.

 

 

위와 같이 구현했다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
        DeepCopyUser user3 = DeepCopyUser.builder()
                .id(0L)
                .address("아프리카")
                .name("빠삐콩")
                .order(order)
                .build();
 
        DeepCopyUser copyUser3 = user3.clone();
        System.out.println(user3==copyUser3); // false
        System.out.println(user3.equals(copyUser3)); // true
 
        user3.getOrder().setPrice(20000000);
        System.out.println(copyUser3.getOrder().getPrice()); // 50000
cs

 

드디어 우리가 원하는 완벽한 복사가 되었고, 쓸데없는 예외 던지기에서도 벗어났다.

그런데 이쯤해서 이런 생각이 들 수 있다.

 

 

이게 굳이 이런 메서드를 별도로 지정해서 할만한 일인가?

그냥 생성자로도 되는 건데?

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@AllArgsConstructor
@Builder
@Data
public class ConstructorUser {
    private Long id;
    private String name;
    private String address;
    private Order order;
 
    public ConstructorUser ( ConstructorUser origin ) {
        this.id = origin.getId();
        this.name = origin.getName();
        this.address = origin.getAddress();
        this.order = new Order(order.getId(), order.getPrice());
    }
}
cs

 

 

이 편이 훨씬 자연스러워 보이고, 실제로 스프링에서 많이 사용되는 방법인 것으로 알고 있다.

반드시 clone이 존재해야만 하는 어떤 이유가 있을지도 모르지만, 아직 내 수준에서는 잘 모르겠다.

추후 실력이 더 쌓이고 이해도가 생기면 내용을 추가하고 싶다.