[Spring] 스프링 핵심 원리-기본편(1) : 객체 지향 원리 적용- #1 AppConfig 감독님과 함께라면 IoC도 두렵지 않아!

Sujung Shin·2023년 2월 7일
0
post-thumbnail

저번 시간까지 다형성 원리를 적용한 순수 자바 코드로 멤버, 주문, 할인 도메인을 설계하고 구현해보았다.

이번 시간에는 코드로 직접 구현체를 변경함으로써, 다형성 원리의 한계를 깨달으며 이를 해결하기 위한 방법에 대해 배울 것이다.

먼저 말하기에 앞서, 좋은 객체 지향 설계가 무엇인지부터 다시금 되새기고 가자.

  • 추상화
    세부 사항은 생략하고 객체의 속성 중 핵심적인 요소만을 모델화하는 것
  • 캡슐화
    특정한 클래스와 관련된 데이터의 일부에만 접근 가능하게 하고, 클래스가 작동하는 방식에 대한 실제 구현 정보를 숨기는 것
  • 상속
    상위 계층의 클래스를 물려받아 다른 특징들을 추가적으로 구현할 수 있도록 하는 것
  • 다형성
    인터페이스를 상속받은 객체가 여러가지 타입을 가질 수 있어, 객체의 유연한 사용이 가능하게 하는 것

이 중에서도 우리는 다형성에 대해서, 역할과 구현을 분리시키는 것이 굉장히 중요하다고 배웠다.(interface/이를 상속받는 class)

우리가 기존에 구현한 회원-주문 할인 정책은 다음과 같다.

💡 할인 정책: 모든 VIP는 1000원을 할인해주는 고정 금액 할인 적용

그런데 만약 기획자가 해당 할인정책이 논의를 통해 변경되었다고 가정해보자.

🤨 (무시무시) 기획자: 고정 금액 1000원 말고, 금액별로 10% 할인해주는 정률(%) 할인정책으로 변경되었어요. 20000원 사면 2000원 할인되고, 30000원 사면 3000원 사고 그런식으로요. 개발자님, 해당 할인 정책을 변경하여 적용해주세요!
🥲 (나)순진 개발자: 네...? 기획자님, 출시가 코앞인데요... 서비스가 지금 당장 오픈 직전이잖아요.
🤨 (무시무시) 기획자: 아니, 에자일 소프트웨어 개발 선언이 있잖습니까... "계획에 따르기보다는 변화에 대응"하셔야죠.
🥲 (나)순진 개발자: 네... oO(그치만 나는 유연한 설계가 가능하도록 애초에 객체지향 설계원칙을 준수했지! 다행이야...!)

기획자의 요구에 따라, 정률 할인 정책으로 변경되었다는 말에 🥲 (나)순진 개발자 는 다음과 같이 행동했다.

기존 DiscountPolicy 역할의 구현을 FixDiscountPolicy에서 RateDiscountPolicy 로 바꾸자!

  • 새로운 할인 정책 적용

- 1. RateDiscountPolicy 추가

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;
        }
    }
}

2. 구현체 OverServiceImpl에서 할인정책을 FixDiscountPolicy에서 RateDiscountPolicy로 변경

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); ==> 주석처리하여 변경
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
		...
}
## discountPolicy를, new() 연산자를 통하여 가져왔다.

이제 바뀐 정책으로 동작 자체는 쉽게 잘된다.
하지만, 여기서 문제점이 발생한다.

이렇게 설계하면 안돼! 😮



위와 같은 방식은 좋은 객체지향 설계(SOLID)에서 OCP와 DIP 원칙을 모두 지키지 못하고 있다.
이렇게 말하자면 다음과 같은 의문이 들 수 있다.

"아니, 나는 역할과 구현도 충실하게 분리하고, 인터페이스와 구현 객체도 분리했는데? 다형성도 잘 지키고, OCP, DIP같은 객체지향 설계 원칙도 철저하게 준수한 거 아니야?"

  • 들어봐, 난 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP도 지켰어.
    • 아니, 클래스 의존 관계를 좀 더 분석해볼래? 인터페이스(추상) 뿐만 아니라 구현 클래스에도 의존하고 있는걸?
  • 뭐? OCP 원칙은 그래도... 지킨 거 아니야? 변경에는 닫혀있고 확장에는 열려있게 설계했으니까...!
    • 아니, 지금 코드를 확장해서 변경하면, 클라이언트 코드 단에서 영향이 가는데? 그럼 OCP(개방-폐쇄 원칙) 위반이야!

기대했던 의존 관계



항상 기대는 좋고, 좋게만 실천될 것 같다. 그렇지만... 막상 현실은 녹록치 않은 것처럼.

실제 의존 관계



실제로는 이렇다. OrderServiceImpl 클라이언트가 DiscountPolicy인터페이스 뿐만 아니라 기존의 FixDiscountPolicy를 의존하고 있기 때문에, 구체화(구현) 말고 추상화(인터페이스)에 집중해라.(추상화 말고 구현체에 의존해라) 라는 DIP원칙에 위반되게 설계한 것이다.

의존관계가 다음과 같기 때문에 만약 RateDiscountPolicyFixDiscountPolicy 를 대체하려면, OrderServiceImpl 클라이언트 코드 단에서 해당 변경을 반영하여야 한다. 따라서
기존의 정책(FixDiscountPolicy)을 주석 혹은 변경하면서 코드를 변경함으로써 확장에는 열려있고 변경에는 닫혀있어야 한다OCP(개방 폐쇄 원칙)을 위반한 것이다.

해결방법


그렇다면 정말 OrderServiceImpl 클라이언트가 DiscountPolicy 인터페이스만 '오로지' 의존하도록 만들어보자.

** ## new() 생성자로 가져오지 말고, 새로운 객체를 선언해버린다.
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;
		...
}

