1. 쿠키 (cookie)

  • 서버에서 생성되어 클라이언트 측에 저장되는 <이름, 값> 쌍 형태의 데이터
  • 쿠키 만료 기간과 같은 추가적 속성값도 가질 수 있음
  • HTTP의 스테이트리스한 특성을 보완하기 위한 대표적 수단
  • atuhorization, shopping carts, recommendation, user session state(Web e-mail)등에 사용될 수 있음

 

1.1.  응답 메시지

서버가 클라이언트에게 쿠키를 전송할 때는 응답 메시지의 Set-Cookie가 활용됨

Set-Cookie: 이름=값
Set-Cookie: 이름=값; 속성1
Set-Cookie: 이름=값; 속성1; 속성2
  • 도메인과 경로, 유효기간, 보안 등이 대표적인 쿠키 속성값
    • 'domain=example.net' : 쿠키를 사용할 도메인을 example.net으로 제한
    • 'path=/lectures' : 쿠키를 사용할 경로를 /lectures로 제한
    • 'Expires=요일, DD-MM-YY HH:MM:SS GMT' ( 'Expires=Fri, 23 Aug 2024 09:00:00 GMT' ) : 명시된 유효기간이 지나면 해당 쿠키는 삭제 되어 전달되지 않음
    • 'Max-Age=3000000' : 초 단위 유효기간 
    • 'Secure' : HTTP의 더 안전한 방식인 HTTPS를 통해서만 쿠키를 송수신하도록 하는 속성
    • 'HttpOnly': 자바스크립트를 통한 쿠키의 접근을 제한, 오직 HTTP 송수신을 통해서만 쿠키에 접근

 

1.2. 요청 메시지

클라이언트는 Cookie 헤더로 전달받은 쿠키를 서버에 전송

Cookie: 이름=값; 이름=값;
  • 특정 서버로부터 쿠키를 전달받았다면 다음부터 해당 서버에 요청을 보낼 때 전달받은 쿠키를 자동으로 전송

 

1.3. 쿠키와 보안

  • 쿠키는 사이트가 나에 대해 너무 많은 정보를 알게 함.
  • third party persistent cookies(tracking cookie): 여러 웹사이트에 걸쳐 사용자의 활동을 추적할 수 있게 해주는 쿠키

 

 

더보기

💁🏻‍♀️웹 스토리지란?

  • 웹 브라우저 내의 저장공간으로, 일반적으로 쿠키보다 더 큰 데이터를 저장할 수 있음
  • 쿠키는 서버로 자동 전송되지만, 웹 스토리지의 정보는 서버로 자동 전송되지 않음
  • 로컬 스토리지
    • 별도로 삭제하지 않는 한 영구적으로 저장이 가능한 정보
  • 세션 스토리지
    • 세션이 유지되는 동안(쉽게 말해, 브라우저가 열려 있는 동안) 유지되는 정보

 

 

2. 캐시

  • 응답 받은 자원의 사본을 임시 저장하여 불필요한 대역폭 낭비와 응답 지연을 방지하는 기술
  • 목표: satisfy client request without involving origin server
  • browser는 모든 HTTP request를 캐시에 보내고 object가 cache에 있으면 cache가 object를 반환하고, 없으면 cache가 origin server에 요청해 object를 받은 후 client에 반환해줌 (web cache는 origin server에게는 client, requesting client에게는 server)
  • 캐시는 주로 ISP(대학, 회사, residential ISPs)에 의해 운영됨
  • 전용 캐시: 클라이언트에 저장
  • 공용 캐시: 클라이언트와 서버 사이에 위치한 중간 서버에 저장

 

 

2.1. 캐시 사용 이유

  • reduce response time for client request (응답 속도 향상)
    • cache is closer to client
  • reduce traffic on an institutions access link
  • enhance entire performance of Internet
  • 인터넷에 캐시가 많이 존재하면, 콘텐츠 제공자가 서버 자원이 부족하더라도 캐시를 이용해 콘텐츠를 더 빠르고 효율적으로 전달 (cf. P2P 파일 공유)

 

 

2.2. 캐시 유효 기간

  • 대부분의 캐시된 데이터에는 유효기간이 설정되어 있음.
    • 유효기간 부여 방법 (응답 메시지)
      • Expires: Fri, 23 Aug 2024 09:00:00 GMT
      • Cache-Control: max-age=1200
    • 클라이언트는 응답받은 자원을 임시 저장하여 이용하다가 유효기간이 만료되면 다시 서버에 자원을 요청해야
  • 유효기간이 부여되는 이유는?
    • 클라이언트가 캐시를 참조하는 사이 서버의 원본 데이터가 변경되어 원본 데이터와 캐시된 사본 데이터 간의 일관성이 깨질 수 있음
    • 유효기간을 설정하고 만료된 자원을 재요청함으로써 캐시 신선도를 검사할 수 있고, 원본 데이터가 변경되었을 때 해당 자원을 다시 응답받음으로써 캐시 신선도를 높게 유지할 수 있음
      • 캐시 신선도(cache freshness) : 캐시된 사본 데이터가 서버의 원본 데이터와 얼마나 유사한지의 정도
  • 캐시 데이터의 유효기간이 자나면 반드시 서버로부터 다시 자원을 응답받아야 할까?
    • No! 캐시된 데이터가 여전히 최신 데이터라면 굳이 서버로부터 같은 자원을 응답 받을 필요가 없음
    • -> 원본 자원이 변경된 적 있는지 질의

 

 

2.2.1. 원본 자원 변경 여부

  • 원본 자원의 변경 여부를 묻는 헤더 (요청 메시지)
    • 'If-Modified-Since: Fri, 23 Aug 2024 09:00:00 GMT' : 특정 시점 이후로 원본 자원에 변경이 있었다면, 그때만 변경된 자원을 메시지 본문으로 응답하도록 서버에 요청하는 헤더
    • Etag(Entity Tag) : 자원의 버전을 식별하기 위한 정보. 자원을 변경할 때 마다 Etag값이 변경됨
      • If-None-Match: "abc" : 명시된 Etag 값("abc")과 일치하는 Etag가 있는지, 없다면 변경된 자원으로 응답하도록 요청하는 헤더
  • 서버가 요청받은 자원이 변경된 경우
    • 서버는 상태 코드 200(OK)과 함께 새로운 자원을 반환
  • 서버가 요청받은 자원이 변경되지 않은 경우
    • 메시지 본문 없이 304 (Not Modified)를 통해 클라이언트에게 자원이 변경되지 않았음을 알림
    • Last-Modified 헤더로 자원의 마지막 변경 시점을 알릴 수 있음
  • 서버가 요청받은 자원이 삭제된 경우
    • 서버는 상태 코드 404(Not Found)를 통해 자원이 존재하지 않음을 알림

도메인 네임과 DNS

DNS(Domain Name System)

  • 분산형 계층형 데이터베이스 
    • 왜 분산형인가? 
      • single point of failure : 한 곳에서 서버가 다운되면, 전체가 영향받을 수 있음
      • traffic volume: 하나의 서버가 모든 요청을 처리할 경우 트래픽의 병목 현상과 성능 저하를 초래할 수 있음
      • distant centralized database: 가까운 DNS 서버에서 빠르게 응답을 받을 수 있음
      • maintenance: 전세계의 수많은 도메인 이름을 하나의 중앙 서버에서 관리하는 것은 비효율적이고 복잡
  • application layer protocol

Domain Name(도메인 네임) 

  • 'https://www.naver.com'과 같은 문자열 형태의 호스트 특정 정보로, 호스트의 IP 주소와 대응
    • IP 주소에 비해 기억하기 쉬움
    • IP 주소가 바뀌더라도 바뀐 IP주소에 도메인 네임을 다시 대응하면 됨 
  • host aliasing
  • mail server aliasing
  • load distribution: 다량의 request를 여러 웹서버가 다루도록 알아서 distribute

 Name Server(네임 서버)

  • 도메인 네임과 그에 대응하는 IP 주소를 관리하는 서버
  • domain name을 관리하는 네임 서버는 DNS 서버라고도 부름
  • 호스트는 네임서버에 '특정 도메인 네임을 가진 호스트의 IP 주소가 무엇인지' 질의함으로써 패킷을 주고받고자 하는 호스트의 IP주소를 얻어낼 수 있음 (리졸빙)

도메인 네임의 계층적 구조

  • 최상단에 root domain(루트 도메인)이 있고, 그 다음 단계에 최상위 도메인(Top-Level Domain, TLD)이 있음 (ex. com, net, org, kr, jp, cn, us 등)
  • 최상위 도메인 하부 도메인은 2단계 도메인(second-level domain), 2단계 도메인의 하부 도메인은 3단계 도메인
  • 도메인의 단계는 더 늘어날 수도 있지만, 일반적으로 3~5단계 정도로 구성
  • 전체 주소 도메인 네임(FQND): 도메인 네임을 모두 포함하는 도메인 네임 (ex. www.example.com.)

 

