[디자인패턴] 생성 패턴 - 프로토타입(Prototype) 패턴

 

 

 

*프로토타입 패턴

- 기존 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법

  • 복제 기능을 갖추고 있는 기존 인스턴스를 프로토타입으로 사용해 새 인스턴스를 만들 수 있다.

 

 

 

 

- Prototype, ConcretePrototypeA, ConcretePrototypeB

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class DatabaseInfo {
    private String db;
    private String host;
    private int port;
}
package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;

@Getter @Setter
public class JdbcConnectionInfo implements Cloneable{
    private int id;
    private String title;
    private DatabaseInfo databaseInfo;

    public JdbcConnectionInfo(DatabaseInfo databaseInfo) {
        this.databaseInfo = databaseInfo;
    }

    public String getConnection() {
        return String.format("jdbc:%s://%s/%d",
                this.databaseInfo.getDb(),
                this.databaseInfo.getHost(),
                this.databaseInfo.getPort());
    }

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

Prototype 인터페이스는 java에서 제공하는 Object 클래스에 있는 clone 메소드를 사용한다. 그러기 위해선 Cloneable 인터페이스를 구현해야 하는데 Clonable 인터페이스를 구현하면 clone 메소드를 오버라이딩 해서 사용할 수 있다. 기본 오버라이딩을 하게되면 부모 클래스에 있는 clone을 가져온다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

public class App {
    public static void main(String[] args) throws CloneNotSupportedException {
        DatabaseInfo databaseInfo = new DatabaseInfo();
        databaseInfo.setDb("mysql");
        databaseInfo.setHost("localhost");
        databaseInfo.setPort(3306);

        JdbcConnectionInfo jdbcConnection = new JdbcConnectionInfo(databaseInfo);
        jdbcConnection.setId(1);
        jdbcConnection.setTitle("first connection");

        String connection = jdbcConnection.getConnection();
        System.out.println(connection); // jdbc:mysql://localhost/3306

        JdbcConnectionInfo clone = (JdbcConnectionInfo) jdbcConnection.clone();
        
        databaseInfo.setDb("oracle");
        
        System.out.println(clone != jdbcConnection); // true
        System.out.println(clone.equals(jdbcConnection)); // false
        System.out.println(clone.getClass() == jdbcConnection.getClass()); // true
        System.out.println(clone.getDatabaseInfo() == jdbcConnection.getDatabaseInfo()); // true

        System.out.println(jdbcConnection.getConnection()); 
        // jdbc:oracle://localhost/3306
        System.out.println(clone.getConnection()); 
        // jdbc:oracle://localhost/3306
    }
}

Object의 clone 메소드를 사용하면 복제하려고 하는 인스턴스의 정보를 가져올 수 있으며, 이 방법은 싱글톤이 아니다. 객체는 다른 객체의 레퍼런스를 참조하지만 값은 그대로 복제 할 수 있다.

여기서, clone 메소드는 기본적으로 깊은 복사(deep copy)가 아닌 얕은 복사(shallow copy)를 한다. 따라서 JdbcConnectionInfo(ConcretePrototypeA)에서 사용하고 있는 DataBaseInfo(ConcretePrototypeB)는 기존의 인스턴스와 clone 인스턴스가 같은 레퍼런스를 참조한다.

 

 

- ConcreatePrototypeA(equals, hashCode 재정의)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;

@Getter @Setter
public class JdbcConnectionInfo implements Cloneable{
    ...

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

    @Override
    public int hashCode() {
        return Objects.hash(id, title, databaseInfo);
    }

	...
}

먼저 equals 메소드와 같이 객체의 주소 값이 아닌 객체 안에 있는 값들의 비교를 원한다면 equals 메소드를 재정의 할 필요가 있다. 이 때 hashCode 메소드도 같이 변경한다.

 

 

- Client(equals, hashCode 재정의)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

public class App {
    public static void main(String[] args) throws CloneNotSupportedException {
        ...
        
        System.out.println(clone != jdbcConnection); // true
        System.out.println(clone.equals(jdbcConnection)); // true
        System.out.println(clone.getClass() == jdbcConnection.getClass()); // true
        System.out.println(clone.getDatabaseInfo() == jdbcConnection.getDatabaseInfo()); // true

        ...
    }
}

clone과 jdbcConnection의 equals 비교값이 바뀌었다.

 

 

- ConcretePrototypeA(clone 메소드 수정)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;

@Getter @Setter
public class JdbcConnectionInfo implements Cloneable{
    ...

    @Override
    protected Object clone() throws CloneNotSupportedException {
        DatabaseInfo databaseInfo = new DatabaseInfo();
        databaseInfo.setDb(this.databaseInfo.getDb());
        databaseInfo.setHost(this.databaseInfo.getHost());
        databaseInfo.setPort(this.databaseInfo.getPort());

        JdbcConnectionInfo jdbcConnection = new JdbcConnectionInfo(databaseInfo);
        jdbcConnection.setId(this.getId());
        jdbcConnection.setTitle(this.getTitle());
        
        return jdbcConnection;
    }

    ...
}

얕은 복사를 깊은 복사로 만드는 방법은 clone 메소드를 재정의 하는 방법을 사용할 수 있다. ConcretePrototype에서 clone을 통해 복사할 데이터의 범위를 직접 명시하면된다.

 

 

- Client(clone 메소드 수정)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

public class App {
    public static void main(String[] args) throws CloneNotSupportedException {
        DatabaseInfo databaseInfo = new DatabaseInfo();
        databaseInfo.setDb("mysql");
        databaseInfo.setHost("localhost");
        databaseInfo.setPort(3306);

        JdbcConnectionInfo jdbcConnection = new JdbcConnectionInfo(databaseInfo);
        jdbcConnection.setId(1);
        jdbcConnection.setTitle("first connection");

        String connection = jdbcConnection.getConnection();
        System.out.println(connection); // jdbc:mysql://localhost/3306

        JdbcConnectionInfo clone = (JdbcConnectionInfo) jdbcConnection.clone();
        
        databaseInfo.setDb("oracle");
        
        System.out.println(clone != jdbcConnection); // true
        System.out.println(clone.equals(jdbcConnection)); // false
        System.out.println(clone.getClass() == jdbcConnection.getClass()); // true
        System.out.println(clone.getDatabaseInfo() == jdbcConnection.getDatabaseInfo()); // false

        System.out.println(jdbcConnection.getConnection()); 
        // jdbc:oracle://localhost/3306
        System.out.println(clone.getConnection()); 
        // jdbc:mysql://localhost/3306
    }
}

얕은 복사에서 깊은 복사로 변경 후에 ConcretePrototypeA(JdbcConnectionInfo)가 참조하는 ConcreatePrototypeB(DatabaseInfo)의 정보가 변경되었을 때 서로 다른 레퍼런스를 참조하기 때문에 equals 메소드나 참조하고 있는 databaseInfo의 값이 다르다는 것을 알수있다. 또한 서로 값 변경에 대해 영향을 주지 않는다.

 

 

- 장점

