프로토타입 (Prototype) 패턴
기존 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법
프로토타입 패턴은 객체를 생성할 때, 기존 객체를 복사하여 새로운 객체를 만드는 생성 패턴 중 하나이다.
이 패턴은 직접 객체 생성하는 대신, 객치의 복제(cloning)를 통해 성능을 향상시키거나 복잡한 객체 생성 과정을
단순화할 수 있도록 도와준다.
특히 새로운 객체를 만드는 비용이 클 때 유용하다.
복제 기능을 갖추고 있는 기존 인스턴스를 프로토타입으로 사용해 새 인스턴스를 만들 수 있다.
프로토타입 (Prototype) 패턴 Before
@Getter @Setter
@EqualsAndHashCode
public class GithubRepository {
private String user;
private String name;
}
@Getter @Setter
@EqualsAndHashCode
public class GithubIssue {
private int id;
private String title;
private GithubRepository repository;
public GithubIssue(GithubRepository repository) {
this.repository = repository;
}
public String getUrl(){
return String.format("https://github.com/%s/%s/issues/%d",
repository.getUser(),
repository.getName(),
this.getId()
);
}
}
public class App {
public static void main(String[] args) {
GithubRepository repository = new GithubRepository();
repository.setUser("jeus1998");
repository.setName("Desgin-Pattern-study");
GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: 스레드 세이프의 의미");
String url = githubIssue.getUrl();
System.out.println(url);
// TODO make clone() API
// GithubIssue clone = githubIssue.clone();
// TODO clone != githubIssue
// TODO clone.equals(githubIssue) => true
}
}
현재 코드를 보면 GithubIssue 인스턴스(객체)를 만들 때
먼저 GithubRepository 인스턴스를 만들고 GithubIssue 생성자로 넣어주면서 GihubIssue 인스턴스를 만든다.
GithubIssue 인스턴스를 만들 때 매번 해당 프로세스(GithubRepository 생성 & 설정)를 반복하게 될 것이다.
이 문제를 프로토타입 패턴을 적용해서 해결해 보자.
프로토타입 (Prototype) 패턴 After
자바 Object 제공 clone() API
package java.lang;
public class Object {
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
public boolean equals(Object obj) {
return (this == obj);
}
}
clone() 메소드는 native 메소드이고 접근 제어자가 protected이다.
자바가 기본으로 제공하는 clone() 메소드를 사용하려면 해당 클래스가 Cloneable 인터페이스를 구현해야 한다.
Cloneable 인터페이스
public interface Cloneable {
}
주석 정리
클래스는 Cloneable 인터페이스를 구현하여 Object.clone() 메서드에 해당 클래스 인스턴스의 필드 간 복사본을 만드는 것이 적법함을 나타낸다.
Cloneable 인터페이스를 구현하지 않는 인스턴스에서 Object의 복제 메소드를 호출하면 CloneNotSupportedException 예외가 발생한다.
프로토타입 적용 (Shallow Copy)
@Getter @Setter
@EqualsAndHashCode
public class GithubIssue implements Cloneable{
private int id;
private String title;
private GithubRepository repository;
public GithubIssue(GithubRepository repository) {
this.repository = repository;
}
public String getUrl(){
return String.format("https://github.com/%s/%s/issues/%d",
repository.getUser(),
repository.getName(),
this.getId()
);
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class App {
public static void main(String[] args) throws CloneNotSupportedException {
GithubRepository repository = new GithubRepository();
repository.setUser("jeus1998");
repository.setName("Desgin-Pattern-study");
GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: 스레드 세이프의 의미");
String url = githubIssue.getUrl();
System.out.println(url);
// throws CloneNotSupportedException
GithubIssue clone = (GithubIssue) githubIssue.clone();
System.out.println(clone.getUrl());
System.out.println(clone != githubIssue);
System.out.println(clone.equals(githubIssue));
System.out.println(clone.getClass() == githubIssue.getClass());
repository.setUser("jeus");
System.out.println(clone.getUrl());
System.out.println(clone.getRepository() == githubIssue.getRepository());
}
}
출력
https://github.com/jeus1998/Desgin-Pattern-study/issues/1
https://github.com/jeus1998/Desgin-Pattern-study/issues/1
true
true
true
https://github.com/jeus/Desgin-Pattern-study/issues/1
true
기본적으로 자바가 제공하는 clone() API는 얕은 복사 (Shallow Copy) 이다.
출력을 보면 GithubIssue 인스턴스와 clone 인스턴스는 서로 다른 인스턴스이다.
하지만 GithubIssue의 repository 인스턴스는 clone의 repository의 인스턴스와 동일하다.
이게 바로 Shallow Copy 이다.
얕은 복사에서는 객체의 필드 값만 복사되고, 그 필드가 참조 타입인 경우 참조만 복사되므로,
복제된 객체와 원본 객체는 동일한 참조 객체를 공유하게 된다.
repository의 이름을 바꾸니까 복제본인 clone url 또한 바뀐다.
프로토타입 적용 (Deep Copy)
@Getter @Setter
@EqualsAndHashCode
public class GithubIssue implements Cloneable{
private int id;
private String title;
private GithubRepository repository;
public GithubIssue(GithubRepository repository) {
this.repository = repository;
}
public String getUrl(){
return String.format("https://github.com/%s/%s/issues/%d",
repository.getUser(),
repository.getName(),
this.getId()
);
}
@Override
protected Object clone() throws CloneNotSupportedException {
GithubRepository githubRepository = new GithubRepository();
githubRepository.setUser(this.repository.getUser());
githubRepository.setName(this.repository.getName());
GithubIssue githubIssue = new GithubIssue(githubRepository);
githubIssue.setId(this.id);
githubIssue.setTitle(this.title);
return githubIssue;
}
}
// true 출력
System.out.println(clone.getRepository() != githubIssue.getRepository());
이제 복사본인 clone의 참조 객체인 repository가 원본 참조 객체인 repository와 달라졌다.
이로 인해 복제본과 원본은 각기 독립적인 GithubRepository 인스턴스를 가지며, 서로의 상태에 영향을 주지 않는다.
정리
자바가 아니라면 직접 Prototype 인터페이스를 통해서 구현해야 하지만
자바라면 Cloneable 인터페이스를 구현하고 clone 메소드를 적절하게 오버라이딩 하면 쉽게 복제가 가능하다.
얕은 복사와 깊은 복사에 대해서 명확하게 구분하고 처리하자.
프로토타입 (Prototype) 패턴 장단점
장점
복잡한 객체를 만드는 과정을 숨길 수 있다.
기존 객체를 복제하는 과정이 새 인스턴스를 만드는 것보다 비용(시간 또는 메모리)적인 면에서
효율적일 수도 있다.
ex) 복잡한 설정 / DB 연결
추상적인 타입을 리턴할 수 있다.
이를 통해서 클라이언트 코드가 특정 구현체에 종속되지 않게 하여, 유연성을 높일 수 있다.
단점
복제한 객체를 만드는 과정 자체가 복잡할 수 있다. (특히, 순환 참조가 있는 경우)
자바에서 Cloneable 인터페이스를 사용하는 경우에도, 복제는 얕은 복사(shallow copy)가 기본이므로, 참조 타입 필드들이 원본과 공유될 수 있어 예상치 못한 동작이 발생할 수 있다. 이를 해결하려면 깊은 복사(deep copy)를 수동으로 구현해야 하는데, 이는 코드의 복잡성을 증가시킬 수 있다.
정리
프로토타입 패턴은 성능 최적화와 유연성 측면에서 강력한 도구가 될 수 있지만, 복제 과정 자체가 복잡하거나 객체 간의 참조 관계가 복잡할 때는 신중하게 구현해야 한다.
자바와 스프링에서 찾아보는 패턴
Collection
@AllArgsConstructor @ToString
@EqualsAndHashCode
public class Student {
String name;
}
public class CollectionExample {
public static void main(String[] args) {
Student student1 = new Student("jeu1");
Student student2 = new Student("jeu2");
ArrayList<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
// List -> cloneable 구현 x
ArrayList<Student> clone = (ArrayList<Student>) students.clone();
System.out.println(clone);
// 생성자 방식
List<Student> clone2 = new ArrayList<>(students);
System.out.println(clone2);
}
}
보통 Collection 사용 시 유연성을 위해 ArrayList 같은 구체 클래스 대신
List 인터페이스를 선언해서 사용한다.
ArrayList에는 colne()이 있지만 List에는 clone()이 없다.
그래서 보통 생성자 방식을 통해서 객체를 복사한다.
복사 방식은 shallow copy 이다.
ModelMapper
@Getter @Setter
@ToString
public class GithubIssueData {
private int id;
private String title;
private String repositoryUser;
private String repositoryName;
}
public class ModelMapperExample {
public static void main(String[] args) {
GithubRepository repository = new GithubRepository();
repository.setUser("jeus1998");
repository.setName("Desgin-Pattern-study");
GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: 스레드 세이프의 의미");
System.out.println(githubIssue);
// 반복 작업
GithubIssueData githubIssueData = new GithubIssueData();
githubIssueData.setId(githubIssue.getId());
githubIssueData.setTitle(githubIssue.getTitle());
githubIssueData.setRepositoryName(repository.getName());
githubIssueData.setRepositoryUser(repository.getUser());
// TODO ModelMapper
ModelMapper modelMapper = new ModelMapper();
GithubIssueData githubIssueData2 = modelMapper.map(githubIssue, GithubIssueData.class);
System.out.println(githubIssueData2);
}
}
출력
GithubIssue(id=1, title=1주차 과제: 스레드 세이프의 의미, repository=GithubRepository(user=jeus1998, name=Desgin-Pattern-study))
GithubIssueData(id=1, title=1주차 과제: 스레드 세이프의 의미, repositoryUser=jeus1998, repositoryName=Desgin-Pattern-study)
GithubIssueData는 GithubIssue가 가지고 있던 GithubRepository를 플랫하게 가지는 클래스이다.
ModelMapper
객체 간의 데이터 매핑을 자동화하는 유용한 라이브러리이다.
객체 간에 일치하는 필드가 있을 경우 이를 자동으로 매핑해주기 때문에, 수작업으로 필드를 일일이 설정하는 과정을 줄일 수 있다. 이를 통해 코드의 중복을 줄이고 유지보수성을 향상시킬 수 있다.
ModelMapper를 사용하니까, GithubIssue 객체를 직접 수동으로 매핑하는 대신 modelMapper.map() 메서드를 사용하여 한 줄로 쉽게 변환할 수 있었다.
내부에서는 리플렉션을 사용하여 객체의 필드들을 자동으로 매핑한다.
참고자료
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4
'Computer Sience > Desgin Pattern' 카테고리의 다른 글
[Design Pattern] 브릿지 (Bridge) 패턴 (0) | 2024.10.23 |
---|---|
[Design Pattern] 어댑터 (Adapter) 패턴 (2) | 2024.10.22 |
[Design Pattern] 빌더(Builder) 패턴 (6) | 2024.10.19 |
[Design Pattern] 추상 팩토리(Abstract Factory) 패턴 (0) | 2024.10.18 |
[Design Pattern] 팩토리 메소드(Factory Method) 패턴 (1) | 2024.10.18 |