리팩토링을 어떻게 하는지 알게 되었음.
DI 적용 방법
캡슐화
아래 코드는 주문하는 클래스에 할인까지 적용을 하고 있다.
캡슐화를 위반, 객체 내부 동작을 너무 잘 알고 있음. 직접 꺼내서 호출하고있으며 내부 로직 순서를 결정하고 있다.
응집도가 떨어지고 결합도가 높아지고 객체의 자율성이 떨어졌다.
할인조건 로직이 바뀌면 주문 클래스의 코드까지 바꿔줘야 한다.
for (DiscountCondition discountCondition : discountConditions){
discountCondition.checkDiscountCondition();
if (discountCondition.isSatisfied()) finalPrice = discountCondition.applyDiscount(finalPrice);
}
-> 할인만 적용해주는 클래스를 만든다.
public class Discount {
private DiscountCondition[] discountConditions;
public Discount(DiscountCondition[] discountConditions) {
this.discountConditions = discountConditions;
}
ProductRepository productRepository = new ProductRepository();
Product[] products = productRepository.getAllProducts();
Menu menu = new Menu(products);
Cart cart = new Cart(productRepository, menu);
Order order = new Order(cart, new Discount(
new DiscountCondition[]{
new CozDiscountCondition(new FixedRateDiscountPolicy(10)),
new KidDiscountCondition(new FixedAmountDiscountPolicy(500))
}));
public class Appconfigurer {
Cart cart = new Cart(productRepository(),menu());
public ProductRepository productRepository(){
return new ProductRepository();
}
public Menu menu(){
return new Menu(productRepository().getAllProducts());
}
public Cart cart(){
return cart;
}
public Order order(){
return new Order(cart(),new Discount(
new DiscountCondition[]{
new CozDiscountCondition(new FixedRateDiscountPolicy(10)),
new KidDiscountCondition(new FixedAmountDiscountPolicy(500))}));}}
"한 클래스는 하나의 책임만 가져야 한다.""
모든 클래스는 각각 하나의 책임만 가져야 하고 수정할 이유는 단 한가지여야 한다.
즉 클래스는 그 책임을 완전히 캡슐화해야 한다.
예를들어 결제 클래스가 있다면 이 클래스는 오직 결제 기능만을 책임지고,
만약 이 클래스를 수정해야한다면 결제에 관련된 문제일 뿐일 것이다.
DI란 Dependency Injection, 의존성 주입이라고 한다.
객체를 직접 클래스 내에서 생성하면 강한 결합이 되기에 느슨한 결합을 위해 DI를 적용하는데 생성자를 만들고 외부에서 주입하는 방식을 많이 쓰고 있다.
이론은 필요없다. 바로 실습 코드로 이해해보자.
실습 했던 burgerQueen을 통해 "DI가 왜 필요한지와 어떻게 적용할 수 있는지"를 알아보겠다.
public class CozDiscountCondition {
private FixedRateDiscountPolicy fixedRateDiscountPolicy = new FixedRateDiscountPolicy(10);
public class KidDiscountCondition {
private FixedAmountDiscountPolicy fixedAmountDiscountPolicy = new FixedAmountDiscountPolicy(500);
할인 조건에 적용되는 할인 정책을 변경해야 할 경우 FixedRateDiscountPolicy 인스턴스화를 지우고 새로 만들고 하는 번거로움이 있다.
왜 이런 번거로움이 생겼는가? CozDiscountConditionrhk KidDiscountCondition 이 사용할
FixedRateDIscountPolicy 객체와 fixedAmountDiscountPolicy의 객체를 직접 결정 및 생성하고 구체적인 클래스 타입의 참조변수에 할당하고 있다.
해결법은 자신이 사용할 객체 두가지를 직접 결정 및 생성을 못하게 하면 된다.
그리고 직접 생성하지 못하게 하는 것은 CozDiscountCondition에서 직접 생성하는 것이 아니라
외부에서 생성 후 주입을 하면 되겠다.
그리고 추상적인 클래스 타입의 참조변수에 할당을 하면 되니까 인터페이스를 사용하면서 추상 메서드를 사용하면 되겠다.
인터페이스 적용
public interface DiscountPolicy { int calculateDiscountedPrice(int price);}
public class FixedAmountDiscountPolicy implements DiscountPolicy{ private int discountAmount; public int calculateDiscountedPrice(int price) { return price - discountAmount; } }
public class FixedRateDiscountPolicy implements DiscountPolicy{ private int discountRate; public int calculateDiscountedPrice(int price) { return price - discountAmount; } }
인터페이스를 적용했으니까 DiscountPolicy를 참조변수의 타입으로 선언을 해준다.
// DiscountPolicy를 구현한 객체만 인스턴스만 할당 될 수 있다.
DiscountPolicy discountPolicy;
//외부에서 받아오도록 생성자를 통해 주입받는다. DI!
public CozDiscountCondition(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public class KidDiscountCondition {
DiscountPolicy discountPolicy;
public void makeOrder(){
CozDiscountCondition cozDiscountCondition = new CozDiscountCondition(★new FixedRateDiscountPolicy(10)★); // ★만 바꿔주면 됨!!
DI를 적용함으로써 할인 정책이 바뀔 때 CozDiscountCondition 클래스 내의 코드들을 수정할 필요 없이 외부에서 주입할 생성자 객체를 바꿔주기만 하면 된다.
왜? 두루뭉실한 참조변수 클래스 타입을 선정했고, 생성자를 통해 외부에서 주입을 받음으로써 CozDiscountCondition은 인스턴스 안에 어떤 인스턴스가 들어오는지도 알 수가 없다.
그래서 할인 정책을 바꿔도 CozDiscountCondition 을 바꿀 필요가 없는 것.
- 얕은 복사 : 객체를 복사할 때 해당 객체만 복사하고 복사된 객체의 인스터스 변수는 운복 객체의 인스턴스 변수와 같은 메모리 주소를 참조한다.
- 깊은 복사 : 객체를 복사할 때, 해당 객체와 인스턴스 변수까지 복사하는데 전부를 복사하여 새 주소에 담기 때문에 참조를 공유하지 않는다.
🟢얕은 복사
public class CopyObject {
private String name;
private int age;
public CopyObject() {
}
public CopyObject(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class CopyObjectTest {
@Test
void shallowCopy() {
CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original; // 얕은 복사
copy.setName("JuBal");
System.out.println(original.getName());
System.out.println(copy.getName());
}
}
///출력
JuBal
JuBal
CopyObject copy = original 의 코드에서 얕은 복사를 통해 주소값을 변경했기에
참조하고 있는 실제 값은 동일하고, 복사한 객체가 변경되면 기촌의 객체도 변경 된다.
🟢깊은 복사
public class CopyObject {
private String name;
private int age;
public CopyObject() {
}
/* 복사 생성자 */
public CopyObject(CopyObject original) {
this.name = original.name;
this.age = original.age;
}
public CopyObject(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@Test
void shallowCopy() {
CopyObject original = new CopyObject("yong", 20);
CopyObject copyConstructor = new CopyObject(original);
copyConstructor.setName("man");
System.out.println(original.getName());
System.out.println(copyConstructor.getName());
}
//출력
yong
man
얕은 복사와는 다르게 Heap 영역에 새로운 메모리 공간을 생성하여 실제 값을 복사한다.
프로그램 내 에서 단 1개만 존재해야 하는 객체가 있으며 이를 프로그램 내부의 여러 부분에서 호출하여 사용하는 경우다.
- 프로그램 내 에서 어떤 객체가 단 1개만 존재해야 한다.
- 프로그램 내부의 여러 부분에서 이 객체를 공유하며 사용한다.
싱글톤 패턴은 객체가 프로그램 내부에서 단 1개만 생성됨 을 보장하며 멀티 스레드에서 이 객체를 공유하며 동시에 접근하는 경우에 발생하는 동시성 문제도 해결해주는 디자인 패턴이다.
🟥싱글톤 패턴의 기본적인 구현 방법
public class Singleton {
// 단 1개만 존재해야 하는 객체의 인스턴스, static 으로 선언
private static Singleton instance;
// private 생성자로 외부에서 객체 생성을 막음
private Singleton() {}
// 외부에서는 getInstance() 로 instance 를 반환
public static Singleton getInstance() {
// instance 가 null 일 때만 생성
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
private 생성자로 외부에서 객체 생성을 막으며, getInstance() 함수 로만 instance 에 접근이 가능하게 한다,
instance 는 static 변수로 선언되며 getInstance() 가 최초로 호출 될 때 에는 null 이므로 한 번만 생성하고, 이후에는 생성해놨던 instance 를 반환한다.
따라서 다른 함수에서 getInstance() 를 여러 번 호출 하더라도 단 하나의 동일한 instance 만을 반환 해준다.