실행해보면 NullPointerException 이란 화려한 에러가 날 감싼다...
인터페이스에만 의존하도록 설계해보았는데, 그러면 '구현체가 없는데?'라는 문제에 또 빠지게 된다... 아 진짜 뭔 인생의 회전목마도 아니고

🤔 관심사를 분리해보자


지금까지 회원-주문 시스템을 설계하고 구현하는 과정에서 꼼꼼히 진행하였지만, 계속해서 객체지향의 핵심원리인 OCP, DIP를 안지키고 있다는 딜레마에 계속 빠지게 된다.

공통적으로 무슨 문제가 반복해서 발견되는가?

  • 코드를 지켜보면 우리는 계속해서 구현체를 new()생성자를 통해 호출하였다.
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

이렇게 되면 new()생성자를 통해 호출한 구현체에 대해 계속 의존한다는 허점이 발견된다. 이게 문제가 되는 이유가 무엇이냐면, 변경이 필요한 경우에 클라이언트 코드 변경이 이루어지기 때문이다. (OCP 위반!!)

  • 생성자를 사용하지 않고 해당 역할에 대해 구현체를 의존하지 않는 코드를 작성해보면?
private DiscountPolicy discountPolicy;

아까 해본 바와 같이 NullPointerException 이란 화려한 에러가 날 감싼다...

대체 어느 상황인가?



❓관심사 분리


조금 더 이론적인 내용으로 들어가보자.

  • 애플리케이션을 하나의 공연이라고 생각해볼까?
    각각의 인터페이스는 하나의 역할이고, 구현체배우(서강준...😍과 이도현...😍)이라고 생각해보자.

  • 그 둘이 공연을 하면 누가 남주 역할을 할 것이고 서브남주 역할을 할 것인지는 서강준/이도현이 하는 것이 아니다. 당연한 얘기겠지만 그런 건 감독이 하는 것이다. 그런데 여기서 이전 코드를 살펴보면, 서강준(남주)가 이도현(서브남주)를 직접 캐스팅하는 것과 같다.
    서강준은 공연도 해야하고, 이도현(서브남주)도 캐스팅하는 다양한 책임을 가지고 있다.



관심사 좀 분리하자!


  • 배우는 본인의 역할/배역을 수행하는 것에만 집중한다.
  • 서강준은 어떤 서브 남주가 캐스팅되더라도, 똑같이 공연을 할 수 있어야 한다.
    • 공연을 구성하고, 담당 배우를 섭외하고, 역할을 분리하여 배우에게 할당하는 감독이 나올 시점‼️

그리하여 스프링은 외부에서 아아주 유명한 감독님을 초빙하게 되는데.... 바로바로!

AppConfig 감독님💞


애플리케이션의 전체 동작을 구성(configuration-)하는 책임을 가지는 별도의 설정 클래스 감독님이 등장하셨다.

😎 AppConfig : 나는 애플리케이션 전반에 대해 운영을 설정하고 구상하는 역할을 하지. 객체를 생성하고 연결하는 건 다 나야. 하하, 잘 부탁해!

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
    }

}

😎 AppConfig : 하하, 강준 군. 괜히 머리 아프게 자네가 역할을 나누지 말고... 내가 하겠네. 남주 역할은 강준 군이, 서브남주 역할은 도현 군이 하시오. 그리고 여주 역할은... 베뉴가 어떤가?

AppConfig라는 설정 클래스는 MemberServiceImpl, OrderServiceImpl등 자체 내에서 구현체를 생성하는 것을 제거해준다.

코드를 통해 알아봅시다


  • 😎 AppConfig는 실제 어플리케이션 동작에 필요한 구현 객체를 생성한다.
    • 😶‍🌫️ MemberServiceImpl
      • 💡MemoryMemberRepository
      • 😶‍🌫️OrderServiceImpl
      • 😍FixDiscountPolicy
  • 😎 AppConfig 는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입/연결한다.
    • 😶‍🌫️ MemberServiceImpl ▶️ 💡MemoryMemberRepository
    • 😶‍🌫️OrderServiceImpl ▶️ 💡MemoryMemberRepository 😍FixDiscountPolicy

* 😶‍🌫️MemberServiceImpl 생성자 주입

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository; 
    // final 필드는 생성자 주입 가능
// MemberReopsitory 구현체를 클라이언트 대신 
// 생성자를 통해 AppcConfig 가 할당해줍니다.
    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);
    }
}

* 😶‍🌫️OrderServiceImpl 생성자 주입

public class OrderServiceImpl implements OrderService{
// final 필드는 생성자 주입 가능
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

// 생성자 파라미터로 받아 this로 처리해줍니다,
// 주입은 AppConfig가 해줍니다.
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discount = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discount);
    }
}



클래스 다이어그램


이렇게 되면

  • 클라이언트인 😶‍🌫️MemberServiceImpl 는 인터페이스(추상화)인 MemberRepository만을 바라보게 되고,
 private final MemberRepository memberRepository; 
    // final 필드는 생성자 주입 가능
// MemberReopsitory 구현체를 클라이언트 대신 
// 생성자를 통해 AppcConfig 가 할당해줍니다.
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
  • 클라이언트인 😶‍🌫️OrderServiceImpl 는 인터페이스(추상화)인 MemberRepository, MemberService만을 바라보게 된다.
// final 필드는 생성자 주입 가능
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

// 생성자 파라미터로 받아 this로 처리해줍니다,
// 주입은 AppConfig가 해줍니다.
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

'구현체가 아닌 추상화에 집중하여라'
이로 인해 DIP 원칙이 충족된다.

이제 클라이언트들은 실행에만! 집중하면 된다.


profile
백문이불여일타

0개의 댓글

관련 채용 정보