  • 복잡한 객체를 만드는 과정을 숨길 수 있다.
  • 기존 객체를 복제하는 과정이 새 인스턴스를 만드는 것보다 비용(시간 또는 메모리)적인 면에서 효율적일 수도 있다.
  • 추상적인 타입을 리턴할 수 있다.

- 단점

  • 복제한 객체를 만드는 과정 자체가 복잡할 수 있다. (특히, 순환 참조가 있는 경우)

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

[디자인패턴] 생성 패턴 - 빌더(Builder) 패턴

 

 

 

*빌더 패턴

- 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법.

  • (복잡한) 객체를 만드는 프로세스를 독립적으로 분리할 수 있다.

 

 

 

 

- Product

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Getter @Setter
public class Photograph {
    private String title;
    private String place;
    private LocalDate date;
    private String dress;
    private List<Suit> suits;

    public Photograph() {
        if (this.suits == null) {
            this.suits = new ArrayList<Suit>();
        }
    }

    public Photograph(String title, String place, LocalDate date, String dress, List<Suit> suits) {
        this.title = title;
        this.place = place;
        this.date = date;
        this.dress = dress;
        this.suits = suits;
    }

    public void addSuit(String color, String shape) {
        this.suits.add(new Suit(color, shape));
    }

    @Override
    public String toString() {
        return "Photograph{" +
                "title='" + title + '\'' +
                ", place='" + place + '\'' +
                ", date=" + date +
                ", dress='" + dress + '\'' +
                ", suits=" + suits +
                '}';
    }
}
package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class Suit {
    private String color;
    private String shape;

    public Suit(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }

    @Override
    public String toString() {
        return "Suit{" +
                "color='" + color + '\'' +
                ", shape='" + shape + '\'' +
                '}';
    }
}

추후 빌더 패턴을 통해서 만들어낼 인스턴스의 Product(데이터)이다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public class App {
    public static void main(String[] args) {
        Photograph firstPhotograph = new Photograph();
        firstPhotograph.setTitle("wedding photos");
        firstPhotograph.setDate(LocalDate.of(2022, 8, 20));
        firstPhotograph.setPlace("the studio");
        firstPhotograph.setDress("White flowing dress");
        firstPhotograph.addSuit("beige", "basic");
        firstPhotograph.addSuit("navy", "stripe");
        System.out.println("firstPhotograph = " + firstPhotograph);

        Photograph secondPhotograph = new Photograph();
        secondPhotograph.setTitle("wedding photos");
        secondPhotograph.setDate(LocalDate.of(2022, 8, 20));
        secondPhotograph.setPlace("the outside");
        secondPhotograph.setDress("Pink flowing dress");
        secondPhotograph.addSuit("brown", "basic");
        System.out.println("secondPhotograph = " + secondPhotograph);

        Photograph thirdPhotograph = new Photograph();
        thirdPhotograph.setTitle("wedding photos");
        thirdPhotograph.setDate(LocalDate.of(2022, 8, 20));
        thirdPhotograph.setPlace("the outside");
        thirdPhotograph.setDress("White slim dress");
        thirdPhotograph.addSuit("charcoal", "basic");
        System.out.println("thirdPhotograph = " + thirdPhotograph);
    }
}

빌더 패턴을 사용하지 않고 인스턴스를 만들게 되면 위와 같이 setter나 생성자를 통해서 인스턴스를 만들 수 있지만 입력해야 하는 값이 많아질수록 setter와 생성자는 입력해야 하는 값이 많아지고 또한 생성자는 값을 입력하지 않으려면 null 값이나 기본값을 입력해야 한다.

 

 

- Builder

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public interface PhotographBuilder {
    PhotographBuilder title(String title);

    PhotographBuilder place(String place);

    PhotographBuilder startDate(LocalDate date);

    PhotographBuilder choiceDress(String dress);

    PhotographBuilder addSuit(String color, String shape);

    Photograph getPlan();
}

Builder 인터페이스를 만들고 필요한 메소드를 만든다. 각 메소드들은 Builder 인터페이스를 타입으로 갖게되어 Builder 인터페이스를 구현하는 구현체로 반환된다. 따라서 메소드 체인처럼 사용하고자 하는 메소드를 사용할 수 있다. 마지막에 Product를 반환하는 메소드를 만든다.

 

 

- ConcreteBuilder

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class DefaultPhotographBuilder implements PhotographBuilder{

    private String title;
    private String place;
    private LocalDate date;
    private String dress;
    private List<Suit> suits;

    @Override
    public PhotographBuilder title(String title) {
        this.title = title;
        return this;
    }

    @Override
    public PhotographBuilder place(String place) {
        this.place = place;
        return this;
    }

    @Override
    public PhotographBuilder startDate(LocalDate date) {
        this.date = date;
        return this;
    }

    @Override
    public PhotographBuilder choiceDress(String dress) {
        this.dress = dress;
        return this;
    }

    @Override
    public PhotographBuilder addSuit(String color, String shape) {
        if (this.suits == null) {
            this.suits = new ArrayList<>();
        }
        this.suits.add(new Suit(color, shape));
        return this;
    }

    @Override
    public Photograph getPlan() {
        return new Photograph(this.title, this.place, this.date, this.dress, this.suits);
    }

    @Override
    public String toString() {
        return "DefaultPhotographBuilder{" +
                "title='" + title + '\'' +
                ", place='" + place + '\'' +
                ", date=" + date +
                ", dress='" + dress + '\'' +
                ", suits=" + suits +
                '}';
    }
}

추상화된 빌더 인터페이스를 구현하는 클래스이다. 각 메소드마다 Builder 인터페이스를 구현하는 구현체를 반환 해야하기 때문에 this를 리턴하고 마지막에 모든 값들을 반환하는 Product를 리턴한다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public class BuilderApp {
    public static void main(String[] args) {
        PhotographBuilder photographBuilder = new DefaultPhotographBuilder();
        Photograph firstPhotograph = photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the studio")
                .choiceDress("White flowing dress")
                .addSuit("beige", "basic")
                .addSuit("navy", "stripe")
                .getPlan();
        System.out.println("firstPhotograph = " + firstPhotograph);

        Photograph secondPhotograph = photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("Pink flowing dress")
                .addSuit("brown", "basic")
                .getPlan();
        System.out.println("secondPhotograph = " + secondPhotograph);

        Photograph thirdPhotograph = photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("White slim dress")
                .addSuit("charcoal", "basic")
                .getPlan();
        System.out.println("thirdPhotograph = " + thirdPhotograph);

    }
}

빌더 패턴을 적용하고 인스턴스를 적용했다. 기존에 setter나 생성자를 사용하는 방식은 장황하게 만들어지는 데이터를 입력하는 부분에 수 많은 null값이나 기본 값을 넣었어야 한다. 빌더 패턴은 사용하고자 하는 부분만 입력하고 반환하면 된다.

 

 

- Director

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public class PhotographDirector {

    private PhotographBuilder photographBuilder;

    public PhotographDirector(PhotographBuilder photographBuilder) {
        this.photographBuilder = photographBuilder;
    }

    public Photograph firstPhotograph() {
        return this.photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the studio")
                .choiceDress("White flowing dress")
                .addSuit("beige", "basic")
                .addSuit("navy", "stripe")
                .getPlan();
    }

    public Photograph secondPhotograph() {
        return this.photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("Pink flowing dress")
                .addSuit("brown", "basic")
                .getPlan();
    }

    public Photograph thirdPhotograph() {
        return this.photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("White slim dress")
                .addSuit("charcoal", "basic")
                .getPlan();
    }
}

또한 빌더 패턴에서 만들어지는 프로세스 등이 자주 반복이 된다면 미리 만들어져있는 부분을 Director에 넣어놓고 재사용할 수 있다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

public class DirectorApp {
    public static void main(String[] args) {
        PhotographDirector photographDirector = new PhotographDirector(new DefaultPhotographBuilder());
        Photograph FirstPhotograph = photographDirector.firstPhotograph();
        System.out.println("FirstPhotograph = " + FirstPhotograph);
        Photograph secondPhotograph = photographDirector.secondPhotograph();
        System.out.println("secondPhotograph = " + secondPhotograph);
        Photograph thirdPhotograph = photographDirector.thirdPhotograph();
        System.out.println("thirdPhotograph = " + thirdPhotograph);
    }
}

Client는 Builder가 아닌 Director를 통해서 자주 사용되거나 반복되는 Photograph에 관한 인스턴스를 구현 할 수 있다.

 

 

 

- 장점

