스프링 핵심 원리 이해2 - 객체 지향 원리 적용

고동현·2024년 3월 17일
0

Spring 기본

목록 보기
3/10

새로운 할인정책 개발

이전에는 vip면 1000원 할인으로 고정된 가격을 할인 해주었다.
그러나 이제부터는 Rate 비율로 VIP면 10%를 할인 시켜 줄 것이다.

  • RateDiscountPolicy 추가

  • RateDiscountPolicy

    10퍼센트 할인을 위해서 grade가 VIP면 price * discountRate/100으로 할인된 가격을 return해주었다.

할인 검증 Test


VIP 멤버를 만들고, 가격을 10000을 넣어주면, 할인된가격은 10%이므로 1000원이 될것이다.
참고로, displayName을하면 test할때 콘솔에 "~"내용을 출력할 수 있다.

새로운 할인정책 적용과 문제점

★★★★★★★★지금부터가 진짜 중요!!!!
앞에껏들은 사실 너무 쉬운내용이고 복잡할게 없지만, 지금부터가 진짜 중요한 내용들이다.
앞에서 만든 정률할인 정책을 적용한다고 하자. 그러면

public class OrderServiceImpl implements OrderService{
   //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
   private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

이렇게만 해도 참 잘만든 코드인것 같다. 그냥 구현체만 바꿔서 끼워주면 되니까 말이다.
그러나,
OCP,DIP같은 객체지향 설계 원칙을 준수하지 않았다.
DIP:주문서비스 클라이언트 OrderServiceImple이 DiscountPolicy인터페이스에 의존하면서 DIP를 지킨거 같은데?=>그러나 지금 DiscountPolicy라는 인터페이스에 의존을하면서도, FixDiscountPolicy,RateDiscountPolicy같은 구체 클래스에도 의존을하고 있다.
고로 아니 그러면 변경을 해야하는데 어떻게 코드를 바꾸지 않으라는거야? 생각 할 수 있다. 가능한가?
그림을 봐보자

지금 OrderServiceImple이 DiscountPolicy 인터페이스에만 의존하는것 같지만, 사실은 FixDiscountPolicy에도 의존하고있다.
고로, FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간, OderServiceImple의 소스코드도 변경해야한다.=>OCP위반

그러면 도대체 어떻게해야, 인터페이스에만 의존하는데? 방법이 뭐 간단하다 저 new뒤에 부분을 없애면된다. 어떻게? 이렇게

public class OrderServiceImpl implements OrderService{
   //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
   private final DiscountPolicy discountPolicy;

아니 근데 이게 discountPolicy가 구현체가 없는데 이거 구동시키면 당연히 NUllPointException이 터질것이다.

해결방안: 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현객체를 대신 생성하고 주입해줘야한다.

관심사의 분리

이제부터 그러면 누군가 구현객체를 대신 생성하고 주입해주는 역할이 누군지 배워보겠다.
AppConfig

AppConfig는 애플리케이션의 실제 동작에 필요한 구현객체를 생성한다.
자 이전에는 MemberService memberservice = new MemberServiceImpl();
그리고 이 MemberServiceImpl 안에서 MemberRepository memberrepository = new MemoryMemberRepository();이런식으로 구현하였다.

그러나 이제 MemberService 생성자 안에서, MemberServiceImple을 만들고 그다음 여기서 필요한 MemoryMemberRepository까지 생성자를 통해서 연결 시켜준다.

OrderService도 마찬가지다. 이 인터페이스의 구현체 OrderServiceImpl에서 필요한 MemoryMemberRepository,FixDiscountPolicy를 객체로 생성하고 이 객체의 참조를 생성자를 통해서 주입(연결)시켜주는것이다.

MemberServiceImple-생성자 주입

4 related problems 무시->아직 test코드 안바꿨음
여기서 이제 MemberServiceImpl에서 필요한 MemberRepository는 생성자를 통해서 주입받는다.
아까위에서 return new MemberServiceImple에서 인자로 MemoryMemberRepository를 넣어줬는데 그러면,
MemberServiceImpl 생성자의 파라미터에 들어가게된다.
참고로, MemberRepository는 인터페이스이고, memorymemberrepository가 구현체인것이다.

핵심은, MemberServiceImpl의 입장에서는 생성자를 통해 어떤 구현객체가 들어올지 알수가 없다는 사실히다.
오로지, 생성자를 통해서 어떤 구현 객체를 주입할것인지는 외부 AppConfig에 의해 결정된다.

