- JavaScript에서 쿠키에 접근하는 것을 막기 위한 HttpOnly설정

- HTTPS 상에서만 웹 애플리케이션이 쿠키 값을 브라우저에 전달할 수 있도록 하기 위한 Secure설정

- Apache 웹 서버의 httpd.conf 를 수정

 

1. HTTP Header를 설정하기 위해서 mod_headers  모듈 활성화

LoadModule headers_module modules/mod_headers.so

httpd.apache.org/docs/2.4/mod/mod_headers.html

 

2. HTTP Header 설정

Header edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure

 

3. Apache 재시작

HTTP 요청 개수 줄이기

- 브라우저는 HTML 파일 응답을 받고 나서, HTML안에 있는 CSS, 자바스크립트, 이미지 등의 콘텐츠들을 전부 받아 올 때까지 호출함
- 콘텐츠의 수가 많을수록 로딩 완료 시간은 길어짐
- 성능을 위해서 HTTP 요청 수를 줄여야 함

 

HTTP 요청을 줄이는 방법

스크립트 파일 병합

- 자바스크립트나 CSS파일들을 기능별로 분리하여 각각의 파일로 저장하고 호출하는 방식을 사용하는데, 이 방법은 HTTP 요청 수를 증가시키는 요인임
- 모듈화 된 여러 파일들을 하나로 합치고, 이 하나의 파일을 브라우저가 실행하는 것이 여러 개의 파일들을 각각 호출하는 것과 동일한 결과를 만들 수 있다면 파일 병합으로 HTTP 요청 수를 줄일 수 있음
- 병합한 파일의 크기가 너무 크다면 그 파일을 로딩하는 시간이 너무 길어질 수 있어서 적절한 크기를 유지해야 함

인라인 이미지

CSS 스프라이트

- 여러 개의 이미지를 하나의 이미지 파일로 결합해서 필요한 이미지가 위치하는 픽셀 좌표 정보를 사용

 

콘텐츠 파일 크기 줄이기

스크립트 파일 압축 전달

- 웹 서버가 지원하는 방식으로 스크립트를 압축해 클라이언트에게 더 작은 크기로 전달하고, 클라이언트가 압축을 해제하여 원본 콘텐츠를 이용
- 서버(Content-Encoding)와 클라이언트(Accept-Encoding)가 서로가 지원하는 압축방식 중 하나를 골라서 정해야 함
- Accept-Encoding : 클라이언트가 자신이 지원하는 압축 알고리즘을 서버에 알려줌
- Content-Encoding : 서버는 클라이언트가 알려준 압축 알고리즘 중 서버가 지원하는 알고리즘 하나를 선택해서 클라이언트에게 알려줌

ex.
Accept-Encoding: gzip, defalte, sdch
Content-Encoding: gzip

 

스크립트 파일 최소화

- 스크립트 파일에 포함된 주석, 공백, 개행 문자들처럼 실제 로직에는 아무 영향을 주지 않는 부분들을 제거하여 파일의 크기를 줄이는 방법
- 개발서버에는 원본을 두고, 운영서버에는 스크립트를 최소화하여 배포
- 더 이상 변경이 일어나지 않은 것이라 예상되는 스크립트를 대상으로 최소화
- Minify

 

Minify JS and CSS online, or include the minifier in your project for on-the-fly compression.

Minify JS and CSS online, or include the minifier in your project for on-the-fly compression.

www.minifier.org

=> 스크립트 파일 최소화로 불필요한 부분을 제거하여 가볍게 만들고, 압축을 해서 클라이언트에 전달

이미지 파일 압축

- 이미지는 웹 사이트에서 가장 많은 용량을 차지하는 콘텐츠
- 이미지는 메타데이터를 많이 가지고 있는데, 이를 제거하여 크기를 줄일 수 있음
- tinypng

 

TinyPNG – Compress PNG images while preserving transparency

Make your website faster and save bandwidth. TinyPNG optimizes your PNG images by 50-80% while preserving full transparency!

tinypng.com

 

적절한 이미지 포맷 사용

- 이미지는 jpg, png, gif, bmp 등의 다양한 포맷을 사용

