Week5 SOLID, 커피 리팩토링 v.1

나현·6일 전

java2

목록 보기
7/7

클린코드와 리팩토링

1) 클린코드

  • 유연하고(Flexible), 견고하며(Robust), 유지보수하기 쉬움(Maintainable)
    -> 개발자의 논리적인 사고 흐름이 반영된 좋은 코드는 협업을 원활하게 하고 시간이 지나도 견고하게 유지되며 빠른 의사소통으로 팀 전체의 생산성을 높임

2) 리펙토링

  • 클린코드를 만들어가는 핵심적인 활동이자, 코드의 구조를 건강하게 개선하는 활동!
  • "소프트웨어의 겉보기 동작은 그대로 유지한 채, 내부 구조를 변경하여 이해하고 수정하기 쉽게 만드는 과정"
  • 버그를 잡거나 새로운 기능을 추가하는 것이 아님!

S.O.L.I.D 기초개념

1) SOLID란

  • 유연성(새로운 기능 쉽게 추가), 유지보수성, 재사용성
  • 끊임없이 변화하는 소프트웨어에 유연하게 대처
  • 결합도 낮추고 응집도 높이기

2) 결합도와 응집도

  • 결합도: 모듈과 모듈간의 의존 정도 <-> 클래스 간의 자유로운 교체
    -> 결합도 낮게!
  • 응집도: 한 모듈내 구성요소의 연관정도 - 클래스 내부가 하나의 목적에 집중
    -> 응집도 높게!
//안 좋은 코드
class Player {
	Sword s = new Sword();
    Gun g = new Gun();
    
    void attack() {
    	s.slash();
        g.shoot();
   }
}

: Player라는 하나의 클래스에서 무기를 생성하고 관리 -> 검과 총을 모두 들음 -> 여러 책임 혼합되어 응집도가 낮음.
또한 새로운 무기 추가와 교체가 어려움 -> 결합도 높음

//좋은 코드
interface Weapon {
	void use();
}

class Sword implements Weapon {
	public void use() {
		System.out.println("칼로 베었다!");
	}
}

class Gun implements Weapon {
	public void use() {
		System.out.println("총을 발사했다!");
	}
}

class Player {
	Weapon w;
	
	public Player(Weapon w) {
		// TODO Auto-generated constructor stub
		this.w=w;
	}
	
	public void attack() {
		w.use(); //총이든 검이든 상관없이 공격가능
	}
	
}

: 무기를 직접 생성하는 대신 주입받음!
Player는 attack()이라는 책임 하나에 집중 -> 응집도 높아짐
무기 종류가 바뀌더라도 Player 코드는 영향이 없음 -> 결합도 낮아짐

총과 검을 Weapon이라는 인터페이스를 구현시켜 따로 클래스를 만듦으로써 사용자가 무기를 무얼 선택하든 바로 w.use만 쓰면 공격할 수 있게 됨.
이런 코드는 나중에 무기가 추가되었을 때도 따로 함수를 사용할 필요 없이 동일하게 Weapon을 구현시킨 클래스를 만들면 되기 때문에 w.use를 통해 간단하게 공격할 수 있음.

3) SOLID의 필요성

  • 이미 존재하는 레거시 코드를 좋은 코드로 만드는 기술이자 원칙임: 기존 코드를 어떻게 개선할까에 대한 방향 제시
  • 기술적 부채를 막는 실용적 방법: 빠른 개발속도 < 처음부터 차근차근 유지보수와 확장이 쉽게!
  • 변화에 강하고 유연한 코드로 전환하는 힘

4) SOLID의 활용

  • 리펙토링
  • 스프링 프레임워크의 핵심: IoC/DI
    cf. IoC(제어의 역전)과 DI(의존성 주입)은 DIP(의존성 역전 원칙)와 OCP(개방-폐쇄 원칙)를 가장 잘 구현한 대표적 예시
  • 유연한 아키텍처 설계 ex) MSA

SOLID 원칙

1) S: 단일 책임의 원칙

  • 클래스는 단 하나의 책임에만 집중