  • 만들기 복잡한 객체를 순차적으로 만들 수 있다.
  • 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.
  • 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들 수도 있다.
  • 불완전한 객체를 사용하지 못하도록 방지할 수 있다.

- 단점

  • 원하는 객체를 만들려면 빌더부터 만들어야 한다.
  • 구조가 복잡해 진다. (트레이드 오프)

 

빌더 패턴은 만드는 순서가 복잡한 인스턴스 같은 경우는 빌더 패턴의 메소드를 사용해야 하는 순서를 통해서만 만들어지도록 사용하는 방식을 강제할 수도 있다.

생성자 하나만으로 사용하게 되어 복잡해질 수 있는 부분을 분산할 수도 있다. 예를 들어, 입력 받은 값들을 검증하는 코드 등을 추가하는 등의 작업을 할 수 있다.

클라이언트 코드가 깔끔하게 만들 수 있고 같은 프로세스에 하위클래스 등을 만들어서 추가적이고 세부적인 것들이 추가 될 수 있다. (VIP용 등)

getPlan()과 같은 빌더패턴을 종결하는 메소드를 호출하기 전까진 메소드를 사용할 수 없기 때문에 에러를 방지할 수 있다.

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

[디자인패턴] 생성 패턴 - 추상 팩토리(Abstract Factory) 패턴

 

 

 

*추상 팩토리 패턴

- 서로 관련있는 여러 객체를 만들어주는 인터페이스