- 각 브라우저에 특화된 이미지를 사용
- WebP : 손실 압축, 구글 개발, 이미지 전달 용도로 많이 사용하던 jpeg를 대체하기 위해 웹 사이트 트래픽 감소, 로딩 속도 단축을 목적으로 개발
- JPEG XR : 마이크로소프트
- 크롬에는 WebP, IE에는 JPEG XR제공 / JPEG만 들고 있다가 요청 브라우저에 따라 각각으로 변환하여 전달

- PNG는 알파채널로 투명 기능을 사용하는데, 투명 기능이 필요 없으면 사이즈가 작은 JPEG를 사용

 

래스터 이미지 vs 벡터 이미지

- 레스터 이미지는 사각형 필셀에 색상 데이터를 입력해서 표현, 큰 사이즈와 고품질 이미지를 위해서는 그만큼 픽셀을 추가해야 하므로, 용량이 커짐

- 벡터 이미지는 수학적인 메타 정보를 가지고 있어서 화면 스케일에 관계없이 항상 선명한 이미지를 제공, 이미지의 그림이 복잡해지면, 이를 위한 정보가 계속 늘어나게 됨, SVG는 텍스트 기반이라 svgz로 만들어서 사용 가능

 

무손실 압축 vs 손실압축

- 무손실 압축은 각 이미지 유형들을 서로 다르게 처리해야 함, 대부분 라이브러리 스크립트로 압축 자동화가 가능

- GIF : ImageMagicK, Giflossy, Gifsicle, gif2webp converter

- PNG : PNG는 청크 형태로 저장, 이미지 표현 정보인 핵심 청크가 아닌 정보성 청크는 삭제해도 됨(Pngcrush, Pngquant, PngOptimizer)

- JPEG : 이미지 외에 많은 메타 데이터가 있음(주석, 공백, 편집 앱 정보, 카메라 정보, 촬영 날짜/위치 등) 제거해도 이미지 품질 손실 없음(MozJPEG, libJpeg, Guetzli)

 

- 손실 압축은 이미지의 정보를 누락, 사용자의 시각적 경험을 손실시키지 않으면서 이미지를 손실시키면 됨

- 사람이 품질 저하를 모르게 하면서 파일 크기를 줄일 수 있는 JPEG품질은 100~75% => 85~80% 권장(시각적 손실이 거의 없고, 사이즈는 70% 이하로 축소 가능)

 

반응형 웹과 이미지

문제점 : (아직) 필요하지 않은 리소스들도 과도하게 다운로드

- 반응형 웹에서는 이미지를 내려받아서 화면의 사이즈에 맞게 줄이는 유동형 이미지 방법을 사용, 이 방법에서 화면상의 이미지가 작아졌다고 실제 이미지의 크기가 작아진 것은 아니고, 브라우저에서 큰 이미지를 받아다가 축소하는 처리하는 추가 과정이 이루어지게 되는 낭비가 발생함

- PC와 모바일에서는 같은 화면이어도 화면에 나타나는 이미지의 개수가 다름(PC가 훨씬 많음), 모바일 화면을 구성하기 위해 미디어 쿼리에 display: none;을 줘서 숨기는데, 이렇다고 해서 이미지를 다운로드하지 않는 것은 아님

- 모바일 화면을 볼 때 아직 스크롤을 내려서 확인하지 않은 화면에 있는 이미지들도 전부 다운로드

 

해결방법 : 사용자의 환경에 따라 그 환경에 적합한 이미지를 제공

- 사용자의 화면 크기에 따라 여러 가지 사이즈(버전)의 이미지를 준비해서 알맞게 전송

- FE : 미디어 쿼리로 사용자의 환경을 파악하고 그 환경에 맞는 이미지를 호출, 과도하게 사용하는 경우 FE코드가 무거워짐

- BE : 서버에서 사용자의 환경에 맞는 이미지를 전송, 서버에서 사용자의 환경을 파악하기 위한 작업이 필요함, Client Hint

- Client Hint : 사용자의 환경은 서버에 전달하기 위함, HTTP 헤더에 넣어서 전송

- 지연 로딩 : 당장 화면에 보여주지 않은 리소스를 처음부터 전부 다운로드하는 것은 낭비임, 실제로 필요하거나 스크롤을 내려서 화면이 움직일 때 이미지를 다운받음(lazyload라이브러리)

