한창 강의를 통해 비즈니스 요구를 만족하는 프로젝트를 설계해 구현하던 중 객체 지향 원칙을
어기고 있다는 예감이 들었고 절망적이게도 그 예감이 맞았습니다.
열림 교회 닫힘과도 같은 OCP 원칙과 복잡시러운 DIP 원칙...
눈물이 앞을 가리는 저를 위해 해당 원칙들을 적용해 프로젝트를 설계하는 글을 쓰겠습니다.
애플리케이션 상황
우선 클라이언트 인터페이스 OrderService와 그의 구현체 OrderServiceImpl이 있습니다.
할인 정책 인터페이스인 DiscountPolicy와 그의 구현체인
RateDiscountPolicy(일정 비율 할인 정책)가 존재합니다.
이러한 상황에서 OrderServiceImpl 클래스에서는 할인 정책 인스턴스를 생성할 때 다음과 같이 초기화 합니다.
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
하지만 만약 할인 정책이 일정 비율이 아닌 고정 금액을 할인해 주는 기획으로 바뀐다면 DiscountPolicy를
구현하는 새로운 객체 FixDiscountPolicy(고정 금액 할인 정책)를 생성한 후 다음과 같이 초기화 시켜야 합니다.
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
간단히 알 수 있듯이 이렇게 되면 설계의 변경이 로직의 변경을 유도한 상황이 됩니다.
즉 OCP 원칙 위반이라고 할 수 있습니다.
뿐만 아니라 이렇게 되면 인터페이스에만 의존하는 게 아니라 구현체에도 의존한 꼴이 됩니다.
즉 DIP 원칙 위반이라고 할 수 있죠.
하지만 해당 로직은 역할과 구현을 충실하게 분리했습니다.
다형성도 활용하고, 인터페이스와 구현 객체를 분리했습니다.
이 모든 조건을 충족함에도 불구하고 dip, ocp 원칙을 위반하고 있습니다.
이런 때에는 어떻게 해야 할까요?
우선 DIP 원칙에 맞게 구현체가 아닌 인터페이스에 의존하도록 초기화 로직을 바꿔줍니다. 다음과 같습니다.
private DiscountPolicy discountPolicy;
상수는 초기화 때 값이 할당되어야 하므로 final 키워드를 제거하고 인터페이스에만
의존하도록 변경하였습니다.
하지만 이렇게 되면 실제 구현체는 없는데 코드를 실행할 수 있을까요?
실행을 해 보면 NPE(null point exeption)가 발생합니다.
원칙을 위반해서 맞게 로직을 바꿨더니 이번에는 컴파일 오류가 납니다. 앞 뒤로 꽉 막혔습니다.
이런 상황에선 어떻게 해야 할까요?
현재 문제 상황은 클라이언트인 OrderServiceImpl이 DiscountPolicy 구현 객체를
초기화 하기에 생긴 문제입니다.
때문에 이 문제를 해결하기 위해서는 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy의
구현 객체를 대신 생성하고 주입해 주어야 합니다.
원활한 이해를 위해 예시를 들어보겠습니다.
애플리케이션을 하나의 공연으로 보고 배역을 인터페이스, 배우를 구현체라고 치부한다면
현재 상황은 배역을 맡은 배우가 다른 배역의 배우를 캐스팅 하는 상황인 것입니다.
배우에게 너무 많은 책임이 요구되는 나쁜 객체 지향의 예시인 것이죠.
이런 상황에서는 아예 따로 캐스팅을 하는 공역 기획자 역할이 필요합니다.
AppConfig
애플리케이션의 전체 동작 방식을 구성(config) 하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는
별도의 설정 클래스를 만드려고 합니다.
애플리케이션 전반에 대한 운영을 책임지는 클래스인 것이죠.
아래는 클라이언트 인터페이스인 MemberService를 구현하는 MemberServiceImpl이라는 클래스입니다.
회원 정책 인터페이스인 MemberRepository와 이를 구현하는
구현체 MemoryMemberRepository를 해당 클래스에서 사용하기 때문에
다음과 같이 초기화 하고 있습니다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
하지만 앞서 말 했듯이 이러한 초기화는 객체 지향 원칙을 위배하는 초기화입니다.
때문에 초기화 로직을 다음과 같이 변경합니다.
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
생성자를 이용해 인터페이스 MemberRepository 데이터 타입의 매개변수를 받고 초기화 된 해당 매개변수를
MemberRepository와 일치시킵니다.
이렇게 되면 클라이언트 클래스에서의 변경은 끝났습니다.
이러한 변경이 오류 없이 돌아갈 수 있도록 새롭게 객체를 생성하고 연결하는 책임을 가지는
AppConfig 클래스를 만들어 안에 로직을 추가해야 합니다.
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
천천히 해당 코드를 뜯어보겠습니다.
우선 해당 코드는 MemberServiceImpl 타입의 새로운 인스턴스를 반환합니다.
하지만 이 때 매개변수로 new MemoryMemberRepository 전달하며,
이 전에 생성자를 이용해 매개변수와 초기화 된 변수를 동일시키게 되므로
결과적으로 클라이언트에서 생성된 인스턴스는 MemoryMemberRepository의 성질을 갖게 됩니다.
이렇게 되면 MemberServiceImpl 클래스의 로직은 아래와 같습니다.
package hello.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
오직 인터페이스에 관하거나 인터페이스에 의존한 로직만 가지고 있게 될 수 있는 것이죠.
OCP, DIP의 원칙을 지킨 것입니다.
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성합니다.
또한 생성한 객체 인스턴스의 참조를 생성자를 통해 주입합니다.
정리하자면 다음과 같습니다.
<클라이언트 클래스에서 한 일>
생성자의 매개변수와 일치되기 위한 인터페이스의 인스턴스 초기화
private final MemberRepository memberRepository;
해당 인스턴스를 다형성으로 초기화시키기 위한 매개변수를 받는 생성자
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
<Confing에서 한 일>
return 과정에서 전달받은 인터페이스에 구현체를 주입시키는 메서드
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
객체 지향 설계의 원칙에 이어 해당 원칙을 위배했을 때 어떻게 변경해야 하는지에 대해 알아보았습니다.
참고로 Config에서 변경한 코드와 같은 것을 클라이언트 클래스 입장에서 보면
의존관계를 마치 외부에서 주입하는 것 같다고 하여 DI(Dependency Injection),
우리말로는 의존 관계 주입, 의존성 주입이라고 합니다.
클라이언트 클래스 입장에서는 생성자를 통해 어떤 구현체가 주입될지 몰라야 하고
어떤 객체가 주입될 지는 오직 외부, AppConfig만 결정한다고 생각하면 편할 것 같습니다.
그럼 다들
좋프 (좋은 프로그래밍이라는 뜻) 하세요 ~!
객체 지향 설계의 원칙 보러가기 !
좋은 객체 지향 설계가 뭐지? (좋은 객체 지향 설계의 5가지 원칙) (tistory.com)
좋은 객체 지향 설계가 뭐지? (좋은 객체 지향 설계의 5가지 원칙)
해당 사진은 객체 지향적으로 대강 잘만 프로그래밍 하면 되는 줄 알았던 안일한 과거의 저의 모습입니다. 좋은 객체 지향 프로그래밍. 말만 들어도 머리가 어지러운데 원칙까지 있다니... 벌써
kes0917.tistory.com
해당 글은 김영한님의 스프링 핵심 강의를 기반으로 작성 되었습니다.
'Spring' 카테고리의 다른 글
[스프링] 자바 기반 AppConfig를 스프링으로 바꿔보자 (0) | 2023.03.15 |
---|---|
[스프링] DI, IoC, 컨테이너가 뭐지? (DI, IoC, 컨테이너의 간단 정리) (0) | 2023.03.15 |
[스프링] AppConfig을 리팩터링 해 보자 (0) | 2023.03.15 |
[스프링] 좋은 객체 지향 설계가 뭐지? (좋은 객체 지향 설계의 5가지 원칙) (2) | 2023.03.10 |
[스프링] 스프링이 뭐지? (스프링 탄생 배경과 객체 지향, 다형성) (0) | 2023.03.08 |
댓글