Inflearn sping 핵심원리 기본편 정리

김석진·2022년 1월 24일
0

작년도에 inflearn강의를 들으면서 spring 핵심원리에 대해서 공부한 것들을 github에 정리해둔것이다!

Spring의 등장 배경

Spring이 등장하기 이전 EJB(Enterprise JavaBeans)는 애플리케이션의 업무 로직을 담당하는 자바 표준 기술이었음.

EJB 등장으로 개발자는 비지니스 로직에 집중할수있는 환경 갖춤, but 하나의 기능을 구현하기 위해 클래스 간 상속, 인터페이스 구현등 클래스 간 의존도가 커지는 현상이 생김.

이로인해 ,마틴 파울러는 EJB에 반발해 간단한 자바오브젝트로 돌아가자라고 말을 하고 이는 POJO(Plain Old Java Object)라는 용어의 기원이됨

이러한 상황에서 2가지의 Open Source가 등장
1. 로드 존슨(Rod Johnson)의 J2EE Design and Development라는 책을 출간하고 거기의 3만 라인 이상의 예제 코드들이 지금의 Spring 기본 코드가 됨 -> EJB컨테이너의 대체
2. 개빈 킹의 하이버네이트 ORM(Object Relation Mapping) 프레임워크의 등장

  • EJB 엔티티 빈 기술을 대체, JPA(Java Persistence API)의 새로운 표준을 정의
  • JPA는 표준 인터페이스, 하이버네이트는 JPA의 구현체

스프링의 역사

  • 2002년 로드 존슨이 EJB의 문제점을 지적하면서 EJB없이도 충분히 고품질의 확장가능한 애플리케이션을 개발할수 있음을 보여주고, 3만 라인 이상의 기반 기술을 예제 코드로 선보임
  • 3만 라인의 예제코드에는 현재 스프링 핵심 개념과 기반 코드인 BeanFactory, ApplicationContext, POJO,IoC(제어의 역전), DI(의존성 주입)등이 포함이 됨
  • 책 출간 직후 유겐 휠러, 얀카로프가 오픈소스 프로젝트를 제안하여 J2EE(EJB)라는 겨울을 넘어 새로운 시작이라는 스프링이라는 프레임워크가 탄생

스프링이란?

  • 스프링은 어떤 특정한 하나가 아니라 여러 기술들의 모음

Spring 생태계


더 자세한 기능들은 Spring.io를 통해 알수있다.

Spring FrameWork

  • 핵심기술: 스프링 DI컨테이너,AOP,이벤트,기타
  • 웹기술: 스프링 MVC,스프링 WebFlux
  • 데이터 접근 기술: 트랜잭션,JDBC,ORM 지원,XML지원
  • 기술 통합: 캐시, 이메일, 원격접근,스케줄링
  • 테스트: 스프링 기반 테스트 지원
  • 언어 : 코틀린, 그루비
    최근에는 스프링 부트를 통해서 스프링을 편리하게 사용 할수있도록 지원

Spring Boot

  • 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
  • Tomcat 같은 웹 서버를 내장해서 별도의 웹서버를 설치 않아도 됨-> 예전에는 별도의 웹서버를 설치하여 복잡함이있었음
  • 손쉬운 빌드 구성을 위한 starter 종속성 제공
    (과거 Spring framework를 사용하기위해 라이브러리들을 다 다운받아야했음 ->Starter가 프로젝트를 구성하는 필요한 라이브러리를 자동으로 구성함으로써 편의성을 증대시킴)
  • 스프링과 3rd parth(외부) 라이브러리 자동 구성
    (스프링 프레임워크의 버전을 확인하고 해당 버전과 호환성이 좋은 외부 라이브러리를 자동으로 구성하므로 개발자가 다양한 라이브러리의 버전을 확인할 필요성을 없앰
    )
  • 메트릭, 상태확인,외부 구성 같은 프로덕션 준비기능 제공
    (운영환경에서 모니터링이 중요->Spring boot가 기본적으로 제공)
  • 관례에 의한 간결한 설정

    스프링 부트는 스프링 프레임워크를 사용하기위해 편리하게 도와주는 기술임

스프링 단어?

  • 스프링이라는 단어는 문맥에 따라 다르게 사용
  • 스프링 DI 컨테이너 기술
  • 스프링 프레임워크
  • 스프링 부트,스프링 프레임워크 등을 모두 포함한 스프링 생태계

스프링 만든 이유?

Spring의 핵심 개념

  • 스프링은 자바 언어 기반의 프레임워크!
  • 자바 언어의 가장 큰 특징- 객체 지향 언어
  • 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크
    결론) 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

좋은 객체 지향 프로그래밍 이란?

객체 지향 프로그래밍

  • 추상화

  • 캡슐화

  • 상속

  • 다형성

  • 컴퓨터 프로그램을 명렁어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 "객체"들의 모임을 파악하고자 하는것이다. 각각의 객체는 메세지를 주고받고 데이터를 처리할수 있다(협력)

  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 사용됨

유연하고,변경이 용이?

  • 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법(레고블럭 조립하듯이)
    -> 객체 지향의 핵심 다형성(Polymorphism)

다형성(Polymorphism)

  • 다형성이란 "여러 가지 형태를 가질 수 있는 능력"을 의미함, 간단히 역할과 구현으로 구분해 보자

    운전자 역할과 자동차 역할이있다. 자동차 하위에 K3, 아반떼 ,테슬라 모델3 이 있다.
    운전자는 자동차를 운전만하기만하면됨 ->자동차의 종류가 바뀌든 자동차역할만 한다면 운전자한테 문제가 없다.
    운전자를 클라이언트라고 생각하고 자동차를 서버라고 생각해보자

  • 클라이언트는 대상 역할(인터페이스)만 알면된다.

    • 운전자는 자동차의 역할만 알면된다.
  • 클라이언트(운전자)는 서버(자동차)의 내부구조를 알필요가 없다

    • 운전자는 자동차의 부품이 무엇으로 구성되어있는지 몰라도 됨
  • 클라이언트는 구현 대상 내부구조가 변동되어도 영향이 없다

    • 운전자는 자동차의 내부구조가 기름차-> 전기차로 변해도 영향이 없다
  • 클라이언트는 구현 대상 자체를 변경해도 영향이 없다
    - 운전자는 자동차의 종류가 변경되어도 운전자에게 영향이 없다.
    결론) 역할과 구현을 구분함-> 변경가능한 대체 가능성이 생김
    -> 유연하고 변경이 가능하다라는 뜻