- 모바일 우선 접근 : 웹을 구현할 때 모바일 화면부터 개발, PC 화면부터 구성하면 모바일 화면을 구성할 때 PC에서 사용한 것들을 모바일에 그대로 사용하는 일이 많음

 

큰 파일은 작게 나누어서 전송

- 크기를 알 수 없거나 대용량 파일의 일부분을 순서대로 다운로드하는 부분 요청 응답 방식
- Range
- Content-Range
- 클라이언트는 구간별로 부분 파일 요청을 반복해서, 콘텐츠가 끊김 없이 서비스될 수 있도록 함

ex.
Range: bytes=0~1023
Content-Range: bytes 0~1023 / 10000 => 전체 10000중에서 1001~2000만 전송
Content-Length: 1024

 

브라우저 렌더링 최적화

- DOM최적화 : 웹 페이지에 구문 오류가 많을수록 예외 처리를 위한 메모리, CPU를 소모하므로, 구문 오류 최소화하기, 과도하게 HTML 태그를 중첩시키지 않기 (15단계 넘지 않도록)
- JS와 CSS배치 : HTML구문분석은 JS를 만나면 JS를 처리할 때까지 DOM생성 작업 중단, JS는 CSSOM생성이 완료될 때까지 JS처리 중단 => JS와 CSS는 랜더링을 지연할 수 있는 요소 => CSS는 최대한 위쪽에 두어서 CSSOM을 빠르게 만들도록, JS는 최대한 하단에 두어서 DOM, CSSOM생성 완료 후에 JS 수행되도록
- JS최적화 : 타사 솔루션 JS를 배치하는 경우도 있어서 하단에 배치하기 어려운 경우도 많음, 꼭 필요한 JS그룹과 그렇지 않은 JS그룹을 구분해서 후자는 async, defer적용
(async : DOM생성과 동시에 JS처리 / defer : DOM생성 중 별도 스레드로 JS 다운로드한 후, 처리는 DOM생성 완료 후에)
- CSS최적화 : 모든 CSS가 첫 화면 구성에 쓰이지는 않음, CSS를 적절히 분리해서 필요한 페이지에 필요한 CSS만 적용
- 이미지 로딩 최적화 : background-image, 지연 로딩, Progressive JPG

 

도메인 분할

- 여러 도메인을 소유한 경우 웹 콘텐츠를 병렬적으로 동시에 다운로드
- 브라우저 만다 허용하는 최대 동시 연결 개수를 제한함 (대부분 6개)
- 사이트 전체의 쿠키 사이즈를 줄일 수 있음
- 각 도메인은 동일한 인증서를 사용해야 함, 인증서가 다르면 추가적인 TLS협상 과정이 생겨버림
- 적절한 도에인 숫자를 결정해야 함, 너무 많으면 도리어 CPU 과부하

 

캐시 최적화

HTTP캐시 목적
1) 원본 서버로의 요청 수를 최소화, 네트워크 왕복 수를 줄여서 사용자에 대한 응답 속도를 단축
2) 완전한 콘텐츠를 응답하지 않아도 됨, 네트워크 대역폭과 리소스 낭비를 줄임

- 캐시 저장소에 접근하는 시간이 원래 데이터 위치에 접근하는 시간보다 짧고, 시스템의 리소스를 아낄 수 있음
- 프록시 서버 : 인터넷상에서 주고받은 데이터를 캐시 하고 다음에 요청하는 사용자에게 이를 제공하는 방식을 사용하여, 사용자들은 좀 더 빠르게 콘텐츠를 받을 수 있음
- PUSH : 미리 캐시 서버에 데이터를 복사해둠 / PULL : 실제 요청이 있을 때 캐시에 저장
- 웹 브라우저에서도 이미지나, 자바스크립트, CSS파일 등을 사용자의 브라우저가 캐시 해서 저장함
- 브라우저 캐시에 대한 내용은 웹서버의 캐시 설정을 따름 => 참고
- Cache-Control 응답 헤더로 클라이언트에 캐시 설정 정보를 알려줌 (유효시간)
- Expire는 만료 시점, Expire는 Date헤더를 함께 보내야 함, Date헤더에는 요청에 대한 응답이 작성된 시점을 표시
- Cache-Control, Expire둘다 있으면 Cache-Control를 우선 사용