네임 서버의 계층적 구조

  • 네임 서버는 여러 개가 존재하며, 전 세계 여러곳에 위치함. 네임 서버는 분산되어 관리됨
  • 도메인 네임 시스템(DNS): 계층적으로 분산되어 있는 도메인 네임에 대한 관리 체계
  • 도메인 네임을 통해 IP 주소를 알아내는 과정
    • 호스트는 가장 먼저 로컬 네임 서버에게 도메인 네임을 질의

 

Local DNS name server

  • 계층 구조에 속하지 않음
  • 각 ISP(residential ISP, 회사, 대학)은 각각 하나의 Local DNS name server를 가지고 있음
  • Default Name server(기본 네임 서버)라고도 불리며, 사용자가 도메인 이름을 입력하면 가장 먼저 쿼리가 보내지는 서버
  • 프록시 역할: Local DNS name server는 사용자를 대신해 프록시 역할을 하여, DNS 쿼리를 상위 DNS 서버에 전달하는 역할을 함. 

 

DNS Name Resolution

 Iterated query

  • Local DNS server에서 모든 도메인 네임 요청을 처리한다.

  1. host가 local DNS server에 DNS 서비스 요청을 한다.
  2.  local DNS server가 도메인 네임을 가지고 있지 않아 root DNS server에 물어본다.
  3. root DNS server도 갖고 있지 않아 TLD DNS server 정보를 알려준다.
  4. TLD server에게 정보를 묻는다.
  5. TLD server도 정보가 없어 authoritative DNS server를 알려준다
  6. authoritative DNS server에 접속한다.
  7. 도메인 네임에 대한 IP주소를 얻는다.

Recursive query

 

Authoritative DNS server: 특정 도메인에 대해 최종적인 IP주소 정보를 제공하는 DNS 서버. 해당 도메인에 대한 정확한 매핑 정보를 가지고 있음. 조직이나 외부 제공자를 통해 운영될 수 있음.

 

DNS Caching

  • DNS 서버는 한번 도메인-주소 매핑 정보를 학습하면 캐시에 저장. 동일한 요청에 대해 더 빠르게 응답할 수 있게 하여 성능을 향상.
  • 캐시된 항목들은 일정시간(TTL)이 지나면 자동으로 만료됨. TTL이 만료되면 해당 캐시 항목은 삭제됨
    • TTL: Time To Leave
  • TLD 서버에 대한 정보는 로컬 DNS 서버에 자주 캐시됨. 덕분에 Root DNS 서버는 자주 방문되지 않음
  • out of date 문제 발생할 수 있음
    • 도메인의 호스트가 IP 주소를 변경했을 때, 모든 TTL이 만료될 때까지 전 세계적으로 그 변경 사항이 알려지지 않을 수 있음
    • IETF(Internet Engineering Task Force)에서 갱신 및 알림 메커니즘(update/notify mechanisms)이 제안

 

DNS records

Resource Records(RR)

  • DNS 데이터베이스의 각 레코드
  • RR format: (name, value, type, ttl)

유형

  • type = A
    • name = hostname
    • value = IP address
  • type = NS
    • name = domain
    • value = hostname of authoritative name server
  • type = CNAME
    • name = alias name
    • value = canonical(real) name
  • type = MX
    • name = domain
    • value = name of mailserver

DNS message format

  • DNS query와 reply message 모두 같은 message format
  • message header
    • identification: 16bit 숫자. 같은 숫자로 query에 reply
    • flags
      • Query or Reply(QR) : 쿼리인지 응답인지 구분
      • Recursion Desired(RD): 재귀적 응답을 원하는지
      • Recursion Available(RA): 재귀적 쿼리를 지원하는지
      • Reply is Authoritative Answer(AA): 응답이 권한이 있는 서버에서 온 것인지

 

DNS에 레코드 삽입하는 과정

  • "Network Utopia(NU)"가 DNS에 레코드를 삽입하는 과정을 예시로 설명
  1. 도메인 등록: NU는 DNS 등록 기관(예: Network Solutions)을 통해 nu.com이라는 도메인을 등록
  2. TLD 서버에 레코드 삽입: 등록 기관은 .com TLD 서버에 두 개의 리소스 레코드(RR)를 삽입
    1. NS 레코드: (nu.com, dns1.nu.com, NS)
    2. A 레코드: (dns1.nu.com, 21.21.21.2, A)
  3. 권한 있는 서버 생성
    1. NU는 IP 주소가 21.21.21.2인 권한 있는 DNS 서버를 로컬에서 설정
  4. A 레코드 및 MX 레코드 추가
    1. A 레코드 추가: NU는 'www.nu.com' 대한 A 레코드를 생성하여 이 도메인이 특정 IP 주소(21.21.21.2)와 매핑되도록 함
    2. MX 레코드 추가: NU는 nu.com에 대한 MX 레코드를 생성하여 이 도메인으로 수신되는 이메일을 처리할 메일 서버를 정의

 

 

 

자원(Object)과 URI/URL

 

  • 자원: 네트워크 상의 메시지를 통해 주고받는 최종 대상. 즉, 두 호스트가 네트워크를 통해 서로 정보를 주고받을 때 송수신하는 대상 (ex. HTML 파일, 이미지 파일, 동영상 파일 등)

URI (Uniform Resource Identifier)

  • 웹상에서 자원을 식별하기 위한 정보. 자원을(Resource)을 식별(Identifier)하는 통일된 방식(Uniform)
  • URN (Uniform Resource Name): 이름으로 자원을 식별하는 방식
  • URL (Uniform Resource Locator): 위치로 자원을 식별하는 방식

 

 

