Skip to content

Commit

Permalink
feat(ObjectChapter10.md): 비글챌 글쓰기
Browse files Browse the repository at this point in the history
  • Loading branch information
02ggang9 committed Nov 14, 2023
1 parent 026b031 commit 4ae3226
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 13 deletions.
Binary file added _posts/images/java/object/10장디펜던시.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
212 changes: 212 additions & 0 deletions _posts/java/2023-11-14-ObjectChapter10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
---
title: "오브젝트 - 상속과 코드 재사용"
categories:
- Java
---

우아한 테크코스 4주차 프리코스 미션인 크리스마스 문제를 풀고 리팩토링을 하기 위해서 'Chapter 10 - 상속과 코드 재사용'과 조영호 님의 '애플리케이션 아키텍처와 객체지향' 세미나를 참고했습니다. 처음 미션을 진행했을 때 중복되는 코드가 굉장히 많았는데 어떻게 중복되는 코드를 줄이고 추상화에 의존하는 방법에 대해서 알아보도록 하겠습니다.

## 상속과 중복 코드
중복 코드를 제거해야 하는 가장 큰 이유는 변경을 방해하기 때문입니다. 비즈니스 요구 사항은 매번 바뀌고 100% 작성한 모든 코드는 수정되기 마련입니다. 이번 프리코스 미션에서 작성한 코드를 예시로 중복 코드가 일으키는 문제점에 대해서 설명하겠습니다.

~~~java
// 특별 할인 정책
public class SpecialDiscountEvent {
List<Integer> days = List.of(3, 10, 17, 24, 25, 31);
private static final int BASE_DISCOUNT_AMOUNT = 1000;

public int discountAmount(int dateOfVisit) {
if (days.contains(dateOfVisit)) {
return BASE_DISCOUNT_AMOUNT;
}
return 0;
}
}
~~~
~~~java
// 크리스마스 할인 정책
public class ChristmasDiscountEvent {
private static final Integer BASE_DISCOUNT_AMOUNT = 1000;

public static int discountAmount(int dateOfVisit) {
return BASE_DISCOUNT_AMOUNT + ((dateOfVisit-1) * 100);
}
}
~~~
~~~java
// 평일 할인 정책
public class WeekdayDiscountEvent {
List<Integer> days = List.of(3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 17, 18, 19, 20, 21, 24, 25, 26, 27, 28, 31);

private static final Integer BASE_DISCOUNT_AMOUNT = 2023;

public int discountAmount(int dateOfVisit, int dessertCount) {
if (days.contains(dateOfVisit)) {
return BASE_DISCOUNT_AMOUNT * dessertCount;
}
return 0;
}
}
~~~
~~~java
// STEP 7 : 혜택 내역 출력
int christmasDiscountAmount = getChristmasDiscountAmount();
int weekdayDiscountAmount = getWeekdayDiscountAmount();
int weekendDiscountAmount = getWeekendDiscountAmount();
int specialDiscountAmount = getSpecialDiscountAmount();
int giveDiscountAmount = getGiveDiscountAmount(giveMenu);
int totalDiscountAmount = christmasDiscountAmount + weekdayDiscountAmount + weekendDiscountAmount + specialDiscountAmount + giveDiscountAmount;

printBenefitDetails(totalDiscountAmount, weekdayDiscountAmount, christmasDiscountAmount, weekendDiscountAmount, specialDiscountAmount, giveDiscountAmount);
~~~

추가적으로 주말 할인 정책 등이 있습니다. 위 3개의 클래스 모두 discountAmount라는 중복 코드가 있습니다. 만약 세금을 부과한다는 요구사항이 추가된다면 5개의 클래스(+ 주말 할인, 증정 할인 클래스) 모두 수정해야 합니다. 그리고 추가 할인 정책이 들어온다면 전체 할인 금액을 출력하기 위해서 파라미터의 개수 또한 증가하게 됩니다.

