Skip to main content

스프링 핵심 원리 - 기본편

본 내용은 김영한님의 스프링 핵심원리 - 기본편을 정리한 내용입니다.

(또한 제가 이해한 수준에서 정리한 내용이기 때문에 김영한님 강의의 모든 내용을 포함하지 않습니다.)

Spring을 직접 사용하기 이전에 앞서 히스토리와 개념들을 파악해보자!

스프링이 나오게된 배경

→ 스프링 이전 자바진영의 표준 웹개발(?) 기술은 EJB 였다.

→ ORM, 분산 처리 지원 등 다양한 기술을 지원했지만 문제가 많았다.

→ 로드 존슨이라는 개발자가 EJB의 문제점을 지적하고 어떻게 해결할 수 있는지에 대한 을 출간함.

→ 이 책에 스프링의 핵심 기술들이 들어가 있음.

→ 책 출간 직후 유겐 휠러, 얀 카로프가 오픈소스 프로젝트를 제안했으며 유겐 휠러가 지금도 상당 수의 스프링 코드를 개발 중.

→ 이후로 스프링, 스프링 부트, 스프링 리엑티브 등 다양한 프로젝트가 나오며 지금까지 계속해서 발전 중.

스프링 프레임워크

  • 핵심 기술
    • 스프링 DI 컨테이너
    • AOP
    • 이벤트
    • 기타
  • 웹 기술
    • 스프링 MVC
    • 스프링 WebFlux
  • 데이터 접근 기술
    • 트랜잭션
    • JDBC
    • ORM 지원
    • XML 지원
  • 기술 통합
    • 캐시
    • 이메일
    • 원격접근
    • 스케줄링
  • 테스트
    • 스프링 기반 테스트 지원
  • 언어
    • 코틀린
    • 그루비

위 목록의 다양한 프로젝트를 통합하여 스프링 프레임워크라고 하며, 이러한 스프링 프레임워크(하위의 다양한 프로젝트들)을 편리하게 사용할 수 있게 하는 기술이 “스프링 부트” 이다.

스프링 부트

  • 톰캣과 같은 웹 서버를 내장해서 별도의 웹 서버 설치 필요 X
  • 스프링과 외부 라이브러리 자동 구성 (예전에는 버전끼리 호환이 안되는 문제가 빈번하게 발생했다고 함.)
  • 설정의 간편화

스프링의 핵심 개념(컨셉)

객체 지향의 특징을 가장 잘 활용하여 어플리케이션을 개발할 수 있게 하자.

좋은 객체 지향 설계란 ?!

유연하게 확장하고 수정할 수 있게 하는 것.

이를 위한 핵심 개념: 다형성

객체는 서로 얽혀서 협력한다.

역할과 구현으로 바라보자.

한계

역할(인터페이스) 자체가 변하면 클라이언트(요청 객체)와 서버(응답 객체) 모두에 큰 변경이 발생한다.

따라서 인터페이스를 안정적으로 잘 설계한는 것이 중요하다.

스프링과 객체 지향

  • 다형성이 가장 중요
  • 스프링은 다형성을 극대화해서 이용할 수 있게 도와준다.
  • 이를 위한 기술이 IoC, DI
  • 레고 블럭 조립하듯이
  • 다형성 + SOLID 원칙

SOLID (by. 로버트 마틴)

단일책임원칙

  • 변경시 영향을 미치는 규모가 작을수록 단일 책임원칙을 잘 지켰다고 볼 수 있을 것이다.

개방폐쇄원칙 (중요)

  • 다형성을 활용하면 확장에는 개방적이며 변경에는 폐쇄적인 원칙을 실천할 수 있다.
    • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있도록.
  • 인터페이스의 구현체를 만드는 것이 기존 코드에 변경 처리
  • 다만 클라이언트 코드에서 사용하는 구현체가 달라지면 변경이 필요함.
    • 이를 해결하기위해 DI가 존재.

리스코프 치환 원칙

  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것

인터페이스 분리 원칙

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다

