Programming/Spring(KYH)

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

잇(IT) 2023. 6. 9. 21:53

- 객체 지향 원리를 그림과 코드를 통해 분석해보려 한다.

@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 코드 작성 시 1. given 2. when 3. then 구조를 맞춰서 작성하는 것이 좋다.


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

- 위의 그림과 같이 DiscountPolicy 클래스에서 FixDiscountPolicy로 변경하게 될 경우 OCP, DIP 같은 객체지향 설계 원칙을 준수하지 못한다.

- 클래스 의존 관계를 보면 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.

- 또한 OCP의 경우 클라이언트 코드에 영향을 준다.


- 기대했던 의존관계와 달리 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy인 구체 클래스도 함께 의존하고 있다.

- 결과적으로, FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야 한다. OCP를 위반하게 된다.

 

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

- 위와 같이 DiscountPolicy 인터페이스에만 의존하도록 작성한다.


- 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들어서 사용하는 것이 좋다.

public class AppConfig {
 	public MemberService memberService() {
 		return new MemberServiceImpl(new MemoryMemberRepository());
 	}
 	public OrderService orderService() {
 		return new OrderServiceImpl(
 			new MemoryMemberRepository(),
 			new FixDiscountPolicy());
 	}
}

- 위 클래스는 구현에 필요한 객체를 생성한다.

- AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.

 

package hello.core.member;
public class MemberServiceImpl implements MemberService {
	private final MemberRepository memberRepository;
         
	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
 	}
 	public void join(Member member) {
		 memberRepository.save(member);
 	}
 	public Member findMember(Long memberId) {
 		return memberRepository.findById(memberId);
 	}
}

- MemberServiceImpl 클래스에 memberRepository를 매개변수로 하는 생성자를 추가한다.(생성자 주입 방식)

- 위 코드를 보게 되면 전부 인터페이스에만 의존하고 있다.

- MemberServiceImpl은 생성자를 통해 어떤 구현 객체가 들어올지 알 수 없다.

- 구현 객체는 AppConfig를 통해서 설정된다.


클래스 다이어그램

- 결과적으로 DIP가 완성된 것을 알 수 있다. 위와 같이 추상에만 의존하고 구체 클래스는 몰라도 사용이 가능하게 된다.

 

public class AppConfig {
 	public MemberService memberService() {
 		return new MemberServiceImpl(new MemoryMemberRepository());
 	}
 	public OrderService orderService() {
 		return new OrderServiceImpl(
 			new MemoryMemberRepository(),
 			new FixDiscountPolicy());
 	}
}

- AppConfig는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.

- 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 의존관계 주입 또는 의존성 주입이라고 한다.

* OrderServiceImpl 클래스 또한 매개변수 2개를 받는 생성자를 생성해준다.


public class AppConfig {
 	public MemberService memberService() {
 		return new MemberServiceImpl(new MemoryMemberRepository());
 	}
 	public OrderService orderService() {
 		return new OrderServiceImpl(
 			new MemoryMemberRepository(),
 			new FixDiscountPolicy());
 	}
}

- 현재 구성한 AppConfig의 경우 중복이 있고, 역할에 따른 구현이 잘 드러나지 않는다.

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

- 아래와 같이 코드를 변경해준다. 아래 코드의 경우 위의 MemoryMemberRepository 메소드의 중복이 사라졌다.


- 그림과 같이 할인 정책을 변경하기 위해선 사용 영역에선 어떤 코드를 건드릴 필요 없이 구성 영역에 있는 AppConfig의 코드만 변경하게 되면 정책을 변경할 수 있게된다.

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

- 위와 같이 FixDiscountPolicy() -> RateDiscountPolicy()로 변경 시키면 된다.


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

- DIP 의존관계 역전 원칙 : 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

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


- IoC, DI, 그리고 컨테이너

- 위의 코드들에서 AppConfig 클래스가 프로그램에 대한 제어 흐름에 대한 권한을 전부 가지고 있다.

- 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.

 

- 프레임워크 vs 라이브러리

1. 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit)

2. 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.


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

1. OrderServiceImpl 은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.

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

 

- 정적인 클래스 의존관계

클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다. 클래스 다이어그램을 보자 OrderServiceImpl 은 MemberRepository , DiscountPolicy 에 의존한다는 것을 알 수 있다. 그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl 에 주입 될지 알 수 없다.

클래스 다이어그램

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

- 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.

 

객체 다이어그램

1. 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.

2. 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.

3. 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

4. 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

 

IoC 컨테이너, DI 컨테이너

1. AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을

2. IoC 컨테이너 또는 DI 컨테이너라 한다.

3. 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.

4. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.


- 스프링의로 전환하기

@Configuration
	public class AppConfig {
	@Bean
 	public MemberService memberService() {
         return new MemberServiceImpl(memberRepository());
 	}
	@Bean
 	public OrderService orderService() {
 		return new OrderServiceImpl(
 			memberRepository(),
 			discountPolicy());
 	}
	 @Bean
	 public MemberRepository memberRepository() {
 		return new MemoryMemberRepository();
 	}
	@Bean
 	public DiscountPolicy discountPolicy() {
 		return new RateDiscountPolicy();
	}
}

- AppConfig에 설정을 구성한다는 뜻의 @Configuration을 붙여준다.

- 각 메서드에 @Bean을 붙여준다. 이렇게하면 스프링 컨테이너에 스프링 빈으로 등록한다.

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

- 스프링 컨테이너

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

2. 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.

3. 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.

4. 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )

5. 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.

6. 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.

 

 

 

 

 

 

 

출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (스프링 핵심원리 - 기본 편)

728x90