[스프링 핵심 원리(김영한)] ‘이야기 - 자바 진영의 추운 겨울과 스프링의 탄생’ ~ ‘스코프와 프록시’
인프런 강의 주소
보는 방법
- 블로그에 메모한 내용만으로 복습하기에는 무리.
- 강의 자료는 해당 강의에서 제공해 줌. 강의 자료로 복습하고, 기억나지 않는다 싶으면 영상 보기.
- 여기서는 다음을 기록.
- 강의 핵심 키워드
- (가끔씩) 강의를 보고 생긴 의문에 대해 구글링해서 찾은 내용
- (가끔씩) 나중에 다시 볼 때 참고할 만한 정보
객체 지향 설계와 스프링
이야기 - 자바 진영의 추운 겨울과 스프링의 탄생
- EJB(Enterprise Java Beans)
- 지금의 spring, jpa 기능을 다 제공해 줌
- 속도 느리고, 너무 복잡해서 코드 엉망되고, EJB에 너무 의존되고, 비싸고 …
- 스프링
- EJB 컨테이너 대체
- 하이버네이트
- EJB 엔티티빈 기술을 대체
- 이후 JPA(Java Persistence API) 새로운 표준 정의
스프링이란?
- 스프링 프레임워크
- 핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트 등
- 웹 기술: 스프링 MVC
- 데이터 접근 기술: 트랜잭션, JDBC, ORM 지원
- 테스트: 스프링 기반 테스트 지원
- 스프링 부트
- 스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
- 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
- Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨
- 스프링과 3rd parth(외부) 라이브러리 자동 구성
- 스프링의 진짜 핵심
- 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크
좋은 객체 지향 프로그래밍?
- 객체 지향 특징
- 추상화
- 상속
- 다형성
- 캡슐화
- 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프
트웨어 개발에 많이 사용된다.
- 컴퓨터 부품 갈아 끼우듯이
- 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법
- 다형성(Polymorphism)
- 다형성의 비유: 역할과 구현으로 세상을 구분
- 자동차 역할이 있으면, 이를 현대차, 기아차, 테슬라차로 구현하는 것
- 장점
- 클라이언트는 대상의 역할(인터페이스)만 알면 되지, 구현 대상의 내부 구조를 몰라도 된다.
- 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않으므로, 구현 대상 자체를 변경해도 영향을 받지 않는다
- 따라서 프로그램을 유연하고 변경이 용이하게 만든다.(다른 구현체로 쉽게 변경할 수 있음.)
- 또 (구현체를) 계속 확장 가능하다.
- 결론: 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.
- 자바 언어의 다형성: 오버라이딩
- 오버라이딩 된 메서드가 실행
- 다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있다.
- 물론 클래스 상속 관계도 다형성, 오버라이딩 적용가능
- 스프링과 객체 지향
- 스프링에서 이야기하는 제어의 역전(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): 의존관계 역전 원칙
- 5가지 원칙 중에서 OCP, DIP가 중요
- SRP(single responsibility principle)
- 한 클래스는 하나의 책임만 가져야 한다
- 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것
- OCP(Open/closed principle)
- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다
- 기존 클라이언트 코드는 변경하면 안 되고(closed), 확장(인터페이스를 구현한 새로운 클래스)에는 열려있어야 한다.
- 아래 경우의 문제점
- MemberService 클라이언트가 구현 클래스를 직접 선택(
MemberRepository m = new MemoryMemberRepository();
) - 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.(MemoryMemberRepository를 JdbcMemberRepository로)
- 분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.
- 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다(스프링 컨테이너)
- MemberService 클라이언트가 구현 클래스를 직접 선택(
- LSP(Liskov substitution principle)
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다
- 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것
- ISP(Interface segregation principle)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
- 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
- 인터페이스가 명확해지고, 대체 가능성이 높아진다
- DIP(Dependency inversion principle)
- 프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
- 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
MemberRepository m = new MemoryMemberRepository();
- MemberService 클라이언트가 구현 클래스를 직접 선택
- MemberService는 인터페이스에 의존하지만, 구현 클래스도 동시에 의존한다
- DIP 위반
객체 지향 설계와 스프링
- 스프링 이야기에 왜 객체 지향 이야기가 나오는가?
- 스프링은 DI 컨테이너를 제공해서 다형성, OCP, DIP를 가능하게 지원
- 즉 클라이언트 코드의 변경 없이 기능 확장(쉽게 부품을 교체하듯이 개발)
- 이상적으로는 모든 설계에 인터페이스를 부여하자
- 기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법이다.
스프링 핵심 원리 이해1 - 예제 만들기
프로젝트 생성
- 스프링 입문(김영한) 강의 때의 프로젝트 생성과 비슷한데, 더 간단하다.
- 간단히 스프링 부트 스타터 사이트로 이동해서(혹은 vsCode Spring Initializr ~ 익스텐션으로) 프로젝트 gradle로 생성
- 이후 따로 설정할 건 없음
비즈니스 요구사항과 설계
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)
- 미확정이더라도, 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계하면 된다
회원 도메인 설계
- 설계 그림
- 도메인 협력 관계: 기획자도 볼 수 있는 그림
- 클래스 다이어그램: 도메인 협력 관계를 구체화해서 개발자가 보는 그림
- 객체 다이어그램
- MemberRepository에 MemoryMemberRepository를 넣을지, DBMemberRepository를 넣을지는 동적으로 결정됨.
- 따라서 클래스 다이어그램 만으로는 판단하기 힘들기 때문에, 클라이언트가 실제 사용하는 객체끼리의 참조를 나타냄
회원 도메인 개발
- HashMap 은 동시성 이슈가 발생할 수 있지만 이 강의에서는 간단히 개발하기 위해 이를 사용한다.
- 동시성을 고려한다면 ConcurrentHashMap 을 사용하자.
회원 도메인 실행과 테스트
- 회원 도메인 설계의 문제점: 의존관계가 인터페이스뿐만 아니라 구현까지 모두 의존하는 문제점이 있음
주문과 할인 도메인 설계
- 딱히 메모할 내용 X
주문과 할인 도메인 개발
- 딱히 메모할 내용 X
주문과 할인 도메인 실행과 테스트
- memberId를 기본 자료형인 long이 아니라, Long으로 한 이유
- Long에는 null이 들어갈 수 있지만, long에는 null이 들어갈 수 없어서
- 나중에 db에 넣을 때 null이 들어갈 수 있기 때문에
스프링 핵심 원리 이해2 - 객체 지향 원리 적용
새로운 할인 정책 개발
- 딱히 메모할 내용 X
새로운 할인 정책 적용과 문제점
- 왜 클라이언트 코드를 변경해야 할까?
- DIP 위반: 지금까지의 코드는 클라이언트인 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다.
- OCP 위반: 그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다.
- 해결방안
- 이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.
관심사의 분리
- AppConfig
- 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.
- 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
- appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.
- 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입이라 한다
- 관심사의 분리
- AppConfig를 통해서 관심사를 확실하게 분리했다.
- AppConfig는 공연 기획자다. AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다.
- 이제 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.(OrderServiceImpl 등은 기능을 실행하는 책임만 지면 된다.)
AppConfig 리팩터링
- AppConfig 속 중복 있음
- 역할과 구현 클래스가 한눈에 안 들어옴
새로운 구조와 할인 정책 적용
- AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.
- 이후론 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. 사용 영역의 어떤 코드도 변경할 필요가 없다.
전체 흐름 정리
- 딱히 메모할 것 없음
좋은 객체 지향 설계의 5가지 원칙의 적용
- SRP, DIP, OCP
IoC, DI, 그리고 컨테이너
- IoC(Inversion of Control): 제어의 역전
- 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것(앞에서는 AppConfig가 하는 일)을 제어의 역전(IoC)이라 한다
- 프레임워크도 내가 작성한 코드를 제어하고, 대신 실행함으로 IoC라고 할 수 있음
- 즉 프레임워크가 자기 만의 실행 순서를 가지고 제어한다. 우리는 그에 맞게 필요한 부분만 구현하는 느낌. 프레임워크가 실행 흐름을 제어하는 것.
- ex) JUnit
- 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.
- DI(Dependency Injection): 의존관계 주입
- 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
- 정적인 클래스 의존관계
- 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다.
- ex) 클래스 다이어그램
- 동적인 객체 인스턴스 의존 관계: 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.
- 정적인 클래스 의존관계
- 의존관계 주입
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것
- 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
- 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
- IoC 컨테이너, DI 컨테이너
- IoC 컨테이너, DI 컨테이너: AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을
- 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
- 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다
스프링으로 전환하기
- 스프링 컨테이너
- ApplicationContext를 스프링 컨테이너라 한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다.
- 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
- 스프링 빈
- 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다
- 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
- 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다
스프링 컨테이너와 스프링 빈
스프링 컨테이너 생성
- 스프링 컨테이너
- ApplicationContext를 스프링 컨테이너라고 한다. 또한 이는 인터페이스이다.
- 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스(AppConfig)로 만들 수 있다.
new AnnotationConfigApplicationContext(AppConfig.class);
- 이 클래스는 ApplicationContext 인터페이스의 구현체이다.
- 스프링 컨테이너의 생성 과정
- 스프링 컨테이너 생성
- new AnnotationConfigApplicationContext(AppConfig.class)
- 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다. 여기서는 AppConfig.class 를 구성 정보로 지정했다.
- 스프링 빈 등록
- 스프링 컨테이너 안에 스프링 빈 저장소가 있다.
- 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다
- 이 때 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.
- 스프링 빈 의존관계 설정
- 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다
- 스프링 컨테이너 생성
컨테이너에 등록된 모든 빈 조회
- 모든 빈 출력하기
- 실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.
- ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
- ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
- 애플리케이션 빈 출력하기
- 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력해보자.
- 스프링이 내부에서 사용하는 빈은 getRole()로 구분할 수 있다.
- ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
- ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
스프링 빈 조회 - 기본
- 빈 이름으로 조회:
ac.getBean("memberService",MemberService.class);
- 이름 없이 타입만으로 조회:
ac.getBean(MemberService.class);
- 구체 타입으로 조회
ac.getBean("memberService",MemberServiceImpl.class);
- 구체 타입으로 조회하면 변경시 유연성이 떨어진다. (일반적으로는 별로 좋지 않은 코드다.)
스프링 빈 조회 - 동일한 타입이 둘 이상
- 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하자.
- ac.getBeansOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.
스프링 빈 조회 - 상속 관계
- 부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
BeanFactory와 ApplicationContext
- BeanFactory
- 스프링 컨테이너의 최상위 인터페이스
- 스프링 빈을 관리하고 조회하는 역할을 담당(ex- getBean())
- ApplicationContext
- BeanFactory 기능을 모두 상속받아서 제공한다.
- 추가로 수 많은 부가기능을 제공한다.
- ApplicatonContext가 제공하는 부가기능
- 메시지소스를 활용한 국제화 기능: 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
- 환경변수: 로컬 환경, 개발 환경, 운영 환경 등을 구분해서 처리
- 애플리케이션 이벤트: 이벤트를 발행하고 구독하는 모델을 편리하게 지원
- 편리한 리소스 조회: 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
다양한 설정 형식 지원 - 자바 코드, XML
- appConfig.xml 설정 정보와 자바 코드로 된 AppConfig.java 설정 정보를 비교해보면 거의 비슷하다는 것을 알 수 있다.
- xml 기반으로 설정하는 것은 최근에 잘 사용하지 않으므로 이정도로 마무리함.
스프링 빈 설정 메타 정보 - BeanDefinition
- BeanDefinition
- BeanDefinition을 빈 설정 메타정보라 한다. @Bean당 하나씩 메타 정보가 생성된다.
- 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다.
- 스프링 컨테이너는 메타정보(BeanDefinition)를 기반으로 스프링 빈을 생성한다.
- 따라서 스프링은 이렇게 다양한 설정 형식을 지원할 수 있는 것이다.
- BeanDefinition 정보: factoryBeanName, factoryMethodName, Scope, lazyInit, InitMethodName, DestroyMethodName 등
- 정리: BeanDefinition에 대해서는 너무 깊이있게 이해하기 보다는, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다.
싱글톤 컨테이너
웹 애플리케이션과 싱글톤
- 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
- 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
- 고객 트래픽이 초당 10000이 나오면 초당 10000개 객체가 생성되고 소멸된다 -> 메모리 낭비가 심하다.
- 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다 -> 싱글톤 패턴
싱글톤 패턴
- 싱글톤 패턴
- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
- 싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.
- 테스트 코드에서 isSameAs는 인스턴스 주소값을 비교함. (==과 같은 역할)
- 싱글톤 패턴 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다
- 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 결론적으로 유연성이 떨어진다. (의존관계 주입같은 걸 하기 힘들다)
싱글톤 컨테이너
- 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다
- 싱글톤 컨테이너
- 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.(싱글톤 컨테이너 역할)
- 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
- 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
- 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
- DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
싱글톤 방식의 주의점
- 이건 꼭 명심하기(강조함)
- 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다
- 무상태(stateless)로 설계해야 한다!
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다! (가급적 읽기만 가능해야 한다.)
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
- 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!
- 실무에서 이런 경우를 종종 보는데, 이로인해 정말 해결하기 어려운 큰 문제들이 터진다.(몇년에 한번씩 꼭 만난다.)
- 진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자.
@Configuration과 싱글톤
- AppConfig의 자바 코드를 보면 분명히 각각 2번 new MemoryMemberRepository 호출해서 다른 인스턴스가 생성되어야 하는데?
- 확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다. 왜?
@Configuration과 바이트코드 조작의 마법
- @Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장
- 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다
- 그 후 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
- @Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?
- @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
- 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.
바이트코드란?
- 인용 사이트
- 자바 바이트 코드(Java bytecode)란 자바 가상 머신이 이해할 수 있는 언어로 변환된 자바 소스 코드를 의미합니다.
- 자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1바이트라서 자바 바이트 코드라고 불리고 있습니다.
- 이러한 자바 바이트 코드의 확장자는 .class입니다.
- 자바 바이트 코드는 자바 가상 머신만 설치되어 있으면, 어떤 운영체제에서라도 실행될 수 있습니다.
컴포넌트 스캔
컴포넌트 스캔과 의존관계 자동 주입 시작하기
- 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.
- 등록해야 할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생한다.
- 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.
- @ComponentScan
- 컴포넌트 스캔을 사용하려면 먼저 @ComponentScan 을 설정 정보에 붙여주면 된다.
- 컴포넌트 스캔은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
- 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
- @Autowired 의존관계 자동 주입
- 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
- 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
- @ComponentScan
탐색 위치와 기본 스캔 대상
- 탐색할 패키지의 시작 위치 지정
- 모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
- basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 탐색한다.
- basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
- 만약 지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
- 권장하는 방법: 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단(프로젝트 시작 루트)에 두는 것이다.
- 컴포넌트 스캔 기본 대상
- 컴포넌트 스캔은 @Component 뿐만 아니라, @Controlller, @Service, @Repository, @Configuration도 대상에 포함된다.
- 컴포넌트 스캔의 용도 뿐만 아니라 부가 기능을 수행한다.
- 애노테이션에의 상속관계가 없다. 자바 언어가 지원하는 기능은 아니고, 스프링이 지원하는 기능이다.
필터
- includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
- excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
- 단, includeFilters 를 사용할 일은 거의 없다. 또 excludeFilters는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다.
- 개인적으로는 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.
중복 등록과 충돌
- 컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?
- 자동 빈 등록 vs 자동 빈 등록: ConflictingBeanDefinitionException 예외 발생
- 수동 빈 등록 vs 자동 빈 등록: 수동 빈이 자동 빈을 오버라이딩 해버린다.
- 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.
- 왜냐면 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는 여러 설정들이 꼬여서 오버라이딩해 버리는 일이 대부분이기 때문이다.
- 그러면 정말 잡기 어려운 버그가 만들어진다. 항상 잡기 어려운 버그는 애매한 버그다.
의존관계 자동 주입
다양한 의존관계 주입 방법
- 의존관계 주입 방법
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입: 거의 안 씀
- 일반 메서드 주입: 아예 안 씀
- 생성자 주입
- 생성자를 통해서 의존 관계를 주입 받는 방법이다
- 생성자 호출시점에 딱 1번만 호출되는 것이 보장
- 불변, 필수 의존관계에 사용(변하지 않고, 필수적일 때)
- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당한다.
- 수정자 주입(setter 주입)
- setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용
- 자바빈 프로퍼티 규약: 필드의 값을 직접 변경하지 않고, setter와 getter 메서드로 값을 수정하거나 읽는 규칙
- 선택, 변경 가능성이 있는 의존관계에 사용(필수적이지 않고(없어도 되고), 변할 수 있을 때)
- @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.
- setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입
옵션 처리
- 주입할 스프링 빈이 없어도 동작해야 할 때
- @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
- org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
- Optional<>: 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
- @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다
생성자 주입을 선택해라!
- 생성자 주입을 권장 이유
- 불변
- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
- 누락
- 프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에 수정자 주입을 하면 실행 결과 오류가 발생
- 생성자 주입을 사용하면 주입 데이터를 누락 했을 때 컴파일 오류가 발생한다.
- final 키워드
- 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.
- 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
- 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
- 불변
롬복과 최신 트랜드
- 대부분이 다 불변이고, 그래서 필드에 final 키워드를 사용하게 된다.
- 그런데 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 하고… 귀찮다.
- 롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
- 정리
- 최근에는 생성자를 딱 1개 두고, @Autowired 를 생략하는 방법을 주로 사용한다
- 여기에 Lombok 라이브러리의 @RequiredArgsConstructor를 사용하면 간단하게 만들 수 있다.
조회 빈이 2개 이상 - 문제
- @Autowired 는 타입(Type)으로 조회한다. 따라서
ac.getBean(DiscountPolicy.class)
와 유사하게 동작한다. - 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
- 스프링 빈을 수동 등록해서 문제를 해결해도 되지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.
@Autowired 필드 명, @Qualifier, @Primary
- 조회 대상 빈이 2개 이상일 때 해결 방법
- @Autowired 필드 명 매칭: @Autowired 는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
- @Qualifier: @Qualifier 는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
- @Primary: @Primary 는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.
- @Primary, @Qualifier 활용하면 된다. (ex- 메인/서브 데이터베이스 커넥션 획득할 때)
- 우선권
- @Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다.
- 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다.
- 따라서 @Qualifier 가 우선권이 높다.
애노테이션 직접 만들기
- @Qualifier(“mainDiscountPolicy”) 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다. 이는 애노테이션을 만들어서 문제를 해결할 수 있다.
- 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
- @Qulifier 뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다.
- 물론 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분별하게 재정의 하는 것은 유지보수에 더 혼란만 가중할 수 있다
조회한 빈이 모두 필요할 때, List, Map
- 어떤 타입의 스프링 빈이 다 필요한 경우도 있다. 예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있을 때를 생각해보자.
- 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.
- 주입 설명
Map<String, DiscountPolicy>
: map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.List<DiscountPolicy>
: DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다
Strategy pattern
- 인용 사이트
- the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime.
- Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
- Deferring the decision about which algorithm to use until runtime allows the calling code to be more flexible and reusable.
자동, 수동의 올바른 실무 운영 기준
- 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.: “물론 애노테이션을 조금씩 수정해야 하기 때문에 그럴 수 있는데, 이것만 제외하면 OCP, DIP를 지킨다고 볼 수 있죠.”
- 간단 요약
- 편리한 자동 기능을 기본으로 사용하자
- 직접 등록하는 기술 지원 객체는 수동 등록
- 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자
빈 생명주기 콜백
빈 생명주기 콜백 시작
- 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다. (데이터베이스 커넥션 풀, 네트워크 소켓 등)
- 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주고, 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.
- 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다.
- 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 하기 때문에 생명주기 콜백을 준다.
- 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
- 소멸전 콜백: 빈이 소멸되기 직전에 호출
- 스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원
- 인터페이스(InitializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 종료 메서드 지정
- @PostConstruct, @PreDestroy 애노테이션
- 참고) 객체의 생성과 초기화를 분리하자.
- 생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다
- 반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다
- 따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는, 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.
인터페이스 InitializingBean, DisposableBean
- 거의 사용하지 않는다
- 스프링 전용 인터페이스이기 때문에, 내 코드가 스프링 전용 인터페이스에 의존한다.
- 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
빈 등록 초기화, 소멸 메서드 지정
- 설정 정보에 @Bean(initMethod = “init”, destroyMethod = “close”) 처럼 초기화, 소멸 메서드를 지정할 수 있다.
- 장점
- 스프링 빈이 스프링 코드에 의존하지 않는다.
- 메서드 이름을 자유롭게 줄 수 있다.
- 코드가 아니라 설정 정보를 사용하기 때문에, 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.
애노테이션 @PostConstruct, @PreDestroy
- 특징
- 패키지를 잘 보면 javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다
- 컴포넌트 스캔과 잘 어울린다.
- 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다.
- 결론
- @PostConstruct, @PreDestroy 애노테이션을 사용하자
- 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod, destroyMethod를 사용하자.
빈 스코프
빈 스코프란?
- 스코프는 빈이 존재할 수 있는 범위를 뜻한다.
- 스프링은 다양한 스코프를 지원한다.
- 싱글톤: 기본 스코프. 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
프로토타입 스코프
- 프로토타입 빈의 특징 요약
- 스프링 컨테이너에 요청할 때마다 새로 생성된다.
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
- 종료 메서드가 호출되지 않는다.
- 그래서 프로토타입 빈은 이를 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
- 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할때 마다 새로 생성해서 사용하는 것을 원한다.
- 하지만 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결
- DL(Dependency Lookup): 의존관계를 외부에서 주입(DI) 받는게 아니라, 직접 필요한 의존관계를 찾는 것
- 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 기능만 제공하는 무언가가 필요
- ObjectProvider
- getObject() 를 호출하면 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
- JSR-330 Provider(자바 표준)
- get() 을 호출하면 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
- ObjectProvider
- 결론
- 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
- ObjectProvider, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
- 둘 중 무엇을 고르지?: 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다
웹 스코프
- 웹 스코프
- 웹 환경에서만 동작
- request: HTTP 요청 하나가 들어오고 응답이 나갈 때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈이 생성되고, 관리된다.
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
ServletContext? Application/Session/Cookie 정리
- “서블릿/JSP(뉴렉처) 1 ~ 53강”
- ServletContext
- 서블릿을 사용할 때 데이터를 이어갈 수 있는 저장소가 필요한데, 이를 서블릿 컨텍스트(context)라고 한다. 즉 서블릿 간에 문맥(컨텍스트)를 이어갈 수 있는 공간(상태 저장 공간)이라는 뜻. 이곳을 어플리케이션 저장소라고 말하기도 한다.
ServletContext application = request.getServletContext();
: 어플리케션 저장소를 생성- Application 객체(ServletContext)는 어플리케이션 전역에서 쓸 수 있다.
- Application/Session/Cookie 정리
- application
- 사용범위: 전역 범위에서 사용하는 저장 공간
- 생명주기: was가 시작해서 종료할 때까지
- 저장위치: was 서버의 메모리
- session
- 사용범위: 세션 범위(특정 사용자)에서 사용하는 저장 공간
- 생명주기: 세션이 시작해서 종료할 때까지
- 저장위치: was 서버의 메모리
- cookie
- 사용범위: 웹 브라우저별 지정한 path 범주(특정 url에 대해서만 사용 가능) 공간
- 생명주기: 브라우저에 전달한 시간부터 만료시간까지
- 저장위치: web browser의 메모리 또는 파일
- application
websocket?
- 인용 사이트
- 웹 소켓이란?
- HTML5 표준 기술로, HTTP 환경에서 클라이언트와 서버 사이에 하나의 TCP 연결을 통해 실시간으로 전이중 통신을 가능하게 하는 프로토콜이다.
- 전이중 통신이란, 일방적인 송신 또는 수신만이 가능한 단방향 통신과 달리 가정에서의 전화와 같이 양방향으로 송신과 수신이 가능한 것을 말한다.
- 웹 소켓 연결은 주로 새로고침이나 창 닫기 등의 이벤트 발생 시 닫힌다.
- 웹 소켓 vs HTTP
- 웹 소켓이 HTTP 요청으로 시작되며 HTTP에서 동작하지만, 두 프로토콜은 분명히 다르게 동작한다.
- HTTP는 클라이언트와 서버간 접속을 유지하지 않으며 요청과 응답 형태로 단방향 통신만 가능하다. 따라서 서버에서 클라이언트로의 요청은 불가능하다. 또한 요청-응답이 완료되면 수립했던 연결이 닫힌다. 웹 소켓은 클라이언트와 서버간 접속이 유지되며 요청과 응답 개념이 아닌 서로 데이터를 주고 받는 형식이다.
- REST한 방식의 HTTP 통신에서는 많은 URI와 Http Method를 통해 웹 어플리케이션과 상호작용하지만, 웹 소켓은 초기 연결 수립을 위한 오직 하나의 URL만 존재하며, 모든 메시지는 초기에 연결된 TCP 연결로만 통신한다.
request 스코프 예제 만들기
- 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않는다. 이 빈은 실제 고객의 요청이 와야 생성할 수 있다.
스코프와 Provider
- ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
- ObjectProvider.getObject() 를 LogDemoController , LogDemoService 에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다
스코프와 프록시
- @Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
- 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 특징
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다
- 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.