728x90

HTTP 메서드에 대해 알아본다.

 

  • HTTP API를 만들어보자

만약 학생 정보 관리 API를 만들어라 라고 한다면

지금의 우리는 URI를

- 학생 목록 조회 /read-member-list

- 학생 조회 /read-member-by-id

- 학생 등록 /create-member

- 학생 수정 /update-member

- 학생 삭제 /delete-member

 

이렇게 설계할 것이다.

 

하지만 이렇게 설계하는 것은 좋지 않은 방법이다.

리소스가 식별이 되도록 설계를 해야한다.

여기서는 '학생 목록 조회'가 리소스가 아니라, 학생 그 자체가 리소스이다.

조회든, 수정이든 신경쓰지 않고 학생이라는 리소스만 식별하면 되기 때문에 학생 리소스를 URI에 매핑한다.

그럼 이렇게 리소스가 학생이라는 것을 알았는데, 조회나 수정은 어떻게 해야할까?

우리는 이 행위를 메서드에 넘기게 된다.

 

  • HTTP 메서드

HTTP 주요 메서드 종류에는

GET : 리소스 조회

POST : 요청 데이터 처리, 주로 등록에 사용

PUT : 리소스를 대체, 해당 리소스가 없으면 생성

PATCH : 리소스 부분 변경

DELETE : 리소스 삭제

가 있다.

 

GET

리소스 조회

서버에 전달하고 싶은 데이터를 query를 통해 전달한다.

 

이렇게 서버에 리소스 조회를 한다.

 

POST

요청 데이터 처리

메시지 바디를 통해 서버로 요청 데이터를 전달하며, 들어온 데이터를 처리하는 모든 기능을 수행한다.

POST는 서버가 아직 식별하지 않은 새 리소스를 생성하거나 요청 데이터를 처리하거나, 아니면 다른 메서드로 처리하기 애매한 경우에 사용한다. (거의 만능이라고 한다)

 

PUT

기존에 있던 리소스를 대체한다.

리소스가 있으면 대체하고 리소스가 없으면 생성한다.

POST와의 차이점은 PUT은 클라이언트가 리소스의 위치를 알고 URI로 지정한다.

 

리소스를 대체하는 경우만 살펴보자.

 

PATCH

PUT과 같이 대체하는 것이 아니라 부분만 변경한다.

 

DELETE

리소스를 제거해버린다.

 

HTTP 메서드의 속성

- 안전

호출해도 리소스를 변경하지 않는다.

- Idempotent

호출 횟수에 관계 없이 같은 결과가 나온다.

- 캐시 가능

응답 결과 리소스를 캐시해서 사용해도 되는가?

 

이러한 속성들이 있다.

각각의 메서드들에 살펴보면

HTTP 메서드 안전 Idempotent 캐시 가능
GET O O O
POST X X O
PUT X O X
DELETE X O X
PATCH X X O

 

'백엔드 > HTTP' 카테고리의 다른 글

HTTP 6일차  (0) 2023.03.09
HTTP 5일차  (1) 2023.03.09
HTTP 3일차  (0) 2023.03.06
HTTP 2일차  (0) 2023.03.02
HTTP 1일차  (0) 2023.03.01
728x90

이제 부터 제대로 HTTP에 대해 알아보자.

 

  • HTTP의 기본 개념

HTTP는 HyperText Transfer Protocol의 약자로

최근에는 이 HTTP 메시지에 모든 것을 전송하고 있다.

HTML 뿐만아니라 이미지, 영상, JSON 거의 모든 형태의 데이터를 전송 가능하다.

 

지금 가장 많이 사용하는 HTTP는 1.1 버전으로 1997년에 등장하게 되었다.

그 뒤로는 2, 3 버전이 나왔으며 현재는 섞어서 사용하고는 있지만 그래도 1.1 버전을 가장 많이 사용한다.

 

전에 배웠던 TCP, UDP 중에서 TCP가 가장 좋아 쭉 TCP를 사용할 것 같지만

3 버전 이후로는 속도가 빠른 UDP를 사용하며 2와 1.1도 점점 사용하는 추세이다.

 

  • 클라이언트 서버 구조

JSP에서 공부했던 것처럼 Request와 Response 구조이다.

클라이언트는 서버에 요청을 보내고, 응답을 대기한다.

그러면 서버가 요청에 대한 결과를 만들어서 응답하는 것이다.

  • 무상태 프로토콜

서버가 클라이언트의 상태를 보존하지 않는 것을 말한다.

무상태로 서버를 만들면 응답 서버를 쉽게 바꿀 수 있다.

 

예를 들어보자.

이렇게 상태를 유지하는 서버가 있다.

클라이언트는 첫번째 서버로 A 물품, 3개의 Request의 요청을 보낸다.

그러다가 갑자기 첫번째 서버에 고장이 나면

첫번째 서버에서 상태를 저장하고 있기 때문에 큰 일이 난다.

 

하지만 상태를 저장하지 않고 요청에 정보를 담아 보내는 무상태 서버라면 그냥 다른 곳으로 응답을 요청하면 된다.

어차피 고장난 서버에서 상태를 저장하고 있지 않기 때문에

그리고 같은 이유로 서버 확장에도 유리하다.

하지만 모든 것을 무상태로 설계 할 수는 없다.

로그인과 같은 경우에는 상태 유지로 설계를 해야한다.

 

  • 비 연결성

