[Java] SOLID 원칙 5가지

전혜린·2023년 2월 19일
0

1. SOLID 원칙이란?

SOLID

  • 소스 코드를 가독성 있고 확장을 쉽게 하도록 하는 객체 지향 설계 원칙

  • SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), ISP(인터페이스 분리 원칙), DIP(의존관계 역적 원칙)가 있음



2. SOLID 원칙 5가지

2.1. SRP: Single responsibility principle (단일 책임 원칙)

"어떠한 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다." - 로버트 C. 마틴

  • 하나의 클래스는 하나의 책임만 가져야함
  • 변경이 있을 경우 파급 효과가 적으면 단일 책임 원칙을 잘 따르는 것


2.1.1. SRP 적용 전 예제
// MemberOrder.java
public class MemberOrder {

	// 회원 정보
    private Long id;
    private String name;
    private Grade grade;
    
    // 주문 정보
    private String itemName;
    private int itemPrice;
    private int discountPrice;
    
    public MemberOrder(Long id, String name, Grade grade, String, itemName, int itemPrice, int discountPrice) {
    	this.id = id;
        this.name = name;
        this.grade = grade;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }
    
    public int calculatePrice() {
        return itemPrice - discountPrice;
    }
    
    // (회원 정보, 주문 정보 필드의 getter/setter 메서드 생략)
    
}
  • MemberOrder 클래스는 회원 정보와 회원 별 주문 정보에 대한 것
  • 회원의 정보 필드를 추가해야 할 경우, 주문 정보 필드를 추가해야 할 경우 수정이 필요
  • 해당 클래스를 변경해야 하는 이유는 두가지이므로 SRP를 위반 ! ! !
  • 회원의 정보와, 회원별 주문 정보를 분리하는 것이 필요한 것으로 보임



2.1.2. SRP 적용 후 예제
// Member.java
public class Member {
	
    private Long id;
    private String name;
    private Grade grade;
    
    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }
    
    // (getter/setter 메서드 생략)
}


// Order.java
public class Order {

    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    public int calculatePrice() {
        return itemPrice - discountPrice;
    }
    
    // (getter/setter 메서드 생략)
}
  • 회원을 정보 필드를 추가 또는 변경해야 할 경우 Member 클래스를 수정
  • 주문 정보 필드를 추가 또는 변경해야 할 경우 Order 클래스를 수정
  • 각 클래스를 변경해야 하는 이유는 한가지씩뿐, SRP 적용 성공 ! ! !


2.2. OCP: Open/Closed principle (개방-폐쇄 원칙)

  • 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 함
  • 다형성을 잘 활용하면 가능 -> 역할(인터페이스)과 구현(구현 클래스)을 분리


2.2.1. OCP 적용 전 예제
public class DiscountPolicy {
	private int discountFixAmount = 1000;	// 정액 할인 정책
    
    public int discount(Member membr, int price) {	// 정액 할인 정책에 따른 할인 금액 계산
    	if(member.getGade() == Grade.VIP) {
        	return discountFixAmount;
        } else {
        	return 0;
        }
    }
}
  • DiscountPolicy 클래스는 할인 정책에 따른 할인 금액을 계산하는 역할
  • 새로운 할인 정책이 추가되면 필드와 메서드를 추가해야 하는 등 기존 코드를 수정해야 하므로 OCP를 위반 ! ! !


2.2.2. OCP 적용 후 예제
// DiscountPolicy.java
public interface DiscountPolicy {
	
    int discount(Member member, int price);
}

// FixDiscountPolicy.java 
public class FixDiscountPolicy implements DiscountPolicy {

	private int discountFixAmount = 1000;
    
    @Override
    public int discount(Member member, int price) {
    	if(member.getGrade() == Grade.VIP) {
        	return discountFixAmount;
        } else {
        	return 0;
        }
    }
}

// RateDiscountPolicy.java
public class RateDiscountPolicy implements DiscountPolicy {
	
    private int discountPercent = 10;
    
    @Override
    public int discount(Member member, int price) {
    	if(member.getGrade == Grade.VIP) {
        	return price * discountPercent / 100;
        } else {
        	return 0;
        }
    }
}
  • DiscountPolicy 인터페이스에 할인 정책에 따른 할인 금액을 계산하는 메서드(역할)를 선언
  • 만약 할인 정책이 추가되면 새로운 클래스를 생성해서 DiscountPolicy 인터페이스를 구현하면 됨
    => 변경에는 닫혀있고 추가에는 열려있게 되어 OCP 적용 성공 ! ! !



2.3. LSP: Liskov subsititution principle (리스코프 치환 원칙)

  • 하위 타입의 인스턴스가 바뀌어도 프로그램의 정확성을 깨뜨리면 안됨



2.4. ISP: Interface segregation principle (인터페이스 분리 원칙)

  • 인터페이스가 명확해야 함
  • 특정 클라이언트를 위한 개별 인터페이스를 만드는 것이 좋음
    ex1) 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스
    ex2) 사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트
    => 정비사 인터페이스가 변경되어도 운전자 클라이언트에 영향을 주지 않음