//before
class ReportService {
	public void generateReport() {//보고서작성}
	public void sendEmail() {//이메일전송}
}
//after
class ReportGenerator{
	public void generateREport(){//보고서작성}
}

class EmailSender {
	public void sendEmail() {//이메일 작성}
}

: 하나의 클래스에 있던 함수들을 두개의 클래스로 분리
-> 유지 보수가 용이해짐

2) O: 개방-폐쇄의 원칙

  • 확장에 개방, 수정에 폐쇄
  • 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있어야 함
  • 추상화에 의존O, 구체적 클래스에 의존X

    : 결제방식을 추가할 때마다 PaymentProcesssor라는 기존의 클래스를 수정할 필요 없이, 결제방식이라는 추상 인터페이스를 구현한 새로운 결제방식 클래스를 만들 수 있다

3) L: 리스코프 치환의 원칙

  • 하위 타입은 언제나 상위 타입으로 대체될 수 있어야 한다
  • 자식 클래스는 부모 클래스가 사용되는 곳에 문제없이 들어갈 수 있어야 함 (IS-A관계)

    : 타조클래스가 새 클래스를 상속했을 때, 타조는 새의 특성인 fly를 하지 못한다는 문제점 발생.(자식 클래스가 부모 클래스에서 사용하지 못하는 부분이 생김!)
    -> 새를 Flyling Bird와 Walking Bird로 분리하여 자식클래스가 부모 클래수의 규약을 모두 지킬 수 있도록(IS-A 관계 명확히 따르도록) 수정.

4) I: 인터페이스 분리의 원칙

  • 자신이 사용하지 않는 메소드에 의존해서는 안됨(책임 위주로 보기)
  • 하나의 거대한 인터페이스 < 여러 개의 구체적인 인터페이스 -> 인터페이스를 기능별로 분리!
  • 인터페이스 변경 시 영향받는 클래스 최소화(인터페이스의 기능 최소화)

    : 로봇은 eat()을 구현할 필요가 없으므로, 원래 하나의 인터페이스로 합쳐져있던 work()와 eat() 메소드를 각각의 인터페이스로 분리

5) D: 의존성 역전의 원칙(DIP)

  • 상위 모듈은 하위모듈에 의존해서는 안됨. 둘 다 추상화에 의존해야함!
    (하위모듈은 변하기 쉽기 때문에, 변하기 어렵고 빈도 낮은 인터페이스나 추상클래스에 의존하라는 의미)

    : 상위 모듈은 추상화계층의 계약만 알면 되고,
    하위모듈은 그 계약을 충족하는 방식으로 구현하기만 하면 됨.
    새로운 구현체(AirConditioner, CoffeeMachine)이 추가되더라도 상위모듈(SmartHomeSwitch)는 수정할 필요가 없음

제어의 역전(IoC)와 의존성 주입(DI)

1) 제어의 역전(IoC)

  • 프레임워크(전문가): 추상화에 의존할 때 구체적인 구현체를 넣어줌
    -> 프레임워크를 통해 제어방법을 역전시키고 모든것을 관리함
  • 구현방법: 의존성(DI)을 주입

2) 의존성 주입(DI)

  • DI: IoC를 구현하는 대표적 기술
  • 클래스 내부에서 new를 통해 의존 객체를 직접 생성하는 것이 아니라, 외부(프레임워크)에서 의존 객체를 '주입'받는 방식
  • 구현방법 - 생성자주입: 의존성 불변, 필수 의존성 명확, 테스트 용이
  • Setter 주입
  • 필드 주입

커피 리팩토링

- CoffeeMachine 인터페이스: I(인터페이스 분리)




: Coffee는 구체적인 기계(예: EspressoMachine)에 직접 의존하지 않고, 오직 "커피(머신)를 추출할 줄 안다 (brew())"라는 규칙을 가진 CoffeeMachine 인터페이스만 바라본다
-> 덕분에 에스프레소 머신이든 드립 머신이든, 필요할 때 쉽게 바꾸어 낄 수 있음!

