[Spring] 핵심원리 기본편 - 스프링 핵심 원리 이해1 (객체 지향 원리 적용)
Java & Spring

[Spring] 핵심원리 기본편 - 스프링 핵심 원리 이해1 (객체 지향 원리 적용)

인프런 - 스프링 핵심 원리 (김영한) 강의를 듣고 간단하게 정리한 글입니다.

새로운 할인 정책 개발

기획자가 서비스 오픈 직전에 할인 정책을 고정 금액 할인에서 주문 금액당 몇 퍼센트 할인 해주는 정률 할인으로 변경 요구했다고 가정

-> 새로운 정률 할인 정책을 추가해보자!

 

DiscountPolicy.java 인터페이스와 같은 폴더 위치에 RateDiscountPolicy.java 파일을 추가해준다.

여기서는 VIP 고객에게 10퍼센트 할인을 넣어줬다.

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

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

그럼 이 할인 로직이 잘 동작하는지 테스트 코드를 작성해보자.

 

클래스 이름에 커서를 가져가 대고 cmd + shift + T (mac 기준) 누르면 아래와 같은 창이 뜬다. 아래처럼 세팅하고 OK.

create test 팝업

test/java/hello/core/discount/RateDiscountPolicyTest.java 파일이 생긴다.

아래와 같이 VIP인 경우, BASIC인 경우 나눠서 테스트 해본다.

package hello.core.discount;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import hello.core.member.Grade;
import hello.core.member.Member;

class RateDiscountPolicyTest {

	RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

	@Test
	@DisplayName("VIP는 10% 할인이 적용되어야 한다") // Junit5의 기능으로 한글로 테스트명을 작성하는 annotation
	void vip_o() {
		// given
		Member member = new Member(1L, "memberVIP", Grade.VIP);

		// when
		int discount = discountPolicy.discount(member, 10000);

		// then
		Assertions.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
		Assertions.assertThat(discount).isEqualTo(0);
	}
}

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

이제 위에서 추가한 새로운 할인 정책을 OrderServiceImpl.java에서 수정해서 적용해보자.

// OrderServiceImpl.java

...

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

...

여기에 문제가 있다.

역할과 구현을 분리했고, 다형성도 활용했지만

DIP, OCP를 위반했다!

 

왜?

 

DIP: DiscountPolicy라는 추상클래스에 의존할 뿐만이 아니라 FixDiscountPolicy, RateDiscountPolicy라는 구현클래스에도 의존하고 있음!

OCP: 위에서 기능을 확장(새로운 할인 정책 추가)해서 변경하면 클라이언트 코드(OrderServiceImpl)에도 영향을 준다!

 

이제 어떻게 바꿔야 할지 밑에서 알아보자.


관심사의 분리

앱의 전체적인 동작방식을 구성(config)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!

 

hello/core/AppConfig.java

package hello.core;

import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;

public class AppConfig {

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

 

hello/core/member/MemberServiceImpl.java

아래와 같이 생성자를 통해 memberRepository의 구현체를 주입시켜준다. == 생성자 주입 방식

package hello.core.member;

public class MemberServiceImpl implements MemberService {

	private final MemberRepository memberRepository;

	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도 마찬가지로 바꿔준다.

 

hello/core/AppConfig.java

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

}

 

hello/core/order/OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;

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

	@Override
	public Order createOrder(Long memberId, String itemName, int itemPrice) {
		Member member = memberRepository.findById(memberId);
		// 주문에서는 할인이 변경되더라도 건드리지 않아도 됨 -> 단일책임원칙이 잘 지켜진 것
		int discountPrice = discountPolicy.discount(member, itemPrice);

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

 

이제 완전 DIP를 지키게 되었다!

 

이전에 만들어줬던 MemberApp과 OrderApp도 수정해준다.

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;

public class MemberApp {

	public static void main(String[] args) {
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService();
		// MemberService memberService = new MemberServiceImpl();
		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());
	}
}

 

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;

public class OrderApp {

	public static void main(String[] args) {

		AppConfig appConfig = new AppConfig();

		MemberService memberService = appConfig.memberService();
		OrderService orderService = appConfig.orderService();

		// MemberService memberService = new MemberServiceImpl();
		// OrderService orderService = new OrderServiceImpl();

		Long memberId = 1L;
		Member member = new Member(memberId, "memberA", Grade.VIP);
		memberService.join(member);

		Order order = orderService.createOrder(memberId, "itemA", 10000);

		System.out.println("order = " + order);
	}
}

 

테스트 코드도 수정해준다.

여기서 BeforeEach annotation은 테스트 실행하기 전에 매번 해당 코드를 실행해준다는 의미다.

package hello.core.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import hello.core.AppConfig;

public class MemberServiceTest {

