스프링 핵심 원리 - 기본편
본 내용은 김영한님의 스프링 핵심원리 - 기본편을 정리한 내용입니다.
(또한 제가 이해한 수준에서 정리한 내용이기 때문에 김영한님 강의의 모든 내용을 포함하지 않습니다.)
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 특히 계방폐쇄원칙과 의존관계역전원칙을 지키면 자연스레 스프링 프레임워크가 만들어짐.
- 한단계 추상화 되어있기 때문에 실무적 관점에서 코드를 한 번 더 까보아야하는 비용이 발생.
- 변경 및 확장 가능성이 클지를 잘 판단하여 추상화를 할지 말지 결정하는 것이 좋다.
- 처음에는 구현체를 바로 사용하고 추후에 추상화를 하는 것도 좋은 방법이 될 수 있다.
- 이러한 것들을 잘 판단하여 아키텍쳐를 설계할 수 있는 능력을 갖춰보자.
참고자료
실습을 통해 어떻게 스프링이 객체지향의 원칙을 잘 실천하는지 알아보자!
실습 구성
- 순수 자바 코드로 유저, 주문, 할인 로직 구현 및 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 :
- 싱글톤 패턴을 적용하지 않는 다면, 웹 어플리케이션 특성상 여러 유저가 요청을 하는데, 요청 할 때마다 객체 생성 → 메모리 낭비! 이를 해결하기 위해 여러번의 요청이와도 동일한 객체를 사용하도록 하자 !→ 싱글톤,
- 싱글톤 패턴을 구현하는 방법은 여러가지가 있다.
- 싱글톤 패턴은 다음과 같은 여러가지 문제점이 있어서 안티패턴으로도 불리는데, 스프링의 싱글톤 컨테이너(스프링컨테이너)는 이러한 여러 문제점을 해결하였다.
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 스프링에서 싱글톤 패턴을 사용하기 때문에 주의해야할 점.
- 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
- 무상태(stateless)로 설계해야 한다!
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
- 가급적 읽기만 가능해야 한다.
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
- 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!
- ex 결제 값 price를 빈에 필드로 보관하는 경우 여러 명의 유저가 해당 빈에 접근하여 price가 변경됨에 따라 다른 값이 다른 유저에게 갈 수 있다.
- @Configuration 을 붙이면 new 키워드를 여러번 호출해도 싱글톤을 보장해줌. (cglib를 이용해서)
Component Scan :
- @ComponentScan은 @SpringBootApplication에 내제되서 전체 @Component들을 스캔하여 빈을 생성하며, 특정 옵션으로 특정 객체의 빈생성을 제외하고 포함시킬 수 있다.
- 빈의 중복 등록 상황 시 (자동 vs 자동, 수동 vs 자동) 빈 충돌 에러를 낸다.
DI :
- 4 Way
- 생성자 주입
- 이름 그대로 생성자를 통해 의존성을 주입받는 방식.
- 한 번만 주입됨을 보장한다. setter등이 있지 않는 이상 이후에 변경될 수 없다.
- 생성자가 하나 일 때는, 생성자에 @autowired를 붙이지 않아도 된다.
- 이 기능으로 인해, lombok의 @RequiredArgConstructor를 붙여주면 생성자 조차도 쓰지 않아도됨.
- 권장
- 의존관계 주입은 한번 일어나면 어플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
- 수정자 주입, 필드 주입을 사용하면 변경 가능성이 있다.
- 즉 의존관계 주입에 대해서 ‘불변’하게 설계할 수 있기 때문에 권장.
- 생성자 주입으로 구현하면 장점으로,
테스트 시 주입받아야하는 의존관계들을 명확히 할 수 있으며,
final 키워드를 넣을 수 있다.
(수정자 주입이라면 수정메소드로 의존관계를 넣어주지 않아도 실행 전까지 오류가 없다.)
final을 넣어주면 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에서 잡아줄 수 있다.
- setter 주입
- setter를 통해 주입받는 방식. 생성자에 @Autowired 붙여주 듯, setter 메소드에 @Autowired를 붙여주면 된다.
- 선택, 변경 가능성이 있는 의존관계에 사용.
- 자바빈 프로퍼티 규약의 수정자 메소드 방식을 사용하는 방법.
- 필드 주입
- 이름 그대로 필드에다가 바로 주입하는 방식.
- 필드에 @Autowired를 붙여주면 된다. private이어도 가능함.
- 코드가 간결하지만, 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점 존재.
- 비추.
- 테스트 코드에서는 간단하게 사용하기에 사용하는 것도 좋다.
- 일반 메서드 주입.
- 수정자 주입, 생성자 주입이랑 메서드 명만 (ex. init) 다르지 비슷한 걸로 보면됨. @autowired 붙여주면 된다.
- 한번에 여러 필드를 주입 받을 수 있다. (생성자 주입도 동일.)
- 생성자 주입
- 옵션 처리
- 빈주입이 없어도 동작해야되는 경우, 여러가지 옵션을 통해 빈이 없으면 주입 처리를 해서 동작하도록 할 수 있다.
- required =false
- @Nullable
- Optional(Object)
- 빈주입이 없어도 동작해야되는 경우, 여러가지 옵션을 통해 빈이 없으면 주입 처리를 해서 동작하도록 할 수 있다.
- 조회 대상 빈이 2개 이상일 때 해결 방법(빈으로 등록해야하는 상위 클래스가 동일한 하위 클래스가 2개 이상)
- @Autowired 필드 명 매칭 (타입이 상위클래스더라도, 필드명이 하위 클래스 명이면 자동으로 해당되는 타입을 빈으로 생성함.)
- @Quilifier 사용
- Component에 해당 어노테이션을 붙이고 이름을 지정해준다음, @Autowired 부분에도 동일한 이름으로 어노테이션을 붙여주면 매칭해준다.
- @Primary 사용
- 우선순위를 가지게할 클래스에(컴포넌트가 붙은) 해당 어노테이션을 붙여주면 의존관계 주입에 우선순위를 가지게 됨.
- 4 Way
빈 생명주기 콜백
스프링 빈의 간단한 라이프사이클 : 객체 생성 → 의존관계 주입
- 생성자 주입의 경우에는 동시에 이루어지지만,
스프링 빈의 이벤트 라이프사이클 (싱글톤 타입)
- 스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전콜백 → 스프링 종료
- 여기서 말하는 초기화는 의존관계주입까지 완료된 후 사용할 준비가 된 객체의 상태를 말함.
- 초기화콜백메소드를 활용해서 개발자는 의존관계 주입까지 되었음을 알 수 있다.
- 스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전콜백 → 스프링 종료
스프링 빈 생명주기 콜백 지원 방법 3가지
- @PostConstruct , @PreDestroy 권장
콜백 메소드는 어디에 사용하면 용이할까?
빈 스코프
- 싱글톤 - 디폴트 스코프로 컨테이너의 시작과 종료까지 유지되는 가장 젋은 범위를 가지는 스코프
- 프로토타입
- 스프링컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프이다.
- 특정 client가 요청하는 시점에 컨테이너는 만들고 주입한다음 던져준 후에 더이상 관리하지 않는다. (반대로 싱글톤 스코프는 관리를 한다.) 즉, 종료는 관리가 필요하다면 클라이언트가 관리해야한다. (즉, @PreDestroy가 동작하지 않는다. )
- 클라이언트가 요청할 때마다 새로운 빈을 생성하여 반환한다.
- 웹 관련 스코프
- request - HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
- session - HTTP Session과 동일한 생명주기를 가지는 스코프
- application - 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
- 싱글톤 빈과 프로토타입 비늘 함께 사용할 때 문제점
- 싱글톤이 프로토타입빈을 의존하게 되면 프로토타입빈은 자신의 기능을 상실하게 된다. 그 이유는 싱글톤 객체는 최초 생성 후 의존관계를 주입받고 이후로는 주입받지 않기 때문에 최초에 생성된 프로토타입빈만 사용이 되기 때문이다.
- 이를 해결하는 방법으로 Provider(DL 기능)를 적용할 수 있다.
- 싱글톤이 프로토타입빈을 의존하게 되면 프로토타입빈은 자신의 기능을 상실하게 된다. 그 이유는 싱글톤 객체는 최초 생성 후 의존관계를 주입받고 이후로는 주입받지 않기 때문에 최초에 생성된 프로토타입빈만 사용이 되기 때문이다.
- request Scope
request 요청만다 빈이 생성되고 관리된다. → request마다 구분지어야 하는 경우 적용해볼 수 있다.
일반적으로 인터셉터에서 많이 사용한다.
컨트롤러 단에서 사용하고 리퀘스트 범위의 빈을 생성자로 주입해줄 때, DL을 통해 요청마다 빈을 주입해 주는 것일까 ? 아니면 리퀘스트 범위의 빈을 의존하고있는 빈도(여기서는 컨트롤러) 같이 초기화되는 것일까 ?
Provider와 함께 사용하여 MyLogger빈을 메소드 실행 시점에 주입해준다. ( 이것은 request 스코프 빈이 스프링 컨테이너를 띄우는 시점에는 없기 때문에 에러를 내는 것을 해결하기 위한 이유로도 사용함.)
Scope 어노테이션의 인자로 proxyMode를 설정하여 줄 수도 있다. 이렇게하면 가짜 객체를 일단 넣어주고 나중에 실제로 동작해야할 때 진짜를 넣어줌.
- 싱글톤이 아닌 스코프들은 꼭 필요한 곳에서만 사용하는 것을 권장한다. 유지보수가 어려워질 수 있다. 가령 request 스코프를 proxyMode를 설정하여 사용할 경우, 싱글톤 객체와의 차이를 클라이언트 단의 코드만 봤을 때 알 수 없다.