  • 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다. (클라이언트에서 사용하는 인스턴스를 만들어 쓰는 코드를 인터페이스로 추상화한다)

- 구체적인 팩토리에서 구체적인 인스턴스를 만드는 부분은 팩토리 메소드 패턴과 비슷하지만 초점은 팩토리를 사용하는 클라이언트에 맞춰져 있다. 

 

 

 

 

- Product(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

import lombok.Getter;
import lombok.Setter;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Oxidizer;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Tonic;

@Getter @Setter
public class HairDye {
    ...
    
    private DarkOxidizer oxidizer;
    private DarkTonic tonic;

    ...
}

기존의 구현체에 필요한 데이터 필드를 추가한다.

 

 

- ConcreateProductA, ConcreateProductB

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkOxidizer{
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkTonic{
}

추가된 구현체에 대한 클래스를 만든다.

 

 

- ConcreteCreator(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DarkHairDye;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DefaultHairDesigner;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDye;

public class DarkHairDesigner extends DefaultHairDesigner {
    @Override
    public HairDye dyingService() {
        HairDye hairDye = new DarkHairDye();
        hairDye.setOxidizer(new DarkOxidizer());
        hairDye.setTonic(new DarkTonic());
        return hairDye;
    }
}

팩토리(creator)에 구현체에 추가된 Oxidizer나 Tonic를 구현하기 위해서 현재는 new 키워드를 통해 직접적으로 구현하는데, 추상 팩토리 패턴으로 클라이언트 측에서 사용하는 구현체에 따라 인스턴스가 정해지도록 한다.

 

 

- Abstract Factory

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public interface HairDyeProducts {

    Oxidizer useOxidizer();

    Tonic useTonic();
}

추상 팩토리 패턴으로 만들 인터페이스를 정의한다. 이 부분엔 추가되는 구현체(Product)를 정의한다.

 

 

- ProductA, ProductB

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public interface Oxidizer {
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public interface Tonic {
}

추상 팩토리에서 사용될 구현체(Product)의 인터페이스를 정의한다.

 

 

- ConcreteProductA, ConcreteProductB

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkOxidizer implements Oxidizer{
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkTonic implements Tonic{
}

인터페이스로 정의된 구현체를 구체적인 클래스를 정의한다.

 

 

- ConcreteFactory

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkHairDyeProducts implements HairDyeProducts{
    @Override
    public Oxidizer useOxidizer() {
        return new DarkOxidizer();
    }

    @Override
    public Tonic useTonic() {
        return new DarkTonic();
    }
}

추상 팩토리를 구현할 구현 클래스를 정의한다.

 

 

- ConcreteCreator(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DarkHairDye;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DefaultHairDesigner;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDye;

public class DarkHairDesigner extends DefaultHairDesigner {

    private HairDyeProducts hairDyeProducts;

    public DarkHairDesigner(HairDyeProducts hairDyeProducts) {
        this.hairDyeProducts = hairDyeProducts;
    }

    @Override
    public HairDye dyingService() {
        HairDye hairDye = new DarkHairDye();
        hairDye.setOxidizer(hairDyeProducts.useOxidizer());
        hairDye.setTonic(hairDyeProducts.useTonic());
        return hairDye;
    }
}

기존 DarkHariDesigner에서 Oxidizer나 Tonic을 정의 하려면 new 인스턴스를 사용하기 때문에 의존성이 강하게 결합되어 있었지만, HairDyeProducts 인터페이스를 통해서 의존성의 결합을 느슨하게 변경했다.

 

 

추가적으로 형태는 같지만 다른 기능을 하는 클래스를 추가하는 경우,

 

- ConcreteProductC, - ConcreteProductD

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkExOxidizer implements Oxidizer{
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkExTonic implements Tonic{
}

다른 구현체(Product)를 추가한다.

 

 

- ConcreteFactory

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkHairDyeExProducts implements HairDyeProducts{
    @Override
    public Oxidizer useOxidizer() {
        return new DarkExOxidizer();
    }

    @Override
    public Tonic useTonic() {
        return new DarkExTonic();
    }
}

추상 팩토리를 구현하는 또 다른 구현체를 정의한다.

 

 

- Product(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

import lombok.Getter;
import lombok.Setter;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Oxidizer;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Tonic;

@Getter @Setter
public class HairDye {
    ...
    
    private Oxidizer oxidizer;
    private Tonic tonic;

    ...
}

기존에 DarkOxidizer, DarkTonic 등의 클래스 타입으로 되어있는 부분을 느슨한 결합을 위해 인터페이스 타입으로 수정한다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDesigner;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDye;

public class Client {
    public static void main(String[] args) {
        HairDesigner hairDesigner = new DarkHairDesigner(new DarkHairDyeProducts());
        HairDesigner hairDesigner = new DarkHairDesigner(new DarkHairDyeExProducts());

        HairDye hairDye = hairDesigner.dyingService();
        System.out.println(hairDye.getOxidizer().getClass());
        System.out.println(hairDye.getTonic().getClass());
        
//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkOxidizer
//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkTonic

//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkExOxidizer
//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkExTonic        
    }
}

클라이언트에서 구조는 같지만 비슷한 형태의 인스턴스를 변경하기만 하면 다른 결과 값을 얻을 수 있다.

 

 

 

 

팩토리 메소드 패턴과 추상 팩토리 패턴은 비슷하다. 이 두가지는 관점의 차이이다. 팩토리 메소드 패턴은 객체 또는 인스턴스를 만드는(구현하는) 과정에 관점이 집중이 되어있고, 추상 팩토리 패턴은 팩토리를 사용하는 쪽(클라이언트)의 관점에 집중하여 보고있다.

  • 팩토리 메소드 패턴: 구체적인 객체 생성 과정을 하위 또는 구체적인 클래스로 옮기는게 목적
  • 추상 팩토리 패턴: 관련있는 여러 객체를 구체적인 클래스에 의존하지 않고 만들 수 있게 하는것이 목적

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

[디자인패턴] 생성 패턴 - 팩토리 메소드(Factory method) 패턴

 

 

 

*팩토리 메소드 패턴

- 구체적으로 어떤 인스턴스를 만들지 서브 클래스에서 정하게 한다.

  • 다양한 구현체(Product)가 있고, 그 중에서 특정한 구현체를 만들 수 있는 다양한 팩토리(Creator)를 제공할 수 있다.

- 확장엔 열려있고 변경엔 닫혀있는 OCP(Open Closed Principle)에 부합한다. 

 

 

 

 

 

예를 들어

 

HairDesigner(Creator)와 HairDye(Product)로 팩토리 메소드 패턴을 구현한다면

 

 

- Product

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class HairDye {
    private String colorNameKr;
    private String colorNameEn;
    private String colorCode;

    @Override
    public String toString() {
        return "사용된 염색약은 " +
                colorNameKr +
                "(" + colorNameEn + "), " +
                "code: " + colorCode +
                " 입니다";
    }
}

팩토리 메소드 패턴에서 Product, 팩토리에서 만들어 낼 데이터 오브젝트이다.

 

 

- concreteProductor

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class DarkHairDye extends HairDye{
    public DarkHairDye() {
        setColorCode("2N");
        setColorNameEn("Dark");
        setColorNameKr("흑색");
    }
}
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class BrownHairDye extends HairDye{
    public BrownHairDye() {
        setColorCode("5N");
        setColorNameEn("Brown");
        setColorNameKr("자연갈색");
    }
}

Product의 규격은 같으면서 다른 값들의 오브젝트를 구체화한다.

 

 

- Creator

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public interface HairDesigner {
    default HairDye dying(String color, int time) {
        validate(color);
        selectedDesigner();
        HairDye hairDye = dyingService();
        spreadHairDye(color);
        waitFor(time);
        return hairDye;
    }

    void selectedDesigner();

    HairDye dyingService();

    private void validate(String color) {
        if (color == null || color.isBlank()) {
            throw new IllegalArgumentException("색상이 선택되지 않았습니다");
        }
    }

    private void spreadHairDye(String color) {
        System.out.println(color + " 염색약을 바릅니다");
    }

    private void waitFor(int time) {
        System.out.println(time + "분간 기다립니다");
    }
}
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public abstract class DefaultHairDesigner implements HairDesigner {
    public void selectedDesigner() {
        System.out.println("일반 디자이너 선생님이 작업합니다.");
    }
}

Product를 사용할 팩토리(Creator)의 기본적인 규격을 정해놓은 추상화된 객체이다. DefaultHairDesigner 같은 추상 클래스의 경우는 사용해도 되고 사용하지 않아도 된다. 추가적으로 디자이너를 선택할 수 있는 기능이 추가되면 파라미터로 변경할 수 있게 만들어도 된다. java 9 버전부터 interface에서 private 제한자를 사용할 수 있지만, 하위 버전에선 사용할 수 없다. 

 

 

- ConcreteCreator

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class DarkHairDesigner extends DefaultHairDesigner {
    @Override
    public HairDye dyingService() {
        return new DarkHairDye();
    }
    
}
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class BrownHairDesigner extends DefaultHairDesigner {
    @Override
    public HairDye dyingService() {
        return new BrownHairDye();
    }
}

팩토리(Creator)에서 추상화된 구현체를 구체화 한다. 각각의 클래스가 정해진 기능만을 수행할 수 있도록 한다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class Client {
    public static void main(String[] args) {
        Client client = new Client();
        client.print(new DarkHairDesigner(), "검정색", 40);
        client.print(new BrownHairDesigner(), "갈색", 30);
    }
//        일반 디자이너 선생님이 작업합니다.
//        검정색 염색약을 바릅니다
//        40분간 기다립니다
//        사용된 염색약은 흑색(Dark), code: 2N 입니다

//        일반 디자이너 선생님이 작업합니다.
//        갈색 염색약을 바릅니다
//        30분간 기다립니다
//        사용된 염색약은 자연갈색(Brown), code: 5N 입니다

    private void print(HairDesigner hairDesigner, String color, int time) {
        System.out.println(hairDesigner.dying(color, time));
    }
}

사용자(Client)가 팩토리 메소드 패턴을 통해서 동일한 규격의 다른 데이터들을 구현할 수 있다.

 

 

 

 

팩토리 메소드 패턴은 팩토리(Creator)가 특정한 구현체(Product)를 다양한 방법으로 구현할 수 있다. 또한 기존의 팩토리(DarkHairDesigner, BrownHairDesigner) 뿐만 아니라 추가적인 팩토리(ex. YellowHairDesigner)를 만들 수 있으며(Open) 추가 하더라도 기존의 코드를 변경 또는 수정하지 않을 수 있다.(Closed)

하지만 단점으로, 기능을 계속 추가하게 되면 각자의 역할이 세분화 되어있기 때문에 클래스가 늘어나는 부분이 생긴다.

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

[디자인패턴] 생성 패턴 - 싱글톤(Singleton) 패턴

 

 

*싱글톤 패턴

- 인스턴스를 오직 한개만 제공하는 클래스

  • 인스턴스가 여러개 있을때 문제가 발생할 수 있는 경우를 대비해 한개의 인스턴스만 제공하는 클래스가 필요하다.

 

 

- 기본적인 싱글톤패턴

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if(instance == null){
      instance = new Singleton();
    }
    
    return instance;
  }
}
public class App {
 public static void main(String[] arg){
   Singleton singleton = Singleton.getInstance;
   System.out.println(singleton == Singleton.getInstance); // true
 }
}

위와 같은 경우는 하나의 싱글톤 인스턴스를 반환하지만 문제가 있다. 웹 애플리케이션 같은 멀티스레드 환경에선 이 코드가 안전하지 않다.(Thread-Safe 하지 않다)

 

멀티스레드 환경에서 안전하려면 다음과 같은 방법으로 구현할 수 있다.

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static synchronized Singleton getInstance() {
    if(instance == null){
      instance = new Singleton();
    }
    
    return instance;
  }
}

synchronized 키워드를 사용하게되면 여러 스레드에서 getInstance 메소드에 동시에 접근할 때, Thread-Safe하게 접근할 수 있다. 멀티 스레드 환경에서 synchroinzed 키워드를 만났을 때, 어떤 스레드가 해당 부분에 진입한 상태라면 다른 스레드는 그 스레드의 작업을 끝날 때까지 대기한다. 하지만 synchronized는 내부적으로 동기화라는 매커니즘으로 locking 작업을 하기때문에 성능적은 불이익이 생길 수 있다.

 

 

 

그래서 성능을 조금 더 신경쓰고 싶다면 다른 방법을 사용할 수도 있다.

public class Singleton {
  private static final Singleton INSTANCE = new Singleton();
  
  private Singleton() {}
  
  public static Singleton getInstance() { 
    return INSTANCE;
  }
}

synchronized를 사용하지 않고 인스턴스를 먼저 생성한다. 이른 초기화라고 하는데 생성자를 먼저 생성한 후에 getInstance 메서드는 생성된 인스턴스만을 반환한다. Thread-Safe하며 synchronized 동기화에 관한 성능이슈도 없지만 미리 만드는것 자체가 이슈가 될 수 있다.

생성하는 인스턴스 자체로 메모리에 할당을 하게 되는데 만약 미리 만들어졌지만 애플리케이션 내에서 사용하지 않는다면 ? 이것 또한 자원의 낭비가 될 것이다.

 

 

그래서 또 다른 방법이 있다.

public class Singleton {
  private static volatile Singleton instance; // volatile java 1.5 버전 이상부터 사용
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if(instance == null) {
      synchronized (Singleton.class) {
        if(instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

synchronized 키워드를 사용하긴 하지만 메서드 레벨에서 사용하는 것이 아닌, 메서드 안의 로직에서 사용한다. double checked locking 이라는 기법으로 if 문을 통해서 2번의 체크하는 로직을 가진다.

여러 스레드가 동시에 getInstance 메서드에 진입하더라도 if문에서 한번 필터링을 하고(동기화 매커니즘까지 가지 않음) synchronized 블럭 안에서 한번 더 if로 필더링을 하기 때문에, 메서드에서 synchronized를 사용한 경우처럼 메서드를 호출할 때마다 동기화 locking 작업을 하지 않는다.

하지만, 이 방법은 복잡하다. 필드에 선언된 volatile을 사용한 이유는 자바 1.5버전 이하(자바 1.4부터)의 메모리처리 방법을 이해 해야한다.

 

 

따라서 또 다른 방법이 있다.

public class Singleton {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
}

이 방법은 권장하는 방법중 하나이다. 싱글톤을 static inner 클래스를 만들어서 구현하는 방법이다.

이 방법은

1) Thread-safe하며

2) double check locking처럼 복잡하지도 않고

3) getInstance를 호출할 때 클래스 로딩을 통해서 인스턴스를 생성하기 때문에 lazy loading도 가능하다.

 

 


 

 

이러한 방법들은 싱글톤을 보장하며 멀티스레드 환경에서 적절한지, 성능상으로도 괜찮은지, 버전에 따른 복잡한 절차가 들어 있는지에 대한 문제에 대해 자유로울 수 있다.

하지만, 이러한 싱글톤의 구현을 깨뜨리는 방법 또한 존재한다. 사용자가 정해진 방법이 아닌 다른 방법으로 사용한다면 이러한 싱글톤을 보장하지 않을수도 있다.

  • 리플렉션
  • 직렬화와 역직렬화

이 두가지의 방법을 이용하면 위의 싱글톤을 깨뜨린다.

 

 

- 리플렉션

public class App {
  public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessable(true); // private 접근
    Singleton singleton1 = constructor.newInstance();
    
    System.out.println(singleton == singleton1); // false
  }
}

우리가 의도한대로 만든 싱글톤과 리플렉션을 사용하여 클래스에 접근하여 만들어낸 생성자는 다른 인스턴스를 만들어낸다.

 

 

- 직렬화 & 역직렬화

public class Singleton implements Serializable {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
}
public class App {
  public static void main(String[] args) throws IOException {
    Singleton singleton = Singleton.getInstance();
    Singleton singleton1 = null;
    
    try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) {
      output.writeObject(singleton); // 직렬화
    }
    
    try (ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) {
      singleton1 = (Singleton) in.readObject(); // 역직렬화
    }
    
    System.out.println(singleton == singleton1); // false
  }
}

리플렉션과 마찬가지로 직렬화 & 역직렬화를 통해 만든 객체 인스턴스는 싱글톤으로 만들어낸 인스턴스와 다른 객체를 참조한다.

 

* 역직렬화 대응방안

public class Singleton implements Serializable {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
  
  protected Object readResolve() {
    return getInstance();
  }
}

명시적으로 readResolve 메서드가 있는건 아니지만 이 메서드는 역직렬화 될때 사용된다. 그래서 readResolve 메서드의 구현을 getInstance 메서드를 반환하게 바꿔주면 동일한 인스턴스를 얻을 수 있다.

 

 

 

권장하는 방법중 또 하나는 enum 타입으로 구현하는 방법이 있다.

public enum Singleton {
  INSTANCE;
}

enum 타입으로 싱글톤을 구현하게 되면 class로 구현하게 되어 생기는 리플렉션으로 싱글톤이 깨지는 현상에 대응 할수 있다. 

 

public class App {
  public static void main(String[] args) {
    Singleton singleton = Singleton.INSTANCE;
    Singleton singleton1 = null;
    
    Constructor<?>[] constructors = Singleton.class.getDeclaredConstructors();
    for (Constructor<?> constructor: constructors) {
      constructor.setAccessable(true);
      singleton1 = (singleton) constuctor.newInstance("INSTANCE"); // cannot reflectively create enum objects
    }
    
    System.out.println(singleton == singleton1);
  }
}

enum은 리플렉션에서 new를 통한 생성자를 만들수 없다. 따라서 유일한 인스턴스가 보장이 된다. enum은 역직렬화나 리플렉션과 같은 이탈행위를 방지할 수 있다. 하지만 단점은 enum은 클래스를 로딩하는 순간 미리 인스턴스가 만들어진다. 또한 enum은 오로지 enum만 상속 받을 수 있기때문에 다른 class를 상속 받을 수 없다.

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

[JAVA] JAVA, JVM, JDK, JRE

 

 

 

- JVM(Java Virture Machine)

자바 가상 머신으로 자바 바이트코드(.class) 파일을 해당하는 운영체제(OS)에 맞는 코드로 변환하여 실행한다.(컴파일)

 

- JRE(Java Runtime Environment)

JVM + 라이브러리이며, 자바를 실행 할 수 있도록 구성된 배포판이다. 자바 런타임 환경에서 사용하는 리소스 파일 등을 가지고 있다. (11버전부터 따로 제공되지 않음)

 

- JDK(Java Development kit)

JRE + 개발 툴이며, JRE에서 추가적으로 개발에 관련된 도구들을 포함한다. Java를 사용하는 개발자들은 JDK를 설치하여 개발한다.

 

 


*JVM의 구조

  • 클래스 로더
  • 메모리
  • 실행 엔진
  • 네이티브 메소드 인터페이스(JNI)
  • 네이티브 메소드 라이브러리

 

 

1) 클래스 로더

자바 클래스를 자바 가상 머신으로 동적 로드하는 자바 런타임 환경의 일부이다. 로딩, 링크, 초기화 등의 작업을 수행한다. 자바 클래스(.class)에서 바이트코드를 읽고 메모리에 저장한다. (메모리에 가기 전 바이트코드를 조작할 수 있다)

 

  • 로딩: 클래스로더가 바이트코드(.class)를 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들어 "메소드" 영역에 저장한다. 저장되는 데이터는 클래스, 인터페이스, 이늄, 클래스의 전체경로, 메소드, 변수 등이 있다. 로딩을 마치고나면 해당 클래스 타입의 Class 객체를 힙 영역에 저장한다.
  • 링크: 바이트코드(.class)를 검증, 준비, resolve 등의 작업을 거친다. 바이트코드(.class) 파일의 형식이 유효한지 체크하고 클래스 변수(static 변수)와 기본값에 필요한 메모리 등을 준비하고 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다.
  • 초기화: Static 변수의 값을 할당한다. (static 변수나 블럭을 이때 실행한다.)

 

 

2) 메모리

  • 스택: 쓰레드마다 런타임 스택을 만들고, 그 안에 메소드 호출을 스택으로 쌓는다. 쓰레드 종료시 런타임 스택도 사라진다.
  • PC: 쓰레드마다 쓰레드 내 현재 실행할 스택 프레임을 가리키는 포인터를 생성한다.
  • 네이티브 메소드 스택: 네이티브 메소드가 실행될 때, 스택을 만들고 네이티브 메소드 호출을 스택으로 쌓는다. 쓰레드에 따라 동작한다.
  • 힙: 객체를 저장 공유하는 자원이다.
  • 메소드: 클래스에 수준의 정보(클래스 이름, 메소드, 변수 등)를 저장 공유하는 자원이다.

 

 

3) 실행 엔진

  • 인터프리터: 바이트코드를 한줄씩 실행한다.
  • JIT 컴파일러: 인터프리터에서 바이트코드를 한줄씩 실행하면서 반복되는 바이트코드를 발견하면 JIT컴파일러로 반복되는 코드를 네이티브 코드로 바꿔두고 사용한다.(성능적 이점)
  • GC(Garbage Collector): 더 이상 참조되지 않는 객체를 모아서 정리한다.

 

 

4) JNI(Java Navtive Interface)

네이티브 인터페이스로 네이티브 메소드 라이브러리에 있는, 또는 native 키워드가 사용된 함수를 사용할 수 있는 방법을 제공한다. 

 

 

5) 네이티브 메소드 라이브러리