역할과 구현을 분리

자바언어

  • 자바 언어의 다형성을 활용
    • 역할 = 인터페이스
    • 구현 = 인터페이스를 구현한 클래스, 구현 객체
  • 객체를 설계할 때 역할과 구현을 명확히 분리
  • 객체 설계시 역할(인터페이스)를 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만들기
  • 혼자 있는 객체는 없음
  • 클라이언트는 요청을 함, 서버는 응답
  • 수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가짐

자바 언어의 다형성

  • 오버라이딩(Overriding) 을 떠올려보자
  • 오버라이딩은 자바 기본 문법
  • 오버라이딩 된 메스드가 실행이된다
  • 다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할수 있다
  • 물론 클래스 상속 관계도 다형성,오버라이딩 적용가능
    의존한다?->쟤를 알고있다라고 생각하면됨


=> 클라이언트를 변경하지않고, 서버의 구현기능(JDBCMemberRepository,MemoryMemberRepository)을 유연하게 변경할수있다,

다형성의 본질

  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할수있다
  • 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야함

역할과 구현을 분리

  • 유연하고 변경이 용이
  • 확장 가능한 설계
  • 클라이언트에 영향을 주지 않는 변경 가능
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요(제일중요)

한계점

  • 역할(인터페이스) 자체가 변하면,클라이언트,서버 모두에 큰 변경이 발생
  • 인터페이스를 안정적으로 잘 설계하는것이 중요

오버로드란, 자바의 한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메소드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메소드를 정의할 수 있다. 즉 기존에없는 새로운 메소드를 추가하는것
오버라이딩(Overriding): 부모클래스로부터 상속받은 메소드를 자식클래스에서 재정의하는것, 자식클래스에서는 오버라이딩하고자하는 메소드 이름,매개 변수,리턴 값이 모두 같아야한다. 즉 상속받은 메소드를 재정의하는것


스프링과 객체 지향

  • 스프링은 다형성을 극대화해서 이용할 수 있게 도와준다
  • IoC(제어의역전),DI(의존관계 주입)은 다형성을 활용해서 역할구현을 편리하게 다룰 수 있도록 지원
  • 스프링을 사용하면 구현을 편리하게 변경할 수 있다.

좋은 객체 지향 설계의 5가지 원칙(SOLID) -면접에서 잘나오는부분 ㅋㅋ

SOLID

클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리한것

  • SRP:단일 책임 원칙(single responsibility principle)
  • OCP:개방-폐쇄 원칙(Open/closed principle)
  • LSP:리스코프 치환 원칙(Liskov substitution principle)
  • ISP:인터페이스 분리 원칙(Interface segregation principle)
  • DIP:의존관계 역전 원칙(Dependency inversion principle)

SRP 단일 책임 원칙(Single responsibility principle)

  • 한 클래스는 하나의 책임만 가져야한다.
  • 하나의 책임은 모호함(클수도 있고,작을수 있다, 문맥과 상황에 따라 다름)
  • 중요한 기준은 변경(변경이 있을때 파급효과가 적으면 SRP에 잘따른것)
  • 예) UI변경,객체의 생성과 사용을 분리
    -> 변경이 있을때 딱 한 클래스가 한지점만 고치면 SRP를 잘따른것

OCP 개방-폐쇄 원칙(가장 중요)

  • 소프트웨어 요소는 확장에는 열려있으나 ,변경에는 닫혀있어야함
    • 확장하려면 당연히 기존코드를 변경해야하는데..? 좀이상하네
  • 다형성을 활용해보자
  • 인터페이스를 구현한 새로운 클래스를 하나 만들어서(하나 만드는건 기존 코드를 바꾸는게 아님) 새로운 기능을 구현

  • 구현 객체를 변경하려면 클라이언트 코드를 변경해야한다
  • 분명 다형성을 사용했지만 OCP 원칙을 지킬수 없다.<-클라이언트에서 코드를 변경을했음
  • 해결방법) 객체를 생성하고, 연관관계를 맺어주는 별도의 조립,설정자가 필요함

LSP (리스코프 치환 원칙; Liskov substitution principle)

  • 프로그램의 객체는 프로그램의 정확성을 깨트리지 않으며서 하위 타입의 인스턴스를 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요하다
  • 예) 자동차 인터페이스의 엑셀은 앞으로 가라는기능, 뒤로가게 구현하면 LSP 위반

ISP (인터페이스 분리 원칙,Interface segregation principle)

  • 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다
  • 자동차 인터페이스-> 운전 인터페이스, 정비 인터페이스로 분리
  • 사용자 클라이언트-> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지않음
  • 인터페이스가 명확해지고(이 인터페이스가 뭐하는건지 기능을 명확하게 알수잇을듯)
    , 대체 가능성이 높아진다.

DIP (의존관계 역전 원칙, Dependency inversion principle)- 이거도 중요

  • 프로그래머는 "추상화에 의존해야지,구체화에 의존하면 안된다", 의존성 주입은 이 원ㅊㄱ을 따르는 방법 중 하나
  • 쉽게 이야기-> 구현 클래스에 의존하지 말고, 인터페이스 의존하라는 뜻
  • 앞에서 이야기한 역할(Role)에 의존해야한다는것과 같음. 객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할수있다.

    의존한다? 저 코드를 안다

정리

  • 객체 지향의 핵심= 다형성
  • 다형성 만으로는 쉽게 부품 갈아 끼우듯이 개발할수 X
  • 다형성 만으로는 구현 객체를 변경할때 클라이언트 코드도 함께 벼경됨
  • 다형성 만으로는 OCP, DIP를 지킬수 없다

객체 지향과 스프링

  • 스프링은 다음 기술로 다형성+ DIP,OCP를 가능하게 지원한다
    • DI(Dependency Injection):의존관계, 의존성 주입
    • DI 컨테이너 제공
  • 클라이언트 코드의 변경 없이 기능 확장-> 쉽게 부품 교체하듯이 개발가능

스프링 책 추천: 토비의 스프링
JPA 책 추천: 자바 ORM 표준 JPA 프로그래밍
객체 지향 책 추천: 객체지향의 사실과 오해
강의 다듣고 토비의 스프링 읽어보기


스프링 핵심 원리 이해1 -예제 만들기