연결을 유지하는 모델은 이렇게 응답을 받아도 TCP/IP 연결을 계속 유지한다.

하지만 연결을 유지하지 않는 모델은 응답을 받으면 TCP/IP 연결을 종료한다.

HTTP는 기본이 연결을 유지하지 않는 모델이다.

어차피 일반적으로 초 단위 이하의 빠른 속도로 응답하기 때문이다.

덕분에 서버 자원을 매우 효율적으로 사용할 수 있다.

 

하지만 아무리 빠른 속도로 응답을 하더라도 3 way handshake 시간이 추가된다.

또한 웹 브라우저로 사이트를 요청하면 HTML 뿐만 아니라 자바스크립트, css등의 자원이 함께 다운로드 된다.

그렇기에 HTTP도 지속 연결을 사용하기도 한다.

 

HTTP 지속연결은 아래와 같이 자원을 한번에 받고 연결을 종료하는 방법이다.

 

  • HTTP 메시지

위에서 HTTP 메시지에 모든 것을 전송할 수 있다고 했었다.

그럼 그 메시지들을 한 번 보도록 하자

이게 가장 대표적인 예시로 요청과 응답에 사용했던 HTTP 요청 메시지, HTTP 응답 메시지이다.

 

HTTP 메시지의 기본 구조는 아래와 같다.

이 기본 구조만 지키면 되는 것이다.

그럼 이 구조들을 하나씩 살펴보자.

 

시작 라인

이 부분은 요청과 응답이 살짝 다르다.

 

우선 요청부터 보자면

request-line = [method] [request-target] [HTTP-version]엔터

위의 요청 메시지에서 HTTP method는 GET이었고, 요청 대상은 /search?q=hi&hl=ko이며 HTTP/1.1임을 나타내고 있다.

 

method는 서버가 수행해야 할 동작을 지정하며

종류에는 GET, POST, PUT, DELETE가 있다.

 

request-target은 절대경로[?query]의 형식으로 경로와 넘길 커리를 지정한다.

 

HTTP-version은 사용한 HTTP 버전을 나타낸다.

 

응답을 보면

status-line=[HTTP-version] [status-code] [reason-phrase]

 

HTTP-version은 위처럼 사용한 HTTP 버전이다.

 

status-code는 성공과 실패를 나타낸다.

200은 성공, 400은 클라이언트 요청 오류, 500은 서버 내부 오류등을 나타낸다.

 

reason-phrase는 사람이 이해할 수 있도록 설명을 적는 곳이다.

 

헤더

header-field=[field-name:] [field-value]의 구조를 가진다.

HTTP 전송에 필요한 모든 부가정보를 담는다.

예를 들면 메시지 바디의 내용, 메시지 바디의 크기....

 

HTTP 메시지 바디

실제 전송할 데이터를 담는다.

HTML 문서, 이미지, 영상, JSON등을 모두 전송 가능하다.

'백엔드 > HTTP' 카테고리의 다른 글

HTTP 6일차  (0) 2023.03.09
HTTP 5일차  (1) 2023.03.09
HTTP 4일차  (0) 2023.03.08
HTTP 2일차  (0) 2023.03.02
HTTP 1일차  (0) 2023.03.01
728x90

URI와 웹 브라우저 요청에 대해 알아보자.

 

URI(Uniform Resource Identifier)

URI라고 하면 뭔가 URL의 오타같이 느껴질 것이다.

URL은 많이 봤어도 URI는 처음 봤을 것이다.

URI는 URL(locator), URN(name)로 나뉘고 URL은 그 종류일 뿐이다.

 

URI는

Uniform: 리소스를 식별하는 통일된 방식

Resource: 자원, URI로 식별할 수 있는 모든 것

Identifier: 다른 항목과 구분하는데 필요한 정보

의 약자이다.

 

URL: Uniform Resource Locator

URN: Uniform Resource Name

 

URL은 리소스가 있는 위치를 지정하고 URN은 리소스에 이름을 부여한다. 하지만 URN 이름만으로 실제 리소스를 찾을 수 있는 방법이 보편화 되지 않았기 때문에 보통 URL을 사용한다.

 

URL 분석

https://www.naver.com/search?q=hi&hl=ko 

를 타겟으로 분석해보자.

 

일단 URL의 전체 문법은

scheme://[userinfo@]host[:port][/path][?quert][#fragment] 이다.

 

  • scheme

주로 프로토콜(어떤 방식으로 자원에 접근할 것인가 하는 약속 규칙, http와 https 등등) 사용

http는 80포트, https는 443 포트를 사용하며 이 포트들은 생략이 가능하다.

 

  • userinfo

URL에 사용자정보를 포함해서 인증... 한다고 한다.

거의 사용하지 않기 때문에 그냥 넘어가도 될 듯 하다.

 

  • host

호스트명이다.

주로 도메인명을 사용하지만 IP 주소를 직접 사용할 수 있다.

 

  • port

어디에 접속하는지를 나타내는 포트이다.

 

  • path

리소스 경로이다.

ex) /home/target1.jpg

  • query

key=value형태로 나타낸 데이터이다.

?로 시작을 하며, 추가 할 때마다 &로 이어주면 된다.

 

  • fragment

서버에 전송하는 정보가 아닌 html 내부 북마크 등에 사용한다.

 

기본 문법을 알아보았으니, 타겟 주소로 분석을 해보자.

우선 www.google.com을 DNS 서버에서 조회해 IP 주소를 얻은 후 그곳으로 보낼 HTTP 요청 메시지를 생성한다.

 

