Post

Design Pattern

✅ 디자인 패턴

소프트웨어 디자인 과정(🟰 코드 구현 전 설계) 전형적인 해결책

디자인 패턴 🟰 제품 제작 전 구상도
비즈니스 상황 별 최적의 설계 노하우/전략/공략법 정리
UML(Unified Modeling Language)로 객체 간 구조도 작성

✔️ 생성패턴

기존 코드의 재사용성 증가

  • 빌더 패턴
  • 싱글턴 패턴

✔️ 구조 패턴

구조를 유지하면서 더 큰 구조로 조립

  • 데코레이터 패턴

✔️ 행동패턴

알고리즘 및 객체 책임 할당

  • 전략 패턴

🧩 JAVA 싱글톤 패턴

단 하나 인스턴스만 생성 및 공유하여 자원 절약일관성 유지를 목적으로 하는 디자인 패턴

  • static
  • synchronized

filewriter 개선

파일을 여러번 읽고 쓸 때,
인스턴스를 여러번 만드는 것이 아니라
싱글톤 패턴 적용하여 한 번만 스레드 생성하게 만들어서 모든 스레드가 이 filewriter사용하도록 하기

👎🏻 기존 코드

비슷한 작업을 하는 객체 여러번 생성해야 함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//Filewriter.java
public class Filewriter {
    private String filename;

    public Filewriter(String filename) {
        this.filename = filename;
    }