  • 회원 객체 인스턴스 다이어그램

    appconfig에서 memoryMemberRepository를 생성하고 그다음에 memberServiceimpl 객체를 생성과 동시에, 이 memoryMemberRepository를 주입까지 해준다.

OderServiceImple

여기서도 생성자 주입방식으로, memberRepoistory와 discountPolicy에는 어떤 구현체가 들어올지 아무도 모른다. 그냥 앞에서 했던것처럼 Appconfig에서 구현체인 OrderServiceImpl을 생성과 동시에 memoryMemberRepository와 FixDiscountPolicy라는 구현체를 만들어서 주입까지해준다.

테스트 코드 오류수정

Appconfig를 생성해주고,
MemberService에 appConfig.memberService를 호출해서 넣어준다.
그러면 멤버서비스의 구현체와, 그 구현체에 필요한, memoryMember가 DI되서 생성되게 된다.

여기서도 마찬가지다 인터페이스 MemberService와 OrderService를 만들고 여기에 필요한 구현체들은 Appconfig가 생성해서 주입까지 해주었다.

여기서 혹시 이전글들을 보지 않아 BeforeEach가 뭔데 싶으신분들을 위해 간략하게 설명드리자면, BeforeEach는 각 test전에 무조건 수행되게 하는것으로,
만약에 하나의 memberService,orderService를 만들고 각 테스트마다 공유해서 사용하면, 1번테스트에서 id=1 name=동현 생성하고 2번테스트에서도 동일하게 생성한다면 오류가 발생하므로, 이를 방지하기 위함입니다. 각 테스트마다 새로 객체를 만든다고 생각하시면 됩니당

AppConfig 리팩터링

리팩터링(refactoring)은 소프트웨어 공학에서 '결과의 변경 없이 코드의 구조를 재조정함'을 뜻한다. 주로 가독성을 높이고 유지보수를 편하게 한다. 버그를 없애거나 새로운 기능을 추가하는 행위는 아니다. 사용자가 보는 외부 화면은 그대로 두면서 내부 논리나 구조를 바꾸고 개선하는 유지보수 행위이다.

Appconfig가 잘 만들긴 했는데 모든 역할들이 보이지 않는다.
한번 봅시다
처음부터 만든 것과 다르게, 한번도 이 과정을 거치지 않은사람들이 봤을때,

아 역할이 memberService와 orderService 두개가 있구나, 라고 생각할 수 있습니다.
그러나 우리는 역할이 주문 서비스 역할, 회원 저장소 역할, 할인정책 역할, 멤버 서비스 역할등 4개가 있었습니다.
어? 그럼 나머지, memberRepository와, discountPolicy가 어디 갔는거죠?
이 두개의 코드느 MemberServiceImple과 OrderServiceImpl에 생성자 주입으로 적혀져 있기 때문에 보이지 않는것입니다.
그래서 모든 역할들을 보여주기위해서, 아래와같이 코드를 변경했습니다.

일단 여기서 @Bean은 생략해서 봐주세요. 저 Bean없어도 현재 단계에서는 잘 돌아갑니다.

memberService,orderService,memberRepository,discountPolicy의 역할을 모두 드러냈습니다.

또한 여기에 필요한 객체들은 함수를 호출해서 인자로 넘겨주었습니다.

그럼 여기서 더 나아가서, 아 역할들을 모두다 드러내주어서 코드의 가독성은 높인거 같아.
근데 굳이 이렇게 까지 바꿀 필요가 있을까? 더 한번 생각해보자.

지금은 이렇게 역할이 4개밖에 또한, 이 역할에 필요한 구현체들이 얼마 없었다.

근데 리팩터링전의 코드에서 만약 memoryMemberRepository가 아니라 다른 DB리포지토리로 바꾼다고 생각해보자.
그러면 여기있는 코드를 전부 다 뒤져서 memoryMemberRepository에서=>DB리포지토리로 전부 다 바꿔줘야한다.

그러나 리팩터링 후에는, memberRepository생성자에 있는 new MemoryMemberRepository 한군데만 바꿔주면 되는 신기한 일이 생기는것이다.

이렇게되면 유지보수가 매우 편해져서 좋다.

새로운 할인정책 적용

더이상 할 말이 없다... 객체지향과 DI의 끝판왕!!

그저 Appconfig에서 discountPolicy만 고치면 된다.

클라이언트 코드인 OrderServiceImpl을 포함해서 어떠한 사용영역 코드도 변경할 필요가 없다.

사용영역이라 하면 OrderService, OrderServiceImpl, 아무데도 바꾸지 않아도 된다.

pulbic DiscountPolicy discountPolicy(){
      return new RateDiscountPolicy();
}

좋은 객체지향 설계의 5가지 원칙 적용

이전글에서 solid에 대해서 설명을 드렸는데,
여기서 3가지 SRP,DIP,OCP가 어떻게 적용되었는지 설명드리도록 하겠습니다.