비지니스 요구사항과 설계

  • 회원
    • 회원을 가입하고 조회할수있다
    • 회원은 일반과 VIP 두가지 등급이 있다
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다(미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다
    • 회원 등급에 따라 할인 정책을 적용할 수 있다
    • 할인 정책은 모든 VIP는 1000원 할인해주는 고정 금액 할인 적용해달라(변경가능)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못하고 오픈직전까지 고민을 미루고 싶다. 최악의 경우 할일을 적용하지 않을수 있다-> 미확정

요구사항을 보면 회원 데이터, 할인 정책ㄹ 같은 부분은 지금 결정하기 어려움
그렇다고 이런 정책이 결정될때까지 개발을 무기한 연기할 수 없다.
-> 객체 지향 설계 방법을 사용

회원 도메인 설계

  • 회원 도메인 요구사항
    • 회원을 가입하고 조회할수 있다(회원 서비스 회원가입,회원조회)
    • 회원은 일반과 VIP 두가지 등급
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다(미확정)->(회원저장소(인터페이스)를 따로만듬)

      회원 클래스 다이어그램(정적)
      회원 객체 다이어그램 (클라이언트객체가 실제 참조하는거를 보여줌,동적인부분)
      클라이언트-> 회원서비스(impl)-> 메모리 회원저장소
  • 회원서비스는 MemberServiceImpl

항상 개발할때는 위의 3구조로 설명을 함

회원 도메인 개발

등급을 먼저 만듬 등급은 enum으로 만듬(등급은 VIP,BASIC이있다)
;

  • 회원엔티티 (속성이 3개(id,name,grade)
  • 회원 저장소 인터페이스를 만듬(MemberRepository.interface)->인터페이스 구현체(MemberRepository.java)도 만들어줘야함


    동시성 이슈가 있을수 있어서 ConcurrentHashMap을 써야함 예제니까 hashmap을 씀
  • 회원서비스를 만듦(MemberService 인터페이스와 구현체를 만들자)

여기까지 회원 클래스 다이어그램을 만든것

실행해보기(APP형태로)

  • Spring 관련 코드를 하나도 안쓰고 JAVA로만 개발을 했다 하지만 애플리케이션 로직으로 메인메소드 테스트 실행을 하였다. 이것은 문제가 되는데 지금은 예제만 해서 그렇지 큰 프로젝트를 했을때는 문제가 생길수있다.
  • 테스트코드를 만들어서 원하는 기능들만 테스트를 해볼것이다.(예제에는 Junit을 사용)
  • 테스트가 실패를 했다면 빨간색으로 체크가 됐을것이다.

회원 도메인 설계의 문제점

  • 무엇이 문제일까?
  • 만약 다른 저장소로 변경할때 OCP 원칙을 잘 준수할까?
  • DIP를 잘 지키고 있을까?
  • 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음
    • -> 주문까지 만들고나서 문제점과 해결방안을 설명

주문과 할인 도메인 설계

  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다
    • 회원 등급에 따라 할인 정책을 적용할 수 있다
    • 할인 정책은 모든 VIP는 1000원 할인해주는 고정 금액 할인 적용해달라(변경가능)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못하고 오픈직전까지 고민을 미루고 싶다. 최악의 경우 할일을 적용하지 않을수 있다->미확정
  1. 주문생성:클라이언트(main or Controller)는 주문서비스에 주문 생성을 요청한다(회원 id,상품명, 상품가격)
  2. 회원조회: 할인을 위해서 회원 등급이 필요, 그래서 주문서비스는 회원 저장소에서 회원을 조회(findbyID)
  3. 할인적용: 주문서비스는 회원 등급에 따른 할인여부를 할인 정책에 위임
  4. 주문결과 반환: 주문 서비스는 할인결과를 포함한 주문 결과를 반환

    실제로는 주문데이터를 DB에 저장하겠지만 예제이기때문에 단순히 주문 결과를 반환

    역할과 구현을 분리함-> 자유럽게 구현 객체를 조립할수있다. 덕분에 회원 저장소는 물론이고 할인 정책도 유연하게 변경할수있다.



주문과 할인 도메인을 개발

  1. hello.core아래 discount 패키지를 생성
  2. 할인정책인터페이스를만듬(DiscountPolicy.interface)
  3. 할인정책인터페이스를 만들었으니 정액할인정책 구현체를 만듦
  4. hello.core아래 order패키지를 생성
  5. Order 도메인을 만듦
  6. Order서비스 인터페이스를 만듦
  7. OrderService 구현체를 만듦

주문과 할인 도메인 실행과 테스트

  1. 메인메소드에서 주문서비스 테스트
  • 메인메소드에서 테스트를 하는것은 좋은방법이아님-> 테스트 코드작성
  1. 테스트 코드를 생성

    단위테스트가 중요하다. (순수 자바코드로 테스트하는것)
  • 인터페이스와 구현체를 잘구분함 -> 다형성을 잘 이용함 -> 만약에 요구사항을 바꿨을때 객체지향적으로 잘 개발했을까? 클라이언트에 영향을 주는것인가 확인해봐야함!

새로운 할인 정책 개발

  • 새로운 할인 정책을 확장해보자(기획자가 나타나서 할인정책을 적용해야함ㅠㅠ)

  • RateDiscountPolicy 추가 (간단함)-RateDiscountPolicy만 추가로 개발하면됨

  1. 테스트케이스를 만들려면 만든 코드의 클래스명에 드래그한다음 ctrl+shift+T을 누르면 자동으로만들어줌
  2. 테스트 코드를 작성한다(성공인경우, 실패인경우 테스트할때는 성공과 실패 모두 테스트해야함)

  • 실패인경우 예측한(Expected)은 1000원이지만 실제값은 0원이기때문에 실패했다고 뜸 테스트 성공

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

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야한다.

  • 문제점 발견
    • 우리는 역할과 구현을 충실하게 분리함(O)
    • 다형성도 활용, 인터페이스와 구현 객체를 분리했다(O)
    • OCP,DIP같은 객체지향 설계 원칙을 준수했는가?->그렇게 보이지만 X
    • DIP: 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨것 같은데...
      - 클래스 의존관계를 분석해보자, 추상(인터페이스)뿐만 아니라 구체(구현)클래스에도 의존하고 있다
      - 추상(인터페이스) 의존: DiscountPolicy
      - 구체(구현_ 클래스: FixDiscountPolicy, RateDiscountPolicy

    • OCP:변경하지 않고 확장할수 있다고 햇는데
      • 지금 코드는 기능을 확장해서 변경하면,클라이언트 코드에 영향을 줌 (OCP를 위반)
      • FixDiscount에서 RateDiscount로 코드를 변경하는순간 OCP위반!

어떻게 문제를 해결할수 있을까

  • 클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy의 인터페이스 뿐만아니라 구체클래스도 함꼐 의존
    • 그래서 구체 클래스를 변경할때 클라이언트 코드도 함께 변경해야한다
  • DIP위반 -> 추상에만 의존하도록 변경(인터페이스에만 의존하도록)
  • DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면된다.

    인터페이스에만 의존하도록 코드를 변경

  • 인터페이스에만 의존하도록 설계와 코드를 변경
  • 그런데 구체가 없는데 어떻게 코드를 실행할수있을까?
    • 실제 실행 해보면 NPE(Null Point Exception)가 발생한다.

해결방법

  • 이문제를 해결하려면 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy 의 구현객체를 대신 생성하고 주입하면 해결이된다

관심사의 분리(중요)

관심사를 분리하자

  • 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야한다.
  • 디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연할수 있어야함
  • 공연을 구성하고 담당배우를 섭외, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 있어야한다
  • 공연기획자를 만들고, 배우가 공연 기획자의 책임을 확실히 분리

AppConfig 등장

  • 애플리케이션의 전체 동장 방식을 구성(config)하기 위해 '구현 객체를 생성' 하고,"연결"하는 책임을 가지는 별도의 설정 클래스를 만들자
  • AppConfig는 애플리케이션의 실제 동작에 필요한 '구현객체를 생성'한다
    • MemberServiceImpl
    • MemoryMemberRepositoty
    • OrderServiceImpl
    • FixDiscountPolicy
  • AppConfig는 생성한 객체 인스턴스의 참조(래퍼런스)를 생성자를 통해서 주입 연결해준다.

    - MemberServiceImpl에는 MemoryMemberRepository가 들어가고
    - OrderServiceImpl에는 MemoryMemberRepository와 FixDiscountPolicy가 들어간다

    MemberServiceImpl- 생성자 주입

  • 설계변경으로 MemberServiceImpl은 MemoryMemberRepository 를 의존하지 않는다
  • 단지 MemberRepository 인터페이스만 의존한다
  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현객체가 들어올지는 알수없다
  • MemberService의 생성자를 통해서 어떤 구현객체가 들어올지는 AppConfig가 결정한다
  • 이제 MemberServiceImpl은 의존관계에 대한 고민은 외부에 맞기고 실행에만 집중하면된다. \
  • 객체의 생성과 연결은 AppConfig 가 담당
  • DIP완성 MemberServiceImpl은 MemberRepository인 추상에만 의존하면된다
  • 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리됐다.

  • appconfig객체는 memoryMembewrRepository를 생성하고 memberServiceImpl을 생성하면서 생성자로 전달한다
  • 클라이언트인 memberServiceImpl입장에서 의존관계를 마치 주입하는것과 같다고해서 DI(dependency injection)이라고 한다

OrderServiceImpl-생성자 주입

  • 설계변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지않는다
  • 단지 DiscountPolicy 인터페이스에만 의존한다
  • OrderServiceImpl에는 MemoryMemberRepository와 FIxDiscountPolicy에만 집중하면된다
  • MemberAPP 코드 수정
  • OrderAPPP 수정

  • 테스트 코드 수정

정리

  • AppConfig를 통해서 관심사를 확실하게 분리
  • 배역,배우를 생각해보자, AppConfig는 공연 기획자이다
  • AppConfig는 구체 클래스를 선택한다, 배역에 맞는 담당배우를 선택한다, 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다
  • 이제 각 배우들은 담당 기능을 실행하는 책임만 지면된다

Appconfig 리팩터링

현재 AppConfig를 보면 중복이 있고, '역할'에 따른 '구현'이 잘안보인다.


  • new MemoryMemberRepository() 이부분이 중복제거됨->MemoryMemberRepository를 다른구현체로 변경할때 한 부분만 변경하면된다.
  • Appconfig를 보면 역할과 구현 클래스가 한눈에 들어온다. -> 애플리케이션 전체 구성이 어떻게 되었는지 빠르게 파악할수있다.

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

  • AppConfig에서 역할과 구현을 구분했기때문에 할인 정책을 새롭게 적용하려면 AppConfig에서 할인 역할부분만 수정하면된다.
  • AppConfig때문에 사용영역과 구성영역으로 역할이 구분되었다.
  • 이제 할인 정책을 변경해도 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면됨. 클라이언트코드인 OrderServiceImpl를 포함해서 사용영역의 어떤 코드도 변경할 필요는 없다.

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

여기서 3가지 SRP,DIP,OCP 적용

SRP 단일 책임 원칙

  • 클라이언트 객체는 직접 구현 객체를 생성,연결,실행하는 다양한 책임을 가지고 있음
  • SRP 단일 책임원칙을 따르면서 관심사 분리-> 구현 객체를 생성,연결하는 책임은 AppConfig가 담당
  • 클라이언트 객체는 실행하는 책임만 담당

DIP 의존관계 역전 원칙

  • 새로운 할인 정책을 개발하고,적용하려고 하니 클라이언트 코드도 변경해야했다, 왜냐면 기존 클라이언트 코드(OrderServiceImpl)은 DIP를 지키며 DiscountPolicy 추상화 인터페이스에 의존하는것 같았지만 FixDiscountPolicy 구체화 구현 클래스에도 함께 의존했음
  • 클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경-> 하지만 클라이언트코드는 인터페이스만으로 아무것도 실행할수없었따
  • AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입->DIP원칙을 적용하고 위의 문제를 해결

OCP

  • 다형성 사용하고 클라이언트가 DIP를 지킴
  • 애플리케이션 사용 영역과 구성영역으로 나눔
  • AppConfig가 의존관계를 FixDiscountPolicy를 RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도됨
  • 소프트웨어 요소를 새롭게 확장해도 사용영역의 변경은 닫혀있엇음!
    변경이 닫혀있다?는 변경할 필요가없다는 뜻

IoC,DI,컨테이너

제어의 역전IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결,실행 했다.
    -> 한마디로 구현 객체가 프로그램의 제어흐름을 스스로 조종(개발자 입장에서는 자연스러운 흐름)
  • AppConfig 등장한 이후 구현객체는 자신의 로직을 실행하는 역할만 담당 프로그램의 제어흐름을 이제 AppConfig가 가져간다.
    예를 들어 OrderServiceImpl은 필요한 인터페이스들을 호출하지만 구현 객체들이 실행될지는 모름
  • 프로그램에 대한 제어흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl도 AppConfig가 생성을 함, 그리고 AppConfig는 OrderServiceImpl이 아닌 OrderService 인터페이스의 다른 구현객체를 생성하고 실행할수 있다.
  • 이렇듯 프로그램의 제어흐름을 직접 제어하는것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라고 함

프레임워크 VS 라이브러리

  • 프레임워크가 내가 작성한 코드를 제어하고,대신 실행하면 그것은 프레임워크가 맞음(Junit)
  • 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다

의존관계주입 DI(Dependency Injection)

  • OrderServiceImpl은 DiscountPolicy 인터페이스에 의존,실제 어떤 구현객체가 사용될지는 모름
  • 의존관계는 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체(인스턴스)의존관계 들을 분리해서 생각해야함

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

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

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

IoC컨테이너, DI컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는것을 IoC컨테이너 또는 DI컨테이너라고함
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI컨테이너라고 함

스프링으로 전환

지금까지 순수한 자바 코드만으로 DI를 적용, 이제 스프링을 사용해보자

AppConfig를 스프링 기반으로 변경하기

memberapp 스프링적용(결과)

orderapp 스프링적용(결과)

  • 코드를 실행하면 스프링 관련 로그가 몇줄 실행 되면서 기존과 동일한 결과가 출력됨

스프링 컨테이너

  • ApplicationContext를 스프링 컨테이너라고 함
  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만,이제부터는 스프링 컨테이너를 통해서 사용
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성)정보로 사용,여기서 @Bean이라고 적힌 메서드들 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
  • 스프링 빈은 @Bean이 붙은 메서드 명을 스프링빈의 이름으로 사용한다{memberService, orderService}
  • 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야한다. 스프링빈은 applicationContext.getBean() 메서드를사용해서 찾을 수 있다.
  • 기존에는 개발자가 직접 자바코드로 모든것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링빈으로 등록하고 스프링컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.

-> 스프링 컨테이너를 사용하면 어떤장점이 있는지는 아직모르겟다 복잡해진거같은데.. 아직 헷갈리는 부분이다.


스프링 컨테이너와 스프링 빈

스프링 컨테이너 생성

스프링 컨테이너가 생성되는 과정
// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigurationContext(AppConfig.class);

  • ApplicationContext를 스프링 컨테이너라 한다(ApplicationContext는 인터페이스임-> 다형성 적용가능)
  • 스프링컨테이너는 XML 기반으로 만들수잇고, 애노테이션 기반의 자바 설정 클래스로 만들수 있다.
  • 직전에 AppConfig를 사용했던 방식이 애노테이션 기반의 자바설정클래스로 스프링 컨테이너를 만든것

스프링 컨테이너의 생성 과정

  1. 스프링 컨테이너 생성
  • key는 빈이름 값은 빈 객체가 됨
  1. 스프링 빈 등록
  • 빈이름
    • 빈이름은 메서드 이름을 사용
    • 빈이름을 직접 부여할수도 있다. (@Bean(name="memberService2))
  • 빈 이름은 항상 다른이름을 부여해야함(같은이름은 다른빈이 무시되거나 기존 빈을 덮어버리거나 오류가 뜰수도)
  1. 스프링 빈 의존관계 설정- 준비
  • 위에서 객체를 생성을 했다. 의존관계를 준비한다
  1. 스프링 빈 의존관계 설정 -완료
  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다
  • 단순한 자바 코드를 호출하는것 같지만,차이가있음

정리
스프링 컨테이너를 생성하고,설정(구성)정보를 참고해서 스프링 빈도 등록하고, 의존관계도 설정함

컨테이너에 등록된 모든 빈 조회

스프링 컨테이너에 제대로 등록됐는지확인

  • 모든 빈 출력하기
    • 실행하면 스프링에 등록된 모든 빈정보를 출력할수 있다.
    • ac.getBeanDefintionNames(): 스프링에 등록된 모든 빈 이름을 조회한다.
    • ac.getBean(): 빈 이름으로 빈 객체(인스턴스)를 조회한다
  • 애플리케이션 빈 출력하기
    • 스프링이 내부에서 사용하는 빈을 제외하고, 내가 등록한 빈만 출력하기
    • 스프링이 내부에서 사용하는 빈은 getRole()로 구분할수있다.
    • ROLE_APPLICATION:일반적으로 사용자가 정의한 빈
    • ROLE_INFRASTRUCTURE:스프링이 내부에서 사용하는빈

스프링 빈 조회- 기본

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인방법

  • ac.getBean(빈이름,타입)
  • ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 예외 발생

스프링 빈 조회-동일한 타입이 둘이상인 경우

  • 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다 -> 이때는 빈 이름을 지정
  • ac.getBeanOfType()을 사용하면 해당 타입의 모든 빈을 조회할수있다.


스프링 빈 조회-상속관계(중요)

  • 부모타입으로 조회하면 자식타입도 함꼐 조회한다.
  • 그래서 모든 자바 객체의 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.
    (자바는 최상위 부모는 Object임)



BeanFactory와 ApplicationContext

BeanFactory와 ApplicationContext에 대해서 알아보자

BeanFactory

  • 스프링 컨테이너의 최상위의 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 담당
  • getBean()등을 제공
  • 지금까지 했던것 대부분의 기능은 BeanFactory가 제공하는 기능
    ApplicationContext
  • BeanFactory 기능을 모두 상속받아서 제공
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해줌 둘의 차이점은?
  • 애플리케이션을 개발할 때는 빈은 관리하고 조회하는 기능은 물론이고,수많은 부가기능이 필요

ApplicationContext가 제공하는 부가기능이 존재한다

  • 메세지소스를 활용한 국제화 기능
    • 예를 들어 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수
    • 로컬,개발,운영등을 구분해서 처리
  • 애플리케이션 이벤트
    • 이벤트를 발행하고 구독하는 모델을 관리하게 지원
  • 편리한 리소스 조회
    • 파일,클래스패스,외부 등에서 리소스를 편리하게 조회

      정리

      • ApplicationContext는 BeanFactory의 기능을 상속받음
      • ApplicationContextsms 빈 관리기능+편리한 부가 기능을 제공
      • BeanFactory를 직접 사용할 일은 거의 없음!, 부가기능이 포함된 ApplicationContext를 사용
      • BeanFactory나 ApplicationContext를 스프링 컨테이너라 함

다양한 설정 형식 지원- 자바 코드, XML

  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.
    • 자바 코드, XML, Groovy등등

애노테이션 기반 자바 코드 설정 사용

  • 지금까지 했던것
  • new AnnotationConfigApplicationContext(AppConfig.class)
  • AnnotationConfigApplicationContext 클래스를 사용하면서 자바코드로된 설정 정보를 넘기면됨

XML 설정 사용

  • 최근에는 스프링 부트를 많이 사용하면서 XML 기반의 설정을 잘 사용하지 않는다. 아직많은 레거시 프로젝트들이 XML로 되어있고, XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한번쯤 배워두는 것도 괜찮다.
  • GenericXMLApplicationContext를 사용하면서 xml 설정파일을 넘기면된다.


    appConfig.xml과 appConfig.java를 비교를 해보면 구조와 모습이 똑같음을 알수있다.

    테스트코드를 진행을 해보면 자바코드 설정을 사용한것과 동일하게 실행 됨을 볼수있다. 이렇게 실습을 한 이유는 자바코드로 설정된 정보를 XML 설정으로도 만들수있고 구조에대해서도 어떻게 생긴건지 알수있기때문이다.

스프링 빈 설정 메타정보-BeanDefinition

  • 스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까? 그 중심에는 BeanDefinition(인터페이스) 이라는 추상화가 있다.
  • 쉽게 이야기 해서 역할과 구현을 개념적으로 나눈것
    • XML을 읽어서 BeanDefinition을 만들면 된다
    • 자바 코드를 읽어서 BeanDefinition을 만들면 된다
    • 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefintion만 알면됨
  • BeanDefinition 은 빈 설정 메타정보라 한다
    • @Bean, 당 각각 하나씩 메타 정보가 생성됨
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성함

    코드 레벨로 좀더 깊이 있게 들어가본다.
  • AnnotationConfigApplicationContext는 AnnotationBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성
  • GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml을 읽고 BeanDefinition을 생성
  • 새로운 형식의 설정 정보가 추가되면 XxxxBeanDefintionReader를 만들어서 BeanDefinition을 생성하면 됨

BeanDefinition 살펴보기

BeanDefinition 정보

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예)memberService
  • Scope: 싱글톤(기본값)
  • lazylint: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
  • InitMethodName:빈을 생성하고,의존관계를 적용한 뒤에 호출되는 초기화 메소드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메소드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)

    정리

    • BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수 도 있다. 하지만 실무에서는 BeanDefinition을 직접 정의하거나 사용하는 일은 거의 없다
    • BeanDefinition에 대해서는 너무 깊이있게 이해하기 보다는, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 됨
    • 가끔 스프링 코드나 스프링 관련 오픈 소스의 코드를 볼때, BeanDefinition 이라는 것이 보일때가 있다, 이때 이러한 매커니즘을 떠올리면됨

웹 애플리케이션과 싱글톤

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이다, 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할수 있다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.


각각의 고객이 요청을하면 객체가 총 3개가 생성이됨->문제 웹애플리케이션은 계속요청을 받음->계속 객체를 만들어야함

  • 스프링이 없는 순수한 DI 컨테이너 테스트

    memberService1,memberService2가 다른게 생성됨을 알수있다(총 객체가 memberService객체2개 memberRepository 2개가 생김).->이런식으로 요청때마다 계속 객체를 생성하면 비효울적임

정리

  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸됨-> 메모리 낭비가 심하다
  • 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다-> 싱글톤 패턴

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다
  • 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야함
    • private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야함


  • 1.static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
  • 2.이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환
  • 3.딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.(제일 중요함)

테스트

-> 호출할때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.

싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수있다. 하지만 싱글톤 패턴은 다음과 같은 수많은 문제점들을 가지고 있다

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 존재
  • 의존관계상 클라이언트가 구체 클래스에 의존한다->DIP를 위반
  • 클라이언트가 구체 클래스에 의존해서 OCP원칙을 위반할 가능성이 높다.
  • 테스트 하기 어렵다
  • 내부 속성을 변경하거나 초기화 하기 어렵다
  • private 생성자로 자식 클래스를 만들기 어렵다
  • 결론적으로 유연성이 떨어짐
  • 안티패턴으로 불리기도 함

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다. 지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
    • 이전에 설명한 컨테이너 생성과정을 자세히 보자, 컨테이너는 객체를 하나만 생성해서 관리함
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할수 있다.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP,OCP,테스트,private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.

스프링 컨테이너를 사용하는 테스트 코드


99퍼는 singletonBean을 사용 가끔 빈 스코프를 사용

싱글톤 방식의 주의점 (실무에서 중요함)

  • 싱글톤 패턴이나, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 그래서 무상태(stateless)로 설계해야 함
    • 특정 클라이언트에 의존적인 필드가 있으면 안됨(특정 클라이언트가 값을 바꾸면안됨)
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됨
    • 가급적 읽기만 가능해야 함-> 즉 수정하게 하면안됨
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 함
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할수 있다.




@Configuration과 싱글톤

그런데 이상한점이 있음!, AppConfig 코드를 확인해보자

  • @Bean memberService 빈을 만드는 코드를 보면 memberRepository()를 호출
    • 이 메서드를 호출하면 new MemoryMemberRepository()을 호출
  • @Bean orderService빈을 만드는 코드를 보면 memberRepository()를 호출
    • 이 메서드를 호출하면 new MemoryMemberRepository()을 호출

결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는게 아닌가? 라는 의구심이든다(스프링컨테이너가 어떻게 처리하는지 궁금)-> 테스트 코드로 돌려보기

  • 확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용
  • AppConfig의 자바 코드를 보면 분명 각각 2번 new MemoryMemberRepository 호출해서 다른 인스턴스가 생성 되어야 하는데 뭘까-> 확인해보기


@Configuration과 바이트코드 조작의 마법

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자바코드까지 어떻게 하기는 어렵다. 자바코드를 보면 memberRepository가 분명 3번 호출되야하는게 맞음 그래서 스프링은 클래스와 바이트코드를 조작하는 라이브러리를 사용한다
모든 비밀은 @Configuration을 적용한 AppConfig에 있다.


참고 AppConfig@CGLIB은 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다.

@Configuration을 적용하지않고 @Bean 만 적용하면 어떻게 될까

@Configuration을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean 만 적용하면 어떻게 될까 => 결론은 된다 but 문제가 발생

다른 싱글톤이 깨짐 memberRepository가 3번 호출이됨 1번은 @Bean에 의해 스프링 컨테이너에 등록하기 위해서이고 2번은 각각 memberRepository()를 호출하면서 발생한 코드

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만,싱글톤을 보장하지 않는다
    • memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할때 싱글톤을 보장하지 않는다
  • 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration을 사용하자

컴포넌트 스캔

컴포넌트 스캔과 의존관계 자동 주입 시작하기

  • 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.
  • 예제에서 몇개가 안되었지만, 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생한다.
  • 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
  • 또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.

  • 컴포넌트 스캔을 사용하려면 먼저 @ComponentScan을 설정 정보에 붙여주면 된다.
  • 기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다.

    이제 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 붙여줌
  • 이전에 AppConfig에서는 @Bean으로 직접 설정 정보를 작성했고, 의존관계도 직접 명시를 했다. 이제는 이런 설정 정보 자체가 없기때문에 의존관계 주입도 이 클래스 안에서 해결해야함
  • @Autowired는 의존관계를 자동으로 주입해준다.

OrderServiceImpl에도 위와 같이 @Component@Autowired를 추가해준다.

  • @Autowired를 사용하면 생성자에서 여러 의존관계도 한꺼번에 주입받을 수 있다.
  • AnnotationConfigApplicationContext를 사용하는것은 기존과 동일
  • 설정 정보로 AutoAppConfig 클래스를 넘겨줌
  • 실행해보면 잘되는것을 확인할수잇다.
    로그를 잘보면 컴포넌트 스캔이 잘 동작함을 알수있음

컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지에 대한 그림
1. @ComponentScan

  • @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록
  • 이때 스프링 빈의 기본이름은 클래스명을 사용, 맨앞글자만 소문자로 사용
    • 빈 이름 기본 전략:MemberServiceImpl 클래스->memberServiceImpl
    • 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶음녀 @Component("memberService2")이런식으로 이름을 부여하면 됨
  1. @Autowired의존관계 자동 주입
  • 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입
  • 이때 기초 조회 전략은 타입이 같은 빈을 찾아서 주입한다
    • getBean(MemberRepository.class)와 동일하다고 이해하면 된다.
  • 생성자에 파라미터가 많아도 다 찾아서 자동으로 주입함

탐색 위치와 기본 스캔 대상

탐색할 패키지의 시작 위치 지정

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래걸림-> 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

  • hello.core부터 컴포넌트 스캔을 한다는 뜻이다. 예) hello.core.member를 하면 member만 컴포넌트 스캔을함
  • basePackages: 탐색할 패키지의 시작 위치를 지정함, 이 패키지를 포함해서 하위 패키지를 모두 탐색
    • basePackages={"hello.core","hello.service"} 이렇게 여러 시작 위치를 지정할 수 있다.
  • basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위로 지정할 수 있다.
  • 지정을 하지않으면(defualt)는 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 되어 hello.core밑에 있는 폴더들을 전부다 컴포넌트 스캔을 함

권장하는 방법
패키지 위치를 위에서 처럼 지정하지 않고, 설정 정보 클래스의 위치를 최상단에 두는것, 최근 스프링 부트도 이 방법을 기본으로 제공 함

컴포넌트 스캔 기본 대상

컴포넌트 스캔은 @Component 뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함한다.

  • @Component: 컴포넌트 스캔에서 사용
  • @Controller: 스프링 MVC 컨트롤러에서 사용
  • @Service: 스프링 비지니스 로직에서 사용
  • @Repository: 스프링 데이터 접근 계층에서 사용
  • @Configuration: 스프링 설정 정보에서 사용

참고: 사실 애노테이션에는 상속관계라는 것이 없다. 그래서 이렇게 애노테이션이 특정 애노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지우너하는 기능은 아니고, 스프링이 지원하는 기능이다.

컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행함

  • @Controller: 스프링 MVC 컨트롤러로 인식
  • @Repository: 스프링 데이터 접근 계층으로 인식, 데이터 계층의 예외를 스프링 예외로 변환해줌
  • @Configuration: 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 함
  • @Service: 사실 @Service는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비지니스 로직이 여기에 있겠구나라고 비지니스 계층을 인식하는데 도움이 된다.

참고: useDefaultFilters 옵션은 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외됨

필터

  • includeFilters: 컴포넌트 스캔 대상을 추가로 지정한다

  • excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정한다

    모든 코드는 테스트 코드에 추가

  • 컴포넌트 스캔 대상에 추가할 애노테이션

  • 컴포넌트 스캔 대상에 제외할 애노테이션

  • 컴포넌트 스캔 대상에 추가할 클래스

  • @MyincludeComponent 적용

  • 컴포넌트 스캔 대상에 제외할 클래스

  • @MyExcludeComponent 적용

  • 설정 정보와 전체 테스트 코드

  • includeFilters 에 MyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다

  • excludeFilters 에 MyExcludeComponent 애노테이션을 추가해서 BeanB가 스프링 빈에 등록되지 않는다.

FilterType 옵션

FilterType은 5가지 옵션이 있다.

  • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
    • ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    • ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용
    • ex) org.example..*Service+
  • REGEX: 정규 표현식
    • ex) org.example.Default.*
  • CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
    • ex) org.example.MyTypeFilter

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈이름을 등록하면 발생하는 두가지 상황
1. 자동 빈 등록 vs 자동 빈 등록
2. 수동 빈 등록 vs 자동 빈 등록