URL의 구조

  1. scheme
  • 자원에 접근하는 방법
  • 일반적으로는 사용할 프로토콜이 명시됨 (ex. http://, https://)
  1. authority
  • 호스트를 특정할 수 있는 IP 주소나 도메인 네임이 명시됨
  • 콜론(:) 뒤에 포트 번호를 명시할 수도 있음
  1. path
  • 자원이 위치하고 있는 경로가 명시됨
  • 슬래시(/)를 기준으로 계층적으로 표현되며, 최상위 경로 또한 슬래시로 표현
  1. query
  • URL에 대한 매개변수 역할을 하는 문자열
  • 쿼리 문자열, 쿼리 파라미터 등으로도 불림
  • 자원을 식별하기 위한 추가적인 정보
  • ? 키=값 형태, &를 사용하여 여러 쿼리 문자열을 연결할 수 있음 (ex. ?location=seoul&rooms=2&size=100)
  1. fragment
  • 자원의 일부분, 자원의 한 조각을 가리키기 위한 정보
  • 해시 기호(#) 뒤에 오는 문자열
  • 특정 섹션이나 리소스를 식별하는 데 사용

URN

  • URL은 자원의 위치가 변하면 유효하지않음. 이에 반해 URN은 자원에 고유한 이름을 붙이기 때문에 위치와 무관한게 자원 식별 가능
  • URL이 더 많이 사용됨

HTTP의 특징과 메시지 구조

HTTP 메시지 구조

좌: HTTP request message 우: HTTP response mssage

  • method: 클라이언트가 서버의 자원에 대해 수행할 작업의 종류

 

 

HTTP 메서드와 상태 코드

HTTP 메서드

GET 자원을 습득하기 위한 메서드
HEAD 헤더만을 응답받는 메서드
POST 특정 잡업을 처리하게끔 하는 메서드
PUT 자원을 대체하기 위한 메서드
PATCH 자원에 대한 부분적 수정을 위한 메서드
DELETE 자원을 삭제하기 위한 메서드
CONNECT 자원에 대한 양방향 연결을 시작하는 메서드
OPTIONS 사용 가능한 메서드 등 통신 옵션을 확인하는 메서드
TRACE 자원에 대한 루프백 테스트를 수행하는 메서드

 

HTTP 상태코드

100번대(100~199) 정보성 상태 코드
200번대(200~299) 성공 상태 코드
300번대(300~399) 리다이렉션 상태 코드
400번대(400~499) 클라이언트 에러 상태 코드
500번대(500~599) 서버 에러 상태 코드

 

 

HTTP 주요 헤더

요청 메시지에서 주로 활용되는 HTTP 헤더

Host

  • 요청을 보낼 호스트가 명시되는 헤더
  • 도메인 네임이나 IP주소로 표현되며, 포트 번호가 포함될 수도 있음

User-Agent

  • 요청 메시지를 보낸 클라이언트의 프로그램과 관련한 정보가 명시됨
  • HTTP요청을 보낸 클라이언트의 접속 수단(ex. 웹 브라우저)을 유추할 수 있음

Referer

  • 클라이언트가 요청을 보낼 때 머무르던 URL이 명시됨
  • 클라이언트의 유입 경로를 파악할 수 있음

 

응답 메시지에서 주로 활용되는 HTTP 헤더

Server

  • HTTP 응답 메시지를 보내는 서버 호스트와 관련된 정보가 명시됨

Allow

  • 처리 가능한 HTTP 헤더 목록을 알리기 위해 사용됨

Location

  • 클라이언트에게 자원의 위치를 알려 주기 위해 사용됨
  • 주로 리다이렉션이 발생했을 때나 새로운 자원이 생성되었을 때 사용됨

 

요청과 응답 메시지 모두에서 활용되는 HTTP 헤더

Date

  • 메시지가 생성된 날짜와 시각에 관련한 정보를 담은 헤더

Content-Length

  • 메시지 본문의 바이트 단위 크기를 표현하기 위해 사용

Content-Type, Content-Language, Content-Encoding

  • 메시지 본문이 어떻게 표현되었는지와 관련된 표현 헤더
  • Content-Type: 본문에서 사용된 미디어 타입
  • Content-Language: 메시지 본문에 어떤 자연어가 사용되었는지 (ex. 'ko-Kr', 'en-GB', 'en-US')
  • Content-Encoding: 메시지 본문을 압축하거나 반환한 방식 (ex. gzip, compress, br)Connection
  • HTTP 메시지를 송신하는 호스트가 어떠한 방식의 연결을 원하는지 명시 (ex. keep-alive, close)

HTTP는 애플리케이션의 다양한 자원을 네트워크를 통해 송수신 하는 프로토콜

 

HTTP의 특징

1. 요청 응답 기반 프로토콜

  • 요청 메시지를 보내는 클라이언트와 이에 대한 응답 메시지를 보내는 서버가 서로 메시지를 주고 받는 구조로 작동
  • HTTP 요청 메시지와 HTTP 응답 메시지의 형태는 다름

2. 미디어 독립적 프로토콜

  • 다양한 종류의 자원을 주고받을 수 있음 (ex. HTML, JPEG, PNG, PDF 등), 자원의 종류는 미디어 타입(Media type)이라고 부름
  • 자원의 특성과 무관하게 자원을 주고받는 인터페이스의 역할만 수행

3. 스테이트리스(Stateless) 프로토콜

  • 상태를 유지하지 않는 스테이트리스 프로토콜
  • 과거 HTTP 요청을 보낸 클라이언트 관련 정보를 기억하지 않음. 때문에 모든 HTTP요청은 독립적인 요청으로 간주됨.
  • 스테이트리스인 이유
    • HTTP서버가 처리해야할 요청 메시지의 수는 수도 없이 많을 수 있음. 이런 상황에서 모든 클라이언트의 상태 정보를 유지하는 것은 큰 부담.
    • 서버는 여러대로 구성될 수도 잇는데, 여러 대로 구성된 서버 모두가 모든 클라이언트의 상태를 유지해야하는 것은 매우 번거롭고 복잡한 작업

 HTTP 서버의 설계 목표

  • 확장성: 언제든 쉽게 서버를 추가할 수 있음
  • 견고성: 서버 중 하나에 문제가 생기더라도 쉽게 다른 서버로 대체 가능

 

HTTP Connections

1. none-persistent HTTP(no pipelining)

  • HTTP 1.0
  • 3-way handshake를 통해 연결한 후, 응답을 받으면 연결을 종료하는 방식
  • 추가적인 요청-응답 메시지를 주고받으려면 매번 새롭게 연결을 수립하고 종료해야함.
  • 하나의 Object을 얻기위해 2RTT가 소요됨 (transmission time을 무시했을 때)
  • 하나의 TCP connection에 최대 한 개의 Object만 보낼 수 있음
  • downloading multiple objects requires multiple connections
  • 비효율적
RTT: 패킷이 목적지에 도착하고 나서 다시 출발지로 돌아오기까지 걸리는 시간

2. none-persistent HTTP  (pipelining)

  • HTTP 1.1
  • use parallel TCP connections
  • OS가 지원하는 최대 parallel TCP connection 수를 고려해야함
  • multiple objects can be sent over single TCP connection
  • 10개의 Object를 주고받을 때
    • no pipelining의 경우 1(initiate TCP connection)+1(base htmp file recieved) +10*2(Object 하나를 얻기 위해 필요한 시간) = 22RTT
    •  pipelining의 경우  1(initiate TCP connection)+1(base htmp file recieved)+2(Object 10개를 동시에 ) = 4RTT

no pipelining

 

3. persistent HTTP (keep-alive)

  • HTTP 1.1
  • kepp-alive 옵션
  • 하나의 TCP 연결 상에서 여러 개의 요청-응답을 주고받을 수있는 기술
  • none-persistent HTTP보다 빠른 속도로 HTTP 요청과 응답을 처리할 수 있음 (3RTT)

네트워크의 기본 구조

  • 네트워크: 노드간선으로 이루어진 자료구조. 그래프의 형태를 띔.
    • 노드: 네트워크 기기 (서버, 라우터, 스위치 등)
    • 간선: 네트워크 기기 간에 정보를 주고받는 유무선의 통신 매체
  • 인터넷: network of networks

Edge

  • 네트워크의 가장 자리에 있는 모든 통신 기기 (hosts, servers)
    • hosts: 네트워크를 통해 주고받는 정보를 최초로 송신하고 최종 수신하는 노드
      • client: 통신 기기의 보내진 정보를 사용자(모바일, 컴퓨터 등)에게 보여주는 쪽. 정보를 요청하는 역할
      • server: 클라이언트의 요청을 응답하는 역할

 

 

LAN과 WAN

LAN

  • 근거리 네트워크
  • 가정이나 기업처럼 비교적 가까운 거리를 연결하는 한정된 공간에서의 네트워크

WAN

  • 원거리 네트워크
  • LAN간의 통신이 이루어지게 함
  • ISP가 구축하고 관리 (ex. KT, LG U+, SK 브로드밴드)

 

패킷 교환 네트워크

  • 패킷 단위로 주고받는 정보를 쪼개서 송수신하고 수신지에서 재조립하며 패킷을 주고 받는 것
    • Packet(패킷): 네트워크를 통해 송수신되는 데이터의 단위 (네트워크를 통해 주고 받는 데이터는 여러 데이터로 쪼개져서 송수신 됨)
    • Packet의 구성: payload + header + (trailer)
      • payload: 패킷에서 송수신하고자 하는 데이터 (cf. 택배 물품)
      • header, trailer: 패킷에 추가되는 부가 정보 (cf. 송장)

 

주소의 개념과 전송 방식

  • 주소: packet의 header에 명시되는 정보(ex. IP주소와 MAC주소)
  • unicast(유니캐스트): 송신지와 수신지가 일대일로 메시지를 주고받는 방식
  • broadcast(브로드캐스트): 네트워크 상의 모든 호스트에게 메시지를 전송하는 방식
    • broadcast domain(브로드캐스트 도메인): 브로드캐스트가 전송되는 범위. 호스트가 같은 브로드캐스트 도메인에 속해있는 경우 같은 LAN에 속해있다고 간주
  • multicast(멀티캐스트): 네트워크 내의 동일 그룹에 속한 호스트에게만 전송하는 방식
  • anycast(애니캐스트): 네트워크 내의 동일 그룹에 속한 호스트 중 가장 가까운 호스트에게 전송하는 방식

 

Protocol

  • 컴퓨터나 원거리 통신 장비 사이에서 메시지를 주고 받는 양식과 규칙의 체계이다. 즉 통신 규약 및 약속.
  • Define format, order of messages sent and received and actions taken on message transmission
  • 프로토콜마다 목적과 특징이 다름. 각 프로토콜의 목적과 특징을 집중해 살펴보는 것이 좋음.

 

네트워크 참조 모델

  • 통신이 이루어지는 단계를 계층적으로 표현한 것
  • 패킷을 송신하는 쪽: 상위 계층 -> 하위 계층 정보 보냄
  • 패킷을 수신하는 쪽: 하위 계층 -> 상위 계층 정보 받아들임
  • 대표적 네트워크 참조 모델: OSI 모델, TCP/IP 모델

 

OSI 모델

  • ISO(국제 표준화 기구)에서 만든 네트워크 참조 모델. 통신 단계를 7개의 계층으로 나눠 OSI 7계층이라고 부름.

Physical Layer(물리계층)

  • 가장 최하위 계층
  • 0과 1로 이루어진 신호를 유무선 통신 매체를 통해 운반
  • 패킷의 이름: symbol, bit

Data Link Layer(데이터 링크 계층)

  • 같은 LAN에 속한 호스트끼리 올바르게 정보를 주고받기 위한 계층
  • 같은 네트워크에 속한 호스트를 식별할 수 있는 MAC주소를 사용
  • 물리 계층을 통해 주고받는 정보에 오류가 없는지 확인
  • 패킷의 이름: frame

Network Layer(네트워크 계층)

  • 네트워크 간 통신을 가능하게 하는 계층
  • 같은 LAN을 넘어 다른 네트워크와 통신을 주고받기 위해 필요한 계층
  • 네트워크간 통신 과정에서 호스트를 식별할 수 있는 IP주소를 사용
  • 패킷의 이름: 패킷 또는 데이터그램

Transport Layer(전송 계층)

  • 신뢰성 있는 전송을 가능하게 하는 계층 (ex. 송수신된 패킷 유실, 순서 뒤바뀜 등)
  • port(포트)라는 정보를 통해 특정 응용 프로그램과의 연결 다리 역할을 수행
  • 전송 계층 대표 프로토콜: TCP, UDP
  • 패킷의 이름: segment(TCP 기반), datagram(UDP 기반)

Session Layer(세션 계층)

  • 세션(응용 프로그램 간의 연결된 상태)을 관리하기 위한 계층
  • 응용 프로그램 간의 연결 상태를 유지하거나 새롭게 생성하고, 끊는 역할
  • 패킷의 이름: data, message

Presentaion Layer(표현 계층)

  • 인코딩과 압축, 암호화와 같은 작업을 수행
  • 응용 계층에 포함하여 간주하는 경우가 많음
  • 패킷의 이름: data, message

Application Layer(응용 계층)

  • 사용자와 가장 밀접하게 맞닿아 여러 네트워크 서비스를 제공하는 계층
  • 중ㅇ요한 프로토콜 다수 포함: HTTP, HTTPS, DNS
  • 패킷의 이름: data, message

 

 

TCP/IP 모델

  • OSI 모델이 주로 네트워크의 이론적 기술을 목적으로 사용하는 반면, TCP/IP 모델은 구현과 프로토콜에 중점을 둔 네트워크 참조 모델

Network Access Layer(네트워크 엑세스 계층)=Link Layer(링크 계층)=Network Interface Layer(네트워크 인터페이스 계층)

  • 최하위 계층
  • OSI 모델의 데이터 링크 계층과 유사

Internet Layer(인터넷 계층)

  • OSI 모델의 네트워크 계층과 유사

Transport Layer(전송 계층)

  • OSI 모델의 전송 링크 계층과 유사

Application Layer(전송 계층)

  • OSI 모델의 세션 계층, 표현 계층, 응용 계층과 유사

 

캡슐화와 역캡슐화

  • 각 계층에서는 어떤 정보를 송신할 때 상위 계층으로부터 내려받은 패킷을 페이로드로 삼아, 각 계층에 포함된 프로토콜의 각기 다른 목적과 특징에 따라 헤더 혹은 트레일러를 덧붙인 다음 하위 계층으로 전달. (상위 계층의 패킷 = 하위 계층의 페이로드)
  • 캡슐화: 헤더(및 트레일러)를 추가해 나가는 과정
  • 역캡슐화: 캡슐화 과정에서 붙인 헤더(및 트레일러)를 각 계층에서 확인한 뒤 제거하는 과정

 

처리량과 지연 시간

  • 처리량(throughput): 링크 내에서 성공적으로 전달된 데이터의 양. 보통 얼만큼의 트래픽을 처리했는지를 나타냄
    • 트래픽(traffic): 흐르는 데이터
    • 처리량(throughput): 처리되는 트래픽
  • 지연 시간(latency): 요청이 처리되는 시간. 어떤 메시지가 두 장치 사이를 왕복하는데 걸린 시간

이름 짓기 규칙

  • Lower Camel Case : 함수, 메소드, 변수, 상수
    • ex) someVariableName
  • Upper Camel Case: 타입(class, struct, enum, extension ...)
    • ex)Person, Point,Week
  • 대소문자 구분함

 