	MemberService memberService;

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

	@Test
	void join() {
		// given 어떤 환경
		Member member = new Member(1L, "memberA", Grade.VIP);

		// when 어떤 것을 했을 때
		memberService.join(member);
		Member findMember = memberService.findMember(1L);

		// then 어떻게 된다
		Assertions.assertThat(member).isEqualTo(findMember);
	}
}
package hello.core.order;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

public class OrderServiceTest {

	MemberService memberService;
	OrderService orderService;

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

	@Test
	void createOrder() {
		Long memberId = 1L;
		Member member = new Member(memberId, "memberA", Grade.VIP);
		memberService.join(member);

		Order order = orderService.createOrder(memberId, "itemA", 10000);
		Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
	}
}

AppConfig 리팩터링

AppConfig.java 파일을 보자.

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

}

역할에 따른 구현이 잘 드러나 있지 않다.

cmd + option + m을 눌러서 아래와 같이 바꿔준다.

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(memberRepository());
	}

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

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

	private FixDiscountPolicy discountPolicy() {
		return new FixDiscountPolicy();
	}

}

이렇게 바꾸면 역할과 구현이 한눈에 들어온다. 즉, 어플리케이션의 구조가 한눈에 들어온다.


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

이제 새로운 할인 정책으로 변경해보자.

FixDiscountPolicy -> RateDiscountPolicy 로 변경한다.

이제 할인 정책을 변경하려면 구성영역인 AppConfig 파일만 수정해주면 된다!

// AppConfig.java

...

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

...

할인 정책을 바꿨지만 우리는 사용 영역인 OrderServiceImpl의 어떤 코드도 수정해주지 않고,

단지 구성 영역인 AppConfig 파일만 수정해줬다.

DIP도 지키고 OCP도 지켰다!


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

그럼 위에서 수정한 코드가 좋은 객체 지향 설계의 원칙을 어떻게 지키게 되었는지 3가지에 대해 정리해보자.

SRP 단일 책임 원칙 (하나의 클래스는 하나의 책임만 가져야 한다.)

기존의 클라이언트 객체: 직접 구현 객체 생성, 실행.. 등

구현 객체를 생성하고 연결 -> AppConfig가 담당하게됨!

수정된 클라이언트 객체: 실행만 담당

 

DIP 의존관계 역전의 원칙 (프로그래머는 추상화에 의존하지 구체화에 의존해서는 안된다.)

클라이언트 코드가 DiscountPolicy라는 인터페이스(추상화)에 의존하게 만들고

RateDiscountPolicy, FixDiscountPolicy라는 구현체에 의존하지 않게 수정했다.

즉, AppConfig에서 FixDiscountPolicy의 인스턴스를 생성해 클라이언트 코드에 의존관계를 주입했다.

 

OCP (소프트웨어의 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.)

AppConfig 파일에서 RateDiscountPolicy, FixDiscountPolicy를 변경해주므로 클라이언트 코드에서는 코드를 변경해주지 않아도 된다.

 


IOC, DI, 그리고 컨테이너

이번엔 IOC, DI, 그리고 컨테이너의 개념에 대해 알아보자.

제어의 역전 IOC (Inversion Of Control)

개발자가 무언가를 호출하는게 아니라 프로그램이 호출해주는 것.

즉, 기존에는 개발자가 구현 객체(ex. OrderServiceImpl)를 직접 생성하고 실행하면 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다.

하지만 AppConfig가 등장하고, AppConfig가 프로그램의 제어 흐름을 조종하게 되었다. 구현 객체를 단지 자신의 로직을 실행만 한다.

이렇게 외부에서 프로그램의 제어를 하는 것을 제어의 역전 IOC 라고 한다.

 

여기서 프레임워크와 라이브러리의 가장 큰 차이점을 발견할 수 있다.

만약 제어권을 프레임워크가 가져간다면 그건 프레임워크가 맞고,

내가 작성한 코드가 제어권을 가진다면 그건 프레임워크가 아니라 라이브러리다.

 

의존관계 주입 DI (Dependency Injection)

정적인 클래스 의존관계

import코드만 보고 의존관계를 파악할 수 있는 것. 즉, 앱 실행전에도 의존관계를 파악할 수 있음

 

동적인 클래스 의존관계 (실행시점에 결정)