이를 해결하기 위해 상속을 이용할 수 있지만 이번주 프리코스 미션에서는 상속을 사용할 경우 생기는 커플링 문제점을 보여드릴 수 없기 때문에 과감히 넘어가도록 하겠습니다. 자세한 예시는 오브젝트 책 p.318쪽을 참고하시면 됩니다.


## 추상화에 의존하고 차이를 메서드로 추출하라
커플링 문제를 없애기 위해서는 추상화를 이용하면 됩니다. 자식 클래스가 부모 클래스의 구현에 의존하도록 만드는 것이 아니라 부모 클래스와 자식 클래스 모두 추상화에 의존하도록 설계하면 됩니다.

~~~java
// 추상 클래스
public abstract class Discount {
protected List<DiscountPolicy> policies;

public Discount(DiscountPolicy ... policies) {
this.policies = Arrays.asList(policies);
}

public abstract void calculateDiscountAndSaveDetail(BenefitDetail benefitDetail, OrderSheet orderSheet);
}
~~~
~~~java
// Discount 추상 클래스에 의존하는 크리스마스 할인 정책 클래스
public class ChristmasDiscount extends Discount{
private static final Integer BASE_DISCOUNT_AMOUNT = 1000;
private static final String DISCOUNT_NAME = "크리스마스 디데이 할인: -";

public ChristmasDiscount(DiscountPolicy... policies) {
super(policies);
}

@Override
public void calculateDiscountAndSaveDetail(BenefitDetail benefitDetail, OrderSheet orderSheet) {
for (DiscountPolicy policy : policies) {
if (policy.isSatisfiedBy(orderSheet)) {
benefitDetail.saveEvent(DISCOUNT_NAME, discountPrice(orderSheet.getDateOfVisit()));
}
}
}

private int discountPrice(int dateOfVisit) {
return BASE_DISCOUNT_AMOUNT + ((dateOfVisit-1) * 100);
}
}
~~~
~~~java
// Discount 추상 클래스에 의존하는 평일 할인 정책 클래스
public class WeekdayDiscount extends Discount {
private static final String DISCOUNT_NAME = "평일 할인: -";
private static final Integer BASE_DISCOUNT_AMOUNT = 2023;

public WeekdayDiscount(DiscountPolicy... policies) {
super(policies);
}

@Override
public void calculateDiscountAndSaveDetail(BenefitDetail benefitDetail, OrderSheet orderSheet) {
for (DiscountPolicy policy : policies) {
if (policy.isSatisfiedBy(orderSheet)) {
benefitDetail.saveEvent(DISCOUNT_NAME, discountPrice(orderSheet));
}
}
}

private int discountPrice(OrderSheet orderSheet) {
return BASE_DISCOUNT_AMOUNT * orderSheet.getOrders()
.entrySet()
.stream()
.filter(o -> o.getKey().getMenuType() == MenuType.DESSERT)
.mapToInt(Map.Entry::getValue)
.sum();
}
}
~~~

Discount 추상화 클래스에 의존하게 만들어서 커플링 문제를 해결했지만 아직 calculateDiscountAndSaveDetail() 메서드가 모든 클래스에서 중복됩니다.

## 중복 코드를 부모 클래스로 올려라
위에서 calculateDiscountAndSaveDetail() 메서드에서 for문을 돌면서 할인 정책에 해당하는지 확인하는 과정은 변하지 않는 부분이고 세부 할인 금액을 계산하는 과정은 각 클래스마다 변하게 됩니다. 완전히 동일한 코드인 calculateDiscountAndSaveDetail() 메서드는 부모 클래스로 올리고 할인 금액을 구하는 세부 로직은 자식 클래스에서 오버라이딩할 수 있도록 protected로 선언하면 '추상화에 의존하고 중복된 코드를 제거'하는 최종 목표에 도달할 수 있습니다.

완성된 코드는 아래와 같습니다.