자동 빈 등록 vs 자동 빈 등록

  • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.
    • ConflictingBeanDefinitionException 예외발생

수동 빈 등록 vs 자동 빈 등록


이 경우 수동 빈 등록이 우선권을 가짐
(수동 빈이 자동 빈을 오버라이딩 해버림)
물론 개발자가 의도적으로 이런 결과를 기대했다면 수동이 자동보다 우선권을 가지는것이 좋다. 하지만 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는 여러 설정들이 꼬여서 이런 결과가 만들어지는 경우가 대부분.
그러면 정말 잡기 어려운 버그가 만들어진다. 항상 잡기 어려운 버그는 애매한 버그이다.
그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾼다.

스프링 부트인 CoreApplication을 실행해보면 오류를 볼 수 있다.


의존관계 자동 주입

다양한 의존관계 주입 방법

의존관계 주입은 크게 4가지 방법이 있다.
1. 생성자 주입
2. 수정자 주입(setter 주입)
3. 필드 주입
4. 일반 메서드 주입

생성자 주입(요즘 자주쓰이는것)

  • 이름 그대로 생성자를 통해서 의존 관계를 주입하는 방법(지금까지 했던것)
  • 특징
    • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장
    • 불변,필수 의존관계에 사용

      final은 값은 꼭있어야한다는 뜻임(세팅을 해야한다는뜻)
      중요한 부분임 생성자가 딱 1개만 있으면, @Autowired를 생략해도 자동 주입된다. 물론 스프링 빈에만 해당됨