Cache-Control: max-age=3600 => 브라우저 캐시 저장 후 3600초 동안 유효
Cache-Control: no-store => 캐시 하면 안 되는 콘텐츠
Cache-Control: no-cache => 원본 서버의 콘텐츠 갱신 여부를 확인하고 변경이 없는 경우만 캐시 된 콘텐츠 사용
Cache-Control: no-cache, no-store, must-revalidate : 캐시 절대 불가능
Expire: Mon, 30 Nov 2021 08:00:00 GMT => 2021년 11월 30일 08시에 만료

- 콘텐츠에 따라 캐싱을 어떻게 할 것인지 판단해야 함

- 동일한 파일을 서버의 여러 곳에 분산시키면 안 됨, 캐시 서버는 URL을 키로 하여 동작하기 때문에, 동일한 내용이 캐시에도 또 생김
- 캐시 오염 제거 : 원본은 한 개인데, 캐시에는 많음 => 쿼리 스트링 값이 달라도 응답이 매번 같다면, 캐시 키는 쿼리 스트링을 무시하도록, 쿼리 스트링의 순서를 동일하게

 

- 캐시 서버는 캐시 키를 사용

- 캐시 서버가 원본의 복사본을 저장하고 빠르게 조회하기 위해서 사용하는 키 값

- 일반적인 웹 캐시는 클라이언트가 요청하는  URL을 캐시 키로 사용함 (호스트/패스?쿼리스트링)

- 캐시 방지를 위해서 'version=오늘날짜' 와 같은 쿼리 스트링을 추가해서 요청할 수도 있음

 

EX) 아래 3개의 캐시 키는 서로 다른 객체를 참조함

www.abc.com/ui/css/common.css

www.abc.com/ui/css/common.css?vesion=20210721

www.abc.com/ui/css/common.css?vesion=20210722

CDN

- CDN은 웹 콘텐츠를 사용자에 세 빠르게 전달하기 위해 캐시 서버나 에지 서버와 같은 대용량 인터넷 캐시 영역에 콘텐츠들을 저장해서 사용하는 방식
- 클라이언트와 서버 사이에 발생 가능한 네트워크 지연이나 패킷 손실이 줄어듦
- 사용자는 비교적 가까운 에지 서버에서 콘텐츠를 받기 때문에 RTT가 줄어들어 빠르게 받을 수 있음
- CDN 서버가 콘텐츠를 전달해 주므로, 원본 서버의 부하를 덜어 줄 수 있음
- 캐시 사용률이 높아짐
- 서버 유지보수 용이

 

객체 생성 → 의존관계 주입

스프링 빈은 객체 생성, 의존관계 주입이 끝나야 필요한 데이터를 사용할 수 있음, 객체의 데이터 초기화는 의존관계 주입이 끝난 뒤에 가능

 

콜백을 통해서 초기화 시점과 종료 시점을 알려줌

 

1) 스프링 컨테이너 생성

2) 스프링 빈 생성

3) 의존관계 주입

4) 초기화 콜백

5) 빈 사용

6) 소멸 전 콜백

7) 스프링 종료

 

객체의 생성과 초기화를 분리

생성자는 객체 생성을 위한 정보를 받고, 메모리에 객체를 생성하는 역할

초기화는 생성된 객체를 활용하여 외부와 커넥션을 연결하는 무거운 동작을 수행

=> 생성자 안에서 무거운 초기화 작업을 하지 않고, 객체를 생성하는 부분과 초기화하는 부분을 분리(별도의 초기화 메서드)

 

 

스프링의 콜백 지원

1) InitializingBean, DisposableBean 인터페이스 => 사용 X

- 스프링 전용 인터페이스라서, 스프링에 의존적임

- 인터페이스의 메서드를 오버 라이딩하기 때문에, 초기화/소멸 메서드의 이름 변경 불가

- 코드 수정이 불가능한 외부 라이브러리에 적용 불가

 

2) 설정 정보에 초기화/소멸 메서드 지정 => 외부 라이브러리 연동에 사용

- @Bean(initMethod = "init", destroyMethod = "close")

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 url : " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void connect() {
        System.out.println("connect : " + url);
    }

    public void call(String msg) {
        System.out.println("call = " + url + " / msg = " + msg);
    }

    public void disConnect() {
        System.out.println("close = " + url);
    }

    // 초기화 메서드
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("메시지~~~~");
    }

    // 소멸 메서드
    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}
