해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

문제 상황


  • 상당히 많은 객체의 인터페이스
    • 공통적인 인터페이스의 부재
    • 앞으로도 여러 인터페이스가 추가될 수 있음.

Command Pattern


캡슐화를 더 높은 수준으로
메소드 호출을 캡슐화

  • 각각의 메소드를 인터페이스 별로 분기처리하여서 작업하는 것은 유지보수성이 떨어짐
  • 작업을 요청하는 쪽(객체)과 그 작업을 처리하는 쪽(커맨드 객체)을 분리
  • 커맨드 객체를 추가하여 작업
    • 특정 객체에 관한 특정 작업 요청을 캡슐화
  • (객체와 커맨드 객체를 분리 시)패턴이 어떻게 돌아가는지 파악하기 어려움

주문하는 과정

  • 주문서는 주문 내용을 캡슐화
    • 주문서는 주문 내용을 요구하는 객체
    • 식사 준비에 필요한 orderUp() 메소드와 주방장 레퍼런스만 들어있음.
    • 주문을 받으면 주방장 레퍼런스(작업을 처리하는 객체)에게 orderUp() 을 호출해주기만 하면 됨.
  • 종업원은 주문서를 받고 orderUp()  호출
    • orderUp() 메소드를 호출하는 객체
    • 주문서의 내용, 주방장이 누구인지 => 아무도 관심이 없음.
    • takeOrder() 메소드에는 여러 고객의 주문서를 매개변수로 전달.
  • 주방장은 식사를 준비하는데 필요한 정보
    • 식사 준비 방법은 주방장만 알고 있음.

용어정리

앞으로 나오는 객체의 대한 용어를 정의.

  • Client
    • 커맨드 객체 생성
  • Command
    • 리시버에 전달할 일련의 행동(execute()함수)을 구성.
    • execute() 함수를 사용하여 행동을 캡슐화하고, 특정 행동을 처리
  • Invoker
    • 커맨드 객체를 쓰이기 전까지 보관. 
    • 클라이언트를 통해서 커맨드 객체를 넘겨받음.
  • Receiver
    • 행동 메소드 실행 

커맨드 객체 만들기

  • 커맨드 인터페이스 구현
    • 모두 같은 인터페이스 구현
    • execute() 메소드 정의
  • 조명을 켤 때 필요한 커맨드 클래스 구현
    • LightOnCommand 클래스 구현
      • 특정 커맨드 객체로 제어한 클래스의 인터페이스 전달.(light 인터페이스)
    • execute() 메소드 오버라이드

example

public class RemoteControlTest {
    public static void main(String[] args) {
        // Light 커맨드 객체
        SimpleRemoteControl remote = new SimpleRemoteControl(); // invoker
        Light light = new Light("1"); // receiver
        LightOnCommand lightOn = new LightOnCommand(light); // command

        remote.setCommand(lightOn); // invoker <- command
        remote.buttonWasPressed();

        // Garage 커맨드 객체
        GarageDoor garageDoor = new GarageDoor("2");
        GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);

        remote.setCommand(garageDoorOpenCommand);
        remote.buttonWasPressed();
    }
}

public class SimpleRemoteControl {
    Command slot;
    public SimpleRemoteControl() {}

    public void setCommand(Command command) {
        slot = command;
    }

    public void buttonWasPressed() {
        slot.execute();
    }
}


// Receiver
public class Light {
    String location = "";

    public Light(String location) {
        this.location = location;
    }
    public void on() {
        System.out.println("조명이 켜졌습니다");
    }

    public void off() {

    }
}

// 행동을 캡슐화하기 위한 공통 interface
public interface Command {
    public void execute();
    public void undo();
}

// Command 객체(Light)를 실행할 Command class
public class LightOnCommand implements Command {
	Light light;

	public LightOnCommand(Light light) {
		this.light = light;
	}

	public void execute() {
		light.on();
	}

	public void undo() {
		light.off();
	}
}

커맨드 패턴 정의


💡 커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 가능.
  • 커맨드 객체는 일련의 행동을 특정 리시버와 연결. 요청을 캡슐화
  • 메타 커맨드 패턴 => 여러개의 명령을 매크로로 한번에 실행 가능

Command Pattern

Flow

  • ClientConcreteCommand(LightOnCommand, 커맨드 객체)를 생성하고 Receiver(Light, 리시버 객체)를 설정
  • Invoker(RemoteController, 인보커 객체)는 명령이 들어 있으며 execute() 호출로 커맨드 객체에게 특정 작업 수행 가능
  • Command는 모든 커맨드 객체가 구현해야하는 인터페이스
    • execute()로 메소드 호출
    • 리시버에게 특정 작업을 처리하라는 지시 전달.
  • Receiver는 요구사항을 수행할때 어떤 일을 처리해야 할 지.
  • ConcreteCommand는 특정 행동과 리시버를 연결
    • Invoker에서 execute() 호출로 요청하면 ConcreteCommand 객체에서 리시버의 메소드를 호출. 작업 처리.
💡 NoCommand 객체
NoCommand 객체는 일종의 null 객체.
널 객체는 딱히 리턴할 객체도 없고 클라이언트가 null을 처리하지 않게 하고 싶을 때 활용하면 좋음.
public class NoCommand implements Command {
	public void execute() { }
	public void undo() {}
}

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Template Method Pattern  (0) 2024.06.05
[Design Pattern] Adapter Pattern  (0) 2024.06.04
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