그 때 HTTP 요청 메시지는

이렇게 만들어 질 것이다.

그 HTTP 요청 메시지가 단계를 거치며 포장이 되어 서버로 갈 것이다.

위 그림은 그 과정을 나타낸 것이다.

 

그 때 만들어진 패킷은

이렇게 메시지를 감싸서 만들어지게 될 것이다.

 

이렇게 만들어진 패킷을 서버에 보내면 서버에서 확인을 하고 응답 메시지를 만들어 보낼 것이다.

그러면 클라이언트는 이 응답 메시지를 웹 브라우저에서 렌더링하여 화면을 표시할 것이다.

'백엔드 > HTTP' 카테고리의 다른 글

HTTP 6일차  (0) 2023.03.09
HTTP 5일차  (1) 2023.03.09
HTTP 4일차  (0) 2023.03.08
HTTP 3일차  (0) 2023.03.06
HTTP 1일차  (0) 2023.03.01
728x90

HTTP 1일 차로 인터넷 네트워크에 대해서 공부한다.

 

  • 인터넷 통신

만약 컴퓨터 두 대가 직접적으로 연결이 되어 있다면

이렇게 직접 통신을 하면 될 것이다.

 

하지만 수많을 컴퓨터들이 있는 인터넷 내에서는 어떻게 통신을 할까?

이렇게 수많은 노드들을 거친 후에 서버에 도착해야 할 것이다.

과연 어떤 방법으로 서버를 찾아가게 될까?

  • IP

그 방법으로는 컴퓨터마다 고유의 번호를 부여하고 그 번호로 찾아가게 하는 것이다.

고유의 번호를 IP 주소라고 한다.

지정한 IP 주소에 패킷이라는 통신 단위로 데이터를 저장하게 된다.

이렇게 전송 데이터에 IP 관련된 정보를 넣어서 감싼 단위를 패킷이라고 한다.

패킷 안에는 IP 정보가 들어가 있기 때문에 해당 정보를 이용하여 목적지까지 패킷을 운반한다.

하지만 이 IP 프로토콜에도 한계가 있다. 

- 비연결성

만약 패킷을 받을 대상이 없거나 서비스 불가 상태여도 데이터를 전송한다.

 

- 비신뢰성

데이터가 중간에 소실될 수 있고, 패킷이 순서대로 오지 않을 수도 있다.

 

- 프로그램 구분

같은 IP를 사용하는 서버에서 통신하는 애플리케이션이 둘 이상이면 어느 애플리케이션으로 가야 하는지 알 수 없다.

  • TCP, UDP

해당 방법을 TCP에서 해결할 수 있다.

 

프로토콜 계층을 살펴보면

인터넷 프로토콜 스택의 4계층이다.

애플리케이션 계층 - HTTP, FTP
전송 계층- TCP, UDP
인터넷 계층 - IP
네트워크 인터페이스 계층

 

    이거를 사용하는 단계까지 같이 표현을 해보자면

이렇게 나타내진다.

데이터를 전송할 때에는 이 모든 과정을 거쳐 전송하게 된다.

IP 패킷으로 포장하기 전에 TCP 하나를 추가하게 되는 것인데

TCP는 이전에 IP 프로토콜에서 생겼던 문제들을 해결할 수 있다.

TCP 특징

- 연결지향(TCP 3 way handshake, 가상연결)

데이터를 전송하기 전에 서버에 접속 요청을 보낸 후, 서버에서 요청 수락을 하고 서버에서 클라이언트로 접속 요청을 보내면 그것에 응답하여 서로 연결이 된 것을 확인한 후 데이터를 전송하는 방법이다.

클라이언트와 서버 간에는 연결이 된 것을 확인할 수 있지만, 그 중간의 노드들은 접속이 된 것을 알지 못하기 때문에 가상 연결이라고도 한다.

 

- 데이터 전달 보증

데이터를 수신하면 데이터를 수신한 곳에서 송신한 곳으로 수신이 완료되었음을 알리는 신호를 보낸다.

이렇게 하면 데이터가 전달되었다는 것을 보증할 수 있다.

 

- 순서 보장

순서가 맞지 않는다면, 맞지 않는 곳에서부터 재전송을 요청하여 순서대로 도착함을 보장한다.

이렇게 TCP에 대해 알아보았다.

 

UDP는 이와 정반대의 특징을 가지는데

연결지향, 데이터 전달 보증, 순서 보장의 특징을 가지지 않아 IP와 거의 비슷하지만 속도가 훨씬 빠르다.

  • PORT

이렇게 한 번에 둘 이상 연결할 경우가 있다.

이럴 때에는 어떤 데이터가 어디에 쓰이는지 어떻게 구별할 수 있을까?

 

이럴 때에는 위에 같이 포장되어 있는 PORT의 정보를 이용한다.

 

이렇게 포트 번호를 보고 어디에서 온 데이터인지 구별하여 사용한다.

  • DNS

IP는 숫자로만 구성되어 있어 기억하기 어렵다.

우리가 웹 사이트에 접속할 때마다 저 번호를 기억해서 치고 들어가면 상당히 힘들 것이다.

그렇기 때문에 우리는 DNS(Domain Name System)을 사용한다.

도메인 명을 IP 주소로 변환해 주는 방법이다.

우리가 google에 접속하려고 하면 구글의 google.com을 치고 접속한다.

