좋은 객체 지향 설계의 5가지 원칙
SRP
: 단일 책임 원칙(single responsibility principle)
OCP
: 개방-폐쇄 원칙 (Open/closed principle)
LSP
: 리스코프 치환 원칙 (Liskov substitution principle)
ISP
: 인터페이스 분리 원칙 (Interface segregation principle)
DIP
: 의존관계 역전 원칙 (Dependency inversion principle)
👉 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경되므로, 다형성 만으로는 OCP
, DIP
를 지킬 수 없다.
스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원한다. (클라이언트 코드의 변경 없이 기능 확장)
DI
(Dependency Injection): 의존관계, 의존성 주입DI 컨테이너
제공
하지만 인터페이스를 도입하면 추상화라는 비용이 발생한다.
기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법이다.
의존성 주입을 받으면 클래스간의 결합도가 약해진다.
결합도가 약해진 다는 것은 한 클래스가 변경될 경우 다른 클래스가 변경될 필요성이 적어진다는 뜻으로, 아래와 같은 이점이 생긴다.
다음과 같은 구조로 할인 정책을 만들어 보자.
public interface DiscountPolicy {
// @return 할인 대상 금액
int discount(Member member, int price);
}
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;
}
}
}
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;
}
}
}
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);
}
}
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를 참고해보도록 하자.