~~~java
public abstract class Discount {
protected List<DiscountPolicy> policies;

public Discount(DiscountPolicy ... policies) {
this.policies = Arrays.asList(policies);
}

public void calculateDiscountAndSaveDetail(BenefitDetail benefitDetail, OrderSheet orderSheet) {
policies.stream()
.filter(discountPolicy -> discountPolicy.isSatisfiedBy(orderSheet))
.forEach(discountPolicy -> calculateAndSave(benefitDetail, orderSheet));
}

abstract protected void calculateAndSave(BenefitDetail benefitDetail, OrderSheet orderSheet);
}
~~~
~~~java
public class ChristmasDiscount extends Discount{
private static final Integer BASE_DISCOUNT_AMOUNT = 1000;
private static final String DISCOUNT_NAME = "크리스마스 디데이 할인: -";

public ChristmasDiscount(DiscountPolicy... policies) {
super(policies);
}

@Override
protected void calculateAndSave(BenefitDetail benefitDetail, OrderSheet orderSheet) {
benefitDetail.saveEvent(DISCOUNT_NAME, discountPrice(orderSheet.getDateOfVisit()));
}

private int discountPrice(int dateOfVisit) {
return BASE_DISCOUNT_AMOUNT + ((dateOfVisit-1) * 100);
}
}
~~~
~~~java
public class WeekdayDiscount extends Discount {
private static final String DISCOUNT_NAME = "평일 할인: -";
private static final Integer BASE_DISCOUNT_AMOUNT = 2023;

public WeekdayDiscount(DiscountPolicy... policies) {
super(policies);
}

@Override
protected void calculateAndSave(BenefitDetail benefitDetail, OrderSheet orderSheet) {
benefitDetail.saveEvent(DISCOUNT_NAME, discountPrice(orderSheet));
}

private int discountPrice(OrderSheet orderSheet) {
return BASE_DISCOUNT_AMOUNT * orderSheet.getOrders()
.entrySet()
.stream()
.filter(o -> o.getKey().getMenuType() == MenuType.DESSERT)
.mapToInt(Map.Entry::getValue)
.sum();
}
}
~~~

## 마치며
이번 글에서는 오브젝트 책에서 나온 내용을 토대로 4주차 프리코스 미션인 산타 게임을 풀어봤습니다. 상속만 사용한다면 코드를 재사용하고 있다는 느낌은 들지만 사실 중복 코드 제거를 위해 새로운 중복 코드를 만들어야 합니다. 또, 부모 클래스 코드를 재사용하기 위해 개발자가 세운 가정을 이해해야 하는 번거로운 과정을 거쳐야 합니다. 따라서 추상 클래스를 상속해야 하며, 그렇게 했을 경우에 생기는 이점에 대해서도 알아봤습니다.