Singleton Pattern 사용이유


먼저 singleton pattern 사용 이유를 먼저 설명하겠습니다.

  • 하나만 있어도 충분히 돌아가는 또는 하나만 있어야 하는 객체에 사용
    • Thread pool, Cache, Logger
  • 이러한 객체가 두개이상 있으면
    • 프로그램이 이상하게 돌아갈 가능성.
    • 자원을 불필요하게 사용.
    • 결과에 일관성이 없어질 수 있음.
  • 정적 클래스와 메소드의 접근 변경자(public, private...)를 잘 다룰 줄 알아야함

전역변수

  • 싱글톤 패턴과 비슷.
  • 단점
    • 객체를 한번도 쓰지 않는다면 리소스 낭비
  • 싱글톤 패턴은 필요할 때만 객체를 생성 가능.

고전적인 싱글톤 패턴


public class Singleton {
    private static Singleton singleton; // Singleton 클래스의 하나뿐인 인스턴스를 저장하는 정적변수

    private Singleton() {} // 생성자를 private로 선언했으므 Singleton 클래스에서만 생성자 생성 가능
    
    public static Singleton getInstance() { // instance 호출
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
  • singleton 은 클래스의 하나뿐인 정적변수
  • 인스턴스가 필요한 상황 전까지 인스턴스를 만들지 않고 필요할 때 인스턴스를 생성. ⇒ Lazy instantiation
  • 유일한 객체
  • public으로 지정된 생성자가 없음.
  • getInstance() 정적 메서드 존재. 정적 인스턴스 호출

싱글톤 패턴


💡 싱글턴 패턴
클래스 인스턴스를 하나만 만들고 그 인스턴스로의 전역 접근을 제공.

문제

MultiThread 동시성 문제

  • 멀티 스레딩에서의 문제가 생김.
    • 두 스레드에서 다른 객체가 생김.

해결책

synchronized 키워드 사용

  • synchronized 키워드만 추가하면 한 스레드가 메소드 사용을 끝나기 전까지 다른 스레드는 기다려야함.
  • 속도 문제가 생길 수 있음
public class Singleton {
	private static Singleton uniqueInstance;
 
	// other useful instance variables here
 
	private Singleton() {}
 
	public static synchronized Singleton getInstance() {
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
 
	// other useful methods here
	public String getDescription() {
		return "I'm a thread safe Singleton!";
	}
}

선언시 인스턴스 생성

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return uniqueInstance;
    } 
}
  • JVM에서 Singleton의 인스턴스를 하나 생성

voliate 키워드 사용

  • DCL(Double-Checked Locking) 사용
  • 인스턴스가 있는지 확인 후 동기화 블록에 들어감
  • synchronized 내부의 블록에 한번 더 체크 하므로 동기화
public class Singleton {
	private volatile static Singleton uniqueInstance;
 