public class LifeCycleTest {

    @Test
    public void lifeCycleTest() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig {
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient() {
            NetworkClient nc = new NetworkClient();
            nc.setUrl("http://hello.com");
            return nc;
        }
    }
}
생성자 url : null
NetworkClient.init
connect : http://hello.com
call = http://hello.com / msg = 메시지~~~~
16:19:33.041 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@45018215, started on Sun Apr 18 16:19:32 KST 2021
NetworkClient.close
close = http://hello.com

설정 클래스 LifeCycleConfig를 보고 객체 생성 → 의존관계 주입 초기화 콜백 메서드(init메서드) → 사용 소멸 콜백 메서드(close메서드) → 종료(ac.close())

 

- 초기화/소멸 메서드 이름을 맘대로 할 수 있음

- 스프링에 의존 X

- 설정 정보의 @Bean에 지정하기 때문에 외부 라이브러리에도 적용이 가능

- destroyMethod이름을 따로 지정하지 않으면 close나 shutdown이라는 메서드를 호출함 (기본값이 추론 / inferred)

(라이브러리의 종료 메서드는 대부분 close나 shutdown)

 

 

3) @PostConstruct, @PreDestroy 어노테이션 => 사용 권장

- 초기화 메서드에 @PostConstruct

- 소멸 메서드에 @PreDestroy

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 url : " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void connect() {
        System.out.println("connect : " + url);
    }

    public void call(String msg) {
        System.out.println("call = " + url + " / msg = " + msg);
    }

    public void disConnect() {
        System.out.println("close = " + url);
    }

    // 초기화 메서드
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("메시지~~~~");
    }

    // 소멸 메서드
    @PreDestory
    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}

- 어노테이션만 붙이면 돼서 간편함

- @PostConstruct, @PreDestroy는 스프링에 종속된 것이 아니고 자바 표준임

- @Bean을 사용하지 않기 때문에 컴포넌트 스캔과 잘 어울림

- 단점은 설정 클래스에 작성하는 것이 아니라 외부 라이브러리에는 적용을 못함

 

 

정리

- 초기화/소멸 메서드 지정을 위해서 @PostConstruct, @PreDestroy를 사용

- 외부 라이브러리에 적용해야 할 때는 @Bean의 initMethod, destroyMethod를 사용

자동 등록 선호

- 스프링 부트는 컴포넌트 스캔을 기본으로 사용

- 스프링 MVC패턴에서 @Controller, @Service, @Repository 사용

- @Bean을 하나하나 만드는 것보다 @Component만 작성하는 것이 훨씬 간편

- OCP, DIP 준수 가능

 

 

수동 등록을 사용하면 좋은 경우

- 애플리케이션의 컨트롤러, 서비스, 리포지토리와 같은 업무 로직은 자동 등록을 사용

 

1) 업무 로직 외에 DB커넥션, Transaction설정, 공통 Log, 외부 라이브러리 연동과 같은 기술 지원 빈들은 개수가 적기 때문에 수동 등록으로 하여 업무 로직과 분리하여 관리하면 좋음

 

2) 의존관계 주입 중 조회한 빈이 모두 필요한 경우 List, Map을 사용하는데, 이에 해당하는 빈들은 하나에 모아서 @Bean으로 수동 등록하면 한 번에 보고 이해가 쉬움 (or 동일 패키지에 두고 자동 등록)

@Configuration
public class AppConfig {
   
    @Bean
    public DiscountPolicy rateDiscountPolicy(){
        return new RateDiscountPolicy();        
    }
    
    @Bean
    public DiscountPolicy fixDiscountPolicy(){
        return new FixDiscountPolicy()
    }

}

DiscountPolicy라는 동일 타입의 빈인 RateDiscountPolicy, FixDiscountPolicy를 모두 가져와 동적으로 사용하고 싶은 경우는 어떻게 할까?

=> List, Map에 빈을 담아와서 필요할 때 꺼내서 사용

 

public class AllBeanTest {