의존관계 역전 원칙 (중요)

  • 프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.”
  • 그냥 해당 객체를 알면 의존하는 것이다 ㅎ
    • 가령 Controller에서 서비스 인터페이스의 구현체를 직접할당하면 인터페이스(추상화)에도 의존하고 구현체(구체화)에도 의존하게 된다. 따라서 스프링은 DI를 통해 추상화에만 의존하도록 한다.
  • 구현체를 사용할 때 어떻게 표시 안하게 할 수 있을까 ?
    • 제 3의 객체가 구현체를 밖에서 넣어준다면, 즉 DI를 통해 구현체를 사용단에서 표시하지 않을 수 있게 된다.
    • ex) private MemberService memberService;

다시, 스프링과 SOLID(객체지향)

  • SOLID 특히 계방폐쇄원칙과 의존관계역전원칙을 지키면 자연스레 스프링 프레임워크가 만들어짐.
  • 한단계 추상화 되어있기 때문에 실무적 관점에서 코드를 한 번 더 까보아야하는 비용이 발생.
  • 변경 및 확장 가능성이 클지를 잘 판단하여 추상화를 할지 말지 결정하는 것이 좋다.
  • 처음에는 구현체를 바로 사용하고 추후에 추상화를 하는 것도 좋은 방법이 될 수 있다.
  • 이러한 것들을 잘 판단하여 아키텍쳐를 설계할 수 있는 능력을 갖춰보자.

참고자료

**스프링역사객체지향프로그래밍 **SOLID**


실습을 통해 어떻게 스프링이 객체지향의 원칙을 잘 실천하는지 알아보자!

Repo

실습 구성

  • 순수 자바 코드로 유저, 주문, 할인 로직 구현 및 DI 구현.
  • 스프링으로 전환.

 Point

  • 역할(인터페이스)을 기준으로 다이어그램을 구성하고,
  • 해당 역할에 대해 여러가지로 구현해주면
  • 객체지향적인 다이어그램이 완성된다.

구현체는 요구사항이 변경되거나하면 추가하여 갈아끼워주기만 하면됨.

번외로 꿀팀 intellij 단축키.

  • F2누르면 에러난 곳으로 바로 이동
  • command + n. → generator
  • 자동완성 시, command + shift + enter 누르면 세미클론까지 포함하여 완성시킴.
  • psvm (main method 생성)