	private Singleton() {}
 
	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			synchronized (Singleton.class) {
				if (uniqueInstance == null) {
					uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}

고찰


  • 클래스 로더마다 서로 다른 네임스페이스 정의하기 때문에 클래스 로더가 두개 이상이면 같은 클래스 여러번 로딩하기에 객체가 두개 이상 생길 수 있음.
  • 리플랙션, 직렬화, 역직렬화에서 문제가 생길 수 있음. ⇒ 체크하면서 개발해야함.
  • 싱글턴은 느슨한 결합 원칙에 위배됨.
  • 클래스는 기본적으로 하나의 책임만 져야함. 하지만 싱글턴은
    • 하나의 클래스가 자신의 인스턴스를 관리하는 일
    • 인스턴스를 사용하는 목적.
    • 이 두가지 책임을 가짐.

 

 

 

 

요약

 

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Adapter Pattern  (0) 2024.06.04
[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
[Design Pattern] Observer Pattern  (0) 2024.05.27

프로젝트 진행 목표 및 과정

멘토님께서 Redis라는 큰 오픈소스의 이슈를 해결하기에는 4주라는 기간은 너무 짧다고 판단하여서, 해당 기간 동안 Redis에 대한 학습에 집중하기로 결정하셨습니다.

 

우선, 프로젝트 시작 전에 2주 동안은 오픈소스 커뮤니티에서 제공하는 유튜브 강의를 시청하여 git에 대한 기본적인 이해를 정리하는 시간을 가졌습니다. Git은 협업 및 버전 관리를 위해 필수적인 도구이며, 오픈소스 프로젝트에 기여할 때 필수적인 요소입니다. 따라서, 기본적인 Git의 개념과 사용법을 숙지하고자 이러한 학습을 진행했습니다.

 

다음으로, 나머지 4주 동안은 Redis에 대한 깊은 학습에 집중하였습니다. 이 기간 동안에는 Redis의 주요 기능과 용도를 학습하고, 실제로 어떻게 사용되는지에 대한 심도 있는 이해를 갖도록 노력하고 있습니다.

또한, Redis의 소스 코드를 분석하고 멘토님께서 주시는 과제를 수행하면서 레디스의 코드를 실제 수정하고 디벨롭하면서 실전 경험을 쌓고있습니다. 이를 통해 오픈소스 프로젝트에 기여하는 데 필요한 기술과 지식을 획득하고, 더 나아가 Redis와 관련된 이슈를 해결하는 데 기여할 수 있는 역량을 키우는 것이 목표입니다.

이러한 과정을 통해 Redis에 대한 전문 지식을 쌓고, 오픈소스에 기여할 수 있는 능력을 키우는 것이 이 프로젝트의 목표입니다. 더불어, Git을 활용한 협업 및 버전 관리에 대한 이해도 함께 높이고자 합니다. 이를 통해 프로젝트 진행 과정에서 필요한 기술과 역량을 갖추어, 효과적으로 오픈소스 프로젝트에 기여할 수 있는 개발자로 성장하는 것이 최종 목표입니다.

온⸱오프라인 모임을 통한 기여 및 활동 내역

Redis의 개념을 잘 모르기에 멘토님이 말씀해주시는 것을 기록하고 그 이외의 파생해서 조금 더 공부가 필요한 것은 따로 공부해서 정리하였습니다.

레디스 캐싱전략

💡
2주차 온라인 미팅: https://dev-hiro.tistory.com/13
NoSQL: https://dev-hiro.tistory.com/10
과제3: https://dev-hiro.tistory.com/9
과제3 해설: https://dev-hiro.tistory.com/11
과제4: https://dev-hiro.tistory.com/14

활동소감

이 강의를 통해 DB와 같은 개념은 이론적으로만 알고 있었던 저에게 실제 코드 레벨에서의 이해와 적용 방법을 배울 수 있어서 매우 유익했습니다.

 

레디스는 많은 기업에서 사용되는 인메모리 데이터 구조 저장소로서, 그 유연성과 뛰어난 성능으로 유명합니다. 그런데도 실제로 그 내부 동작 메커니즘을 알고 코드 레벨에서 어떻게 활용할 수 있는지에 대해 배우는 것은 새로운 경험이었습니다. 이를 통해 레디스가 어떻게 작동하는지에 대한 이해가 더 깊어졌고, 이를 통해 개발 프로젝트에서 레디스를 보다 효율적으로 활용할 수 있을 것 같습니다.

 

특히, 이러한 강의를 통해 오픈소스에 대한 기여에 대한 관심이 더 커졌습니다. 다음에는 Redis에 대해 더 깊게 공부하고, 커뮤니티에 기여하는 것이 목표입니다. 오픈소스 프로젝트에 참여함으로써 다른 사람들과 지식을 공유하고 성장할 수 있는 기회를 얻을 수 있을 것으로 기대됩니다.

또한, 이를 통해 제 개인적인 기술 스택을 향상시키고, 전반적인 개발 커리어에 도움이 될 것으로 기대됩니다.

앞으로 Redis를 더 깊게 공부하고 기여하는 과정에서 어려움이 있을지도 모르지만, 그 과정에서 더 많은 것을 배우고 성장할 수 있는 기회라는 것을 인식하고 있습니다. 다양한 프로젝트와 커뮤니티에서의 경험을 통해 개발자로서의 역량을 향상시키고, 다른 이들과 함께 협업하며 성공을 이끌어 나갈 수 있기를 기대합니다.

 

이러한 강의를 열어주신 Open Up에 감사드리며, Redis 강사님이신 강대명 멘토님께도 감사드립니다.

해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

디자인 패턴의 설계

느슨한 결합으로 객체지향 디자인

  • 객체의 인스턴스는 항상 공개 되어야 하는 것이 아니며 모든 것을 공개 했다가는 문제가 생길 수도 있음
  • new 연산자가 구상을 생각!!
    • new를 사용하면 구상 클래스의 인스턴스가 만들어짐
    • 이는 구상 클래스를 바탕으로 코드를 수정해야 할 가능성이 커지고 유연성이 떨어짐.
      Duck duck = new MallardDuck()

new의 문제?

  • 구상 클래스 간의 변화되는 부분이 문제!
  • 인터페이스에 맞춰 코딩하면 변화에 대응이 가능함. ⇒ 다형성
  • 변경에는 닫혀있고 확장에는 열려있어야한다.

피자 코드 설계


public class Pizza {
    public Pizza() {
    }

    Pizza orderPizza(String type) {
        Pizza pizza;

        if (type.equals("cheese")) {
            pizza = new ChesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}
  • pizza는 type에 따라 구상 클래스의 인스턴스를 생성.
  • 사라지는 피자와 새로 생기는 피자가 생기면 코드 지우고 코드 새로 추가해야됨.

객체 생성 부분 캡슐화(변화가 생기는 부분)

  • 객체 생성을 처리하는 클래스를 팩토리라고 함.
  • SimplePizzaFactory가 OrderPizza()메소드가 새로 만든 객체의 클라이언트 생성해줌.
public class SimplePizzaFactory {
    Pizza create(String type) {
        Pizza pizza = null;
        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }

        return pizza;
    }
}
  • 이러한 방법은 static factory라고 부름
    • 객체 생성 메소드를 실행하려고 객체의 인스턴스를 만들지 않아도 되기 때문.

Client Code 수정하기

객체의 팩토리 인스턴스 저장하기

public class PizzaStore {
    SimplePizzaFactory factory;

    public PizzaStore(SimplePizzaFactory factory) { 
        this.factory = factory;
    }

    public Pizza orderPizza(String type) {
        Pizza pizza;

        pizza = factory.createPizza(type);

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}

public class SimplePizzaFactory {

    public SimplePizzaFactory() {
    }

    Pizza create(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }

        return pizza;
    }
}
  • 각각의 지점들은 굽는 방식이 달라지거나 피자의 토핑이 달라질 수 있음.
  • PizzaStore를 만들어 각각 스타일대로 구현

다양한 팩토리 만들기


다양한 스타일의 피자 가게가 있음.

public abstract class PizzaStore { 
	abstract Pizza createPizza(String item);
 
	public Pizza orderPizza(String type) {
		Pizza pizza = createPizza(type);
		System.out.println("--- Making a " + pizza.getName() + " ---");
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		return pizza;
	}
}

public class NYPizzaStore extends PizzaStore {
	Pizza createPizza(String item) {
		if (item.equals("cheese")) {
			return new NYStyleCheesePizza();
		} else if (item.equals("veggie")) {
			return new NYStyleVeggiePizza();
		} else if (item.equals("clam")) {
			return new NYStyleClamPizza();
		} else if (item.equals("pepperoni")) {
			return new NYStylePepperoniPizza();
		} else return null;
	}
}

public class ChicagoPizzaStore extends PizzaStore {
	Pizza createPizza(String item) {
		if (item.equals("cheese")) {
			return new ChicagoStyleCheesePizza();
		} else if (item.equals("veggie")) {
			return new ChicagoStyleVeggiePizza();
		} else if (item.equals("clam")) {
			return new ChicagoStyleClamPizza();
		} else if (item.equals("pepperoni")) {
			return new ChicagoStylePepperoniPizza();
		} else return null;
	}
}

// 뉴욕식 치즈피자
PizzaStore nyStore = new NYPizzaStore();
nyStore.orderPizza("cheese");

// 시카고식 치즈피자
PizzaStore chicagoStore = new ChicagoPizzaStore();
chicagoStore.orderPizza("cheese");

팩토리 메소드 패턴

Factory Method Pattern

  • 팩토리 패턴객체 생성(Pizza)을 캡슐화
  • 팩토리 메서드 패턴은 서브클래스(NYPizzaStore, ChicagoPizzaStore)에서 어떤 클래스를 만들지 정하므로, 객체 생성을 캡슐화
  • 제품 클래스(Pizza) 와 생산자 클래스(PizzaStore)를 분리함으로써 구체적인 구현은 구상 클래스가 책임 지도록.
  • 특징
    • 캡슐화: 객체 생성 로직을 팩토리 클래스나 메서드로 분리하여 외부에서 직접 객체를 생성하지 않도록 함.
    • 유연성: 객체 생성 방식을 변경할 때 클라이언트 코드를 수정할 필요 없이 팩토리 클래스만 수정하면 됨.
    • 확장성: 새로운 종류의 객체를 추가할 때도 기존 클라이언트 코드에 영향을 주지 않고 팩토리 클래스만 확장하면 됨.
    • OCP: 하나의 클래스에 추가적인 기능을 넣어도 다른클래스의 변경을 하지 않아도된다.
💡 팩토리 메서드 패턴
객체를 생성할 때 필요한 인터페이스(PizzaStore)를 만들고 어떤 클래스의 인터페이스를 만들지는
서브 클래스(NYPizzaStore, ChicagoPizzaStore)에서 결정하도록 함.
💡 Simple Factory VS Factory Method 
팩토리 메소드 패턴간단한 팩토리와 상당히 비슷하지만 간단한 팩토리는 일회용 처방에 불과한 반면, 팩토리 메소드 패턴을 사용하면 여러번 재사용이 가능한 프레임워크를 만들 수 있습니다. 예를 들어, 팩토리 메소드 패턴의 orderPizza( ) 메소드는 피자를 만드는 일반적인 프레임워크를 제공합니다. 그 프레임워크는 팩토리 메소드 피자 생성 구상 클래스를 만들었죠. PizzaStore 클래스의 서브 클래스를 만들 때, 어떤 구상 제품 클래스에서 리턴할 피자를 만들지를 결정합니다. 이 프레임워크를 간단한 팩토리와 한번 비교해 보세요. 간단한 팩토리는 객체 생성을 캡슐화하는 방법을 사용하긴 하지만 팩토리 메소드만큼 유연하지는 않습니다. 생성하는 제품을 마음대로 변경할 수 없기 때문입니당. => (SimpleFactory라는 인스턴스를 가지고 있기 때문)

 

의존성 뒤집기 원칙(Dependency Inversion)

  • 구상 클래스의 의존성을 떨어뜨리기
  • 즉, 고수준 구성요소는 저수준 구성요소에 의해 정의되는 행동이 들어있는 구성요소
    • PizzaStore는 고수준 구성요소
    • Pizza 클래스는 저수준 구성요소.
💡 추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 만든다.
  • DI를 지키는 방법
    • 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
    • 구상 클래스에서 유도된 클래스를 만들지 말자.
    • 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 말자.

추상 팩토리 패턴

Abstract Factory Pattern

💡 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스 제공.

팩토리 메소드 패턴 VS 추상 팩토리 패턴

비교\패턴 팩토리 메소드 패턴 추상 팩토리 패턴
공통점 애플리케이션을 특정 구현으로부터 분리.
제품 생성 클래스를 사용 객체를 사용
제품 생성 방식 상속 구성
제품 생성 방법 객체를 생성하고 클래스를 확장할 때에는 팩토리 메소드를 오버라이드하여 서브 클래스에서 객체를 생성 제품군을 만드는 추상 형식을 제공. 인스턴스를 만든 다음 추상 형식을 써서 만든 코드에 전달.

 

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Decorator Pattern  (0) 2024.05.28
[Design Pattern] Observer Pattern  (0) 2024.05.27
[Design Pattern] Strategy Pattern  (0) 2024.05.27
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

카페 프로그램 개발


  • 음료수가 많으니 추상 클래스에서 Beverage 클래스 정의
  • 더 자세한 음료는 서브 클래스에서 구현

문제점

  • 인기가 증가하면서 메뉴가 너무 많이 증가하고, 모든 메뉴가 Beverage를 상속함
  • 첨가물도 있으니 첨가물이나 가격변동에 따른 대비가 어려움

해결책

  • 인스턴스 변수와 슈퍼 클래스 상속으로 관리

문제점

  1. 첨가물의 가격이 바뀔때마다 코드 수정
  2. 새로운 음료가 출시될 경우, 특정 첨가물이 들어가면 안되는 음료가 있을 수 있음.
  3. 같은 첨가물을 두번 주문하는 경우

고찰


  • 상속만으로는 무조건 유연하거나 관리하기 좋은 디자인은 아님
  • 구성과 위임으로 실행 중에 행동을 상속하는 방법이 있음
    • 서브 클래스로 행동을 상속받으면 컴파일 타임에 결정됨.
    • 구성으로 객체의 행동을 확장하면 실행 중에 동적으로 할당가능.
💡 OCP(Open-Closed Principle)
클래스 확장에는 열려있지만 변경에는 닫혀있다.

 

Decorator Pattern


이전까지는 클래스가 너무 많아지거나 적합하지 않은 기능을 추가해야했음.

첨가물 == 음료의 장식(decorate)으로 생각

Decorator Pattern

방법

  1. DarkRoast 객체 생성
  2. Mocha 객체로 장식
  3. Whip 객체로 장식
  4. cost() 메서드 호출. 첨가물의 가격을 계산하는 일은 객체에게 위임.

정리

  1. 데코레이터의 슈퍼 클래스는 자신이 장식하고 있는 객체의 슈퍼클래스와 동일(Beverage)
  2. 한 객체(음료)를 여러개의 데코레이터(샷, 우유, 모카...)로 감싸기
  3. 데코레이터는 행동을 위임하는것 말고도 다른 추가 메서드 가능
  4. 실행중에 데코레이터 적용 가능
💡 Decorator Pattern
객체에 추가 요소를 동적으로 더할 수 있음.
서브 클래스를 만들때보다 유연하게 기능확장 가능.

 

고찰

  • CondimentDecorator에서 Beverage 클래스를 확장 ⇒ 상속아닌가?
    • 상속맞지만 행동을 물려받는게 아님
  • 이 구조로 실행 중에 동적으로 결정 가능

Java IO 클래스

데코레이터 패턴을 적용한 예

예시

public abstract class Beverage {
    enum SIZE {
        TALL, GRANDE, VENTI
    }

    String description = "아무것도 없음";
    SIZE size = SIZE.TALL;

    public String getDescription() {
        return description;
    }

    public SIZE getSize() {
        return size;
    }

    public void setSize(SIZE size) {
        this.size = size;
    }

    abstract double cost();
}

public abstract class CondimentDecorator extends Beverage {
    Beverage beverage;

    public abstract String getDescription();
}


public class DarkRoast extends Beverage {
    private double cost;

    public DarkRoast() {
        description = "다크 로스트 커피";
    }

    double cost() {
        if (getSize() == SIZE.TALL) {
            cost += 0.99;
        } else if (getSize() == SIZE.GRANDE) {
            cost += 1.49;
        } else {
            cost += 1.99;
        }

        return cost;
    }
}

public class Mocha extends CondimentDecorator {
    public Mocha(Beverage b) {
        this.beverage = b;
    }

    public String getDescription() {
        return beverage.getDescription() + "모카";
    }

    public double cost() {
        return beverage.cost() + 0.2;
    }
}

public class Whip extends CondimentDecorator {
    public Whip(Beverage b) {
        description = "휘핑";
        this.beverage = b;
    }

    public String getDescription() {
        return beverage.getDescription() + "휘핑";
    }

    public double cost() {
        return beverage.cost() + 0.4;
    }
}

public class StarbuzzCoffee {
    public static void main(String[] args) {
        Beverage espresso = new Espresso();
        System.out.println("espresso: " + espresso.getDescription() + " cost: " + espresso.cost());

        Beverage darkRoast = new DarkRoast();
        darkRoast = new Mocha(darkRoast);
        darkRoast = new Mocha(darkRoast);
        darkRoast = new Whip(darkRoast);
        System.out.println("Dark double Mocha whipping: " + darkRoast.getDescription() + " cost: " + darkRoast.cost());

        Beverage sameB = new Whip(new Mocha(new Mocha(new DarkRoast())));
        System.out.println("Dark double Mocha whipping: " + sameB.getDescription() + " cost: " + sameB.cost() + "\t" + sameB.getClass());

        // SIZE = GRANDE;
        Beverage darkRoastGrande = new DarkRoast();
        darkRoastGrande.setSize(Beverage.SIZE.GRANDE);
        darkRoastGrande = new Mocha(darkRoastGrande);
        darkRoastGrande = new Mocha(darkRoastGrande);
        darkRoastGrande = new Whip(darkRoastGrande);
        System.out.println("Dark Grande double Mocha whipping: " + darkRoastGrande.getDescription() + " cost: " + darkRoastGrande.cost());
    }
}

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Observer Pattern  (0) 2024.05.27
[Design Pattern] Strategy Pattern  (0) 2024.05.27
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

Observer 패턴 이해하기


신문사 + 구독자 = Observer 패턴(publish-subscribe 패턴과 다름)

  • 구성요소
    • 신문사 => 주제(Subject)
    • 구독자 => 옵저버(Observer)
  • 주제에서는 중요한 데이터를 관리
  • 주제의 데이터가 바뀌면 옵저버에게 이벤트(데이터가 바뀌었다는 소식)를 전달.
  • 주제를 구독하고 있는 옵저버에게 소식이 전달되었기에 전달받은 내용을 옵저버는 갱신.(최신 데이터 유지)

작동원리

  • 주제는 옵저버들에게 데이터를 전달.
  • 어떤 객체가 주제의 데이터를 전달받고 싶어하면 주제를 구독하면 됨.(옵저버가 되는 형태)
  • 주제의 데이터를 그만 업데이트하고 싶으면, 주제의 구독을 해지.

Observer 패턴 

객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동적으로 내용이 갱신되는 방식.
일대다(one-to-many) 의존성을 정의(일: Subject, 다: Observer)

observer pattern class diagram

느슨한 결합


  • 한 객체가 다른 객체에 너무 의존적이면 단단한 결합이라 함.

느슨한 결합

  • 객체가 부서질 가능성이 낮음
  • 객체의 세세한 부분까지 알 필요가 없음
  • 상호작용할 수 있지만 잘 모르는 관계
  • 다른 객체와 상관없이 설계하면 변화에 더 잘 대응할 수 있는 디자인을 만들 수 있음 => 유연한 디자인

옵저버 패턴에 적용한 내용

  • 주제는 옵저버가 특정 인터페이스(Observer interface)를 구현한다는 사실만 알고 있음.
  • 옵저버는 언제든 추가/제거 가능
  • 새로운 형식의 옵저버를 추가해도 주제 변경이 필요가 없음.
  • 주제와 옵저버는 서로 독립적으로 재사용 가능.
  • 주제나 옵저버가 달라져도 서로에게 영향을 미치지 않음.
💡 상호작용하는 객체 사이에는 느슨한 결합을 사용해야함.

풀 방식으로 코드 바꾸기


  • 이전까지의 방식
    • 주제가 옵저버에게 데이터를 보내는 푸시(PUSH)
  • 현재 사용할 방식
    • 옵저버가 주제로부터 데이터를 당겨오는 풀(PULL)

코드


/* 
 * observer interface
 */
public interface Observer {
    public void update(float temp, float humidity, float pressure);
    public void update();
}

/* 
 * display interface
 */
public interface DisplayElement {
    public void display();
}

public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    // PUSH 방식
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;

        display();
    }

    // PULL 방식
    public void update() {
        this.temperature = weatherData.getTemperature();
        this.humidity = weatherData.getHumidity();

        display();
    }

    public void display() {
        System.out.println("CurrentConditions Display");
        System.out.println("temperature = " + temperature + " humidity = " + humidity);

    }
}

/* 
 * subject interface
 */
public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer observer);
    public void notifyObservers();
}

public class WeatherData implements Subject {

