📝 java에 관해 공부한 것을 한 큐에👁 정리해보고자 합니다 (업데이트 상시 예정)
자바가 객체지향 프로그래밍 언어이면, 자바가 지닌 객체지향 프로그래밍 특징은 무엇일까?
* 자세한 정리는 객체지향 포스팅 참조
static
은 변수와 메소드에 선언하여 '공통적으로 같은 값을 유지' 하도록 만든다.
static(정적) 은 '공통적인, 고정된'이란 의미를 가지고 있다.
static
이라는 키워드를 사용하여 변수와 메소드에 선언하게 된다면 static 변수(정적 변수)와 static 메소드(정적 메소드)를 만들 수 있다. 이때, static 키워드를 사용한 변수나 메소드는 클래스가 메모리에 올라갈 때 자동으로 생성되므로, 인스턴스 생성 없이 바로 사용 가능하기 때문에 프로그램 내에서 공통으로 사용되는 데이터들을 관리할 때 이용합니다.
static 키워드를 통해 생성된 정적멤버들은 Heap 영역이 아닌 Static 영역에 할당된다.
Static 영역에 할당된 메모리는 모든 객체가 공유 하여 하나의 멤버를 어디서든지 참조할 수 있는 장점을 가진다. 때문에 static은 자주 변하지 않는 값이나 공통으로 사용되는 값 같은 공용자원에 대한 접근에 있어서 매번 메모리에 로딩하거나 값을 읽어들이는 것보다 일종의 '전역변수'와 같은 개념을 통해 접근하는 것이 비용도 줄이고 효율을 높일 수 있습니다.
하지만, Garbage Collector의 관리 영역(Heap) 밖에 존재하기 때문에 프로그램이 종료될 때까지 메모리에 값이 유지된 채로 존재하게 되므로 static을 남발하게되면 성능에 악영향을 줄 수 있다.
* 자세한 정리는 이전 static 과 객체지향적 관점 포스팅 참조
final
은 클래스, 메소드, 변수, 인자를 선언할 때 사용하여 '변경 불가능' 하도록 만든다.
이때 불변 객체 란?
불변 객체는 객체 생성 이후 내부의 상태가 변하지 않는 객체이다.
Java에서는 필드가 원시 타입인 경우 final 키워드를 사용해 불변 객체를 만들 수 있다.
하지만, 참조 타입일 경우 에는 추가적인 작업이 필요하다.
* 자세한 정리는 이전 final 과 객체의 불변성 포스팅 참조
객체지향 프로그래밍에서는 모든 것이 객체로 다루어져야 한다. 하지만 기본 자료형같은 경우 객체로 다루어지지 않는 것을 볼 수 있는데, 필요에 따라 기본형 변수도 객체로 다뤄야한다.
(ex. 매개변수로 객체를 요구하거나, 객체간의 비교가 필요한 경우 등)
이럴때 Wrapper class를 사용하여 기본형 값을 객체로 다룰 수 있다.
boolean
, char
, byte
, short
, int
, long
, float
, double
Boolean
, Character
, Byte
, Short
, Integer
, Long
, Float
, Double
Primitive vs Reference
기본형(
primitive type
)에는 8개의 타입(자료형)이 있다.
→boolean
,char
,byte
,short
,int
,long
,float
,double
매개변수의 타입이 기본형일 때는 기본형 값이 복사되지만, 참조형이면 인스턴스의 주소가 복사된다.
주소가 복사된다는 의미는 값이 복사될 때와 다르게 주소에 직접 접근하여 변경을 시도하기 때문에 값의 변경이 이루어진다.
자바에서 String은 불변 객체(immutable instance)이다.
즉, 한 번 객체가 생성되면 할당된 메모리 공간이 변하지 않으므로,
String 연산(ex.substring, replace 등)을 하게 될 경우, 기존에 생성된 String 클래스 객체 문자열에 다른 새로운 문자열을 기존 문자열에 붙이는 것이 아니라 새로운 객체를 생성하게 된다. 때문에 문자열 연산이 많은 경우 성능이 좋지 않을 수 있다.
String은 두가지 생성 방식이 있고 그에 따라 각각 차이점이 존재한다.
new
연산자를 이용한 방식 : new 연산자로 생성하면 같은 내용이라도 여러 개의 객체가 각각 Heap 영역을 차지하게 된다.=
(리터럴)을 이용한 방식 : Heap 영역 내 String Constant Pool에 저장되어 재사용될 수 있다.String name1 = new String ("kim"); //heap 영역에 생성
String name2 = "Lee"; //String pool에 객체 생성
StringBuilder, StringBuffer 의 경우 String과 다르게 가변 객체(mutable)이다.
String과 다르게 memory 에 append 하는 방식 으로, 클래스에 대한 객체를 직접 생성하지 않는다.
다만, StringBuffer는 동기화를 지원하여 멀티 쓰레드 환경에서 주로 사용하며,
StringBuilder는 동기화를 지원하지 않아 싱글 쓰레드 환경에서 주로 사용한다.
abstract
는 '추상의, 미완성의' 의미를 지니고 있다.
추상클래스는 자식 클래스가 그 추상클래스를 상속받아서 기능을 이용하고 확장 시키는데 목적 이 있다.
추상 클래스는 클래스 내 추상 메소드가 하나 이상 포함되거나 선언부만을 작성하며, 실제 내용은 상속받는(extends
) 자손 클래스에서 구현받아서 적절히 구현해야한다. (단일 상속만이 가능하다.)
interface
도 일종의 추상 클래스이다. 다만, 모든 메소드가 추상 메서드와 상수로만 이루어져 있는 더 추상화된 형태이다.
상속받을 자손 클래스에게 구현할 메소드들의 원형(껍데기)을 모두 알려주어, 클래스가 자신의 목적에 맞게 메소드를
구현하도록 하는 것이다. 구현 객체의 같은 동작을 보장하기 위한 목적 이 있다
그 자체로만으로 사용된다기 보다는 상속(implements
)을 통해 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다. (다중상속이 가능하다.)
자바에서는 주로 오버로딩(Overloading)과 오버라이딩(Overriding)을 통해서 다형성을 지원한다.
(* 다형성 : 하나의 메서드나 클래스가 있을 때 그것이 다양한 방법으로 동작하는 것)
Riding
= 올라타다. (같은 함수를 올라타서 덮어 씌우고 새롭게 정의한다)
상위 클래스에 있는 메소드를 하위 클래스에서 상속받아 재정의 하는 것이다.
한마디로, 부모에게 물려받아 변형한 함수.
public interface LottoGenerator {
List<LottoNumber> makeLottos();
}
public class RandomLottoGenerator implements LottoGenerator {
private final List<LottoNumber> numbers;
...
public List<LottoNumber> makeLottos() { //매개변수가 같아야한다.
Collections.shuffle(numbers);
return numbers.subList(LOTTO_NUMBER_START_INDEX, LOTTO_NUMBER_END_INDEX);
}
}
Loading
= 불러오다. (전혀 다른 함수를 불러오다)
매개변수의 개수나 타입을 다르게 하여 같은 이름의 메소드를 여러 개 정의하는 것이다.
한마디로, 이름은 같지만 그냥 전혀 다른 함수.
void setStudent(String name){
this.name = name;
}
void setStudent(String name, ing age){ //매개변수가 다르다.
this.name = name;
this.age = age;
}
JVM(Java Virtual Machine) 이란?
OS에 종속받지 않고 CPU 가 Java를 인식하고 실행할 수 있게 하는 가상 머신이다.
(*가상머신 : 프로그램을 실행하기 위해 물리적인 머신과 유사한 머신을 소프트웨어로 구현한 것)
JVM의 역할
JVM의 메모리 영역(Runtime Data Area)은 프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간이다.
크게 Method area, Heap, Thread 마다 생성되는 영역(PC Register, JVM Stack, Native method Stack)으로 볼 수 있다.
new
연산자 로 생성되는 객체와 배열을 저장한다.스택 프레임
이 생성되며 메서드와 관련된 정보(지역 변수,리턴 값 등)를 저장하고, 메서드가 종료되면 프레임이 소멸된다.JVM의 메모리 관리 기법 중 하나이다.
JVM은 GC를 이용하여 동적으로 할당된 메모리(Heap) 영역 중 더는 사용하지 않는 메모리를 자동으로 회수한다.
가비지 컬렉션 과정
Heap의 Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어 있기 때문에, GC의 세부적인 동작 방식은 다르다.
하지만 기본적으로 가비지 컬렉션이 실행된다고 하면 Stop The World, Mark and Sweep 2가지 공통적인 단계를 따르게 된다.
.java
파일을 생성하고 build 한다.javac
의 명령어를 통해 바이트코드(.class
)를 생성한다.* 더 자세한 정리는 JVM 포스팅 참조
자바에서 컬렉션이란 '데이터를 저장하는 클래스들을 표준화한 설계'이다.
쉽게 보면, 여러 원소들을 담을 수 있는 자료구조로 데이터들을 저장하고 연산할 수 있는 집합이다.
제네릭(Generic)은 데이터의 타입을 하나로 지정하지 않고 사용할 때마다 범용적으로 지정할 수 있는 기법이다.
모든 종류의 타입을 다룰 수 있도록 일반화 된 타입 매개 변수 (generic type)로 클래스나 메서드를 선언한다.
기존에는 Object
객체를 사용 했지만 이제는 지네릭 클래스 <T>
를 사용함으로써 컴파일 시에 타입을 지정할 수 있게 되었고 다운 캐스팅이 필요없게 되었다.
java 는 기본적으로 객체지향 언어라 함수형 프로그래밍이 불가능한데,
JDK8부터 Stream API 와 람다식 등을 통하여 Java 를 이용해 함수형으로 프로그래밍 할 수 있는 API 를 제공해준다.
* 더 자세한 정리는 Stream 포스팅 참조
자바에서 리플렉션은 구체적인 클래스 타입은 알지 못하지만, 그 클래스의 정보에 접근할 수 있도록 해주는 자바API를 말한다.
자바에서는 JVM이 실행되면 작성된 자바 코드가 static
영역에 저장된다.
Reflection API는 이 정보를 활용하여 (구체적인 클래스 타입을 알지 못해도) 클래스 이름을 통해 static 영역에서 그 클래스의 정보(메서드, 타입, 변수 등등)에 접근할 수 있게 해준다.
때문에, 코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르지만 런타임 시점에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우 사용된다.
스프링에서 BeanFactory
는 어플리케이션이 실행한 후 객체가 호출 될 당시 객체의 인스턴스를 생성하게 되는데 이때 필요한 기술이 Reflection이다. 또한 프레임워크나 IDE에서 이런 동적 바인딩을 이용한 기능을 제공한다. (ex. intelliJ의 자동완성 기능, 스프링의 어노테이션)
Refelction API 는 JPA의 Entity가 기본 생성자를 지녀야 하는 이유와도 관련이 있다.
Reflection API 는 (클래스의 정보 중에서) 생성자의 인자 정보는 가져올 수 없다.
때문에 JPA를 사용할 때에는 기본 생성자가 있어야 객체를 생성할 수 있고 생성된 객체를 통해서 Reflection API는 필드 값 등을 넣어줄 수 있다.
* 더 자세한 정리는 JPA Entity 생성자 포스팅 참조
Runnable
인터페이스를 확장해 run() 메소드 구현Thread
클래스를 상속받고 run() 메소드를 오버라이딩해 구현다만, Runnable 인터페이스를 implements 받는 방법을 추천한다.
'extends' 와 'implements' 의 차이점에서 알 수 있다.
extends을 받으면 내가 받고 싶지 않은 메소드도 상속 받게 되는 문제가 생긴다.
또한 private가 아닌 메소드나 변수를 자식 클래스가 받게 되고 낭비가 생긴다.
따라서 인터페이스 implements 방식을 사용해 필요한 기능만 갖게 되고 결합도를 낮추는 방식이 객체지향적으로 적합하다.
Design Pattern 이란?
소프트웨어 개발 과정에서 발견한 설계 노하우를 패턴으로 정리한 것이다.
검증된 구조이며, 범용적인 코드스타일로 의사소통을 효율적으로 할 수 있다.
목적에 따라서 생성패턴, 구조패턴, 행동패턴으로 나눠진다.
클래스의 인스턴스가 하나만 생성되도록 하는 개발방법이다.
스프링 환경은 사용자의 요청이 1만건이 넘어서 그때마다 1만개의 객체를 생성할 수 없기에 하나의 객체를 생성해서 쓰레드로 공유하는 싱글톤 패턴 방식을 사용한다.
생성자가 private 인 것에 주목해보자.
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
}
객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여, 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법이다.
간단히 말해서, 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴이다.
public interface LottoGenerator {
List<LottoNumber> makeLottos();
}
public class RandomLottoGenerator implements LottoGenerator {
private final List<LottoNumber> numbers;
...
public List<LottoNumber> makeLottos() {
Collections.shuffle(numbers);
return numbers.subList(LOTTO_NUMBER_START_INDEX, LOTTO_NUMBER_END_INDEX);
}
}
객체를 직접 생성하지 않고 객체를 생성하는 Factory 객체를 사용하는 패턴이다.
어댑터를 사용해서 호환되지 않는 인터페이스를 호환하도록 하는 패턴이다.
상속을 통해서 부모 클래스의 기능을 확장할 때 사용하는 방법이다.
실세계의 사물 또는 개념들을 객체
로 바라보고, 상태와 행위를 가진 객체를 만들어 그 객체들 간 상호작용을 통해 프로그램이 동작하는 방식입니다.
유지보수가 어려웠던 절차지향, 구조적 프로그래밍을 해결하기 위해 등장했고,
특징인 상속, 추상화, 캡슐화, 다형성을 통해 객체를 독립성과 신뢰성이 보장되게 만들어 놓으면 재사용성이 높아지므로 유지보수가 용이합니다. 상추캡(이)다
(* 객체는 현실의 무언가를 추상적으로 표현한 것으로, 같은 종류의 데이터와 로직이 함께 있는 구성체라고 볼 수 있습니다. ex) 커피, 주문)
다형성은 하나의 메소드나 클래스가 다양한 방법으로 동작하는 것을 말합니다.
부모 클래스의 메소드를 자식 클래스가 오버라이딩해서 자신의 역할에 맞게 활용하는 것이라고 볼 수 있으며
오버라이딩을 통해 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경 할 수 있어 서버의 구현 기능을 유연하게 변경할 수 있습니다.
상속이란, 기존 클래스의 변수와 메소드를 모두 취하여 추가적인 기능도 가지는 클래스를 새로 만드는 것입니다.
부모 클래스에서 클래스의 행동과 정의를 가져다가 자식 클래스에서 사용할 수 있게 해줍니다. 자식 클래스를 정의할 때 이전 부모 클래스에서 새로운 행동을 추가하거나 새로운 타입에 대한 행동을 오버라이드 할 수 있습니다.
SRP, OCP, LSP, ISP, DIP 5개의 원칙이 있습니다.
SRP는 하나의 클래스는 하나의 책임만 가져야 한다.
OCP는 확장에는 개방되어있고, 변경에는 폐쇄적이어야한다.
LSP는 상위 타입은 항상 하위 타입으로 대체할 수 있어야 한다.
ISP는 범용 인터페이스 하나로 해결하기 보단, 여러 인터페이스로 분리하는 것이 좋다.
DIP는 구현체에 의존하지 말고 추상화에 의존해야한다.
(* 컴파일은 프로그램이 실행되기 전 소스코드를 기계어로 번환하는 과정)
(* 프로그램 수행시 해석된 바이트 코드가 Runtime Data Area에 배치되어 수행된다)
JVM은 Java가 OS에 상관없이 재사용이 가능하게 도와주는 가상 머신입니다.
또한 가비지 콜렉션을 통해 자동으로 메모리 관리를 해주는 역할을 수행합니다.
(JVM 은 크게 클래스 로더, 실행 엔진, 런타임 데이터 영역으로 이루어져있습니다)
자바의 메모리 공간은 크게 Method(Static) 영역, Heap 영역, Stack 영역으로 구분됩니다.
메소드 영역은 전역변수, static 변수 등을 저장하며, 프로그램의 시작부터 종료까지 메모리에 남아있습니다.
힙 영역은 new 연산자로 생성되는 인스턴스(객체,배열)를 저장하며 GC의 대상이 됩니다.
스택 영역은 메서드가 호출될 때마다 스택 프레임을 생성하여 지역 변수 등 메서드와 관련된 정보들을 저장하고, 메서드 종료시 소멸됩니다.
Method 영역은 JVM이 동작해서 클래스가 로딩될 때 생성되고,
Heap 영역은 컴파일 타임시 할당되며,
Stack 영역은 런 타임시 할당되며 메모리 영역이 할당됩니다.
힙 영역에서 생성되는 메모리 영역으로, 자바 프로세스 종료까지 계속 유지됩니다.
프로그래머가 작성한 상수에 대해 찾아보고, 없으면 상수 풀에 추가한 후 그 주소값을 리턴해줌으로써 메모리를 절약합니다.
JVM이 가비지 컬렉션을 실행하기 위해 애플리케이션 실행을 멈추고, GC를 실행하는 쓰레드를 제외한 모든 쓰레드의 작업을 중단합니다. Heap에서 사용하지 않는 메모리를 제거하고나면 중단된 작업이 재개됩니다.
이때 삭제는 가비지 컬렉터가 Stack의 모든 변수, Reachable 객체를 스캔하면서 각각 어떤 객체를 참고하고 있는지 찾아서 마킹하고, 마킹되지 않은 객체를 Heap에서 제거하는 방식으로 이루어집니다.
싱글톤 패턴은 인스턴스를 한 개만 생성하여 사용하는 디자인 패턴입니다.
인스턴스가 1개만 존재해야하는 것을 보장하고 싶거나 동일한 인스턴스를 자주 생성해야 하는 경우에 주로 사용합니다.
메모리 낭비 방지할 수 있다는 장점이 있습니다.
오버라이딩(Overriding)은 상위 클래스에 있는 메소드를 하위 클래스에서 재정의 하는 것을 말하고,
오버로딩(Overloading)은 매개변수의 개수나 타입을 다르게 하여 같은 이름의 메소드를 여러 개 정의하는 것을 말합니다.
static
을 변수와 메소드에 선언하면 Static 영역에 할당되되므로 인스턴스를 생성하지 않고도 프로그램 내에서 공통으로 사용되는 데이터들을 관리할 수 있습니다.
하지만, GC 관리 영역 밖에 존재하기 때문에 메모리에 주의하여 사용해야합니다.
static은 자주 변하지 않는 값이나 공통으로 사용되는 값 같은 공용자원에 대한 접근에 있어서 매번 메모리에 로딩하거나 값을 읽어들이는 것보다 일종의 '전역변수'와 같은 개념을 통해 접근하는 것이 비용도 줄이고 효율을 높일 수 있습니다.
자바 메모리 공간은 크게 static , stack , heap 영역으로 구분된다. 이때 static 메모리 공간은 전역변수와 static(정적)멤버변수를 저장하고 static 영역의 데이터는 프로그램의 시작부터 종료가 될 때까지 메모리에 남아있게 된다.
때문에 무분별하게 이 곳에 저장하다보면 메모리를 낭비하여 필요한 변수만 사용할 필요가 있다.
또한, 객체지향적 프로그래밍 측면에서 static은 외부에서 변경이 가능하게 되므로 객체의 데이터들이 캡슐화되어야 한다는 객체지향 프로그래밍 원칙에 위반된다.
추상 클래스는 클래스 내 추상 메소드가 하나 이상 포함되거나 abstract로 정의된 경우를 말하고,
인터페이스는 모든 메소드가 추상 메소드로만 이루어져 있는 것을 말합니다.
추상클래스는 다중상속이 불가능하지만, 인터페이스는 다중상속이 가능하다.
기본 자료형(Primitive data type)에 대한 객체 표현을 Wrapper class라고 합니다.
기본 자료형 → Wrapper class로 변환하는 것을 Boxing이라 하며,
Wrapper class → 기본 자료형으로 변환하는 것을 UnBoxing이라 합니다.
new String()은 new 키워드로 새로운 객체를 생성하기 때문에 Heap 메모리 영역에 저장되고,
""는 Heap 안에 있는 String Constant Pool 영역에 저장됩니다.
String은 불변의 속성을 가지며, StringBuffer와 StringBuilder는 가변의 속성을 가집니다.
StringBuffer는 동기화를 지원하여 멀티 쓰레드 환경에서 주로 사용하며,
StringBuilder는 동기화를 지원하지 않아 싱글 쓰레드 환경에서 주로 사용합니다.
https://dololak.tistory.com/699
리플렉션이란 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API 입니다.
코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르지만, 런타임 시점에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우 사용됩니다.
프레임워크나 IDE에서 이런 동적인 바인딩을 이용한 기능을 제공합니다. intelliJ의 자동완성 기능, 스프링의 어노테이션이 리플렉션을 이용한 기능이라 할 수 있습니다.
제네릭은 데이터의 타입을 하나로 지정하지 않고 사용할 때마다 범용적으로 지정할 수 있습니다.
타입을 사용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있어 에러를 사전에 방지할 수 있습니다.
Collection class에서 Generic을 사용하게되면, 컴파일러는 특정한 타입만 포함될 수 있도록 컬렉션을 제한합니다.
컬렉션 클래스에 저장되는 인스턴스 타입을 제한하여 런타임에 발생할 수 있는 잠재적인 모든 예외를 컴파일 타임에 잡아낼 수 있도록 도와주기 때문에 사용합니다.
HashMap은 key,value에 null을 입력할 수 있습니다.
HashTable은 key,value에 null을 허용하지 않습니다.
ConcurrentHashMap은 HashMap을 thread-safe 하도록 만든 클래스로, key,value에 null을 허용하지 않습니다.
여러 개의 쓰레드가 한 개의 자원을 사용하고자 할 때, 현재 데이터를 사용하고 있는 쓰레드를 제외하고 나머지 쓰레드들은 데이터에 접근할 수 없게 막는 개념입니다.
데이터의 thread-safe를 하기 위해 자바에서 Synchronized 키워드를 제공해 멀티 쓰레드 환경에서 쓰레드간 동기화를 시켜 데이터의 thread-safe를 보장합니다.
[참조]
Java의 정석
https://github.com/JaeYeopHan/Interview_Question_for_Beginner/tree/master/Java#jvm-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-gc-%EC%9D%98-%EC%9B%90%EB%A6%AC
https://dev-coco.tistory.com/153?category=1056309#%F-%-F%--%A-%--Java%EC%--%--%EC%--%-C%--%EC%A-%-C%EA%B-%B-%ED%--%--%EB%-A%--%--%EC%-B%--%EC%-B%-C%--%ED%--%--%EC%-E%--%EB%--%A-%EC%--%--%--%EB%AC%B-%EC%--%--%EC%-D%B-%--%EC%-E%--%EA%B-%A-%-C%--%EA%B-%--%EA%B-%--%--%EB%AA%--%--%EB%B-%--%EC%-D%B-%ED%-A%B-%EB%A-%BC%--%EC%B-%A-%EC%A-%--%ED%--%--%EB%--%--%EC%-A%--%-F
https://imbf.github.io/interview/2020/12/11/NAVER-Interview-Preparation-3.html
https://dololak.tistory.com/699 /String
https://tecoble.techcourse.co.kr/post/2021-09-30-java8-functional-programming/ 함수형 프로그래밍
https://n1tjrgns.tistory.com/288
https://victorydntmd.tistory.com/292 / 전략 패턴