수정자 주입(setter 주입)

  • setter라고 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
  • 특징
    • 선택,변경 가능성이 있는 의존관계에 사용
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

      참고: @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required=false)로 지정하면된다.

필드 주입

  • 필드에 바로 주입하는 방식
  • 특징
    • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 사용하지 말자..!(아래는 사용가능 한 부분)
      - 애플리케이션의 실제 코드와 관계 없는 테스트 코드에서 사용가능
      - 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

일반 메서드 주입

  • 일반 메서드를 통해서 주입 받을 수 있다.
  • 특징
    • 한번에 여러 필드를 주입 받을 수 있다.
    • 일반적으로 잘 사용하지 않는다.

옵션처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
@Autowired만 사용하면 required 옵션의 기본값이 true로 되어있어서 자동 주입 대상이 없으면 오류가 발생

자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력됨
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

  • 정리
    • Member는 스프링 빈이 아니다.
    • setNoBean()은 @Autowired(required=false)이므로 호출 자체가 안된다.

생성자 주입을 선택

과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장함

이유

  • 불변

    • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점 까지 의존관계를 변경할 일이 없음, 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안됨(불변해야한다는뜻)
    • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다.
    • 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아님.
    • 생성자 주입은 객체를 생성할 때 딱 한번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
  • 누락

    • 프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에 다음과 같이 수정자 의존관계인 경우

  • final 키워드
    생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아준다.