    // 인스턴스 변수 선언
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers = new ArrayList<Observer>();
    }

    public void registerObserver(Observer o) {
        observers.add(o);
    }

    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    public void notifyObservers() {
        // Push방식
        // Observer들에게 갱신 값 보내주기.
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }

        // Pull
        // Observer에서 값 가져오기
        for (Observer o : observers) {
            o.update();
        }

    }

    public void measurementsChanged() {

        notifyObservers();
        /*
         * 현재 구조에서는 display들이 update 메소드를 요청하는데 하나로 묶을 수 있을 것 같음.
         * 최신 측정 값을 가져오는 시점을 판단하기 어려움
         * Observer 패턴 도입.
         */
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;

        measurementsChanged(); // 최신 측정값 Observer들에게 갱신 값 보내주기.
    }
}

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
[Design Pattern] Strategy Pattern  (0) 2024.05.27
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

SimUduck


  • 모든 오리가 할 수 있는 동작 Duck 클래스에서 정의
    • quack()
    • swim()
    • display()
      • 모든 오리의 동작이 다르므로 추상 메소드에서 정의
  • MallardDuck 클래스와 RedHeadDuck 클래스가 Duck 클래스를 상속받아서 display override
  • 이후에 모든 오리에게 날 수 있는 동작을 정의하고 싶음
    • Duck 클래스에서 fly() 메소드 추가로 정의로 해결 가능