콘솔 로그

  • print : 단순 문자열 출력
  • dump : 인스턴스의 자세한 설명(description property)까지 출력

print(yagom)
dump(yagom)

 

문자열 보간법

  • 프로그램 실행 중 문자열 내에 변수 또는 상수의 실질적인 값을 표현하기 위해 사용하는 것
    • ex) "안녕하세요! 저는 \(age + 5)살 입니다."

 

상수와 변수

let : 상수 선언 키워드, 차후에 값을 변경할 수 없음.

let 이름: 타입 = 값

var : 변수 선언 키워드, 차후에 값 변경이 가능함.

var 이름: 타입 = 값
  • 값의 타입이 명확하다면 타입은 생략가능
  • 나중에 할당하려고 한느 상수나 변수는 타입을 꼭 명시해야함
let sum: Int
sum = InputA + inputB
  • 띄어쓰기 신경쓰기

 

기본 데이터 타입

  • Bool : true / flase, 0이나 1로 표현 불가능
  • Int : 정수형 타입
  • UInt: Unsigned Int, 양의 정수.UInt에 Int 대입 불가
  • Float:  부동소수 타입, 정수타입을 넣어도 문제 없음
  • Double: 64비트 부동수소 타입, Double에 Float 대입 불가
  • Character: 문자 하나. 큰따옴표(")로 묶어주기
  • String: 문자열 타입. String에 Character 대입 불가

 

 

Any, AnyObject, nil

  • Any: Swift의 모든 타입을 지칭하는 키워드. 어떤 타입도 수용 가능.
  • AnyObject: 모든 클래스 타입을 지칭하는 프로토콜
  • nil: 없음을 의미하는 키워드 (다른 언어의 null과 같은 의미)
  • Any나 AnyObject에 nil을 할당할 수는 없음

컬렉션 타입

Array: 순서가 있는 리스트 컬렉션

//1
var Integers: Array<Int> = Array<Int> ()
//2
var Integers: Array<Int> = [Int]()
//3
var Integers: [Int] = [Int]()
//4
var Integers: [Int] = []
//요소 추가
Integers.append(1)
Integers.append(100)
//요소 존재 확인
Integers.contains(100) //true
Integers.contains(99) //false
//요소 삭제
Integers.remove(at:0) //인덱스
Integers.removeLast() //마지막 요소
Integers.removeAll() //모든 요소
//요소 개수
Integers.count()
//let을 사용하여 Array를 선언하면 불변 Array (append,remove 등 불가)
let immutableArray = [1,2,3]

Dictionary: 키와 값의 쌍으로 이루어진 컬렉션

//1
var anyDictionary: Dictionary<String,Any> = [String: Any]()
//2
var anyDictionary: [String:Any] = [:]
//초기에 값추가
var initializedDictionary: [String:String] =["name" : "yagom", "gender" : "male"]
//값 추가
anyDictionary["someKey"] = "value"
anyDictionary["anotherKey"] = 100
//값 변경
anyDictionary["someKey"] = "dictionary"
//값 제거
anyDictionary.removeValue(forKey: "anotherKey")
anyDictionary["someKey"] = nil

Set: 순서가 없고, 멤버가 유일한 컬렉션 

var IntegerSet: Set<Int> = Set<Int>()
//요소 추가 결과:[100,99,1] - 중복 허용 X
IntegerSet.insert(1)
IntegerSet.insert(100)
IntegerSet.insert(99)
IntegerSet.insert(99)
IntegerSet.insert(99)
//요소 존재 확인
IntegerSet.contains(99)
//요소 삭제
IntegerSet.remove(100) //100삭제
IntegerSet.removeFirst()
//요소 개수
IntegerSet.count

//합집합
let union: Set<Int> = setA.union(setB)
//요소 정렬
let sortedUnion: [Int] = union.sorted() //배열 표현
//교집합
let intersection: Set<Int> = setA.intersection(setB)
//차집합
let subtracting: Set<Int> = setA.subtracting(setB)

 

함수

//기본형태
func 함수이름(매개변수1이름: 매개변수1타입, 매개변수2이름: 매개변수2타입, ... ) -> 반환타입 {
	함수 구현부
    return 반환값
}

//예시
func sum(a: Int, b: Int) -> Int {
	return a + b
}

//반환값 없을 때는 Void, 또는 생략
func printMyName(name: String) -> Void {
	print(name)
}
func printMyName(name: String) {
	print(name)
}

//함수 호출
sum(a: 3, b: 5)
printMyName(name: "yagom")

매개 변수 기본값

  • 매개 변수 목록 중 뒤쪽에 위치하는것이 좋다
//매개변수 기본값
func 함수이름(매개변수1이름: 매개변수1타입, 매개변수2이름: 매개변수2타입 = 매개변수 기본값, ... ) -> 반환타입 {
	함수 구현부
    return 반환값
}

//예시
func greeting(freind: String, me: String = "yagom") {
	print("Hello \(friend)! I'm \(me)")
}

greeting(friend: "Hana")
greeting(friend: "John", me: "eric")

 

전달인자 레이블

  • 매개변수의 역할을 좀 더 명확하게 하거나 함수 사용자의  입장에서 표현하고자 할 때 사용
  • 함수 내부에서 전달인자를 사용할 때는 매개변수 이름 사용
  • 함수를 호출할 때는 전달인자 레이블 사용
//기본형태
func 함수이름(전달인자 레이블 매개변수1이름: 매개변수1타입, 전달인자 레이블 매개변수2이름: 매개변수2타입, ... ) -> 반환타입 {
	함수 구현부
    return 반환값
}
//예시
func greeting(to freind: String, from me: String = "yagom") {
	print("Hello \(friend)! I'm \(me)")
}

greeting(to: "hana", from: "yagom")

가변 매개변수

  • 전달 받을 인자의 개수를 알기 어려울 때 사용
  • ...을 붙이면 됨
  • 가변매개변수는 함수 당 하나만 사용 가능
//기본형태
func 함수이름(매개변수1이름: 매개변수1타입, 전달인자 레이블 매개변수2이름: 매개변수2타입... ) -> 반환타입 {
	함수 구현부
    return 반환값
}

//예시
func sayHelloToFriend(me: String, friends: String... ) -> String {
    return "Hello \(friend)! I'm \(me)"
}

sayHelloToFriend(me: "yagom", friends: "hana", "eric", "wing")
 
 /* sayHelloToFriend(me: "yagom", friends: nil)
 sayHelloToFriend(me: "yagom", friends: )는 오류! 
 전달인자가 없을 경우 그냥 생략 */
sayHelloToFriend(me: "yagom")

데이터 타입으로서의 함수

  • 스위프트는 함수형 프로그래밍 패러다임을 포함하는 다중 패러다임 언어
  • 스위프트의 함수는 일급 객체 -> 변수, 상수등에 저장 가능, 매개변수를 통해 전달 가능

 

함수의 타입표현

(매개변수 1 타입, 매개변수 2 타입 ...) -> 반환타입

var someFunction: (String,String) -> Void = greeting(to:from:)
someFunction("eric", "yagom") //Hello eric! I'm yagom

someFunction = greeting(friend:me:)
someFunction("eric", "yagom") //Hello eric! I'm yagom

someFunction = sayHelloToFriends(me:friends:) //error, 매개변수타입이 다름
func runAnother(function: (String,String) -> Void) {
	function("jenny", "mike")

}

runAnother(function: greeting(friend:me:))
runAnother(function: someFunction)

 

조건문

if-else

  • 조건에는 항상 Bool 타입이 와야한다.
if condition {
	statements
}else if condition{
	statements
}else{
	statements
}

 

switch

switch value{
case pattern:
	code
default:
	code
}
  • 범위 연산자를 활용하면 더욱 쉽고 유용
switch someInteger{
case 0:
	print("zero")
case 1..<100:
	print("1~99")
case 100:
	print("100")
case 101...Int.max:
	print("over 100")
default:
	print("unknown")

}
  • break를 명시해주지 않아도 각 case에서 break가 걸림
switch value{
case pattern1:
case pattern2:
	code
default:
	code
} //오류!!!


//방법 1
switch value{
case pattern1,pattern2:
	code
default:
	code
} 

//방법 2
switch value{
case pattern1:
	code
    fallthrough //다음 케이스까지 내려가게됨
case pattern2:
	code
default:
	code
} //오류!!!

 

반복문

for-in

var integers = [1, 2, 3]
let people = ["yagom": 10, "eric": 15, "mike": 12]


for integer in integers {
    print(integer)
}

// Dictionary의 item은 key와 value로 구성된 튜플 타입
for (name, age) in people {
    print("\(name): \(age)")
}

 

while

  • 조건문에는 Bool 타입이어야함
while (integers.count > 1) {  //소괄호는 선택사항
    integers.removeLast()
}

 

repeat-while

  • do-while문과 비슷한 역할
repeat {
    integers.removeLast()
} while integers.count > 0

 

 

옵셔널

  • 값이 있을 수도, 없을 수도 있음을 뜻함
  • nil의 가능성을 명시적으로 표현 
    • nil 가능성을 문서화 하지 않아도 코드만으로 충분히 표현 가능 -> 문서/주석 작성 시간 절약
    • 전달받은 값이 옵셔널이 아니라면 nil체크를 하지 않더라도 안심하고 사용 -> 효율적인 코딩, 예외 상황을 최소화하는 안전한 코딩

Implicitly Unwrapped Optional ( ! )

var implicitlyUnwrappedOptionalValue: Int! = 100

switch implicitlyUnwrappedOptionalValue {
case .none:
    print("This Optional variable is nil")
case .some(let value):
    print("Value is \(value)")
}

// 기존 변수처럼 사용 가능
implicitlyUnwrappedOptionalValue = implicitlyUnwrappedOptionalValue + 1

// nil 할당 가능
implicitlyUnwrappedOptionalValue = nil

// 잘못된 접근으로 인한 런타임 오류 발생
//implicitlyUnwrappedOptionalValue = implicitlyUnwrappedOptionalValue + 1

  Optional ( ? )

var optionalValue: Int? = 100

switch optionalValue {
case .none:
    print("This Optional variable is nil")
case .some(let value):
    print("Value is \(value)")
}

// nil 할당 가능
optionalValue = nil

// 기존 변수처럼 사용불가 - 옵셔널과 일반 값은 다른 타입이므로 연산불가
//optionalValue = optionalValue + 1

 

 

Optional Unwrapping ( 옵셔널 값 추출)

Optional Binding

  • nil체크 + 안전한 값 추출
  • if let 구문 사용
func printName(_ name: String) {
    print(name)
}

var myName: String? = nil

//printName(myName)
// 전달되는 값의 타입이 다르기 때문에 컴파일 오류발생

if let name: String = myName {
    printName(name)
} else {
    print("myName == nil")
}


var yourName: String! = nil

if let name: String = yourName {
    printName(name)
} else {
    print("yourName == nil")
}
  • ,를 사용해 한 번에 여러 옵셔널을 바인딩 할 수 있음
  • 모든 옵셔널에 값이 있을 때만 동작
myName = "yagom"
yourName = nil

if let name = myName, let friend = yourName {
    print("\(name) and \(friend)")
}
// yourName이 nil이기 때문에 실행되지 않습니다

yourName = "hana"

if let name = myName, let friend = yourName {
    print("\(name) and \(friend)")
}

 

Force Unwrapping

  • 옵셔널의 값을 강제로 추출
  • 변수뒤에 !를 붙여주면됨
  • 추천하는 방법은 아님
printName(myName!) // yagom

myName = nil

//print(myName!)
// 강제추출시 값이 없으므로 런타임 오류 발생


//yourName은 선언시점부터 강제추출을 가정함
yourName = nil

//printName(yourName)
// nil 값이 전달되기 때문에 런타임 오류발생

 

 

구조체

프로퍼티 및 메서드

struct Sample {
    // 가변 프로퍼티
    var mutableProperty: Int = 100
    
    // 불변 프로퍼티
    let immutableProperty: Int = 100
    
    // 타입 프로퍼티
    static var typeProperty: Int = 100
    
    // 인스턴스 메서드
    func instanceMethod() {
        print("instance method")
    }
    
    // 타입 메서드
    static func typeMethod() {
        print("type method")
    }
}

 

구조체 사용

var mutable: Sample = Sample()

mutable.mutableProperty = 200
//mutable.immutableProperty = 200 불변 프로퍼티 오류

let immutable: Sample = Sample()

//불변 인스턴스 오류
//immutable.mutableProperty = 200
//mutable.immutableProperty = 200

//타입 프로퍼티 및 메서드
Sample.typeProperty = 300
Sample.typeMethod()

//인스턴스에서 사용시 오류
//mutable.typeProperty = 300
//mutable.typeMethod()

 

학생 구조체 예시

struct Student {
    var name: String = "unknown"
    
    // 키워드도 `로 묶어주면 이름으로 사용할 수 있습니다
    var `class`: String = "Swift"
    
    // 타입 메서드
    static func selfIntroduce() {
        print("학생타입입니다")
    }
    
    // 인스턴스 메서드
    // self는 인스턴스 자신을 지칭하며, 몇몇 경우를 제외하고 사용은 선택사항입니다
    func selfIntroduce() {
        print("저는 \(self.class)반 \(name)입니다")
    }
}

// 타입 메서드 사용
Student.selfIntroduce() // 학생타입입니다

// 가변 인스턴스 생성
var yagom: Student = Student()
yagom.name = "yagom"
yagom.class = "스위프트"
yagom.selfIntroduce()   // 저는 스위프트반 yagom입니다

// 불변 인스턴스 생성
let jina: Student = Student()

// 불변 인스턴스이므로 프로퍼티 값 변경 불가
// 컴파일 오류 발생
//jina.name = "jina"
jina.selfIntroduce() // 저는 Swift반 unknown입니다

 

클래스

프로퍼티 및 메서드

class Sample {
    // 가변 프로퍼티
    var mutableProperty: Int = 100
    
    // 불변 프로퍼티
    let immutableProperty: Int = 100
    
    // 타입 프로퍼티
    static var typeProperty: Int = 100
    
    // 인스턴스 메서드
    func instanceMethod() {
        print("instance method")
    } 
    
    // 타입 메서드
    // 재정의 불가 타입 메서드 - static
    static func typeMethod() {
        print("type method - static")
    }
    
    // 재정의 가능 타입 메서드 - class
    class func classMethod() {
        print("type method - class")
    }
}

 

클래스 사용

  • var, let 상관없이 가변 프로퍼티 변경 가능
// 인스턴스 생성 - 참조정보 수정 가능
var mutableReference: Sample = Sample()

mutableReference.mutableProperty = 200


// 인스턴스 생성 - 참조정보 수정 불가
let immutableReference: Sample = Sample()

// 클래스의 인스턴스는 참조 타입이므로 let으로 선언되었더라도 인스턴스 프로퍼티의 값 변경이 가능합니다
immutableReference.mutableProperty = 200

// 다만 참조정보를 변경할 수는 없습니다
// 컴파일 오류 발생
//immutableReference = mutableReference


// 타입 프로퍼티 및 메서드
Sample.typeProperty = 300
Sample.typeMethod() // type method

// 인스턴스에서는 타입 프로퍼티나 타입 메서드를 사용할 수 없습니다
// 컴파일 오류 발생
//mutableReference.typeProperty = 400
//mutableReference.typeMethod()

학생 클래스 예시

class Student {
    // 가변 프로퍼티
    var name: String = "unknown"
    
    // 키워드도 `로 묶어주면 이름으로 사용할 수 있습니다
    var `class`: String = "Swift"
    
    // 타입 메서드

    class func selfIntroduce() {
        print("학생타입입니다")
    }
    
    // 인스턴스 메서드
    // self는 인스턴스 자신을 지칭하며, 몇몇 경우를 제외하고 사용은 선택사항입니다
    func selfIntroduce() {
        print("저는 \(self.class)반 \(name)입니다")
    }
}

// 타입 메서드 사용
Student.selfIntroduce() // 학생타입입니다

// 인스턴스 생성
var yagom: Student = Student()
yagom.name = "yagom"
yagom.class = "스위프트"
yagom.selfIntroduce()   // 저는 스위프트반 yagom입니다

// 인스턴스 생성
let jina: Student = Student()
jina.name = "jina"
jina.selfIntroduce() // 저는 Swift반 jina입니다

빈 생명주기 콜백 시작

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

애플리케이션 시작 시점에 connect() 를 호출해서 연결을 맺고, 애플리케이션이 종료되면 disConnect() 를 호출해서 연결을 끊는 NetworkClient 예제를 살펴보자

//Networkclient.java

package hello.core.lifecycle;

public class Networkclient {

    private String url;

    public Networkclient(){
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

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

    //서비스 시작시 호출
    public void connect(){
        System.out.println("connect: "+ url);
    }

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

    //서비스 종료시 호출
    public void disconnect(){
        System.out.println("close: "+url);
    }
}
//BeanLifeCycleTest.java

package hello.core.lifecycle;

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 ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        Networkclient client = ac.getBean(Networkclient.class);
        ac.close();

    }
    @Configuration
    static class LifeCycleConfig {
        @Bean
        public Networkclient networkclient(){
            Networkclient networkClient = new Networkclient();
            networkClient.setUrl("<http://hello-spring.dev>");
            return networkClient;
        }
    }
}