- Coffee 인터페이스: S(단일책임 원칙), DI(IoC)





: Coffee 메뉴 객체 생성
-> 각각의 메뉴(에스프레소, 아메리카노, 라떼)는 음료 스스로의 레시피만 알고 있음(prepare())
각각의 메뉴는 생성자에서 Machine에게 의존성 주입받음(DI)
-> 커피를 직접 바로 생성하지 않고 머신의 기능(brew)을 통해 머신의 기능을 이용함

- CoffeeMaker: DI, IoC


  • 기존 방식(Bad): CoffeeMaker 내부에서 new Espresso(), new Americano()처럼 재료와 커피를 직접 생성함
    -> 강한 결합도
  • CoffeeMaker를 통한 개선 방식: 외부에서 완성된 Coffee 객체를 setCoffee(coffee)로 주입(DI)받음 -> CoffeeMaker는 그 커피가 아메리카노인지 라떼인지 일일이 알 필요 없이, 단순 주입받은 음료의 makeCoffee() 의 coffee.prepare()만 호출해서 서빙을 위임

cf. 연관, 의존관계 재정리

  • 연관관계: 어떤 객체가 다른 객체를 멤버 변수(필드)로 계속 쥐고 있는 상태. (생명 주기를 어느정도 같이 하거나 지속적인 관계를 맺을 때)
    ex) 음료 객체(에스프레소, 아메리카노, 라떼) -> 커피머신
public class Espresso implements Coffee{
	CoffeeMachine c;
	public Espresso(CoffeeMachine c) {
		// TODO Auto-generated constructor stub
		this.c=c;
	}
  • 의존관계: 멤버 변수로 쥐고 있지는 않지만, 메서드의 매개변수로 잠깐 받아서 쓰거나, 인터페이스의 규칙을 따르기 위해 영향을 받는 일시적/구조적 관계
    ex) 메인 -> 모든 구체 클래스
public class Main {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		EspressoMachine em = new EspressoMachine();
		MilkFrother m = new MilkFrother();
		
		Coffee coffee1 = new Espresso(em);
		System.out.println(coffee1.prepare());
		
		Coffee latte1 = new Latte(coffee1, m);
		System.out.println(latte1.prepare());
		
		CoffeeMaker maker = new CoffeeMaker(); //DI를 이용해 라떼 만들기
		maker.setCoffee(latte1); //
		maker.makeCoffee();
	}

+ Coffeemaker 클래스를 통해 연관, 의존관계를 더 알아보장

// 연관관계일때!
public class CoffeeMaker {
    private Coffee coffee; // 핵심!! 멤버 변수(필드)로 가지고 있음!

    // 1. 연관 관계를 맺어주는 역할 (의존성 주입)
    public void setCoffee(Coffee coffee) {
        this.coffee = coffee; 
    }

    // 2. 이미 맺어진 연관 관계를 사용하는 역할
    public void makeCoffee() {
        this.coffee.prepare(); 
    }
}
// 의존관계일때!
public class CoffeeMaker {
    // 필드(멤버 변수)가 없음!

    // 메서드의 매개변수로 잠깐 받아서 씀 (잠깐 의존함)
    public void makeCoffee(Coffee coffee) {
        coffee.prepare(); 
    }
}

처음에 코드 다이어그램 봤을 때는 Coffee..CoffeeMachine.. 다 모르겠고 그냥 다 합쳐서 if문으로 메뉴 구분하고 짜면 쉬운거 아닌가.. 싶었는데 SOLID원칙을 통해 짠다면 지금은 조금 코드 짤 때 힘들어도 나중에 메뉴가 더 추가되거나 코드를 수정해야할 때 더 편하겠구나 하는 걸 체감했다. 나중엔 더 복잡하고 구체적인 코드를 짜게 될텐데 이렇게 클래스를 많이 짜고 하다보면 다 머릿속에서 꼬일 것 같긴한데 .. 연습하다보면 익숙해지겠지..

0개의 댓글