    public void writeToFile(String message) {
        try {
            FileWriter fileWriter = new FileWriter(filename, true);
            fileWriter.write(message + "\n");
            fileWriter.flush();
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

//FilewriterTest.java
public class FilewriterTest {
    public static void main(String[] args) {
        //👎 같은 파일에 쓰기 위해 매번 객체 생성해야 한다.
        Thread thread1 = new Thread( () -> {
            Filewriter writer = new Filewriter("src/chap60_singleton/test.txt");
            writer.writeToFile("Thread 1: Message 1");
            writer.writeToFile("Thread 1: Message 2");
        });

        Thread thread2 = new Thread(() -> {
            Filewriter writer = new Filewriter("src/chap60_singleton/test.txt");
            writer.writeToFile("Thread 2: Message 3");
            writer.writeToFile("Thread 2: Message 4");
        });

        Thread thread3 = new Thread(() -> {
            Filewriter writer = new Filewriter("src/chap60_singleton/test.txt");
            writer.writeToFile("Thread 2: Message 3");
            writer.writeToFile("Thread 2: Message 4");

            //👎 JVM GC 회수 매번 이루어져야 함
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

👌🏻 싱글턴 사용한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//FilewriterSingleton.java
public class FilewriterSingleton {
    //⭐️ 스스로를 static으로 지정
    private static FilewriterSingleton instance;
    private FileWriter fileWriter;

    //FilewriterSingleton constructor
    public FilewriterSingleton() {
        try {
            //스스로를 만든다.
            //같은 파일에다가 쓸거니까 constructor에 바로 파일 정해버림
            this.fileWriter = new FileWriter("src/chap60_singleton/test.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }
        }
    //자기 자신의 인스턴스 생성
    //❗️FilewriterSingleton는 공통영역이므로 동기화 문제 발생 가능
    //⭐️ synchronized
    public synchronized static FilewriterSingleton getInstance(){
        if(instance == null){
            //인스턴스가 처음에는 null
            //null이면 FilewriterSingleton을 생성한다
            instance= new FilewriterSingleton();
        }
        //instance가 null이 아니고 이미 만들어져 있으면 이미 만들어진 instance return
        return instance;
    }

    public synchronized void writeToFile(String message){
        try{
            fileWriter.write(message+"\n");
            fileWriter.flush();
        } catch (IOException e){
            e.printStackTrace();
        }
    }
    public synchronized void closeFile(){
        try {
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

//FilewriterSingletonTest.java
public class FilewriterSingletonTest {
    public static void main(String[] args) {
        Thread thread1 = new Thread( () -> {
            // 객체 생성 매번 ❌
            // getInstance
            FilewriterSingleton  writer= FilewriterSingleton.getInstance();
            // writer.writeToFile("Thread 1: Message 1");
            // writer.writeToFile("Thread 1: Message 2");
        });
        //두 번째 호출부터는 FilewriterSingleton getInstance가 null이 아님
        //따라서 다시 만들지 않고 모든 thread가 이 객체 사용
        //따라서 singleton으로 코드 재사용성 높일 수 있다. ⬆️
        //❗️FilewriterSingleton는 공통영역이므로 동기화 문제 발생 가능
        //따라서 synchronize
        Thread thread2 = new Thread(() -> {
            FilewriterSingleton  writer= FilewriterSingleton.getInstance();
            // writer.writeToFile("Thread 2: Message 3");
            // writer.writeToFile("Thread 2: Message 4");
        });

        // thread1.start();
        // thread2.start();

        //join해서 파일 닫아야 함
        //GC도 FilewriterSingleton 재사용될 것 알아서 치워버리지 않음
    }
}

🧩 JAVA 빌더 패턴

복잡한 객체의 생성 과정을 단순화해서 가독성유연성 높여 객체 생성하기
객체 생성 과정을 1단계, 2단계, 3단계…이렇게 단순화해서

  • static inner class
  • 내부 this 변환

userBuilder 개선

유저 객체 생성할 때 이름, 이메일 등 순서를 헷갈릴 수 있음
실수할 수 있는 여지를 줄이기
Builder 이용하면 순서 바꿔서 입력해도 괜찮음.

👎🏻 기존 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//User.java
public class User {
    private String firstName;
    private String lastName;
    private int age;
    private String email;

    public User(String firstName, String lastName, int age, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.email = email;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }

    public String getEmail() {
        return email;
    }
}

//BuilderTest.java
public class BuilderTest {
    public static void main(String[] args) {
        // 적용 전
        User user1 = new User("John", "Doe", 30, "johndoe@example.com");
        System.out.println("적용 전 User: " + user1);
    }
}

👌🏻 빌더 패턴 사용한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//User.java
public class User {
    // private String firstName;
    // private String lastName;
    // private int age;
    // private String email;

    // public User(String firstName, String lastName, int age, String email) {
    //     this.firstName = firstName;
    //     this.lastName = lastName;
    //     this.age = age;
    //     this.email = email;
    // }

    //⭐️ 새로운 User생성자
    //private
    //️UserBuilder를 아래에서 받아온다.
    private User(UserBuilder userBuilder){
        this.firstName= userBuilder.firstName;
        this.lastName= userBuilder.lastName;
        this.age= userBuilder.age;
        this.email= userBuilder.email;
    }

    //⭐️UserBuilder는 static inner 클래스로 만든다.
    static class UserBuilder{
        //⭐️ user클래스의 필드 그대로 가지고 있는다.
         String firstName;
         String lastName;
         int age;
         String email;

        //⭐️ UserBuilder 내부 정적 클래스의 constructor
        //빈 constructor을 생성한다.
        //UserBuilder 내부 정적 클래스 안에서 constructor 정의해야 함 유의!
        public UserBuilder() {}

        //⭐️ method
        //UserBuilder을 반환하는 메소드
        public UserBuilder firstName(String firstName){
           this.firstName = firstName;
           return this;
        }
        public UserBuilder lastName(String lastName){
            this.lastName= lastName;
            return this;
        }
        public UserBuilder age(int age){
            this.age= age;
            return this;
        }
        public UserBuilder email(String email){
            this.email= email;
            return this;
        }
        //⭐⭐⭐ 최종 user 반환하는 method
        public User build(){
            return new User(this);
        }

    }

    // public String getFirstName() {
    //     return firstName;
    // }

    // public String getLastName() {
    //     return lastName;
    // }

    // public int getAge() {
    //     return age;
    // }

    // public String getEmail() {
    //     return email;
    // }

    // @Override
    // public String toString() {
    //     return "User{" +
    //             "firstName='" + firstName + '\'' +
    //             ", lastName='" + lastName + '\'' +
    //             ", age=" + age +
    //             ", email='" + email + '\'' +
    //             '}';
    // }
}

public class BuilderTest {
    public static void main(String[] args) {
        // 적용 전
        // User user1 = new User("John", "Doe", 30, "johndoe@example.com");
        // System.out.println("적용 전 User: " + user1);

        //Builder 적용 후
        //이름은 이거, 나이는 이거...이렇게 명시하니 헷갈리지 않아서 좋음
        User user2= new User.UserBuilder()
                .firstName("John")
                .lastName("Doe")
                .age(30)
                .email("johndoe@example.com")
                .build();
        System.out.println("빌더 적용 후 User: " + user2);

        //Builder와 함께하면 순서 바꿔도 상관 없음
        User user3= new User.UserBuilder()
                .email("johndoe@example.com")
                .lastName("Doe")
                .age(30)
                .firstName("John")
                .build();
        System.out.println("빌더 적용하면 순서 바꿔도 상관 없어: " + user3);

    }
}

🧩 JAVA 데코레이터 패턴

기존 객체 변경 없이 동적으로 기능을 추가하거나 수정하는 디자인 패턴
기존 객체에 옵션이나 기능을 추가해야 하는 상황에 주로 사용

  • 추상클래스
  • 인터페이스
  • upcasting

커피 만드는 코드

그냥 커피에 우유, 설탕, 크림 추가하는 코드

  • implements inferface
  • extends class
  • upcasting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//✅ Beverage interface
public interface Beverage {
    String getDescription();
    double cost();
}

//✅ BeverageDecorator.java
public class BeverageDecorator implements Beverage{
    protected Beverage beverage;

    public BeverageDecorator(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription();
    }

    @Override
    public double cost() {
        return beverage.cost();
    }
}

//✅ Coffee.java
public class Coffee implements Beverage{
    @Override
    public String getDescription() {
        return "Coffee";
    }

    @Override
    public double cost() {
        return 5.0;
    }
}

//✅ milk.java
public class Milk extends BeverageDecorator{
    public Milk(Beverage beverage) {
        super(beverage);
    }

    //👌🏻 데코레이터 사용한 코드
    @Override
    public String getDescription() {
        return super.getDescription() + ", Milk";
    }
    //👌🏻 데코레이터 사용한 코드
    @Override
    public double cost() {
        return super.cost() + 0.5;
    }
}

//✅ sugar.java
public class Sugar extends BeverageDecorator{

    public Sugar(Beverage beverage) {
        super(beverage);
    }
    //👌🏻 데코레이터 사용한 코드
    @Override
    public String getDescription() {
        return super.getDescription()+ ", Sugar";
    }
    //👌🏻 데코레이터 사용한 코드
    @Override
    public double cost() {
        return super.cost() + 0.3;
    }
}

//✅ Cream.java
public class Cream extends BeverageDecorator{


    public Cream(Beverage beverage) {
        super(beverage);
    }
    //👌🏻 데코레이터 사용한 코드
    @Override
    public String getDescription() {
        return super.getDescription() + ", Cream";
    }
    //👌🏻 데코레이터 사용한 코드
    @Override
    public double cost() {
        return super.cost() + 2.0;
    }
}

//✅ 실행클래스
public class OrderCoffee {
    public static void main(String[] args){

        // 현재 Milk 추가 가능
        //💡upcasting: coffee ➡️ beverage
        Beverage coffee = new Coffee();
        System.out.println(coffee.getDescription() + ": $" + coffee.cost());

        //💡upcasting: milk ➡️ beverage
        Beverage coffeeWithMilk = new Milk(coffee);
        System.out.println(coffeeWithMilk.getDescription() + ": $" + coffeeWithMilk.cost());


        //💡upcasting: sugar ➡️ beverage
        Beverage coffeeWithSugar= new Sugar(coffeeWithMilk);
        System.out.println(coffeeWithSugar.getDescription()+ ": $" +coffeeWithSugar.cost());

        Beverage coffeeWithCream= new Cream(new Milk(new Coffee())); //커피에 우유 타고 그 위에 크림 얹기
        System.out.println(coffeeWithCream.getDescription() + ": $" + coffeeWithCream.cost());

    }
}
// 실행결과
// Coffee: $5.0
// Coffee, Milk: $5.5
// Coffee, Milk, Sugar: $5.8
// Coffee, Milk, Cream: $7.5

✔️ JAVA 데코레이터 패턴과 I/O Stream

I/O Stream이 대표적인 데코레이터 패턴이다.
bufferReader같은 경우, reader의 기능을 유지하면서 속도 빠르게 기능만 추가하는 것이고
Printwriter도 writer의 기능을 유지하면서 println, printf기능을 추가하는 것이다.

🧩 JAVA 전략 strategy 패턴

동적으로 교체 가능한 전략을 제공하고 객체 관계를 우연하게 만드는 디자인
골프칠 때 거리, 상황에 맞는 골프채를 고르는 것
변경이나 수정이 잦은 내부 정책 클래스에 주로 사용
상황마다 비즈니스 전략 바뀌는 경우
(백화점에서 시즌에 따라서 세일을 진행하거나, 가격 인상을 하는 경우)

  • 인터페이스
  • setter
  • 다형성

discount 코드에 전략 패턴 적용

신규 가입자/ 시즌 할인/ 친구 추천 할인 적용된 전략 패턴 코드 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//Interface
//✅ DiscountStrategy.java
public interface DiscountStrategy {
        double calculateDiscount(double amount);
}

//✅ DiscountCaculator.java
//계산기는 여기서
public class DiscountCaculator {
    private DiscountStrategy discountStrategy;

    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double calculateDiscount(double amount) {
        if (discountStrategy != null) {
            return discountStrategy.calculateDiscount(amount);
        } else {
            return 0; // 할인 없음
        }
    }
}

//✅ NewCustomerDiscount.java
//신규고객 할인
public class NewCustomerDiscount implements DiscountStrategy{
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.2; // 20% 할인
    }
}
//✅ SeasonDiscount.java
//시즌 할인
public class SeasonDiscount implements DiscountStrategy{
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.1;
    }
}
//✅ FriendDiscount.java
//친구추천 할인
public class FriendDiscount implements DiscountStrategy{
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.15;
    }
}
//✅ StrategyTest.java
//실행메소드
public class StrategyTest {
    public static void main(String[] args) {

        DiscountCaculator calculator = new DiscountCaculator();

        // 신규 가입자 할인
        // 상황마다 각각 다른 전략
        calculator.setDiscountStrategy(new NewCustomerDiscount());
        double discount1 = calculator.calculateDiscount(10000);

        System.out.println("신규 가입자에게 " + discount1+ "원 만큼 할인해 줍니다.");

        //시즌 할인
        calculator.setDiscountStrategy(new SeasonDiscount());
        double discount2= calculator.calculateDiscount(10000);

        System.out.println("시즌 할인으로 " + discount2+ "원 만큼 할인해 줍니다.");

        //친구 할인
        calculator.setDiscountStrategy(new FriendDiscount());
        double discount3= calculator.calculateDiscount(10000);

        System.out.println("친구 추천으로 " + discount3+ "원 만큼 할인해 줍니다.");
    }
}

🧩 Template Method Pattern

Capsulize part of the overall service
change small details for each stage

  • 상속 extends 이용한 대표적인 디자인 패턴
  • parent class: define overall algorithm
  • child class: override algorithm

  • 전체적으로는 동일
  • 부분적으로는 다른 구문으로 구성되는 메소드의 코드 중복 최소화
  • parent class: 동일한 기능 정의
  • child class: 확장/변화 필요한 부분만 서브 클라스에서 구현

  • 예시
  • 대량 집 짓기
  • parent class: 뼈대 만들기
  • child class: 세부 사항 조절

  • 👍🏻 reduce code repetition
  • 👍🏻 OOP
  • 👍🏻 child class burden ⬇️
  • 👍🏻 centralized management of critical code
  • 👎🏻 lots of abstract class
  • 👎🏻 relationship among class can become complex
This post is licensed under CC BY 4.0 by the author.