생성자 부분을 보면 url 정보 없이 connect가 호출되는 것을 확인할 수 있다. 객체를 생성하는 단계에는 url이 없고, 객체 생성 이후에 setUrl() 이 호출되어 url이 입력 된다.

 

스프링 빈은 간단하게 다음과 같은 라이프사이클을 가진다.

객체 생성 → 의존관계 주입

그러므로 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야한다.

 

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.

스프링 빈의 이벤트 라이프사이클

스프링 컨테이너 생성 → 스프링빈 생성 → 의존관계 주입 → 초기화 콜백 (빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출)→ 사용 → 소멸 전 콜백 (빈이 소멸되기 직전에 호출) → 스프링 종료

객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.

 

스프링의 빈 생명주기 콜백 방법

  • 인터페이스(InitializingBean, DisposableBean)
  • 설정 정보에 초기화 메서드, 종료 메서드 지정
  • @PostConstruct, @PreDestroy 애노테이션 지원

 

 

인터페이스 InitializingBean, DisposableBean

//Networkclient.java

package hello.core.lifecycle;

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

public class Networkclient implements InitializingBean , DisposableBean {

    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 message){
        System.out.println("call: "+url+" message = " + message);
    }

    //서비스 종료시 호출
    public void disconnect(){
        System.out.println("close: "+url);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}

  • InitializingBean 은 afterPropertiesSet() 메서드로 초기화를 지원한다.
  • DisposableBean 은 destroy() 메서드로 소멸을 지원한다.

