[Spring 핵심 원리 이해] 2. 객체 지향 원리 적용

2024. 12. 18. 22:10·Spring

해당 포스팅은 

https://camuscoding.tistory.com/6

 

[Spring 핵심 원리 이해] 1. 예제 만들기

해당 포스팅은 인프런에서 강의중이신 김영한 선생님의 스프링 강의를 듣고 복습하는 목적으로 작성되었습니다.https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B

camuscoding.tistory.com

이전 포스팅과 연결됩니다! 

+ 역시나 김영한 선생님의 강의를 복습하는 용도입니다.


저번 포스팅때 주문과 할인 도메인까지 함께 만들어 보았습니다. 

이번 포스팅에는 우리가 지금까지 작성했던 코드가 어떤 문제점을 가지고 있는지 그리고 이 코드들을 스프링을 활용해서 어떻게 극복할 수 있을지!! 알아보겠습니다 ㅎㅎ

 

1. 새로운 할인 정책 개발 

열심히 할인 도메인을 구현했더니 이제오서 할인 정책을 바꾸기로 했다.. ㅠㅠ 

원래 할인 정책은 FixDiscount ->  VIP 고객 1000원 고정 할인이었지만 이제는 RateDiscount -> 10% 할인으로 ! 

package hello.core_review.Discount;

import hello.core_review.member.Grade;
import hello.core_review.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;
        }
    }
}

 

우린 이전에 DiscountPolicy라는 인터페이스르 만들어 두었기 때문에 이를 기반으로 RateDiscount 클래스르 만들었다. 

그럼 이 할인 정책이 잘 작동하는지 확인해보아야 할 것이다.

 

package hello.core_review.discount;

import hello.core_review.Discount.RateDiscountPolicy;
import hello.core_review.member.Grade;
import hello.core_review.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

 

그냥 간단하게 RateDiscountPolicyTest를 만들고 Test를 돌려보자. 

고객이 VIP 고객일 때와 VIP 고객이 아닐 때를 나누어서 테스트 해보았다. @DisplayName 어노태이션은 Test를 돌렸을 때 테스트 이름을 표시해주는 것이다. 

 

결과는? 

성공!

 

그럼 이제 orderservice에도 적용을 해야 할 차례이다. 

 

OrderServiceImpl 의 코드 내용을 

 

이렇게 바꿔주었다. 그냥 간단하게 FixDiscountPolicy가 아니라 RateDiscountPolicy를 사용하는 것이다. 

 

그런데 이 지점에서 우리가 작성한 코드의 문제점이 드러난다. 

 

먼저 DIP 원칙을 지키지 않은 것이다.

우리는 인터페이스를 만들며 구현 객체를 분리해왔다. 그리고 DIP원칙은 구체적인 것이 아니라 추상적인 것에 의존해야 한다는 것이다.

그런데 우리는 정말 추상적인 것만을 의존하고 있을까? 

discountPolicy는 추상클래스이다. 그런데 우리는 이 discountPolicy에 구체적인 클래스(FixDiscount, RateDiscount)를 생성해서 이를 함께 의존하고 있다. 

이 부분에서 우리는 DIP를 위반하고 있다는 것이다. 

 

또한 OCP 원칙을 지키지 않고 있다. 

OCP는 확장에 열려 있다는 것인데, 우리는 코드를 확장(구체 클래스 변경)하면서 클라이언트 코드를 고치고 있다. 이는 OCP 원칙 또한 위반한 것이다! 

 

이를 어떻게 해결할 수 있을까? 

사실 정답은 간단하다. 그냥 interface에만 의존하도록 바꾸면 되는 것이다. 

그런데 말은 쉽지 어떻게 interface만으로 코드를 만든단 말인가? 

 

그런데 만약 

"누군가 클라이언트 코드인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해줄 수 있다면 어떨까?"

 

* 여기서 클라이언트 코드란 호출하는 코드라고 생각하면 된다. 반대로 호출 당하는 쪽을 서버라고 한다. 

 

2. AppConfig 등장 

'구성' 이라는 의미를 가진 config 에서 알 수 있듯 AppConfig는 어플리케이션 전체 동작 방식을 구성해준다. 

 

package hello.core_review;

import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import hello.core_review.member.MemoryMemberRepository;
import hello.core_review.order.OrderService;
import hello.core_review.order.OrderServiceImpl;

public class AppConfig {

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

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

 

이렇게 AppConfig를 통해서 애플리케이션이 실제 동작하는데 필요한 구현 객체들을 생성한다. 

그리고 생성한 객체 인스턴스의 참조를 생성자를 통해서 클라이언트에 주입(연결) 해주는 것이다. 

 

OrderServiceImpl와 MemberServiceImpl 도 이에 맞게 바꿔주자. 

package hello.core_review.order;

import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.Discount.RateDiscountPolicy;
import hello.core_review.member.Member;
import hello.core_review.member.MemberRepository;
import hello.core_review.member.MemoryMemberRepository;

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);
    }
}
package hello.core_review.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);
    }
}

 