C, C++로 작성된 라이브러리이며 Java에선 native 키워드로 사용된 메소드로 되어있다.

 

 

 

 

[Spring Boot] IoC(제어의 역전)와 DI(의존주입) 스프링 컨테이너

 

 

우리는 자바란 프로그래밍 언어를 이용해서 프로젝트를 개발 시에 스프링이라는 프레임워크를 이용해서 개발한다. 스프링 프레임워크를 사용하는 이유는 여러가지가 있겠지만 가장 주된 이유로는 자바가 객체지향프로그래밍 언어라는 이유일 것이다.

 

객체지향 프로그래밍은 추상화, 캡슐화, 상속, 다형성을 가지며 이 특징들로 SOLID라는 5가지의 원칙을 만족하는 프로그래밍 방식을 의미한다. 이러한 특징들을 가진 프로그래밍 방식으로 여러개의 독립된 단위로 나누며 각각의 객체들의 역할과 구현방식으로 구분하여 프로그램을 유연하고 변경이 용이 하도록 만들 수 있다. 그러므로 확장이나 재 사용에 이점을 보인다.

 

설계를 할 때, 인터페이스와 구현체를 가지고 다형성을 만족하는 객체 지향 설계를 할 수있다. 하지만 다형성 만으로는 완전한 객체 지향 설계를 했다고 보기는 힘들다.

 