문제발생

  • fly() 메소드는 일부 Duck 클래스를 상속한 서브 클래스에만 되어야했는데 모든 서브 클래스에 적용이 다 되어버림

해결책


1. 인터페이스 설계

  • 모든 오리가 날지 못함 + 모든 오리가 꽥꽥거리면서 울지 않음
    • Flyable, Quackable 이라는 인터페이스 설계

2. 캡슐화

💡 애플리케이션에서 달라지는 부분을 찾아내고 달라지지 않는 부분과 분리.

 

  • 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화
    • 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이면서 시스템의 유연성을 향상.
  • 구현보다는 인터페이스에 맞춰 프로그래밍한다.
    • 각 행동은 인터페이스로 표현(FlyBehavior, QuackBehavior…)
    • 메소드는 Duck 클래스에서 구현X. 인터페이스에서 먼저 정의
💡 구현보다는 인터페이스에 맞춰 프로그래밍한다.

오리행동 통합하기


나는 행동과 꽥꽥거리는 행동을 Duck 클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스로 위임하는 행동.

  • Duck 클래스에 FlyBehaviorQuackBehavior을 인스턴스 변수로 저장.
    • Duck 클래스는 인터페이스에서 구현한 fly()quack() 메소드를 실행하기 위한 메소드 정의(performFly(), performQuack())