실습 강의를 들으면서 새로 알게된 혹은 잊고 있었던 지식들.

  • primitive 타입이 아닌 인스턴스 타입을 사용하는 이유 : primitive는 null이 안담기기 때문에 db와 연동시 불편할 수 있기 때문

  • ApplicationContext를 스프링 컨테이너라고 하며, @Contiguration이 붙은 파일을 설정(구성) 정보로 사용한다. @Bean이라 적힌 메서드를 모두 호출해서 객체를 스프링 컨테이너에 등록한다.

  • BeanFactory : 스프링 컨테이너의 최상위 인터페이스 이다. ( ApplicationContext → BeanFactory )

  • Bean을 조회하고 하는 빈관련 기능이 BeanFactory에 정의되어있으며 이를 포함하여 부가기능이 추가되어있는 ApplicationContext로 BeanFactory의 기능을 사용한다. 둘다 스프링 컨테이너라 보면된다.

  • BeanDefinition : 스프링이 설정 정보를 java, xml, 등 다양한 타입을 지원해주는 이유는 spring은 결국 BeanDefinition 객체를 읽으며 해당 여러 타입에서는 BeanDefinition으로 변환(추상화?)하는 기능이 있기 때문이다. (~Reader)

  • Singleton :

    • 싱글톤 패턴을 적용하지 않는 다면, 웹 어플리케이션 특성상 여러 유저가 요청을 하는데, 요청 할 때마다 객체 생성 → 메모리 낭비! 이를 해결하기 위해 여러번의 요청이와도 동일한 객체를 사용하도록 하자 !→ 싱글톤,
    • 싱글톤 패턴을 구현하는 방법은 여러가지가 있다.
    • 싱글톤 패턴은 다음과 같은 여러가지 문제점이 있어서 안티패턴으로도 불리는데, 스프링의 싱글톤 컨테이너(스프링컨테이너)는 이러한 여러 문제점을 해결하였다.
      1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
      2. 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
      3. 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
      4. 테스트하기 어렵다.
    • 스프링에서 싱글톤 패턴을 사용하기 때문에 주의해야할 점.
      • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
      • 무상태(stateless)로 설계해야 한다!
        • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
        • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
        • 가급적 읽기만 가능해야 한다.
        • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
      • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!
        • ex 결제 값 price를 빈에 필드로 보관하는 경우 여러 명의 유저가 해당 빈에 접근하여 price가 변경됨에 따라 다른 값이 다른 유저에게 갈 수 있다.
    • @Configuration 을 붙이면 new 키워드를 여러번 호출해도 싱글톤을 보장해줌. (cglib를 이용해서)
  • Component Scan :

    • @ComponentScan은 @SpringBootApplication에 내제되서 전체 @Component들을 스캔하여 빈을 생성하며, 특정 옵션으로 특정 객체의 빈생성을 제외하고 포함시킬 수 있다.
    • 빈의 중복 등록 상황 시 (자동 vs 자동, 수동 vs 자동) 빈 충돌 에러를 낸다.
  • DI :

    • 4 Way
      • 생성자 주입
        • 이름 그대로 생성자를 통해 의존성을 주입받는 방식.
        • 한 번만 주입됨을 보장한다. setter등이 있지 않는 이상 이후에 변경될 수 없다.
        • 생성자가 하나 일 때는, 생성자에 @autowired를 붙이지 않아도 된다.
          • 이 기능으로 인해, lombok의 @RequiredArgConstructor를 붙여주면 생성자 조차도 쓰지 않아도됨.
        • 권장
          • 의존관계 주입은 한번 일어나면 어플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
          • 수정자 주입, 필드 주입을 사용하면 변경 가능성이 있다.
          • 즉 의존관계 주입에 대해서 ‘불변’하게 설계할 수 있기 때문에 권장.
        • 생성자 주입으로 구현하면 장점으로,
          1. 테스트 시 주입받아야하는 의존관계들을 명확히 할 수 있으며,

          2. final 키워드를 넣을 수 있다.

            (수정자 주입이라면 수정메소드로 의존관계를 넣어주지 않아도 실행 전까지 오류가 없다.)

            final을 넣어주면 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에서 잡아줄 수 있다.

      • setter 주입
        • setter를 통해 주입받는 방식. 생성자에 @Autowired 붙여주 듯, setter 메소드에 @Autowired를 붙여주면 된다.
        • 선택, 변경 가능성이 있는 의존관계에 사용.
        • 자바빈 프로퍼티 규약의 수정자 메소드 방식을 사용하는 방법.
      • 필드 주입
        • 이름 그대로 필드에다가 바로 주입하는 방식.
        • 필드에 @Autowired를 붙여주면 된다. private이어도 가능함.
        • 코드가 간결하지만, 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점 존재.
        • 비추.
        • 테스트 코드에서는 간단하게 사용하기에 사용하는 것도 좋다.
      • 일반 메서드 주입.
        • 수정자 주입, 생성자 주입이랑 메서드 명만 (ex. init) 다르지 비슷한 걸로 보면됨. @autowired 붙여주면 된다.
        • 한번에 여러 필드를 주입 받을 수 있다. (생성자 주입도 동일.)
    • 옵션 처리
      • 빈주입이 없어도 동작해야되는 경우, 여러가지 옵션을 통해 빈이 없으면 주입 처리를 해서 동작하도록 할 수 있다.
        • required =false
        • @Nullable
        • Optional(Object)
    • 조회 대상 빈이 2개 이상일 때 해결 방법(빈으로 등록해야하는 상위 클래스가 동일한 하위 클래스가 2개 이상)
      • @Autowired 필드 명 매칭 (타입이 상위클래스더라도, 필드명이 하위 클래스 명이면 자동으로 해당되는 타입을 빈으로 생성함.)
      • @Quilifier 사용
        • Component에 해당 어노테이션을 붙이고 이름을 지정해준다음, @Autowired 부분에도 동일한 이름으로 어노테이션을 붙여주면 매칭해준다.
      • @Primary 사용
        • 우선순위를 가지게할 클래스에(컴포넌트가 붙은) 해당 어노테이션을 붙여주면 의존관계 주입에 우선순위를 가지게 됨.
  • 빈 생명주기 콜백

    • 스프링 빈의 간단한 라이프사이클 : 객체 생성 → 의존관계 주입

      • 생성자 주입의 경우에는 동시에 이루어지지만,
    • 스프링 빈의 이벤트 라이프사이클 (싱글톤 타입)

      • 스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전콜백 → 스프링 종료
        • 여기서 말하는 초기화는 의존관계주입까지 완료된 후 사용할 준비가 된 객체의 상태를 말함.
        • 초기화콜백메소드를 활용해서 개발자는 의존관계 주입까지 되었음을 알 수 있다.
    • 스프링 빈 생명주기 콜백 지원 방법 3가지

      - @PostConstruct , @PreDestroy 권장

      콜백 메소드는 어디에 사용하면 용이할까?

  • 빈 스코프

    1. 싱글톤 - 디폴트 스코프로 컨테이너의 시작과 종료까지 유지되는 가장 젋은 범위를 가지는 스코프
    2. 프로토타입
      • 스프링컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프이다.
      • 특정 client가 요청하는 시점에 컨테이너는 만들고 주입한다음 던져준 후에 더이상 관리하지 않는다. (반대로 싱글톤 스코프는 관리를 한다.) 즉, 종료는 관리가 필요하다면 클라이언트가 관리해야한다. (즉, @PreDestroy가 동작하지 않는다. )
      • 클라이언트가 요청할 때마다 새로운 빈을 생성하여 반환한다.
    3. 웹 관련 스코프
      1. request - HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
      2. session - HTTP Session과 동일한 생명주기를 가지는 스코프
      3. application - 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
    • 싱글톤 빈과 프로토타입 비늘 함께 사용할 때 문제점
      • 싱글톤이 프로토타입빈을 의존하게 되면 프로토타입빈은 자신의 기능을 상실하게 된다. 그 이유는 싱글톤 객체는 최초 생성 후 의존관계를 주입받고 이후로는 주입받지 않기 때문에 최초에 생성된 프로토타입빈만 사용이 되기 때문이다.
        • 이를 해결하는 방법으로 Provider(DL 기능)를 적용할 수 있다.
    • request Scope
      • request 요청만다 빈이 생성되고 관리된다. → request마다 구분지어야 하는 경우 적용해볼 수 있다.

      • 일반적으로 인터셉터에서 많이 사용한다.

      • 컨트롤러 단에서 사용하고 리퀘스트 범위의 빈을 생성자로 주입해줄 때, DL을 통해 요청마다 빈을 주입해 주는 것일까 ? 아니면 리퀘스트 범위의 빈을 의존하고있는 빈도(여기서는 컨트롤러) 같이 초기화되는 것일까 ?

        Provider와 함께 사용하여 MyLogger빈을 메소드 실행 시점에 주입해준다. ( 이것은 request 스코프 빈이 스프링 컨테이너를 띄우는 시점에는 없기 때문에 에러를 내는 것을 해결하기 위한 이유로도 사용함.)

        Scope 어노테이션의 인자로 proxyMode를 설정하여 줄 수도 있다. 이렇게하면 가짜 객체를 일단 넣어주고 나중에 실제로 동작해야할 때 진짜를 넣어줌.

    • 싱글톤이 아닌 스코프들은 꼭 필요한 곳에서만 사용하는 것을 권장한다. 유지보수가 어려워질 수 있다. 가령 request 스코프를 proxyMode를 설정하여 사용할 경우, 싱글톤 객체와의 차이를 클라이언트 단의 코드만 봤을 때 알 수 없다.