예를 들어

public class Member {

    private Long id;
    private String name;
    private boolean check; // 회원 여부
    private int count = 10000; // 가진 금액

    public Member(Long id, String name, boolean check) {
        this.id = id;
        this.name = name;
        this.check = check;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isCheck() {
        return check;
    }

    public void setCheck(boolean check) {
        this.check = check;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

}

Member라는 도메인과

 

public class MemberMain {

    public static void main(String[] args) {

        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

로직을 수행 할 Main

 

public interface MemberService {

    void enter(Member member);

}
public class MemberServiceImpl implements MemberService {

    private GradeMember gradeMember = new CommonGradeMember();

    @Override
    public void enter(Member member) {
        gradeMember.countInfo(member);
    }
}

회원 여부 별 입장에 관한 로직을 수행할 서비스 인터페이스와 서비스 구현체를 나눴다.

 

package com.example.demo.memberGrade;

import com.example.demo.domain.Member;

public interface GradeMember {

    void countInfo(Member member);

}
package com.example.demo.memberGrade;

import com.example.demo.domain.Member;

public class CommonGradeMember implements GradeMember {

    private final int MEMBER_PRICE = 4000;
    private final int COMMON_PRICE = 10000;

    @Override
    public void countInfo(Member member) {
        System.out.println("------------------------");
        System.out.println(member.getName() + "님, 회원 여부 : " + member.isCheck());

        checkResult(member);

        System.out.println("------------------------");
    }

    private void checkResult(Member member) {
        if (member.isCheck()){
            System.out.println("결제 될 금액은 " + MEMBER_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - MEMBER_PRICE) + "원");
        }else{
            System.out.println("결제 될 금액은 " + COMMON_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - COMMON_PRICE) + "원");
        }
    }
}

지불해야 할 금액에 관한 상세 인터페이스와 구현체로 나눴다.

 

 

Member의 정보 별로 입장에 관한 인터페이스와 구현체, Member의 회원 여부에 따른 금액 측정에 관한 인터페이스와 구현체로 나눈 것이다. main 함수를 실행하면

------------------------
memberA님, 회원 여부 : true
결제 될 금액은 4000원 입니다
남은 금액: 6000원
------------------------

의 형태로 출력된다.

 

 

인터페이스(역할)과 구현체(구현)을 분리하여 개발했고 다형성(polymorphism)을 통해 하나의 객체 타입으로 여러가지 타입을 가질 수 있게, 즉 인터페이스를 통해 해당 인터페이스를 구현한 여러가지 구현체를 가질 수 있도록 만들었다.

 

하지만 만약에 여기서 회원정보 별로 결제될 금액이 고정 값이 아니라 할인율을 통해서 할인이 된다고 바뀐다고하면 위에서 말한 SOLID의 5원칙을 지키지 못하게 된다.

 

public class VIPGradeMember implements GradeMember {

    private final int MEMBER_PRICE = 10;
    private final int COMMON_PRICE = 10000;

    @Override
    public void countInfo(Member member) {
        System.out.println("------------------------");
        System.out.println(member.getName() + "님, 회원 여부 : " + member.isCheck());

        checkResult(member);

        System.out.println("------------------------");
    }

    private void checkResult(Member member) {

        if (member.isCheck()){
            int discount = (COMMON_PRICE / MEMBER_PRICE);
            int resultCount = (member.getCount() - discount);

            System.out.println("할인 될 금액은 " + discount + "원 입니다");
            System.out.println("남은 금액: " + resultCount + "원");
        }else{
            System.out.println("결제 될 금액은 " + COMMON_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - COMMON_PRICE) + "원");
        }

    }
}

할인을 고정 값이 아닌 할인율을 통해 지정하는 구현체이다.

 

 

MemberServiceImpl 구현체가 GradeMember 인터페이스를 의존하면서 CommonGradeMember 구현체까지 의존하고 있다.

 

 

위의 VIPGradeMember 구현체로 변경하게되면 MemberServiceImpl 구현체에서 아래와 같은 작업을 해야한다.

// private GradeMember gradeMember = new CommonGradeMember();
private GradeMember gradeMember = new VIPGradeMember();

 

 

SOLID 5원칙 중 DIP(Dependency inversion principle)의 원칙에 따라 구현체에 의존하지 않으며 인터페이스에 의존해야 하기때문에 설계를 변경해야한다.

 

// private GradeMember gradeMember = new CommonGradeMember();
private final GradeMember gradeMember;

인터페이스에만 의존하도록 설계를 변경하게되어 실제로 구현체가 없어 예외가 발생한다. 그렇기 때문에 다른 곳에서 GradeMember의 구현 객체를 대신 생성하여 주입해야한다.

 

public class MemberServiceImpl implements MemberService {

