[Java] 커피메이커 리팩토링 - SOLID 원칙 적용 +V2,V3 | 추가패턴조사

2025. 10. 11. 16:12·컴퓨터공학과/Java 1 & 2

.커피메이커 V1 or 배틀  or  Hunter&Animal 중 하나를 선택합니다.

2. 현재코드의 구조와 문제점을 분석하고 -  구조개선 - 코드품질을 개선합니다 : SOLID원칙 3개 이상

3. SOLID원칙을 적용하여 구조를 개선 한 후에는 기존 기능을 변경하거나 확장을 해보며 그 효과를 검증해봅니다.
- 시나리오 추가 / 기능추가 등  스스로 적용할 수 있는 범위에서 변화를 주어 응용해보세요.

4. 마지막으로, 느낀점을 정리하고,  주요이론과 실습을 복기하며 보고서를 작성하여 제출합니다.

 

능동체로 추가하자. 행위중심의 사용자로 봅시다

(ex) 사용자는 커피를 주문한다. <=> 키오스크는 주문을 받는다 (accept? NO)

해당 이 커피메이커는 여러가지 패턴이 적용되어있는 종합 소프트웨어이다.
 
 
 
팩토리-메소드 패턴 : 객체 생성 로직을 하위 클래스에 위임
WHEN : 생성 로직을 캡슐화하고 싶을 때
Before : 커피 , 커피제조방법, 순서대로 로직 작성해야한다.
After : coffeeFactory.createCoffee("라떼") : 커피 종류 추가 시, Factory 클래스만 수정하면됨.
 
 
 

포착해낸 문제점

1. 프로젝트 구조가 보기 불편함(너무많은파일들이 다같이있다.)

2. 커피머신이 여러개다. 머신가격이 카페에서 제일비싼데,,,, 웬만한 커피숍아니면 커피머신여러개 두기힘들다.

3. 커피머신 하나로 모든 커피를 처리하자. 

 

=> V2 에서 Manager로 행위주체를 변경하였다.

 

다음의 S.O.L.I.D 원칙을 적용해보자 (요약)

SRP: 단일책임의원칙 

OCP: 개방-폐쇄원칙 

LSP : 리스코프-치환 원칙  : 하위타입은 언제나 상위타입으로 대체될 수 있어야한다.

ISP : 인터페이스 분리원칙

DI : 의존성 역전 원칙 : 추상화에 의존하고, 구체 클래스에 의존하지 말아야한다.

기존의 커피메이커 - 구조를 중심으로 분석

 

 

현재 있는거에 것에 v2를 적용시켰다.  뚫고 지나간다. 그런데 여기서 잠깐. 사용하는 관계가 상하위로 표시되어있다. 이를 바꿔보자.(하단의 class diagram)

오른쪽과같이 되었다.

Q. 왜 Americano와 Latte는 espresso를 사용하지 않고 Coffee를 가져와서 사용할까?

=> 이것이 바로 DIP, OCP의 포인트! 

Americano가 Espresso를 직접 가지면 ‘구체 타입에 의존’하지만, Coffee(인터페이스)에 의존하면 ‘추상화에 의존’하게 된다.

 

DIP를 다시 되돌아보자 : 고수준 모듈은 저수준 모듈에 의존하지 말고, 둘 다 추상화에 의존해야 한다

OCP를 다시 되돌아보자. : 개방 폐쇄 원칙. =>확장에는 열려있고, 수정에는 닫혀있어야한다.

여기에서 사용하는 `Coffee base`는 에소프레소 이외의 다른 확장가능성을 남겨둔 것!

 

만약 Espresso라는 구체 클래스에 의존하게된다면 어떤 상황이 발생하는가?

  • 나중에 DecafEspresso, DoubleShotEspresso 같은 걸 만들어도 Americano를 재사용할 수 없다. => 수정이 필요하게된다.
  • OCP, DIP 위반!!

 

Refactoring... 

#1. 패키지 설정 리팩토링 

기존에 하나의 패키지에 만들었었던 커피메이커프로젝트를 패키지별로 나눠서 수정을 용이케하는 수정을 거쳤다.

before => after

 

기존의 알파벳 순서로 나열된 수많은 자바 파일들을, 패키지로 나눠주니 훨씬 가독성이 좋아졌다. 이제 SOLID로 더 리팩토링할 것을 찾아보자.

 

#2. 출력 수정 : 커피메이커가 아닌 행위주체, '매니저'가 마지막으로 커피를 제공한다.

CoffeeMaker 에서 커피를 제조하고, 커피를 ready 시켰다. 근데 manager가 주문을 받았으므로, manager가 최종적으로 손님에게 제공하는 과정이 필요할 것 같다.

매니저가 출력하는 것은 앞에 `[Manager]`을 표시하게 해서 정리했다. 