2.4.1. ISP 적용 전 예제
// CarRole.java
public interface CarRole {
	
    void drive();
    void repair();
}

// CarDriver.java
public class CarDriver implements CarRole {
	
    private String name;
    
    public CarDriver(String name) {
    	this.name = name;
    }
    
    @Override
    public void drive() {
    	System.out.println("자동차 운전자가 운전을 합니다.");
    }
    
    @Override
    public void repair() {
    	System.out.println("운전자의 역할이 아닙니다.");
    }
    
    // (getter/setter 메서드는 생략)
}

// Mechanic.java
public class Mechanic implements CarRole {
	
    private String name;
    
    public Mechanic(String name) {
    	this.name = name;
    }
    
    @Override
    public void drive() {
    	System.out.println("정비사의 역할이 아닙니다.");
    }
    
    @Override
    public void repair() {
    	System.out.println("정비사가 자동차를 정비합니다.");
    }
    
    // (getter/setter 메서드는 생략)
}
  • CarRole 인터페이스에 자동차의 역할을 정의함
  • CarDriver 클래스는 자동차 운전자, Mechanic 클래스는 정비사
  • CarDriver 클래스는 driver() 메서드, Mechanic 클래스는 repair() 메서드를 구현하기 위해 CarRole 인터페이스를 구현해야 함
  • 이때, CarDriver, Mechanic 클래스에서 불필요한 메서드도 오버라이딩해야 하는 문제가 발생
    => 인터페이스에 변경이 발생되면 CarDriver, Mechanic 클래스 모두 수정이 필요하므로 ISP를 위반 ! ! !


2.4.2. ISP 적용 후 예제
// Drive.java
public interface Drive {
	
    void drive();
}

// Repair.java
public interface Repair {

    void repair();
}

// CarDriver.java
public class CarDriver implements Drive {

    private String name;

    public CarDriver(String name) {
        this.name = name;
    }

    @Override
    public void drive() {
        System.out.println("자동차 운전자가 운전을 합니다.");
    }
    
    // (getter/setter 메서드는 생략)
}

// Mechanic.java
public class Mechanic implements Repair {

    private String name;

    public Mechanic(String name) {
        this.name = name;
    }
    
    @Override
    public void repair() {
        System.out.println("정비사가 자동차를 정비합니다.");
    }
    
    // (getter/setter 메서드는 생략)
}
  • 인터페이스를 구현 클래스에 맞게 분리하므로 인터페이스가 더 명확해짐
  • CarRole 인터페이스를 Drive, Repair 인터페이스로 분리
    • CarDriver 클래스는 Drive 인터페이스를 구현
    • Mechanic 클래스는 Repair 인터페이스를 구현
  • 인터페이스가 명확해져 각 클래스에서 불필요한 메서드를 오버라이딩 할 필요가 없어져 ISP 적용 성공 ! ! !



2.5. DIP: Dependency inversion principle (의존관계 역전 원칙)

  • 구체화(구현 클래스) 안되고 추상화(인터페이스)에만 의존해야 함


2.5.1. DIP 적용 전 예제
// DiscountPolicy.java
public interface DiscountPolicy {

    int discount(Member member, int price);
}

// FixDiscountPolicy.java
public class FixDiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000;   // 1000원 할인
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}

// OrderServiceImpl.java
public class OrderServiceImpl implements OrderService {
	
    // OrderServiceImpl 클래스(클라이언트 코드)가 DiscountPolicy 인터페이스(추상화)도 의존하면서 FixDiscountPolicy 클래스(구체화)도 의존함
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}
  • 'OrderServiceImpl' 클래스(클라이언트 코드)에서 DiscountPolicy 인터페이스(추상화)와 FixDiscountPolicy 클래스(구체화)를 의존함
  • 클라이언트 코드에서 구체화도 의존하여 클라이언트의 코드를 수정해야 하는 상황이 발생되어 DIP를 위반 !!!
  • OrderServiceImpl 클래스에서 추상화만 의존하도록 수정해야 함



2.5.2. DIP 적용 후 예제
// DiscountPolicy.java
public interface DiscountPolicy {

    int discount(Member member, int price);
}

// FixDiscountPolicy.java
public class FixDiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000;   // 1000원 할인
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}

// OrderServiceImpl.java
public class OrderServiceImpl implements OrderService {
	
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

// AppConfig.java
public class AppConfig {

	public OrderService orderService() {
        return new OrderServiceImpl(discountPolicy());
    }
    
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
  • OrderServiceImpl 클래스에서 DiscountPolicy 인터페이스만 의존
  • FixDiscountPolicy 클래스는 외부에서 인스턴스를 생성하여 OrderServiceImple 클래스에 주입 시켜줘야함 -> DI(의존관계 주입) 이라고 부름
  • AppConfig 클래스에서 DI를 수행하도록 하여 DIP 적용 성공 ! ! !
profile
개발새발 혤린이

2개의 댓글

comment-user-thumbnail
2023년 2월 22일

잘 읽어보안네요 덕분에 SOLID에 대해 알게되었읍니다. 정리는 좀 더 하실 필요 있으시네요. 오늘도 무탈하셔요

1개의 답글