정리

  • 생성자 주입 방식을 선택하는 이유는 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 함
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 됨. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
  • 항상 생성자 주입을 선택하자, 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라, 필드 주입은 사용하지 않는것이 좋다.

롬복과 최신 트렌드

막상 개발을 하면 대부분이 불변이고 생성자에 final 키워드를 사용하게 됨
그런데 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야하고 필드 주입처럼 편리하게 사용하는 방법을 알아야함

  1. Prefrences(윈도우 File Settings) plugin lombok 검색 설치 실행 (재시작)
  2. Prefrences Annotation Processors 검색 Enable annotation processing 체크 (재시작)
  3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인

    롬복은 getter setter를 Annotation으로 선언하면 자동으로 getXxx,setXxx를 만들어준다.
    실무에서 많이 쓰임

이제 기존코드를 수정해보기

롬복을 적용을 하면

롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. (다음 코드에는 보이지 않지만 실제 호출 가능하다.)
이렇게 하면 의존관계 추가할때도 편리하고 실무에서 많이 사용이 되는 기능이다.

정리
최근에는 생성자를 딱 1개 두고, @Autowired를 생략하는 방법을 주로 사용함, 여기에 Lombok 라이브러리의 @RequireArgsConstructor 함게 사용하면 기능은 다 제공하고, 코드는 깔끔하게 사용할 수 있다.