    @Test
    public void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);

        Member member = new Member(1, "memberA", Grade.VIP);

        int discountPrice = discountService.discount(member, 25000, "rateDiscountPolicy");

        Assertions.assertThat(discountPrice).isEqualTo(2500);
    }

    static class DiscountService {

        // 여러개의 빈이 모두 필요
        private final Map<String, DiscountPolicy> dcPolicyMap;
        private final List<DiscountPolicy> dcPolicyList;

        public DiscountService(Map<String, DiscountPolicy> dcPolicyMap, List<DiscountPolicy> dcPolicyList) {
            this.dcPolicyMap = dcPolicyMap;
            this.dcPolicyList = dcPolicyList;
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = dcPolicyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }

    }

}

 

- @AutoWired는 의존관계 주입을 위해서 생성자의 파라미터 Type으로 스프링 빈을 찾음

- 이때 찾은 빈이 2개 이상이면 오류가 발생함

NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
@Component
public class FixDiscountPolicy implements DiscountPolicy{
	......
}

@Component
public class RateDiscountPolicy implements DiscountPolicy{
	......
}

=> DiscountPolicy라는 Type으로 찾은 빈이 2개라 오류 발생함

 

 

찾은 빈이 2개 이상인 경우 어떤 빈을 사용?

1. @AutoWired 필드 이름, 생성자 파라미터 이름 매칭

생성자 파라미터 Type으로 찾음 => 2개 이상 => 필드 이름, 생성자 파라미터 이름으로 찾음

private final DiscountPolicy rateDiscountPolicy;

 

2. @Qualifier

- 추가로 구분할 수 있는 수단 제공

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{
	......
}

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{
	......
}
@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, 
    		@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    /*
    	비즈니스 로직
    */
}

 

3. @Primary

- 우선순위를 지정

- @AutoWired가 여러개의 빈을 찾게 되면 @Primary가 붙은 빈을 우선적으로 사용

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{
	......
}

@Component
public class FixDiscountPolicy implements DiscountPolicy{
	......
}

 

ex)

main DB connection 빈 <- @Primary로 지정

sub DB connection 빈 <- @Qualifier로 지정

 

1. 생성자 주입

- final로 필드를 만들고, 생성자로 의존관계를 작성 => 작성할 코드가 많음

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    /*
    	비즈니스 로직
    */
}

 

2. 생성자가 1개니깐 @AutoWired생략 가능

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    /*
    	비즈니스 로직
    */
}

 

3. Lombok적용

- Lombok의 @RequiredArgsConstructor를 사용하면 final이 붙은 필드들에 대한 생성자 코드를 자동으로 생성함

- 생성자 코드를 작성할 필요 없음

- final키워드 필드 + @AutoWired +생성자 1개 => @RequiredArgsConstructor

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    /*
    	비즈니스 로직
    */
}
@Component
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{

   private final MemberRepository memberRepository;
   
   /*
   	비즈니스 로직
   */

    
}

 

생성자 주입

- 생성자를 활용하여 의존관계를 주입

- 빈 등록과 동시에 의존관계 주입이 발생함 (의존관계 주입을 생성자로 하는데, 빈 등록을 위해 객체 생성을 하려면 생성자를 바로 호출하니깐...)

- 딱 1번만 발생

- final 키워드 사용 가능 (생성자로 바로 초기화가 가능하니깐...)

=> 불변, 필수

- 생성자가 1개만 있으면 @AutoWired를 생략해도 됨

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired // 생략 가능
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    /*
    	비즈니스 로직
    */
}

 

수정자 주입

- setter 메서드를 활용

- setter 메서드는 나중에도 호출이 가능

- final 키워드 사용 불가

=> 변경, 선택

@Component
public class OrderServiceImpl implements OrderService{

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy){
        this.discountPolicy = discountPolicy;
    }

    /*
    	비즈니스 로직
    */
}

 

필드 주입

- 그냥 필드에 @AutoWired만 써서 바로 주입

- 코드가 간결하지만, 외부에서 변경이 불가능해서 테스트가 어려움

- 스프링 프레임워크가 없으면 기능 X

- 그냥 Java코드로 테스트 어려움

@Component
public class OrderServiceImpl implements OrderService{

	@AutoWired
    private MemberRepository memberRepository;
    @AutoWired
    private DiscountPolicy discountPolicy;    

    /*
    	비즈니스 로직
    */
}

 

일반 메서드 주입

- 사용 X

@Component
public class OrderServiceImpl implements OrderService{

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy){
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    /*
    	비즈니스 로직
    */
}

 

 

