[Spring] OOP원칙과 DI, 그리고 config 파일(간략)

인프런 김영한님의 Spring 핵심 강의를 복습하는 글입니다
강의를 수강함으로서 객체지향개념을 직관적으로 경험 할 수 있었습니다.

  • 객체지향 설계 원칙을 준수함으로서 [ 유연한 설계 ] 가 가능하다.

간단하게, Spring을 사용하지 않았을 때, 설계를 구현하는 과정과 , 이 때 어떤 객체지향 설계원칙을 어기게 되는지 살펴보자.
이 때 의존성 주입의 필요성을 느끼게 되는데 이를 위한 config 파일을 간단하게 살펴본 후 ,
Spring을 도입할 경우에는 어떤 차이가 있는지 살펴본다.

Spring을 사용하지 않는다면?

  • Spring과 관련된 거 하나 없이 , 순수한 JAVA코드로 이를 작성할 수 있다.

회원 서비스



설계자체를 보면, 상속 보다도, composite을 활용하여 매우 잘 설계된 것 처럼 보인다.

회원 도메인

@Getter @Setter
public class Member {
    private Long id;
    private String name;
    private Grade grade;

    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

회원 레포지토리

public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId);
}
  • 이는 흡사 JPA repository interface 모습
public class MemoryMemberRepository implements MemberRepository {
	..구현 어쩌고
 }

회원 서비스

public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository =  new MemoryMemberRepository();
    ....구현 어쩌고
 }

위 설계의 문제점

현재 상황

  • 마치 💥 설계만 보면, 다형성을 활용 -> 인터페이스와 구현체를 잘 분리해 놓은 것 같다.
  • DIP 준수?
    DIP(Dependency inversion principle) : 의존관계 역전 원칙
    - 사용자는 [ 추상화에 의존 ] 해야 한다. [ 구체 화에 의존하면 ❌ ] --> interface에 의존해야 한다
    - 즉, 역할에 의존하여 사용해야지, 구현체를 변경하는 것이 가능하다. 사용자가 구현체에 의존한다면 변경이 어려워진다.

    현재 상황 : [ Class의 의존관계 ] 를 분석 --> interface 뿐만 아니라 구현체에도 의존하고 있다.
    ===> MemberRepository의 구현체를 다른 것으로 바꿔끼우기 위해 MemberServiceImpl 코드에 직접 수정을 하게 된다.

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository =  new MemoryMemberRepository();
    ....구현 어쩌고
 }
  • OCP 준수 ?
    OCP(Open/closed principle) : 개방-폐쇄 원칙 : SW 요소는 [ 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다 ]
    - 다형성을 잘 이용하면 가능
    - "역할"과 "구현"의 분리를 한다면 가능.

    MemberRepository의 구현체를 확장 시 문제 발생(위에서 본 것 처럼 , 코드에 변화가 생긴다) : MemberRepostiory의 하위 클래스로 MemoryMemberRepository 뿐만 아닌, DbMemberRepository로 확장할 경우 -> Client 코드 자체에 영향이 가는 것. -> OCP 위반


🚀 Spring을 사용한다면?

이를 Spring의 Bean 주입을 사용하는 코드로 변경한다면 어떻게 될까 ?

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;
    public MemberServiceImpl(MemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }
    ....구현 어쩌고
 }
  • 구현체 (MemoryMemberRepository) 에 의존하지 않고, interface 에 의존하게 된다.
  • 즉, 누군가가 Client인 MemberServiceImpl에 MemberRepository의 구현 객체를 대신 생성하고 주입 해 주면 된다.

AppConfig의 등장

Spring Bean 도입 하지 않을 것임. 여전히 Spring을 사용하지 않은 상황에서 개선해가는 과정을 설명한다.

  • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, [ 구현 객체를 생성하고, 연결하는 책임 ]을 가지는 별도의 [ 설정 클래스 ]를 만든다.
    • app 전반의 구성을 책임지는 설정 클래스는, 패키지 바로 밑에 생성 .

DI : 의존성 주입

DI (Dependency Injection) : 의존성 주입

  • Client입장에서, 의존관계를 마치 외부에서 주입해주는 것과 같다고 하여, DI라고 한다.
  • Client 측에서는, 어떤 인스턴스를, 다형성을 이용하여, 주입을 받고 ,
  • 별도의 클래스에서, 해당 인스턴스를 생성하고, 연결 해 준다면?

Appconfig

  • Appconfig 에서, MemberService에 필요한 💥 MemberRepositry 인스턴스를 생성하여, MemberService의 생성자를 호출하며 이를 인자로 넘겨준다.
public class AppConfig {
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
    }
}

MemberServiceImpl

  • 생성자 주입 , 생성자를 호출 시, 인자로 MemberRepository 인스턴스를 전달 받는다 .
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;

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

test code 예시

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)에서 생성하여 넣어준다.
  • MemberServiceImpl은, MemberRepository라는 interface에만 의존 -> 추상화에 의존(어떤 구현체가 들어올지는 알 수 없다. Polymorphism에 의해 무엇인가가 들어올 뿐) -> DIP(의존관계 역전 원칙)를 따르게 된다.

관심사가 분리 되었다.

  • MemberServiceImpl은, 회원 서비스 역할에만 충실할 수 있게 되었다. 따로 MemberRepository의 인스턴스를 생성하던 것을 하지 않아도 된다.

DI : 의존성 주입
--> 외부에서 생성한 인스턴스를 Cleint가 주입받아 사용한다.

