월루를 꿈꾸는 대학생

[핵심원리-기본] 객체지향 원리 적용 본문

Programing/Spring Boot

[핵심원리-기본] 객체지향 원리 적용

하즈시 2023. 7. 18. 21:04
728x90

새로운 할인 정책 개발

  • 새로운 요구사항으로 주문 금액에 따라 할인율을 다르게 적용!

객체지향 설계를 준수했다면 요구사항 변경에 유연하게 대체 가능

계획에 따르기 보다는 변화에 대응하기를..


원래 있던 discountPolicy인터페이스에 구현체를 하나 더 추가함
-> RateDiscountPolicy

할인과 관련된 부분만 별도로 떼어냈으니까 테스트 하기가 쉬워진거다

class RateDiscountPolicyTest {  

    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();  

    @Test // 성공 테스트  
    @DisplayName("VIP는 10% 할인이 적용되어야 한다")  
    void vip_o(){  
        //given  
        Member member = new Member(1L, "memberVIP", Grade.VIP);  
        //when  
        int discount = discountPolicy.discount(member, 10000);  
        //then  
        assertThat(discount).isEqualTo(1000);  
    }  

    @Test  
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다")  
    void vip_x(){  
        //given  
        Member member = new Member(2L, "memberBASIC", Grade.BASIC);  
        //when  
        int discount = discountPolicy.discount(member, 10000);  
        //then 할인이 안 되니까 0       
        assertThat(discount).isEqualTo(0);  
    }  
}

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

할인 정책을 변경하려면 기존의 코드 클라이언트인 OrderServiceImpl코드를 고쳐야 한다.

public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
  • 역할과 구현을 충실하게 분리
  • 다형성 활용 및 인터페이스와 구현 객체를 분리

문제점 발견

OCP , DIP 설계 원칙을 준수하지 못함

DIP
추상뿐만 아니라 구현 클래스에도 의존을 하는 중

OCP
해당 코드는 기능을 추가 혹은 수정할 때 클라이언트인 OrderServiceImple의 코드 수정이 불가피하다


인터페이스뿐만 아니라 구체 클래스도 함께 의존 중 DIP위반

정책을 변경하면 코드를 변경해야함 -> 의존관계

변경하는 순간에 OrderServiceImple 코드를 주석처리 추가해야함

기름차에서 전기차로 바꿔도 우리는 운전할 수 있는데 해당 코드는 라이센스를 새로 갱신하는 수준으로 코드의 변경이 있어서 문제


해결방법

DIP를 위반하지 않도록 인터페이스만 의존하도록 의존 관계를 변경해야한다

이게 쉽게 되는가??

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

다음과 같이 값을 바로 할당하는 것이 아니라 변수만 따로 두는 식으로 나중에 갈아끼울 수 있도록

다만 해당 코드는 null 에러가 끔 . 현재 아무 값도 할당이 되어 있지 않으니까!!!

즉 인터페이스만 있으면 돌아가지 않음

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


관심사의 분리

애플리케이션을 하나의 공연이라고 가정
로미오 & 줄리엣 역할이 있음
이전 코드는 로미오라는 역할을 맡은 배우 (구현체)이 줄리엣 역할(인터페이스)를 누구를 시킬지 배우(구현체) 섭외하고 연기를 맞추는 등 여러가지 다양한 책임 을 준 코드였다.

OrderServiceImpl가 해당 로직만 해야하는데 discountPolicy는 누가 해야하고 해당 구현체는 또 멀 해야하는지 하나만 잘해도 모자를 판에 여러가지 신경쓰고 있음

너무 책임이 많다 분리하자!

배우는 본인의 역할만 수행하는 것에 집중

  • OrderServiceImpl은 아무것도 신경쓰지 말고 오로지 가격 계산해서 고객에게 보여주는 것에 중점을 줘야함

디카프리오는 어떤 여자 배우가 와도 똑같이 공연할 수 있어야한다

  • OrderSErviceImpl은 어떤 discountPolicy 구현체가 와도 정상적으로 기능 해야함

공연 구성, 배우 섭외 등 역할에 맞는 배우 지정하는 책임 담당자 공연 기획자 라는 역할이 필요
책임을 확실히 분리 해야한다


AppConfig 등장

  • 애플리케이션의 전체 동작방식을 구성(config)하기 위해서 구현 객체 생성연결 하는 책임을 가지는 별도의 설정 클래스가 필요하다
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();

해당 코드보면 구현체가 인터페이스를 의존하지만 구현체까지 의존하게 됨
배우가 직접 담당 배우 섭외하는 꼴
해당 이런 작업을 환경설정을 AppConfig에서 하자

아래와 같이 직접 선택하는 것이 아니라 생성자를 통해서 주입하는 것으로 변경
해당 주입은 AppConfig에서 구성함