어떤 주입 방법을 사용?

=> 생성자 주입 사용

- 생성자 주입은 불변, 필수의 특징이 있음

- 불변 : 의존관계는 한번 주입이 일어나면 애플리케이션 종료까지 변경될 일도 없고, 변경되면 안 됨, 생성자 주입은 생성자 호출 시 단 1회만 발생

- 필수 : 객체 생성 시 생성자 호출이 필수적

- final 키워드는 생성자 주입에서만 사용이 가능 => 값이 초기화되지 않는 경우 IDE에서 컴파일 오류로 알려줌

- 테스트 시에 생성자 파라미터를 누락한 경우 IDE에서 컴파일 오류로 알려줌

- 그냥 Java로 테스트 가능

 

- 수정자 주입이나 일반 메서드 주입은 메서드를 public으로 두기 때문에 누가 나중에 변경할 수 도 있음 => 불변 X

 

- AppConfig처럼 @Bean을 사용하여 직접 코드를 작성하고 의존 관계도 작성해서 스프링 빈으로 등록할 수 있었음

- 이렇게 스프링 빈을 작성하면, 등록할 빈의 개수가 많아지면 하나하나 작성해줘야 하고, 관리도 어려울 수 있음

=> 스프링에서는 Java설정 파일이나 XML 없이도 스프링 빈을 등록할 것들을 찾아서 등록하는 컴포넌트 스캔이라는 기능을 제공하고, @AutoWired를 사용하여 의존관계 주입도 자동으로 해줌

 

AppConfig

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberServiceImpl() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderServiceImpl() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

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

}

AutoAppConfig

- @ComponentScan사용

- @Bean사용 X

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

 

@ComponentScan

- @ComponentScan을 붙여서 컴포넌트 스캔을 사용하면 @Componet가 붙은 클래스를 스캔하여 스프링 빈으로 등록

- 스프링 빈 등록시 빈 이름은 첫글자가 소문자인 클래스명을 사용 (@Bean은 메서드 명을 빈 이름으로)

@Component
public class MemberServiceImpl implements MemberService{
	......
}

@Component
public class OrderServiceImpl implements OrderService{
	......
}

@Component
public class RateDiscountPolicy implements DiscountPolicy{
	......
}

@Component
public class MemoryMemberRepository implements MemberRepository{
	......
}

 

- 생성자에 @AutoWired를 붙이면 자동으로 의존관계를 주입해줌

- 생성자 파라미터의 Type으로 스프링 컨테이너에서 스프링 빈을 찾아서 주입함

 

 

@Component
public class OrderServiceImpl implements OrderService{   

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

   ......
}

 

1) 설정클래스에 @ComponentScan이 붙음

2) @Component가 붙은 클래스들을 클래스 명으로 스프링 반 등록함

3) @AutoWired + 생성자를 사용하여 의존관계를 자동으로 주입함 (생성자의 Type으로 빈 찾음)

=> @ComponentScan, @Component, @AutoWired는 항상 같이 사용됨

 

컴포넌트 스캔 범위

- 어플리케이션 구동 시 모든 클래스들을 스캔하려면 오래 걸리므로, 필요한 위치를 탐색하도록 지정

- @ComponetScan에 basepackage를 지정하지 않으면 @ComponetScan가 붙은 설정 클래스가 존재하는 패키지가 스캔 시작 위치 => 따로 basePackage를 지정하지 않고 설정 클래스를 프로젝트의 최상단에 두면 됨 => 하위 패키지까지 모두 스캔함

 

@Component

@Component

@Controller : 스프링 MVC 컨트롤러

@Service : 비즈니스 로직

@Repository : 데이터 접근 로직, 데이터 접근 예외를 스프링 예외로 변환해줌

@Configuration : 스프링 설정 정보

=> 컴포넌트 스캔 대상임 @Controller, @Service, @Repository, @Configuration은 내부에 @Component를 포함함

 

 

스프링 빈 중복 등록, 충돌

- @Bean : 수동등록, 메서드 명을 빈 이름으로 사용

- @Component : 자동 등록, 클래스 명을 빈 이름으로 사용

 

1) 자동등록으로 동일한 이름으로 빈을 등록하려 하는 경우 에러 발생 : ConflictingBeanDefinitionException

 

