[스프링 핵심원리 기본편] 객체 지향 원리 적용

흑수·2022년 1월 22일
0

김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.

새로운 할인 정책 개발

기존에 변경될 수도 있다고 이야기한 할인 정책이 갑자기 바뀌었다고 생각해봅시다.
그렇다면 새로운 할인 정책이 기존에 만들어 놓은 할인 정책 대신에 들어가야 해요.

객체 지향 설계 원칙을 준수 했다면 큰 어려움이 없다!


정률 할인 정책

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

VIP 회원일 때 10%를 할인해주는 정책입니다.


테스트 코드

public class RateDiscountPolicyTest {
    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용")
    void vip_o() {
        Member member = new Member(1L, "memberA", Grade.VIP);
        int discount = discountPolicy.discount(member, 10000);
        assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP 아니라면 할인 적용 x")
    void vip_x() {
        Member member = new Member(1L, "member2", Grade.BASIC);
        int discount = discountPolicy.discount(member, 10000);
        assertThat(discount).isEqualTo(0);
    }
}

@DisplayName 어노테이션은 테스트 클래스와 메소드의 이름을 붙여주는 역할을 합니다.

위와 같이 변경하면 무엇이 문제일까?

이렇듯, 할인 정책을 새롭게 변경하게 되면 할인 정책을 이용하는 OrderServiceImpl 역시 변경되어야 합니다. 아래처럼 말이지요.

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

역할(인터페이스)과 구현(구현체)을 잘 구분 했고, DIP, OCP 같은 객체 지향 설계 원칙을 잘 준수했다고 보여요. (정말 ?!)

하지만!!

기대한 의존관계

실제 의존관계

ServiceImpl(클라이언트)의 입장에서는 역할을 담당하는 DiscountPolicy에만 의존하는 것이 아니라 FixDiscountPolicy, RateDiscountPolicy도 알고 있어야 하므로, 구현체에도 의지한다고 할 수 있답니다.

그렇기에 구현체를 변경하면 클라이언트의 코드도 변경해야하므로 OCP도 위반하고 있다는 사실..!


결국, 우리가 해야할 일은 클라이언트가 인터페이스에만 의존하도록 변경하는것

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

이 상태 그대로 실행하면 구현체가 없으니 Null 포인터 오류가 나게 됩니다.
그래서 누군가가 구현 객체를 대신 생성해서 주입해주어야 합니다.

관심사의 분리

어플리케이션이 하나의 공연이라고 생각해보겠습니다.
각각의 인터페이스를 배우 역할이라고 한다면, 그 역할에 맞는 사람을 고르는 것은 누가 하게 될까요?!

드라마 그 해 우리는에서 국연수(김다미) 역할을 최웅(최우식)이 정할까요? 아닙니다. 바로 연출자, 감독이 정하게 되겠죠.

이전까지는 국연수 역할을 최우식이 정하고 있었답니다. 즉, 배우의 책임이 증가했다는 이야기죠.

결국, 관심사를 분리하자는 이야기!


AppConfig

이를 가능하게 하기 위해, 어플리케이션의 전체 동작 방식을 구성(config)하는 파일을 생성해줍니다. 구현 객체를 설정하고, 연결하는 책임을 가진 클래스입니다.

AppConfig

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

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

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

  • MemberServiceImpl -> MemoryMemberRepository
  • OrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicy

이렇게 되면 기존에 작성한 다른 파일들에 생성자를 추가해야 합니다.


MemberServiceImpl

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

이렇게 되면 MemerServiceImpl은 구현체를 의존하지 않고, 인터페이스인 MemberRepository만 의존하게 됩니다.

  • 생성자를 통해 어떤 구현체가 들어오게 될지 전혀 알 수 없습니다.
  • 무엇을 주입하게 될지는 외부(AppConfig)에서 결정합니다.

AppConfig가 MemoryMemberRepository 객체를 생성해 그 참조를 MemberSerivceImpl을 생성하면서 주입하게 됩니다.


OrderServiceImpl

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 실행

MemberApp

public class MemberApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        ... 동일
}

OrderApp

public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
        ... 동일
}

MemberServiceTest

public class MemberServiceTest {

    MemberService memberService;

    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
	... 동일
}

OrderServiceTest

public class OrderServiceTest {
    MemberService memberService;
    OrderService orderService;

    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }

}