public class MemberServiceImpl implements MemberService{  

    private final MemberRepository memberRepository;  

    public MemberServiceImpl(MemberRepository memberRepository) {  
        this.memberRepository = memberRepository;  
    }
}
public class AppConfig {  
    public MemberService memberService(){  
        return new MemberServiceImpl(new MemoryMemberRepository());  
    }

이제 impl 코드는 구현체 관련 코드는 없고 오로지 인터페이스가 있는 추상화에 의존
구체적인 거는 밖에서 주입해주게 됨 -> #생성자_주입

또 OrderService에 코드 또한 구현체에 의존적이었기에 변경이 필요

  • 변경전

    public class OrderServiceImpl implements  OrderService{  
    
      // 멤버리파지토리에서 회원 찾기 위해서  
      private final MemberRepository memberRepository = new MemoryMemberRepository();  
    

/* ch03
private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); private final DiscountPolicy discountPolicy = new RateDiscountPolicy();*/ private DiscountPolicy discountPolicy;


- 변경후
- final로 변수를 선언하면 **초기화를 처음**에 해주거나 **생성자를 통해서 초기화** 해줘야한다
```java
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;  
    }
  • 생성자 주입 AppConfig
    public OrderService orderService(){  
      return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());  
    }

코드가 구현체에 대해서 전혀 모름
어떤 구현체가 들어올지 모른다
그냥 대본보고 연기하듯 실행만 하면 오케

AppConfig는 애플리케이션 동작에 필요한 구현객체를 직접 생성한다

  • MemberSErviceImpl

  • MemmoryMemberRepository

  • OrderServiceImpl

  • FixDiscountPolicy

    AppConfig는 생성한 객체 인서턴스의 참조를 생성자를 통해서 주입

    • MemberServiceImpl -> MemortyMemberRespository
    • OrerserviceImpl -> MemoryMemberRepository , FixDiscountPolicy

Impl은 이제 인터페이스에만 의존
어떤 객체 구현체가 들어올지 모르고 오로지 외부 AppConfig에서 결정된다 : 공연기획자
즉 *의존 관계에 대한 고민은 외부로 실행에만 집중!! *

객체의 생성은 AppConfig 담당
DIP완성 : impl들은 추상에만 의존하고 구현 클래스 몰라도 오케
관심사 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 분리됨


appconfig에서 객체를 생성하고 그 참조값을 impl에 전달해서 impl이 해당 객체를 사용할 수 있도록 함
클라이언트 입장에선 이런 의존 관계를 외부에서 주입하는 거 같다고 하여 DI(Dependencty Injection) 의존 관계 주입 이라한다

public class MemberApp {  
    public static void main(String[] args) {  

        //기존 코드  
        //MemberService memberService = new MemberServiceImpl();  

        //생성자 주입 배운 후  
        //Appconfig를 만들고 appconfig에 설정된 구현체 서비스를 가지고와서 참조해서 사용하도록 함   
        AppConfig appConfig = new AppConfig();  
        MemberService memberService = appConfig.memberService();  
        Member member = new Member(1L, "memberA", Grade.VIP);  
        memberService.join(member);

OrderApp

public class OrderApp {  
    public static void main(String[] args) {  
        //기존 코드  
        //MemberService memberService = new MemberServiceImpl(null);  
        //OrderService orderService = new OrderServiceImpl(null,null);  
        //생성자 주입  
        AppConfig appConfig = new AppConfig();  
        MemberService memberService = appConfig.memberService();  
        OrderService orderService = appConfig.orderService();

MemberServiceTest

  • @BeforeEach : 각 테스트가 실행전에 실행되는 코드

  • 테스트가 실행전 appconfig로 객체 만들고 그걸 멤버변수에 넣어서 테스트 코드 실행

    public class MemberServiceTest {  
    
      //MemberService memberService = new MemberServiceImpl();  
    
      //변경 후  
      MemberService memberService;  
    
      @BeforeEach  
      public void beforeEach(){  
          AppConfig appConfig = new AppConfig();  
          memberService = appConfig.memberService();  
      }

정리

  • AppConfig를 통해서 관심사를 확실하게 분리했다.
  • 배역, 배우를 생각해보자.
  • AppConfig는 공연 기획자다.
  • AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다. 애플리케이션이 어떻게 동작해
    야 할지 전체 구성을 책임진다.
  • 이제 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.
  • OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.

Appconfig 리팩토링

구성에서 위와 같은 그림이 나오도록 역할이 드러나도록 리팩토링이 필요

기존

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

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

}

변경 후

public class AppConfig {  
    public MemberService memberService(){  
        return new MemberServiceImpl(memberRepository());  
    }  

    private static MemoryMemberRepository memberRepository() {  
        return new MemoryMemberRepository();  
    }  

    public OrderService orderService(){  
        return new OrderServiceImpl(memberRepository(), discountPolicy());  
    }  
    public DiscountPolicy discountPolicy(){  
        return new FixDiscountPolicy();  
    }  

}

메서드 명을 보는 순간 역할이 드러남
각 구현을 어떤 거 할지 확인 가능
중복을 제거하고 구현이 보임


새로운 구조와 할인 정책 적용

fixDiscountPolicy -> RateDiscountPolicy로 변경

Appconfig 등장으로 애플리케이션의 사용영역과 객체 생성하고 구성하는 영역으로 분리가 가능해짐


할인율 변경은 구성영역의 appconfig만 고치면 된다!

public DiscountPolicy discountPolicy(){  
    //return new FixDiscountPolicy();  
    return new RateDiscountPolicy();  
}

정말 간단하게 해당 코드 (구성 영역) 수정만 하면사용영역의 코드 변경이 없이 기능의 수정이 가능하다

구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로
생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.


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

SRP 단일 책임 원칙

한 클래스는 하나의 책임만 가져야한다

  • 관심사를 분리
  • 구현 객체 생성 연결은 AppConfig담당
  • 클라이언트 객체는 실행하는 책임만 담당

DIP의존 관계 역전 원칙

프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다.

  • 의존성 주입
  • 추상화에 의존해야지 구현 클래스에도 함께 의존하면 안된다
  • 추상화 인터페이스에만 의존하도록 코드를 변경하고 해당 인터페이스에 넣어줄 객체는 AppConfig에서 만들어서 넣어주는 형태로 의존관계 주입을 실행

OCP

소프트웨어 요소는 확장에는 열려있어야 하며 변경에는 닫혀 있어야한다

  • 사용영역과 구성영역으로 나눔
  • 코드를 주입해서 클라이언트 코드는 변경하지 않아도 됨 구성영역만 변경
  • 소프트웨어 요소를 새로 확장해도 사용영역은 변경 안 해도 된다!

IoC , DI 그리고 컨테이너

제어의 역전 IoC

  • 내가 호출하는게 아니라 프레임워크가 내 코드를 대신 호출해주는 것
  • 원래 구현객체가 프로그램 제어 흐름을 스스로 조절했으나 AppConfig 등장이후 객체는 로직 실행하는 역할만하고 제어 프름을 AppConfig가 가져옴
  • Impl만 보고서는 흐름자체가 제어 안 됨 즉 어떤 구현 객체들이 실행될지 모른다 제어권이 다 AppConfig가 가지고 있음
  • 프로그램 제어 흐름을 직접 제어하는 것이 아니라 외부 (AppConfig)에서 관리하는 것을 제어의 역전(IoC) 라고 한다

#question

프레임워크와 라이브러리

  • 프레임워크 : 내가 코드 제어하고 실행하는 것 Junit
  • 라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당하는 것

의존 관계 주입

  • 의존 관계는 정적인 클래스의 의존 관계와 실행 시점에서 결정되는 동적인 객체 (인스턴스) 의존 관계 둘을 분리해서 생각해야한다.

정적인 클래스 의존관계

  • import코드로 의존관계 파악 가능
  • 실행하지 않아도 분석이 가능하다! 서로서로 참조함을 코드로 파악 가능
  • 인텔리제이로 클래스 다이어그램으로 보는 그림으로 파악 가능 show dependency
  • 의존관계는 파악이 가능한데 실제 어떤 객체가 OrderServiceImpl에 주입되는지는 해당 코드만으로는 파악이 불가하다 -> 구현체가 코드에 적힌게 아니라 추상화 되어 있으니까

동적인 객체 인스턴스 의존 관계

  • 실행 시점 (런타임)에 외부에서 실제 구현객체를 생성하고 클라이언트 전달 그리고 클라이언트와 서버어의 실제 의존관계가 연결되는 것을 의존 관계 주입이라고 한다
  • 객체 인스턴스를 생성하고 그 참조값을 전달해서 연결함
  • 의존관계 주입을 사용하면 클라이언트 사용영역 코드 변경하지 않고 클라이언트가 호출하는 대상 타입 인스턴스 변견가능 - 의존 관계 주입
  • ==의존관계 주입을 사용하면 정적 클래스 의존관계 변경없이 동적인 객체 인스턴스 의존 관계 변경 가능==

#important

IoC 컨테이너 , DI컨테이너

  • AppConfig처럼 객체 생성 및 관리 , 의존관계 연결해주는 것을 IoC 컨테이너 / DI 컨테이너 라고 한다

이래서 먼저 AppConfig로 개념을 잡고 넘어간거였나.. 갓


스프링으로 전환하기

지금은 코드만으로 DI 했는데 스프링으로 변경하자 !

기존

public class AppConfig {  
    public MemberService memberService(){  
        return new MemberServiceImpl(memberRepository());  
    }  

    private static MemoryMemberRepository memberRepository() {  
        return new MemoryMemberRepository();  
    }  

    public OrderService orderService(){  
        return new OrderServiceImpl(memberRepository(), discountPolicy());  
    }  
    public DiscountPolicy discountPolicy(){  
        //return new FixDiscountPolicy();  
        return new RateDiscountPolicy();  
    }  

}

@Configuration : 설정정보
@Bean : 스프링 컨테이너에 등록

변경후

@Configuration  // 어노테이션 추가  구성정보를 담당한다는 뜻  
public class AppConfig {  
    @Bean  
    public MemberService memberService(){  
        return new MemberServiceImpl(memberRepository());  
    }  
    @Bean  
    private static MemoryMemberRepository memberRepository() {  
        return new MemoryMemberRepository();  
    }  
    @Bean  
    public OrderService orderService(){  
        return new OrderServiceImpl(memberRepository(), discountPolicy());  
    }  
    @Bean  
    public DiscountPolicy discountPolicy(){  
        //return new FixDiscountPolicy();  
        return new RateDiscountPolicy();  
    }  

}

어노테이션 추가함
해당 어노테이션 추가함으로서 해당 코드는 구성 정보 를 담당한다는 의미를 내포하게함

  • MemberApp을 DI 넣어서 변경

    public class MemberApp {  
      public static void main(String[] args) {  
    
          //생성자 주입 배운 후  
          //Appconfig를 만들고 appconfig에 설정된 구현체 서비스를 가지고와서 참조해서 사용하도록 함  
          //AppConfig appConfig = new AppConfig();  
          //MemberService memberService = appConfig.memberService();  
    
       //DI 배운 후  
       //스프링은 모든게 ApplicationContext 시작함 이게 스프링 컨테이너임 모든 bean 객체를 관리함      // AnnotationConfigApplicationContext -> AppCOnfig에 있는 어노테이션들이 해당됨  
      // 파라미터는 해당 어노테이션이 있는 AppConfig의 클래스들 / 스프링이 @Bean 붙어있는거 객체 생성해서 컨테이너에 넣어줌  
          ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);  
          // 컨테이너에 있는 Bean을 꺼내야함 getBean사용해서 AppConfig에 @Bean붙어 있는 놈들 중 고르고 싶은거 넣어주면 됨  
          // 보통 메서드 이름으로 등록되니까 메서드 이름으로 찾기  
          // class는 왜 붙이지?  
          MemberService memberService = applicationContext.getBean("memberService", MemberService.class);  
    
          Member member = new Member(1L, "memberA", Grade.VIP);  
          memberService.join(member);  
    
          Member findMember = memberService.findMember(1L);  
          System.out.println("new member = : "+member.getName());  
          System.out.println("find member = : "+findMember.getName());  
    
      }  
    }
  • ApplicationContext : 스프링에 있는 모든 bean객체를 관리하는 친구

  • AnnotationConfigApplicationContext : @bean , @Configuration 어노테이션 관리하는 클래스 해당 클래스에 어노테이션 있는 AppConfig를 넣어서 *AppConfig에 @Bean붙은 객체들을 전부 인스턴스화해서 컨테이너에 이동 *

  • 컨테이너 bean꺼낼 때 getBean 메서드를 사용하며 키는 메서드 이름 , 벨류는 return값

    실행하면 AppConfig 안에 있는 모든 @Bean객체들이 떴는 거 확인 가능

스프링컨테이너한테 이름주고 꺼내면 된다

OrderServiceApp

public class OrderApp {  
    public static void main(String[] args) {  
        //기존 코드  
        //MemberService memberService = new MemberServiceImpl(null);  
        //OrderService orderService = new OrderServiceImpl(null,null);  
        //생성자 주입  
//        AppConfig appConfig = new AppConfig();  
//        MemberService memberService = appConfig.memberService();  
//        OrderService orderService = appConfig.orderService();  

        ApplicationContext apc = new AnnotationConfigApplicationContext(AppConfig.class);  
        MemberService memberService = apc.getBean("memberService", MemberService.class);  
        OrderService orderService = apc.getBean("orderService", OrderService.class);

스프링 컨테이너

  • ApplicationContext = 스프링 컨테이너
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에
    등록된 객체를 스프링 빈
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService ,
    orderService )
  • 스프링 빈은 applicationContext.getBean()로 호출 가능

근데 그냥 AppConfig 써도 코드 길이도 그렇고 더 편한것도 없는 거 같은데 무슨 이점이 있는가??

나중에 알려주시는 듯..!
스프링 컨테이너의 핵심 기능을 알아보면서 나오는 거 같다

출처
https://inf.run/wFfL

728x90