그러면 도메인 서버에서 해당 도메인명에 일치하는 IP주소를 찾아 접속을 해준다.

그러면 우리는 IP 주소를 몰라도 google에 접속할 수 있게 되는 것이다.

'백엔드 > HTTP' 카테고리의 다른 글

HTTP 6일차  (0) 2023.03.09
HTTP 5일차  (1) 2023.03.09
HTTP 4일차  (0) 2023.03.08
HTTP 3일차  (0) 2023.03.06
HTTP 2일차  (0) 2023.03.02
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

빈 스코프에 대해서 배워보자.

 

11일차에 우리는 스프링 빈이 컨테이너가 시작될 때 생성이되어 컨테이너가 종료될 때까지 유지된다고 배웠다.

싱글톤 스코프로 생성이 되기 때문인데, 스코프는 말 그대로 빈이 존재할 수 있는 범위이다.

 

스프링은 다음과 같은 스코프를 지원한다.

  • 싱글톤

기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

  • 프로토타입

스프링 컨테이너는 프로토타입 빈의 생성과 의존관계까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프이다.

  • 웹 관련 스코프

웹 요청에 관한 스코프들로 request, session, application이 있다.

 

 

빈 스코프는

@Scope("prototype")로 설정할 수 있다.

 

프로토타입 스코프

기존의 싱글톤을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환했다.

하지만 프로토타입 스코프를 조회하면 항상 새로운 인스턴스를 생성해서 반환해준다.

 

 

프로토타입 스코프의 빈을 스프링 컨테이너에 요청하면

스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

그 빈을 스프링 컨테이너는 반환한다.

 

스프링은 딱 이 과정에만 관여하고, 그 후에는 관리를 하지 않는다.

따라서 @PreDestroy와 같은 종료 메서드도 호출되지 않는다.

 

늘 하던대로 싱글톤 스코프 빈을 먼저 테스트 해보자

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonTest {
    
    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }
        
        @PreDestroy
        public void destroy(){
            System.out.println("SingletonBean.destroy");
        }
    }
    
    @Test
    public void singletonBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SingletonBean.class);
        
        SingletonBean singletonBean1 = applicationContext.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = applicationContext.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);

        Assertions.assertSame(singletonBean1, singletonBean2);
        
        applicationContext.close();
    }
}

이렇게 같은 인스턴스가 출력이 되는 것을 볼 수 있다.

 

이번에는 싱글톤이 아닌 프로토타입을 테스트해보자.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeTest {

    @Scope("prototype")
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

    @Test
    public void prototypeBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);

        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = applicationContext.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        Assertions.assertNotSame(prototypeBean1, prototypeBean2);
        applicationContext.close();
    }
}

이렇게 프로토타입을 테스트해보면

다른 인스턴스가 생성이 되는 것을 확인할 수 있다.

그리고 초기화도 2번이 실행된 것을 확인할 수 있다.

또한 프로토타입은 생성, 의존관계주입, 초기화까지만 관여하기에 @PreDestroy의 메서드가 출력되지 않는 것을 볼 수 있다.

 

프로토타입과 싱글톤을 함께 사용시의 문제점

프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 반환한다.

하지만 싱글톤 빈에 들어간 프로토타입 빈은 어떻게 될까?

 

싱글톤 빈이 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈일 것이다.

그렇기 때문에 사용할 때마다 생성되는 것이 아니며, 안에 프로토타입 빈이 있더라도 프로토타입 빈으로의 작동을 못하게 될 것이다.

이렇게 사용할 수도 있지만, 아마 이것은 프로토타입을 사용하는 취지에 어긋날 것이다.

그렇기에 항상 새로운 프로토타입 빈을 생성하는 방법에 대해 알아보자.

 

Provider로 프로토타입 스코프를 사용