초기화, 소멸 인터페이스의 단점

  • 이 인터페이스는 스프링 전용 인터페이스이다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기화,소멸 메서드의 이름을 변경할 수 없다.
  • 코드를 수정할 수 없는 외부 라이브러리에 적용할 수 없다.

→ 거의 사용하지 않는다.

 

 

빈 등록 초기화, 소멸 메서드

@Bean(initMethod = "초기화 메서드 이름", destroyMethod = "소멸 메서드 이름") 코드를 통해 초기화, 소멸 메서드를 지정할 수 있다.

//Networkclient.java

package hello.core.lifecycle;

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 message){
        System.out.println("call: "+url+" message = " + message);
    }

    //서비스 종료시 호출
    public void disconnect(){
        System.out.println("close: "+url);
    }

    public void init() {
        connect();
        call("초기화 연결 메시지");
    }

    public void close() {
        disconnect();
    }
}
//BeanLifeCycleTest.java

package hello.core.lifecycle;

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 ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        Networkclient client = ac.getBean(Networkclient.class);
        ac.close();

    }
    @Configuration
    static class LifeCycleConfig {
        @Bean(initMethod = "init", destroyMethod = "close")
        public Networkclient networkclient(){
            Networkclient networkClient = new Networkclient();
            networkClient.setUrl("<http://hello-spring.dev>");
            return networkClient;
        }
    }
}

특징

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

종료 메서드 추론

라이브러리는 대부분 ‘close’, ‘shutdown’이라는 이름의 종료 메서드를 사용한다.

@Bean의 ‘destroyedMethod’ 는 기본 값이 ‘inferred’로 등록되어 있는데, 이 추론 기능은 ‘close’, ‘shutdown’이라는 이름의 메서드를 자동으로 호출해준다.

따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 도착한다.

이 기능을 사용하기 싫을 경우 destroyedMethod=””와 같이 빈 공백을 지정하면 된다.

 

 

 

애노테이션 @PostConstruct, @PreDestroy

@PostConstruct , @PreDestroy 이 두 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.

 //Networkclient.java
 
 @PostConstruct
    public void init() {
System.out.println("NetworkClient.init"); connect();
call("초기화 연결 메시지");
}
    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }

특징

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 패키지를 잘 보면 javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

 

 

정리

@PostConstruct, @PreDestroy 애노테이션을 사용하자

코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean initMethod , destroyMethod 를 사용하자.

 

다양한 의존관계 주입 방법

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

생성자 주입

생성자를 통해서 의존 관계를 주입 받는 방법

지금까지 했던 방법

특징

  • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장
  • 불변, 필수 의존관계에 사용
//OrderServiceImpl.jav

@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;

    }
}
  • MemberRepository, DiscountPolicy 변경 불가 (불변)
  • 생성자 호출할 때 MemberRepository, DiscountPolicy 필수로 넘겨주어야 함 (필수)

< 중요! > 생성자가 하나일 경우는 @Autowired를 생략해도 자동 주입 된다.

수정자 주입

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법

특징

  • 변경,선택 가능성이 있는 의존관계에서 사용
  • 자바빈 프로퍼티 규약의 수정자 메서드를 사용하는 방법
//OrderServiceImpl.java

@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(required = false)
    public void setMemberRepository(MemberRepository memberRepository) {
        
        this.memberRepository = memberRepository;
    }
  • @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다.
    • (required = false)를 추가함으로써 memberRepository는 필수값이 아니게 됨 ( 선택 )

참고: 자바빈 프로퍼티: 자바에서는 과거부터 필드의 값을 직접 변경하지 않고 setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 이것이 자바빈 프로퍼티 규약이다.

  • 프로퍼티 값을 구하는 메소드는 get으로 시작한다.
  • 프로퍼티 값을 변경하는 메소드는 set으로 시작한다.
  • get과 set 뒤 첫 글자는 대문자로 바꾼다.

필드 주입

필드에 바로 주입하는 방법

특징

  • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 순수한 테스트를 만들 수 없음. 값을 넣어줄 때 따로 setter를 만들어 주입을 해줘야하는데, 그렇게 setter가 만들어지면 그냥 수정자 주입과 다를 바가 없다.
@Component
public class OrderServiceImpl implements OrderService{
    @Autowired private MemberRepository memberRepository;
    @Autowired private DiscountPolicy discountPolicy;
}
  • 가급적 사용하지않는게 좋지만 사용할 수 있는 경우가 있다.
    • 테스트 코드
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳

일반 메서드 주입

일반 메서드를 통한 주입

특징

  • 한번에 여러 필드를 주입 받을 수 있다.
  • 일반적으로 잘 사용하지는 않는다.
    • 생성자 주입, 수정자 주입으로 다 해결되기 때문
//OrderServiceImpl.java