    /*private GradeMember gradeMember = new CommonGradeMember();*/
    private final GradeMember gradeMember;

    public MemberServiceImpl(GradeMember gradeMember) {
        this.gradeMember = gradeMember;
    }

    @Override
    public void enter(Member member) {
        gradeMember.countInfo(member);
    }
}

MemberServiceImpl에서는 생성자를 통해 주입된 구현체의 인터페이스만 의존하여 처리하도록 변경했기때문에 GradeMember를 의존하는 구현체는 어떠한 것이든 받을 수 있고 구현체가 바뀌어도 따로 구조를 변경하지 않아도 된다.

 

public class MemberConfig {

    public MemberService memberServiceImpl() {

//        return new MemberServiceImpl(new CommonGradeMember());
        return new MemberServiceImpl(new VIPGradeMember());
    }
}
public class MemberMain {

    public static void main(String[] args) {

        MemberConfig memberConfig = new MemberConfig();
        MemberService memberService = memberConfig.memberServiceImpl();
//        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

위의 MemberConfig라는 클래스를 생성했다. 해당 클래스는 앞에서 할인율을 어떻게 적용할지 변경 할 때, 인터페이스와 구현체의 로직 변경은 일어나지 않고 단순히 구성 부분만 변경함으로 할인율을 변경 할 수 있다.

 

MemberConfig를 통해서 전체 동작 방식을 구성할 수 있다. 해당 클래스에서선 구현체를 생성하고 연결하는 역할을 하게 된다. 이로써 각 구현 객체들은 어떤 객체가 들어오게 되던 자신의 로직을 실행하는 역할만을 담당하고 전체 프로그램의 제어의 흐름은 MemberConfig 객체가 하게된다. 이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 한다.

 

 

프로그램의 의존관계를 보게되면 MemberServiceImpl은 GradeMember에 의존하지만 어떤 구현체가 사용 될지는 모른다. 의존 관계는 정적 클래스 의존관계와 동적 객체 인스턴스 의존 관계로 나누어진다.

 

- 정적 클래스 의존관계

  • 클래스가 사용하는 코드만 보고 의존관계를 판단 할 수 있다. MemberServiceImpl은 GradeMember에 의존한다.

- 동적 객체 인스턴스 의존관계

  • 애플리케이션 실행 시점에서 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다. CommonGradeMember객체를 구현체로 쓸건지 VIPGradeMember 구현체인지는 실행 시에 결정된다. 런타임 시에 객체를 생성하고 클라이언트와 서버의 실제 구현체와의 의존관계가 연결 되는 것을 의존관계 주입(DI)이라 한다. 

MemberConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. 

 


 

지금까지 작성한 IoC, DI는 순수하게 자바로만 구현한 것이다. 추가되어야 할 부분이 많지만 기본 틀은 이러한 방식을 사용했다. 스프링을 이용하게되면 IoC와 DI 컨테이너와 함께 그에 관련된 수 많은 추가 기능들을 제공한다. 앞에선 개발자가 MemberConfig와 같은 제어하는 객체를 직접 만들고 의존관계를 주입했지만 스프링에선 스프링이 의존관계를 주입해준다. 그것을 스프링 컨테이너라고 하고 여기에 등록된 객체를 스프링 빈이라고 한다.

 

@Configuration
public class MemberConfig {

    @Bean
    public MemberService memberServiceImpl() {

//        return new MemberServiceImpl(new CommonGradeMember());
        return new MemberServiceImpl(new VIPGradeMember());
    }
}
public class MemberMain {

    public static void main(String[] args) {

        ApplicationContext ac = new AnnotationConfigApplicationContext(MemberConfig.class);
        MemberService memberService = ac.getBean("memberServiceImpl", MemberService.class);

//        MemberConfig memberConfig = new MemberConfig();
//        MemberService memberService = memberConfig.memberServiceImpl();

//        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

기존에 DI 컨테이너로 사용하던 MemberConfig와 데이터를 입력하는 MemberMain을 스프링에서 지원하는 어노테이션 방식으로 사용을 변경했다. main함수에서 사용된 ApplicationContext 객체는 스프링 컨테이너이며 스프링컨테이너는 @Configuration이 붙은 MemberConfig를 설정 정보로 사용한다. 그리고 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체로 스프링 컨테이너에 등록한다. 다시 말해서 @Bean이 붙은 객체는 스프링 컨테이너에 등록되어 있으며 그것을 스프링 빈이라고 한다.

 

이처럼 스프링은 제어의역전(IoC)과 의존주입(DI)에 대해 기본적인 설정들을 편리하게 제공하며, 공통적으로 누구나 사용하면서 고려해볼 수 있는 무수히 많은 설정들을 제공한다.

+ Recent posts