조회 빈이 2개 이상 -문제

@Autowired 는 타입(Type)으로 조회한다.

타입으로 조회하기 때문에 마치 다음 코드와 유사하게 동작(실제로는 더많은 기능을 제공)
ac.getBean(DiscountPolicy.class)

스프링 빈 조회해서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생
DiscountPolicy의 하위타입인 FixDiscountPolicy, RateDiscountPolicy 둘다 스프링 빈으로 선언해보자

이때 하위 타입으로 지정할 수 도 있지만, 하위 타입으로 지정하는 것은 DIP를 위배, 유연성이 떨어짐
이름만 다르고, 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안됨.
스프링 빈을 수동 등록해도 문제가 해결되지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있음

@Autowired 필드명, @Quilifier,@Primary

해결 방법
조회 대상 빈이 2개 이상일 때 해결 방법

  • @Autowired 필드 명 매칭
  • @Quilifier->@Quilifier끼리 매칭->빈 이름 매칭
  • @Primary 사용

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름 이나 파라미터 이름 으로 빈 이름을 추가 매칭한다.


필드명이 rateDiscountPolicy 이므로 정상 주입됨
필드 명 매칭은 먼저 타입 매칭을 시도 하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능

@Autowired 매칭 정리
1. 타입 매칭
2. 타입 매칭의 결과가 2개 이상일 때 필드 명,파라미터 명으로 빈 이름 매칭