프로토타입 빈을 요청받는 방법은 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

    @Scope("prototype")
    static class PrototypeBean{
        private int count = 0;

        public void addCount(){
            count++;
        }

        public int getCount(){
            return count;
        }

        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

    static class ClientBean{
        @Autowired
        private ApplicationContext applicationContext;

        public int logic(){
            PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

이렇게 의존관계를 찾는 방법을 Dependency Lookup이라고 하는데, 스프링 컨테이너 종속적인 코드가 되고 좋은 코드가 아니다.

 

따라서 다른 방법들을 사용한다.

 

ObjectFactory, ObjectProvider

Dependency Lookup 서비스를 제공하는 것이 바로 ObjectProvider이고, 이걸 상속받아 기능을 추가한 것이 ObjectProvider이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

    @Scope("prototype")
    static class PrototypeBean{
        private int count = 0;

        public void addCount(){
            count++;
        }

        public int getCount(){
            return count;
        }

        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

    static class ClientBean{
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
        
        public int logic(){
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

ClientBean 클래스의 내용을 이렇게 변경하면 된다.

이렇게 다른 인스턴스를 가지는 것을 볼 수 있다.

ObjectProvider의 getObject()를 호출하면 스프링 컨테이너에서 해당 빈을 찾아서 반환한다.

 

여기에 추가로 자바 표준을 사용하는 방법이 있다.

 

JSP-330 Provider

스프링 부트 3.0.1을 사용하는 방법이다.

우선 gradle에

implementation 'jakarta.inject:jakarta.inject-api:2.0.1'

을 추가해준다.

 

그리고 Provider에 맞게 ClientBean의 코드를 수정해준다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Provider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonWithPrototypeTest1 {

    @Scope("prototype")
    static class PrototypeBean{
        private int count = 0;

        public void addCount(){
            count++;
        }

        public int getCount(){
            return count;
        }

        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

    static class ClientBean{
        
        @Autowired
        private Provider<PrototypeBean> provider;
        
        public int logic(){
            PrototypeBean prototypeBean = provider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        Assertions.assertEquals(1, prototypeBean1.getCount());

        PrototypeBean prototypeBean2 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        Assertions.assertEquals(1, prototypeBean2.getCount());
    }

    @Test
    void singletonClientUsePrototype(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertEquals(2, count2);
    }
}

둘 중에 상황에 맞는 DL을 사용하면 된다.

 

웹 스코프

지금까지는 싱글톤과 프로토타입 스코프를 학습했다.

 

이번에는 웹 스코프에 대해서도 알아보자.

웹 스코프는 웹 환경에서만 동작한다.

그리고 프로토타입과는 다르게 해당 스코프의 종료시점까지 관리해준다.

 

종류로는

request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 요청마다 별도의 인스턴스가 생성된다.

session: HTTP Session과 동일한 생명주기를 가지는 스코프

application: ServletContext와 동일한 생명주기를 가지는 스코프

 

바로 request 스코프 예제를 만들어보자.

웹 스코프는 웹 환경에서만 동작하기 때문에 해당 라이브러리를 추가해줘야 한다.

implementation 'org.springframework.boot:spring-boot-starter-web'

해당 라이브러리를 추가하면

http://localhost:8080/ 해당 페이지를 통해 접속할 수 있게 된다.

 

request 요청이 오면 로그를 남기는 예제를 만들어보자.

우선 로그를 출력하기 위한 클래스를 만든다.

package hello.core.web;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Scope(value = "request")
public class MyLog {
    private String uuid;
    private String requestURL;
    
    public void setRequestURL(String requestURL){
        this.requestURL = requestURL;
    }
    
    public void log(String message){
        System.out.println("UUID: " + uuid + ", requestURL: " + requestURL + ", message: " + message);
    }
    
    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("UUID: " + uuid + ", request scope bean create: " + this);
    }
    
    @PreDestroy
    public void close(){
        System.out.println("UUID: " + uuid + ", request scope bean close: " + this);
    }
}

Scope를 request로 지정해서, HTTP 요청 당 하나씩 생성이 되고, 요청이 끝나는 시점에 소멸이 된다.

 

이제 Controller를 만든다.

package hello.core.web;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class LogController {

    private final LogService logService;
    private final ObjectProvider<MyLog> myLogObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.setRequestURL(requestURL);

        myLog.log("controller");
        logService.logic("testId");

        return "OK";
    }
}

request를 받아야 URL을 알 수 있기에 이 단계에서 URL을 넣어준다.

 

package hello.core.web;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogService {

    private final ObjectProvider<MyLog> myLogObjectProvider;

    public void logic(String id){
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.log("service id = " + id);
    }
}

여기에서 ObjectProvider로 MyLog를 두개 생성하지만, 같은 HTTP 요청이기 때문에 동일한 빈이 들어간다.

ObjectProvider 덕분에 Object.getObject() 호출 전까지, request scope빈의 생성을 지연할 수 있다.

 

하지만 여기에서 더 나아가

 

스코프와 프록시

프록시 방식을 사용한다.

프록시 방식은

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLog{
}

로 proxyMode를 추가하는게 핵심이다.

적용 대상이 class면 TARGET_CLASS, 적용 대상이 인터페이스면 INTERFACES를 선택한다.

이렇게 하면 MyLog의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 주입해 둘 수 있다.

 

 

이렇게 myLog를 출력해보면, 이 객체를 상속받은 다른 객체가 출력되는 것을 볼 수 있다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

빈의 생명주기

객체의 초기화 작업과 종료 작업이 어떻게 진행되는지 알아둘 필요가 있다.

예제 코드로

package hello.core.lifeCycle;

public class LifeCycleClient {

    private String print;

    public LifeCycleClient(){
        System.out.println("생성자 호출, print = " + print);
        beanStart();
        call("생성자!");
    }

    public void setPrint(String print){
        this.print = print;
    }

    //빈 시작시 호출
    public void beanStart(){
        System.out.println("beanStart: " + print);
    }

    public void call(String message){
        System.out.println("call: " + print + " message = " + message);
    }

    //빈 종료시 호출
    public void beanClose(){
        System.out.println("close: " + print);
    }
}

이런 class를 만들어보자.

우리는 이 class로 빈의 생명주기에 대해 알아볼 것이다.

package hello.core.lifecycle;

import hello.core.lifeCycle.LifeCycleClient;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean//(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그리고 테스트코드로 해당 빈을 열고 닫아보자.

실행해 보면

이렇게 print에 값이 들어가 있지 않은 것을 볼 수 있다.

생성자를 통해 생성하는 단계에는 print에 대한 값이 들어가있지 않고, 모두 생성이 된 후에 setter를 통해 값이 들어가기 때문이다.

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 준비가 된다.

그렇기에 스프링은 의존관계 주입이 완료되면 스프링 빈에게 초기화 시점을 알려주는 기능을 제공한다. 당연히 스프링 컨테이너가 종료되기 직전에 소멸을 알려주는 기능도 제공한다.

 

스프링 빈의 LifeCycle

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료

의 주기를 가진다.

 

물론 생성자에서 로직들을 추가하여 초기화를 할 수도 있지만, 유지보수를 위해 객체를 생성하는 부분과 초기화하는 부분을 분리는 것이 좋다.

 

생명주기 콜백

스프링은 3가지 방법으로 빈 생명주기 콜백을 지원한다.

InitializingBean, DisposableBean 인터페이스

설정 정보에 초기화 메서드, 종료 메서드를 지정

@PostConstruct, @PreDestroy annotation

 

  • InitializingBean, DisposableBean 인터페이스

InitializingBean, DisposableBean 인터페이스를 implements 하고 해당 인터페이스들에 있는 메서드들을 afterPropertiesSet(), destroy() 메서드들을 구현하는것이다.

package hello.core.lifeCycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class LifeCycleClient implements InitializingBean, DisposableBean {
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }


    public void afterPropertiesSet() throws Exception{
        System.out.println("NetworkClient.init");
        beanStart();
        call("초기화 연결 메시지");
    }

    public void destroy() throws Exception{
        System.out.println("NetworkClient.close");
        beanClose();
    }
}

이렇게 해당 메서드들을 구현하고 테스트를 진행해보자.

이렇게 생성자가 먼저 호출이 되고, 그 후에 초기화 콜백 메서드가 실행이 된다.

빈의 로직들이 실행이 되고, 소멸 전 콜백 실행 해 소멸이 되는 것을 볼 수 있다.

이 방법은 메서드의 이름을 변경할 수 없고 내가 코드를 고칠 수 없는 외부 라이브러리에는 적용이 불가능하여 스프링 초기에 사용하던 방법이고 최근에는 사용하지 않는다고 한다.

 

  • 설정 정보에 초기화 메서드, 종료 메서드를 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")를 입력해 메서드들을 지정할 수 있다.

바로 해보도록 하자, 우선 LifeCycleClient를 좀 수정하고

package hello.core.lifeCycle;

public class LifeCycleClient{
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }
    
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

@Bean에 initMethod와 DestroyMethod를 추가한다.

package hello.core.lifeCycle;

import hello.core.lifeCycle.LifeCycleClient;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그러면

이렇게 생명주기에 맞게 실행이 되는 것을 볼 수 있다.

@DestroyMethod에 추록기능이 있어 close, shutdown을 찾아가지만 그래도 지정해주도록 하자.

 

  • @PostConstruct, @PreDestroy annotation

가장 많이 사용하는 방법이라고 한다, 하지만 외부 라이브러리에는 적용하지 못하기 때문에 외부 라이브러리에 사용하려면 위의 방법을 사용해야 한다고 한다.

예상했던대로

package hello.core.lifeCycle;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class LifeCycleClient{
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    @PostConstruct
    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }

    @PreDestroy
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

그냥 이렇게 @PostConstruct, @PreDestroy annotation을 달아주는 방법이다.

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

다양한 의존관계 주입 방법

의존관계 주입은 4가지 방법이 있다.

  • 생성자 주입
  • 수정자 주입
  • 필드 주입
  • 일반 메서드 주입

 

  • 생성자 주입

말 그대로 생성자를 통해서 의존 관계를 주입받는다.

이제까지 우리가 사용했던 방법이며, 생성자가 호출되는 시점에서 딱 1번만 호출된다.

불변, 필수 의존관계에 사용이 된다.

 

생성자에 @Autowired를 달아서 사용하며, 생성자가 하나만 있으면 생략이 가능하다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

  • 수정자 주입(setter 주입)

setter를 만들고 그 메서드를 통해서 의존관계를 주입하는 방법이다.

선택, 변경 가능성이 있는 의존관계에 사용한다.

@Component
public class OrderServiceImpl implements OrderService{
	private CustomerRepository customerRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void setCustomerRepository(CustomerRepository customerRepository){
    	this.customerRepository = customerRepository;
    }
    
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy){
    	this.discountPolicy = discountPolicy;
	}
}

@Autowired는 주입할 대상이 없으면 오류가 발생한다.

주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정해야 한다.

 

  • 필드주입

이름 그대로 필드에 주입하는 방법이다.

코드는 간단하지만, 사용하지 말자.

외부에서 변경이 불가능해 테스트가 힘들고, DI 프레임워크가 없으면 아무것도 할 수 없다.

@Component
public class OrderServiceImpl implements OrderService{
	@Autowired
    private CustomerRepository customerRepository;
    
    @Autowired
    private DiscountPolicy discountPolicy;
}

 

  • 일반 메서드 주입

당연히 일반 메서드를 통해서도 주입 받을 수 있다.

하지만 일반적으로 잘 사용하지는 않는다.

@Component
public calss OrderServiceImpl implements OrderService{

    private CustomerRepository customerRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void init(CustomerRepository customerRepository, DiscountPolicy discountPolicy){
    	this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

가끔은 주입할 스프링 빈이 없어도 동작을 해야한다.

그럴 때는 위에서 말한 방법처럼 @Autowired(required = false)를 사용할 수 있다.

이외에도 방법들이 더 있는데

package hello.core.autowired;

import hello.core.customer.Customer;
import jakarta.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.Optional;

public class AutowiredTest {

    @Test
    void AutowiredOption(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestBean.class);
    }


    static class TestBean{
        @Autowired(required = false)
        public void setNoBean1(Customer noBean1){
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Customer noBean2){
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Customer> noBean3){
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

@Autowired(required = false): 자동 주입할 대상이 없으면 메서드가 호출이 되지 않는다.

@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.

Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

 

이렇게 주입에는 많은 방법들이 있지만, 생성자 주입이 가장 좋다고 한다.

대부분의 의존관계들은 변경할 필요가 없으며, final로 사용할 수 있고, setter로 의도하지 않은 수정이 일어나지 않기 때문에 가장 좋다.

 

Lombok

우리는 의존관계를 생성자를 통해 주입하기로 하였다.

그러면 이 방법을 더 간단하게 할 수 없을까?

그 방법이 Lombok이다.

기존에 사용하던 OrderServiceImpl이다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

생성자가 하나이기 때문에 @Autowired를 생략 할 수 있다.

 

그리고 Lombok을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;
}

이렇게만 해주어도 자동으로 생성자를 만들고 의존관계를 주입해준다.

Lombok 적용하는 방법은 소개하지 않도록 하겠다.

 

조회 빈이 2개 이상

@Autowired는 타입에 맞는 의존관계를 넣어준다.

만약 같은 타입이 2가지 이상이라면 어떻게 될까?

지금 DiscountPolicy에서 FixDiscountPolicy, RateDiscountPolicy가 있는 것 처럼 말이다.

 

일단 둘 다 @Component annotation을 달아주자.

그러고 테스트에 작성한 basicScan을 실행해보면

이런 오류가 발생하게 된다.

 

해결방법으로는

@Autowired 필드 명 매칭

@Qualifier

@Primary

가 있다.

 

  • Autowired 필드 명 매칭
package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

기존에 이렇게 작성된 코드에서 

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy rateDiscountPolicy;

    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.rateDiscountPolicy = discountPolicy;
    }
}

로 필드 명을 rateDiscountPolicy(빈 이름)으로 변경한 것이다.

이러면 우리가 원하는 rateDiscountPolicy가 주입이 된다.

 

  • Qualifier

@Qualifier이라는 추가 구분자를 붙여주는 방법이다.

빈 이름을 변경하는 것은 아니고, 단순히 비교에만 사용한다.

 

이렇게 빈 등록할 때 @Qualifier를 붙여준다.

@Component
@Qualifier("rateDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{}

 

 

이렇게 붙여주고 생성자에서

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, @Qualifier("rateDiscountPolicy") DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

@Qualifier를 추가해준다.

 

  • @Primary

@Primary로 우선 순위를 정하는 방법이다.

@Autowired시에 여러개의 빈이 충돌하면 @Primary가 우선권을 가진다.

 

주입할 빈에 가서

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{}

@Primary를 추가해준다.

 

우선순위로는 @Qualifier가 @Primary보다 더 높다.

 

Annotation 직접 만들기

가끔씩 annotation을 직접 만들어서 사용하기도 한다고 한다.

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Indexed;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("myDiscountPolicy")
public @interface MyDiscountPolicy {
}

이렇게 @Component들의 요소를 끌어오고 우리가 사용할 @Qualifier도 달아준다.

@Component
@rateDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{}

그러고 이렇게 달아주면 우리가 원하는 annotation을 달아줄 수 있다.

 

생성자에도 이렇게 annotation을 추가해주면 된다.

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.annotation.MyDiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, @MyDiscountPolicy DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }
}

annotation에는 상속이라는 개념이 없지만, 여러 annotation을 모아서 사용하는 기능을 spring에서 제공해준다.

하지만 spring에서 제공하는 기능이라고 해도 너무 목적없이 막 사용하지는 말자.

 

다양한 빈들이 필요할 때 List, Map

다양한 빈들 중 필요한 것을 찾아서 사용할 때가 있다.

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.Grade;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.List;
import java.util.Map;

public class AllBeanTest {

    @Test
    void findAllBean(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = applicationContext.getBean(DiscountService.class);
        Customer customer = new Customer(1L, "A", Grade.VIP);

        int discountPrice = discountService.discount(customer, 10000, "rateDiscountPolicy");

        Assertions.assertEquals(500, discountPrice);
    }

    static class DiscountService{
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policyList;

        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
            this.policyMap = policyMap;
            this.policyList = policyList;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = "+ policyList);
        }

        public int discount(Customer customer, int price, String discountCode){
            DiscountPolicy discountPolicy = policyMap.get(discountCode);

            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);

            return discountPolicy.discount(customer, price);
        }
    }
}

이 코드를 보면 discount에 관련된 빈들을 Map에 저장을 해서 검색을 하며 사용하고 있다.

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 12일차  (0) 2023.02.15
스프링 11일차  (0) 2023.02.12
스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07
스프링 7일차  (0) 2023.02.07
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

지금까지는 스프링 빈을 등록할 때 자바의 @Bean을 사용하였다.

등록할 스프링 빈의 수가 많아 질수록 등록하는 데에 걸리는 시간일 길어진다.

그렇기 때문에 스프링은 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.

의존관계도 자동으로 주입해주는 @Autowired도 제공을 해준다.

 

기존에 만들었던 AppConfig.java에 추가로 AutoAppConfig.java를 만들자.

package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

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

기존 AppConfig는

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        System.out.println("call AppConfig.customerService");
        return new CustomerServiceImpl(customerRepository());
    }


    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    @Bean
    public CustomerRepository customerRepository(){
        System.out.println("call AppConfig.customerRepository");
        return new MemoryCustomerRepository();
    }

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

안에 @Bean을 나열한 것에 비해 코드가 상당히 짧을 것을 볼 수 있다.

 

이제 @ComponentScan에서 자동으로 읽을 대상이 될 수 있도록 @Component annotation을 붙여주자.

MemoryCustomerRepository

package hello.core.customer;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class MemoryCustomerRepository implements CustomerRepository{

    private static Map<Long, Customer> store = new HashMap<>();

    @Override
    public void save(Customer customer) {
        store.put(customer.getId(), customer);
    }

    @Override
    public Customer findById(Long id) {
        return store.get(id);
    }
}

 

RateDiscountPolicy

package hello.core.Discount;

import hello.core.customer.Customer;
import hello.core.customer.Grade;
import org.springframework.stereotype.Component;

@Component
public class RateDiscountPolicy implements DiscountPolicy{

    private int VIPDiscountPercent = 5; // 할인되는 비율
    private int VVIPDiscountPercent = 10; //할인되는 비율

    @Override
    public int discount(Customer customer, int price) {
        if(customer.getGrade() == Grade.VIP){
            return price * VIPDiscountPercent / 100;
        }
        else if (customer.getGrade() == Grade.VVIP){
            return price * VVIPDiscountPercent / 100;
        }
        else{
            return 0;
        }
    }
}

 

CustomerServiceImpl

package hello.core.customer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

 

OrderServiceImpl

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

        return new Order(id, itemName, itemPrice, discountPrice);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

생성자에 @Autowired를 사용하면 의존관계를 주입받을 수 있다.

 

이렇게 고치고 AutoAppConfigTest.java를 만들어보자.

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AutoAppConfigTest {

    @Test
    void basicScan(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        CustomerService customerService = applicationContext.getBean(CustomerServiceImpl.class);
        Assertions.assertInstanceOf(CustomerService.class, customerService);
    }
}

테스트를 해보면 AppConfig와 동일하게 작동하는 것을 볼 수 있다.

 

ComponentScan과 Autowired의 기본 전략

@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.

그 때 빈 이름은 맨 앞글자만 소문자로 바꾼 후 사용한다.

바꿀 수는 있지만 보통은 그냥 사용한다.

 

@Autowired를 사용하면 의존관계를 자동으로 주입해주는데, 기본적으로 타입이 같은 빈을 찾아서 주입하게 된다.

 

탐색 위치와 기본 스캔 대상

모든 코드들을 찾으면서 @Component를 찾다보면 시간이 오래 걸릴 것이다.

그래서 탐색 범위를 지정해 줄 수 있다.

 

basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 해당 패키지의 하위 패키지까지 모두 탐색한다.

basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.

 

만약 범위를 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작위치가 된다.

 

요즘은 이 설정 정보를 프로젝트 최상단에 올린 후 스캔 하는 것이 권장된다고 한다.

스프링의 시작 정보인 @SpringBootApplication 안에도 @ComponentScan이 들어있어 프로젝트 시작 루트 위치에 두는 것이 관례이다.

 

컴포넌트 스캔의 대상에는

@Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함된다.

  • @Controller: 스프링 MVC 컨트롤러에서 사용, 스프링 MVC 컨트롤러로 인식.
  • @Service: 스프링 비즈니스 로직에서 사용, 특별한 처리는 하지 않는다.
  • @Repository: 스프링 데이터 접근 계층에서 사용, 스프링 데이터 접근 계층으로 인식.
  • @Configuration: 스프링 설정 정보에서 사용, 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

해당 annotation들을 보면 @Component를 포함하고 있다.

 

필터

필터를 사용해서 스캔 대상을 추가로 지정하거나 스캔 대상에서 재외 할 수 있다.

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

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

 

예제 Component들을 만들고 실습해보록 하자.

우선 annotation들을 만들고

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TmpExcludeComponent {

}
package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TmpIncludeComponent {
    
}

우리가 스캔할 class들에 달아준다.

package hello.core.scan.filter;

@TmpIncludeComponent
public class MyBeanA {

}
package hello.core.scan.filter;

@TmpExcludeComponent
public class MyBeanB {

}

이 class들을 스캔해보자

package hello.core.scan.filter;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

public class ComponentFilterAppConfigTest {

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = TmpIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = TmpExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{

    }

    @Test
    void filterScan() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);

        MyBeanA myBeanA = applicationContext.getBean("myBeanA", MyBeanA.class);
        Assertions.assertNotNull(myBeanA);

        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean("myBeanB", MyBeanB.class));
    }
}

이렇게 테스트를 해보면 includeFilter에 들어간 Component들만 스캔이 되고 exclydeFilter에 들어간 Component는 스캔이 되지 않는 것을 볼 수 있다.

 

중복 등록과 충돌

스프링에서 이름이 같은 경우에 충돌하는 경우가 생긴다.

 

1. 자동 빈 등록 VS 자동 빈 등록

이 경우에는

이렇게 ConflictionBeanDefinitionException이 발생하게 된다.

그럴 때에는 이름을 바꿔주도록 하자.

 

2. 수동 빈 등록 VS 자동 빈 등록

이 경우에는 수동 빈이 자동 빈을 오버라이딩 해버린다.

 

그리고 그런 경우에는 이런 로그가 남는다.

하지만 이런 경우에 의도하지 않는 버그가 만들어 질 수도 있으니, 피하도록 하고 최근에는 스프링 부트에서 오류가 발생하도록 해주고 있다.

최대한 명확하게 사용하도록 하지.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
스프링 8일차  (0) 2023.02.07
스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06

+ Recent posts