이렇게 하면 클라인트 코드는 추상 클라스에만 의존하게 된다. AppConfig가 구현 객체를 넘겨주기 때문에 클라이언트 코드는 어떤 구현 객체가 주입될지는 알지 못하게 되는 것이다.

 

이제 이와 관련된 테스트 코드도 바꿔 줘야 한다. 

우리는 먼저 MemberApp과 OrderApp을 만들어 자바 코드를 기반으로 한 테스트를 진행했었다.

 

MemberApp과 OrderApp에서 각각 

AppConfig 객체를 만들고 memberService와 orderService에 appConfig.memberService와 appConfig.orderService를 넣어주자. 

 

그리고 Test 패키지에 존재하는 RateDiscountPolicy와 MemberServiceTest도 수정해준다. 

 

package hello.core_review.member;

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

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(member.getId());

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }

}

 

 

package hello.core_review.order;

import hello.core_review.AppConfig;
import hello.core_review.member.Grade;
import hello.core_review.member.Member;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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 = 1;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

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

 

 

@BeforeEach 어노태이션은 메서드가 테스트가 실행되기 호출되도록 한다.

그래서 여기에 memberService와 orderService가 AppConfig를 통해 구현 객체를 전달받도록 하는 것이다.

 

 

3. AppConfig 리팩터링- IoC와 DI

그런데 현재 AppConfig를 보면 중복된 코드가 존재하고, 역할과 구현이 명확하지 않다. 

예를 들어서 memberService 메서드는 반환값으로  MemoryMemberRepository와 같은 구현 객체를 가지고 있다. 

 

더 명확히 바꿔보자 

package hello.core_review;

import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.member.MemberRepository;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import hello.core_review.member.MemoryMemberRepository;
import hello.core_review.order.OrderService;
import hello.core_review.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();
    }
}

이렇게 바꿔주면 AppConig 클래스 안에서도 역할과 구현이 명확해지며, 중복되는 코드를 없앨 수 있다. 

 

이렇게 되면 이제 할인 정책이 바뀐다고 하더라도 AppConfig의 DiscountPolicy 메서드의 반환값만 바꿔주면 된다. 

이제 클라이언트를 수정할 일도 없을 뿐더러 구체적인 것을 의존하는 일도 일어나지 않게 되었다!

 

다시 말하지만 AppConfig는 애플리케이션 전체의 동작에서 생성될 구현 객체를 도맡아 생성해주고, 이를 다시 연결해주는 역할을 수행한다.

 

AppConfig의 등장으로 우리는 SOLID 5원칙 중 DIP와 OCP를 지키게 되었다. 또한 단일 책임 원칙이라는 SRP 원칙 또한 지키게 된다.

SRP 원칙이란 한 클래스는 하나의 책임만 가져야 한다는 것이다. 기존의 클라이언트 코드는 직접 구현 객체를 생성하고, 이와 연결하며 실행까지 하는 다양한 책임을 가지고 있었다. 그런데 AppConfig가 구현객체 생성과 연결을 수행하기에 클라이언트 객체는 실행만 하면 되는 것이다. 

 

- Ioc 제어의 역전 - 

여기서 제어의 역전(Inversion of Control)가 발생된다.

 

원래 클라이언트 코드는 스스로 필요한 구현 객체를 생성하고 연결하고 실행했다.

그런데  AppConfig가 등장하면서 구현 객체의 생성과 연결의 권한을 가져간다. 

심지어 클라이언트 코드(OrderService Impl, MemberServiceImpl)또한 AppConfig가 생성하며, 클라이언트 코드는 어떤 구현 객체가 주입되는지도 알지 못한다. 

즉, 프로그램에 대한 제어 흐름의 권한이 모두 AppConfig에게 있게 되는 것이다. 

이렇게 프로그램의 제어 흐름 관리가 외부에 존재하게 되는 것을 제어의 역전이라고 한다. 

 

- DI 의존관계 주입 -

또한 클라이언트와 서버의 실제 의존 관계가 연결되는데 이를 의존 관계 주입(Dependenct Injection)이라고 한다. 

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

 

정적인 클래스 의존 관계는 

클래스 다이어 그램을 그려보면 쉽게 생각할 수 있다. 즉 클래스 간의 의존 관계를 나타낸다. 

 

출처 - 김영한 스프링 강의

OrderServiceImpl은 MemberRepository와 DiscountPolicy에 의존하고 있다. 

그런데 클래스 의존 관계 만으로는 어떤 구현 객체가 OrderServiceImpl에 주입되는지 알 수 없다. 

 