  • SRP 단일책임 원칙--한클래스는 하나의 책임만 가져야한다.
    이전에는 클라이언트 객체가 직접 구현객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있었다.
    예를 들어, MemberServiceImpl이 memoryMemberRepository구현 객체를 생성해서 연결까지 해줬다.
    그런데 이제 구현객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.
    클라이언트 객체는 이제 실행만 담당하면 된다.

  • DIP 의존관계 역전 원칙 -- 추상화에 의존해야지 구체화에 의존해서는 안된다.
    DiscountPolicy 추상화 인터페이스에 의존하는것 처럼 보이지만 FixDiscountPolicy라는 구현체에도 의존했었다.
    고로 DiscountPolicy에만 의존하게 변경하였고, 필요한 구현체는 AppConfig가 생성후 주입해줬다.

  • OCP 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야한다.
    애플리케이션을 사용영역 service와 구성영역 Appconfig로 나눔.
    그러면 Appconfig의 의존관계를 fix에서 Rate로 변경해서 주입한다고 한들,
    클라이언트의 코드는 변경하지 않아도됨.
    소프트웨어 요소를 새롭게 확장해도, 사용영역인 Service에서는 그 어떤 코드도 수정하지 않아도됨.

다음시간에는 IoC,DI,컨테이너들의 spring에서 사용하는 이름들과 실재로 우리가 만든 코드에 어떻게 적용될 수 있을 지 알아 보겠습니다.

IoC,DI,컨테이너

IoC(제어의 역전)
이전에 우리가 구현한 class를 보면 memberRepository라던지 직접 new를 해서 필요한 구현객체를 만들고, 연결해서, 실행하였다.
반면에, Appconfig를 만들고 난후에는, 메소드나 객체의 호출 작업을 개발자가 결정하지 않고, 외부에서 결정된다.

순서를 따져보면,
기존에는

  1. 객체 생성(new)
  2. 의존성 객체 생성=>클래스 내부에서 생성
  3. 의존성 객체 메소드 호출

이제는

