• spring
  • oop
  • java

SOLID 원칙과 OCP·DIP를 지키는 방법

객체지향 5원칙을 정리하고, 역할과 구현을 나눠도 깨지는 OCP·DIP를 AppConfig로 해결하는 과정을 살펴본다.

시리즈 · Spring1 / 14
  1. SOLID 원칙과 OCP·DIP를 지키는 방법
  2. 의존성 주입(DI)과 스프링 컨테이너
  3. 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
  4. FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
  5. @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
  6. System.out 대신 SLF4J로 로그 남기기
  7. 세션 직접 만들어 보기와 HttpSession
  8. 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
  9. 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
  10. JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
  11. JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
  12. JPA 변경 감지(dirty checking)와 병합(merge)
  13. 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
  14. 싱글톤 빈의 동시성 문제와 ThreadLocal

스프링의 탄생 배경은 결국 “객체지향의 장점을 살리자”였다. 좋은 객체지향 설계의 5가지 원칙을 앞 글자를 따 SOLID라고 한다.

SOLID

  • SRP (단일 책임 원칙): 클래스는 하나의 책임만 가지며, 클래스가 제공하는 모든 기능은 그 책임을 수행하는 데 집중되어야 한다.
  • OCP (개방-폐쇄 원칙): 소프트웨어 요소는 확장에는 열려 있고 변경에는 닫혀 있어야 한다.
  • LSP (리스코프 치환 원칙): 프로그램의 정확성을 깨뜨리지 않으면서 객체를 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • ISP (인터페이스 분리 원칙): 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • DIP (의존관계 역전 원칙): 구현 클래스가 아니라 추상화(인터페이스)에 의존해야 한다. 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.

이 중 OCP와 DIP가 가장 크게 와닿아 코드로 정리한다.

역할과 구현을 나눠도 OCP·DIP는 깨진다

public class OrderServiceImpl implements OrderService {
    // private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

DiscountPolicy라는 역할과 구현을 분리했지만, 할인 정책을 바꾸려면 OrderServiceImpl의 코드를 고쳐야 한다. OrderServiceImpl이 인터페이스인 DiscountPolicy뿐 아니라 구현 클래스인 FixDiscountPolicy, RateDiscountPolicy에도 의존하고 있기 때문이다.

  • 구현 클래스에 의존한다 → DIP 위반
  • 기능을 확장하려면 사용하는 쪽 코드를 변경해야 한다 → OCP 위반

이 구조에서는 기능이 바뀔 때마다 그 구현체를 사용하는 수많은 클래스를 함께 수정해야 한다.

해결: 구현체를 외부에서 주입한다

OrderServiceImpl은 인터페이스에만 의존하고, 어떤 구현체를 쓸지는 외부에서 결정해 생성자로 넣어 준다.

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

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

    public DiscountPolicy discountPolicy() {
        // 사용 영역의 코드 수정 없이 기능을 확장, 변경할 수 있다.
        return new RateDiscountPolicy();
    }

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

이제 할인 정책을 바꿀 때 OrderServiceImpl은 건드리지 않고 AppConfig만 수정하면 된다. 즉 사용 영역의 코드는 그대로 두고 구성 영역의 코드만 변경한다.

이렇게 객체를 생성하고 의존관계를 연결해 주는 AppConfig 같은 존재를 IoC 컨테이너 또는 DI 컨테이너라고 한다.

참고

  • 김영한, 스프링 핵심 원리 - 기본편 (인프런)