ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 객체 지향 설계의 적용
    Backend Dev/Spring Framework 2022. 3. 8. 03:14
    728x90

     

    객체 지향의 원리와 설계 원칙을 소스코드로 적용해보려 하고, 스프링을 사용하지 않는 순수 자바로만 코드를 작성하는 단계에서 생기는 문제점부터 짚어볼 것이다. 

     

     

    아래의 클라이언트 코드를 변경해야 하는데, 왜 변경이 필요한 것일까?

     

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

     

     

    OrderServiceImpl이 DiscountPolicy라는 인터페이스에 의존하면서 구체 클래스에도 의존을 하기에 DIP 원칙을 위반하는 것이고, 기능을 확장해서 변경시 클라이언트 코드까지 영향을 주므로 OCP에도 위반된다. FixDiscountPolicy에서 인터페이스 구체 클래스가 추가되거나 사용이 필요할때마다 클라이언트에 영향이 가는 것이다.

     

     

     


    본 문제는 아래와 같이 인터페이스에만 의존하도록 의존관계를 변경해야 한다. 이때, 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 정상적으로 실행이 가능할 것이다.

     

    public class OrderServiceImpl implements OrderService {
    	private DiscountPolicy discountPolicy;
    }



    AppConfig의 등장

     


    애플리케이션을 하나의 공연이라고 비유했을 때, 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다. 공연 구성, 담당 배우 섭외, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 책임자가 나와야한다. 책임자를 만들고 배우와 책임자(기획자)의 책임을 확실히 분리시켜야 한다.

     

     

    애플리케이션의 실제 동작에 필요한 "구현 객체를 생성"하고 생성한 객체 인스턴스의 참조를 "연결"하는 책임을 가지는 별도의 설정 클래스가 필요한데, 이 클래스가 AppConfig이다.

     

    package hello.core;
    
    import hello.core.discount.FixDiscountPolicy;
    import hello.core.member.MemberService;
    import hello.core.member.MemberServiceImpl;
    import hello.core.member.MemoryMemberRepository;
    import hello.core.order.OrderService;
    import hello.core.order.OrderServiceImpl;
    
    public class AppConfig {
        public MemberService memberService() {
        	return new MemberServiceImpl(new MemoryMemberRepository());
        }
        public OrderService orderService() {
            return new OrderServiceImpl(new MemoryMemberRepository(),
            new FixDiscountPolicy());
        }
    }

     

    AppConfig에서 별도로 구현 객체를 직접 생성해주고 인스턴스 참조를 통해 연결시키는 작업을 한다. 이때 이 참조를 클라이언트 생성자를 이용해 주입시켜준다.

     

    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(외부)에서 결정
    - 클라이언트는 이제 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중한다.

     

     

    객체의 생성과 연결은 AppConfig가 담당하고, 클라이언트는 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 되므로, 객체를 생성하고 연결하는 역할(구성영역)과 실행(사용영역)하는 역할이 명확히 분리되었다. 

     

    이는 클라이언트 관점에서 의존관계를 마치 외부에서 주입해주는 것 같다고 하여 "DI(Dependency Injection)" 즉, 의존관계 주입 또는 의존성 주입이라 한다.

    AppConfig에서 앱이 어떻게 동작해야 할지 전체 구성을 책임지며, 각 클라이언트는 담당 기능을 실행하는 책임만 지면된다. 기능이 추가되어도 구성 역할을 담당하는 AppConfig만 변경하면 되므로 클라이언트 코드인 사용영역의 어떤 코드도 변경할 필요없다. 이로써, DIP와 OCP 원칙까지 지켜진다.

     

     

    추가적으로 리팩토링을 통해 AppConfig의 중복성을 제거할 수 있고 애플리케이션 구성이 어떻게 되어있는지 빠르게 파악할 수 있다. 

     

    package hello.core;
    
    import hello.core.discount.DiscountPolicy;
    import hello.core.discount.FixDiscountPolicy;
    import hello.core.member.MemberRepository;
    import hello.core.member.MemberService;
    import hello.core.member.MemberServiceImpl;
    import hello.core.member.MemoryMemberRepository;
    import hello.core.order.OrderService;
    import hello.core.order.OrderServiceImpl;
    
    public class AppConfig {
        public MemberService memberService() {
        	return new MemberServiceImpl(memberRepository());
        }
        
        public OrderService orderService() {
            return new OrderServiceImpl(
            memberRepository(),
            discountPolicy());     
       }
       
        public MemberRepository memberRepository() {
        	return new MemoryMemberRepository();
        }
        
        public DiscountPolicy discountPolicy() {
        	return new FixDiscountPolicy();
     }

     

    AppConfig 내부 코드에서 중복이 제거되었고 역할에 따른 구현이 명확하게 나눠졌다. 이렇게 AppConfig의 등장으로 사용 영역과 객체를 생성하고 연결하는 구성 영역으로 분리되었다.

     

     

     

     

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

     

    AppConfig 클래스를 구현한 이후 아래 3가지의 원칙이 적용된다.

     

     

    SRP - 단일 책임 원칙


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

    - 클라이언트 객체는 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있었음
    - 이제 구현 객체를 생성, 연결하는 책임은 AppConfig가 담당하고 클라이언트 객체는 실행하는 책임만 담당함

     

     

    DIP - 의존관계 역전 원칙


    "추상화에 의존을 해야하며, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다. 

     

    - 새로운 기능을 추가하거나 기존 기능의 변경이 필요할 때 추상화 인터페이스뿐만 아니라 구현 클래스에도 의존을 하였음
    -  AppConfig가 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입(생성자 주입)하여 DIP 원칙을 따름

     


    OCP - 개방-폐쇄 원칙


    "소프트웨어 요소 확장에는 열려있으나, 변경에는 닫혀있어야 한다."

    - 다형성을 사용하고 클라이언트가 DIP를 지킴
    - 앱을 사용 영역과 구성 영역으로 나눔으로써 AppConfig가 클라이언트 코드에 변경 사항을 알아서 생성자 주입하므로 클라이언트 코드는 변경하지 않아도 됨
    - 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있음

     

    본 내용은 인프런-스프링 핵심원리 기본편 김영한님의 강의를 참고하였습니다.
    728x90

    댓글

Designed by Tistory.