이런 객체의 인스턴스 의존 관계를 동적인 의존관계라고 한다. 

동적인 의존 관계는 애플리케이션 실행 시점에 생성되는 객체 인스턴스들의 참조를 의미한다. 

출처 - 김영한 스프링 강의

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

 

의존 관계 주입은 정적인 클래스의 의존 관계를 변경하지 않으면서도 동적인 객체 인스턴스들의 의존 관계를 쉽게 변경하도록 돕는다. 

 

이렇게 AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 혹은 DI 컨테이너라고 한다. 

요즘에는 의존관계 주입에 초점을 맞춰 주로 DI 컨테이너라고 한다

 

 

4. 스프링으로 전환하기 

놀랍게도 지금까지 순수 자바 코드만으로 DI를 적용해왔다. 

이제 드디어 스프링을 활용해서 코드를 작성해보자. 

 

AppConfig 를 바꿔주자 

package hello.core_review;

import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.member.MemberRepository;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import hello.core_review.member.MemoryMemberRepository;
import hello.core_review.order.OrderService;
import hello.core_review.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

먼저 @Configuration 어노태이션은 해당 클래스를 애플리케이션의 설정(구성) 정보로 쓰겠다는 의미이다.

그리고 @Bean은 이라 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록해둔다. 

이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다. 

스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. 

 

그럼 스프링 컨테이너는 어떻게 사용하는가? 

 

package hello.core_review;

import hello.core_review.member.Grade;
import hello.core_review.member.Member;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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());
    }
}
package hello.core_review;

import hello.core_review.member.Grade;
import hello.core_review.member.Member;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import hello.core_review.order.Order;
import hello.core_review.order.OrderService;
import hello.core_review.order.OrderServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

        long memberId = 1;
        Member member = new Member(memberId, "memeberA", Grade.VIP);
        memberService.join(member);

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

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

    }
}

 

이렇게 ApplicationContext를 스프링 컨테이너라고 한다.

기존에는 AppConfig를 직접 사용해 직접 객체를 생성하고 DI 해줬지만, 이제는 스프링 컨테이너를 통해서 사용하면 된다.

또한 개발자가 필요한 객체를 AppConfig를 통해 직접 조회해야 했지만 이제 스프링 컨테이너를 통해 필요한 스프링 빈을 찾아 사용한다. 

applictaionContext.getBean() 메서드를 사용하면 된다. 여기에 메서드의 이름으로 등록한 스프링 빈의 이름과 클래스를 매개변수로 넣는다. 

 

이제 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하면 된다!

 

그럼 스프링 컨테이너를 사용하면 어떤 장점이 있는 걸까? 

 

다음 포스팅에서 계속된다!

'Spring' 카테고리의 다른 글

[Spring 핵심 원리 이해] 6. 의존관계 자동 주입  (6) 2025.01.03
[Spring 핵심 원리 이해] 5. 컴포넌트 스캔  (3) 2024.12.28
[Spring 핵심 원리 이해] 4. 싱글톤 컨테이너  (4) 2024.12.27
[Spring 핵심 원리 이해] 3. 스프링 컨테이너와 스프링 빈  (9) 2024.12.20
[Spring 핵심 원리 이해] 1. 예제 만들기  (6) 2024.12.07
'Spring' 카테고리의 다른 글
  • [Spring 핵심 원리 이해] 5. 컴포넌트 스캔
  • [Spring 핵심 원리 이해] 4. 싱글톤 컨테이너
  • [Spring 핵심 원리 이해] 3. 스프링 컨테이너와 스프링 빈
  • [Spring 핵심 원리 이해] 1. 예제 만들기
코뮝
코뮝
  • 코뮝
    코뮝의 기술 블로그
    코뮝
  • 전체
    오늘
    어제
    • 분류 전체보기 (25)
      • DB (1)
      • JAVA (0)
      • 운영체제 (11)
      • Spring (9)
      • 알고리즘 (2)
      • 카테캠 3기 (1)
      • DevOps (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    분할 메모리 할당
    Spring
    카테캠 백엔드
    가변 크기 할당
    원자 명령
    운영체제 공부
    CS
    김영한
    타조 알고리즘
    생산자 소비자 문제
    명품 운영체제
    고정 크기 할당
    스프링
    운영체제
    카태켐3기
    커널 레벨 스레드
    시스템 콜
    기아 스레드
    카태켐
    프로그래머스 유연근무제
    참조의 지역성
    Java
    코스만 조건
    역페이지테이블
    스프링 기본
    OS
    기초 cs
    백엔드
    스레드 매핑
    자바
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
코뮝
[Spring 핵심 원리 이해] 2. 객체 지향 원리 적용
상단으로

티스토리툴바