[CS] 자바 객체지향 SOLID, 다형성, DI, IoC
객체지향과 자바
이번글에서는 객체지향 5원칙 SOLID 에서 OCP 와 DIP 를 중점으로 작성할 것이다. 그리고 OCP 와 DIP 를 지키기위해 사용하는 DI, IoC 에 대해 자바 예제 코드와 함께 설명할 것이다.
SOLID
원칙 | 설명 |
---|---|
SRP (Single Responsibility Principle) 단일 책임 원칙 | 한 클래스는 하나의 책임만 가져야 한다. 하지만 하나의 책임이라는게 클 수 도 있고, 작을 수 도 있고, 문맥과 상황에 따라 다르게 해석된다. SRP 를 잘 지킨다는 것은 변경이 있을때, 변경에 따른 파급효과가 적을 수록 원칙을 잘 지킨것이다. |
OCP (Open Closed principle) 개방-폐쇄 원칙 | 소프트웨어 요소는 확장에 열려있으나 변경에 닫혀있다. 이 원칙을 지키기위해 IoC, DI 를 사용한다. |
LSP (Liskov Substitution Principle) 리스코프 치환 원칙 | 상위타입의 객체를 하위 타입의 객체로 치환해도 상위타입을 사용하는 프로그램은 정상적으로 동작해야 한다. 즉 자식객체를 참조하는 부모객체는, 부모객체가 가진 역할도 정상적으로 수행이 가능해야 한다. 대표적인 예제로 직사각형/정사각형이 있다. |
ISP (Interface Segregation Principle) 인터페이스 분리 원칙 | 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다. 특정 클라이언트 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. 너무 범용적인 인터페이스를 만들면 인터페이스 변경이 많이 일어날 수 있다. 적당한 크기의 인터페이스를 만들어야 한다. |
DIP (Dependency Inversion Principle) 의존관계 역전 원칙 | 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 클라이언트가 구현을 알면안되고, 역할만 알아야 한다. 이 원칙을 지키기 위해 IoC 와 DI 를 사용한다. |
다형성
자바에서는 역할(interface) 과 구현(인터페이스 구현 클래스, 구현 객체), 역할을 사용하는 클라이언트로 나뉜다. 구현부분이 변경되어도 클라이언트와 역할은 변경하지 않고 그대로 수행할 수 있어야 한다. 즉, 클라이언트는 내부구조를 몰라도, 내부구조가 바뀌어도, 클라이언트는 똑같이 역할을 수행할 수 있다. 구현되는 부분은 클라이언트에게 영향을 주지 않고 무한히 확장이 가능하다! 구현 되는 부분은 유연하고 변경에 용이해야 한다.
좋은 다형성을 가질려면
- 클라이언트는 역할만 알고있으면 된다.
- 클라이언트는 구현 대상의 내부 구조를 모른다.
- 클라이언트는 구현 대상의 내부 구조가 변경되도 영향이 없다.
- 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.
자바에서는 인터페이스를 활용해 다형성을 가질려고 한다. 다음과 같이 설계하면 역할에 대해서 무한한 확장성을 가진다.
OrderServiceImpl
: 클라이언트DiscountPolicy
: 역할(interface)FixDiscountPolicy
RateDiscountPolicy
: 역할 구현 객체
할인정책 DiscountPolicy
에 대해 여러 종류 할인 정책을 구현이 가능하다. 평범하게 코드를 작성해보면 다음과 같다.
DiscountPolicy
1
2
3
4
5
6
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
FixDiscountPolicy
1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
4
5
6
7
8
9
10
11
12
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10; //10% 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
OrderServiceImpl
1
2
3
4
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
이렇게 클래스다이어그램으로 주어진대로 구현을 했더니 문제가 발생했다. 할인정책을 FixDiscountPolicy
에서 RateDiscountPolicy
로 변경을 할려고했더니 클라이언트 OrderServiceImpl
에서 코드 수정이 일어났다. OCP 의 변경하지 않고 확장할 수 있다는 원칙을 위배했다!
그리고 클라이언트 OrderServiceImpl
는 DiscountPolicy
만 의존하는게 아닌 역할 구현 객체FixDiscountPolicy
RateDiscountPolicy
에도 의존하고 있다. 추상화뿐만아니라 구현체에도 의존을 하고있다! DIP 의 추상화에만 의존해야 한다는 원칙을 위배했다!
실제 의존관계는 다음 그림과 같다.
OrderServiceImpl
은 현재 인터페이스 DiscountPolicy
와 구현객체 FixDiscountPolicy
둘 다 의존하고 있다.
DIP 와 OCP 를 지키고 싶다면 IoC 와 DI 를 이용해야 한다.
IoC 와 DI
위에서 말한 원칙위배 문제를 해결하기 위해 먼저 OrderServiceImpl
를 수정해주자.
1
2
3
4
5
6
7
8
9
10
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;
}
}
OrderServiceImpl
는 이제 오직 생성자를 통해서만 구현객체를 초기화해줄 것이다. 객체 스스로 어떤 구현객체를 결정할 수 없으며, 외부에서 만들어서 주어야할 것이다.
AppConfig
1
2
3
4
5
6
7
8
public class AppConfig {
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
AppConfig
가 이제 구현객체를 생성하고, 생성한 객체 인스터스 레퍼런스(참조)를 OrderServiceImpl
에게 생성자를 이용해 주입시켜준다. 이를 생성자 주입이라고 부른다. 이렇게 하면 결과적으로 클라이언트 코드는 정책에 변경이 일어나도 일절 코드를 수정하지않는다. AppConfig
에서 코드를 변경해주면 되기 때문이다. 결과적으로 DIP 와 OCP 원칙을 잘 지킬 수 있게 되었다.
IoC
제어의역전(IoC) : 클라이언트 구현객체OrderServiceImpl
가 스스로 흐름을 제어하는게 아닌, 클라이언트는 오직 본인 로직만 실행하며, 클라이언트의 제어의흐름은 외부AppConfig
에서 가져간다.
프레임워크와 라이브러리의 차이
- 프레임워크에서 실행권(제어흐름)은 프레임워크가 가진다(제어의역전)
- 라이브러리는 프로그램내에서 실행권(제어흐름)이 없다. 그저 프로그램의 코드의 일부를 사용될 뿐이다.
DI
의존관계주입(DI, Dependency Injection) : 인터페이스를 가진객체가 어떤 구현객체를 받는지 모르는 객체(동적인 객체 인스턴스), 실행시점(런타임)에 외부에서 구현객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는것을 DI 라고한다.
IoC 컨테이너, DI 컨테이너
객체를 생성하고 관리하면서 의존관계를 연결해 주는 어떤것(여기서는 AppConfig
), 주로 DI 컨테이너라고 부른다. 스프링을 이용하면 이런 AppConfig
같은것을 알아서 만들어준다.