@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;
   }

 
}

참고: 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

 

 

옵션 처리

주입할 스프링 빈이 없어도 동작해야할 때가 있다.

자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required = false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
package hello.core.autowired;

import hello.core.member.Member;
import jakarta.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;

import java.util.Optional;

public class AutowiredTest {

    @Test
    void AutowiredOption(){
        ApplicationContext ac = new AnnotationConfigReactiveWebApplicationContext(TestBean.class);

    }

    static class TestBean{

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

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

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

    }
}

실행 결과

  • member는 스프링 빈이 아니다.
  • setNoBean1은 @Autowired(required = false) 이므로 호출 자체가 안된다

참고: @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어 생성자 자동 주입에서 특정 필드에 사용해도 된다.

생성자 주입을 선택하라

불변

  • 대부분의 의존관계 주입은 한번 일어난 후 종료시점까지 의존관계를 변경할 일이 없다.
  • 수정자 주입을 이용하면 setXxx메서드를 public으로 열어두어야하는데, 이 때문에 누군가 실수로 변경할 가능성이 생긴다.

→ 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 불변하게 설계 가능하다.

누락

프레임워크 없이 순수한 자바 코드로 단위 테스트를 하는 경우

//OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@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
    public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy) {
        this.memberRepository=memberRepository;
        this.discountPolicy = discountPolicy;

    }*/

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member,itemPrice);

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

    //테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
//OrderServiceImplTest.java

package hello.core.order;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class OrderServiceImplTest {

    @Test
    void createOrder(){
        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(1L,"itemA",10000);

    }

}

해당 테스트 코드를 실행하면 NullPointerException에러가 뜬다

→ memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문

//OrderServiceImplTest.java

package hello.core.order;

import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceImplTest {

    @Test
    void createOrder(){
        MemberRepository memberRepository = new MemoryMemberRepository();
        memberRepository.save(new Member(1L, "catsbi", Grade.VIP));

        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.setMemberRepository(memberRepository);
        orderService.setDiscountPolicy(new FixDiscountPolicy());

        Order order = orderService.createOrder(1L, "itemA", 10000);

        assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }

}

위와 같이 코드를 수정해주어야 정상적으로 동작함

수정자 주입의 경우 누락된 데이터가 무엇인지 코드를 보며 일일이 찾아보며 수정해주어야한다.

final 키워드

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.

→ 생성자에서 값이 설정되지 않는 경우, 해당 오류를 컴파일 시점에 막아준다.

 

 

기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여할 수 있다. (생성자 주입과 수정자 주입을 동시에 사용할 수 있음)

 

롬복과 최신 트렌드

생성자 주입을 사용하면 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 하는 번거로움이 생긴다.

→ 롬복을 통해 이 번거로움을 줄일 수 있다.

start.spring.io에서 초기 프로젝트를 생성할 때 롬복을 추가해줄 수 있지만 우리는 프로젝트 생성 시 롬복을 추가하지 않았으므로 build.gradle 에 아래와 같이 코드를 추가해준다.

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.6'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
//lombok 설정 추가 시작

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
//lombok 설정 추가 끝
repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	//lombok 라이브러리 추가 시작
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	//lombok 라이브러리 추가 끝
}

tasks.named('test') {
	useJUnitPlatform()
}

코드 추가 후 우측상단에 코끼리 모양을 눌러주면 새롭게 추가한 롬복 라이브러리가 적용된다.

추가적으로 IntelliJ IDEA > settings 에서 Annotation Processors를 찾아 Enable annotation processing을 체크해준다

 

롬복 적용 후 코드

@Component
@RequiredArgsConstructor
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;

    }*/
}

@RequiredArgsConstructor : final이 붙은 필드들을 모아 생성자를 자동으로 생성해줌. 

 

 

 

 

조회 빈이 2개 이상 - 문제

@Autowired는 타입으로 조회한다.

DiscountPolicy 의 하위 타입인 FixDiscountPolicy , RateDiscountPolicy 둘다 스프링 빈으로 선언할 경우 문제가 발생한다.

실행해보면 하나의 빈을 기대했는데 ‘fixDiscountPolicy’ , ‘rateDiscountPolicy’ 2개가 발견되었다는 오류메시지가 뜬다.

 

이 문제를 해결하기 위해 매번 하위 타입을 지정하는 것은 DIP를 위배하고 유연성이 떨어진다.

어떻게 해결할 수 있을까?

 

@Autowired 필드명, @Qualifier, @Primary

앞서 언급한 문제를 해결할 수 있는 방법을 알아보자.

  • @Autowired 필드 명 매칭
  • @Qualifier @Qualifier끼리 매칭 빈 이름 매칭
  • @Primary 사용

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

//기존 코드
@Autowired
  private DiscountPolicy discountPolicy
//필드명을 빈 이름으로 수정한 코드
@Autowired
  private DiscountPolicy rateDiscountPolicy

필드 명을 ‘rateDiscountPolicy’로 지정하여 오류 없이 정상 주입 된다.

@Autowired는 먼저 타입 매칭을 시도하고, 그 결과 여러 빈이 있을 때 필드 명, 파라미터 명 으로 빈 이름을 매칭한다.

@Qualifier 사용

@Qualifier는 추가 구분자를 붙여준느 방법이다. 빈 이름을 변경하는 것이 아닌 추가적인 방법을 통한 주입이다.

빈 등록시 @Qualifier를 붙여준다.

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

생성자 자동 주입 예시

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

@Qualifier 로 주입할 때 @Qualifier("mainDiscountPolicy") 를 못찾으면 ‘mainDiscountPolicy’라는 이름의 스프링 빈을 추가로 찾는다. 하지만 @Qualifier는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다

@Qualifier는 먼저 @Qualifier끼리 매칭을 시도하고, 없을 경우 빈 이름을 매칭한다. 매칭되는 빈을 찾지 못한다면 NoSuchBeanDefinitionException 예외가 발생한다.

@Primary

@primary는 우선순위를 지정하는 방법이다.

아래와 같이 @Primary를 붙여주게 되면 rateDiscountPolicy가 fixDiscountPolicy보다 우선순위를 갖게 된다.

@Component
  @Primary
  public class RateDiscountPolicy implements DiscountPolicy {}

@Qualifier의 경우 주입 받을 때 모든 코드에 @Qualifier를 붙어주어야 한다는 단점이 있다. 반면에 @Primary는 @Qualifier처럼 모든 코드에 애노테이션을 붙이지 않아도 된다는 장점이 있다.

@Primary와 @Qualifier의 우선 순위

@Primary는 기본값 처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다. 스프링은 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 @Qualifier가 우선순위가 더 높다.

 

 

애노테이션 직접 만들기

@Qualifier("mainDiscountPolicy") 처럼 문자를 적으면 컴파일시 타입 체크가 안된다. 즉, @Qualifier("mmainDiscountPolicy")와 같이 오타가 발생해도 동작한다.

다음과 같이 애노테이션을 만들어서 문제를 해결할 수 있다.

package hello.core.annotation;

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

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
        ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
package hello.core.discount;

import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
//@Qualifier("mainDiscountPolicy")
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{
    private int discountPercent =10;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent /100 ;
        }else{
            return 0;
        }

    }
}
//OrderServiceImpl.java

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

    }

애노테이션에는 상속이라는 개념이 없다.이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다. @Qualifier뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다.

 

조회한 빈이 모두 필요할 때, List, Map

지금까지는 자동주입하기위해 스프링 빈 검색해서 2개 이상 나오는 경우 하나를 골라서 주입하는 방법에 대해 알아보았다. 이번에는 2개 이상의 스프링 빈을 모두 조회해야하는 경우를 살펴보자.

예를 들어, 소비자가 rateDiscountPolicy와 fixDiscountPolicy 중 선택할 수 있다고 가정해보자.

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.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;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {

    @Test
    void findAllBean(){
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000,
                "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);
    }
    static class DiscountService{
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;
        public DiscountService(Map<String, DiscountPolicy> policyMap,
                               List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }
        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);
            return discountPolicy.discount(member, price);
        }

    }
}

로직 분석

DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy , rateDiscountPolicy 가 주입된다.

discount () 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.

주입 분석

Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.

List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

 

자동, 수동의 올바른 실무 운영 기준

“편리한 자동 기능을 기본으로 사용하자”

수동 빈 등록은 언제 사용하면 좋을까?

  • 업무 로직 빈: 비즈니스 요구사항들을 개발할 때 추가되거나 변경된다. → 자동 기능 적극 사용
  • 기술 지원 빈: 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들 → 수동 빈 등록을 사용

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.

비즈니스 로직 중에서 다형성을 적극 활용하는 경우

DiscountService 가 의존관계 자동 주입으로 Map<String, DiscountPolicy> 에 주입을 받는 상황을 생각해보자. 여기에 어떤 빈들이 주입될 지, 각 빈들의 이름은 무엇일지 코드만 보고 한번에 쉽게 파악할 수 있을까?