마지막으로 이번 미션에서의 디펜던시를 그려보면서 마무리 하도록 하겠습니다.
![디펜던시](https://github.com/02ggang9/02ggang9.github.io/blob/master/_posts/images/java/object/10장디펜던시.jpeg)



13 changes: 0 additions & 13 deletions _posts/wooteco/2023-11-14-우테코4차소감문.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@ categories:


## 클래스 분리 연습

지난 주 미션을 진행하면서 공부한 '함수형 인터페이스'에 관한 내용을 블로그 글에 정리하면서 이번주 미션에는 '상속과 합성'에 대한 내용이 등장할 것 같다라는 글을 남겼는데 실제 주된 내용이 '상속과 합성'이라서 깜짝 놀랐습니다. 예측은 성공했지만 실제 추상 클래스와 인터페이스를 사용한 적이 손에 꼽을 정도라 처음 객체들을 설계할 때 상속을 고려하지 못했습니다. 일단 최종 코딩테스트를 진행하는 마음으로 일단 돌아가는 쓰레기를 만들도록 노력했습니다.

1차 구현을 완성한 제 코드는 객체지향언어를 사용해서 완성했다라고는 도저히 믿어지지 않을 정도의 코드였습니다. 예를 들어, 크리스마스 할인, 주말 할인 클래스 등을 전부 다 따로 만들고 각각 할인 금액을 계산하도록 설계 했습니다. 전체적으로 맡은 책임은 같아 하는 행동은 같았지만 메서드의 이름은 서로 달랐기 때문에 구현을 하면서도 어떻게 상속으로 깔끔하게 처리하지? 의문이 들었지만 일단 주어진 5시간 이내에 완성하려고 노력했습니다.

4시간이 지나고 주어진 테스트 케이스를 전부다 통과하는 코드를 완성했습니다. 이때부터는 마음을 차분히 가라 앉히고 현재 작성한 코드의 디펜던시를 그려보고 CRC CARD를 이용해 완성된 객체의 책임과 협력 관계를 A4용지에 그려보았습니다. 그려보니 객체와의 협력은 하나도 없었고 전체 할인 금액을 구할 때 파라미터의 수가 6개가 넘어가는 상황이 만들어졌습니다. 만약 할인 이벤트가 계속해서 추가된다면 파라미터의 수는 계속해서 증가할 것이고 이는 언제가는 터질 폭탄처럼 보이는 코드였습니다.

위의 문제를 해결하기 위해 어김없이 '오브젝트'라는 책을 중심으로 학습했습니다. 이번 미션을 진행하면서 가장 인상 깊었던 내용은 '상속과 코드 재사용'입니다. 위에서 얘기드렸지만 해당하는 할인 정책에 맞는 금액을 계산하는 책임은 모든 클래스가 가지고 있었지만 메서드의 이름도 다르고 계산 과정이 달랐기 때문에 중복코드가 5개 이상이였습니다. '상속과 코드 재사용' Chapter를 읽으면서 어떻게 중복 코드를 제거하고 추상화에 의존시키는지 알게 되었습니다. 여러가지 할인 클래스를 Discount 클래스를 상속받게 하고 클라이언트가 Discount라는 추상화에 의존하도록 완성 시켰습니다.

3차 구현으로는 오브젝트 책의 '차이를 메서드로 추출하라'의 내용을 적극적으로 반영했습니다. 2차 구현에서 추상화에 의존하도록 만들고 Discount를 상속받도록 만들었지만 여전히 큰 흐름의 로직이 중복되는 느낌을 받았습니다. 이를 해결하기 위해서 서로다른 계산 로직이 있는 부분만 메서드로 추출하고 중복이 되는 코드를 부모 클래스로 올렸습니다. 이렇게 함으로써 단일 책임 원칙을 준수하고 응집도가 높아지는 효과를 얻었습니다. 또, 다음에 있을 새해 이벤트에서도 주말 할인, 특별 할인 등의 클래스를 재사용할 수 있게 되었습니다.

---

지난주 미션을 진행하면서 공부한 '함수형 인터페이스'에 관한 내용을 블로그 글에 정리하면서 이번 주 미션에는 '상속과 합성'에 대한 내용이 등장할 것 같다는 글을 남겼는데 실제 주된 내용이 '상속과 합성'이라서 깜짝 놀랐습니다. 예측은 성공했지만, 실제 추상 클래스와 인터페이스를 사용한 적이 손에 꼽을 정도라 처음 객체들을 설계할 때 상속을 고려하지 못했습니다. 일단 최종 코딩테스트를 진행하는 마음으로 일단 돌아가는 쓰레기를 만들도록 노력했습니다.

1차 구현을 마친 코드는 객체지향 언어를 사용해 완성된 것이라고는 믿기 어려울 정도로 엉망이었습니다. 예를 들어, 크리스마스 할인, 주말 할인 클래스 등을 별로도 설계하고 각각의 할인 금액을 계산하는 방식은 전체적인 책임이 같고 행동도 같았지만 메서드 이름과 계산 방식이 달라 중복되는 코드가 많았습니다. 중복되는 코드가 많아짐으로써 인스턴스 변수의 수는 자연스럽게 증가하고 하나의 객체가 수많은 책임을 가지게 되었습니다. 3주 차 피드백 내용을 적극적으로 반영하지 못했다는 느낌이 들었고 리팩토링을 마음먹었습니다.
Expand Down

0 comments on commit 4ae3226

Please sign in to comment.