public void takeOrder(String coffeeType, Payable payment) {
		if(!menu.hasItem(coffeeType)) {
			System.out.println("Sorry, we don't serve "+ coffeeType);
			return;
		}
		int price = menu.getPrice(coffeeType);
		System.out.println("[Manager] 주문 접수: " + coffeeType);
		payment.pay(price);
		Coffee coffee = factory.createCoffee(coffeeType);
		maker.make(coffee);
		System.out.println("[Manager] 주문 완료: " + coffeeType + " ☕\n");
	}

 

manager는 말하자면, Controller 역할을한다. 여기저기 일시키는 총괄역할.

  • 1. 주문받기 -> 메뉴에있는가? 체크 .orElseThrow(()-> new NotFoundException("Sorry, we don't serve ...")
  • 2.가격을 확인하고 주문정보 받은 커피타입을 출력한다.
  • 3. factory(기계)에서 만든 커피생성 (어떤커피? 결정)
  • 4. maker에게 커피를 준비시킴.
  • 5. 커피를 가져와서 제공해줌.

 

 

 

 

#3. OCP (개방-폐쇄원칙), DIP, SRP : 데코레이터 패턴을 적용하여 '디카페인 샷'을 추가해보자.)

Main에 한줄 추가.

manager.takeOrder("DecaffeineAmericano",new CreditCardStrategy());

 

디카페인 샷을 내리기 위해선 새로운 클래스는 뭐가 필요할까?

오.로.지 DecaffeineMachine(디카페인 샷을 내림)!

public class DecaffeineMachine implements CoffeeMachine {
	@Override
	public String brew() {
		return "디카페인 샷을 내립니다 ..... ";
	}
}

잠깐!!! 그럼 왜 Decaffeine이라는 클래스는 필요없을까? 더보기란 확인 ▼

더보기

 만들게된다면 하단과 같은 구조를 가진다. = Espresso 클래스랑 완전히 같다! 그럼 중복코드가 되는거다. 

 

본질을 생각하자. 에소프레소를 만들때에는 원두가 달라진다 = 커피를 내리는 머신만달라져서, 다른동작을 하면된다.

public class Decaffeine implements Coffee {
	private CoffeeMachine machine;

	public Decaffeine(CoffeeMachine machine) {
		this.machine = machine;
	}

	@Override
	public String prepare() {
		return machine.brew();
	}
}

즉 결과를 비교해보자. `brew()`는 machine이 결정하므로, 굳이 구체적인 커피- Decaffeine 클래스가 생길필요는 없었다!

파란색 라인과 결과를 보자. 똑같다.

끝. 

 


factory에도 관련 코드를 몇개추가해주자.
커피머신주입받아서, 생성자 만드는부분수정해준다. 반환하는 메뉴도추가해준다.

public class CoffeeFactory {
	private final CoffeeMachine espressoMachine;
	private final CoffeeMachine decafMachine;
	private final CoffeeMachine dripMachine;
	private final MilkFrother frother;

	public CoffeeFactory(CoffeeMachine espressoMachine, CoffeeMachine decafMachine, CoffeeMachine dripMachine,
			MilkFrother frother) {
		this.espressoMachine = espressoMachine;
		this.decafMachine = decafMachine;
		this.dripMachine = dripMachine;
		this.frother = frother;
	}

	public Coffee createCoffee(String name) {
		switch (name) {
		case "Latte":
			return new Latte(new Espresso(espressoMachine), frother);
		case "Americano":
			return new Americano(new Espresso(espressoMachine));
		case "DecaffeineAmericano":
			return new Americano(new Decaffeine(decafMachine));
		case "Decaf":
			return new Espresso(decafMachine); // 머신만 교체!
		case "Drip":
			return new Espresso(dripMachine); // 머신만 교체!
		default:
			return new Espresso(espressoMachine);

		}
	}
}

결과 : 디카페인샷이 잘 출력되고있다.

 

 

#4. CoffeeMachine들의 SRP. - 커피를 내리는데 왜 머신을바꿔? 원두를 바꿔야지!

현재 상황을보자.... 커피를 내리는데 드립커피야 뭐 내리는 방법이 다르니까 상관없지만, 디카페인/ 일반 머신의 경우 커피종류에따라 머신을바꾸고있다. 그러나 카페에 가면 나같은 알바는 원두 뽑아서 탬핑하고 그걸 '머신'에 꽂아서 `brew()`를 한다. 

 

즉, 현재 커피를 내릴때마다 머신을 지정해줘야함. 다만, 현실은? machine 고정에 원두만 바뀐다!

머신의 본질적인 기능 커피추출(`brew()`)에 집중하자.

이제, 새로운 원두를 추가할 때마다, 인자로 넣어주면, 자동으로 해당 원두를 사용해 커피를 내리게 되었다. 

 

수정해 보도록 하자.

 

이러고 CoffeeFactory를 수정해주었다. 에소프레소 클래스 또한, 원두를 받아서 샷을 내리도록 재수정하였다.

=> CoffeeMachine은 샷을 추출만한다. SRP 충족!

(라떼/아메리카노 등은 베이스를 활용만 하니까 수정할필요없음)

기존 것(좌측 주석처리) 에서는 원두를 내리는데 머신을 바꾸면서 했지만, 이제는 원두를 넣어주면된다.

 

결과

 

소감

사실 커피메이커 자체에서는 크게 변경할게 없었다. 데코레이터 패턴을 활용해 디카페인 샷을 추가하면서 확장 하였다.

다만 머릿속에서 현실세계의 커피매장이랑 코드랑 비교하다보니 너무나도 많은 오류가있었다. 다만 고치려면 구조를 뜯어고쳐야해서 고치지않았다.

고치라면 시나리오부터 다시 짜고 해야할것같은데, 진짜 남이 짠 코드의 리팩토링은 그사람의 코드를 이해하고, 내가 바꿔야하므로 정말 오랜시간이 걸리는게 맞다.... 그래서 여기까지하자.

 

예) 커피는 '사람'이 만든다. 커피 스스로 머신에게 prepare하라고 시킬 수 없다.

