생성패턴이란: 생성패턴은 인스턴스를 만드는 절차를 추상화하는 패턴으로 객체를 생성 합성하는 방법이나 객체의 표현 방법과 시스템을 분리해준다.
인스턴스를 오직 한개만 제공하는 클래스
시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 생길 수 있는 경우가 있다. 인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요하다.
public class Settings1 {
private static Settings1 instance;
private Settings1() { }
public static Settings1 getInstance() {
if (instance == null) {
instance = new Settings1();
}
return instance;
}
}
기본 생성자를 private 으로 하고 static 멤버 변수를 자기 참조형으로 하여 static 메서드를 통해 멤버 변수가 초기화 되지 않으면 초기화 하고 초기화가 되어있다면 세팅된 값을 반환한다.
멀티 쓰레드 환경에서 동시에 getInstance 메서드에 접근 시 쓰레드별 사용하는 객체가 서로다른 참조 값을 가지고 있을 수 있다.
public static synchronized Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
static 메서드인 getInstance 에 synchronized 키워드를 추가하여 lock을 걸어 메서드에 하나의 쓰레만 접근하게 한다.
getInstance 를 통해 객체를 생성 할 때마다 synchronized 키워드로 인해 lock 이 사용되므로 처리속도 및 메모리가 낭비된다.
public class Settings {
private Settings() {
}
//멀티쓰레드 환경에서도 안전하다, lazy 로딩도 가능하다.
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
마찬가지로 기본 생성자를 private 하게 막는다.
static inner 클래스 내부에서 static 멤버 변수로 값을 세팅해놓기 때문에 언제나 같은 참조값을 같는 같은 객체이다.
static inner 클래스는 getInstance 메서드를 통해 호출할 때 jvm 에 클래스 로더에 의해서 로딩되므로 lazy 하게 초기화 된다. 싱글톤 객체가 진짜 필요할 때까지 초기화를 늦춘다.
구체적으로 어떤 인스턴스를 만들지는 서브 클래스가 정한다.
구체적인 팩토리에서 구체적인 제품 객체를 생성하는 패턴.
다양한 구현체 (Product)가 있고, 그중에서 특정한 구현체를 만들 수 있는 다양한 팩토리 (Creator)를 제공할 수 있다.
기존 코드를 수정하지 않고 새로운 인스턴스를 여러 방법으로 생성할 수 있는 "확장에 열려있고 변경에 닫혀있는 객체 지향 원칙"을 만족하는 객체 생성 방법
public class Ship {
private String name;
private String color;
private String logo;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getLogo() {
return logo;
}
public void setLogo(String logo) {
this.logo = logo;
}
@Override
public String toString() {
return "Ship{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", logo='" + logo + '\'' +
'}';
}
}
public class WhiteShip extends Ship {
public WhiteShip() {
setName("whiteShip");
setLogo("\uD83D\uDEE5️");
setColor("white");
}
}
public class BlackShip extends Ship {
public BlackShip() {
setName("blackShip");
setLogo("⚓");
setColor("black");
}
}
WhiteShip, BlackShip 은 Ship 을 상속 받는다.
public interface ShipFactory {
default Ship orderShip(String name, String email) {
validate(name, email);
prepareFor(name);
Ship ship = createShip();
sendEmailTo(email, ship);
return ship;
}
Ship createShip();
void validate(String name, String email);
void prepareFor(String name);
void sendEmailTo(String email, Ship ship);
}
공통적인 성질을 갖고 있는 제품 객체를 생성하는 팩토리를 추상화한다.
public abstract class DefaultShipFactory implements ShipFactory {
@Override
public void sendEmailTo(String email, Ship ship) {
System.out.println(ship.getName() + " 다 만들었습니다.");
}
@Override
public void validate(String name, String email) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("배 이름을 지어주세요.");
}
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("연락처를 남겨주세요.");
}
}
@Override
public void prepareFor(String name) {
System.out.println(name + " 만들 준비 중");
}
}
인터페이스의 default 메서드 에서 사용하는 메서드를 오버라이딩하여 구현한다.
public class WhiteShipFactory extends DefaultShipFactory {
@Override
public Ship createShip() {
return new WhiteShip();
}
}
public class BlackShipFactory extends DefaultShipFactory {
@Override
public Ship createShip() {
return new BlackShip();
}
}
구체적인 factory 클래스이다. 여기서 product 객체를 생성한다.
public class Client {
public static void main(String[] args) {
Client client = new Client();
client.print(new WhiteShipFactory(), "Whiteship", "keesun@mail.com");
client.print(new BlackShipFactory(), "blackship", "keesun@mail.com");
}
private void print(ShipFactory shipFactory, String name, String email) {
System.out.println(shipFactory.orderShip(name, email));
}
}
팩토리 메소드 패턴을 사용하는 경우 직접 객체를 생성해 사용하는 것을 방지하고 서브 클래스에 위임함으로써 보다 효율적인 코드 제어를 할 수 있고 의존성을 제거한다
추상 팩토리 패턴은 팩토리 메소드 패턴에서 구현체 객체를 직접 생성하지 않고 서브 클래스에 위임하여 구체적인 클래스에 의존하지 않고 만들 수 있게 한다.
위 예제인 팩토리 메소드 패턴의 팩토리 클래스의 부분이다.
public class WhiteshipFactory extends DefaultShipFactory {
@Override
public Ship createShip() {
Ship ship = new Whiteship();
ship.setAnchor(new WhiteAnchor());
ship.setWheel(new WhiteWheel());
return ship;
}
}
setAnchor 에서 WhiteAnchor 타입이 아닌 다른 타입으로 바뀔경우 전부 수정하여야 한다. -> OCP 위반
public interface Anchor {
}
public class WhiteAnchorPro implements Anchor{
}
public interface Wheel{
}
public class WhiteWheelPro implements Wheel {
}
public interface ShipPartsFactory {
Anchor createAnchor();
Wheel createWheel();
}
public class WhitePartsProFactory implements ShipPartsFactory {
@Override
public Anchor createAnchor() {
return new WhiteAnchorPro();
}
@Override
public Wheel createWheel() {
return new WhiteWheelPro();
}
}
public class WhiteshipFactory extends DefaultShipFactory {
private ShipPartsFactory shipPartsFactory;
public WhiteshipFactory(ShipPartsFactory shipPartsFactory) {
this.shipPartsFactory = shipPartsFactory;
}
@Override
public Ship createShip() {
Ship ship = new Whiteship();
ship.setAnchor(shipPartsFactory.createAnchor());
ship.setWheel(shipPartsFactory.createWheel());
return ship;
}
}
위에서 만든 ShipPartsFactory 를 주입받아 Ship 객체를 생성하는 과정에서 추상화를 사용하여 더 유연하게 생성한다.
관점이 다르다.
목적이 조금 다르다.
생성자로 객체를 생성하는 경우에는 매개변수가 많아질수록 코드 리딩이 급격하게 떨어집니다. 빌더 패턴을 사용하면 매개변수가 많아져도 가독성을 높일 수 있습니다.
public class TourPlan {
private String title;
private int nights;
private int days;
private LocalDate startDate;
private String whereToStay;
private List<DetailPlan> plans;
public TourPlan() {
}
public TourPlan(String title, int nights, int days, LocalDate startDate, String whereToStay, List<DetailPlan> plans) {
this.title = title;
this.nights = nights;
this.days = days;
this.startDate = startDate;
this.whereToStay = whereToStay;
this.plans = plans;
}
@Override
public String toString() {
return "TourPlan{" +
"title='" + title + '\'' +
", nights=" + nights +
", days=" + days +
", startDate=" + startDate +
", whereToStay='" + whereToStay + '\'' +
", plans=" + plans +
'}';
}
public void addPlan(int day, String plan) {
this.plans.add(new DetailPlan(day, plan));
}
}
TourPlan 객체를 생성자로 생성할 경우 매개변수도 많고 복잡하게 생성하여야 한다. -> 빌더 패턴을 적용해보자.
public interface TourPlanBuilder {
TourPlanBuilder nightsAndDays(int nights, int days);
TourPlanBuilder title(String title);
TourPlanBuilder startDate(LocalDate localDate);
TourPlanBuilder whereToStay(String whereToStay);
TourPlanBuilder addPlan(int day, String plan);
TourPlan getPlan();
}
public class DefaultTourBuilder implements TourPlanBuilder {
private String title;
private int nights;
private int days;
private LocalDate startDate;
private String whereToStay;
private List<DetailPlan> plans;
@Override
public TourPlanBuilder nightsAndDays(int nights, int days) {
this.nights = nights;
this.days = days;
return this;
}
@Override
public TourPlanBuilder title(String title) {
this.title = title;
return this;
}
@Override
public TourPlanBuilder startDate(LocalDate startDate) {
this.startDate = startDate;
return this;
}
@Override
public TourPlanBuilder whereToStay(String whereToStay) {
this.whereToStay = whereToStay;
return this;
}
@Override
public TourPlanBuilder addPlan(int day, String plan) {
if (this.plans == null) {
this.plans = new ArrayList<>();
}
this.plans.add(new DetailPlan(day, plan));
return this;
}
@Override
public TourPlan getPlan() {
return new TourPlan(title, nights, days, startDate, whereToStay, plans);
}
}
빌더 패턴을 사용하기 위해 만들고자 하는 객체의 클래스와 동일한 필드를 가진 builder 클래스를 생성한다.
그리고 필드와 대응하는 메서드를 통해 builder 객체의 값을 하나하나 채운다.
빌더 객체를 통해 만들고자 하는 객체를 생성하고 싶은 경우 getInstance 메서드를 호출하여 builder 객체 내부에 만들고자 하는 인스턴스와 동일한 필드를 가지고 있으므로 builder 객체의 필드 값을 통해 새로운 객체를 생성한다.
public class TourDirector {
private TourPlanBuilder tourPlanBuilder;
public TourDirector(TourPlanBuilder tourPlanBuilder) {
this.tourPlanBuilder = tourPlanBuilder;
}
public TourPlan cancunTrip() {
return tourPlanBuilder
.title("칸쿤 여행")
.nightsAndDays(2, 3)
.startDate(LocalDate.of(2020, 12, 9))
.whereToStay("리조트")
.addPlan(0, "맘마")
.addPlan(1, "식사")
.getPlan();
}
}
director 를 통해 미리 세팅해놓은 객체를 반환할 수 있다.
• 만들기 복잡한 객체를 순차적으로 만들 수 있다.
• 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.
• 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들 수도 있다.
• 불완전한 객체를 사용하지 못하도록 방지할 수 있다.
• 원하는 객체를 만들려면 빌더부터 만들어야 한다.
• 구조가 복잡해 진다. (트레이드 오프)
public class GithubRepository {
private String user;
private String name;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class GithubIssue implements Cloneable {
private int id;
private String title;
private GithubRepository repository;
public GithubIssue(GithubRepository repository) {
this.repository = repository;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public GithubRepository getRepository() {
return 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 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;
}
@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);
}
@Override
public int hashCode() {
return Objects.hash(id, title, repository);
}
}
Object 의 clone 메서드를 사용하기 위해서는 복사 대상 클래스는 Clonable 인터페이스를 구현하여야 한다.
protected native Object clone() throws CloneNotSupportedException;
Object 클래스의 clone 메서드는 위와 같이 protected 이며 CloneNotSupportedException 을 예외로 던지고 있으므로 바로 사용할 수 없다.
public class App {
public static void main(String[] args) throws CloneNotSupportedException {
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);
GithubIssue clone = (GithubIssue) githubIssue.clone();
System.out.println(clone.getUrl());
repository.setUser("Keesun");
System.out.println(clone != githubIssue);
System.out.println(clone.equals(githubIssue));
}
}
Object 의 clone 메서드는 얕은 복사이다. 깊은 복사를 위해서는 따로 clone 메서드에 구현해줘야 한다.