스프링 입문-SOLID

서론

예비군 갔다와서 쓰는 첫 포스팅이다. 실습 부분은 이전 포스팅과 겹치는 부분이 많으니 핵심적인 내용만 요약하여 정리하도록 하겠다.

SOLID

좋은 객체지향 프로그래밍을 위해서 지켜야하는 5가지 원칙을 SOLID 원칙이라고 한다. 각각은 다음과 같다.

  • SRP(Single Responsibility Principle) : 단일 책임 원칙. 각 클래스는 하나의 책임을 가져야 한다는 원칙이다. 그래야 변경이 있을 때 파급 효과가 적기 때문이다.
  • OCP(Open-Closed Principle) : 개방-폐쇄 원칙. 프로그램이 확장에는 열려있으나 수정에는 닫혀있어야 한다는 뜻이다. 즉 확장은 용이하게, 수정은 최소한으로 가능하게 한다는 뜻이다.
  • LSP(Liskov Substitution Principle) : 객체는 프로그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 변경이 가능해야 한다. 이렇게 설명하면 잘 모르겠지만 구현체는 인터페이스의 의도를 지켜서 설계되어야 한다는 뜻이다. 인터페이스의 save() 메서드가 실제 구현에서는 저장과 상관없는 구현이 되어선 안된다는 것이다.
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙. 범용 인터페이스 하나보다 각 역할을 수행하는 인터페이스 여러 개가 낫다는 뜻이다. 그래야만 역할이 명확해지고, 수정의 파급력이 낮아지기 때문이다.
  • DIP(Dependency Inversion Principle) : 의존관계 역전 원칙. 프로그래머는 추상화에 의존해야 하고 구체화에 의존해서는 안된다. 즉 인터페이스에 의존해야 하고 실제 구현된 구현체에 의존해서는 안된다는 뜻이다. 이 또한 수정의 파급력을 낮추기 위함이다.

내용을 살펴보면 코드의 재사용을 용이하게, 수정이 있을 때 수정의 파급력을 최소화하기 위함임을 알 수 있다.

이러한 SOLID 원칙을 준수한 프로그래밍을 강의에서 실습을 하며 알아보았다.

IOC(제어의 역전)

다음 코드는 실습을 하며 만든 OrderService의 구현체이다.

  
package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository=new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy=new FixDiscountPolicy();

    @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);
    }
}

위 코드의 문제점을 살펴보자. 위 코드는 일견 인터페이스에 의존하며 DIP 원칙을 준수하는 듯 보인다. 하지만 실제로는 해당 인터페이스의 구현체에도 의존하고 있음을 볼 수 있다.
따라서 DIP 원칙을 위반하고 있고, 만약 구현체의 수정이 있다면 위의 클라이언트 코드에도 수정이 있어야 하므로 OCP 원칙도 위반하고 있다고 볼 수 있다. 또한 구현체인 클라이언트 코드에서 인터페이스와 구현체를 연결해주는 역할도 하고 있으므로 SRP도 위반했다고 볼 수 있다.
그렇다면 이러한 코드를 어떻게 수정해야 SOLID를 준수한 코드로 만들 수 있을까?

  
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.*;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    private static DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

위 코드는 외부에서 의존관계의 주입을 설정하기 위해 만든 AppConfig 코드이다.

  
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

AppConfig를 사용하고 위의 구현체 코드를 다음과 같이 수정하였다. 이러한 수정에는 다음과 같은 장점들이 있는데, 첫째로 구현체의 역할과 인터페이스와 구현체를 연결해주는 역할을 분리하여 SRP를 준수하였다.
둘째로는 이제 구현체는 더 이상 인터페이스의 구현체가 무엇인지 알지 못한다. 외부에서 AppConfig가 의존관계를 주입하여 주므로 위의 구현체는 온전히 추상화에 의존한다고 볼 수 있다. 따라서 DIP를 준수한다.
마지막으로는 이제 수정이 있을 때 구현체를 수정하는 것이 아닌 AppConfig만 수정해주면 인터페이스의 구현체를 손쉽게 갈아 끼울 수 있다. 따라서 OCP를 준수한다.

이제 프로그램의 모든 제어 흐름은 AppConfig가 가지고 있다. 메인 메서드에서는 AppConfig의 메서드들을 가져다 쓰고, 구현체의 생성도 AppConfig가 하며 해당 구현체를 주입해주는 역할도 AppConfig가 한다. 나머지 클래스들은 자신의 로직을 묵묵히 실행할 뿐이다.
이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아닌 외부에서 따로 관리해주는 것을 IoC(Inversion of Control), 제어의 역전이라고 한다.

DI(Dependency Injection)

위의 구현체 코드에서 프로그램을 실행하기 전까지는 선언된 인터페이스 참조변수와 구현체간에는 아무런 관계가 없다. 프로그램이 실행되면 런타임에서 AppConfig가 구현체의 객체를 생성하고 생성자를 통해 참조값을 전달하여 인터페이스의 참조변수와 연결해준다.
이렇게 런타임에 생성 및 전달하여 클라이언트와 서버의 연결관계가 형성되는 것을 DI(Dependency Injection), 의존관계 주입이라고 한다. DI를 사용하면 클라이언트 코드를 변경하지 않으면서 클라이언트와 실제로 연결되는 구현체를 쉽게 변경할 수 있고, 이미 코드에서 정의된 정적인 의존관계를 변경하지 않으면서 동적인 인스턴스 의존관계를 변경할 수 있다.
AppConfig와 같이 객체를 생성, 관리하고 의존관계를 설정해주는 것을 DI 컨테이너라고 한다.