우리가 정률 할인 정책을 쓸지 정액 할인 정책을 쓸지 모른다. 즉, 실행시점에서야 결정되게 되는 것을 동적인 클래스 의존관계라고 한다.

 

  • 어플리케이션 실행시점에 외부에서 실제 구현 객체를 생성해서 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라고 함.
  • 객체 인스턴스를 생성하고 참조값을 전달해서 연결됨.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 인스턴스 변경 가능
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 클래스 의존관계를 쉽게 변경 가능

IOC 컨테이너, DI 컨테이너

AppConfig 처럼 객체를 생성하고 관리하고 의존관계 연결해 주는 것을 IoC 컨테이너 혹은 DI 컨테이너라고 함

그중에서 요새는 의존관계 주입에 집중하게 되면서 DI 컨테이너라고 많이 부름. (== 어셈블러, 오브젝트 팩토리)

앞으로 공부할 스프링이 바로 이 DI 컨테이너로 역할을 하게 되는 것이다.


스프링으로 전환하기

지금까지 순수 자바 코드로 DI를 했다면 이제 스프링으로 바꿔보자!

 

우선 AppConfig 부터

클래스 위에 @Configuration annotation을 붙여주고

메서드마다 @Bean을 붙여줬다.

 

@Configuration: 앱의 설정정보를 의미

@Bean: 스프링 컨테이너에 Bean으로 등록

package hello.core;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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;

@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();
		return new RateDiscountPolicy();
	}

}

 

MemberApp

스프링 컨테이너를 생성하고 필요한 설정 정보들을 빈에 등록해줬다.

package hello.core;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;

public class MemberApp {

	public static void main(String[] args) {
		// AppConfig appConfig = new AppConfig();
		// MemberService memberService = appConfig.memberService();
		// MemberService memberService = new MemberServiceImpl();

		// ApplicationContext: 스프링 컨테이너
		// AnnotationConfigApplicationContext(AppConfig.class): AppConfig에 있는 환경설정 정보를 스프링 컨테이너에 등록해서 관리
		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
		// memberService라는 메서드 이름을 가지고, MemberService라는 타입을 가진 빈을 꺼내옴
		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());
	}
}

 

OrderApp

스프링 컨테이너를 생성하고 필요한 설정 정보들을 빈에 등록해줬다.

package hello.core;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;

public class OrderApp {

	public static void main(String[] args) {

		// AppConfig appConfig = new AppConfig();
		// MemberService memberService = appConfig.memberService();
		// OrderService orderService = appConfig.orderService();

		// MemberService memberService = new MemberServiceImpl();
		// OrderService orderService = new OrderServiceImpl();

		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

		MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
		OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

		Long memberId = 1L;
		Member member = new Member(memberId, "memberA", Grade.VIP);
		memberService.join(member);

		Order order = orderService.createOrder(memberId, "itemA", 10000);

		System.out.println("order = " + order);
	}
}

 

정리하자면

ApplicationContext(스프링 컨테이너)를 통해 기존에 AppConfig로 직접 DI했던걸 스프링 컨테이너로 하게 바꾼 것.

스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용하고, @Bean이 붙은 메서드를 모두 빈에 등록하는데 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 부름. 스프링 빈의 이름은 디폴트로는 메서드명을 쓰고 옵션값으로 바꿀 수도 있다.

이렇게 해서 기존에는 AppConfig를 통해 직접 필요한 객체를 조회했지만 이제는 스프링 컨테이너에서 getBean()을 통해 필요한 스프링 빈(객체)를 찾게 된다.

 

이렇게 하면 어떤 장점이 있을까?

이제부터 그걸 차차 알아간다.


정리

우리는 자바 순수 코드로 작은 애플리케이션을 구현했지만

좋은 객체 지향 원리를 준수하지 않는 코드를 작성했다.

그래서 이를 준수하도록 AppConfig 클래스 파일을 생성해 관심사 분리를 해주면서 원칙을 지키게 되었다.

이때 생성한 AppConfig 처럼 의존관계를 주입해주는 것을 DI 컨테이너라고 하는데,

마지막 파트에서 우리는 스프링 컨테이너를 생성해서 AppConfig를 설정값으로 등록해주면서 DI 컨테이너를 Spring을 통해 구현하게 되었다. 이제 의존관계 주입은 Spring 컨테이너가 해주게 된다! 그럼 이제 이 스프링 컨테이너가 어떤 장점이 있고, 어떤 기능을 하는지 다음 강의를 통해 알아보자.

728x90
반응형