[스프링 핵심 원리 - 기본편] SOLID, 의존성 주입 필요성 (DI)

강신현·2022년 8월 15일
0

⭐️ SOLID

좋은 객체 지향 설계의 5가지 원칙

SRP : 단일 책임 원칙(single responsibility principle)

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다.
  • 예) UI 변경, 객체의 생성과 사용을 분리

OCP : 개방-폐쇄 원칙 (Open/closed principle)

  • 다형성 활용
  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다
  • 즉, 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현
  • 구현 객체를 변경하려면 클라이언트 코드를 변경해야 하는 문제를 해결하기 위해
    객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다.

LSP : 리스코프 치환 원칙 (Liskov substitution principle)

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 그러기 위해 단순히 컴파일 성공을 넘어서, 다형성에서 하위 클래스는 특정 인터페이스 규약을 다 지켜야 한다.
  • 예) 자동차 인터페이스의 엑셀은 앞으로 가라는 기능, 뒤로 가게 구현하면 LSP 위반, 느리더라도 앞으로 가야함

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

  • 범용 인터페이스 하나로 사용하는 것보다, 특정 클라이언트를 위한 인터페이스 여러 개로 나누어 사용하는 것이 낫다.
  • 예) 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
    사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 이렇게 인터페이스를 나누면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음

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

  • 구현 클래스에 의존하지 말고, 인터페이스(역할 Role)에 의존하라는 뜻이다.
  • 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다.
    구현체에 의존하게 되면 변경이 아주 어려워진다.

👉 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경되므로, 다형성 만으로는 OCP, DIP를 지킬 수 없다.


💡 DI

스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원한다. (클라이언트 코드의 변경 없이 기능 확장)

  1. DI(Dependency Injection): 의존관계, 의존성 주입
  2. DI 컨테이너 제공

하지만 인터페이스를 도입하면 추상화라는 비용이 발생한다.
기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법이다.

의존성 주입의 의의

의존성 주입을 받으면 클래스간의 결합도가 약해진다.
결합도가 약해진 다는 것은 한 클래스가 변경될 경우 다른 클래스가 변경될 필요성이 적어진다는 뜻으로, 아래와 같은 이점이 생긴다.

  • 리펙토링이 쉬워짐
  • 특정 클래스를 테스트하기 편해짐
  • 인터페이스 기반으로 코드를 설계하여 유연하고, 확장에 용이하다
  • 생명주기 별로 Container를 관리할 수 있게 되어 리소스의 낭비를 막을 수 있다.

예제

다음과 같은 구조로 할인 정책을 만들어 보자.

역할 (interface)

  • DiscountPolicy
public interface DiscountPolicy {
    // @return 할인 대상 금액
    int discount(Member member, int price);
}

구현 객체 (class)

  • FixDiscountPolicy
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;
        }
    }
}
  • RateDiscountPolicy
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;
        }
    }
}

클라이언트

  • OrderServiceImpl
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // DIP, OCP 위반
    // private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    // private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

    private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {

        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

- SOLID 만족 여부 확인

문제 코드

public class OrderServiceImpl implements OrderService{
	// 이전 정책
	// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    
    // 변경 정책
	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

LSP ⭕️
(리스코프 치환 원칙)
다형성에서 하위 클래스는 특정 인터페이스 규약을 다 지켜야 한다.

ISP ⭕️
(인터페이스 분리 원칙)
범용 인터페이스 하나로 사용하지 않고, 특정 클라이언트를 위한 인터페이스 여러 개로 나누어 사용

SRP
(단일 책임 원칙)
한 클래스는 하나의 책임만 가져야 하는데, 객체의 생성과 사용이 분리되어 있지 않다.
👉 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당하고, 클라이언트 객체는 실행하는 책임만 담당하도록 분리해야 한다.

DIP
(의존관계 역전 원칙)
인터페이스(DiscountPolicy) 뿐만 아니라 구현 클래스(FixDiscountPolicy와 RateDiscountPolicy)에도 의존하고 있으므로 DIP가 지켜지지 않는다.
👉 인터페이스에만 의존하도록 의존관계를 변경해야 한다.

OCP
(개방-폐쇄 원칙)
구현 객체를 변경하려면 클라이언트 코드를 변경해야 하는 문제가 있으므로 OCP가 지켜지지 않는다.
👉 따라서 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현해야 한다.


문제 해결 코드

public class OrderServiceImpl implements OrderService{
	private DiscountPolicy discountPolicy;
}

LSP ⭕️
ISP ⭕️
SRP ⭕️ ..? (개념상으로는 만족하지만 실제 코드가 돌아가지 않으므로 AppConfig 관심사 분리 필요)
DIP ⭕️ ..? (개념상으로는 만족하지만 실제 코드가 돌아가지 않으므로 AppConfig 관심사 분리 필요)
OCP ⭕️


SOLID를 만족시키기 위해 인터페이스에만 의존하도록 설계와 코드를 변경했다.

하지만 실제 실행을 해보면 NPE(null pointer exception)가 발생한다. 구현체가 없기 때문이다.
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다. (의존성 주입)
다음 포스트인 관심사의 분리 AppConfig를 참고해보도록 하자.


강의 출처

[인프런 - 김영한] 스프링 핵심 원리 - 기본편

https://kotlinworld.com/64

profile
땅콩의 모험 (server)

0개의 댓글