[Spring] 기본편 03. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용
이 글은 스프링 핵심 원리 - 기본편 을 듣고 정리한 내용입니다.
📌 새로운 할인 정책 개발
- 정률 할인 정책으로 바꾸고자 함. (구매 금액에 따라 할인율이 달라지는 정책)
- RateDiscountPolicy.class 추가
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;
}
}
}
- RateDiscountPolidcyTest.class 추가
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
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
assertThat(discount).isEqualTo(1000);
}
}
- RateDiscountPolicy 정책으로 변경
- OrderServiceImple 파일에서 기존 정책을 주석처리하고, 새로운 정책을 생성.
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
❗️ 문제점 발견
- 클래스 의존관계를 보면, 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
- OrderServiceImpl에서
-> 추상(인터페이스) 의존 : DiscountPolicy
-> 구체(구현)클래스 의존 : FixDiscountPolicy, RateDiscountPolicy - 즉,지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다. 따라서 OCP 위반.
- 그리고, 구체 클레스를 변경할 때, 클라이언트 코드도 변경해야 하므로 DIP 위반
❓ 어떻게 해결?
- 인터페이스에만 의존하도록 설계를 변경하자.
- 인터페이스에만 의존하도록 코드 변경
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
- 인터페이스에만 의존을 하긴 하지만 구현체가 없어서 코드를 실행하면 NPE(Null Pointer Exception) 발생.
- 해결방안 : 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현객체를 대신 생성하고 주입해주어야 함.
📌 관심사의 분리
- 관심사를 분리해야 한다.
- 공연을 예로 들면, 배우는 본인 역할인 배역을 수행하는 것에만 집중 해야 한다.
- 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 있어야 한다.
- 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자 -> 그 역할을 AppConfig가 한다.
🌱 AppConfig 등장
-
애플리케이션 전체 동장 방식 구성(Config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스 이다.
-
MemberServiceImpl - 생성자 주입
-> MemberServiceImpl 내에서 MemberRepository 구현체를 new로 생성하는것이 아니라 생성자를 통해 MemberRepository를 주입받도록 변경하였다. -
이렇게 설계 변경으로 인해 MemberServiceImpl은 MemoryMemberRepository 를 의존하지 않는다.
-
MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
//memberRepository interface필요, but interface만 가지고 있으면 nullPointerException 터질것임.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
- AppConfig.class
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
- OrderServiceImple도 같은 방식으로 수정한다. (구현체 받는 부분 제거 및 생성자 생성)
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
- AppConfig.class에 OrderService 부분 추가
public OrderService orderService(){
return new OrderServiceImpl( new MemoryMemberRepository(), new FixDiscountPolicy());
}
🌱 설계변경으로 인해 이전과 달라진점
-
OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정한다.
-
OrderServiceImpl은 이제부터 '의존관계에 대한 고민은 외부'에게 맡기고, '실행에만 집중'하면 됨.
-
객체의 생성과 연결은 AppConfig가 담당
-
DIP 완성 : MemberServiceImpl은 MemberRepository인 추상에만 의존하면 된다. (구체 클래스는 이제 몰라도 됨)
-
관심사의 분리 성공
-> 객체를 생성하고 연결하는 역할(AppConfig)과 실행하는 역할(Service)이 명확히 분리되었다.
🌱 의존관계 주입
- appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.
- 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) , 즉 의존관계 주입 또는 의존성 주입이라 한다.
- MemberApp과 OrderApp이 더이상 구체클레스에 의존하지 않도록 코드 수정
- AppConfig를 생성하여, AppConfig에 의존하여 필요한 service를 생성한다.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
}
}
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService= appConfig.orderService();
}
}
🌱 Test Code 수정
- @BeforeEach는 각 테스트 실행 전에 무조건 실행되는 부분
ex) 테스트가 3개 있으면 총 3번 실행됨. - TestCode 들도 마찬가지로 Appconfig에 의존하여 service를 받는 방식으로 수정한다.
public class MemberServiceTest {
MemberService memberService;
@BeforeEach // 각 테스트 실행 전에 무조건 실행되는 부분
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
}
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach // 각 테스트 실행 전에 무조건 실행되는 부분
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
}
📌 AppConfig 리팩토링
- 현재 AppConfig에는 new의 중복이 있고, 역할에 따른 구현이 잘 안보임.
- 중복을 제거하고, 역할에 따른 구현이 잘보이게 해야한다.
- AppConfig.class 수정
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
- new MemoryMemberRepository() 중복부분이 제거됨. 이제 MemoryMemberRepository를 다른 구현체로 변경할 때 한부분만 변경하면 된다.
- 이제 AppConfig를 보면 각각의 객체가 무엇을 리턴하는지 한눈에 파악할수 있다. (변경시, 리턴 부분만 변경해주면 됨)
- 역할과 구현 클래스가 한눈에 들어온다. -> 애플리케이션 전체 구성을 빠르게 파악할 수 있다.
📌 할인 정책 변경
- 기존에 FixDiscountPolicy를 RateDiscountPolicy로 변경해 보겠다.
- AppConfig.class파일에서 다음 부분만 변경하면 끝이다 .
public DiscountPolicy discountPolicy(){
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
- 할인 정책을 변경할 때, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. (구성영역)
- 클라이언트 코드인 OrderServiceImpl를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
- 구성 역할을 담당하는 AppConfig는 공연에서 기획자의 역할과 같다. 기획자는 공연 참여자인 구현 객체들을 모두 알아한다.
📌 좋은 객체지향 설계의 5가지 원칙 적용
-
지금까지 개발내용에서는 이중에 3가지인 SRP, DIP, OCP이 적용된 것이다
-
1. SRP - 단일 책임 원칙
- 단일 책임 원칙을 따르면서 관심사를 분리하였다.
- 구현 객체를 생성하고, 연결하는 책임은 AppConfig가 담당하고, 클라이언트 객체는 실행하는 책임만 담당한다.
-
2. DIP - 의존관계 역전 원칙
- 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다"는 원칙을 다르는 방법이다.- 이전 개발에서는 클라이언트에서 추상화 인터페이스인 DiscountPolicy에도 의존하였지만 구체화 구현 클래스인 FixDiscountPolicy에도 의존하였다.
- 그래서 클라이언트 코드가 인터페이스에만 의존하도록 변경하였으나 NPE이 발생하여 아무것도 실행할 수 없었다.
- 그래서 AppConfig에서 구체화 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입하였다.
- 이렇게 DIP 원칙을 따를 수 있게 되었다.
-
3.OCP - 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 애플리케이션을 사용 영역과 구성 영역으로 나누었다.- AppConfig가 의존관계를 FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드를 변경하지 않아도 된다.
- 즉 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있게 된 것이다!
📌 IOC, DI, 그리고 컨테이너
🌱 제어의 역전 IoC(Inversion of Contorl)
- 기존 프로그램에서는 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고 실행하였다.
- 그러나, AppConfig를 통해 구성 영역과 사용 영역이 구분되었고, 프로그램 제어 흐름에 대한 권한
은 AppConfig가 가지고 있다.- 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는것을 제어의 역전(IoC)이라 한다.
🌱 프레임워크 vs 라이브러리
- 프레임워크 : 내가 작성한 코드를 프레임워크가 제어하고, 대신 실행한다
- 라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당하면 그것은 내가 사용한 라이브러리 이다.
-> 즉 제어의 주도권이 나에게 있으면 라이브러리, 그것에게 있으면 프레임워크.
🌱 의존관계 주입 DI(Dependency Injection)
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는것을 의존관계 주입 이라한다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계(ex. OrderService - MemberRepository - DiscountPolidcy)를 변경하지 않고, 동적인 객체 인스턴스 의존관계 (ex. 어떤 회원저장소, 어떤 할인정책)를 쉽게 변경 할 수 있다.
🌱 IoC 컨테이너, DI 컨테이너
- AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너(=DI 컨테이너)라 함.
- 최근에는 주로 DI 컨테이너 라고 불림.
📌 스프링으로 전환하기
-
지금까지 자바로만 작성하였는데 스프링을 사용해보도록 하자.
-
AppConfig.class를 스프링 기반으로 변경
- 기존에서 달라진 점은, AppConfig클래스에 @Configuration 어노테이션이 추가 되었고, 각각의 메서드마다 @Bean 어노테이션이 추가 되었다.
- 이렇게 함으로써, 스프링 컨테이너에 스프링 빈으로 등록한다.
@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에 스프링 컨테이너 적용
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("findMember = "+findMember.getName());
}
}
- OrderApp에 스프링 컨테이너 적용
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService= appConfig.orderService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService",MemberService.class);
OrderService orderService = applicationContext.getBean("orderService",OrderService.class);
// 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", 20000);
System.out.println("order = "+ order);
System.out.println("order.calculatePrice= "+order.calculatePrice());
}
}
🌱 스프링 컨테이너
- ApplicationContext를 스프링 컨테이너 라고 한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성)정보로 사용한다.
- 여기서 @Bean 어노테이션이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
- 스프링 컨테이너에 등록된 객체들을 스프링 빈이라고 한다.
- 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
- 스프링 빈은 applicationContext.getBean()메서드로 찾을 수 있다.
❓코드가 더 복잡해진것 같은데 ,스프링 컨테이너를 왜 사용할까?
- 기존에는 개발자가 직접 자바코드로 모든것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 빈을 찾아서 사용하도록 변경되었다.
- 스프링 컨테이너를 사용하면 뭐가 좋은지는 다음 강에서 이어서 배워볼 예정~
Author And Source
이 문제에 관하여([Spring] 기본편 03. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@somyeong0623/Spring-기본편-02.-스프링-핵심-원리-이해1-객체-지향-원리-적용저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)