public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    // 모든 오리의 동작을 여기서 정의

    void performQuack() {
        quackBehavior.quack();
    }

    void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    void performFly() {
        flyBehavior.fly();
    }

    void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    void swim() {
        System.out.println("헤엄!");

    }

    // 오리에 따른 description
    abstract void display();
}

/*
 * 오리 날기 정의
 * 인터페이스로 정의 후 각각의 날개의 방법대로 정의.
 */
public interface FlyBehavior {
    void fly();
}

public class FlyNoWay implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("i can't fly!");
    }
}

public class FlyWithWings implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("i can fly!");
    }
}

/*
 * 오리 울음소리 정의
 * 인터페이스로 정의 후 각각의 소리마다 class로 정의
 */
public interface QuackBehavior {
    void quack();
}

public class Quack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("꽥꽥!");
    }
}

public class Squeak implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("삑삑!");
    }
}

 

Example

public class RedheadDuck extends Duck {

    public RedheadDuck() {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Squeak();
    }

    @Override
    void display() {
        System.out.println("RedheadDuck!");
    }
}
  • RedheadDuck 클래스는 Duck 클래스를 상속.
  • 상속자에서 날기 동작과 울음소리를 정의.
    • 날기 동작과 울음소리는 인터페이스로 정의되어 있는 것에 클래스로 할당.
  • display()는 Duck 클래스에서 추상메소드로 정의하였기 때문에 클래스에서 정의.