AppConfig 의 역할

  • [ 설정 정보 ] 의 역할을 한다.
    • 따라서, [ 어떤 역할 ] 에 [ 어떤 구현체를 담고 있다] 라는 것이 한 눈에 보일 수 있는게 좋다.
      • MemberRepository의 구현체를 DB구현체로 바꿀 경우, 여기서만 바꿔주면 된다.
    • 중복을 제거해 주도록 해야 한다.

클래스 다이어그램

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();
    }
}
  • 이렇게 해 줌으로서, memberservice역할, memberRepository역할, orderservice역할,discountPolicy역할 까지 다 드러남 -> 한 눈에 보인다
    • 현재 나의 application에서는 memberService는 MemberServiceImpl을 사용할 거야.
    • MemberRepository는 MemoryMemberRepository를 할 거야. → 나중에 DB로 바뀐다면, AppConfig쪽에서 return new MemoryMemberRepository(); 이 코드만 바꿔주면 되는것.
    • discountPolicy또한 나중에 그렇게 변경해주면 되는 것.

IoC와 DI

IoC(Inversion of control)

  • Framework를 사용하기 전!! 에는, 개발자가 직접 필요한 객체를 생성, 호출 , 그 안에서 또 다른 객체를 생성, 호출 -> 직접 컨트롤 , 제어
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository =  new MemoryMemberRepository();//이런식
    .....
}

IoC : 개발자가 아닌, [ Framework가 대신 "생성" && 호출 ]

  • AppConfig가 등장한 이후 [ 구현 객체는 자신의 로직을 실행하는 역할만 담당 ] 하게 됨.
  • AppConfig가 [ 프로그램에 대한 제어 흐름 권한 ] 을 갖게 됨. -> [ AppConfig와 같은 외부 ]에서 [ 흐름을 관리 ]하게 되어 -> 제어의 역전이 일어남 .

프레임워크? 라이브러리?

  • 작성하는 코드에서 [ 내가 직접 ] 제어의 흐름을 담당 -> 라이브러리를 사용하는 것
  • 나는 로직만을 작성 했고, 실행과 제어를 [ 외부에서 ] 담당 -> Framework
    • FrameWork는, [ 자신의 lifeCycle ]이 존재함
    • 예를 들어 Junit framework
      • @BeforeEach를 먼저 실행 -> @Test -> @AfterEach 이런 라이프 사이클 속에서 실행

DI (Dependency Injection)

의존관계는

  • 정적인 클래스 의존관계 ( 클래스 다이어그램 )
  • 동적 객체(인스턴스) 의존 관계 ( 객체 다이어그램)
    를 분리해서 생각해야 한다.

IoC 컨테이너, DI 컨테이너

  • AppConfig처럼 [ 객체를 생성,관리 ] 하면서 [ 의존관계를 연결 ] 해주는, [ 제어의 역전을 일으키는 ] 것을 ---> "IoC 컨테이너" == "DI 컨테이너" 라고 한다

    [ Spring도 이런 DI container 역할 ] 을 해 주는 것

Spring으로 넘어오자

먼저

  • AppConfig -> Spring기반으로 변경

Appcofing

  1. 설정 정보에 @Configuration 어노테이션 붙이기
  2. 각 method에는 @Bean 을 붙이기
    - method가 리턴하는 인스턴스들[ Spring Container에 Spring Bean으로 자동등록 ] 된다.
@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(),discountPolicy());

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

🚀 return type을 interface타입으로

  • 이걸 보고 "역할"을 알 수 있는 것이라 생각하면 된다.
  • [ 의존관계를 주입할 때에도 "특정한 역할" 에만 의존하는게 '] 객체지향관점에서 더 좋다.

설정정보 사용 예시

  • 참고로, Test framework를 사용하지 않고, main method 를 사용하여 테스트 하는 예시다.

Spring을 사용 하기 전

  • 개발자가 AppConfig를 사용하여, 필요한 객체를 직접 조회
//Test unit을 사용하지 않은 test.
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);//Long type이라서 L붙여줘야함.
        memberService.join(member);
        //
        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());
    }
}

Spring 사용 후

  • Spring Container로부터 [ 이미 등록되어있는 Bean을 찾아와 ] 사용한다.
public class MemberApp {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = ac.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());
    }
}

ApplicationContext

  • Spring은 모든게 ApplicationContext라는 것으로 시작 (???)
  • 모든 객체들(Spring Bean) 을 관리해 준다.

AnnotationConfigApplicationContext(AppConfig.class);

  • @Configuration 어노테이션을 class-level에 가진 AppConfig.class를 인자로 전달
  • Appconfig 에 있는 설정정보를 갖고, Spring이 @Bean 붙은 것들을 [ Spring Container에 객체 생성 및 관리 ]하게 된다.

ApplicationContext로부터 Bean을 찾아올 수 있다.
ac.getBean("memberService", MemberService.class);

  • 이는 Spring Container에서 관리하고 있는 Bean을 가져오는 것.
  • 위의 코드는 [ 찾아오려는 Bean의 이름 ] 을 적어 준 것
    • Bean의 등록은 [ 기본적으로 ][ config파일에서 Bean을 생성하여 리턴하는 method이름 ] 으로 등록이 되어있다.
    • 이런식으로 [ 직접 Bean의 이름을 지정 ]도 가능
    @Bean(name="MemberSerViceBean") 

싱글톤 인스턴스 Bean

위의 MemberApp의 main method를 실행하면 아래와 같은 결과가 나온다.

  • 현재 bean은 싱글톤 scope으로 형성된다.(default) ( Bean 의 scope은 이와 다른 설정도 가능하다)
  • 위의 5개의 bean은 Spring 내부적으로 필요해서 등록하는 bean, 그 밑의 bean들은 AppConfig에서 직접 등록한 Bean들이다. -> method이름으로 등록됨을 볼 수 있다.

좋은 웹페이지 즐겨찾기