SOLID 원칙과 OCP·DIP를 지키는 방법
객체지향 5원칙을 정리하고, 역할과 구현을 나눠도 깨지는 OCP·DIP를 AppConfig로 해결하는 과정을 살펴본다.
시리즈 · Spring1 / 14
- SOLID 원칙과 OCP·DIP를 지키는 방법
- 의존성 주입(DI)과 스프링 컨테이너
- 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
- FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
- @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
- System.out 대신 SLF4J로 로그 남기기
- 세션 직접 만들어 보기와 HttpSession
- 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
- 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
- JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
- JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
- JPA 변경 감지(dirty checking)와 병합(merge)
- 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
- 싱글톤 빈의 동시성 문제와 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 컨테이너라고 한다.
참고
- 김영한, 스프링 핵심 원리 - 기본편 (인프런)