💡 특정 구현에 맞춰서 프로그래밍하면 안됨.
하지만 new FlyWithWings() 처럼, 특정 객체에 구현되어 있는 구상 인스턴스를 만들었음. ⇒ 잘못되었지만 1장이라 이처럼 표현

동적으로 행동 지정하기

  • Duck 클래스에 Setter method 정의(setQuackBehavior(), setFlyBehavior())

두 클래스를 합치는 방법

💡 상속(as-is)보다는 구성(has-is)을 활용
  • A에는 B가 있다. ⇒ Duck 클래스에 FlyBehavior와 QuackBehavior가 있어서 Duck 클래스에 나는 행동과 꽥꽥거리는 행동을 위임. ⇒ 구성

전략패턴


알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해주는 패턴

클라이언트로부터 알고리즘을 분리해서 독립적으로 분리.

요약 :

변화가 생길 기능(행동:behavior)을 예상하여 이를 캡슐화(및 추상화) 한다.

캡슐화 후 각 기능별로 구현해두고(ex: fly 중 noWay, WithWing, Rocket 등)

각 객체별로 사용할 기능을 (쉽게) 교환 받는다.

각 기능들이 추상화되어 있어 재사용성이 좋고, 수정이 용이하다.

 

기억 추적 :

서브클래스는 단순히 super 클래스를 상속받는식의 구현이 아닌,

interface, abstract 등을 이용하여 기능의 동작만 받음.

각 기능은 인터페이스를 구현하여 미리 정의해두고 모델은 이를 조합하여 기능을 만들어가는 방식.

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
[Design Pattern] Observer Pattern  (0) 2024.05.27

오픈소스 기여! 나도 할수 있다⭐️

여러분은 우리가 매일 사용하는 다양한 소프트웨어가 어떻게 만들어지고 발전하는지 궁금해 본 적 있으신가요?
그 답 중 하나는 바로 '오픈소스'에 있습니다. 오픈소스는 전 세계의 개발자들이 힘을 합쳐 더 나은 소프트웨어를 만들어가는
협업의 장입니다. 이 공간에서는 누구나 자신의 아이디어를 더하고, 문제를 해결하며, 기술을 발전시킬 수 있습니다.

오픈소스에 기여하는 것은 마치 전 세계 사람들과 함께 퍼즐을 맞추는 것과 같습니다. 각자가 가진 작은 조각들이 모여 큰 그림을 완성해 나가는 과정이죠. 처음에는 어디서부터 시작해야 할지 막막할 수 있지만, 조금씩 참여하다 보면 어느새 여러분도 중요한 일원으로 활약하고 있을 거예요.

 