예) 커피팩토리는 커피의 생성만하자. 현재는 커피팩토리가 커피에 들어갈 커피머신, 거품기, 원두 등 다 조합하고있는데 혼자 너무 많은 책임을 가지고있다.(SRP 위반) 그런데 나중에가서, 카페모카등이 추가되어 초콜릿시럽, 휘핑크림을 위한 휘핑기가 추가된다면? 이를 수정하려면 Builder 패턴 등이 필요하지않을까? 

https://terms.naver.com/entry.naver?docId=3532962&cid=58528&categoryId=58528

 

builder 패턴

복잡한 것을 만들 때는 전체를 한꺼번에 만들기보다는 작게 분리하여 만든 후 다시 합치는 것이 편리하다. builder 패턴은 복잡한 인스턴스를 조립하여 만드는 구조로, 복합 객체를 생성할 때 객

terms.naver.com

 해당 패턴이 필요한 이유:

객체들의 조합으로 만들어져야하는 객체의 경우, 다 만든 후에 합친다.

builder 패턴에서   director 클래스는 construct 메서드를 하나 가지고 있고, 그 메서드를 사용해서 어떤 구체 클래스가 선택되는지 결정한다.

 

지금 Factory는 조합까지 같이한다.

  • 어떤 머신을 쓸지 (드립커피머신, 휘핑기, 거품기, 커피머신 등등)
  • 어떤 원두를 넣을지(일반 원두, 디카페인 원두)

→ SRP 위반, Factory가 지나치게 많은 조합 책임을 갖고 있다.

 

Factory가 '무엇을 만들지'만 알고, 조합은 다른 책임자(Builder/Director)가 담당해야 더 직관적일 것 같다.

'컴퓨터공학과 > Java 1 & 2' 카테고리의 다른 글

[java] Gui (+Thread) 실습#6  (1) 2025.11.02
[Java] SOLID 실습 짱구의 매너전략  (0) 2025.10.15
[Java] Thread  (0) 2025.09.22
[Java] Set과 Map  (0) 2025.09.17
JAVA backjoon 10815 이진탐색, 스트링빌더, 배열  (0) 2025.07.13
'컴퓨터공학과/Java 1 & 2' 카테고리의 다른 글
  • [java] Gui (+Thread) 실습#6
  • [Java] SOLID 실습 짱구의 매너전략
  • [Java] Thread
  • [Java] Set과 Map
sihyes
sihyes
24학번 컴퓨터공학과
  • sihyes
    시혜적으로개발
    sihyes
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (114)
      • 단순 설정 (10)
      • 백엔드 공부(BE, AWS) (8)
        • 로그인&회원가입 (3)
        • 파일업로드&GPT (2)
      • 컴퓨터공학과 (51)
        • 운영체제 (0)
        • Artificial Intelligence (0)
        • Java 1 & 2 (23)
        • 컴퓨터네트워크 (3)
        • 모앱JavaScript (0)
        • Data structures (9)
        • 소프트웨어공학 (5)
        • 오픈SW플랫폼 제출용 (5)
        • Python - 문해프 (1)
      • 개인 프로젝트 (2)
        • 알바솔로몬 (1)
        • PLACO 프로젝트 (0)
      • 도서 공부(정리) (20)
        • 알고리즘 코딩 테스트 자바 편 (1)
        • SQL첫걸음 (8)
        • 코딩 자율학습 스프링 부트 3 자바 백엔드 개발 .. (6)
        • Do it! 지옥에서 온 문서 관리자 깃&깃허브 .. (5)
      • 개인공부정리페이지 (12)
        • 백준 & 프로그래머스 (3)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ㅇ
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.4
sihyes
[Java] 커피메이커 리팩토링 - SOLID 원칙 적용 +V2,V3 | 추가패턴조사
상단으로

티스토리툴바