@Quilifier 사용

@Quilifer는 추가 구분자를 붙여주는 방법, 주입시 추가적인 방법으로 제공하는 것이지 빈 이름을 변경하는것은 X



@Quilifier로 주입할때 @Quilifier("mainDiscountPolicy")를 못찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
직접 빈 등록시에도 @Quailifier를 동일하게 사용할 수 있다.

@Quilifier 정리
1. @Quilifier 끼리 매칭
2. 빈이름 매칭
3. NoSuchBeanDefinitionException 예외발생

@Primary 사용(자주사용됨)

@Primary는 우선순위를 정하는 방법, @Autowired시에 여러 빈이 매칭되면 @Primary가 우선권을 갖음
rateDiscountPolicy가 우선권을 갖도록 함

코드를 실행해보면 @Primary가 잘 동작함
@Quilifier의 단점은 주입 받을때 모든 코드에 @Quilifier를 붙여줘야함 반면에 @Primary를 사용하면 @Quilifier를 붙일 필요가 없음

@Primary,@Quilifier 활용
코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자, 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Quailifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득 할 때는 @Quailifier를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다. 물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것은 상관없다.

  • 우선순위
    • @Primary는 기본값 처럼 동작하는것 ,@Quilifier는 매우 상세하게 동작함
    • 스프링은 자동보다는 수동이 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선권이 높음-> @Quilifier가 우선권이 높다.

애노테이션 직접 만들기

profile
주니어 개발자 되고싶어요

0개의 댓글

관련 채용 정보