하지만 오픈소스 기여를 처음 시작하는 사람들에게는 어디서부터 시작해야 할지 막막할 수 있습니다. 어떤 프로젝트를 선택해야 할지, 어떻게 기여할 수 있을지, 기여 과정에서 어떤 도구와 방법을 사용해야 할지에 대한 명확한 지침이 필요합니다. 이 블로그에서는 오픈소스 기여의 기초부터 시작하여, 단계별로 기여 방법에 대해 알려드리도록 할게요😁

 

오픈소스 선정

처음에 오픈소스를 선정하는 것은 어려운 작업입니다. 평소에 자주 사용하는 오픈소스 일 지라도 어떤 이슈가 할당되어 있으며, 오픈소스의 플로우를 대략적으로 파악하고 있어야 이슈를 해결할 수 있기에 오픈소스를 선정하는 것은 어려운데요, 몇가지 팁이 있습니다!

라벨을 이용하자!

각각의 오픈소스들에는 issue에 라벨이 할당되어 있습니다.

spring-security issues

issue 제목 옆에 색상으로 구분되어 있는 것이 라벨인데요! 해당 이슈에 할당된 라벨을 살펴보면 적절한 이슈를 찾을 수 있습니다.

특히 오픈소스 기여의 초보자일 경우에는 first-time-only와 같은 이슈를 해결하면 좋은데요!

type이 bug인 issue나 enhancement의 경우에는 비교적 쉬운 로직으로 오픈소스를 해결할 수도 있습니다.

깃허브 고급 검색을 사용하자!

github 고급검색

깃허브에는 고급 검색이 있는 것을 알고 계셨나요?

깃허브에서 spring을 검색한 후의 왼쪽 nav바 하단에 Advanced search가 있는 것을 볼 수 있는데요 해당 내용을 살펴보면

  • language: 선택한 언어로 개발
  • 팔로워의 수나 스타 수: ex) > 5000
  • 라벨: ex) first-time-only

과 같이 고급 검색을 이용하면 조금 더 이슈를 편하게 확인할 수 있습니다!

오픈소스 기여하기

이제 기여할 이슈를 찾았다면 해당 레포를 포크한 후, 각자의 레포에서 개발하시면 되는데요!

이슈를 해결하고 커밋을 남겨 pull request 하는 데에 정답은 없지만 제가 지키는 몇가지가 있는데요

커밋은 하나만 남기자!

개발을 하다보면 커밋 내용이 많아질 수도 있고, pr을 보내 메인 테이너가 리뷰를 주어 몇몇 수정사항이 생기는데요, 해당 이슈에 대해 커밋을 여러개 남기는 것은 커밋 이력을 확인하기 어렵습니다ㅜㅜ 따라서 저는 rebase와 squash를 사용하여, 하나의 이슈에 대해서는 하나의 커밋만 남기는 편입니다!

커밋 메시지를 자세히 그리고 issue 번호까지!

저 같은 경우에는 커밋 메시지를 한 줄로 적는 경우가 많았습니다(git commit -m "<commit message>"

하지만 기존 프로젝트와 달리 오픈소스의 경우는 많은 개발자의 작업으로 이루어지는 것이므로 커밋 메시지를 자세히 작성하는 것이 좋다고 생각합니다.

armeria 커밋 메시지

제가 해결했던 armeria 이슈 해결 커밋 내역인데요, git commit을 통해 이슈 내용과, 수정사항들을 자세히 작성했습니다!

또 커밋에 #이후에 이슈 번호를 작성하면 링크가 자동적으로 걸려 해당 이슈로 이동도 가능합니다!

 

마무리

이렇게 오픈소스를 기여하는 방법에 대해 알아봤는데요. 사실 전세계에 있는 사람들이 사용하는 오픈소스에 기여하는 것은 조금 두렵고 겁나는 일인 것은 사실입니다. 그래서 몇가지 도움을 주시는 분들이 있는데 소개해드리자면

과학기술통신부 주최의 open up에서 진행하는 컨트리뷰션 아카데미가 있습니다. 해당 리드 개발자분들이 이끌어주어 참여 기간동안 해당 오픈소스를 분석하고, 이슈를 해결하는 프로젝트가 있습니다. 체험형/입문형/참여형으로 나누어지며 신청기간은 3~5월 사이에 이루어지니 참고하시기 바랍니다ㅎㅎ

그리고 저도 참여했던 오픈소스 멘토링이 있습니다. 리드를 해주시는 인제님과 같이 이슈를 같이 분석하고 해결하며 pr까지 정해진 날짜에 짧은 기간동안 적극적으로 도와주십니다. 구글 밋을 통해 진행하고 굉장히 친절하고 깊이 있게 알려주시니 초보자분들도 무리없이 지원하실 수 있을 것 같습니다😁

 

이제 오픈소스 기여의 매력과 방법에 대해 어느 정도 감이 오셨나요? 여러분의 작은 기여가 모여 큰 변화를 만들어낼 수 있습니다. 처음에는 작은 일부터 시작해보세요. 코드 한 줄의 수정, 문서의 개선, 버그 리포팅 등 모든 기여는 소중합니다. 
이번 주말부터 오픈소스 기여에 같이 동참할까요😁?

 

 

+ Recent posts