  1. 객체 생성(외부 AppConfig)
  2. 의존성 객체 주입 =>스스로 class내부에서 만드는게 아니라 제어권을 스프링에 위임하여 스프링이 만든 객체를 주입한다.
  3. 의존성 객체 메소드 호출

조금 더 자세히 설명을 해보자면, 일반적인 프로그램에서는 객체의 생명주기(객체를 생성한다던지, 소멸, 객체의 메서드 호출등)를 내(개발자)가 관리해 왔다.

예를 들어, 어떤 객체를 언제 생성하고, 어떻게 초기화하며, 언제 소멸시킬지를 개발자가 결정하고 코드로 구현한다.
또한, 외부 라이브러리나 다른 클래스의 메서드를 호출할 때도 그 시점을 개발자가 직접 결정한다.

그러나, IoC는 이러한 제어 권한을 개발자로부터 빼앗아, 프레임워크나 라이브러리 같은 외부 요소에 위임한다.
이를 통해 개발자는 비즈니스 로직에 더 집중할 수 있게 되며, 코드의 유지보수성과 확장성이 향상된다.

앞에서 만든 AppConfig가 제어 흐름에 대한 권한을 가지고있는것이다.
예를 들어, OrderServiceImpl에서는 필요한 인터페이스들을 호출하긴 한다. 근데 여기서 어떤 구현객체들이 들어갈지 모른다. 뭐 memoryRepository인지, 뭐 DbReposiotry인지 모르는거다.
또한, 이 OrderServiceImp조차도 AppConfig가 생성한다.

extra) 프레임워크 vs 라이브러리
프레임워크와 라이브러리의 차이를 IoC를 통해서 설명할 수 있다.
테스트에서 Junit프레임워크를 사용할때, 개발자가 각각 Test 메서드를 작성한다. 그러나 이러한 Test메서드를 호출하는게 아니다.
우리가 짠 Test메서드를 어디서 호출하는 프로그램은 작성하지 않았으니 말이다.
우리는 그저 @Test 어노테이션만 붙이면, Junit 프레임워크가 이 Test 메서드를 호출한다. @BeforeEach나 @AfterEach 에노테이션이 붙은 메서드도 마찬가지다.

이처럼, 내가 BeforeEach,AfterEach의 호출 시점을 정의하지 않았고, 테스트 메서드를 호출하는 프로그램을 짜지 않았음에도, Junit 프레임워크로 제어권이 역전되어서 실행되는것이다.
=>즉, 내가 작성한 코드를 제어하고, 대신 실행

반면에 라이브러리는 개발자가 만든 클래스들의 나열로, 다른 프로그램들에서 사용할 수 있도록 제공하는 방식이다. 생각해보면 C++에서 #include 하면 vector 라이브러리에 정의된, 함수들을 가져다 쓸 수 있다. => vector라이브러리에 대한 객체 생성, 객체의 메서드 호출들을 main에서 내가 컨트롤 할 수 있다.
=>즉, 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리이다.[main메서드에서 vector라이브러리에 있는 객체를 직접 만들고 객체의 함수를 직접 호출함.]

DI(의존성 주입)
의존관계는 정적인 클래스 의존관계와, 실행시점에 결정되는 동적인 객체 의존관계를 둘이 분리해서 생각해야한다.

예를 들어, 정적인 클래스 의존관계는 import만 보고 알아차릴수있다. OrderServiceImpl은 MemberReposiotry,DiscountPolicy 인터페이스에 의존하고 있음을 쉽게 판단할 수 있다. 그러나 여기서 MemberReposiotry에서 memory인지 Db인지 구현체는 알 수 가 없다.

반면에, 동적인 객체 인스턴스 의존관계는 애플리케이션 실행시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에게 전달하여 클라이언트와 서버의 실제 의존관계가 연결되는것을 DI라고 한다.
우리는 AppConfig에서 실제 구현객체로 MemroyRepository와 FixDiscountPolicy라는 구현객체를 생성하고 이것을 클라이어트에게 전달해서 생성자 호출로 injection해줬다.

객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결한 것이다.
의존관계 주입을 사용하면, 정적인 클래스 의존관계를 변경하지 않고(orderServiceImpl에서 코드 수정없이), 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.(memoryRepository=>DbRepository from AppConfig[외부])

이렇게 AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는것을, IoC컨테이너 혹은 DI컨테이너라고한다.

스프링으로 전환하기

지금까지는 순수한 자바 코드로만 DI를 적용했다. 이제는 Spring을 사용해서 DI를 적용 시켜 보겠다.

간단하게 Configuration과 모든 메서드에 Bean에노테이션을 붙여주면된다.
이걸 하다가 이전에 입문편에서 생각난, Component가 생각났는데 글을 쓸만한 내용이 있는거 같아서 따로 다른 글을 적어보겠다.
component? configuration?

이렇게 하면, 메서드 이름을 key로 등록이 되고 value는 당연히 new로 생성된 객체일 것이다.
이렇게 Bean애노테이션을 통해 스프링 컨테이너에 스프링 빈으로 등록이 된다.

MemberApp에 스프링 컨테이너 적용

주석처리를 한것처럼 원래는 그냥 AppConfig생성자를 불러서 객체 생성을하고, 메서드를 호출하는 방식을 사용했었다.=>직접 객체를 생성하고 DI
그러나 이제는 new AnnotationConfigApplicationContext를 통해 인자로 AppConfig.class를 설정하였다.

ApplicationContext를 스프링 컨테이너라고 한다.

Configuration이 붙은 AppConfig를 설정 정보라고 생각한다.

그래서 여기서 Bean이 붙은 모든 메서드를 호출해서 반환된 객체를 스프링 컨테이너인 ApplicationContext에 저장해둔다.

이러면 Key가 메서드명, value가 new로 생성한 객체가 된다.

그래서 이전에는 필요한 객체를 AppConfig에서 직접 찾아서 조회했지만,
[Appconfig appconfig = new AppConfig(),
MemberService memberService = appConfig.memberService() 이런식으로 찾았음]

이제는
applicationContext.getBean(내가 찾고싶은 객체)로 찾을 수 있다.

다음시간에는 스프링 컨테이너와, 스프링빈에대해서 더 자세히 살펴보는 시간을 가지도록 하겠다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글