1-1) 동일한 이름은 아니지만 타입이 같은 빈을 두 개 등록하는 경우(FixDiscountPolicy, RateDiscountPolicy에 둘 다 @Component작성)

=> @AutoWired 의존관계 주입시 에러 발생 가능 (생성자 파라미터로 찾은 스프링 빈이 2개 이상...)

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderServiceImpl' defined in file [C:\Users\남궁준\Desktop\study\demo\out\production\classes\com\example\order\OrderServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
@Component
public class FixDiscountPolicy implements DiscountPolicy{
	......
}

@Component
public class RateDiscountPolicy implements DiscountPolicy{
	......
}
@Component
public class OrderServiceImpl implements OrderService{ 

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

   ......
}

 

2) 수동등록, 자동 등록으로 이름이 같은 빈을 등록하려 하는 경우 : 수동이 우선권(수동 빈이 자동 빈을 오버라이딩해버림) => 사용 X => 스프링 부트에서는 오류를 발생시킴(오버라이딩 막음)

@Component
public class MemoryMemberRepository implements MemberRepository{
	......
}
@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class) // AppConfig.class 제외
)
public class AutoAppConfig {
    @Bean(name="memoryMemberRepository")
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

- 애플리케이션은 보통 다수의 클라이언트들이 요청을 보냄, 매 요청마다 서버에 객체를 생성해서 전달하는 것은 메모리 낭비가 심함

 

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
    }

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
        //return new FixDiscountPolicy()
    }

}
public class MemberApp {

    public static void main(String[] args) {       
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();        

        Member member = new Member(1, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("findMember.getName() = " + findMember.getName());
        System.out.println("findMember.getName() = " + findMember.getGrade());
    }

}

- 위와 같이 스프링을 사용하지 않은 AppConfig는 클라이언트에서 매번 new를 통해 새로운 객체를 생성하게 됨

 

 

싱글톤 패턴

- 최초에 static영역에 객체를 1개만 생성

- 외부에서는 getInstance() 메서드를 통해서만 객체를 받아갈 수 있도록 만듦

- 외부에서 생성자 호출을 못하도록 private생성자를 만들어 둠

public Class SingleTonService() {

	private static final SingleTonService instance = new SingleTonService();
    
    public static SingleTonService getInstance() {
    	return instance;
    }
    
    private SingleTonService(){}
    
}

 

스프링 컨테이너는 스프링 빈을 싱글톤으로 관리함

- 스프링 컨테이너에서는 직접 코드로 싱글톤을 구현하지 않아도 알아서 Bean을 하나만 생성해서 싱글톤으로 관리함

- 스프링 컨테이너를 사용하면 웹 애플리케이션에서 직접 싱글톤 구현 없이 간편하게 객체들을 싱글톤으로 관리할 수 있음

public class SingleTonTest {

    @Test
    void SingleTonTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isSameAs(memberService2);

    }
    
}
memberService1 = com.example.member.MemberServiceImpl@5aac4250
memberService2 = com.example.member.MemberServiceImpl@5aac4250

 

CGLIB

- @Configuration을 사용하면 CGLIB를 사용함

- 스프링 컨테이너가 싱글톤 보장을 위해서 각 클래스를 싱글톤 패턴으로 코드를 생성하지 않고, 바이트코드를 조작하는 CGLIB 라이브러리를 사용

- 스프링 컨테이너는 AppConfig를 그냥 사용하지 않고, CGLIB를 활용하여 설정 클래스 AppConfig를 상속받는 임의의 클래스인 AppConfig$$EnhancerBySpringCGLIB$$6de7d4d1를 만들어서 스프링 Bean으로 등록

- AppConfig$$EnhancerBySpringCGLIB$$6de7d4d1 에는 스프링 Bean등록 작업을 할 때, 이미 스프링 컨테이너에 있으면, 존재하던 Bean을 반환, 없으면 생성해서 등록하는 코드가 생성되었을 것임..

public class MemberTest {

    @Test
    void MemberTest() {             
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig appConfig = ac.getBean(AppConfig.class);        

        System.out.println("appConfig.getClass() = " + appConfig.getClass());       
    }
}
appConfig.getClass() = class com.example.demo.AppConfig$$EnhancerBySpringCGLIB$$6de7d4d1