@BeforeEach 어노테이션은 테스트 실행하기 전에 호출됩니다.

결국 AppConfig라는 공연 기획자가 모든것(구현체)을 결정하며 
어플리케이션이 어떻게 돌아갈지 전체적인 책임을 지게 된다.
각 클라이언트는 담당 기능만 실행하는 책임을 지면 된다.

AppConfig 리팩토링

현재는 중복도 있고 역할에 따른 구현이 잘 보이지 않습니다.
new MemoryMemberRepository()가 두개 있으며,
MemoryMemberRepository의 경우 역할인 MemberRepository가 서로 구분되지 않아요.

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

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

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

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

중복을 지워주고 보니 역할과 구현도 확실히 분리됩니다.
만약 할인 정책을 다시 바꾼다고 한다면 new FixDiscountPolicy() 부분만 새롭게 바꿔주면 된답니다.


어플리케이션을 사용하는 영역과 구성하는 영역(Configuration)으로 나누어졌습니다.
또한, 앞으로는 AppConfig만 변경하고 클라이언트 코드를 전혀 손볼 필요가 없어졌습니다.

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

SRP, 단일 책임 원칙

  • 이 원칙을 따르면서 관심사를 분리
  • 클라이언트는 실행하는 책임만 담당, 구현 객체 생성 및 연결은 AppConfig 담당

DIP, 의존 관계 역전 원칙

  • 구체화에 의존하는 것이 아닌, 추상화에만 의존하게 변경
  • 그렇게 되면 클라이언트단에서는 Null 포인터 오류가 남
  • AppConfig가 객체를 생성해서 주입해주면서 문제 해결

OCP, 개방 폐쇄의 원칙

  • AppConfig쪽에서 구현체를 변경하기에 클라이언트에서는 폐쇄적
  • 단 확장성은 있음

IOC, DI, 그리고 컨테이너

제어의 역전(IOC, Inversion of Control)

클라이언트측에서 생성하고 연결하고 실행하는 것은 매우 자연스러운 일입니다.
다만, 앞에서 했듯이 모든 제어권을 이제 클라이언트가 아닌 AppConfig에서 관리하게 됩니다.
클라이언트는 오직 자신의 로직만 수행할 뿐이죠. 이러한 현상을 제어의 역전이라고 부릅니다.

프레임워크 vs 라이브러리

  • 프레임워크: 내가 작성한 코드를 제어하고 대신 실행
  • 라이브러리: 코드를 제어하는 주체가 나

의존 관계 주입(DI, Dependency Injection)

정적인 클래스 의존 관계와 동적인 클래스 의존 관계 두가지가 존재합니다.

정적인 클래스 의존 관계
import문을 보고 바로 의존관계를 쉽게 파악할 수 있습니다. 즉, 실행시키지 않아도 확인할 수 있다는 이야기에요.
가령, OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있는 것처럼 말이에요.
다만, 이것만 보고 실제로 어떤 구현체가 주입될지는 알 수 없답니다.

동적인 클래스 의존 관계
앱이 실행된 후에야 의존 관계를 파악할 수 있습니다. 외부에서 객체를 생성해서 클라이언트에 전달하고 서로 의존 관계가 연결 됩니다.

DI 컨테이너

AppConfig처럼 객체를 생성, 관리하면서 의존 관계를 연결해주는 것을 DI컨테이너라고 부릅니다.

스프링으로 전환하기

지금까지는 순수한 자바코드로 DI 적용을 했다면, 이제는 스프링으로 해보겠습니다!

AppConfig

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

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}

MemberApp

public class MemberApp {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        ... 동일
}

OrderApp

public class OrderApp {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        OrderService orderService = ac.getBean("orderService", OrderService.class);
	... 동일
}

  • ApplicationContext를 스프링 컨테이너라고 함
  • AppConfig를 이용해서 직접 객체를 생성하고 DI를 하는 것이 아닌 ApplicationContext를 이용
  • @Configuration 어노테이션이 붙은 파일을 설정 정보로 확인
  • @Bean 어노테이션이 붙은 메소드를 실행시켜 반환되는 객체들을 스프링 컨테이너에 등록, 등록된 객체를 스프링 빈이라고 부름
  • .getBean()을 이용해서 필요한 객체를 직접 조회

코드가 더 복잡해 보이는데 어떤 장점이 있는걸까요?
다음 글에서 만나겠습니다...

profile
기록용

0개의 댓글