→ 수동 빈으로 등록하거나, 자동으로 할 경우 특정 패키지에 같이 묶어 두는 것이 좋다.

컴포넌트 스캔과 의존관계 자동 주입 시작하기

  • 스프링 빈을 @Bean이나 XML의 <bean> 등을 통해서 설정 정보에 직접 등록했다. 등록해야할 스프링 빈이 수 십, 수 백개가 된다면?
    • 귀찮고, 설정 정보도 커지며 누락하는 문제도 발생
  • 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공
  • 또 의존관계도 자동으로 주입하는 @Autowired도 제공

 

//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(
        //컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다.
        // 그래서 excludeFilters 를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
  • 컴포넌트 스캔을 사용하려면 먼저 @ComponentScan을 설정 정보에 붙여주면 된다.
  • 컴포넌트 스캔은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록 ( 참고: @Configuration 이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.)
  • MemoryMemberRepository, RateDiscountPolicy ,MemberServiceImpl, OrderServiceImpl에 @Component 추가
  • MemberServiceImpl,OrderServiceImpl의 경우 생성자에 @Autowired 추가
    • 의존 관계 주입

 

테스트

//AutoAppconfigTest.java

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;

public class AutoAppConfigTest {

    @Test
    void basicScan() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);

    }

}
  • 로그를 통해 잘 동작하는 것 확인

 

 

 

 

 

 

탐색 위치와 기본 스캔 대상

 

탐색할 패키지의 시작 위치 지정

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸리기 때문에 필요한 위치부터 탐색하도록 지정할 수 있다.

@ComponentScan(
          basePackages = "hello.core",
          
           // 여러 시작 위치를 지정할 수도 있음
          basePackages = {"hello.core", "hello.service"}
          
		  // 지정한 클래스의 패키지를 탐색 위치로 지정
          basePackageClasses =AutoAppConfig.class

}

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

 

권장하는 방법
개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 
설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다. 
최근 스프링 부트도 이 방법을 기본으로 제공한다.

 

 

 

컴포넌트 스캔  기본 대상

컴포넌트 스캔은 @Component 뿐만 아니라 아래도 추가로 대상에 포함한다.

컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.

  • @Component : 컴포넌트 스캔에서 사용
  • @Controlller : 스프링 MVC 컨트롤러에서 사용, 부가 기능 : 스프링 MVC 컨트롤러로 인식
  • @Service : 스프링 비즈니스 로직에서 사용, 부가 기능 :사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다
  • @Repository : 스프링 데이터 접근 계층에서 사용, 부가 기능 :스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환
  • @Configuration : 스프링 설정 정보에서 사용, 부가 기능 :스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리
참고 : useDefaultFilters - 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.

 

필터

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

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

 

예제

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}
package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}
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;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.context.annotation.ComponentScan.*;

public class ComponentFilterAppConfigTest {

@Test
void filterScan() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("beanB", BeanB.class));
}

@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {
}
}
  • includeFilters 에 MyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.
  • excludeFilters 에 MyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.

FilterType 옵션

  • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
    • ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    • ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용
    • ex) org.example..*Service+
  • REGEX: 정규 표현식
    • ex) org\.example\.Default.*
  • CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
    • ex) org.example.MyTypeFilter
 참고: @Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters 는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다.>  특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 개인적으로는 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고, 선호하는 편이다.

 

 

 

중복 등록과 충돌

두 가지 상황 존재

  1. 자동빈등록vs자동빈등록
  2. 수동빈등록vs자동빈등록

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.

-> ConflictingBeanDefinitionException 예외 발생

 

 

수동 빈 등록 vs 자동 빈 등록

수동 빈 등록이 우선권을 가진다. -> 수동 빈이 자동 빈을 오버라이딩

수동 빈 등록시 로그가 남음

Overriding bean definition for bean 'memoryMemberRepository' with a different
  definition: replacing

 최근에는 스프링 부트에서 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
 

 

 

 

웹 애플리케이션과 싱글톤

//SingletonTest.java

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class SingletonTest {
    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();
        //1. 조회: 호출할 때 마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();
        //2. 조회: 호출할 때 마다 객체를 생성
        MemberService memberService2 = appConfig.memberService();
        //참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        //memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }

}

결과

  • 위의 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸됨 -> 메모리 낭비가 심함
  • 해결방안
    • 객체가 딱 1개만 생성되고, 공유하도록 설계 -> 싱글톤 패턴

 

 

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는디자인 패턴
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 함
//SingletonService.java
//싱글톤 예제 코드

package hello.core.singleton;

public class SingletonService {

    //1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    //2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance() {
        return instance;
    }
    //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다. 
    private SingletonService() {
    }
    public void logic() { 
        System.out.println("싱글톤 객체 로직 호출");
    }
}

1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
2.
이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
3.
1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

 

  @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    public void singletonServiceTest() {
    //private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
    // new SingletonService();

    //1. 조회: 호출할 때 마다 같은 객체를 반환
        SingletonService singletonService1 = SingletonService.getInstance();
    //2. 조회: 호출할 때 마다 같은 객체를 반환
        SingletonService singletonService2 = SingletonService.getInstance();
    //참조값이 같은 것을 확인
        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);
        // singletonService1 == singletonService2
        assertThat(singletonService1).isSameAs(singletonService2);
        singletonService1.logic();
    }

결과

  • 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.
  • 스프링 컨테이너가 기본적으로 객체를 싱글톤 패턴으로 만들어 저장해줌
  • 싱글톤 패턴을 구현하는 방법은 여러가지. 위에서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택
    • 스프링컨테이너가 알아서 해주기 때문에 참고만

싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다. ( 구체클래스.getInstance() )
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다. ( DI 적용 힘듦 )
  • 안티패턴으로 불리기도 한다.

 

싱글톤 컨테이너

  • 스프링 빈이 바로 싱글톤으로 관리되는 법
  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 전부 해결하고 싱글톤 패턴의 장점만 가져감.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP,OCP,테스트,private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있음
    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        //1. 조회: 호출할 때 마다 같은 객체를 반환
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        //2. 조회: 호출할 때 마다 같은 객체를 반환
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);
        //참조값이 같은 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        //memberService1 == memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }

결과

  • 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공 -> 뒤에 빈 스코프에서 설명
  • 99%는 싱글톤

 

 

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다!
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
package hello.core.singleton;

public class StatefulService {
    private int price; //상태를 유지하는 필드
    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; //여기가 문제!
    }
    public int getPrice() {
        return price;
    }

}
package hello.core.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import static org.junit.jupiter.api.Assertions.*;

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
    //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);
    //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);
    //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
    //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }



}

결과

  • 사용자A의 주문 금액은 10000원이 되어야하는데, 20000원이라는 결과가 나왔다.
  • 무상태로 설계해야함
package hello.core.singleton;

public class StatefulService {
    //private int price; //상태를 유지하는 필드
    public int order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
      //  this.price = price; //여기가 문제!
        return price;
    }
    //public int getPrice() {
       // return price;
    //}

}
package hello.core.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import static org.junit.jupiter.api.Assertions.*;

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
    //ThreadA: A사용자 10000원 주문
       int userAprice =  statefulService1.order("userA", 10000);
    //ThreadB: B사용자 20000원 주문
       int userBprice= statefulService2.order("userB", 20000);
    //ThreadA: 사용자A 주문 금액 조회
       // int price = statefulService1.getPrice();
    //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + userAprice);
       // Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }



}

 

 

 

@Configuration과 싱글톤

AppConfig를 보면

  •  memberService 빈을 만드는 코드에서 memberRepository() 호출 -> new MemoryMemberRepository() 호출
  • orderService 빈을 만드는 코드에서 memberRepository() 호출 -> new MemoryMemberRepository() 호출

결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보임

 public class MemberServiceImpl implements MemberService {
      private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
          return memberRepository;
      }
  }
public class OrderServiceImpl implements OrderService {
      private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
          return memberRepository;
      }
}
package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class ConfigurationSingletonTest {
    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        //모두 같은 인스턴스를 참고하고 있다.
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository  = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);

        //모두 같은 인스턴스를 참고하고 있다.
        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}

결과

  • 확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.
  • AppConfig에 호출 로그를 남겨서 확인해보면 모두 1번씩만 호출된다.

 

@Configuration과 바이트코드 조작의 마법

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class ConfigurationSingletonTest {

    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        //AppConfig도 스프링 빈으로 등록된다.
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean.getClass());

    }

}

결과

  • 순수한 클래스라면 다음과 같이 출력되어야 한다.

    class hello.core.AppConfig

     

  • 예상과는 다르게 클래스명에 CGLIB가 붙어있음
    • 스프링이  CGLIB라는 바이트코드 조작 라이브러리를 사용해서 Appconfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것
    • 이게 싱글톤이 보장되도록 해줌

@Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까?

  • AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 확인할 수 있다.
  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • 항상 @Configuration을 사용하자!

참고 ) @Autowired ( 자동 의존관계 주입 ) 를 사용하는 방법도 있음

 

+ Recent posts