반응형

알리 익스프레스에서 sata ssd 960gb를 단돈 1만원에 판매를 하고 있어 일단 질러 보았다.

(만원?!? 이건 못참지....  설마 내가 똥손이겠어1.. 안되면 디스풋걸면 되겠지)

5일 배송은 대단했다.

 

포장을 뜯어보니 깔끔하고 새것같았다. 외관은 아무 흔적도 없고 깨끗했다.

다른 커뮤니티나 블로그의 후기를 보았을 때 이 ssd는 재생낸드를 사용하였다고 했는데 잘만 돌아가면 되지뭐..

(설마 내가 똥손이겠어2...)

 

컴퓨터에 장착을 하고 켜보니

알 수 없는 중국어와 인식이 잘 되는 것을 볼 수 있다.

 

여러 정보들을 보면

 

음... 그렇군..

 

이제 가장 중요한 속도와 과연 이 용량은 뻥카일지 확인해보자

속도는 그럭저럭 잘 나오고

 

이제 뻥용량인지 확인을 해보면?

(너무 오래 걸린다... 그냥 믿고 쓸걸 그랬나 이제와서 멈추기도 뭐하다)

 

H2test2w 프로그램을 통해서 이 ssd의 최대용량만큼 쓰기를 하였고 정상적으로 끝까지 쓰기가 되었다.

(만원 개혜자... 갓 알리다)

 

이제 중요하지 않은 프로그램이나 파일을 여기 깔아서 써야겠다. 혹시 자료가 날라 갈 수도 있으니까.

 

추가)

이녀석의 컨트롤러는 무엇일까..

대충 이건거같은데 DRAM-less 인듯하다.

추가로 저 Bad Blcok From Pretest는 뭘까..

728x90
반응형
블로그 이미지

아상관없어

,
반응형

20200616_184947000_iOS.MOV. 프로젝트 개발 개요 

1. 프로젝트 개요 

시대와 기술의 발전에 따라 이 시대를 살아가는 현대인의 삶의 질은 점점 더 개선되고 있다. 많은 현대인은 기계나 컴퓨터 장치의 케어를 받으며 하루를 살아가고 있고 관련 기술들은 지속적으로 시장에 출시되고 있다. 이러한 기술들중 수면과 관련된 기술들(슬립테크, 스마트침대)은 지속적으로 출시되고 발전하고있지만 기상한 이후의 시간을 보다 편하게 관리해주는 기술들이 전무하다는 것을 알게 되었고 이 시간을 관리해주는 서비스에 대한 필요성에 대하여 생각해보았다. 하루중에 가장 힘들고 귀찮은 시간이 언제인가, 팀원들과 이야기를 나눠본 결과 팀원들 모두 아침에 일어나 출근, 등교를 준비하는 시간이 가장 힘들고 귀찮다고 생각하고 있었다. 이러한 논의의 결과로 기상부터 출근까지 시간을 관리해주는 서비스를 개발해보자 라는 생각을 하게 되었고 이를 프로젝트 주제로 선정하여 개발을 시작하게 되었다. 

  

2. 국내외 관련 현황  

부족한 모닝케어 관련 서비스 

 최근 수면산업이 급부상하면서 슬립테크가 주목을 받고 있다. 인공지능, 사물인터넷 빅데이터를 활용하여 수면 상태를 체크하고 숙면을 도와주는 제품과 기술들이 등장하고 있다. 대표적인 슬립테크 기술은 침구류에서 찾아볼 수 있다.  

이케아코리아는 편안함, 온도, 빛, 향, 소리 등 ‘굿 슬립(good sleep)’을 위한 다섯 가지 홈퍼니싱 요소를 소개하는 팝업스토어를 지난 8~9월 강남역에서 운영했다. 바디프랜드는 안마의자에 ‘수면 프로그램’을 적용했다. 부교감신경을 자극하는 마사지를 통해 교감신경을 억제하고 긴장된 몸을 이완시켜 숙면을 유도하는 시스템이다. 신세계백화점의 란제리 PB 상품인 ‘언컷’은 인견 파자마, 안대 등 숙면 관련 상품을 대표 상품으로 판매 중이다. LG U+IoT 숙면등은 사용자에게 무드 등으로 편안한 숙면을 제공하고 새소리나 음악으로 상쾌한 아침을 도와준다. 

하지만 사용자들의 수면 건강과 같은 슬립 테크 관련기술은 많지만 정작 수면 후 아침, 모닝 케어 기술은 LG U+ IoT 숙면등의 아침 알람 서비스 외에 찾아보기 힘들다. 

 

 

IoT 인프라의 확대와 성장 

한국IDC(https://www.idc.com/kr)는 최근 발간한국내 IoT 플랫폼 시장 전망 보고서 통해 2019년 국내 IoT 플랫폼 시장 규모는 전년 대비 19.5% 증가한 7,540억원에 이른다고 밝혔다. 해당 시장은 2023년까지 16.1%의 연평균성장률을 보이며 1조 3,308억원에 이를 것으로 내다봤다. 

보고서에 따르면 기업들이 IoT 플랫폼을 IoT 도입의 출발점으로 인식하면서 해당 시장의 성장에 기여하고 있는 것으로 보인다. 기업들이 적극적으로 디지털 트랜스포메이션을 가속화하고 IoT기술 생태계 내 플랫폼의 역할이 확대되면서 국내 IoT플랫폼 시장은 2023년까지 계속해서 두 자릿수의 성장세를 보일 것이라는 전망이다.  

 

이처럼 IoT 인프라는 연평균 22.6%씩 증가하고 있으며 점점 더 성장해 나갈 것으로 예상이 된다. 

 

 

 

1인 가구 증가 

KB금융경영연구소가 발표한 '2019 한국 1인가구 보고서'에 따르면 우리나라의 1인가구는 2017년 기준 약 562만 가구로, 전체 인구의 10.9%다. 

이는 기존 예상치인 556만 가구를 넘어서는 것으로, 1인가구 증가 추세가 이전보다 빨라졌음을 의미한다. 

한국의 총인구는 2028년 5천194만명을 정점으로 줄어들 것으로 예상되지만, 1인가구의 비율은 계속해서 성장해 2045년 16.3%에 이를 것으로 예상된다. 

보고서는 "미혼·이혼 인구의 증가 등 가구 형태의 변화를 이끄는 요인들이 작용하면서 1인가구는 계속 증가할 것"이라며 "1인가구의 생활 행태가 사회·경제 전반에 미치는 영향도 지속적으로 확대될 것"이라고 내다봤다. 

1인가구 비중의 증가는 전국적인 현상으로, 서울 등 9개 지방자치단체에서는 이미 30%를 넘었다. 

페이지 나누기 

. 프로젝트 개발 목표와 내용 

 

1. 구성 및 주제 선정 

주제선정 회의를 하기위해 팀 미팅을 중 팀원 모두가 아침시간이 힘들고 바쁘다고 공감을 하여 아침 관련 케어 서비스로 주제를 정하였다. 관련 현황과 특허를 조사하였고 구현하려는 아침 관련 서비스를 찾기는 힘들었다. 구현관련 기술을 조사하여 센서와 아두이노, 안드로이드 앱을 이용하여 구체적 구현을 생각하였다. 대략적인 구현 계획을 세운 후 각자 어떠한 업무를 담당할지에 대하여 논의하였고 본인자신있고 최대한의 역량을 이끌어 낼 수 있는 역할선택하여 각자 담당하기로 하였다. 

 

김우현, 정원영 - 어플리케이션 개발, 외부 API연동, 시연품 제작 

이창민, 김찬경 - 아두이노 센서 연동, 시연품과의 연동, 시연품 제작 

 

페이지 나누기 

2. 개발 목표 및 내용 

수면 기상부터 집을 나서기까지 해야 할 모든 일을 총괄해서 관리해주는 서비스를 구축한다. 

 

  1. 사용자가 어플리케이션에서 알람 시간을 설정하면 그 시간에 알람이 울림과 동시에 온수 보일러가 가동된다.  
  1. 침대의 압력센서가 사용자가 침대에 누워있는지를 감지하고 압력이 감지되지 않을 때까지 알람이 울리도록 설계하여 회사나 수업에 지각하는 일을 방지해준다.  
  1. 이런 과정동안 욕조에는 자동으로 데워진 물이 채워지고 일정한 수위가 넘어가면 수도꼭지가 잠겨 더 이상 욕조의 물이 넘치는 일을 방지한다.  
  1. 사용자가 씻는 과정을 마친 후에는 자동으로 화장실의 습도를 측정하여 일정 수준을 넘기면 환풍기가 작동하여 화장실의 습도를 조절해준다.  
  1. 출근 준비가 끝난 사용자는 어플리케이션을 실행하여 메인 화면을 통해 회사 지역의 시간별 날씨를 확인하고 대중교통의 다음 도착시간, 그날 아침의 뉴스 헤드라인을 한번에 확인할 수 있다.  
  1. 또한 오늘 챙겨야 할 물품들을 전날 미리 어플리케이션에 등록하는 과정을 통해 챙겨야 할 물품들을 다시한번 확인하여 준비물을 까먹고 그냥 출근하는 경우를 예방한다.  
  1. 사용자가 집을 나서기 전, 현관에 위치한 초음파 센서를 통해 사용자가 집을 나가는 것을 확인하면 집 안의 환풍기를 제외한 모든 센서와 조명등이 자동으로 꺼진다.  

 

 

이러한 일련의 과정을 통해 아침에 일어나 출근을 준비하는 과정이 매우 쾌적해질 것이다.    

 

페이지 나누기 

. 프로젝트 개발 환경, 추진 전략 및 계획 

1. 프로젝트 개발 환경 

 

               

 

프로젝트에 사용되는 기술로는 크게 애플리케이션, IoT이다. 이러한 기술을 구현하기 위해서 애플리케이션은 안드로이드 스튜디오를, IoT의 구현을 위해서 Arduino를 사용하였다. 

애플리케이션의 경우 사용자가 휴대폰으로 당일의 날씨, 교통정보, 뉴스를 한 화면에 볼 수 있도록 현을 하기로 하였으며 툴의 선택에 있어서 현재 한국에서 IoT분야에 많이 사용되는 안드로이드 운영체제에 맞추어 안드로이드 스튜디오를 선택하게 되었다. 안드로이드 스튜디오에서는 상기 설명된 요소들과 추가적으로 사용자가 시간을 시, 분 단위로 선택하여 알람을 설정할 수 있도록 구현하였으며 설정된 시간이 되면 애플리케이션에서 블루투스 모로 신호를 보내는 기능을 구현하였다. 

IoT의 경우 블루투스 모듈을 통해 신호를 수신 받을 경우 침대에 위치한 진동 센서와 압력센서를 사용하도록 했고 곧바로 보일러의 전원인 led센서와 수위 센서를 작동시키며 이후 초음파 센서를 통해 필요 센서 몇 가지를 제외한 모든 센서를 종료시키도록 하였다. 그리고 온습도 센서와 모터를 이용하여 환기 시스템도 구축하였다. 이렇게 아두이노에서는 블루투스 모듈, 압력 센서, 진동센서, LED, 초음파 센서, 물 수위센서, 온습도 센서, 모터를 이용하였으며 이 모두를 제어하기 위해 Arduino Uno대신 Mega를 택하였다 

페이지 나누기 

다음은 아두이노, 안드로이드 구현 코드이다. 

 

<Arduino>

 

<Arduino Serial Monitor> 

 

 

<안드로이드-activity_main.xml> 

 

 

<안드로이드-mainActivity.java> 

 

 

<안드로이드-XML파싱> 

 

 

 

페이지 나누기2. 프로젝트 개발 추진 전략 및 계획 

(1) 프로젝트 개발 추진 전략 

매주 팀의 진행상황과 진행해야 할 부분들을 파악하기 위해 전략 생성 

미리 milestone을 계획하여 큰 틀을 잡은 후 세부사항을 결정하는 것으로, 그 장점으로는 현재 상황을 객관적으로 판단이 가능하며 계획을 구체화하며 구현하는데에 있어 보다 쉽게 피드백이 가능하다는 점이 있다. 

 

프로젝트 효율을 위한 공동작업 환경 마련 & 개별 작업 

코로나-19의 영향을 받는 현 시국에 원활하고 의욕을 고취하기 위해 FaceTime과 OneDrive, GoogleDocs비대면 회의방식을 마련하였으며, 시연품 제작 전 각자 센서들을 연동, 각 파트를 테스트하였다. 시연품 제작과 테스트의 경우 대면이 불가피 하기 때문에 milestone에 따라 시연품 제작 주에 모여서 3일간 이동하지 않고 한 장소에서 작업을 마쳤다. 

 

 

 

 

 

페이지 나누기 

. 연구개발 추진일정 및 마일스톤 

1. 프로젝트 개발 세부 추진일정 

1주차(3월 3주차) : 팀원 구성, 역할 분담 

2주차(3월 4주차) : 역할 분담의 세분화 및 계획/일정 수립과 아이디어 후보 만들기, 개발 환경 설정 

3주차(4월 1주차) : 재료 신청 준비, 필요 기술 지식 습득 시작 

4주차(4월 2주차) : 아이디어 확정, 재료 신청, 기술/지식 공부 

5주차(4월 3주차) : 계획의 구체화, 필요 기술 습득 확인 및 복습, 재료 수령 

6주차(4월 4주차) : 필요 지식 습득 확인 및 기술 설계 

7주차(51주차) : 안드로이드 스튜디오 애플리케이션 제작 시작, api 사용연습 

8주차(5월 2주차) : api 설정 성공, 애플리케이션 오류 발견 

9주차(5월 3주차) : 시연품 프로토타입 제작 시작 

10주차(5월 4주차) : 애플리케이션 오류 해결, 시연품 제작 시작/완료 

11주차(6월 1주차) : 시연품 테스트, 앱과 시연품간의 연결 성공 

13주차(6월 2주차) : 발표 ppt 시연하여 마무리 

2. 마일스톤  

<short term> 

 

페이지 나누기<long term> 

 

먼저 프로젝트의 구현을 위해 분석단계부터 시작한다. 우리는 분석을 3단계로 나누어 각각 필요사항과 현황, 모듈, 환경 분석으로 지정했다. 

분석 단계에서는 현재 사람들이 어떤 서비스를 원하고 있는지 분석하고 결정하며, 팀에서 최종적으로 결정된 서비스와 유사한 서비스가 있는지 파악하며 유사한 서비스가 있다면 팀만의 차별적인 장점에 대해 구상하여 어떤 전략으로 잠재적 소비자들을 플랫폼으로 끌어당길 수 있는지 분석하고 설계 부분에서 적용시킬 것이다. 이러한 현황, 필요사항 분석과 더불어 개발시에 필요한 모듈과 환경에 대해 정리하고 분석하여 이를 기준으로 어떤 환경에서 웹/앱 플랫폼을 만들 것인지 결정한다. 

설계 단계에서는 계획한 플랫폼의 개발에 필요한 모든 요소들을 설계해 플랫폼의 뼈대를 잡는다. 우선 비즈니스 모델의 간단한 틀을 잡고 설계한 다음, 전체적인 플랫폼의 설계와 구상을 마치고 거시적, 미시적 모두를 만족하는 비즈니스 모델을 설계한다. 비즈니스 모델이 완성되면 사용자들이 거부감을 느끼지 못하고 친근성 있게 접근할 수 있도록 UI/UX 설계를 진행한다. 

개발이 모두 끝나면 최종적으로 테스트 단계에 들어가는데, 이 시점에서 전에 결정한 플랫폼의 유형에 따라 테스트를 하게 됩니다. 개발한 코드와 모듈을 모두 테스트해보고 문제가 발생하는 문제는 없는지, 발견하지 못하여 숨어 있는 오류는 없는지 등을 체크하여 결과를 분석한다. 테스트 결과를 분석하는 단계는 최종 테스트에 있어서 가장 중요한 부분이기 때문에 이 단계에서는 발견되는 오류는 없어야 하며 완벽해야 한다.  

결과 분석이 종료되면 마지막으로 최종테스트를 진행한다. 

테스트까지 끝난 후에는 최종 배포 단계에 접어드는데, 추후에 발견되는 여러가지 문제들을 처리하기 위한 유지/보수도 포함되는 단계이다.페이지 나누기 

. 프로젝트 개발 결과물 활용 계획 

1. 프로젝트 개발 결과물 활용 방안 

(1)  스마트 원룸 시스템 구축 

프로젝트를 구현하면서 만들어지는 작품은 스마트홈 시스템에 기반을 두고 있으며, 1인 가구를 목표로 하여 앞으로는 아파트나 주택 뿐만이 아닌 원룸형식의 주거지에도 스마트 홈이 추가될 것이라 전망하여 원룸 형식의 주거지를 목표로 하였다. 그러므로 시공 업체와 IT관련 아키텍트가 협력하여 작업할 수 있다면 앞으로 모든 스마트 원룸(홈) 시스템에는 해당 프로젝트가 도입될 것으로 예상된다. 각 주거지마다 해당 프로젝트를 추가하면 상기 목표와 같이 오전 준비시간에 있어서 기상부터 세면, 교통정보와 환기, 현관까지의 과정을 관리 받을 수 있다. 

(2)  시스템의 확장으로 종합적 시스템 구축 가능 

본 프로젝트에서는 오전의 준비시간 시스템에 관하여 집중을 했지만 차후 이를 다른 스마트 홈과 연계시키고 원룸 뿐만이 아닌 아파트나 주택 등의 여러 종류의 주거지에 적용 시킬 수 있게 프로젝트를 모듈화 할 수 있다.  

 

 시연영상 - 시연영상.MOV

<시연품> 

페이지 나누기 

. 위험요소 분석 

1. 예상되는 어려운 점 

(1) 집 마다 구조가 다르다!  

집 마다 중인 난방시스템(중앙난방, 개별난방), 구조, 크기가 다르기 때문에 통일된 모듈을 적용하기가 쉽지는 않을 것이라 예상된다. 이를 해결하기 위해 건설업계와 연계를 통해 그들의 노하우로 집의 구조마다 차별화된 모듈 적용방식을 채택하고 필요한 경우 간단한 공사를 통해 모듈을 적용할 수 있도록 한다. 

 

 (2) 지속적인 서비스 제공을 위한 유지보수 

구조, 크기가 다르기 때문에 통일된 모듈을 적용하기가 쉽지는 않을 것이라 예상된다. 이를 해결하기 위해 건설업계와 연계를 통해 그들의 노하우로 집의 구조마다 차별화된 모듈 적용방식을 채택하고 필요한 경우 간단한 공사를 통해 모듈을 적용할 수 있도록 한다. 

아무래도 여러가지 모듈과 기술들이 합쳐진 상태로 제공되는 기술이다 보니 장치 고장에 의한 서비스 제공 장애가 잦을 것이라 예상된다. 이러한 장치 고장을 최대한 빠르게 해결하기 위해 각 지역마다 최소 하나씩의 서비스 센터를 유지해야 할 것이고 시장점유율이 점점 커짐에 따라 서비스 센터의 개수를 늘려 서비스 제공에 지연이 없도록 해야 할 것이다.  

 

 

 

 

 

페이지 나누기 

2. 기술적 위험요소 분석  

(1) 보안 관련  

IoT 시스템은 집이라는 지극히 개인적인 공간이 네트워크와 연결되어 정보가 수집되고 제어되는 것을 뜻한다. 이는 데이터의 파괴와 변형, 정보의 도용이 나타날 수 있으며 개인의 사생활이 침해될 가능성을 내포하고 있시스템의 취약점을 파악하고 보안구축하는 것이 중요하다. 

각각의 IoT 센서들은 블루투스/와이파이 모듈 등을 통해 네트워크로 연결된다. 

센서로부터 정보가 생성되어 유저에게 넘어가 제어가 이루어지기까지 단계별로 암호화를 적용하여 정보를 보호해야 한다. 또한, 접근 권한을 설정하여 본인 인증이 완료된 경우에만 정보에 접근할 수 있도록 할 수 있다. 해당 보안 조치를 위해서 블록체인의 분산원장 기술을 이용함으로써 신뢰성 높은 시스템을 구축하는 것이 가능하다. 

 

다른 방법으로는 분산 처리능력을 갖춘 개방형 IT 아키텍처를 통해 모바일 컴퓨팅 및 IoT 기술을 가능하도록 하는 Edge Computing 기술을 이용할 수 있다. Edge Computing클라우드나 데이터 센터로 정보를 전송하지 않고, 단말기나 로컬 컴퓨터/서버 차원에서 정보를 처리한다. 이는 데이터를 저장하지 않고 직접 처리하므로 민감한 데이터의 보안을 강화한다는 이점을 가진다.  

 

 

페이지 나누기 

VII. 미팅 일지 

 

 

<아이디어 회의> 

 

<서비스 흐름도 제작> 

 

 

 

<센서 선정> 

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

DSP MATLAB Project Report

Image Processing With GPU

 2020 12 18

 

 

I.    Project 개요

1.1 Project 개발 필요성

요즘 기본적으로 스마트폰을 사용하면서 카메라의 성능 및 스마트폰 자체적인 성능이 발전하여 자체적으로도 아래 사진과 같은 여러가지 필터를 줄 수 있게 되었다.

여러 수식을 이용하여 이미지를 이루고 있는 픽셀 행렬을 다른 값으로 바꾸어 이미지를 변형하는 것. 이것이 이미지 필터링의 정의이다.

이러한 이미지 필터링은 강의 시간에도 연습해 보았지만 주로 cpu를 사용하는것이었고, 이를 gpu를 주체로 사용하여 필터링하고, 다른 방법으로도 사진 파일에 대해 필터링을 하는 프로젝트를 하면 흥미로울 것이라 생각하여 시작하게 되었다.

 

1.2 Project 개발 배경 및 동향

처음 계획을 세우고 공부를 해보니 Mathworks에 위치한 예제들에서 큰 차이를 느끼지 못하였고 단순히 코드를 몇글자 수정하고 필터링 된 결과물들을 나열하는 것에서는 예제의 복사 붙여넣기가 된다고 판단하였다. 이를 해결하기 위해 자체적으로 색에 대해 필터링이 가능한 필터링 함수를 제작하고, UI를 변경하여 단순하게 정해진 사진의 나열이 아니라 사용자가 직접 원하는 사진 파일을 선택하여 필터링하도록 목표를 잡았다.이번학기 딥러닝 수업을 들으면서 이미지 분류 모델을 직접 만들어보았다. 따라서 이미지 품질향상에도 딥러닝을 적용한다면 기존 필터링보다 좋은 결과를 보여줄 것이라 생각하여 딥러닝 모델을 활용하여 이미지 품질 향상을 해보고자 하였다.gpu와 딥러닝을 사용하여 이미지 필터링을 하는 것으로 세부적인 내부 프로세스를 변경하게 되었다.

 

1.3 기본적인 동작 및 기능

필터링을 하고자 하는 이미지를 준비하고, 각각 원하는 필터링 방법에 대한 버튼을 눌러 이미지 파일을 입력받는다. 입력받은 이미지 파일을 MATLAB의 예제에 사용된 sharpenedequalized 필터링 처리하거나 RGB중 원하는 색상만을 표시해주는 필터링을 해주거나 gaussian 필터링 혹은 딥러닝 모듈을 이용한 필터링을 해주도록 만들었다.원본 이미지 파일과의 비교를 위해 원본 파일을 출력하면서 동시에 필터링 된 이미지 파일을 볼 수 있도록 구현하였다.

 

1.4 Project 개발 방법

참고자료로는 MathWorks 페이지의 필터링 기능을 하는 함수와 휘도 색차 구현 함수, 딥러닝 모델을 검색해 사용하였다.

현재 프로젝트는 gpu를 사용한 필터링인데 gpu를 사용하려면 CUDA api 모델이 필요하므로 amd 프로세서를 사용하는 팀원의 컴퓨터에서 호환되지 않는 문제가 발생하므로 호환성 문제를 개선해야 하며, 기여도에 있어서는 기존 함수를 찾아 직접 구현한 것이 대부분이지만, UI 제작과 HSV를 이용한 컬러 필터링 함수를 독자적으로 제작하였으며 딥러닝 모델을 사용하여 휘도값을 바탕으로 해상도를 높이려하였기 때문에 기여도는 50%라고 생각된다. 직접 딥러닝 모델을 개발하고자 하였으나, 데이터셋을 구하기 힘들고 컴퓨팅 성능이 부족하여 좋은 성능을 내기에 어려웠다. 따라서 미리 학습된 VDSR 신경망을 가져와 사용하였다.

II.   Project 수행 내용

2.1 알고리즘

만들고자 하는 프로젝트의 단순한 알고리즘은 다음과 같다.

기능을 크게 3가지로 나누고 각 버튼을 통해 해당 기능에 적용할 이미지를 선택하도록 한뒤,

이미지를 로드하여 gpuArray를 통해 이미지를 처리(Color 관련 filtering, Gaussian/adjust, 딥러닝)하게 된다.

딥러닝을 이용한 이미지 품질향상은 직접 모델을 개발하기엔 컴퓨팅자원과 시간이 부족하여 기존에 학습된 VDSR모델을 사용하였다. VDSR모델은 Accurate Image Super Resolution Using Very Deep Convolutional의 약자로 저해상도의 이미지와 고해상도의 이미지 사이의 매핑을 학습하여 저해상도의 이미지를 고해상도로 upscale시켜준다.

먼저 RGB의 이미지에서 휘도와 색차로 이미지를 나누고 그 중 휘도 이미지를 사용하여 학습을 한다.

여기서 rgb2ycbcr을 하여 RGB이미지를 휘도, 색차로 구분한 뒤 바로 휘도를 사용하지 않고 좀더 부드럽게 하기위해 쌈삼차보간법을 적용한 휘도를 모델의 입력값으로 사용한다.

쌍삼차보간법이 적용된 휘도이미지는 VDSR 네트워크를 통과한 뒤 쌍삼차보간법이 적용된 휘도이미지와 더해져 Super Resoluion 이미지를 생성한다. 생성된 이미지는 High Resolution 이미지와 Mean Squad Error 손실함수를 사용하여 loss를 비교하고 loss를 줄여나가도록 학습된다.

20개의 layer(image input layer +Convolution2D(padding ), ReLU +regression layer)로 구성이 된다. 그리고 각 Convolution2D layer3x3x64 필터를 64개 포함한다.

이미지 향상 딥러닝 모델을 구현하기엔 데이터 셋과 컴퓨팅 자원, 시간이 부족하였으므로 위와 같은 구조로 학습된 batchsize=64, epoch 100 “trainedVDSR-Epoch-100-ScaleFactors-234.mat” 모델을 가져와 사용하였다. (위 모델의 경우 학습시간이 Nvidia사의 Titan X로도 6시간이 걸린다.)

원하는 이미지를 불러와 imread로 읽은 후, rgb2ycbcr을 거쳐 bicubic시킨 y 이미지를 가져와 학습하고 ycbcr2rgb를 하여 향상된 이미지를 얻는다.

 

 

 

참고)

*휘도 = R, G ,B 픽셀 값의 일차 결합을 통해 각 픽셀의 밝기를 나타냄
*색차 = R, G, B 픽셀 값의 다른 일차 결합을 통해 색차 정보를 나타냄
*삼차보간법
삼차보간법은 3차함수를 이용하여 미지의 값을 추정하는 것이다. 삼차함수를 사용하기 때문에 더 부드러워진다. 미지의 값을 찾기 위해 위와 같은 식을 통하여 미지의 값을 추정한다.
쌍삼차보간법은 삼차보간법을 2차원으로 확장한 것인데, 삼차보간법에선 y-1 ,y0, , y1 , y2  이웃한 4개의 점을 참조하지만 쌍삼차보간법은 이웃한 16개의 점을 참조한다.

2.2 MATLAB 구현

우선적으로 app designer를 사용하여 색상 추출을 위한 컬러 필터 함수를 추가하였다.

이미지를 인수로 받아 HSV로 변환하고 계산(mask create)를 통해 분리한 뒤 다시 RGB로 변환시켜

원하는 색조를 추출하는 필터함수이다.

function I = colorfilter(app, image, range)
   % RGB to HSV conversion
   I = rgb2hsv(image);        
   % Normalization range between 0 and 1
   range = range./360;  
   % Mask creation
   if(size(range,1) > 1), error('Error. Range matriz has too many rows.'); end
   if(size(range,2) > 2), error('Error. Range matriz has too many columns.'); end
   if(range(1) > range(2))
   % Red hue case
   mask = (I(:,:,1)>range(1) & (I(:,:,1)<=1)) + (I(:,:,1)<range(2) & (I(:,:,1)>=0));
   else
   % Regular case
   mask = (I(:,:,1)>range(1)) & (I(:,:,1)<range(2));
   end
% Saturation is modified according to the mask
   I(:,:,2) = mask .* I(:,:,2);
   
   % HSV to RGB conversion
   I = hsv2rgb(I);
        end
    end

인자로 받는 범위의 경우 직접 색조를 찾아보면서 정확한 값을 찾아낼 수 있었고, 우리가 판단한 결과 빨간색의 범위는 320~50, 파란색 170~305, 초록색 50~170으로 대부분의 사진에서 색조를 구별하는데 성공하였다.

 이미지의 회색조 영상처리를 한 후 Gaussian 필터링과 medifilter를 사용하여 원본과 adjust, gaussian을 출력하도록 구현하였다.

 

%image enhancement Model Load%
            load('trainedVDSR-Epoch-100-ScaleFactors-234.mat');
dimage = original;
            dimage = im2double(dimage);
            [rows,cols,np] = size(dimage);
            %RGB이미지 색체, 휘도 분리
            ycbcr_image = rgb2ycbcr(dimage);
            y = ycbcr_image(:,:,1);% 휘도
            cb = ycbcr_image(:,:,2);% 색채
            cr = ycbcr_image(:,:,3);% 색채
            %삼차보간법을 거친 휘도,색채
            bicubiced_y = imresize(y,[rows cols],'bicubic'); % bicubiced 휘도
            bicubiced_cb = imresize(cb,[rows cols],'bicubic'); % bicubiced 색채
            bicubiced_cr = imresize(cr,[rows cols],'bicubic'); % bicubiced 색채
%휘도 이미지로 학습된 딥러닝 알고리즘
            dimage = activations(net,bicubiced_y,41);
            dimage = double(dimage);
%이미지를 다시 RGB이미지로
            Super_Resolution = bicubiced_y + dimage;
            vdsr_image = ycbcr2rgb(cat(3,Super_Resolution,bicubiced_cb,bicubiced_cr));

미리 학습된 100 epoch을 거친 VDSR모델을 사용하여 이미지 해상도를 높였다. 원하는 이미지를 불러와 imread로 읽은 후, rgb2ycbcr을 거쳐 bicubic시킨 y 이미지를 가져와 모델에 넣고 ycbcr2rgb를 하여 향상된 이미지를 얻는다.

2.3 입력 및 출력

입력 / 출력




 

 

III.  참여 인원별 역할 및 수행 소감

[정원영] -보고서 작성, GUI 구현, 컬러필터링 함수 구현

 강의시간에 배운 바를 바탕으로 필터링하는데는 문제가 없었지만 정작 하고나니 양도 없고 배워가는 것도 없는것 같아 직접 HSV를 이용한 필터링 함수를 만들어 보았고, 색채같은 부분에 있어어서 많은 애를 먹었다. 하지만 원뿔형 구조에 대한 이해와 많은 시행착오를 겪은 후 약간이나마 이해를 할 수 있었고 사용자가 원하는 색채만을 추출한 이미지 필터링을 구현하는데 성공하였다.

또한 다른 팀원의 버튼을 구성할 수 있다는 제시를 통해 당시까지 구현한 필터링을 버튼 이벤트와 연동할 수 있었으며 결과적으로는 상당히 깔끔한 바탕의 GUI를 갖춘 프로젝트를 만들 수 있었다.

비대면이기에 많은 난항도 겪었지만 이번 프로젝트를 통해 진행에 대한 노하우를 얻었기 때문에 만족스러웠다.

 

[이창민] -보고서 작성, GUI구현, VDSR모델을 이용한 이미지 품질 향상 코드 수정

매트랩 수업과 딥러닝 수업에서 배운 내용을 결합하여 과제를 해보게 되어 매트랩이 여러 분야와 융합되어 다양하게 사용할 수 있음을 깨달았다. 수업시간에 배운 여러 노이즈제거 방법들도 딥러닝 분야와 합쳐 적용된다면 기존 dsp에서 쉽게 향상될 것이라고 느꼈다. 하지만 직접 모델을 개발할 환경이 되지 않아 epoch batchsize등을 늘리는 등 직접 모델을 개발, 수정해보지 못해 아쉬웟다. 완성도를 위해 GUI를 처음 만들어보니 시행착오도 많고 어려움도 많이 겪었지만 이때까지 GUI관련 프로그래밍은 거의 해보질 않아 좋은 경험이었다.

 

 

IV.  Project 후기

 

매트랩을 처음하면서 완성도를 위해 GUI를 구현하는데 있어 시행착오가 많았다. 버튼 이벤트 처리 과정에서 에러가 많이 났으며, 이미지 불러오기 처리 과정에서 CUDA를 지원하지 않는 그래픽카드여서 gpuArray를 사용하는데 에러가 많았다.  CUDA만을 지원하여 다른 intel, AMD 계열의 openCL을 지원하는 함수가 있었다면 범용적으로 사용 할 수 있어 좋았을 것이다.하지만 이번학기 딥러닝 과목을 들으면서 배운 내용을 토대로 다른 과제를 수행해 볼 수 있어서 좋았다. 아쉬운 점은 충분한 데이터 셋을 구할 수 있었다면 기존 모델에서 epoch을 늘리던가 batchsize를 늘려 좀더 좋은 결과 가진 모델을 사용할 수 있었을 것이며, 보고서의 제한 페이지가 존재하여 알고리즘에 대한 설명과 코드에 대한 팀의 이해도를 제대로 서술되지 못한 것 같아 아쉬움이 남는다.

 

이번 프로젝트를 통해 MATLAB이 강력한 언어임을 느꼈고, 이를 제대로 사용하기 위해서는 실 사용자인 우리가 익숙해져야한다는 점을을 느꼈다. 다른 프로젝트를 수행한다면 이번의 경험을 발판삼아 잘 진행할 수 있을 것으로 예상된다.

 

 

V.    Appendix

classdef app3_exported < matlab.apps.AppBase
 
    % Properties that correspond to app components
    properties (Access = public)
        UIFigure                      matlab.ui.Figure
        UploadimageforColorfilterButton  matlab.ui.control.Button
        UploadimageforGaussianButton  matlab.ui.control.Button
        UploadimageforEnhancementButton  matlab.ui.control.Button
        Label                         matlab.ui.control.Label
        UIAxes1_1                     matlab.ui.control.UIAxes
        UIAxes1_2                     matlab.ui.control.UIAxes
        UIAxes1_3                     matlab.ui.control.UIAxes
        UIAxes1_4                     matlab.ui.control.UIAxes
        UIAxes1_5                     matlab.ui.control.UIAxes
        UIAxes1_6                     matlab.ui.control.UIAxes
        UIAxes1_7                     matlab.ui.control.UIAxes
        UIAxes1_8                     matlab.ui.control.UIAxes
        UIAxes1_9                     matlab.ui.control.UIAxes
        UIAxes2_1                     matlab.ui.control.UIAxes
        UIAxes2_2                     matlab.ui.control.UIAxes
        UIAxes2_3                     matlab.ui.control.UIAxes
        UIAxes3_1                     matlab.ui.control.UIAxes
        UIAxes3_2                     matlab.ui.control.UIAxes
    end
 
   
    methods (Access = private)
       
        % ----------------------------------------------------------------------- %
        %                        C O L O R    F I L T E R                         %
        % -------------------------------------------------------------%
         % 빨강 범위: 320~50(hsv )%
         % 파랑 범위: 170~305        %
         % 초록 범위: 050~170        %
 
 
        function I = colorfilter(app, image, range)
 
            % RGB to HSV conversion
            I = rgb2hsv(image);        
   
            % Normalization range between 0 and 1
            range = range./360;
   
            % Mask creation
            if(size(range,1) > 1), error('Error. Range matriz has too many rows.'); end
            if(size(range,2) > 2), error('Error. Range matriz has too many columns.'); end
 
            if(range(1) > range(2))
                % Red hue case
                mask = (I(:,:,1)>range(1) & (I(:,:,1)<=1)) + (I(:,:,1)<range(2) & (I(:,:,1)>=0));
            else
                % Regular case
                mask = (I(:,:,1)>range(1)) & (I(:,:,1)<range(2));
            end
   
            % Saturation is modified according to the mask
            I(:,:,2) = mask .* I(:,:,2);
   
            % HSV to RGB conversion
            I = hsv2rgb(I);
   
        end
    end
   
 
    % Callbacks that handle component events
    methods (Access = private)
 
        % Button pushed function: UploadimageforColorfilterButton
        function UploadimageforColorfilterButtonPushed(app, event)
            global filename;
            global filepath;
           
            [filename, filepath] = uigetfile({'*.png; *.bmp; *.jpg','supported imgaes';...
                                   '*.png', 'Portable Network Graphics(*.png)';...
                                   '*.bmp', 'Bitmap (*.bmp)';...
                                   '*.jpg', 'JPEG(*.jpg)';...
                                   '*.*','All files(*.*)'}, filepath);
 
           
            % Diverse Gpu image filtering by ÀÌâ¹Î, Á¤¿ø¿µ for matlab project%
 
            % Use gpu %
            image = gpuArray(imread([filepath, filename]));
            dimage = im2double(image);
           
            % Sharpened + Equalize %
            gradient = convn(dimage,ones(3)./9,'same') - convn(dimage,ones(5)./25,'same');
            amount = 5;
            sharpened = dimage + amount.*gradient;
            equalized = histeq(dimage);
 
            % no (R/G/B) series %
            redChannel = dimage(:, :, 1);
            greenChannel = dimage(:, :, 2);
            blueChannel = dimage(:, :, 3);
            nored = cat(3, greenChannel, greenChannel, blueChannel);    % cover from here
            nogreen = cat(3, redChannel, redChannel, blueChannel);
            noblue = cat(3, redChannel, greenChannel, redChannel);      % to here
            
 
            orf = colorfilter(app,dimage,[320 50]);     % red
            ogf = colorfilter(app, dimage,[50 170]);     % green
            obf = colorfilter(app, dimage,[170 305]);    % blue
 
            % Output %
            imshow(dimage, 'parent', app.UIAxes1_1);
            imshow(sharpened, 'parent', app.UIAxes1_2);
            imshow(equalized, 'parent', app.UIAxes1_3);
            imshow(nored, 'parent', app.UIAxes1_4);
            imshow(nogreen, 'parent', app.UIAxes1_5);
            imshow(noblue, 'parent', app.UIAxes1_6);
            imshow(orf, [], 'parent', app.UIAxes1_7);
            imshow(ogf, [], 'parent', app.UIAxes1_8);
            imshow(obf, [], 'parent', app.UIAxes1_9);
          
        end
 
        % Button pushed function: UploadimageforGaussianButton
        function UploadimageforGaussianButtonPushed(app, event)
            global filename;
            global filepath;
           
            [filename, filepath] = uigetfile({'*.png; *.bmp; *.jpg','supported imgaes';...
                                   '*.png', 'Portable Network Graphics(*.png)';...
                                   '*.bmp', 'Bitmap (*.bmp)';...
                                   '*.jpg', 'JPEG(*.jpg)';...
                                   '*.*','All files(*.*)'}, filepath);
 
           
            % Diverse Gpu image filtering by ÀÌâ¹Î, Á¤¿ø¿µ for matlab project%
 
            % Use gpu %
            image = gpuarray(imread([filepath, filename]));
            dimage = im2double(image);
           
            % Gaussian + filtering %
            grayed = mat2gray(image3);
            gau = imnoise(grayed, 'gaussian', 0,0.025);
            filtered = medfilt2(gau);
 
            adjim = imadjust(dimage);     %Adjust non colored photo%
 
           
 
            % Output %
            imshow(imresize(dimage3,1),'parent', app.UIAxes2_1);
            imshow(imresize(adjim,1),'parent', app.UIAxes2_2);
            imshow(imresize(filtered,1),'parent', app.UIAxes2_3);
         
        end
 
        % Button pushed function: UploadimageforEnhancementButton
        function UploadimageforEnhancementButtonPushed(app, event)
            global filename;
            global filepath;
           
            [filename, filepath] = uigetfile({'*.png; *.bmp; *.jpg','supported imgaes';...
                                   '*.png', 'Portable Network Graphics(*.png)';...
                                   '*.bmp', 'Bitmap (*.bmp)';...
                                   '*.jpg', 'JPEG(*.jpg)';...
                                   '*.*','All files(*.*)'}, filepath);
 
            original = imread(fullfile(filepath,filename));
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
            %image enhancement Model Load%
            load('trainedVDSR-Epoch-100-ScaleFactors-234.mat');
           
            dimage = original;
            dimage = im2double(dimage);
           
            [rows,cols,np] = size(dimage);
           
            %RGB이미지 색체, 휘도 분리
            ycbcr_image = rgb2ycbcr(dimage);
            y = ycbcr_image(:,:,1);% 휘도
            cb = ycbcr_image(:,:,2);% 색채
            cr = ycbcr_image(:,:,3);% 색채
           
            %삼차보간법을 거친 휘도,색채
            bicubiced_y = imresize(y,[rows cols],'bicubic'); % bicubiced 휘도
            bicubiced_cb = imresize(cb,[rows cols],'bicubic'); % bicubiced 색채
            bicubiced_cr = imresize(cr,[rows cols],'bicubic'); % bicubiced 색채
           
 
           
            %휘도 이미지로 학습된 딥러닝 알고리즘
            dimage = activations(net,bicubiced_y,41);
            dimage = double(dimage);
           
            %이미지를 다시 RGB이미지로
            Super_Resolution = bicubiced_y + dimage;
            vdsr_image = ycbcr2rgb(cat(3,Super_Resolution,bicubiced_cb,bicubiced_cr));
 
           
            imshow(original,'parent', app.UIAxes3_1)
            imshow(vdsr_image,'parent', app.UIAxes3_2)
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%          
        end
    end
 
    % Component initialization
    methods (Access = private)
 
        % Create UIFigure and components
        function createComponents(app)
 
            % Create UIFigure and hide until all components are created
            app.UIFigure = uifigure('Visible', 'off');
            app.UIFigure.Position = [100 100 506 674];
            app.UIFigure.Name = 'MATLAB App';
 
            % Create UploadimageforColorfilterButton
            app.UploadimageforColorfilterButton = uibutton(app.UIFigure, 'push');
            app.UploadimageforColorfilterButton.ButtonPushedFcn = createCallbackFcn(app, @UploadimageforColorfilterButtonPushed, true);
            app.UploadimageforColorfilterButton.Position = [14 641 163 25];
            app.UploadimageforColorfilterButton.Text = 'Upload image for Color filter';
 
            % Create UploadimageforGaussianButton
            app.UploadimageforGaussianButton = uibutton(app.UIFigure, 'push');
            app.UploadimageforGaussianButton.ButtonPushedFcn = createCallbackFcn(app, @UploadimageforGaussianButtonPushed, true);
            app.UploadimageforGaussianButton.Position = [14 284 164 22];
            app.UploadimageforGaussianButton.Text = 'Upload image for Gaussian ';
 
            % Create UploadimageforEnhancementButton
            app.UploadimageforEnhancementButton = uibutton(app.UIFigure, 'push');
            app.UploadimageforEnhancementButton.ButtonPushedFcn = createCallbackFcn(app, @UploadimageforEnhancementButtonPushed, true);
            app.UploadimageforEnhancementButton.Position = [16 135 182 22];
            app.UploadimageforEnhancementButton.Text = 'Upload image for Enhancement';
 
            % Create Label
            app.Label = uilabel(app.UIFigure);
            app.Label.Position = [-68 273 1 5];
 
            % Create UIAxes1_1
            app.UIAxes1_1 = uiaxes(app.UIFigure);
            title(app.UIAxes1_1, '¿øº»')
            app.UIAxes1_1.Position = [12 529 133 113];
 
            % Create UIAxes1_2
            app.UIAxes1_2 = uiaxes(app.UIFigure);
            title(app.UIAxes1_2, 'sharpened ')
            app.UIAxes1_2.Position = [176 529 133 113];
 
            % Create UIAxes1_3
            app.UIAxes1_3 = uiaxes(app.UIFigure);
            title(app.UIAxes1_3, 'Equalized')
            app.UIAxes1_3.Position = [353 529 133 113];
 
            % Create UIAxes1_4
            app.UIAxes1_4 = uiaxes(app.UIFigure);
            title(app.UIAxes1_4, 'red to green')
            app.UIAxes1_4.Position = [12 417 133 113];
 
            % Create UIAxes1_5
            app.UIAxes1_5 = uiaxes(app.UIFigure);
            title(app.UIAxes1_5, 'green to red')
            app.UIAxes1_5.Position = [176 417 133 113];
 
            % Create UIAxes1_6
            app.UIAxes1_6 = uiaxes(app.UIFigure);
            title(app.UIAxes1_6, 'blue to red')
            app.UIAxes1_6.Position = [353 417 133 113];
 
            % Create UIAxes1_7
            app.UIAxes1_7 = uiaxes(app.UIFigure);
            title(app.UIAxes1_7, '(filter)only red')
            app.UIAxes1_7.Position = [16 305 133 113];
 
            % Create UIAxes1_8
            app.UIAxes1_8 = uiaxes(app.UIFigure);
            title(app.UIAxes1_8, '(filter)only green')
            app.UIAxes1_8.Position = [176 305 133 113];
 
            % Create UIAxes1_9
            app.UIAxes1_9 = uiaxes(app.UIFigure);
            title(app.UIAxes1_9, '(filter)only blue')
            app.UIAxes1_9.Position = [353 305 133 113];
 
            % Create UIAxes2_1
            app.UIAxes2_1 = uiaxes(app.UIFigure);
            title(app.UIAxes2_1, '¿øº»')
            app.UIAxes2_1.Position = [12 156 133 113];
 
            % Create UIAxes2_2
            app.UIAxes2_2 = uiaxes(app.UIFigure);
            title(app.UIAxes2_2, 'Adjusted')
            app.UIAxes2_2.Position = [177 156 133 113];
 
            % Create UIAxes2_3
            app.UIAxes2_3 = uiaxes(app.UIFigure);
            title(app.UIAxes2_3, 'Gaussian + filtering')
            app.UIAxes2_3.Position = [353 156 133 113];
 
            % Create UIAxes3_1
            app.UIAxes3_1 = uiaxes(app.UIFigure);
            title(app.UIAxes3_1, '¿øº»')
            app.UIAxes3_1.Position = [29 11 133 113];
 
            % Create UIAxes3_2
            app.UIAxes3_2 = uiaxes(app.UIFigure);
            title(app.UIAxes3_2, 'Deep learning Enhancement')
            app.UIAxes3_2.Position = [197 11 133 113];
 
            % Show the figure after all components are created
            app.UIFigure.Visible = 'on';
        end
    end
 
    % App creation and deletion
    methods (Access = public)
 
        % Construct app
        function app = app3_exported
 
            % Create UIFigure and components
            createComponents(app)
 
            % Register the app with App Designer
            registerApp(app, app.UIFigure)
 
            if nargout == 0
                clear app
            end
        end
 
        % Code that executes before app deletion
        function delete(app)
 
            % Delete UIFigure when app is deleted
            delete(app.UIFigure)
        end
    end
end
 
 

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

스프링 핵심 원리 이해1 - 예제


 

비지니스 요구사항과 설계

  • 회원일반, vip 등급의 회원 존재
  • DB는 미정
  • 회원 가입, 조회 가능
  • 주문과 할인 정책등급에 따라 할인 정책 가능
  • VIP는 무조건 1000원 할인 (정책 가정, 확정은 아님)
  • 회원이 상품 주문가능

 

=> 미정인 부분은 인터페이스로 역할/구현을 분리하면 된다!!

 

** 일단은 스프링이 아닌 순수 자바코드로 구현한다고 가정

 

회원 도메인


 

  • 클라이언트 -> 회원서비스(가입, 조회) -> 회원 저장소(미정)
  • MemberService(구현체: MemberServiceImpl) -> MemberRepository(구현체: 일단 MemoryMemberRepository)
  • 클라이언트 객체 -> 회원서비스 객체(MemberServiceImpl) -> 멤버리포지토리 객체

 

회원 도메인 개발

회원 등급, 회원 엔티티 필요

 

회원등급

package hello.core.member;
public enum Grade {
    BASIC,
    VIP
}

회원 엔티티

package hello.core.member;

//id, name, grade 필요
public class Member {
    private long id;
    private String name;
    private Grade grade;

    public Member(long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public void setId(long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Grade getGrade() {
        return grade;
    }


}

 

회원 저장소

회원저장소 인터페이스, 구현체

 

회원저장소 인터페이스 - 저장, 멤버찾기 기능

package hello.core.member;

//id, name, grade 필요
public class Member {
    private long id;
    private String name;
    private Grade grade;

    public Member(long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public void setId(long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Grade getGrade() {
        return grade;
    }


}

 

구현체 - 일단 메모리 저장소

package hello.core.member;

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

//저장소 필요
public class MemoryMemberRepository implements MemberRepository{

    //일단 임시 저장소 = 메모리 사용
    private static Map<Long, Member> store = new HashMap<>();

    @Override
    public void save(Member member) {
        store.put(member.getId(),member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);//store는 Map이므로 키값으로 member찾음
    }
}

 

 

회원 서비스

회원 가입과 조회 기능이 필요하다.

 

회원 서비스 인터페이스 (역할)

package hello.core.member;

public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}

 

구현

package hello.core.member;

//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{
    
    MemberRepository memberRepository = new MemoryMemberRepository();
    
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

일단 멤버도메인, 멤버서비스 ,멤버저장소 구현완료!!

 

일단 main으로 테스트

package hello.core.member;

//테스트해보는 용도
public class MemberApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1l, "memberA", Grade.VIP);

        //회원가입
        memberService.join(member);

        //가입한 멤버가 있는지 조회
        Member findMember = memberService.findMember(1L);

        //확인
        System.out.println("new member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());


    }
}

은 안좋으니 Junit으로 테스트

 

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    MemberService memberService = new MemberServiceImpl();

    @Test
    void join(){

        //given - vip
        Member member = new Member(1L, "memberA" , Grade.VIP);

        //when - 회원가입
        memberService.join(member);
        Member findMember = memberService.findMember(member.getId());

        //then - 가입한 회원이 맞는지
        Assertions.assertThat(member).isEqualTo(findMember);


    }
}

 

=> memberServiceImpl에서 MemberRepository memberRepository = new MemoryMemberRepository(); 로 구현체를 직접 참조하고 있다. 따라서 구현에 의존하게 되는 문제점이 발생한다.!

 

 

주문과 할인 도메인 설계

  • 회원은 상품 구매가능
  • 등급에따라 할인됨
  • 할인정책으론 모든 vip는 1000원 할인

 

  1. 클라이언트 - (주문생성) -> 주문 서비스 역할 - (회원조회) -> 회원저장소 역할
  2. 주문생성(id, 상품명, 상품가격), 회원조회(id로 조회) => 상품명과 상품가격은 간단하게 객체가 아니라 data로 만듬
  3. 주문서비스 역할 - (할인 적용) -> 할인 정책역할
  4. 할인 적용 ( vip인가? 확인)
  5. 주문서비스 역할 - (주문결과 반환) -> 클라이언트
  6. 주문결과는 간단하게 DB에 저장이 아니라 주문결과를 반환

 

클라이언트 -> 주문서비스 -> 1. 회원저장소 2.할인정책

 

할인 정책에는 고정가 할인 정책과 비율 할인 정책이 있다.

 

할인 정책 인터페이스

package hello.core.discount;

import hello.core.member.Member;

//vip 인지 확인해서 할인된 상품가격 결과를 반환한다.
public interface DiscountPolicy {
    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);
}

 

고정가 할인 정책

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

//vip이면 1000원 할인, 할인금액 반환
public class FixDiscountPolicy implements DiscountPolicy{

    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return discountFixAmount;
        }
        else{
            return 0;
        }
    }
}

 

 

주문 서비스

먼저 주문 엔티티를 만든다.

(회원 id, 상품명, 상품가격)

 

package hello.core.order;

//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
//할인된 결과 필요
public class Order {
    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    //할인된 결과
    public int calculatePrice(){
        return itemPrice - discountPrice;
    }

    public void setMemberId(Long memberId) {
        this.memberId = memberId;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }

    public Long getMemberId() {
        return memberId;
    }

    public String getItemName() {
        return itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }
    
    //결과 쉽게 보기 위해서


    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

 

 

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    
    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

 

주문과 할인 테스트

package hello.core.order;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import net.minidev.json.JSONUtil;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

//회원등급을 조회하고 그에맞는 할인이 적용되었는가?
//주문서비스 -> 저장소, 할인정책
public class OrderServiceTest {
    //given
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();



    //주문 테스트
    @Test
    void createOrder() {

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

        //when
        memberService.join(member);
        Order order = orderService.createOrder(memberId, "itemA", 10000);

        //then

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

 

 



 

새로운 할인 정책 개발

 

10프로 할인 정책!

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

//10퍼센트만 할인 한다고 가정
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;
    
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

 

새 할인 정책 적용!

OrderService 구현체에서 직접 선택해줘야된다 => 따라서 구현체에 의존하게된다.(인터페이스가 아니라)

클라이언트(OrderServiceImpl)의 코드를 수정해야한다.

OCP => 변경하지않고 확장가능 => 깨짐

DIP => 인터페이스 뿐만 아니라 구현체에 의존하게됨.

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

따라서

클라이언트(OrderServiceImpl)를 공연의 예제에 비교하면

배우(로미오 역)이 직접 여배우(줄리엣)역을 섭외하는 격이다!!

배우는 대본에만 집중을 해야지 섭외의 역할까지 맡게되면서 다양한 책임을 가지게 된다.

 

그래서 관심사를 분리해야한다.

배우는 배우 역할에만 신경쓰게!

 

따라서 공연기획자가 필요하다.

공연을 구성, 배우 섭외, 역할에 맞는 배우를 지정하는 책일을 가지는 공연 기획자가 필요!

 

바로 공연기획자가 AppConfig이다.

 

AppConfig

애플리케이션의 전체 동작 방식을 구성한다.

구현 객체를 생성하고, 연결하고 책임을 가진다.

위 역할을 하는 별도의 설정 클래스이다.

 

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

//의존관계 주입!
public class AppConfig {
    
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

각 클라이언트의 생성자를 이용하여 맞는 구현체를 넣어준다.

=> 생성자 주입기법

 

그러면 이제 각 구현체에서 생성자를 통해서 받아오기만 하면 된다!!!

멤버서비스 구현체

package hello.core.member;

//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{

    //배역 배우로 생각할때
    //MemberRepository memberRepository = new MemoryMemberRepository();

    //생성자로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;

    //생성자
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

주문 서비스 구현체

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemoryMemberRepository;

//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //생성자 주입기법으로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

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

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

AppConfig를 통하여 주입받기 위해 MemberApp 클래스를 수정한다.

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;

//테스트해보는 용도
public class MemberApp {
    public static void main(String[] args) {

        //AppConfig를 통하여 주입
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();//주입!
        //멤버서비스 구현체는 생성자로 적절한 구현체들을 받게된다.
        //즉, 멤버 서비스 구현체는 어떤 구현체를 가져올지 고민안해도 됨!
        //밑에선 memberServiceImpl를 가져오고 MemberServiceImpl 내부적으로 어떤걸 쓸지 골라야됬음!
        //MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1l, "memberA", Grade.VIP);

        //회원가입
        memberService.join(member);

        //가입한 멤버가 있는지 조회
        Member findMember = memberService.findMember(1L);

        //확인
        System.out.println("new member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());


    }
}

 

주문 서비스 또한 appConfig로 인젝션을 해준다.

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;

//주문 하기
public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        OrderService orderService = appConfig.orderService(); //주입!!\
        MemberService memberService = appConfig.memberService();

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

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

        System.out.println("Order = " + order); 
    }
    


}

 

각 테스트코드 또한 바꾸어 준다.

@BeforeEach를 사용하여 appconfig로 주입시켜준다.

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    //MemberService memberService = new MemberServiceImpl();

    MemberService memberService;
    
    @BeforeEach
    void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
    @Test
    void join(){

        //given - vip
        Member member = new Member(1L, "memberA" , Grade.VIP);

        //when - 회원가입
        memberService.join(member);
        Member findMember = memberService.findMember(member.getId());

        //then - 가입한 회원이 맞는지
        Assertions.assertThat(member).isEqualTo(findMember);


    }
}

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    //MemberService memberService = new MemberServiceImpl();

    MemberService memberService;
    
    @BeforeEach
    void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
    @Test
    void join(){

        //given - vip
        Member member = new Member(1L, "memberA" , Grade.VIP);

        //when - 회원가입
        memberService.join(member);
        Member findMember = memberService.findMember(member.getId());

        //then - 가입한 회원이 맞는지
        Assertions.assertThat(member).isEqualTo(findMember);


    }
}

 

 

AppConfig 리팩터링

기존

public class AppConfig {
    
    //메모리멤버리포지토리가 두번이나 생성되어 중복된다.
    //그리고 각 역할과 구현이 뚜렷하게 눈에 보이지 않는다.
    
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

 

리팩터링 후

public class AppConfig {

    //멤버 서비스는 멤버리포지토리가 필요하다.
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    //리포지토리는 멤버 리포지토리
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    //할인정책은 FixDiscountPolicy
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
        //return new FixDiscountPolicy();
    }

}

중복이 제거되었고, 각 역할과 구현이 명확하게 보인다.

 

또한 AppConfig 설정을 바꿔줌으로써 할인정책을 바꿀수 잇게 되었다.

따라서 클라이언트의 코드를 수정하지 않고 변경가능!!!

OCP = 확장에 유연, 변경에는 닫힘

을 지킬 수 있고

DIP = 인터페이스에 의존

도 가능해졌다~~!

 

AppConfig를 사용함으로써

사용영역과 구성영역으로 구분이 되었고, 변경사항시 구성영역만 변경하면 되게 되었따.

또한 구성영역을 사용함으로써 역할과 구현이 명확하게 분리 되고, 역할이 잘 보이게 되었고, 중복이 제거되었다.

 

정리하면, SRP, DIP, OCP를 적용하게 되었다.

 

SRP

한 클래스는 하나의 책임만 가져야한다.

기존 - 클라이언트가 직접 구현객체생성, 연결, 실행

AppConfig 사용 - 구현객체를 AppConfig가 생성하고 연결함, 따라서 클라이언트는 실행하는 책임만 가지게됨.

 

DIP 의존관계 역전 원칙

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다."

=> 의존관계 주입은 이 원칙을 따르는 방법 중 하나이다.

 

기존 - 새로운 할인정책을 만들고 적용시킬때, 클라이언트 코드 수정해야했음

AppConfig 사용 - AppConfig가 할인정책 객체 인스턴스를 클라이언트 코드 대신에 생성해서 클라이언트 코드에 의존관계를 주입했다.

즉, 외부에서 객체 인스턴스를 넣어주어 DIP원칙을 지켰다.

 

 

OCP

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.

다형성을 사용하고 클라이언트가 DIP를 지키면서 OCP가능성이 높아졌다.

애플리케이션 => 사용영역 / 구성영역으로 분리되었다.

AppConfig가 의존관계를 클라이언트에 주입해주어 클라이언트 코드변경X

따라서, 소프트웨어 요소를 새로 확장해도 사용 영역의 변경은 없다 => 즉 닫혀있다.!

 

 

IoC, DI, 컨테이너

IoC? => Inversion of Control(제어의 역전) , 제어를 AppConfig가 가지게 됨(외부에서 제어흐름을 가지게 됨)

 

기존 - 클라이언트 구현객체가 스스로 서버구현객체 생성, 연결, 실행 => 구현 객체가 프로그램 제어흐름 조종!

AppConfig - 구현객체는 자신의 로직만 실행, 제어흐름은 AppConfig가 해줌

(예를 들면 OrderServiceImpl은 필요한 인터페이스를 호출 하지만 어떤 구현객체가 올지 모른다. => AppConfig가 주입해줌)

 

 

프레임워크 vs 라이브러리

프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다 => ex) Junit

내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리이다. => 직접 메서드 호출 등..

 

DI(Dependecy Injection)= 의존관계 주입

OrderServiceImpl(구현체)는 DiscountPolicy(인터페이스)에 의존한다. => 어떤 DiscountPolicy가 올지 모른다.

의존관계는 정적인 클래스 의존 관계, 실행시점에 결정되는 동적인 객체 의존 관계를 분리해서 생각해야한다.

 

  • 정적인 클래스 의존 관계즉, 애플리케이션을 실행하지 않아도 분석가능!
  • 하지만 어떤 구현객체가 주입되는지는 모름!
  • 클래스가 사용하는 import 코드만 보고 의존관계 파악이 가능하다.
  • 동적인 객체 의존 관계애플리케이션 실행 시점(런타임)에 외부에서 실제 구현객체를 생성해서 클라이언트에 전달!의존관계 주입을 통해서 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다.
  • 클라이언트와 서버 의존관계가 연결된다. => 의존관계 주입
  • 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스 참조가 연결된 의존 관계

 

IoC컨테이너 = DI컨테이너 (어셉블러(조립), 오브젝트 팩토리(오브젝트를 만드니까) 등으로 불리기도함)

AppConfig와 같은 역할

객체를 생성, 관리하면서 의존관계를 연결해주는 것.

 

 

살짝 정리

이때까지 순수 자바코드로 DI, 의존관계 주입을 해보았다.

다음으론 스프링을 사용하여 의존관계 주입을 해본다.

 

 

 

 

 

스프링으로 전환


 

AppConfig 스프링 기반으로 변경

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
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 MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

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

    @Bean
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }

}

 

MemberApp에 스프링 컨테이너(객체 생성, 관리, 의존관계 주입) 적용

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

//테스트해보는 용도
public class MemberApp {
    public static void main(String[] args) {

        //AppConfig를 통하여 주입
//        AppConfig appConfig = new AppConfig();
//       MemberService memberService = appConfig.memberService();//주입!
        //멤버서비스 구현체는 생성자로 적절한 구현체들을 받게된다.
        //즉, 멤버 서비스 구현체는 어떤 구현체를 가져올지 고민안해도 됨!
        //밑에선 memberServiceImpl를 가져오고 MemberServiceImpl 내부적으로 어떤걸 쓸지 골라야됬음!
        //MemberService memberService = new MemberServiceImpl();


        /**
         * 스프링 컨테이너 적용
         */
        //ApplicationContext를 스프링 컨테이너라 보면된다. 스프링은 모든것이 이 A.C로 부터 시작된다.
        //AppConfig로부터 구성 정보를 가져온다.
        // => 스프링이 빈들을 설정해서 스프링 컨테이너에 객체를 생성해서 갖고 있는다.
        //어노테이션 기반으로 config를 하므로 AnnotationConfigApplicationContext 사용
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        
        //getBean(메서드명, 타입)으로 가져온다.
        //MemberServiceImpl.class와 같이 구체 타입을 지정해도 되나, 구현에 의존하게 되는 꼴이 되버린다.
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        
        Member member = new Member(1l, "memberA", Grade.VIP);


        //회원가입
        memberService.join(member);

        //가입한 멤버가 있는지 조회
        Member findMember = memberService.findMember(1L);

        //확인
        System.out.println("new member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());


    }
}

 

OrderApp에도 스프링 컨테이너를 적용시킨다.

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

//주문 하기
public class OrderApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        OrderService orderService = appConfig.orderService(); //주입!!\
//        MemberService memberService = appConfig.memberService();

        //스프링 컨테이너 생성
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        //빈 가져옴
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

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

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

        System.out.println("Order = " + order);
    }



}

 

 

실행 후 로그를 보면

17:06:34.789 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
17:06:34.808 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
17:06:34.810 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
17:06:34.811 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'

"Creating shared instance of singleton bean" 스프링에 등록된 것을 알 수 있다.

스프링에 등록시 (key(메서드명), value(타입))으로 저장이된다.

 

이걸 정리하면

  • ApplicationContext = 스프링 컨테이너 (Context = 문맥)
  • 기존엔 개발자가 AppConfig를 사용해서 직접 객체 생성하고 DI
  • @Configuration 어노테이션이 붙은 AppConfig를 설정정보로 사용한다.
  • @Bean 이 붙은 메서드를 모두 호출하고 반환된 객체를 스프링 컨테이너에 등록한다. (등록된 객체 = 스프링 빈)
  • 관례로 @Bean이 붙은 메서드 명을 스프링 빈 이름으로 사용한다. (@Bean (name = "AA")와 같이 변경도 가능한데 왠만하면 관례따르는게 보기 편하고 관리 편하다.)
  • 기존엔 직접 필요한 객체를 AppConfig를 이용해서 조회했다.
  • => 스프링 컨테이너를 통해서 applicaionContext.getBean()메서드를 사용해서 스프링빈, 즉 객체를 찾게되었따.

즉, 직접 자바코드로 모든 것을 하다가

  1. 스프링 컨테이너에 객체를 스프링 빈으로 등록
  2. 스프링 컨테이너에서 스프링 빈을 찾아서 사용

과 같이 바뀌게 되었다.

 

이렇게 바꾸었을 때 장점은 무엇일까?..... => 뒤에서

 

 

스프링 컨테이너와 스프링 빈


스프링 컨테이너 생성과정을 다시 보면

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext 이 스프링 컨테이너이고, 인터페이스이다.

따라서 다형성이 적용된다.

"new AnnotationConfigApplicationContext(AppConfig.class);"클래스는 ApplicationContext 인터페이스의 구현체이다.

스프링 컨테이너는 어노테이션 기반의 자바 설정 클래스로 만들수도 있고, XML을 기반으로 만들 수도 있다.

 

정리하면

  1. 스프링 컨테이너 생성
  2. 스프링 컨테이너 생성(AppConfig.class) - 스프링 컨테이너 생성시 구성정보 지정해주어야함.
  3. 스프링 빈 등록빈 이름(Key) : 빈 객체(value) 로 저장된다. (빈이름은 메서드명이 사용된다.)
  4. ex) memberService : MemberServiceImpl@0x1
  5. 파라미터로 넘어온 설정정보(AppConfig.class)를 사용해서 @Bean들을 다 등록함.
  6. 스프링 빈 의존관계 설정 - 준비
  7. 빈들이 스프링 컨테이너에 등록이 되었지만 서로 연결되진 않았다. = 의존관계 주입이 안되었음
  8. 스프링빈 의존관계 설정 - 완료동적 의존 관계 연결시켜줌.
  9. (설정 정보를 참고해서 의존관계가 주입되어짐.)
  10. 각 객체들이 생성되면서 의존관계가 주입되어진다.

 

** 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져있다.

위와같이 자바코드로 스프링 빈 등록시, 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.

    @Bean//스프링 빈으로 등록
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    } // -> 호출되면서 memberservice와 리포지토리가 자동으로 연결되게 된다

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

    @Bean
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    } // 호출되면서 리포지토리, 할인정책이 불려오면서 같이 연결되게 된다.

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

이제 컨테이너에 잘 등록되었는지 확인 해보자.

 

 

컨테이너에 등록된 모든 빈 조회

package hello.core.beanfind;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ApplicationContextInfoTest {
    //스프링 컨테이너 불러옴
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean(){
        //스프링 빈 이름들을 String으로 빼냄
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " Object = " + bean );

        }
    }
    //스프링에 등록된 모든 빈정보가 나옴 (스프링 내부에서 사용하는 빈까지 다 나옴)


    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean(){
        //스프링 빈 이름들을 String으로 빼냄
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            //BeanDefinition => 빈에대한 정보들, 정보들 가져옴
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            //role_appliction과 role_infrastructure(내부에서 사용하는 빈)가 있음
            //역할로 걸러서 출력함
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " Object = " + bean);
            }

        }
    }

}

 

 

스프링 빈 조회 - 기본

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회방법은

ac.getBean(빈이름, 타입)

ac.getBean(타입) => 이름 생략가능

 

만약 조회 대상이 없을 경우엔 예외가 발생한다.

NoSuchBeanDefinitionException: No bean named "xxxxx" available

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

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

public class ApplicationContextBasicFindTest {
    //스프링 컨테이너 가져옴
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName(){
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //isInstanceOf => 어떤 클래스인가?
    }

    @Test
    @DisplayName("이름 없이 빈 타입으로 조회")
    void findBeanByType(){
        //인터페이스로 조회하면 알아서 사용하는 구현체(스프링 빈에 등록된)가 조회된다.
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //isInstanceOf => 어떤 클래스인가?
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findByName2(){
        //구현체에 의존하게 된다.....
        MemberService memberService = ac.getBean(MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //isInstanceOf => 어떤 클래스인가?
    }

    //조회가 안되는 경우도 테스트 해봐야해!!
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findByNameX(){
        //MemberService memberService = ac.getBean("XXX", MemberService.class);
        //조회가 안되면 NoSuchBeanDefinitionException 터친다.
        //Junit Assertion의 assertThrows를 사용해서 예외를 확인 해야한다.
        assertThrows(NoSuchBeanDefinitionException.class, ()->ac.getBean("XXX", MemberService.class));

    }


}

 

**참고 assertThrows

org.junit.jupiter.api.Assertions.assertThrows(

 NoSuchBeanDefinitionException.class, // 발생이 예상되는 예외의 타입

 ()-> ac.getBean("xxxx", MemberService.class) // 예외가 발생될 수 있는 코드 블록

);


/*
.

먼저 해당 테스트 메소드는 존재하지 않는 빈의 이름으로 빈을 가져오려고 할 때 예외가 발생되는 상황을 테스트 하기 위한 메소드입니다.

ac.getBean("xxxx", MemberService.class); 해당 문장을 실행하면 존재하지 않는 빈의 이름(xxxx)으로 빈을 꺼내오려고 할 것입니다. 그러나 당연히 xxxx라는 이름으로 등록된 빈이 없기 때문에 NoSuchBeanDefinitionException 예외가 발생합니다.

.

assertThrows 메소드는 발생이 예상되는 예외의 타입, 예외가 발생될 수 있는 코드 블록을 파라미터로 받아서 실행됩니다.

이 때, assertThrows 내부에서는 예외가 발생될 수 있는 코드 블록을 실행합니다. 만약 해당 코드 블록을 실행 중 예외가 발생한다면 발생된 예외가 발생이 예상되는 예외의 타입과 일치하는지 아닌지 확인합니다. 이때 발생된 예외 타입과 예상되는 예외의 타입이 일치하면 테스트는 성공으로 처리됩니다.

.

아래 코드는 assertThrows의 내부 동작입니다. try 블록 안에서 코드를 실행하여 예외 발생시 catch 내에서 발생한 예외 타입과 예상되는 예외 타입을 비교하고 있습니다.
*/

 

 

스프링 빈 조회 - 동일한 타입이 둘 이상

타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류 발생!!!!

이때는 빈 이름을 지정하자

ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.

 

package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

//타입 으로 조회시 같은 타입이 둘 이상이면...
// 테스트를 위해 간단한 설정 클래스 만듬
public class ApplicationContextSameBeanFindTest {
    //테스트를 위한 sameBean 설정 정보를 가져옴
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);


    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상이면 중복오류 발생")
    void findBeanTypeDuplicate(){
        //먼저 어떤 예외를 던지는지 확인해본다.
        //MemberRepository bean = ac.getBean(MemberRepository.class);
        /*
        org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memberRepository1,memberRepository2
        NoUniqueBeanDefinitionException를 던진다.
         */
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(MemberRepository.class));

    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
    void findBeanByName(){
        //설정클래스에 메소드 둘다 MemberRepository이다.따라서 이름을 같이 지정
        MemberRepository bean = ac.getBean("memberRepository1", MemberRepository.class);
        org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(MemberRepository.class);

    }

    //특정 타입 모두 조회 => 둘 다 꺼내고 싶음
    //getBeansOfType 사용하면 된다.
    //MemberRepository형 모두 꺼냄 
    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanType(){
        //getBeansOfType은 Map형으로 반환함 <이름, 타입>
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key);            
        }
        System.out.println("beansOfType = " + beansOfType);
        org.assertj.core.api.Assertions.assertThat(beansOfType.size()).isEqualTo(2);
        //해당 타입의 개수가 2개이므로 2개 다 꺼내졌는지 확인
    }


    //구성 정보이므로 어노테이션 붙임
    //메서드 명은 다르지만, 반환하는 타입은 같음
    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

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

    }
}

 

 

스프링 빈 조회 - 상속관계

부모타입으로 조회하면 자식타입도 함께 조회된다.

예를 들어 Object타입으로 조회시 모든 스프링빈을 조회하게 된다.

 

테스트코드로 확인해보자

package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;

import java.util.Map;


public class ApplicationContextExtendsFindTest {
    //스프링 컨테이너
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시 자식이 둘 이상 있으면 중복 오류")
    void findBeanByParentTypeDuplicate(){
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        //->NoUniqueBeanDefinitionException
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(DiscountPolicy.class));

    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름 지정해서 쓰면댐")
    void findBeanByParentTypeBeanName(){
        DiscountPolicy bean = ac.getBean("fixDiscountPolicy", DiscountPolicy.class);
        org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(FixDiscountPolicy.class);

    }


    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeansBySubType(){
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(RateDiscountPolicy.class);

    }


    @Test
    @DisplayName("부모 타입으로 다 조회")
    void findBeansByParentType1(){
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key + "+ key + "value = "+ beansOfType.get(key));


        }

    }

    @Test
    @DisplayName("부모 타입으로 다 조회-object")
    void findBeansByParentType2(){
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key + "+ key + "value = "+ beansOfType.get(key));


        }

    }


    //설정 클래스이므로
    @Configuration
    static class TestConfig {

        //스프링 빈에 등록
        @Bean
        public DiscountPolicy rateDiscountPolicy(){
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy(){
            return new FixDiscountPolicy();
        }
    }
}

 

 

직접 getBean할 경우는 잘 없음!!

개발하면서 애플리케이션 컨텍스트에서 빈을 조회할 일이 거의 없음.

하지만 기본기능이고, 가끔 순수 자바애플리케이션에서 스프링컨테이너를 생성해서 쓸때 사용한다.

 


 

BeanFactory와 ApplicationContext

ApplicationContext, AnnotationConfig(구현클래스) ---> ApplicationContext(인터페이스) ---> BeanFactory(인터페이스)

 

BeanFactory

스프링 컨테이너의 최상위 인터페이스.

스프링 빈을 관리, 조회하는 역할 담당, 예시로 getBean을 제공해줌

이전 코드들에서 대부분 사용한 기능은 BeanFactory가 제공하는 기능임,

Then, 왜 BeanFactory가 아닌 ApplicationContext를 사용했을까??

 

ApplicationContext

BeanFactory 기능을 모두 상속받아서 제공한다.

차이점은, 애플리케이션 개발시, 빈은 관리하고 조회하는 기능이외에 수많은 기능들이 필요하다.

 

예를 들면 ApplicationContext가 구현하는 인터페이스들을 보면

MessageSource = 메시지 소스를 활용한 국제화기능(한국에서 들어오면 한국어, 외국에서 들어오면 영어)

EnvironmnetCapable = 환경변수이다. 로컬, 개발, 운영등 구분해서 처리 (예를 들면 로컬 개발환경, 테스트서버 개발환경, 실제운영환경 등등 사용하는 DB가 다르다면? 맞게 설정)

ApplicationEventPublisher = 애플리케이션 이벤트이다. 이벤트를 발행하고 구독하는 모델을 편리하게 지원

ResourceLoader = 편리한 리소스 조회. 파일, 클래스패스,외부 등에서 리소스를 편리하게 조회

 

일단 이런게 있다 정도만 알고 가자...

 

정리하면 ApplicationContext는 BeanFactory 기능들에 편리한 기능을 추가한것이다. 따라서 BeanFactory는 거의 잘 안쓴다.

 

 

 

다양한 설정 형식 지원 - 자바코드, xml

스프링 설정정보를 java파일 이외에 xml, groovy 등등을 사용할 수 있다.

왜냐면

ApplicationContext(인터페이스) 구현체들로 다음과 같은 것들이 있기 때문이다.

AnnotationConfig AppliactionContext => AppConfig.class

GeneriXml ApplicationContext => appConfig.xml

Xxx ApplicationContext => appConfig.xxx =>다양한 설정형식을 지원한다.

 

XML 설정 사용해보기

요즘 잘 안쓰긴하지만 xml을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 잇다.

GenericXmlApplicationContext를 사용해서 xml 설정 파일을 넘기면 된다.

package hello.core.xml;

import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class XmlAppContext {
    @Test
    void xmlAppContext(){
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }

}

이제 appConfig.xml 파일 작성!

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="hello.core.member.MemberServiceImpl" >
        <constructor-arg name="memberRepository" ref="memberRepository" /> //생성자, 참조
    </bean>
    
    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />

    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref = "discountPolicy" />
    </bean>
    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>


</beans>

AppConfig.java와 유사한것을 알 수 있다.

 

중요한 것은 스프링이 이렇게 다양한 설정 형식을 지원한다는 것이다.

과연 어떻게???

 

 

스프링 빈 설정 메타 정보 - BeanDefinition

BeanDefinition이라는 추상화가 있다.

위에서 말한것처럼, 역할과 구현을 개념적으로 나눈것!!!

BeanDefinition을 빈 설정 메타정보라 한다.

@Bean, 당 각각 하나씩 메타정보가 생성되고, 스프링컨테이너는 이 메타정로를 기반으로 스프링 빈을 생성한다.

 

  • 스프링 컨테이너 ------> BeanDefinition <-- AppConfig.class, AppConfig.xml, AppConfig.xxx따라서 BeanDefinition에는 뭐가 들어가는지 모른다. 그냥 그 역할을 수행하면 된다.
  • 스프링 컨테이너는 BeanDefinition 인터페이스에만 의존한다.

 

 

너무 깊게 이해할 필요는 없으니 필기한거 참고!

그냥 저렇게 추상화를 사용해서 다양한 설정형식을 지원한다고 알고 있으면된다.

 

package hello.core.beandefinition;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class BeanDefinitionTest {
    //getBeanDefinition을 사용하기 위해서
    //ApplicationContext 대신 사용
    //getBeanDefinition은 왠만해선 쓸일 거의 없음 => 빈 설정 메타정보임
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 설정 메타 정보 확인")
    void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {

            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                System.out.println("beanDefinition = " + beanDefinition + "beanDefinition =" +beanDefinition);
            }
        }

    }
}

 

 

 

 

 

싱글톤 컨테이너



 

스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.

대부분 스프링앱? => 웹 애플리케이션

웹 애플리케이션? => 대부분 여러 고객이 동시에 요청함

 

스프링이 없는 순수 DI 코드는 memberService를 요청할때 마다 객체를 각각 생성해서 새로 만들어주게된다.

코드로 보자.

package hello.core.singleton;

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

public class SingletonTest {

    @Test
    @DisplayName("스프링이 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        //1. 조회
        MemberService memberService1 = appConfig.memberService();
        //2. 조회
        MemberService memberService2 = appConfig.memberService();
        //두 객체 비교
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }
}

매번 새로운 객체를 만드니까 메모리 낭비도 된다.

초당 만건의 요청이 들어오면 멤버 서비스 객체가 초당 만개가 생성된다....

따라서 딱 하나의 객체만 만들고 서로 공유하도록 설계하면 된다. => 이것이 싱클톤 패턴

 

 

- 싱글톤 패턴


클래스의 인스턴스가 딱 하나만 생성되도록 보장하는 디자인 패턴!

따라서 객체 인스턴스가 2개 이상 생성되지 못하게 막아야함!!!

How?? => private 생성자로 외부에서 객체를 생성하지 못하게 막아버림, 외부에서 new로 생성 불가!

 

싱글톤 패턴 예제

package hello.core.singleton;

public class SingletonService {
    //How? => 생성자를 private로 막아버림
    
    
    //먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
    private static final SingletonService instance = new SingletonService();
    
    //객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
    public static SingletonService getInstance(){
        return instance;
    }
    
    //이제 생성자를 막아보자 private로
    private SingletonService(){
        
    }
    
    //로직 테스트 위해서
    public void logic(){
        System.out.println("싱글톤 객체 호출됨!!");
    }
    
}

 

이제 테스트!!

package hello.core.singleton;

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

public class SingletonService {
    //How? => 생성자를 private로 막아버림


    //먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
    private static final SingletonService instance = new SingletonService();

    //객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
    public static SingletonService getInstance(){
        return instance;
    }

    //이제 생성자를 막아보자 private로
    private SingletonService(){

    }

    //로직 테스트 위해서
    public void logic(){
        System.out.println("싱글톤 객체 호출됨!!");
    }



    @Test
    @DisplayName("싱글톤 패턴이 적용된 객체 사용")
    public void singletonServiceTest(){
        //new SingletonService() => private로 막혀버렷쥬?

        //두번 호출 해본다.
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        //이제 테스트 무엇을? 참조값이 같은지
        Assertions.assertThat(singletonService1).isSameAs(singletonService2);


    }


}

 

 

싱글톤 패턴을 구현하는 방법은 여러방법들이 있다. 여기선 객체를 미리 생성해두는 단순하고 안전한 방법을 썻다.

 

하지만 싱글톤은 여러 단점들이 있다.

  1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. => 접근 못하게 막고, 미리 올려두고 하는 과정이 생겨버림
  2. 의존 관계산 클라이언트가 구체 클래스에 의존 -> DIP위반 ==>SingletonService.getInstance();로 꺼내버림 구체클래스 꺼냄
  3. DIP위반햇으니 OCP 위반할 가능성 많아짐
  4. 테스트하기 어려움 => 유연하지가 않게됨
  5. 내부 속성을 변경하거나 초기화 하기 어렵움
  6. private생성자이므로 자식 클래스를 만들기 어려움

위 단점들때문에 안티패턴이라고도 불림

 

하지만.... 스프링을 쓰면 스프링이 알아서 해결해줌... how?

밑에서 보자.

 

싱글톤 컨테이너


스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤 즉, 1개만 생성해서 관리한다.

(이때 까지 햇던 스프링 빈이 싱글톤으로 관리 되었다.)

 

싱글톤 컨테이너

  • 싱글톤 패턴을 적용안해도 스프링 컨테이너가 다 알아서 해줌 => 객체 인스턴스를 싱글톤으로 관리함
  • 즉, 스프링 컨테이너는 싱글톤 컨테이너 역할을 함
  • 싱글톤 객체를 생성하고 관리하는 기능 => 싱글톤 레지스트리
  • 따라서 스프링을 사용하면 싱글톤 패턴의 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
  • 싱글톤 패턴을 위한 지저분한 코드가 없어짐
  • DIP, OCP, 테스트, private 생성자로부터 자유로워짐!! 호우!

스프링 컨테이너를 사용하는 테스트 코드를 보자

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SingletonTest {

    @Test
    @DisplayName("스프링이 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        //1. 조회
        MemberService memberService1 = appConfig.memberService();
        //2. 조회
        MemberService memberService2 = appConfig.memberService();
        //두 객체 비교
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }


//=> 스프링 컨테이너를 사용!!!
    @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);
        //두 객체 비교
        Assertions.assertThat(memberService1).isSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }
}

 

멤버 서비스 객체를 공유하게 됨!!

 

하지만 이러한 싱글톤 방식도 문제가 있음....ㅠ

 

싱글톤 방식의 주의점


객체 인스턴스를 하나만 생성해서 공유하므로, 객체는 상태를 유지하면 안됨!!!

(운영체제 시간의 동시성 문제를 생각해보자..)

 

즉, 무상태로 설계해야한다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다!!
  • 즉, 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.!!!
  • 따라서 가급적 읽기만 해야됨
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreaLocal등을 사용해야한다.

 

상태를 유지해서 발생하는 문제점 - 예시

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 hello.core.AppConfig;
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;

public class StatefulServiceTest {

    @Test
     void statefulServiceSingleton(){
        //스프링 컨테이너
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        //각각 price 입력해버림 = TheadA
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        statefulService1.order("userA", 10000);

        //각각 price 입력해버림 = ThreadB
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        statefulService1.order("userB", 20000);

        int price = statefulService1.getPrice();

        //뒤에서 값이 바뀌니까 같지 않을 것이다.
        Assertions.assertThat(price).isEqualTo(20000);



    }




    static class TestConfig{
        //빈 등록
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }

}

 

 

각각 Thead 2개라 가정(Order 하는 경우)

price필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경해버리게 된다.

 

싱글톤은 이런 문제가있다....

해결법으론 private int price를 사용하지 않고, int order로 price를 반환하게 해버린다.

즉 price를 지역 변수로 만들어버린다.

 

package hello.core.singleton;

public class StatefulService {
    //private int price;// 상태를 유지함, 즉, 값을 가지고 있음

    public int order(String name, int price){
        System.out.println("name = " + name + "price = " + price);
        return price; // 

    }
/*
    public int getPrice(){
        return price;
    }
    */
}

package hello.core.singleton;

import hello.core.AppConfig;
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;

public class StatefulServiceTest {

    @Test
     void statefulServiceSingleton(){
        //스프링 컨테이너
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        //각각 price 입력해버림 = TheadA
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        int userAPrice = statefulService1.order("userA", 10000);

        //각각 price 입력해버림 = ThreadB
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        int userBPrice =statefulService1.order("userB", 20000);

        //int price = statefulService1.getPrice();

        //뒤에서 값이 바뀌니까 같지 않을 것이다.
        Assertions.assertThat(userAPrice).isEqualTo(10000);



    }




    static class TestConfig{
        //빈 등록
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }

}

 

 

 

@Configuration과 싱글톤


 

AppConfig를 다시보자..

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
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 MemberService memberService(){
        return new MemberServiceImpl(memberRepository()); //memberRepository 호출 => 생성
    }

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

    @Bean
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());//memberRepository호출 => 생성
    }

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

}

//과연 memberRepository => new MemoryMemberRepository();가 두번이나 되었을까??
//?? 모를땐 Test코드로 assertThat Same으로 참조값을 비교해보자...

과연 new MemoryMemberRepository();를 두번하여 각각 다른 객체 인스턴스가 만들어질까?

따라서 싱글톤이 깨질까?

결과적으로 말하면 서로 같은 객체 인스턴스이고, 호출도 한번만 되어진다.

 

??????? => 마! 이게 스프링이다....

 

 

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


스프링 컨테이너 = 싱글톤 레지스터

따라서 스프링 빈이 싱글톤이 되도록 보장해주어야한다.

하지만, 스프링이 자바코드까지 어떻게 하기는 어렵다.

자바코드상으론 분명 3번 호출되어야된다!!

어떻게 이걸 해결했을까???

 

바로 스프링이 클래스의 바이트코드를 조작하는 라이브러릴르 사용했다.

모든 비밀은 @Configuration을 적용한 AppConfig에 있다.

 

    @Test
    void configurationDeep(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        
        //AppConfig또한 스프링 빈으로 등록이 되어진다.
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean);
    }

결과는......

순수한 클래스일 경우 bean = hello.core.AppConfig와 같이 출력되어야횐다.

"bean = hello.core.AppConfig$$EnhancerBySpringCGLIB$$4ab6406e@7e276594"

??

바로 CGLIB라는 바이트 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임이의 다른 클래스를 만들고

그 클래스를 스프링 빈으로 등록했다.

 

AppConfig ->등록X

AppConfig 를 상속받은 클래스(스프링이 조작해둠) -> 등록

 

즉, 그 다른 임의의 클래스가 싱글톤을 보장해준다.

아마도..

    @Bean
    public MemberRepository memberRepository() {

        if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
            return 스프링 컨테이너에서 찾아서 반환;
        } else { //스프링 컨테이너에 없으면
            기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
            return 반환
        }
    }

 

@Bean이 붙은 메서드이면 스프링 빈으로 존재하면 그 빈을 반환하고, 존재하지 않으면 그때 생성해서 반환하는 코드가 동적으로 만들어진다.

 

 

여기서,, 그러면 @Configuration을 적용하지 않고 @Bean만 적용하면?

=> 싱글톤이 보장 안되어짐...

따라서 memoryMemberRepository가 여러번 호출되고 각 각 다 다른 인스턴스가 만들어진다.

 

즉, 결론은 스프링을 사용하면 다 해결된다.

따라서 구성영역이면 @Configuration을 사용하자.

 

 

컴포넌트 스캔


 

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

이때까진 @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
//컴포넌트 스캔을 사용!!
//기존 AppConfig도 컴포넌트 스캔대상이므로 스프링 컨테이너에 등록되지 않게 해주어야한다.
//제외할 필터로 어노테이션 타입의 Configuration이 있는 클래스를 컴포넌트 스캔하지 않게 한다.
//왜냐면 AppConfig는 @Configuration 가 붙어있으니까 이거 빼줄라고
//그리고 AppConfig는 수동으로 빈을 등록하는거니까 자동 등록 테스트를 위해 빈으로 등록하지 않는다.
@ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
    //아무 내용이 없어도 알아서 다 긁어서 스프링빈으로 찾아낸다.
}

 

 

자 이제 다시보면 (하다보니 주석에 다적어버려서 그냥 그대로 올려봣다.)

package hello.core;

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

//설정정보니까
@Configuration
//컴포넌트 스캔을 사용!!
//기존 AppConfig도 컴포넌트 스캔대상이므로 스프링 컨테이너에 등록되지 않게 해주어야한다.
//제외할 필터로 어노테이션 타입의 Configuration이 있는 클래스를 컴포넌트 스캔하지 않게 한다.
//왜냐면 AppConfig는 @Configuration 가 붙어있으니까 이거 빼줄라고
//그리고 AppConfig는 수동으로 빈을 등록하는거니까 자동 등록 테스트를 위해 빈으로 등록하지 않는다.
@ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
    //아무 내용이 없어도 알아서 다 긁어서 스프링빈으로 찾아낸다.
    //어떻게? => 위를 보면 일단 컴포넌트 스캔을 사용한다고 적었다.
    //그러면 컴포넌트들을 찾아야지! 어떻게? => @Component를 붙여서 알려주면됨!
    //빈으로 등록할 객체들을 컴포넌트로 등록!!
    //빈에는 구현체들이 등록이 되어있어야겟지? 그러면 구현체들에 @Component

    //그러면 스프링 컨테이너에 빈으로 등록은 되었다만... 어떻게 의존관계를 연결시키지... 하아..
    //바로 @Autowired 로 자동연결해주면 끄으읕 => 구현체에서 필요한 객체 인스턴스를 가져올 곳에 쓰면댐
}

/**
 * excludeFilters에 대해 다시 설명하면
 * @Configuration 어노테이션을 보면 @Component를 사용한다.!
 * 따라서 @Configuration이 있는 것도 다 긁어온다.
 * 그러므로 앞의 예제들에서 만든 AppConfig, TestConfig 등등 다 긁어와버리니까 제외시켜버렷다.
 *
 * (내 생각이긴한데 컴포넌트 스캔을 사용하는 설정파일은 AutoAppConfig뿐이니까
 * 자기 자신이 설정파일이지만 자기 자신을 제외한 나머지 설정들을 컴포넌트 스캔시 등록 안해버리는거같다.
 * 나중에 검색해봐야지...)
 */

 

즉, 컴포너트 스캔을 한다고 선언을 했다. 그러면 뭘 해야지??

컴포넌트 스캔이 찾을 수 있게 @Component라고 알려줘야댐

어디에?? => 스프링 빈으로 등록할 객체를!

어떤거지? => 실제 사용될 것들이니 실제 사용할 구현체이지

 

그러면 필요한 객체들을 @Component 을 붙여서 스프링 빈으로 등록을 했다.

킹치만.. 설정정보를 다시보면 아무내용도 없다. 그러면 의존관계 주입은 누가하냐???

@Autowired로 의존관계를 주입힌다.

어떻게? 해당 구현체가 필요한 곳에서!

다시 역할, 구현을 구분한다는거에 집중하자 => 배우(자기 역할)는 연기만하면 되지 상대배우(구현)가 누가될지는 몰라도 자기 역할만 하면된다.!

즉, 어떤 리포지토리가 쓰일진 모르지만, 그냥 리포지토리를 불러오면 된다! => 따라서 이러한 곳에 @Autowired로 의존관계 주입 !!

 

자 다시 정리하면, 구현체 들이 스프링빈으로 등록되어야하니

구현체(xxxIpml)들에 @Component!

그리고 구현체들이 다른 구현체가 필요할때 어떤 구현체가 올진 모르겟고 해당 역할만 필요로함! => @Autowired

 

package hello.core.member;

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

@Component //컴포넌트 스캔하여 빈으로 등록하기 위해서 알려줌
//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{

    //배역 배우로 생각할때
    //MemberRepository memberRepository = new MemoryMemberRepository();

    //생성자로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;

    //리포지토리가 필요하네? 근데 어떤 저장소가 올진 내가 신경쓸게 아니지!
    //따라서 의존관계 주입!
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

package hello.core.member;

import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

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

@Component//빈으로 등록하기 위해 컴포넌트라고 알려줌
//저장소 필요
public class MemoryMemberRepository implements MemberRepository{

    //일단 임시 저장소 = 메모리 사용
    private static Map<Long, Member> store = new HashMap<>();

    @Override
    public void save(Member member) {
        store.put(member.getId(),member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);//store는 Map이므로 키값으로 member찾음
    }
}
package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;

//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

package hello.core.order;

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


@Component //빈 등록
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //생성자 주입기법으로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    //역할, 구현 구분! 배우는 누가 캐스팅될지 모르고 걍 연기만 하면댐
    //여기선 어떤 리포지토리, 어떤 할인정책이 올지 모름
    //그냥 리포지토리를 쓰고 할인정책을 쓰면댐!!!
    //따라서 의존관계가 주입되어야되므로 @Autowired
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

자 이러면 이제 뭘해봐야지? => 테스트로 확인

테스트를 해보아서 진짜 다 스프링 빈으로 컨테이너에 등록이 되고 의존관계주입 즉 연결되었는지 확인 해보자!

 

package hello.core.scan;

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

public class AutoAppConfigTest {

    @Test
    @DisplayName("컴포넌트 스캔으로 빈이 등록되었는지 확인, 의존관계도 확인")
    void basicScan(){
        //스프링 컨테이너에 빈들이 등록되었는지 확인 해보면된다.
        //설정정보로 스프링컨테이너 불러오고
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        
        //요렇게하면 안된다. 왜냐면 안알랴줌... 뒤에서 설명해줄게...
//      MemberService memberService = ac.getBean("memberService", MemberService.class);

        //타입으로 찾음
        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

 

위 주석에서 안알랴줌 이유를 설명하자면..

먼저 컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 봐야된다.

 

@Component 를 붙이면 이 어노테이션이 붙은 클래스들을 스프링 컨테이너에 스프링 빈으로 등록시킨다.

(스프링 컨테이너는 스프링 빈을 생성해서 가지고 있음)

등록 될땐, 자바빈 규약에 의해 xxxBean 와 같은 형식으로 빈이름이 지정된다.

(스프링 빈의 기본이름은 크래스 명을 사용하되, 맨 앞글자 소문자를 사용함)

예를 들면 @Component가 붙은 MemberServiceImpl을 보면 memberServiceImpl과 같이 빈이름이 설정된다.

 

자동 의존관계 주입은 @Autowired를 설정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.

(즉 역할만 알고 있으면댐 구현체가 뭔진 몰라도 알아서 가져다줌)

이때 해당 스프링 빈을 찾는 방식은 타입이 같은 빈을 찾아서 주입한다.

 

앞서 getBean(MemberRepositroy.class)를 생각해보자 이러면 구현 객체인 MemoryMemberRepository가 나왔다.

따라서 MemberRepositroy가 필요하면 스프링 컨테이너에서 MemberRepositroy타입으로 등록된 빈을 찾아서 주입해준다.

 

킹치만 같은 타입이 여러개라면?!?!?!?! => 충돌이 일어나겟지... 뒤에서 설명한다.

 

 

 

 

 

탐색위치와 기본 스캔 대상

컴포넌트 스캔시 스캔할 탐색 시작 위치는 어디일까?

만약 모든 파일을 다 찾는다고 생각해보자.

그 모든 불러온 라이브러리들까지 다 뒤져본다고 생각하면 시간이 엄청 오래 걸릴 것이다.

 

따라서 꼭 필요한 위치부터 탐색하도록

시작위치를 지정할 수 있다.

 

@ComponentScan( basePackage = {"hello.core","hello.service"} )

 

이런식으로 해당 패키지를 포함한 하위패키지를 모두 스캔하게 설정해 줄 수 있다.

 

혹은 basePackage외에

basePackageClasses로 "hello.core.member" 와 같이 클래스를 지정해줄 수 있다.

그러면 member클래스의 패키지인 hello.core가 탐색 시작 위치가 된다.

 

기본값은 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작위치가 된다.

 

근데 위에껀 그렇다고 치고,

왠만하면 기본값을 따르자 => HOW??

설정 정보 클래스를 프로젝트 최상단에 두면 자동으로 그 밑을 다 스캔하게 된다.

(스프링 부트도 이러한 방법을 기본으로 제공한다.)

 

예를 들면 com.hello 위치에 설정 정보 클래스를 넣어두는거지

 

=>패키지 시작위치를 설정할 번거로움이 없어지쥬?

 

*참고로 스프링 부트를 사용하면 스프링 부트 대표 시작정보인 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다.(이 설정안에 @ComponentScan이 들어 있기 때문이지)

**참고의 참고로 스프링 프로젝트 생성시 CoreApplication이 자동으로 생성된다. => 얘가 바로 스프링 부트를 실행한다. 스프링부트를 사용하면 @ComponentScan을 사용할 필요가 없어진다. 왜냐? => 바로 위 참고에서 @SpringBootApplication에 @ComponentScan이 들어있다고 말했음...

 

 

 

 

컴포넌트 스캔 기본 대상

컴포넌트 스캔은 @Component 외에도

  • @Controller : 스프링 MVC 컨트롤러에서 사용 MVC컨트롤러로 인식
  • @Service : 스프링 비지니스 로직에서 사용 (개발자가 보기 비지니스 흐름 보기 편하게 적어둠)
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • 예를 들어 DB가 설정 안된경우 어떤 DB가 사용될지 모르니... 쿼리도 다 달라질거다... 이걸 중간에서 번역해준다고 생각
  • @Configuration : 스프링 설정정보 (위에서 썻던거)

 

해당 어노테이션 클래스를 보면 전부 @Component가 들어가있다.

@Component
public @interface Controller {
}
@Component
public @interface Service {
}
@Component
public @interface Configuration {
}

 

사실 어노테이션에는 상속 기능이 없다...

이렇게 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은.. 바로 자바!..가 아니라

스프링이 지원하는 기능이다.

 

컴포넌트 스캔 이외에도 밑과 같은 부가기능을 수행한다.

  • @Controller : 스프링 MVC 컨트롤러로 인식
  • @Service : 특별한 처리를 하지 않는다. 개발자들이 핵심 비지니스 로직이 여기 있겠구나라고 비지니스 계층 인식에 도움준다.
  • @Repository : 스프링 데이터 접근 계층으로 인식하고,데이터 계층의 예외를 스프링 예외로 변횐해준다.
  • 예를 들어 DB가 설정 안된경우 어떤 DB가 사용될지 모르니... 쿼리도 다 달라질거다... 이걸 중간에서 번역해준다고 생각 => 추상화해서 반환
  • @Configuration : 스프링 설정정보로 인식 , 스프링 빈이 싱그톤을 유지하도록 추가처리

 

** 참고로 userDefaultFilters옵션은 기본으로 켜져있는데 이 옵션을 끄면 기본 스캔 대상들이 제외된다. => 이런것도 잇구나~~

 

 

 

필터

두가지가 있다.

  • includeFilters : 컴포넌트 대상을 추가로 지정
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정

 

예제로 확인 해보면

두 어노테이션을 만들어준다.

package hello.core.filter;


import java.lang.annotation.*;

@Target(ElementType.TYPE)//어떤거에 적용? TYPE에 적용가능한 어노테이션이다~~ 즉, class 레벨에 적용하는 어노테이션
@Retention(RetentionPolicy.RUNTIME)//런타임 - 실행시에 이 어노테이션을 참조한다.
@Documented //JavaDoc 생성시 Annotation에 대한 정보도 함께 생성한다.
//어노테이션 선언
public @interface MyExcludeComponent {
}

package hello.core.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)//어떤거에 적용? TYPE에 적용가능한 어노테이션이다~~ 즉, class 레벨에 적용하는 어노테이션
@Retention(RetentionPolicy.RUNTIME)//런타임 - 실행시에 이 어노테이션을 참조한다.
@Documented //JavaDoc 생성시 Annotation에 대한 정보도 함께 생성한다.
//어노테이션 선언
public @interface MyIncludeComponent {
}

 

그리고 클래스 빈A, B를 만들어서 위에서 만든 어노테이션을 적용한다.

package hello.core.filter;

@MyIncludeComponent
public class BeanA {
}

 

package hello.core.filter;

@MyExcludeComponent
public class BeanB {
}

 

 

이제 테스트 해본다.

 

package hello.core.filter;

import hello.core.AppConfig;
import hello.core.AutoAppConfig;
import org.assertj.core.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 org.springframework.stereotype.Component;

public class ComponentFilterAppConfigTest {
    
    @Test
    void filterScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        Assertions.assertThat(beanA).isNotNull();

        org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class, ()->ac.getBean("beanB", BeanB.class));
        
    }
    
    
    @Configuration //설정 정보라고 말해줌
    //스프링 빈으로 등록할거 등록하지 않을거 설정
    @ComponentScan( 
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
    excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class))
    static class ComponentFilterAppConfig{
    }
}

 

 

FilterType은 5가지 옵션이 있다.

  • 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

 

예로 TYPE 즉 클래스로 제외할 수도 있따.

BeanA를 제외해보겟다.

    @Configuration //설정 정보라고 말해줌
    //스프링 빈으로 등록할거 등록하지 않을거 설정
    @ComponentScan(
            includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)}, 
            excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), 
                    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)})
    static class ComponentFilterAppConfig{
    }

 

ANNOTATION외에는 잘 사용하지 않는다...

그리고 includeFilters도 잘 사용하지 않는다.

왜냐하면 @Component면 충분하기 때문이다.

** 최근 스프링 부트는 컴포넌트 스캐을 기본으로 제공한다. 따라서 개인적으로 옵션 변경보단 기본설정에 맞추어 사용하는 것이 편하다...

 

중복등록과 충돌

만약 컴포넌트 스캔에서 같은 빈 이름을 등록하면??

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

 

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해서 자동으로 스프링 빈이 등록된다.

예를 들어 @Component("BeanA"), @Component("BeanA")와 같이 이름이 같은 빈을 등록하려고 하면

스프링은 오류를 발생시킨다. => ConfilctingBeanDefinitionException 발생

 

수동 빈 등록 vs 자동 빈 등록

    @Component
    public class MemoryMemberRepository implements MemberRepository {}

 

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

위와 같이 @Component로 자동으로 등록하고 @Bean으로 같은 이름을 수동으로 등록하는 경우

수동 빈이 우선권을 가진다 => 수동 빈이 자동 빈을 오버라이딩을 해버린다.

 

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

그리고 위와 같은 로그가 남는다.

 

개발자가 의도적으로 위와같이 설정한다면 자동등록보단 수동등록한게 당연히 우선권을 가지는것이 좋다.

 

하지만... 이렇게하면 정말 잡기 어려운 버그가 만들어진다.

 

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

 

 

 

 

 

 

 

 

 

 

 

 

의존관계 자동주입


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

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

 

생성자 주입

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

(위에서 했던 방식들)

생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다. (그 다음부턴 호출 안되게 막을 수 있다.)

불변, 필수 의존관계에 사용한다. => OCP 지킴

    @Component//OrderServiceImpl가 스프링 빈에 등록됨 => 객체 인스턴스가 생성되니까 생성자도 호출!
    public class OrderServiceImpl implements OrderService {
        //private final로 선언됨 => 무조건 값을 세팅해줘!!! 라고 알리는 격임
        //따라서 무조건 값이 있어야댐!!! 아니면 null임
        //외부에서 값 변경이 불가! => setter, getter가 없음
        private final MemberRepository memberRepository;
        private final DiscountPolicy discountPolicy;
        
        
        //생성자가 여기 있음! 
        //Autowired이므로 스프링 컨테이너에서 스프링 빈을 꺼내서(리포지토리, 할인정책)을 주입시켜줌
        // 그리고 왠만하면 생성자에 있는 파라미터들을 다 넣어주는게 좋음! 아니 그래야댐
        @Autowired
        public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
                discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }

 

생성자 주입은 빈을 등록하면서 같이 일어난다.

-> 빈을 등록해야하니까!

빈을 등록?? => 객체 인스턴스를 생성해서 스프링 컨테이너가 들고 있음!!

객체 인스턴스 생성?? => 객체를 생성해야하니까 생성자 호출!!

 

 

 

생성자가 하나인 경우는 Autowired 생략 가능하다. 물론 스프링 빈에만 해당댐

(@Component 햇으니까 스프링 빈임)

    @Component
    public class OrderServiceImpl implements OrderService {
        private final MemberRepository memberRepository;
        private final DiscountPolicy discountPolicy;
       // @Autowired
        public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
                discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }

 

 

 

 

수정자 주입(setter 주입)

setter라 불리는 수정자 메서드로 의존관계를 주입함!

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

자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

    @Component
    public class OrderServiceImpl implements OrderService {
        //final이 아님 => 값 변경 되어도 됨
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
        
        //생성자로 주입 받는것이 아니라 setter 메서드로 주입받음
        @Autowired
        public void setMemberRepository(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
        @Autowired
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }
    }

** @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

=> 예를 들어 MemberRepository 쓸지 안쓸지 잘 모름....

    @Component
    public class OrderServiceImpl implements OrderService {
        //final이 아님 => 값 변경 되어도 됨
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
        
        //쓸지 안쓸지 몰라서 false로 해놓음, 따라서 주입할 대상이 없어도 오류가 안남
        @Autowired(required = false)
        public void setMemberRepository(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
        @Autowired
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }
    }

 

 

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

 

필드 주입

말그대로 필드에 주입!

하지만 안티패턴이다...

코드가 간결하지만, 외부에서 변경이 불간으해서 테스트하기 힘들다는 단점이있다.

DI 프레임워크가 없으면 아무것도 할 수 없다. => 순수 자바코드로는 못한다.

즉, 사용하지 말자!!!!!!!!

주로 실제코드와 관계없는 테스트코드나, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 쓴다.

@Component
public class OrderServiceImpl implements OrderService {
    
 //=> 값을 집어 넣을 수 없다... 어케넣어 setter도 없고 일반 메서드도 없고, 생성자로도 못넣고..
 @Autowired
 private MemberRepository memberRepository;
 @Autowired
 private DiscountPolicy discountPolicy;
    
 //값을 못 집어넣어버림 
 public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  
    
}
@Test
void fieldInjectionTest(){
    //이렇게 new 임의로 생성한 객체는 Autowired 되지 않음
    //Autowired는 스프링 빈에 등록된 객체를 연결하는것이므로
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder( ~~~ )
    
    //할인 정책과 멤버리포지토리를 못 넘겨줌....
    //set 메소드를 만들어서 넣어 주거나 해야댐
    //orderService.setRepository(new MemoryMemberRepositroy) 처럼
}

==> 필드 인젝션은 결국 값을 넣으려면 setter가 필요하다.

즉, @Autowired로 땡겨온다곤 햇는데 넣어 주질 않으니 어케 그 값에 넣으란 말이냐....... 결국 setter와 같은게 필요한거지..

그럼 왜 필드 인젝션 씀? 그러므로 쓰지말자..

 

** 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다 => @Autowired는 스프링 빈에 등록된 걸 가져오는건데 스프링 컨테이너가 없는데 어케 가져와?!?! , 스프링 컨테이너 = ApplicationContext

예시)

@Test
    @DisplayName("컴포넌트 스캔으로 빈이 등록되었는지 확인, 의존관계도 확인")
    void basicScan(){
        //스프링 컨테이너에 빈들이 등록되었는지 확인 해보면된다.
        //설정정보로 스프링컨테이너 불러오고
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
//        MemberService memberService = ac.getBean("memberService", MemberService.class);

        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }

 

 

** @Bean을 사용하는 경우에, 파라미터에 의존관계는 자동 주입된다.

수동 등록시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결할 수 있다.

    @Bean
    OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy
            discountPolicy) {
        new OrderServiceImpl(memberRepository, discountPolicy)
    }

 

 

 

일반 메서드 주입

일반 메서드를 통해서 주입받을 수 있다. (사실상 수정자 주입이랑 같다..)

한번에 여러 필드를 주입 받을 수 있고 일반적으로 잘 사용하지 않는다.

 

    @Component
    public class OrderServiceImpl implements OrderService {
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
        
        //일반 메서드 init를 통해서 주입 받음
        @Autowired
        public void init(MemberRepository memberRepository, DiscountPolicy
                discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }

** 의존 관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.

스프링 빈이 아닌 Member 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

(스프링 컨테이너에 빈으로 등록되어 있어야지 가져오지, 등록이 안되었는데 어케 가져와?!)

 

 

 

 

 

옵션처리

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

예를 들어 해당 스프링 빈이 있으면 실행하고, 없으면 해당 로직이 동작하지 않게 하는 때와 같이..

@Autowired만 사용하면 required 옵션의 기본값이 ture로 되어있어, 무조건 자동주입받을 빈이 있어야된다고 설정이되버린다. 따라서 자동 주입대상이 없으면 오류가 발생한다.

 

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안된다.
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입련된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다. 즉, null로 반환이 되는것을 막는다.

 

 

예제로 보자.

스프링 빈으로 등록되지 않은 Member 객체를 가져오도록 했다.

package hello.core.autowired;

import hello.core.member.Member;
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.lang.Nullable;

import java.util.Optional;

public class AutowiredTest {

    @Test
    void AutoWiredOption() {
        //설정정보도 스프링 빈으로 등록된다.
        //스프링 컨테이너불러옴
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }
    static class TestBean{

        //호출되지 않는다. 해당 객체가 스프링 빈으로 등록되어있지 않으면 가져오지 않으므로 호출 자체가 안됨.
        @Autowired(required = false)
        //여기서 Member는 앞에서 만든 hello.core의 멤버로 스프링 빈으로 등록되어있지 않다.
        public void setNoBean1(Member member){
            System.out.println("setNoBean1 = " + member);
        }

        //호출은 되나 null이 반환된다.
        @Autowired //@Nullable 자동 주입할 대상이 없으므로 null
        public  void  setNoBean2(@Nullable Member member){
            System.out.println("setNoBean2 = " + member);
        }


        //Optional로 감싸서 반환 => Optional.empty 반환
        @Autowired
        public  void  setNoBean2(Optional<Member> member){
            System.out.println("setNoBean2 = " + member);
        }

    }
}

member 객체 인스턴스를 가져와야되는데 스프링빈에 등록되어있지않으므로 null이 반환된다.

 

출력 결과

setNoBean2 = null
setNoBean3 = Optional.empty

 

setNoBean1은 required가 false이다. 따라서 스프링 빈으로 등록된 멤버가 없으므로 해당 메서드가 호출되어지지 않는다.

 

@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다.

예를 들면 생성자 자동 주입에서 특정 필드에만 사용해도된다.

public OrderServiceImpl(MemberRepository memberRepository, @Nullable DiscountPolicy discountPolicy) {...}

과 같이 마지막 파라미터는 없어도 생성자를 생성할 수 있게 할 수 있다.

(없으면 넣지 않아도 생성자가 생성댐)

 

 

 

왠만하면 생성자 주입을 쓰자

이유는 다음과 같다.

 

불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다!(공연을 하기 전에 배역이 다 정해져야댐! )
  • (대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다!)
  • 수정자 주입을 사용하면 setXxx 메서드를 public하게 열어둬야한다.
  • 따라서 누가 실수로 바꿀수도 있다. 변경하면 안되는 메서드는 public으로 열어두지말자!
  • 생성자 주입은 객체를 생성할 때 딱1번만 호출되므로, 이후에 호출되지 않는다. => 불변!

 

누락

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

예를 들어 OrderServiceImpl 을 순수한 자바 코드로 테스트 해보고 싶을때

package hello.core.order;

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


@Component //빈 등록
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //생성자 주입기법으로 맞는 구현체 가져옴
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    //생성자 대신에 set으로 주입 => 수정자 의존관계
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

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


    //역할, 구현 구분! 배우는 누가 캐스팅될지 모르고 걍 연기만 하면댐
    //여기선 어떤 리포지토리, 어떤 할인정책이 올지 모름
    //그냥 리포지토리를 쓰고 할인정책을 쓰면댐!!!
    //따라서 의존관계가 주입되어야되므로 @Autowired

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

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

테스트

package hello.core.order;

import org.junit.jupiter.api.Test;
import org.mockito.internal.matchers.Or;

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

//순수한 자바로 테스트
//OrderServiceImpl을 잘 만들엇는지 테스트
class OrderServiceImplTest {

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

}

 

단순 테스트를 위해서 잠깐 Appconfig설정을 바꾸자

   @Bean
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
       // return new OrderServiceImpl(memberRepository(), discountPolicy());
        return null;
    }

 

 

실행결과

java.lang.NullPointerException
        at hello.core.order.OrderServiceImpl.createOrder(OrderServiceImpl.java:55)

NullPointerException 에러가 난다.

 

왜냐하면 createOrder메서드를 다시 보자

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

memberRepository와 discountPolicy가 필요하다.

가짜로 임의로 만들어서 넣어주더라도 뭔가 넣어 주어야된다.

=> 테스트는 누락을 해버렷음

 

다시 정리하면, OrderServiceImpl을 잘 만들었는지 확인 테스트하고 싶음.

하지만 OrderServiceImpl에서는 리포지토리나 할인정책이 필요함.

따라서 더미 리포지토리나 할인정책을 만들어서 즉, 아무 거나 넣으면댐 그냥(테스트용이니)

 

위에서 OrderServiceImpl을 생성자 주입에서 set 즉, 수정자 주입으로 변경하였음.

테스트 코드를 다시 보면 "new OrderServiceImpl();"

순수 자바코드이므로 컨테이너에서 객체를 받아오는게 아니라 직접 객체를 생성해서 사용함.

그리고 set으로 리포지토리와 할인정책을 받아서 설정해주어야댐.

하지만 set으로 받질 않았으니 아무런 객체 인스턴스가 안들어있음.

여기서 createOrder을 해버리니 리포지토리와 할인 정책이 null 인 상황에서 가져오려하므로 NPE가 발생함.

 

why?? 위 테스트는 순수 자바코드 테스트임!

그런데 위에선 @Autowired로 수정자 주입(set)으로 의존관계를 주입해놨었음.

근데 DI 컨테이너를 안띄었으므로 가져올 리포지토리, 할인 인스턴스가 없음.

 

즉, 의존관계가 주입 안되어있음!

다시 테스트 코드를 보면

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

생성자 메서드가 아니라 수정자 메서드로 의존관계를 주입 하였으므로 의존관계가 잘 안보이게 된다....

그리고 분명 의존관계가 빠졌지만 컴파일은 잘 되고 실행단계에서 오류가 난다.

 

하지만 생성자 주입을 사용하면..

//OrderServiceImpl을 잘 만들엇는지 테스트
class OrderServiceImplTest {

    @Test
    void createOrder(){
        //생성자 단계에서 주입 데이터가 누락이 되어서 컴파일 자체가 안되는 컴파일 오류가 발생함.
        // OrderServiceImpl orderService = new OrderServiceImpl();
        OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
        orderService.createOrder(1L, "itemA", 10000);
    }

}

주입 데이터가 누락되었다고 오류가 뜬다.

세상에서 가장 좋은 오류는 컴파일 오류이다.. 바로 고치면댐!!

 

 

final 키워드

생성자 주입 방법의 또 다른 장점은 필드에 final 키워드를 사용할 수 있다는 것이다.

(setter와 같은 경우는 객체가 생성된 다음에 주입이 되므로 final을 쓰지 못한다.)

따라서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.

@Component
public class OrderServiceImpl implements OrderService {
    
    //final로 선언되어있다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
    }
    //...
}

해당 코드의 생성자를 보면 discountPolicy 주입이 누락되어있다.

따라서 자바는 컴파일 시점에 "java: variable discountPolicy might not have been initialized"오류를 발생시킨다.

컴파일 오류는 세상에서 가장 빠르고 좋은 오류다!!!!!!

참고)

final 키워드는 간단히 말하면 멤버 변수를 '상수'(const)로 만들겠다는 뜻입니다. 변수의 경우는 그렇고 메서드에 붙는 final은 재정의가 불가능, 클래스에 붙는 final은 상속 불가능 등으로 의미가 달라집니다.

.

상수는 한번 값을 할당하면, 다시 그 값을 변경할 수 없음을 말합니다. 그래서 자바에서는 상수는 선언과 동시에 값을 할당하도록 제한하고 있습니다. 

private final int score = 0;

.

그러나 선언과 동시에 값을 할당하지 않아도 되는 예외가 있는데, 생성자에서 상수를 초기화 할 때 입니다. 생성자는 객체를 생성하기 위해 '반드시' 거쳐야 하는 과정이고, 여기서 상수값을 초기화를 하고 있다면 이는 '확실히 상수가 초기화 됨을 보장'합니다. 그래서 이런 경우는 컴파일을 허가 합니다.

private final int score;

public SomeClass(int score) {

    this.score = score;

}

.

setter로 멤버 변수를 설정한다는 것은 객체가 생성되는 과정이 모두 끝난 이후에 setter 메서드를 호출하여 멤버 변수의 값을 할당하겠다는 뜻입니다. 만약 이 객체의 멤버 변수중 상수가 있을경우, 상수가 생성은 되었으나 언제 초기화 될지를 컴파일러가 알 수 없습니다. 그래서 이런 경우 final 키워드를 쓰지 못하도록 컴파일러가 막습니다.

.

자바는 개발자가 실수 할 수 있는 여지를 최대한 컴파일러 레벨에서 방지합니다. 그래서 생성한 객체를 해제하는 역할도 자바언어가 알아서 처리합니다. 과거 C나 C++ 언어에서는 이런 과정을 개발자가 전부 통제해야 하기에, 실수를 하게 되면 원인을 찾기가 더 힘든편입니다.

 

정리하면,

  • 생성자 주입 방법을 사용하면 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살릴 수 있는 방법이다.
  • 기본으로 생성자 주입 방법을 사용하고, 필수 값이 아닌 경우엔 수정자 주입 방식으로 옵션을 부여하자. 따라서 둘다 섞어서 사용하면된다.
  • 항상 생성자 주입을 선택!!! 가끔 옵션이 필요한 경우에 수정자 주입!, 필드주입은 그냥 없다고 생각!

 

 

롬복과 최신 트렌드

막상 개발을 해보면, 대부분이 다 불변이다. 따라서 생성자에 final 키워드를 사용하게 된다.

근데 위에서 봣듯이,

  • 생성자를 만든다
  • 주입 받은 값을 대입하는 코드를 만든다

귀찮쥬??

필드 주입처럼 편한 건 없을려나... (필드 주입하면 생성자 없이 바로 들어오니까)

 

아래 코드를 최적화 해보자.

@Component
public class OrderServiceImpl implements OrderService {
    
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

롬복 라이브러리를 적용하면 된다.

롬복 라이브러리가 제공하는 @RequireArgsConstructor 기능을 사용하면 final 이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

즉, 생성자를 자동으로 만들어준다.(final이 붙은 필드들을)

 

start.io.spring으로 프로젝트 생성시 add dependecy로 롬복을 선택하면 자동 적용된다.

직접 사용하는 방법은...

build.gradle에 해당 코드 추가

//lombok 설정 추가 시작
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
//lombok 설정 추가 끝


// 그리고 dependency안에 추가
//lombok 라이브러리 추가 시작
 compileOnly 'org.projectlombok:lombok'
 annotationProcessor 'org.projectlombok:lombok'
 testCompileOnly 'org.projectlombok:lombok'
 testAnnotationProcessor 'org.projectlombok:lombok'
 //lombok 라이브러리 추가 끝

 

그리고 @RequiredArgsConstructor어노테이션을 붙이면 끝!

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

쉽쥬?

 

롬복이 자바의 어노테이션 프로세서라는 기능을 이용하여 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class를 열어보면 생성자가 추가된걸 알 수 있다.

 

package hello.core;

import lombok.Getter;
import lombok.Setter;

//롬곡 라이브러리가 자동으로 setter, getter를 만들어준다.
@Setter
@Getter
public class HelloLombok {
    private String name;
    private int age;

    public static void main(String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("asdfasdf");


        String name = helloLombok.getName();
        System.out.println("name = " + name);
    }
}

 

조회 빈이 2개 이상 - 문제

@Autowired는 TYPE으로 조회한다.

@Autowired
private DiscountPolicy discountPolicy

하지만

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

해당 타입이 2개 스프링 빈으로 등록 되어 있다.....

그럼 과연 누굴 가져와야하느냐...

NoUniqueBeanDefinitionException 예외가 발생한다.

NoUniqueBeanDefinitionException: No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy

보면 하나의 빈을 기대했는데 두개의 빈이 발견되었다고 알려준다.

 

이때 하위타입으로 지정할 수도 있다.

@Autowired
private FixDiscountPolicy discountPolicy

하지만 이러면 DIP를 위배하고 유연성이 떨어진다.

또한 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 두개 있으면 해결이 안됨!

설정 구성에서 스프링 빈을 수동 등록해서 해결해도되지만, 의존관계 자동 주입에서 해결하는 여러 방법이있다.

 

 

@Autowired 필드 명, @Qualifier, @Primary

조회 대상 빈이 2개 이상인 경우엔

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

 

@Autowired 필드명

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

@Autowired
private DiscountPolicy discountPolicy

위 코드에서 필드 이름을 바꾼다.

@Autowired
private DiscountPolicy rateDiscountPolicy

 

필드명 rateDiscountPolicy 이 정상 주입된다.

따라서 먼저 타입 매칭을 한 다음, 여러빈이 있을때 그중 같은 필드명을 가진 빈을 가져온다.

(여기선 fixDiscountPolicy, rateDiscountPolicy 두개가 있었으므로 DiscountPolicy 타입을 찾고 그중 rate를 가져온다.)

즉, 해당 타입 빈 찾음 => 여러개가 나오니까 필드명이름이랑 매칭되는 거 가져옴

 

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

DiscountPolicy중 mainDiscountPolicy를 가져온다고 알 수 있다.

(구현체에 의존하게 되므로 DIP 위반이지만 트레이드 오프가 있다 생각해야댐)

혹은 생정자 말고 수정자 자동 주입으로 사용하면

 

@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
 return discountPolicy;
}

 

즉, 타입 앞에 @Qualifier("이름")을 붙여서 사용하면 된다.

만약 @Qualifier("mainDiscountPolicy")로 등록된 스프링 빈이 없으면?!?!

그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.

하지만 @Qualifier는 스프링 빈이름을 찾는게 아니라 @Qualfier를 찾도록 하는 것이 좋다.

 

정리하면

@Qualifier끼리 매칭 => 없으면 빈이름 매칭 => 없으면 NoSuchBeanDefinitionException 예외 발생

 

 

@Primary 사용

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

@Autowired 시에 여러 빈이 매칭되면 @Primary를 가져온다.

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

이러면 DiscountPolicy를 가져올때 RateDiscountPolicy가 우선권을 가지므로 Rate를 가져온다.

(따라서 @Qualifier와 같이 클라이언트 코드를 수정할 필요가 없다.)

그리고 또한 Qualifier의 단점은 모든 코드에 @Qualifier를 붙여야한다!!

 

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

등록할때와 그 스프링 빈을 사용할때 다 붙여야댐....

 

@Primary, @Qualfier 활용

예를 들어 코드에서

자주 사용하는 메인 DB의 커넥션을 획득하는 스프링 빈,

특별한 기능으로 가끔 사용하는 서브 DB의 커넥션을 획득하는 스프링 빈

이 있다고 가정.

 

메인DB 커넥션을 획득하는 스프링빈 => @Primary

서브 데이터베이스 커넥션을 획득하는 스프링 빈 => @Qualifier 사용하여 명시적 지정

과 같이 하면 깔끔해진다.

 

이때, 메인 DB의 스프링 빈을 등록할때 @Qualifier 를 지정해주는 것은 상관없다.

 

  • @Primary, @Qualfier 둘 중 우선순위따라서 더 명시적인 @Qualifier가 우선권이 높다.
  • 스프링은 자동보다 수동이, 넓은 범위보단 좁은 범위가 우선순위가 높다.

 

 

참고)

discountPolicy에 두 개의 빈이 찾아져버리므로, 특정 빈을 찾을 수 있도록 인자의 파라미터 이름을 수정해야했습니다. (@Autowired 필드명 방식)

이것이 개방-폐쇠 원칙을 못지킨 것이 아닌가 하는 의문이 들었습니다.

-> 네 맞습니다. 클라이언트 코드를 고쳐야 하기 때문에 OCP를 지키기 못했습니다.



@Quilifier 혹은 @Primary 어노테이션을 붙이기 위해 구현체의 클래스를 찾아가서 수정해줘야하는 것 같습니다.

-> 기존 구현 클래스의 애노테이션도 변경하지 않으면 더 좋겠지만, 이 부분까지는 컴포넌트 스캔의 한계입니다. @Bean을 사용하면 확실하게 되지만 약간은 불편하지요. 따라서 둘의 트레이드 오프로 이해하시면 됩니다.

 

 

 

어노테이션 직접 만들기

@Qualifier 사용시 만약

@Qualifier("mainDiscountPolicy")가 아니라

@Qualifier("mmainDiscountPolicy") 처럼 오타가 났다면?!?!?!

문자를 적으면 컴파일시 타입 체크가 되지 않는다. => 오류 찾기 힘들다...

따라서 직접 어노테이션을 만들어서 컴파일시 체크되게 한다.

package hello.core.annotation;


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

import java.lang.annotation.*;

//ctrl+n 으로 Qualifier 어노테이션꺼 다 긁어와주면댐
//즉 @Qualifier 설정 다 가져옴
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy") //적어줌 여기서
public @interface MainDiscountPolicy {
}

 

그러면 기존에 사용했던 방식을 보자

package hello.core.discount;

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

//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
@Qualifier("mainDiscountPolicy") //이렇게 직접 문자로  적어줬다. <==== 따라서 오타가 나면 찾기 힘들다..
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

 

하지만 만든 어노테이션을 적용하면

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.stereotype.Component;

//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
//@Qualifier("mainDiscountPolicy")
@MainDiscountPolicy // <========================= 요ㅕ기 수정
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

@MainDiscountPolicy 라고 붙여주므로 오타가 나도 컴파일 시점에서 잡아주니까 찾기 쉽다!!

신! 난! 다!

 

또한 DiscountPolicy를 사용할 때 보면

//생성자 자동 주입
@Autowired
//@MainDiscountPolicy 사용
public OrderServiceImpl(MemberRepository memberRepository,@MainDiscountPolicy DiscountPolicy discountPolicy) {
 	this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}


//수정자 자동 주입
@Autowired
//@MainDiscountPolicy 사용
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
 	return discountPolicy;
}

 

사실 어노테이션에는 상속이라는 개념이 없다

그러면 어떻게 MainDiscountPolicy 어노테이션이 @Qualifier와 같은 역할을 하게 되었을까...

바로 스프링이 제공해주는 기능이다. (갓갓...)

따라서 @Qualifier뿐만 아니라 다른 어노테이션들도 함께 조합해서 사용 가능하다.

(@Autowired도 재정의 가능하다..)

킹치만,,, 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분멸하게 재정의하면 유지보수에 혼란을 준다..

 

 

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

의도적으로, 해당 타입의 스프링 빈이 다 필요한 경우가 있다.

예를 들면, 할인 서비스를 제공할때 클라이언트가 할인 정책들 선택할 수 있다고 가정!

코드로 보면

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.beans.factory.annotation.Autowired;
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.assertThat;

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); // vip  고객 등록

        //직접 할인 정책 고름
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        //시스템이 확인
        assertThat(discountPrice).isEqualTo(1000);
        assertThat(discountService).isInstanceOf(DiscountService.class);

    }

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

참고)

위 코드에서 AutoAppConfig를 설정파일로 스프링 컨테이너를 올리므로,,,

AutoAppConfig로 컴포넌트 스캔되는 빈들은 OrderServiceImpl이 있다.

OrderServiceImpl은 생성자로 리포지토리와 할인정책이 들어와야되는데....

현재 스프링 컨테이너에는 @Component로 FixDiscountPolicy, RateDiscountPolicy가 있다..

하지만 어떤 할인 정책이 들어올지 결정해주지 않아서 스프링 컨테이너 생성시 오류가 난다.

=> DiscountPolicy라는 타입이 두개 있는데 뭘 넣어줄지 안정해줫기때문이다.

왜냐면 스프링 빈 등록시 생성자도 같이 불려지기 때문이다.

따라서 OrderServiceImpl은 @Component를 주석처리해서 스프링 빈으로 등록하지 말자

 

결과를 보면

policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@3d3ba765, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@25bc0606}
policies = [hello.core.discount.FixDiscountPolicy@3d3ba765, hello.core.discount.RateDiscountPolicy@25bc0606]
discountCode = fixDiscountPolicy
discountPolicy = hello.core.discount.FixDiscountPolicy@3d3ba765

할인 정책들이 다 빈에 등록되어 있고, fixDiscountPolicy가 잘 선택된걸 알 수있다.

 

 

 

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

이때까지 @Component로 자동 빈 등록하는 법과 @Bean으로 수동 빈 등록하는 법을 봣는데

그러면 언제 자동을 쓰고 언제 수동을 써야될까?

결론은 시간이 갈 수록 점점 자동을 선호하는 추세다.

 

스프링은 @Component뿐만 아니라 @Service, @Controller, @Repository 처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다.

게다가 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.

 

앞에서 배운대로 애플리케이션을 구성하는 부분실제 동작하는 부분을 명확하게 나누는 것이 이상적이다.

하지만 일일히 수동으로 @Bean 다 적고 의존관계를 주입해주는것은 엄청 귀찮다....

또한 관리하는 빈 개수가 늘어나면... 언제 다 적냐... => 부담이 되버림

결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

 

따라서 수동 빈은 다음과 같은 경우에 사용한다.

애플리케이션 => 업무로직 + 기술 지원 로직

  • 업무로직 : 웹을 지원하는 컨트롤러, 핵심 비지니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무로직!!, 보통 비지니스 요구사항을 개발할 때 추가되거나 변경된다
  • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. DB연결이나 공통 로그처리처럼 엄부 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

 

따라서 업무로직은 숫자도 많고, 한번 개발해야하면 컨트롤러, 서비스, 리포지토리처럼 어느정도 유사한 패턴이 있다.

=> 유사한패턴? = 자동 기능을 적극 사용한다. 문제가 발생해도 어디서 문제가 생겼는지 알기 쉽다.

 

기술지원 로직은 그 수가 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다.

기술지원 로직은 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.

 

결론.

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록하자! 따라서 설정 정보에서 바로 나타나니까 유지하기가 쉽다!

 

위의 DiscountServie를 다시보면

조회한 빈이 모두 필요하다. DiscountService가 의존관계 자동주입으로 Map<String, DiscountPolicy> 에 주입 받는 상황에서

어떤 빈이 주입될지, 각 빈들의 이름은 무엇인지 코드만 봐선 알기 힘들다.

만약 이걸 남이 개발해서 준거라면??? => 더더욱 파악하기 힘듬

따라서 할인정책 설정정보를 만들고 거기에 수동 빈 등록하면

할인정책 설정정보만 보면 한눈에 보이게 된다.

@Configuration
public class DiscountPolicyConfig {

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

 

혹은 자동으로 하고 싶은 경우는 DiscountPolicy 패키지를 만들어서 같이 두면 패키지만 열어보면 파악이된다!!

 

핵심은 바로 보고 이해가 되야한다

 

참고로 스프링과 스프링 부트가 자동으로 등록하는 수많은 빈들은 예외이다.

==>

스프링 자체를 잘 이해하고, 스프링의 의도대로 잘 사용하는게 중요하다.

스프링 부트의 경우 DataSource같은 DB 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.

반면, 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 들어내는 것이 좋다.

 

결론.

따로 설정정보 만들거나 같은 패키지로 묶어서 알아보기 쉽게 하자

만약 기술 지원 객체라면 수동 등록하여 명확하게 들어나게하자

 

 

 

 

빈 생명주기 콜백


빈 생명주기 콜백 시작

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

(보통 애플리케이션은 관계형DB를 쓰는데, 미리 애플리케이션 서버가 올라올때 DB랑 연결을 미리 해둔다.)

(네트워크 소켓또한 미리 열어두면 열려잇는 소켓으로 빨리 응답할 수 있다.)

 

예제코드로 스프링을 통해서 초기화 작업과 종료 작업을 어떻게 진행하는지 보자.

 

간단하게 외부 네트워크에 미리 연결을 하는 객체 하나를 생성한다고 가정!

(실제 네트워크에 연결하는 건 아니고, 단순히 문자만 출력하도록 함)

 

NetworkClient는 애플리케이션 시작 시점에 connect()를 호출해서 연결을 맺어두어야하고, 애플리케이션이 종료되면 disconnect()를 호출해서 연결을 끊어야된다.

 

package hello.core.lifecycle;

//예제를 위한 가짜 networkclient => 실제 연결 안함
public class NetworkClient {
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출!!!, url = " + url);
        connect(); //객체가 스프링 빈에 등록되니까 미리 연결
        call("초기화 연결 메시지");
    }

    //외부에서 url 넣음
    public void setUrl(String url) {
        this.url = url;
    }

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

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

    //call할 경우 url과 메시지 보여줌
    public void call(String message){
        System.out.println("call = " + url + "message = " + message);
    }



}

 

이제 테스트 해보자.

package hello.core.lifecycle;

import ch.qos.logback.core.spi.LifeCycle;
import org.junit.jupiter.api.Test;
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() {
        /**
         * 직접 ApplicationContext를 close할 일은 없다!
         * 따라서 ApplicationContext의 하위 구현 클래스인 "ConfigurableApplicationContext"을 사용한다.
         * ConfigurableApplicationContext하위 클래스로 AnnotationConfigApplicationContext가 있다.
         */
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient networkClient = 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 = null
connect = null
call = nullmessage = 초기화 연결 메시지

?????

null이 나와버린다....

why?? => 다시 위로 가서 생성자 부분을 봐바, url 정보 없이 connect가 호출되어진다.

public NetworkClient() {
        System.out.println("생성자 호출!!!, url = " + url);
        connect(); //객체가 스프링 빈에 등록되니까 미리 연결
        call("초기화 연결 메시지");
    }

즉, 객체를 생성하는 단계에는 url이 없고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해서 setUrl()이 호출되어야지 url이 생긴다.

 

스프링 빈의 라이프 사이클을 간단하게 보면

객체 생성 -> 의존관계 주입

(생성자 주입같은 경우는 객체 생성시점에 연결되어진다. 객체 생성하려면 생성자를 불러와야되니까..)

즉, 객체 생성하고 의존관계 주입이 다 끝나야지 필요한 데이터를 사용할 수 있는 준비가 완료된다.

but... 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있을까??

스프링은 의존관계 주입이 완료되면, 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다.

또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료할 수 있다.

 

정리하면

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

  • 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
  • 소멸전 콜백 : 빈이 소멸되기 직전에 호출

 

참고로 객체의 생성과 초기화를 분리하자....

생성자 : 필수정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다.

초기화 : 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.

따라서 생성자에 다 넣지말고 생성하는 것과 초기화하는 것을 명확히 나누는 것이 유지보수 관점에서 좋다.

(초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우엔 생성자에서 한번에 다 처리하는게 더 나을 수 있다.)

즉, 객체 내부에 값을 세팅하는 것같이 가벼운 작업만 생성자에 넣자...

또한 생성/초기화 분리시, 동작을 지연할 수 있다는 장점이 있다.

예시로, 객체를 생성하고 외부 커넥션과 같은 행동은 어떤 액션이 들어오기 전까지 미룰 수 있다.

따라서 (액션이 들어오면) 그 때 초기화하면 된다.

 

싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다.

이와 다르게 생명주기가 짧은 빈들도 있다. 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸전 콜백이 일어난다. 더 자세한건 뒤에서 ....

 

 

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

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

 

인터페이스(IntializingBean, DisposableBean) 구현해서 사용

package hello.core.lifecycle;

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

//예제를 위한 가짜 networkclient => 실제 연결 안함
//InitializingBean => afterPropertiesSet() 메서드가 있음
//말 그대로 설정 끝나면, 의존 관계 주입이 끝나면 set해줌
//DisposableBean => destory 메서드가 있다.
public class NetworkClient implements InitializingBean, DisposableBean {
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출!!!, url = " + url);
        connect(); //객체가 스프링 빈에 등록되니까 미리 연결
        call("초기화 연결 메시지");
    }

    //외부에서 url 넣음
    public void setUrl(String url) {
        this.url = url;
    }

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

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

    //call할 경우 url과 메시지 보여줌
    public void call(String message){
        System.out.println("call = " + url + "message = " + message);
    }


    //디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
    @Override
    public void destroy() throws Exception {
        disconnect();
    }

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

 

 

테스트 결과

생성자 호출!!!, url = null
connect = null
call = nullmessage = 초기화 연결 메시지
connect = http://hello-spring.dev
call = http://hello-spring.devmessage = 초기화 연결 메시지
23:39:19.229 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Thu Mar 31 23:39:19 KST 2022
close = http://hello-spring.dev

초기화 메서드가 주입 완료 후에 적절하게 호출된 것을 확인할 수 있다.

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

이 메서드가 실행되었다.

connect = http://hello-spring.dev
call = http://hello-spring.devmessage = 초기화 연결 메시지

 

그리고 스프링 컨테이너의 종료가 호출되자 소멸 메서드가 호출된 것도 확인할 수 있다.

//디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
    @Override
    public void destroy() throws Exception {
        disconnect();
    }

이 메서드가 호출되었다.

23:39:19.229 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Thu Mar 31 23:39:19 KST 2022
close = http://hello-spring.dev

 

즉, 스프링이 알아서 초기화시 afterPropertiesSet메서드를 호출하고 종료시 destroy 메서드를 호출해준다.

 

하지만 초기화, 소멸 인터페이스를 사용하는 것은 단점이 있다.

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

이러한 단점이 있다고 걱정 ㄴㄴ

인터페이스를 사용해서 초기화 종료하는 방법은 스프링 초창기에 나온 방법들이다.

지금은 더 나은 방법들이 있어서 거의 사용하지 않는다.

(찾아보면 2003년도에 만든 방법이다.....)

 

\1. 외부 라이브러리를 수정할 수 없기 때문에 InitializingBean, DisposableBean을 외부 라이브러리에 적용할 수 없습니다.

코드를 수정할 수 없으니 해당 인터페이스들을 적용할 수 없고

해당 인터페이스를 적용할 수 없으니 메서드(afterPropertiesSet, destroy)를 구현할 수 없고

메서드가 구현되지 않으니 스프링이 로드될 때 afterPropertiesSet, destroy가 호출되지 않습니다.

구현되지 않은 메서드를 호출할 수는 없는 것이죠.

그래서 외부 라이브러리에 InitializingBean, DisposableBean을 적용할 수 없는 것입니다.

만약 난 무조건 InitializingBean, DisposableBean을 사용해서 초기화와 종료를 하겠다면 외부 라이브러리의 Wrapper Class를 만들고 해당 Wrapper Class에 InitializingBean, DisposableBean을 구현하여 Wrapper Class를 빈으로 등록할 수도 있을 것 같습니다. 저라면 그냥 @Bean의 속성(init, destory)을 사용하겠습니다.

\2. "코드가 아니라 설정 정보를 사용한다"에서 코드는 외부 라이브러리 코드를 말합니다. 만약 외부 라이브러리가 수정 가능한 형태(.java)가 아니라 .class 형태라면 우리가 할 수 있는 것은 외부 라이브러리의 API를 호출하는 것 밖에 없습니다.

설정 정보는 Bean을 수동등록하는 Configuration파일을 말합니다.

@Bean()의 init, destory 속성에 외부 라이브러리의 초기화, 종료 메서드를 지정하는 것만으로도 스프링이 시작되고 종료될 때 @Bean의 init, destory 속성에 지정된 메서드가 실행됩니다.

그래서 (외부 라이브러리의) 코드가 아니라 설정 정보(Bean을 수동 등록하는 Configuration파일)를 사용한다 라고 말씀하신 겁니다.

위의 맥락에 따라 외부라이브러리 코드 수정 없이 초기화 메서드, 종료 메서드만 지정해주면 되기 때문에 코드 수정이 필요한 InitializingBean, DisposableBean과 달리 외부 라이브러리를 사용할 수 있다고 표현하신 겁니다.

 

먼저 외부 라이브러리가 어떤 형태의 파일이며 그걸 어떻게 사용하는지에 대해 아시면 이해하는데 도움되실 것 같네요.

외부 라이브러리는 보통 class파일로 구성되어 있습니다. 이를 사용자게에 배포할 때는 일반적으로 java파일이 아닌 컴파일한 뒤 생성된 결과물인 class파일을 jar로 패키징하여 배포합니다.

여기서 문제가 발생합니다.

class파일은 바이트코드로 구성되어있기 때문에 JVM이 읽기 편한 파일이지 개발자가 읽으면서 수정하기 편한 파일이 아닙니다.

그러므로 개발자가 class파일을 수정하기 어렵다는 것입니다. (어려운 것이지 못하는 것은 아닙니다.)

.

예를 들어보겠습니다.

A라는 외부 라이브러리가 있으며 당연히 외부 라이브러리 특성상 모든 파일은 컴파일된 class파일입니다.

이 라이브러리에는 NetworkClient라는 클래스가 있습니다.

이 클래스에 aInit, aDestroy라는 메서드가 존재합니다.

개발자인 우리는 이 클래스를 스프링 빈으로 수동 등록하였습니다.

그런데 스프링이 로딩되고 종료될 때 자동으로 aInit(), aDestroy()가 실행되게 만드려고 합니다.

스프링 초창기에 지원했던 InitializingBean, DisposableBean 인터페이스를 사용하고자 합니다.

위 2가지 인터페이스를 NetworkClient라는 클래스에 적용하여 afterPropertiesSet, destroy를 구현하려고 했습니다.

그런데 NetworkClient는 java파일이 아니라 바이트코드로 구성된 class파일이어서 코드를 변경하기 어려운 것입니다.

그래서 NetworkClient 클래서를 직접 수정하는 방법(인터페이스를 구현하는 것) 대신 수동으로 빈을 등록할 때 @Bean의 속성인 init, destroy를 사용하는 방법을 택했습니다.

이미 NetworkClient 클래스에서 제공하는 메서드인 aInit, aDestroy가 존재하므로 init, destroy 속성의 값으로 aInit, aDestroy를 넣어 스프링이 시작하고 종료될 때 aInit, aDestroy가 실행되게 했습니다.

 

 

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

설정 정보에 @Bean(initMethod = "init", destoryMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

 

NetworkClient 에 초기화, 소멸 메서드의 이름을 바꾸고 테스트 해보자.

package hello.core.lifecycle;

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

//예제를 위한 가짜 networkclient => 실제 연결 안함
//InitializingBean => afterPropertiesSet() 메서드가 있음
//말 그대로 설정 끝나면, 의존 관계 주입이 끝나면 set해줌
//DisposableBean => destory 메서드가 있다.
public class NetworkClient  {
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출!!!, url = " + url);
        connect(); //객체가 스프링 빈에 등록되니까 미리 연결
        call("초기화 연결 메시지");
    }

    //외부에서 url 넣음
    public void setUrl(String url) {
        this.url = url;
    }

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

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

    //call할 경우 url과 메시지 보여줌
    public void call(String message){
        System.out.println("call = " + url + "  message = " + message);
    }


    //디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
   // @Override
//    public void destroy() throws Exception {
//    이름 바꾼다.
    public void close() throws Exception {

        disconnect();
    }

   // @Override
//    public void afterPropertiesSet() throws Exception {
    //이름 바꿈
    public void init() throws Exception {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    // 이름을 바꿧으니 이제 설정정보에 초기화, 소멸 메서드를 알려준다.
}

초기화, 소멸 메서드 이름을 바꾸었으니 이제 설정 정보에 알려주자.

package hello.core.lifecycle;

import ch.qos.logback.core.spi.LifeCycle;
import org.junit.jupiter.api.Test;
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() {
        /**
         * 직접 ApplicationContext를 close할 일은 없다!
         * 따라서 ApplicationContext의 하위 구현 클래스인 "ConfigurableApplicationContext"을 사용한다.
         * ConfigurableApplicationContext하위 클래스로 AnnotationConfigApplicationContext가 있다.
         */
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient networkClient = 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;

        }
    }
}

 

 

결과.. 결과를 보자..

생성자 호출!!!, url = null
connect = null
call = null  message = 초기화 연결 메시지
NetworkClient.init
connect = http://hello-spring.dev
call = http://hello-spring.dev  message = 초기화 연결 메시지
20:35:23.488 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Fri Apr 01 20:35:23 KST 2022
close = http://hello-spring.dev

초기화 메서드와 소멸 메서드가 잘 호출된 것을 볼 수 있다.

 

자 위와같이 설정 정보를 사용한 결과를 보자.

  • 메서드 이름을 자유롭게 줄 수 있게 되었다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다. => 위처럼 메서드 이름 맘대로 바꿔도 됨, 인터페이스 상속받아서 쓴게 아니니까
  • 코드가 아니라 설정정보를 사용하기 때문에 코들르 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있게 된다.
  • (즉, 초기화 할땐 init, 소멸할땐 close를 써!! 라고 지정 가능, 만약 외부라이브러리 초기화 이름이"init2"이면 init2로 이름 설정해주면 댄다.)

 

종료 메서드 추론

  • @Bean의 destoryMethod 속성에는 아주 특별한 기능있음 => 바로.... 기본값이 AbstarctBeanDefinition.INFER_METHOD 임 => String INFER_METHOD = "(infered)" => 즈윽,, 그냥 기본 값이 infer(추론하다)임
  • 라이브러리는 대부분 "close", "shutdown" 이라는 이름의 종료 메서드를 사용하므로 이값들을 추론함
  • close나 shutdown 이라는 이름의 메서드를 알아서 찾아서 가져옴. 말그대로 이름을 추론해서 가져옴
  • 그러므로 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작함(만약 close하지 않을 일이 있거나 그런다면,,, 거의 없겠지만,,,)
  • 추론 기능을 끄고 싶으면 destoryMethod="" 처럼 빈 공백을 지정하면된다.

 

 

 

 

 

어노테이션 @PostConsturct, @PreDestroy <= 결론을 말하면 그냥 이거 쓰면 댐

해당 어노테이션들은 java 진영에서 공식적으로 지원하는 것이므로, 스프링 이외의 다른 컨테이너에도 적용이된다.

import javax.annotation.PostConstruct;

javax 인걸 알 수있다. => javax로 시작하면 java진영에서 공식적으로 지원하는 것.

 

코드로 바로 보자...

package hello.core.lifecycle;

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

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

//예제를 위한 가짜 networkclient => 실제 연결 안함
//InitializingBean => afterPropertiesSet() 메서드가 있음
//말 그대로 설정 끝나면, 의존 관계 주입이 끝나면 set해줌
//DisposableBean => destory 메서드가 있다.
public class NetworkClient  {
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출!!!, url = " + url);
        connect(); //객체가 스프링 빈에 등록되니까 미리 연결
        call("초기화 연결 메시지");
    }

    //외부에서 url 넣음
    public void setUrl(String url) {
        this.url = url;
    }

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

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

    //call할 경우 url과 메시지 보여줌
    public void call(String message){
        System.out.println("call = " + url + "  message = " + message);
    }


    //디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
   // @Override
//    public void destroy() throws Exception {
//    이름 바꾼다.
    @PreDestroy // <====
    public void close() throws Exception {

        disconnect();
    }

   // @Override
//    public void afterPropertiesSet() throws Exception {
    //이름 바꿈
    @PostConstruct // <====
    public void init() throws Exception {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }
}

아까 설정정보에 적엇던 정보는 주석처리 해버리고..

package hello.core.lifecycle;

import ch.qos.logback.core.spi.LifeCycle;
import org.junit.jupiter.api.Test;
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() {
        /**
         * 직접 ApplicationContext를 close할 일은 없다!
         * 따라서 ApplicationContext의 하위 구현 클래스인 "ConfigurableApplicationContext"을 사용한다.
         * ConfigurableApplicationContext하위 클래스로 AnnotationConfigApplicationContext가 있다.
         */
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient networkClient = ac.getBean(NetworkClient.class);
        ac.close();

    }



    @Configuration
    static class LifeCycleConfig{

        //@Bean(initMethod = "init", destroyMethod = "close")
        @Bean
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;

        }
    }
}

 

잘 호출 되는 것을 알 수 있다.

생성자 호출!!!, url = null
connect = null
call = null  message = 초기화 연결 메시지
NetworkClient.init
connect = http://hello-spring.dev
call = http://hello-spring.dev  message = 초기화 연결 메시지
21:17:01.553 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Fri Apr 01 21:17:01 KST 2022
close = http://hello-spring.dev

 

 

@PostConstruct, @PreDestory 어노테이션 특징

  • 최신 스프링에서 가장 권장하는 방법
  • 어노테이션 하나만 붙이면 되니까 편리함
  • 스프링에 종속적인게 아니라 자바표준임 => 다른 컨테이너에도 적용가능
  • 컴포넌트 스캔과 잘 어울림 (@Bean(~~~~)적어 줄 필요가 없어지니까...)
  • 외부 라이브러리에는 적용하지 못한다. 외부 라이브러리를 초기화, 종료해야하면 @Bean의 기능을 사용하자

 

오픈소스들을 외부 라이브러리라고 이해하시면 됩니다. 이 경우 우리가 소스코드를 포함하는것이 아니라 이미 컴파일된 class 파일이 모여있는 jar 파일을 포함하게 됩니다.

따라서 소스코드 수정이 불가능합니다.

@Postconstruct, @PreDestroy를 적용하려면 소스코드를 수정해야겠지요?

감사합니다.

 

왜 어노테이션을 사용하면 외부 라이브러리에 사용을 하지 못할까라는 생각이 들었습니다.

보면 @Bean을 이용하면 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 적용할 수 있다고 하셨습니다.

예로 gradle을 들어보겠습니다. gradle은 저희가 수정할 수 없는 외부 라이브러리입니다. 여기서 테스트를 한다고 했을 때, 우리는 테스트 코드를 짜면서 직접 @Bean으로 등록할 때, 해당 라이브러리에 있는 클래스 안에 있는 메소드들을 파악하고 빈으로 직접 등록하여 초기화, 종료를 할 수 있다.

그러나 어노테이션은 코드에 @을 붙여야하는데 코드를 수정할 수 없기 때문에 사용할 수 없다.

->

@PostConstruct, @PreDestroy 애노테이션들은 코드가 수정 가능한 곳에 적용 가능하며

그렇지 못한 곳에서는 @Bean의 init method, destory method를 통해서 초기화, 종료 메서드를 지정할 수 있습니다.

 

 

여기서 고칠 수 없는 외부라이브러리를 NetworkClient라고 하고

@Bean의 initMethod와 destroyMethod를 지정하면

이 지정한 메소드가 NetworkClient안에 만들어야 한다고 인텔리제이에서 오류수정이 뜨는데 외부라이브러리는 수정할 수가 없는 상황인데 이 부분이 잘 이해가 되질 않습니다. ->

외부에서 제공하는 라이브러리들은 보통 어떤 초기화, 종료 메서드를 제공하는지 알려줍니다.

외부에서 제공하는 라이브러리가 초기화, 종료 메서드가 없다면 스프링에서도 없는 메서드를 호출할 수는 없습니다.

 

정리

  • @PostConstruct, @PreDestroy 어노테이션을 사용하자.
  • 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야되면 @Bean 의 initMethod, destoryMethod 를 사용하자.

 

 

빈 스코프


이때까지는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어 스프링 컨테이너가 종료될 때까지 유지된다고 알았다.

하지만 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다.

스코프 = 빈이 존재할 수 있는 범위

 

스프링이 지원하는 스코프

  • 싱글톤 : 기본 스코프 = 디폴드 값임, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프즉, 요청하면 그때 만든다. + 의존관계 주입도 해준다. + 초기화메서드가 있으면 초기화까지 해서 던져준다.
  • (client에게 반환해주고, 컨테이너는 더이상 관리하지 않는다! => 그러면 종료메서드는 누가 호출해주냐... 클라이언트가 호출하던가 해야댐)
  • 컨테이너가 빈을 만들어서 던져준 뒤 관리하지 않는다.
  • 웹 관련 스코프
    • request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프이다.
    • Request(생성) ~~~ Response(소멸)
    • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
    • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

싱글톤, 프로토타입, request 정도만 알면된다

 

빈 스코프를 지정하는 방법

 

  • 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class Bean{}

 

  • 수동 등록
@Scope("prototype")
@Bean
PrototypeBean Bean(){
	return new Bean();
}

 

 

프로토타입 스코프

싱글톤 스코프 빈 => 조회시 항상 같은 인스턴스를 반환했음

프로토타입 스코프 => 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환함

 

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

그리고 생성한 빈을 클라이언트에게 던져주고 관리하지 않는다.

 

스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리

그러므로 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있음. @PreDestory같은 종료메서드는 호출되지 않음.

따라서 소멸하고 싶으면 client가 해줘야됨.

 

일단 먼저 기존 싱글톤 사용시 객체를 두번 조회해보자

package hello.core.scope;

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.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

//기존 싱글 톤의 경우 생성된 인스턴스가 같다
public class SingletonTest {

    @Test
    public void singletonBeanFind(){
        //Component class 를 넣어주는 것이다.
        //자동으로 component 스캔 대상이된다.
        //따라서 스프링 빈으로 등록되는 것이다.
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);

        assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();

    }

    //@Configuration이 붙지 않으면 순수 객체로 등록된다.
    //@Configuration 은 등록하는 빈들이 여러개 있다.
    @Scope("singleton")//디폴트값임
    static class SingletonBean {

        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }

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

}

 

테스트가 정상적으로 작동한다.

        assertThat(singletonBean1).isSameAs(singletonBean2);

 

SingletonBean.init
23:00:17.485 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4310d43, started on Fri Apr 01 23:00:17 KST 2022
SingletonBean.destroy

싱글톤은 초기화가 한번만 되었따.

 

이제 프로토타입 스코프 빈을 테스트 해보자

package hello.core.scope;


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

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

public class PrototypeTest {

    @Test
    public void prototypeBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

        ac.close(); // 컨테이너 종료


    }


    //@Configuration이 붙지 않으면 순수 객체로 등록된다.
    //@Configuration 은 등록하는 빈들이 여러개 있다.
    @Scope("prototype")//프로토타입으로 설정
    static class PrototypeBean {

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

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

 

assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

성공적으로 동작함을 알 수 있다.

 

이제 결과를 보자

PrototypeBean.init
PrototypeBean.init
23:06:14.069 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4310d43, started on Fri Apr 01 23:06:13 KST 2022

Process finished with exit code 0

앞선 싱글톤 테스트 결과와 비교하면

SingletonBean.init
23:00:17.485 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4310d43, started on Fri Apr 01 23:00:17 KST 2022
SingletonBean.destroy

 

PrototypeBean.init PrototypeBean.init

두번 초기화가 되엇고, 테스트 성공으로 각각 다른 인스턴스가 생겨난것을 알 수 있다.

 

하지만 싱글톤 테스트시에는 SingletonBean.destroy 즉 소멸자가 잘 호출되었는데

프로토타입 테스트시에는 소멸자가 호출되지 않았다.

 

따라서 직접 prototypebean1.destory()와 같이 직접 닫아줘야한다.

 

 

정리

  • 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
  • 컨테이너는 생성, 의존관계주입, 초기화까지만 관여한다.
  • 그러므로 종료 메서드가 호출되지 않는다.
  • 프로토타입 빈은 해당 빈을 조회한 클라이언트가 관리해야한다. 종료 메서드에 대한 호출도 클라이언트가 직접해야한다.

 

 

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

둘다 섞어쓰면 문제가 발생할 수 도 있다.

왜일까? 두 빈의 스코프를 잘 생각해보자.

만약 싱글톤 빈이 프로토타입 빈을 호출하면???

문제) 이때 프로토타입 빈의 스코프는??

-> 바로 싱글톤빈이 죽을때 까지 같이 들고 있는 거다.

원래 프로토타입 빈의 목적은?? -> 조회시 마다 새로 생성해서 다 다른 객체 인스턴스를 가지게하는것!

싱글톤빈이 안고 가버리면?? 같은 객체 인스턴스가 튀어나와버림

 

 

테스트로 확인해보자

먼저 프로토 타입 빈을 조회하여 addCount를 각각 해보자

package hello.core.scope;

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

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);



    }

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

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

    }
}

테스트가 잘 돌아가는 것으로 보아 각각 1씩 저장됨을 알 수 있다.

 

여기서 이제 싱글톤 빈이 프로토타입 빈을 조회하게 해보자.

즉, 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하게해보자.

 

  1. clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
  2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환. (여기서 프로토타입 빈의 count필드 값은 0이다)
  3. clientBean은 프로토타입 빈을 내부 필드에 보관한다.(정확히는 참조값을 보관한다.)
  4. 클라이언트A는 clientBean을 스프링 컨테이너에 요청하고 받는다. (clientBean은 싱글톤이므로 항상 같은 빈임)
  5. 클라이언트A는 clientBean.logic()을 호출한다.
  6. clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. => count값이 1이됨
  7. 그 다음 클라이언트B가 clientBean을 스프링빈에 요청하고 받는다.
  8. 그리고 같은 로직을 실행한다. => prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. => count값이 2가 된다.

 

왜일까? clientBean은 싱글톤이다. 따라서 항상 같은 객체이다.

앞서 클라이언트A가 싱글톤인 clientBean을 요청하고 프로토타입 빈 로직 addCount()을 실행했다.

따라서 싱글톤 clientBean프로토타입 prototypeBean을 가지게 된다.

그래서 클라이언트B 도 같은 prototypeBean을 사용하게 된다.

 

테스트 코드로 보자

package hello.core.scope;

import lombok.RequiredArgsConstructor;
import org.assertj.core.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;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        assertThat(clientBean1.logic()).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        assertThat(clientBean2.logic()).isEqualTo(2); //2가 되어버린다.



    }

    //귀찮아서 롬복으로 생성자 만듬.. => 의존관계 주입
    @RequiredArgsConstructor
    //디폴트 이므로 싱글톤임
    static class ClientBean{

        private final PrototypeBean prototypeBean;

        public int logic(){
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }


    }

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

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

    }
}

 

결론적으론 싱글톤 빈과 함께 계속 유지되어서 문제이다.

 

원했던건 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때마다 새로 생성해서 사용하는 것을 원했다...

 

여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.

ex)ClientA, ClientB가 각각 의존 관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입받는다.

 

 

 

프로토타입 스코트 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

가장 간단한 방법으론 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.

package hello.core.scope;

import lombok.RequiredArgsConstructor;
import org.assertj.core.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;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        assertThat(clientBean1.logic()).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        assertThat(clientBean2.logic()).isEqualTo(1);



    }

    //귀찮아서 롬복으로 생성자 만듬..
    //@RequiredArgsConstructor
    //디폴트 이므로 싱글톤임
    static class ClientBean{

        //프로토타입을 사용할때 마다 스프링 컨테이너에 새로 요청!!
        @Autowired //필드주입
        private ApplicationContext ac;

        //private final PrototypeBean prototypeBean;

        //그리고 로직 실행시 매번 새로 받아옴
        public int logic(){
            //매번 새로운 PrototypeBean 생성 => 컨테이너에 요청
            PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }


    }

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

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

    }
}

 

결과를 보면

PrototypeBean.init
PrototypeBean.init

 

 

ac.getBean()으로 항상 새로운 프로토타입 빈이 생성된다.

이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존 관계 조회(탐색)이라 한다.

(외부에서 의존관계를 주입 받는게 아니라 직접 스프링 컨테이너에게 요청하여 가져왔다.)

스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고 단위테스트도 어려워진다.

스프링 컨테이너에 종속되면 안되는 이유???

거의 그럴일이 없기는 한데, 스프링 말고 다른 DI 컨테이너들도 있습니다^^

스프링 -> 다른 DI 컨테이너로 이동할 때까지 함께 고민하는 부분입니다.

 

결론적으로 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL정도의 기능만 제공하는 무언가이다

스프링은 모든게 준비되었다. 후훗..

 

 

 

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다.

과거에는 ObjectFactory가 있었지만, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다.

 

    //디폴트 이므로 싱글톤임
    static class ClientBean{
//
//        //프로토타입을 사용할때 마다 스프링 컨테이너에 새로 요청!!
//        @Autowired //필드주입
//        private ApplicationContext ac;

        //private final PrototypeBean prototypeBean;

        //필드 주입이다...(테스트니까) => ObjectProvider객체는 빈으로 등록안했는데 어케 의존관계 주입??
        //스프링이 기본적으로 들고 있는다. 즉, 자동 주입해준다.
        //대리자 역할을 한다.
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;

        //그리고 로직 실행시 매번 새로 받아옴
        public int logic(){
            //getObject 하면 알아서 찾아준다.
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }


    }

 

  • prototypeBeanObjectProvider.getObject() 으로 항상 새로운 프로토타입 빈이 생성된다.
  • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. => 즉 DL
  • 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
  • ObjectProvider는 지금 딱 필요한 DL정도의 기능만 제공한다.스프링 컨테이너에 직접 찔러서 요청하는게 아니라 대리자 역할을 해준다.
  • 즉, 스프링 컨테이너를 통해서 찾아주는 과정을 간단하게 도와주는 것이다.

ObjectFactory : 기능이 단순하고 별도의 라이브러리가 필요없다. 스프링에 의존한다.

ObjectProvider : ObjectFactory상속, 옵션, 스트림 처리등 편의기능이 많고, 별도의 라이브러리가 필요없다. 스프링에 의존

 

\1. 그러면 프로토타입을 요청을 하고 생성된 후 생성된 빈을 찾아 반환해주는 것 까지의 모든 과정이 Provider가 혼자 수행하는 역할이 맞나요?(요청 및 반환)

=> Provider가 특화된 기능을 모아서 제공하지만 모든 동작을 단독으로 처리하는 것은 아닙니다. 예를들어 프로토타입 빈을 생성하는 것은 빈 팩토리에게 위임합니다.

 

\2. 또한 이 모든 과정 자체를 DL이라고 볼 수 있는건가요?(생성된 것만을 조회하는것이 아닌 더 넓은 의미로, 처음 요청(조회)하여 생성하고 반환 해주는 것까지 DL이라고 볼 수 있는지)

아니면 요청하고 생성하는 것은 제외한 생성된 빈을 조회하여 반환해주는 것까지의 과정이 DL인가요?

=> 어디까지나 Provider의 핵심 역할은 빈을 조회하여 가져오는 것입니다.(DL) 다만, Prototype의 경우 새로운 빈을 생성해야 하기 때문에 Provider가 빈 팩토리에 이를 요청합니다.

 

\3. getObjetct() 등으로 요청을 한 뒤에, 빈이 생성되는게 맞나요?(순서가 맞나요?)

=> 네 맞습니다.

 

\4. ObjectProvider prototypeBeanProvider @Autowired로 주입 받아서 new 같은 초기화를 안해도 되나요?

=> 이미 생성된 빈을 주입 받는 것이기 때문에 별도로 생성하지 않아도 됩니다.

 

 

\2. 이번 강의에서는 앞 강의와 다르게 @Configuration 을 사용하지 않으셨는데, 그냥 하나의 설정파일에 여러개 빈을 한꺼번에 등록하느냐, 아니면 각각 등록하느냐의 차이만 있을 뿐인가요?

 

 

Provider를 통해서 싱글톤 빈을 DL 하는 경우에는 싱글톤 빈이 생성 되는 것이 아니라 조회됩니다^^

프로토타입은 조회할 때 마다 새로 생성됩니다.

DL은 컨테이너를 통해서 빈을 찾아온다고 생각하시면 됩니다.

 

이번강의에서 ObjectProvider 오브젝트를 Autowired로 의존관계 주입을 받아서 사용하는데요!

제가 토비의 스프링 책이랑 강사님 강의랑 같이 공부중인데, Autowired 로 의존관계 주입을 받을때에는 일단 스프링 컨테이너에 등록된 빈들 중에서 타입이 맞는 오브젝트를 주입해주는 것으로 알고있는데요

그렇다면 저 예제에서 ObjectProvider 인터페이스를 구현한 오브젝트가 스프링 컨테이너에 빈으로 등록이 되어야 의존관계 주입을 받을 수 있을것 같은데요

 

책에서는 ObjectFactory 예제이긴 하지만 어쨋든 XML 파일에 ObjectFactoryCreatingFactoryBean 이라는 빈을 생성해서 ObjectFactory 의존관계를 주입받아서 사용하는데, 강의에서는 빈 생성없이 그냥 바로 주입받아서 쓰는 것 같아서 질문드립니다.

=>

이러한 문제를 해결하는 다양한 방법이 있는데요. ObjectProvider를 사용하면 별도의 빈을 등록하지 않아도 스프링이 이런 부분을 자동으로 처리를 해줍니다.

 

"ApplicationContext에 비해 ObjectProvider는 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다."

라는 말이 이해가 안됩니다..

스프링 없이 순수 자바 코드만으로 단위 테스트할 경우,

테스트할 코드가 ApplicationContext에 의존하던, ObjectProvider에 의존하던 DL 기능을 사용할 수 없는 건 마찬가지 아닌가요?

DL 기능을 구현한다고 하더라도 ApplicationContext이던 ObjectProvider이던 상관없지 않나요?

 

그래서 ApplicationContext이 아닌 ObjectProvider를 써야하는 이유를 잘 모르겠습니다!

=>

테스트에서는 가짜 구현체를 만드는 경우가 있습니다.

여기에서 가짜 구현체는 모든 기능이 동작하지 않아도 됩니다. 테스트를 실행하는데 필요한 정도만 동작하면 됩니다.

ApplicationContext를 mock으로 만들려면 그곳에 있는 수 많은 인터페이스를 모두 구현해야 합니다.

반면에 ObjectProvider를 사용하면 그곳에서 제공하는 매우 적은 인터페이스만 구현해도 되므로 mock을 상대적으로 쉽게 만들 수 있습니다.

 

 

다음 코드를 통해서 스프링 컨테이너가 생성되는데요.

이 시점에 스프링은 생성자의 파라미터로 넘긴 ClientBean.class, PrototypeBean.class 두 클래스는 스프링 빈으로 등록됩니다.

AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

 

스프링 빈으로 둘이 등록되어 있기 때문에 @Autowired가 가능합니다.

 

질문3) "스프링"에서 '컨테이너'라는 것은 하나의 프로젝트 안에서 단 1개로 global하게 존재하는 객체가 아닌, 이곳저곳에 여러 개로 존재할 수 있는 독립적인 객체인가요?

-> 다음 코드가 바로 스프링 컨테이너 1개를 뜻합니다.

new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

 

이것을 여러개 생성한다면 독립적으로 존재할 수 있습니다.

그런데 보통 1개만 사용합니다.

 

provider.get();을 하면 스프링 컨테이너를 통해서 조회하기 때문에, 필요한 의존관계 주입과 초기화의 도움을 받을 수 있습니다.

반면에 new PrototypeBean()을 사용하게되면 의존관계 주입도 안되고, 필요한 초기화도 안됩니다. 모든 것을 직접 수동으로 해주어야 합니다.

그렇다고 둘중에 어떤 것이 좋다기 보다는 개발 시점에 더 적절한 방법을 선택하면 됩니다^^

 

스프링에서 제공하는 Provider외에 자바 표준으로 제공하는 JSR-330 Provider도 있따.

javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.

따라서 해당 라이브러리를 gradle에 추가해야된다.

 

추가

implementation 'javax.inject:javax.inject:1'

 

 

package javax.inject;
public interface Provider<T> {
 T get();
}
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
 	PrototypeBean prototypeBean = provider.get();
 	prototypeBean.addCount();
 	int count = prototypeBean.getCount();
 	return count;
}

 

provider.get()을 통해서 항상 새로운 프토토타입 빈이 생성된다.

provider의 get() 호출 시 내부에서는 스프링 컨테이너를 통해서 해당 빈을 찾아서 반환한다. => DL

자바 표준이고 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 쉬워진다.

Provider는 지금 딱 필요한 DL정도의 기능만 제공한다.

장점= 심플 = 단점

get() 메서드 하나로 깅으 매우 단순하다.

별도의 라이브러리가 필요하다.

 

 

정리

  • 매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요하면 프로토타입 빈을 사용하면된다.
  • 실무에서 웹 애플리케이션을 개발하다보면 싱글톤 빈으로 대부분의 문제를 해결할 수 있기때문에 잘 안씀
  • ObjectProvier, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

 

그럼 둘 중 뭘써야할까??

대부분 스프링이 더 다양하고 편히란 기능을 제공해주기때문에 다른 컨테이너를 쓸일이 없다면 스프링이 제공하는 기능을 쓰면된다.

 

 

 

 

 

웹스코프

  • 웹 환경에서만 동작한다.
  • 스프링이 해당 스코프의 종료시점까지 관리한다 => 따라서 종료 메서드가 호출된다.

 

웹스코드 종류

  • request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각 HTTP요청마다 별도의 빈 인스턴스가 생성되고 관리됨.
  • (요청마다 각각 따로 호출된다.)
  • sessoin : HTTP Session과 동일한 생명주기를 가지는 스코프
  • application : 서블릿컨텍스트와 동일한 생명주기를 가지는 스코프
  • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

 

request를 예로 들면

클라이언트A ->(HTTP request 요청) -> Controller@x01 -> Service@x02

클라이언트B ->(HTTP request 요청) -> Controller@x01 -> Service@x02

여기서 Controller는 myLogger를 요청하는데 각 요청마다 객체를 새로 생성해서 만든다.

A전용@x03 , B전용@x04 .myLogger

 

  1. 컨트롤러는 Request Scope 관련 객체를 조회
  2. (위 예시에서는 log를 찍는다고 가정)
  3. A클라이언트 전용으로 객체가 만들어진다.
  4. 서비스 객체에서 로그객체 조회, 그리고 HTTP request가 같으면 같은 객체를 본다.
  5. B클라이언트 요청이 들어오면, 다른 HTTP 요청이므로 별도의 객체를 생성한다.

=> HTTP request에 딱 요청이 들어오고 나갈때까지의 Life cycle 동안은 무조건 같은애(같은 객체)가 관리된다.

 

웹 환경 추가

웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리 추가를 해줘야한다.

 

//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'

 

위 라이브러리를 추가하면 hello.core.CoreApplication 의 main메서드를 실행하면 웹 애플리케이션이 실행되는 것을 확인할 수 있다.

 

spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해서 웹 서버와 스프링을 함께 실행시킨다.

스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext를 기반으로 애플리케이션을 구동한다.

웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로, AnnotationConfigServletWebServerApplicationContext (컨테이너이다)를 기반으로 애플리케이션을 구동한다.

만약 기본 포트인 8080 포트를 다른곳에서 사용중이어서 오류가 발생하면 포트를 변경해야 한다. 9090 포트로 변경하려면 다음 설정을 추가하자. main/resources/application.properties

server.port=9090

 

 

 

request 스코프 예제 개발

만약 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.

이럴때 request 스코프를 사용하면 편하다.

 

  • 기대하는 공통포멧 [UUID] [requestURL] {message}
  • UUID를 사용해서 HTTP 요청을 구분하자
  • requestURL 정보를 넣어서 어떤 URL을 요청해서 남은 로그인지 확인

 

MyLogger => 로그 남김 (기대하는 공통포멧 [UUID] [requestURL] {message})

LogDemoController => 해당 요청 들어오면 로직이 돌게 처리함.

LogDemoService => 비지니스 로직, 로그를 찍어줌

 

package hello.core.common;


import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;


@Component
@Scope(value = "request") //request 스코프
public class MyLogger {
    /**
     * 기대하는 공통포멧 [UUID] [requestURL] {message}
     */
    private String uuid;
    private String requestURL;

    //빈이 생성되는 시점을 알수 없으므로 set으로 주입받음
    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message){
        System.out.println("[" + uuid +"[" + requestURL + "]" + "[" + message +"]");
    }


    @PostConstruct
    //초기화 => uuid값 설정
    public void init(){
        //절대로 안겹치게 id 랜덤으로 생성해줌
        uuid =  UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);

    }

    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }


}

 

package hello.core.web;


import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo") //log-demo 요청이 오면
    @ResponseBody //뷰화면 없이 그냥 바디부에 보내버림
    //HttpServletRequest => 자바에서 제공하는 표준 서블릿 규약에 의한
    // http request 정보를 받을 수 있음
    public String logDemo(HttpServletRequest request){
        //HttpServletRequest로 http request 정보 받앗으므로
        //getRequestURL로 요청된 url 알 수 있음
        String requestURL = request.getRequestURI().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId"); //일단 아무로직 만듬
        return "OK";
    }
}

package hello.core.logdemo;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {
    /**
     * 비니지스 로직 이 있는 서비스 계층
     * 여기서도 로그를 출력
     *
     * request scope르 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면
     * 파라미터가 많아서 지저분해진다.
     *
     * 다른 문제로는 reqeustURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다.
     *
     * 웹과 관련된 부분은 컨트롤러까지만 사용해야된다.
     * 서비스계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
     *
     * request Scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고
     * MyLogger의 멤버 변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
     *
     */
    private final MyLogger myLogger;

    //myLogger 의 log 사용.
    public void logic(String id){
        myLogger.log("Service id = " + id);
    }
}

 

실행하면 에러가 발생한다.

Caused by: org.springframework.beans.factory.support.ScopeNotActiveException: Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread

 

@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

LogDemoController에선 생성시점에 MyLogger가 필요하다.

@Component
@Scope(value = "request") //request 스코프
public class MyLogger {

Mylogger의 스코프틑 request 스코프이다....

 

따라서 생성자 의존관계 주입시 mylogger가 없다!!

즉, 실제 고객의 요청이 있어야지 mylogger가 생성되어진다.

 

 

해결방안

스코프와 Provider

provider를 사용하는 것이다.

(스프링의 provider 사용해봄)

 

package hello.core.web;


import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
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;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {

    private final LogDemoService logDemoService;
//   private final MyLogger myLogger;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @RequestMapping("log-demo") //log-demo 요청이 오면
    @ResponseBody //뷰화면 없이 그냥 바디부에 보내버림
    //HttpServletRequest => 자바에서 제공하는 표준 서블릿 규약에 의한
    // http request 정보를 받을 수 있음
    public String logDemo(HttpServletRequest request){
        //HttpServletRequest로 http request 정보 받앗으므로
        //getRequestURL로 요청된 url 알 수 있음
        String requestURL = request.getRequestURI().toString();
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId"); //일단 아무로직 만듬
        return "OK";
    }
}
package hello.core.logdemo;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {
    /**
     * 비니지스 로직 이 있는 서비스 계층
     * 여기서도 로그를 출력
     *
     * request scope르 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면
     * 파라미터가 많아서 지저분해진다.
     *
     * 다른 문제로는 reqeustURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다.
     *
     * 웹과 관련된 부분은 컨트롤러까지만 사용해야된다.
     * 서비스계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
     *
     * request Scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고
     * MyLogger의 멤버 변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
     *
     */
//    private final MyLogger myLogger;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    //myLogger 의 log 사용.
    public void logic(String id){
        //getObeject로 조회함.
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.log("Service id = " + id);
    }
}

 

웹브라우저로 해당 request를 요청하면

[64d2b075-c69e-4303-a57c-5cd751b5a3bd] request scope bean create:hello.core.common.MyLogger@1c470380
[64d2b075-c69e-4303-a57c-5cd751b5a3bd[/log-demo][controller test]
[64d2b075-c69e-4303-a57c-5cd751b5a3bd[/log-demo][Service id = testId]
[64d2b075-c69e-4303-a57c-5cd751b5a3bd] request scope bean close:hello.core.common.MyLogger@1c470380

[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c] request scope bean create:hello.core.common.MyLogger@4890e67a
[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c[/log-demo][controller test]
[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c[/log-demo][Service id = testId]
[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c] request scope bean close:hello.core.common.MyLogger@4890e67a

[55130fb8-e05e-4f55-8b8f-461e68d86a75] request scope bean create:hello.core.common.MyLogger@15afd791
[55130fb8-e05e-4f55-8b8f-461e68d86a75[/log-demo][controller test]
[55130fb8-e05e-4f55-8b8f-461e68d86a75[/log-demo][Service id = testId]
[55130fb8-e05e-4f55-8b8f-461e68d86a75] request scope bean close:hello.core.common.MyLogger@15afd791

각각 다른 객체가 생성됨을 알 수 있다.

 

  • ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
  • ObjectProvider.getObject() 를 LogDemoController, LogDemoService 에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다. => 직접 구분하려면 힘들다...

 

하지만 여기서 더 줄일 수 있다...

 

 

스코프와 프록시

프록시 방법을 사용해보자.

프록시를 사용하여서 Provider의 기능을 대체할 수 도 있다.

 

 

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) //request 스코프
public class MyLogger {

 

proxyMode = ScopedProxyMode.TARGET_CLASS

TARGET_CLASS : 적용 대상이 인터페이스가 아닌 클래스이면

INTERFACE : 적용대상이 인터페이스이면

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

 

프록시 객체는 하나만 생성되고 필요한 곳에 프록시 객체가 주입된다.

요청이 들어오면, MyLogger의 프록시 객체는 MyLogger 빈을 찾을테고 없으면 생성한다.

 

  1. 일단 가짜 프록시 객체를 주입
  2. 요청이 들어오면 프록시 객체는 MyLogger 빈을 찾고 없으면 생성한다.
  3. 요청이 응답될때가지 동일한 MyLogger 빈 사용

 

즉 일단 가짜 끼워넣고 진짜 요청이 들어오면 그때 MyLogger 빈 반환

 

\1. 코드적으로는 싱글톤을 사용하는 것 처럼 작성해도 되지만 실제로는 (예제상) LogDemoController와 LogDemoService가 서로 다른 프록시 객체와 의존관계를 맺고 있는것으로 이해하였는데요, 이게 맞는지 궁금합니다.

=> 프록시 객체는 하나만 생성됩니다. 그리고 필요한 곳에 프록시 객체가 주입됩니다.

 

\2. 또한 이렇게 각각의 프록시 객체와 의존관계를 맺게 되더라도, 이 각각의 프록시 객체가 진짜 MyLogger를 이용할 때에는 여전히 동일한 MyLogger가 사용되는지 궁금합니다.

=> 요청이 들어왔을 때 MyLogger의 프록시 객체는 MyLogger 빈을 찾을테고 없으면 생성합니다. 이후부터는 해당 요청이 응답될 때까지 동일한 MyLogger 빈을 사용하게 됩니다.

 

\1. request스코프는 고객의 http request요청이 있어야지만 생성이 가능하다

2.provider는 provider를 이용한 지연처리, DL을 통해 요청시점까지 기다렸다가 요청시점에 생성한다

 

 

\1. Proxy Mode를 사용했을 때, CGLIB를 활용한 빈 객체는 스프링 컨테이너가 생성될 때 최초로 단 한 번만 생성된다. 프록시 객체가 호출되면, 호출될 때 마다 진짜 객체를 만든다. 위의 내용처럼 이해를 하는 것이 맞을까요? 그리고 싱글톤 스코프처럼 사용 시 문제가 발생할 수 있다는 것은 프록시 객체는 단 한번 생성되나, 진짜 객체는 매번 생성되고 소멸되니 그것에 대한 COST가 비싸다는 것으로 이해를 하면 될까요?

=> 프록시 객체가 호출될 때마다 진짜 객체를 매번 생성할지는 말지는 프록시 객체가 알고 있는 진짜 빈의 스코프에 따라 달라집니다. 싱글톤 빈처럼 사용되는 것이 프록시 객체의 장점이지만 그 뒤에서 실제 동작하는 방식은 스코프(Request, Singleton, Protorype 등)에 따라 달리지기 때문에 싱글톤처럼 사용시 문제가 발생할 수 있다는 것입니다.

 

\2. 이 강의해서 사용된 REQUEST 스코프의 동작 방식이 궁금합니다. 사용된 코드 동작방식을 보면 HTTP REQUEST가 들어오면 REQUEST SCOPE 객체를 만들고 나갈 때까지객체를 유지하고, 나간다면 다음으로 들어온 HTTP REQUEST를 입력받아서 동작하는 것처럼 보입니다. 즉, 항상 HTTP REQUEST SCOPE 객체는 스프링 컨테이너에 하나만 존재하는 것처럼 보입니다. 그런데 예를 들어 정말 동시에 HTTP REQUEST가 들어오면, 어떻게 동작하는지 알 수 있을까요? 동시에 들어오면 HTTP REQUEST SCOPE 빈이 스프링 컨테이너에 다수 개가 생성될 것 같아서... 기존에 컨트롤러와 서비스가 동작하던 것처럼 동작하지 않을 것 같습니다.

 

=> 스코프는 객체가 아니라 빈의 라이프 사이클 범위를 말하는 것입니다. 특정 빈의 스코프가 Request라면 사용자 요청이 서버에 들어올 때마다 빈이 생성되고 요청이 다 처리되면 소멸됩니다. 따라서 다수의 요청이 들어왔을 때 Request 스코프인 빈을 호출하게 되면 각 요청별로 빈이 생성되어 사용됩니다.

 

싱글톤처럼 사용 시 문제가 될 수 있다는 부분의 좀 더 정확한 것이 무엇인지 알려주실 수 있으실까요? 프록시 객체는 어찌되었건 필요한 시점에는 객체가 만들어져 있어, 프로그램 동작 과정에서 문제가 없는 것으로 알고 있습니다.

예를 들어 프록시 객체의 진짜 객체가 싱글톤이라고 했을 때, 싱글톤처럼 사용하는 것은 문제가 없을 것입니다. 프록시 객체의 진짜 객체가 Request 스코프라고 가정하면, 매번 프록시 객체를 호출할 때 마다, 객체를 생성하고 반환해주기 때문에 객체를 생성하는데 많은 비용이 들 것입니다. 그런데 이를 싱글톤 객체라고 착각하고 개발자가 그렇게 설계를 할 경우, 이런 비용 관점에서 문제가 있다고 보는 것이 타당할까요?

말씀하신 '싱글톤처럼 사용 시 문제가 될 수 있다는 것'이의 간단한 예라도 하나 알려주시면 너무 감사드리겠습니다.

=> (상태를 가지지 않는) 싱글톤을 사용하는 이유 중 하나는 멀티스레드 환경에서 객체 재사용성을 높이기 위함입니다. 억지스러울 수 있지만 예를 들어보자면 엄청나게 많은 요청이 쏟아질 때 싱글톤처럼 보이는 프록시 객체를 호출했습니다. 그런데 프록시 객체가 뒤에서 호출한 건 프로토타입 스코프를 가지는 빈이었던 겁니다. 그러면 수많은 빈이 생성될테고 이는 서버에 불필요한 부하를 주게 될 것입니다. 원래 의도는 싱글톤 빈인 줄 알고 열심히 호출했었던 것이니깐요.

 

==> 다수의 요청이 들어오면, 각 요청 별로 빈이 생성되어 사용된다는 것은 이해를 하고 있습니다. 질문이 정확하지 못한 점 죄송합니다. 이렇게 질문드리는것이 제 질문 의도와 맞는 것 같습니다.

ClientA가 Http request를 요청하고, Controller에서 해당 request를 처리하고 있는 상황입니다. 이 때, ClientB에서 Http Request를 요청이 또 들어옵니다. 이 때, ClientA의 http 요청은 반환이 되지 않은 상황입니다. 이런 상황이라면 ClientB의 Request 스코프 객체는 ClientA의 Http 요청이 끝나서 완료될 때까지 생성이 연기될까요? 아니면, ClientA의 요청과는 별개로 ClientB의 Request 스코프 객체가 생성되어 스프링 컨테이너에 관리가 될까요?

=> 이미 알고 계신 것 같습니다. 각 요청(request)별로 빈이 생성되고 컨테이너에 의해 관리됩니다.

 

\3. 한 가지 추가 질문이 있습니다! 혹시 이런 프록시 모드로 생성되는 객체는 스프링 컨테이너가 생성될 때, 단 한번 생성된다고 이해를 하면 될까요?

=> 네, 프록시 객체는 한 번만 생성됩니다.

 

 

 

 

주입되어있는 프록시객체는 진짜 객체의 클래스를 상속받았으므로, request가 들어올 때에도 그대로 자리를 유지하여 사용된다. 단, request가 들어올 때 setter 같은 게 동작하여 프록시객체를 진짜스럽게 만들어준다.

스프링 빈에는 객체를 상속받은 프록시가 등록됩니다. (별도의 설정이 없을경우)

프록시를 직접 구현하는 예제를 정리한 블로그 링크를 남기니 참고 하시기 바랍니다.

https://mangkyu.tistory.com/m/175?category=761302

 

 

ProxyMode로 사용하게 되면 PrototypeBean 내부 메서드를 호출할 때마다 새로운 빈이 생성됩니다.

addCount();, getCount();를 순차적으로 호출하게 될텐데

addCount() 호출할 때 빈이 새롭게 생성되고

getCount() 호출할 때 빈이 새롭게 생성됩니다.

실제로 addCount() 로직을 탑니다만 getCount()를 호출할 때에는 새롭게 생성된 빈에서 호출하기 때문에 count가 0으로 보이는 것입니다.

addCount(), getCount() 내에서 prototypebean에 대한 정보를 출력하는 코드를 작성해보시면 서로 다른 빈임을 확인하실 수 있습니다.

Protytype에서의 ProxyMode를 사용할 때에 대한 동작은 아래 블로그를 참고해주세요.

https://renuevo.github.io/spring/scope/spring-scope/

 

감사합니다. 다시 듣고왔는데 configuration부분 에서는 이용해서 스프링이 가짜 객체를 만들어 싱글톤을 보장하는 것이기 때문에 cglib기술을 사용하는것이고 provider를 사용해야 할 때 나중에 생성될 객체를 위해 가짜 객체를 만들어놓기 위해 cglib기술을 사용하는 것이라고 보면 될까요?

Configuration에서는 스프링이 의존관계 주입을 위해서 메서드를 호출할 때 싱글톤을 보장하기 위해서 Configuration 클래스를 가짜 객체로 만들어줍니다^^

 

프록시는 싱글톤으로 되어있지만 진짜 빈을 찾을때는 싱글톤 객체가 되면 안될거 같은데 프록시가 진짜 빈을 요청 할때마다 스프링 컨테이너는 요청이 들어올때마다 새로운 request scope 객체를 생성해서 넘겨주나요 ?

스프링 컨테이너는 동일한 하나의 HTTP 요청안에서 처음 조회시에는 빈이 없으니 생성해서 반환하고, 두번째 조회시에는 앞서 빈을 만들어 두었으니 만들어둔 빈을 반환합니다.

객체가 사라지는 시점은 사용자에게 클라이언트에게 리턴값을 준 후 없어지나요?..

네 맞습니다

 

코드 다시 원상 복구!

package hello.core.logdemo;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {
    /**
     * 비니지스 로직 이 있는 서비스 계층
     * 여기서도 로그를 출력
     *
     * request scope르 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면
     * 파라미터가 많아서 지저분해진다.
     *
     * 다른 문제로는 reqeustURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다.
     *
     * 웹과 관련된 부분은 컨트롤러까지만 사용해야된다.
     * 서비스계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
     *
     * request Scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고
     * MyLogger의 멤버 변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
     *
     */
    private final MyLogger myLogger;
//   private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    //myLogger 의 log 사용.
    public void logic(String id){
//        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.log("Service id = " + id);
    }
}

 

package hello.core.web;


import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
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;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;
  //  private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @RequestMapping("log-demo") //log-demo 요청이 오면
    @ResponseBody //뷰화면 없이 그냥 바디부에 보내버림
    //HttpServletRequest => 자바에서 제공하는 표준 서블릿 규약에 의한
    // http request 정보를 받을 수 있음
    public String logDemo(HttpServletRequest request){
        //HttpServletRequest 로 http request 정보 받앗으므로
        //getRequestURL로 요청된 url 알 수 있음
        String requestURL = request.getRequestURI().toString();
      //  MyLogger myLogger = myLoggerObjectProvider.getObject();
        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId"); //일단 아무로직 만듬
        return "OK";
    }
}

 

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$d7393e42
[07ec8e0b-b299-4395-aebf-3245b535fbe9] request scope bean create:hello.core.common.MyLogger@5650b334
[07ec8e0b-b299-4395-aebf-3245b535fbe9[/log-demo][controller test]
[07ec8e0b-b299-4395-aebf-3245b535fbe9[/log-demo][Service id = testId]
[07ec8e0b-b299-4395-aebf-3245b535fbe9] request scope bean close:hello.core.common.MyLogger@5650b334

 

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$d7393e42

CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.

@Scope의 proxyMode = ScopedProxyMode.TARGET_CLASS 를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서 MyLogger를 상속받은 가짜 프록시 객체를 생성한다.

MyLogger 클래스가 아니라 EnhancerBySpringCGLIB이라는 클래스로 만들어진 객체가 대신 등록된다.

그리고 getBean을 하여서 MyLogger를 찾아도 프록시 객체가 조회된다.

따라서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.

 

 

즉,

클라이언트A ---> MyLoggerProxy(프록시 객체) ---> 진짜 myLogger.logic() 호출 => request scope (A전용) x03

클라이언트B ---> MyLoggerProxy(프록시 객체) ---> 진짜 myLogger.logic() 호출 => request scope (B 전용) x04

 

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임로직이 들어있다.

즉, 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.

클라이언트가 myLogger.logic()을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.

 

가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게 동일하게 사용할 수 있다.(다형성)

 

 

정리

  • CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.
  • 가짜 프록시 객체는 실제요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 있다.
  • 가짜 프록시 객체는 request scope와는 관계가 없다. 가짜이고, 내부에 위임로직만있다. 싱글톤 처럼 동작한다.

 

  • Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진자 객체 조회를 꼭 필요한 시점까지 지연처리한다는 것!
  • 단지 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 다형성과 DI컨테이너가 가진 강점이다.
  • 꼭 웹 스코프가 아니여도 프록시를 사용할 수 있다.

 

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

조회 빈이 2개 이상 - 문제

@Autowired는 TYPE으로 조회한다.

@Autowired
private DiscountPolicy discountPolicy

하지만

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

해당 타입이 2개 스프링 빈으로 등록 되어 있다.....

그럼 과연 누굴 가져와야하느냐...

NoUniqueBeanDefinitionException 예외가 발생한다.

NoUniqueBeanDefinitionException: No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy

보면 하나의 빈을 기대했는데 두개의 빈이 발견되었다고 알려준다.

 

이때 하위타입으로 지정할 수도 있다.

@Autowired
private FixDiscountPolicy discountPolicy

하지만 이러면 DIP를 위배하고 유연성이 떨어진다.

또한 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 두개 있으면 해결이 안됨!

설정 구성에서 스프링 빈을 수동 등록해서 해결해도되지만, 의존관계 자동 주입에서 해결하는 여러 방법이있다.

 

 

@Autowired 필드 명, @Qualifier, @Primary

조회 대상 빈이 2개 이상인 경우엔

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

 

@Autowired 필드명

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

@Autowired
private DiscountPolicy discountPolicy

위 코드에서 필드 이름을 바꾼다.

@Autowired
private DiscountPolicy rateDiscountPolicy

 

필드명 rateDiscountPolicy 이 정상 주입된다.

따라서 먼저 타입 매칭을 한 다음, 여러빈이 있을때 그중 같은 필드명을 가진 빈을 가져온다.

(여기선 fixDiscountPolicy, rateDiscountPolicy 두개가 있었으므로 DiscountPolicy 타입을 찾고 그중 rate를 가져온다.)

즉, 해당 타입 빈 찾음 => 여러개가 나오니까 필드명이름이랑 매칭되는 거 가져옴

 

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

DiscountPolicy중 mainDiscountPolicy를 가져온다고 알 수 있다.

(구현체에 의존하게 되므로 DIP 위반이지만 트레이드 오프가 있다 생각해야댐)

혹은 생정자 말고 수정자 자동 주입으로 사용하면

 

@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
 return discountPolicy;
}

 

즉, 타입 앞에 @Qualifier("이름")을 붙여서 사용하면 된다.

만약 @Qualifier("mainDiscountPolicy")로 등록된 스프링 빈이 없으면?!?!

그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.

하지만 @Qualifier는 스프링 빈이름을 찾는게 아니라 @Qualfier를 찾도록 하는 것이 좋다.

 

정리하면

@Qualifier끼리 매칭 => 없으면 빈이름 매칭 => 없으면 NoSuchBeanDefinitionException 예외 발생

 

 

@Primary 사용

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

@Autowired 시에 여러 빈이 매칭되면 @Primary를 가져온다.

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

이러면 DiscountPolicy를 가져올때 RateDiscountPolicy가 우선권을 가지므로 Rate를 가져온다.

(따라서 @Qualifier와 같이 클라이언트 코드를 수정할 필요가 없다.)

그리고 또한 Qualifier의 단점은 모든 코드에 @Qualifier를 붙여야한다!!

 

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

등록할때와 그 스프링 빈을 사용할때 다 붙여야댐....

 

@Primary, @Qualfier 활용

예를 들어 코드에서

자주 사용하는 메인 DB의 커넥션을 획득하는 스프링 빈,

특별한 기능으로 가끔 사용하는 서브 DB의 커넥션을 획득하는 스프링 빈

이 있다고 가정.

 

메인DB 커넥션을 획득하는 스프링빈 => @Primary

서브 데이터베이스 커넥션을 획득하는 스프링 빈 => @Qualifier 사용하여 명시적 지정

과 같이 하면 깔끔해진다.

 

이때, 메인 DB의 스프링 빈을 등록할때 @Qualifier 를 지정해주는 것은 상관없다.

 

  • @Primary, @Qualfier 둘 중 우선순위

    스프링은 자동보다 수동이, 넓은 범위보단 좁은 범위가 우선순위가 높다.

    따라서 더 명시적인 @Qualifier가 우선권이 높다.

 

 

참고)

discountPolicy에 두 개의 빈이 찾아져버리므로, 특정 빈을 찾을 수 있도록 인자의 파라미터 이름을 수정해야했습니다. (@Autowired 필드명 방식)

이것이 개방-폐쇠 원칙을 못지킨 것이 아닌가 하는 의문이 들었습니다.

-> 네 맞습니다. 클라이언트 코드를 고쳐야 하기 때문에 OCP를 지키기 못했습니다.



@Quilifier 혹은 @Primary 어노테이션을 붙이기 위해 구현체의 클래스를 찾아가서 수정해줘야하는 것 같습니다.

-> 기존 구현 클래스의 애노테이션도 변경하지 않으면 더 좋겠지만, 이 부분까지는 컴포넌트 스캔의 한계입니다. @Bean을 사용하면 확실하게 되지만 약간은 불편하지요. 따라서 둘의 트레이드 오프로 이해하시면 됩니다.

 

어노테이션 직접 만들기

@Qualifier 사용시 만약

@Qualifier("mainDiscountPolicy")가 아니라

@Qualifier("mmainDiscountPolicy") 처럼 오타가 났다면?!?!?!

문자를 적으면 컴파일시 타입 체크가 되지 않는다. => 오류 찾기 힘들다...

따라서 직접 어노테이션을 만들어서 컴파일시 체크되게 한다.

package hello.core.annotation;


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

import java.lang.annotation.*;

//ctrl+n 으로 Qualifier 어노테이션꺼 다 긁어와주면댐
//즉 @Qualifier 설정 다 가져옴
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy") //적어줌 여기서
public @interface MainDiscountPolicy {
}

그러면 기존에 사용했던 방식을 보자

package hello.core.discount;

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

//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
@Qualifier("mainDiscountPolicy") //이렇게 직접 문자로  적어줬다. <==== 따라서 오타가 나면 찾기 힘들다..
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

 

하지만 만든 어노테이션을 적용하면

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.stereotype.Component;

//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
//@Qualifier("mainDiscountPolicy")
@MainDiscountPolicy // <========================= 요ㅕ기 수정
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

@MainDiscountPolicy 라고 붙여주므로 오타가 나도 컴파일 시점에서 잡아주니까 찾기 쉽다!!

신! 난! 다!

 

또한 DiscountPolicy를 사용할 때 보면

//생성자 자동 주입
@Autowired
//@MainDiscountPolicy 사용
public OrderServiceImpl(MemberRepository memberRepository,@MainDiscountPolicy DiscountPolicy discountPolicy) {
 	this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}


//수정자 자동 주입
@Autowired
//@MainDiscountPolicy 사용
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
 	return discountPolicy;
}

 

사실 어노테이션에는 상속이라는 개념이 없다

그러면 어떻게 MainDiscountPolicy 어노테이션이 @Qualifier와 같은 역할을 하게 되었을까...

바로 스프링이 제공해주는 기능이다. (갓갓...)

따라서 @Qualifier뿐만 아니라 다른 어노테이션들도 함께 조합해서 사용 가능하다.

(@Autowired도 재정의 가능하다..)

킹치만,,, 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분멸하게 재정의하면 유지보수에 혼란을 준다..

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

롬복과 최신 트렌드

막상 개발을 해보면, 대부분이 다 불변이다. 따라서 생성자에 final 키워드를 사용하게 된다.

근데 위에서 봣듯이,

  • 생성자를 만든다
  • 주입 받은 값을 대입하는 코드를 만든다

귀찮쥬??

필드 주입처럼 편한 건 없을려나... (필드 주입하면 생성자 없이 바로 들어오니까)

 

아래 코드를 최적화 해보자.

@Component
public class OrderServiceImpl implements OrderService {
    
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

롬복 라이브러리를 적용하면 된다.

롬복 라이브러리가 제공하는 @RequireArgsConstructor 기능을 사용하면 final 이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

즉, 생성자를 자동으로 만들어준다.(final이 붙은 필드들을)

 

start.io.spring으로 프로젝트 생성시 add dependecy로 롬복을 선택하면 자동 적용된다.

직접 사용하는 방법은...

build.gradle에 해당 코드 추가

//lombok 설정 추가 시작
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
//lombok 설정 추가 끝


// 그리고 dependency안에 추가
//lombok 라이브러리 추가 시작
 compileOnly 'org.projectlombok:lombok'
 annotationProcessor 'org.projectlombok:lombok'
 testCompileOnly 'org.projectlombok:lombok'
 testAnnotationProcessor 'org.projectlombok:lombok'
 //lombok 라이브러리 추가 끝

 

그리고 @RequiredArgsConstructor어노테이션을 붙이면 끝!

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

쉽쥬?

 

롬복이 자바의 어노테이션 프로세서라는 기능을 이용하여 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class를 열어보면 생성자가 추가된걸 알 수 있다.

 

package hello.core;

import lombok.Getter;
import lombok.Setter;

//롬곡 라이브러리가 자동으로 setter, getter를 만들어준다.
@Setter
@Getter
public class HelloLombok {
    private String name;
    private int age;

    public static void main(String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("asdfasdf");


        String name = helloLombok.getName();
        System.out.println("name = " + name);
    }
}

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

의존관계 자동주입


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

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

 

생성자 주입

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

(위에서 했던 방식들)

생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다. (그 다음부턴 호출 안되게 막을 수 있다.)

불변, 필수 의존관계에 사용한다. => OCP 지킴

    @Component//OrderServiceImpl가 스프링 빈에 등록됨 => 객체 인스턴스가 생성되니까 생성자도 호출!
    public class OrderServiceImpl implements OrderService {
        //private final로 선언됨 => 무조건 값을 세팅해줘!!! 라고 알리는 격임
        //따라서 무조건 값이 있어야댐!!! 아니면 null임
        //외부에서 값 변경이 불가! => setter, getter가 없음
        private final MemberRepository memberRepository;
        private final DiscountPolicy discountPolicy;
        
        
        //생성자가 여기 있음! 
        //Autowired이므로 스프링 컨테이너에서 스프링 빈을 꺼내서(리포지토리, 할인정책)을 주입시켜줌
        // 그리고 왠만하면 생성자에 있는 파라미터들을 다 넣어주는게 좋음! 아니 그래야댐
        @Autowired
        public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
                discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }

 

생성자 주입은 빈을 등록하면서 같이 일어난다.

-> 빈을 등록해야하니까!

빈을 등록?? => 객체 인스턴스를 생성해서 스프링 컨테이너가 들고 있음!!

객체 인스턴스 생성?? => 객체를 생성해야하니까 생성자 호출!!

 

 

 

생성자가 하나인 경우는 Autowired 생략 가능하다. 물론 스프링 빈에만 해당댐

(@Component 햇으니까 스프링 빈임)

    @Component
    public class OrderServiceImpl implements OrderService {
        private final MemberRepository memberRepository;
        private final DiscountPolicy discountPolicy;
       // @Autowired
        public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
                discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }

 

 

 

 

수정자 주입(setter 주입)

setter라 불리는 수정자 메서드로 의존관계를 주입함!

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

자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

    @Component
    public class OrderServiceImpl implements OrderService {
        //final이 아님 => 값 변경 되어도 됨
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
        
        //생성자로 주입 받는것이 아니라 setter 메서드로 주입받음
        @Autowired
        public void setMemberRepository(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
        @Autowired
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }
    }

** @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

=> 예를 들어 MemberRepository 쓸지 안쓸지 잘 모름....

    @Component
    public class OrderServiceImpl implements OrderService {
        //final이 아님 => 값 변경 되어도 됨
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
        
        //쓸지 안쓸지 몰라서 false로 해놓음, 따라서 주입할 대상이 없어도 오류가 안남
        @Autowired(required = false)
        public void setMemberRepository(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
        @Autowired
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }
    }

 

 

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

 

필드 주입

말그대로 필드에 주입!

하지만 안티패턴이다...

코드가 간결하지만, 외부에서 변경이 불간으해서 테스트하기 힘들다는 단점이있다.

DI 프레임워크가 없으면 아무것도 할 수 없다. => 순수 자바코드로는 못한다.

즉, 사용하지 말자!!!!!!!!

주로 실제코드와 관계없는 테스트코드나, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 쓴다.

@Component
public class OrderServiceImpl implements OrderService {
    
 //=> 값을 집어 넣을 수 없다... 어케넣어 setter도 없고 일반 메서드도 없고, 생성자로도 못넣고..
 @Autowired
 private MemberRepository memberRepository;
 @Autowired
 private DiscountPolicy discountPolicy;
    
 //값을 못 집어넣어버림 
 public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  
    
}
@Test
void fieldInjectionTest(){
    //이렇게 new 임의로 생성한 객체는 Autowired 되지 않음
    //Autowired는 스프링 빈에 등록된 객체를 연결하는것이므로
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder( ~~~ )
    
    //할인 정책과 멤버리포지토리를 못 넘겨줌....
    //set 메소드를 만들어서 넣어 주거나 해야댐
    //orderService.setRepository(new MemoryMemberRepositroy) 처럼
}

==> 필드 인젝션은 결국 값을 넣으려면 setter가 필요하다.

즉, @Autowired로 땡겨온다곤 햇는데 넣어 주질 않으니 어케 그 값에 넣으란 말이냐....... 결국 setter와 같은게 필요한거지..

그럼 왜 필드 인젝션 씀? 그러므로 쓰지말자..

 

** 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다 => @Autowired는 스프링 빈에 등록된 걸 가져오는건데 스프링 컨테이너가 없는데 어케 가져와?!?! , 스프링 컨테이너 = ApplicationContext

예시)

@Test
    @DisplayName("컴포넌트 스캔으로 빈이 등록되었는지 확인, 의존관계도 확인")
    void basicScan(){
        //스프링 컨테이너에 빈들이 등록되었는지 확인 해보면된다.
        //설정정보로 스프링컨테이너 불러오고
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
//        MemberService memberService = ac.getBean("memberService", MemberService.class);

        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }

 

 

** @Bean을 사용하는 경우에, 파라미터에 의존관계는 자동 주입된다.

수동 등록시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결할 수 있다.

    @Bean
    OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy
            discountPolicy) {
        new OrderServiceImpl(memberRepository, discountPolicy)
    }

 

 

 

일반 메서드 주입

일반 메서드를 통해서 주입받을 수 있다. (사실상 수정자 주입이랑 같다..)

한번에 여러 필드를 주입 받을 수 있고 일반적으로 잘 사용하지 않는다.

 

    @Component
    public class OrderServiceImpl implements OrderService {
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
        
        //일반 메서드 init를 통해서 주입 받음
        @Autowired
        public void init(MemberRepository memberRepository, DiscountPolicy
                discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }

** 의존 관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.

스프링 빈이 아닌 Member 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

(스프링 컨테이너에 빈으로 등록되어 있어야지 가져오지, 등록이 안되었는데 어케 가져와?!)

 

 

 

순수한 자바로 테스트한다는 말을 잘 생각해보시면 좋습니다. 스프링 컨테이너를 실행하지 않은 상태(필드 주입을 위한 @Autowired가 동작하지 않음)에서는 생성자나 수정자가 없으면 필드에 구현체를 넣을 방법이 없습니다.

즉, 필드 주입을 사용하고 있는 클래스를 테스트하려면 스프링 컨테이너를 통해 의존관계가 설정되어야 합니다. 만약, 필드 주입을 사용하는데 스프링 컨테이너를 사용하지 않는다면 Null Pointer Exception이 발생할 것 입니다. 해당 필드에 구현체가 존재하지 않기 때문입니다.

따라서 생성자 주입을 사용하게 되면 스프링과 결합된 코드(테스트를 위해 스프링 컨테이너를 실행해야 하는 코드)가 아닌 순수한 자바 코드(스프링 컨테이너를 실행하는 게 없는 코드)로 의존관계를 주입하고 테스트를 할 수 있습니다.

 

순수한 자바로 테스트한다는 말은 "스프링 컨텍스트"를 띄우지 않은 상태에서 하는 테스트를 말하는 것입니다.

그렇기 때문에 순수한 자바로 테스트를 진행하게 되면 필드주입은 발생하지 않습니다. 필드 주입은 스프링 컨텍스트에서 @Autowired를 감지하여 해당 필드에 들어갈 빈을 컨테이너로부터 가져와야 합니다. 그러나 스프링 컨텍스트를 사용하지 않기 때문에 필드주입이 발생하지 않습니다. 마찬가지로 생성자 주입도 "자동"으로 일어나지 않습니다.

다만, 생성자 주입의 경우 스프링 컨텍스트를 사용하여 자동으로 주입되지 않지만 수동으로 의존관계를 주입할 순 있습니다. 직접 구현체를 생성하고 생성된 구현체를 생성자에 전달하면 되니깐요.

순수 자바코드 테스트 에서는 ApplicationContext를 사용하지 않는다.

-> 스프링 컨테이너가 없다.

-> Bean을 관리하지 않는다.

-> @Autowired 포함 @Bean, @Configuration도 동작하지 않는다.

 

 

결국 필드주입은 스프링 컨텍스트를 의존해야 주입받을수있어서 순수자바로 테스트를 못하지만, 생성자 주입의 경우 현재 강의의 초기내용처럼 AppConfig를 통해 주입을 받아 테스트를 할수있기때문에 순수자바로 테스트를 했다고 볼수있다.

라고 이해했습니다. 맞을까요?

꼭 AppConfig가 아니더라도 됩니다. 의존관계에 필요한 객체를 생성해서 생성자의 파라미터로 넘겨주기만 하면 됩니다. 나머진 이해하신 부분이 맞습니다.

 

필드주입의 경우 필드에 @Autowird 어노테이션만 붙이면 된다고 하셨는데

OrderServiceImpl 이라는 클래스에서

@Autowird private MemberRepository memberRepository 이렇게 가지고 있을 경우

Spring에서 MemberRepository 형태로 등록되어 있는 빈을 꺼내서 자동으로 저기에 주입시켜주는 것이 맞나요? 강의의 예제의 경우 MemoryMemberRepository 클래스 정의 상단에 @Component 어노테이션이 있기에 처음에 component scan 방식으로 해당 컴포넌트를 인식하여 MemoryMemberRepository를 빈에 등록을 한 후 OrderServiceImpl에서 @Autowird private MemberRepository memberRepository 해당 부분을 본 후 MemoryMemberRepository도 MemberRepository 인터페이스를 구현한 것이기에 MemberRepository 형태로 빈에서 찾을경우 찾은 결과에 속할 수 있기에

그렇게해서 찾은 MemoryMemberRepository를 OrderServiceImpl의 memberRepository로 의존관계를 주입해주는 것이 맞나요??

만약 맞다면 MemberRepository를 구현하는 메모리멤버리파짓토리와 디비멤버리파지토리가 모두 bean 에 등록되어있을 경우에는 어떻게 작동하게 되는건가요?

 

MemberRepository 인터페이스를 구현한 모든 것이 주입의 대상이 됩니다. 따라서 이 경우 충돌이 발생합니다.

바로 조금 뒤에 이런 문제를 어떻게 해결하는지 자세히 설명드립니다^^

감사합니다.

728x90
반응형
블로그 이미지

아상관없어

,
반응형

컴포넌트 스캔


 

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

이때까진 @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
//컴포넌트 스캔을 사용!!
//기존 AppConfig도 컴포넌트 스캔대상이므로 스프링 컨테이너에 등록되지 않게 해주어야한다.
//제외할 필터로 어노테이션 타입의 Configuration이 있는 클래스를 컴포넌트 스캔하지 않게 한다.
//왜냐면 AppConfig는 @Configuration 가 붙어있으니까 이거 빼줄라고
//그리고 AppConfig는 수동으로 빈을 등록하는거니까 자동 등록 테스트를 위해 빈으로 등록하지 않는다.
@ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
    //아무 내용이 없어도 알아서 다 긁어서 스프링빈으로 찾아낸다.
}

 

 

자 이제 다시보면 (하다보니 주석에 다적어버려서 그냥 그대로 올려봣다.)

package hello.core;

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

//설정정보니까
@Configuration
//컴포넌트 스캔을 사용!!
//기존 AppConfig도 컴포넌트 스캔대상이므로 스프링 컨테이너에 등록되지 않게 해주어야한다.
//제외할 필터로 어노테이션 타입의 Configuration이 있는 클래스를 컴포넌트 스캔하지 않게 한다.
//왜냐면 AppConfig는 @Configuration 가 붙어있으니까 이거 빼줄라고
//그리고 AppConfig는 수동으로 빈을 등록하는거니까 자동 등록 테스트를 위해 빈으로 등록하지 않는다.
@ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
    //아무 내용이 없어도 알아서 다 긁어서 스프링빈으로 찾아낸다.
    //어떻게? => 위를 보면 일단 컴포넌트 스캔을 사용한다고 적었다.
    //그러면 컴포넌트들을 찾아야지! 어떻게? => @Component를 붙여서 알려주면됨!
    //빈으로 등록할 객체들을 컴포넌트로 등록!!
    //빈에는 구현체들이 등록이 되어있어야겟지? 그러면 구현체들에 @Component

    //그러면 스프링 컨테이너에 빈으로 등록은 되었다만... 어떻게 의존관계를 연결시키지... 하아..
    //바로 @Autowired 로 자동연결해주면 끄으읕 => 구현체에서 필요한 객체 인스턴스를 가져올 곳에 쓰면댐
}

/**
 * excludeFilters에 대해 다시 설명하면
 * @Configuration 어노테이션을 보면 @Component를 사용한다.!
 * 따라서 @Configuration이 있는 것도 다 긁어온다.
 * 그러므로 앞의 예제들에서 만든 AppConfig, TestConfig 등등 다 긁어와버리니까 제외시켜버렷다.
 *
 * (내 생각이긴한데 컴포넌트 스캔을 사용하는 설정파일은 AutoAppConfig뿐이니까
 * 자기 자신이 설정파일이지만 자기 자신을 제외한 나머지 설정들을 컴포넌트 스캔시 등록 안해버리는거같다.
 * 나중에 검색해봐야지...)
 */

 

즉, 컴포너트 스캔을 한다고 선언을 했다. 그러면 뭘 해야지??

컴포넌트 스캔이 찾을 수 있게 @Component라고 알려줘야댐

어디에?? => 스프링 빈으로 등록할 객체를!

어떤거지? => 실제 사용될 것들이니 실제 사용할 구현체이지

 

그러면 필요한 객체들을 @Component 을 붙여서 스프링 빈으로 등록을 했다.

킹치만.. 설정정보를 다시보면 아무내용도 없다. 그러면 의존관계 주입은 누가하냐???

@Autowired로 의존관계를 주입힌다.

어떻게? 해당 구현체가 필요한 곳에서!

다시 역할, 구현을 구분한다는거에 집중하자 => 배우(자기 역할)는 연기만하면 되지 상대배우(구현)가 누가될지는 몰라도 자기 역할만 하면된다.!

즉, 어떤 리포지토리가 쓰일진 모르지만, 그냥 리포지토리를 불러오면 된다! => 따라서 이러한 곳에 @Autowired로 의존관계 주입 !!

 

자 다시 정리하면, 구현체 들이 스프링빈으로 등록되어야하니

구현체(xxxIpml)들에 @Component!

그리고 구현체들이 다른 구현체가 필요할때 어떤 구현체가 올진 모르겟고 해당 역할만 필요로함! => @Autowired

 

package hello.core.member;

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

@Component //컴포넌트 스캔하여 빈으로 등록하기 위해서 알려줌
//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{

    //배역 배우로 생각할때
    //MemberRepository memberRepository = new MemoryMemberRepository();

    //생성자로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;

    //리포지토리가 필요하네? 근데 어떤 저장소가 올진 내가 신경쓸게 아니지!
    //따라서 의존관계 주입!
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

package hello.core.member;

import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

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

@Component//빈으로 등록하기 위해 컴포넌트라고 알려줌
//저장소 필요
public class MemoryMemberRepository implements MemberRepository{

    //일단 임시 저장소 = 메모리 사용
    private static Map<Long, Member> store = new HashMap<>();

    @Override
    public void save(Member member) {
        store.put(member.getId(),member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);//store는 Map이므로 키값으로 member찾음
    }
}
package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;

//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

package hello.core.order;

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


@Component //빈 등록
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //생성자 주입기법으로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    //역할, 구현 구분! 배우는 누가 캐스팅될지 모르고 걍 연기만 하면댐
    //여기선 어떤 리포지토리, 어떤 할인정책이 올지 모름
    //그냥 리포지토리를 쓰고 할인정책을 쓰면댐!!!
    //따라서 의존관계가 주입되어야되므로 @Autowired
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

자 이러면 이제 뭘해봐야지? => 테스트로 확인

테스트를 해보아서 진짜 다 스프링 빈으로 컨테이너에 등록이 되고 의존관계주입 즉 연결되었는지 확인 해보자!

 

package hello.core.scan;

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

public class AutoAppConfigTest {

    @Test
    @DisplayName("컴포넌트 스캔으로 빈이 등록되었는지 확인, 의존관계도 확인")
    void basicScan(){
        //스프링 컨테이너에 빈들이 등록되었는지 확인 해보면된다.
        //설정정보로 스프링컨테이너 불러오고
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        
        //요렇게하면 안된다. 왜냐면 안알랴줌... 뒤에서 설명해줄게...
//      MemberService memberService = ac.getBean("memberService", MemberService.class);

        //타입으로 찾음
        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

 

위 주석에서 안알랴줌 이유를 설명하자면..

먼저 컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 봐야된다.

 

@Component 를 붙이면 이 어노테이션이 붙은 클래스들을 스프링 컨테이너에 스프링 빈으로 등록시킨다.

(스프링 컨테이너는 스프링 빈을 생성해서 가지고 있음)

등록 될땐, 자바빈 규약에 의해 xxxBean 와 같은 형식으로 빈이름이 지정된다.

(스프링 빈의 기본이름은 크래스 명을 사용하되, 맨 앞글자 소문자를 사용함)

예를 들면 @Component가 붙은 MemberServiceImpl을 보면 memberServiceImpl과 같이 빈이름이 설정된다.

 

자동 의존관계 주입은 @Autowired를 설정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.

(즉 역할만 알고 있으면댐 구현체가 뭔진 몰라도 알아서 가져다줌)

이때 해당 스프링 빈을 찾는 방식은 타입이 같은 빈을 찾아서 주입한다.

 

앞서 getBean(MemberRepositroy.class)를 생각해보자 이러면 구현 객체인 MemoryMemberRepository가 나왔다.

따라서 MemberRepositroy가 필요하면 스프링 컨테이너에서 MemberRepositroy타입으로 등록된 빈을 찾아서 주입해준다.

 

킹치만 같은 타입이 여러개라면?!?!?!?! => 충돌이 일어나겟지... 뒤에서 설명한다.

728x90
반응형
블로그 이미지

아상관없어

,
반응형

@Configuration과 싱글톤


 

AppConfig를 다시보자..

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
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 MemberService memberService(){
        return new MemberServiceImpl(memberRepository()); //memberRepository 호출 => 생성
    }

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

    @Bean
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());//memberRepository호출 => 생성
    }

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

}

//과연 memberRepository => new MemoryMemberRepository();가 두번이나 되었을까??
//?? 모를땐 Test코드로 assertThat Same으로 참조값을 비교해보자...

과연 new MemoryMemberRepository();를 두번하여 각각 다른 객체 인스턴스가 만들어질까?

따라서 싱글톤이 깨질까?

결과적으로 말하면 서로 같은 객체 인스턴스이고, 호출도 한번만 되어진다.

 

??????? => 마! 이게 스프링이다....

 

 

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


스프링 컨테이너 = 싱글톤 레지스터

따라서 스프링 빈이 싱글톤이 되도록 보장해주어야한다.

하지만, 스프링이 자바코드까지 어떻게 하기는 어렵다.

자바코드상으론 분명 3번 호출되어야된다!!

어떻게 이걸 해결했을까???

 

바로 스프링이 클래스의 바이트코드를 조작하는 라이브러리를 사용했다.

모든 비밀은 @Configuration을 적용한 AppConfig에 있다.

 

    @Test
    void configurationDeep(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        
        //AppConfig또한 스프링 빈으로 등록이 되어진다.
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean);
    }

결과는......

순수한 클래스일 경우 bean = hello.core.AppConfig와 같이 출력되어야횐다.

"bean = hello.core.AppConfig$$EnhancerBySpringCGLIB$$4ab6406e@7e276594"

??

바로 CGLIB라는 바이트 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고

그 클래스를 스프링 빈으로 등록했다.

 

AppConfig ->등록X

AppConfig 를 상속받은 클래스(스프링이 조작해둠) -> 등록

 

즉, 그 다른 임의의 클래스가 싱글톤을 보장해준다.

아마도..

    @Bean
    public MemberRepository memberRepository() {

        if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
            return 스프링 컨테이너에서 찾아서 반환;
        } else { //스프링 컨테이너에 없으면
            기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
            return 반환
        }
    }

 

@Bean이 붙은 메서드이면 스프링 빈으로 존재하면 그 빈을 반환하고, 존재하지 않으면 그때 생성해서 반환하는 코드가 동적으로 만들어진다.

 

여기서,, 그러면 @Configuration을 적용하지 않고 @Bean만 적용하면?

=> 싱글톤이 보장 안되어짐...

따라서 memoryMemberRepository가 여러번 호출되고 각 각 다 다른 인스턴스가 만들어진다.

 

즉, 결론은 스프링을 사용하면 다 해결된다.

따라서 구성영역이면 @Configuration을 사용하자.

 

스프링 빈으로 등록되기 때문에 @Configuration이 있든 없든 스프링빈에 등록될 때는 싱글톤으로 생성이 되는 것은 맞습니다.

그런데 문제는 다음 부분입니다.

 

@Bean

MemberService memberService() {

return new MemberServiceImpl(memberRepository());

}

@Bean

public MemberRepository memberRepository() {

return new MemoryMemberRepository();

}

memberSerivce를 스프링빈으로 등록할 때 위의 memberService() 메서드가 호출됩니다. 이 메서드가 호출되면 new MemberServiceImpl()을 생성하면서 memberRepository() 메서드를 호출해서 의존관계를 찾습니다.

그런데 @Configuration이 없으니 memberRepository() 메서드가 스프링 코드인지 아닌지 인식하지 못하고, 순수한 자바 메서드로 호출 해버립니다. 그러면 내부에서 new MemoryMemberRepository()가 호출되는 것이지요. 결국 스프링의 도움을 받지 못하고 스프링 빈이 아닌 순수 자바로 생성된 new MemoryMemberRepository()가 주입되어 버립니다. 이것은 스프링이 관리하는 객체가 아니라 방금 발생한 메서드 호출로 새로 생성된 객체입니다!

결국 다음과 같이 되는 것이지요.

@Autowired MemberService memberService; //여기 내부에 들어있는 memberRepository는 스프링 빈이 아님 단순히 new로 생성한 MemoryMemberRepository임

@Autowired MemberRepository memberRepository; //스프링 빈이 등록한 memberRepository

memberService.getMemberRepository() != memberRepository //따라서 둘은 다른 객체

 

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

싱글톤 컨테이너



 

스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.

대부분 스프링앱? => 웹 애플리케이션

웹 애플리케이션? => 대부분 여러 고객이 동시에 요청함

 

스프링이 없는 순수 DI 코드는 memberService를 요청할때 마다 객체를 각각 생성해서 새로 만들어주게된다.

코드로 보자.

package hello.core.singleton;

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

public class SingletonTest {

    @Test
    @DisplayName("스프링이 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        //1. 조회
        MemberService memberService1 = appConfig.memberService();
        //2. 조회
        MemberService memberService2 = appConfig.memberService();
        //두 객체 비교
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }
}

매번 새로운 객체를 만드니까 메모리 낭비도 된다.

초당 만건의 요청이 들어오면 멤버 서비스 객체가 초당 만개가 생성된다....

따라서 딱 하나의 객체만 만들고 서로 공유하도록 설계하면 된다. => 이것이 싱글톤 패턴

 

 

- 싱글톤 패턴


클래스의 인스턴스가 딱 하나만 생성되도록 보장하는 디자인 패턴!

따라서 객체 인스턴스가 2개 이상 생성되지 못하게 막아야함!!!

How?? => private 생성자로 외부에서 객체를 생성하지 못하게 막아버림, 외부에서 new로 생성 불가!

 

싱글톤 패턴 예제

package hello.core.singleton;

public class SingletonService {
    //How? => 생성자를 private로 막아버림
    
    
    //먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
    private static final SingletonService instance = new SingletonService();
    
    //객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
    public static SingletonService getInstance(){
        return instance;
    }
    
    //이제 생성자를 막아보자 private로
    private SingletonService(){
        
    }
    
    //로직 테스트 위해서
    public void logic(){
        System.out.println("싱글톤 객체 호출됨!!");
    }
    
}

 

이제 테스트!!

package hello.core.singleton;

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

public class SingletonService {
    //How? => 생성자를 private로 막아버림


    //먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
    private static final SingletonService instance = new SingletonService();

    //객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
    public static SingletonService getInstance(){
        return instance;
    }

    //이제 생성자를 막아보자 private로
    private SingletonService(){

    }

    //로직 테스트 위해서
    public void logic(){
        System.out.println("싱글톤 객체 호출됨!!");
    }



    @Test
    @DisplayName("싱글톤 패턴이 적용된 객체 사용")
    public void singletonServiceTest(){
        //new SingletonService() => private로 막혀버렷쥬?

        //두번 호출 해본다.
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        //이제 테스트 무엇을? 참조값이 같은지
        Assertions.assertThat(singletonService1).isSameAs(singletonService2);


    }


}

 

 

싱글톤 패턴을 구현하는 방법은 여러방법들이 있다. 여기선 객체를 미리 생성해두는 단순하고 안전한 방법을 썻다.

 

하지만 싱글톤은 여러 단점들이 있다.

  1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. => 접근 못하게 막고, 미리 올려두고 하는 과정이 생겨버림
  2. 의존 관계상 클라이언트가 구체 클래스에 의존 -> DIP위반 ==>SingletonService.getInstance();로 꺼내버림 구체클래스 꺼냄
  3. DIP위반햇으니 OCP 위반할 가능성 많아짐
  4. 테스트하기 어려움 => 유연하지가 않게됨
  5. 내부 속성을 변경하거나 초기화 하기 어렵움
  6. private생성자이므로 자식 클래스를 만들기 어려움

위 단점들때문에 안티패턴이라고도 불림

 

하지만.... 스프링을 쓰면 스프링이 알아서 해결해줌... how?

밑에서 보자.

 

싱글톤 컨테이너


스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤 즉, 1개만 생성해서 관리한다.

(이때 까지 햇던 스프링 빈이 싱글톤으로 관리 되었다.)

 

싱글톤 컨테이너

  • 싱글톤 패턴을 적용안해도 스프링 컨테이너가 다 알아서 해줌 => 객체 인스턴스를 싱글톤으로 관리함
  • 즉, 스프링 컨테이너는 싱글톤 컨테이너 역할을 함
  • 싱글톤 객체를 생성하고 관리하는 기능 => 싱글톤 레지스트리
  • 따라서 스프링을 사용하면 싱글톤 패턴의 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
  • 싱글톤 패턴을 위한 지저분한 코드가 없어짐
  • DIP, OCP, 테스트, private 생성자로부터 자유로워짐!! 호우!

스프링 컨테이너를 사용하는 테스트 코드를 보자

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SingletonTest {

    @Test
    @DisplayName("스프링이 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        //1. 조회
        MemberService memberService1 = appConfig.memberService();
        //2. 조회
        MemberService memberService2 = appConfig.memberService();
        //두 객체 비교
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }


//=> 스프링 컨테이너를 사용!!!
    @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);
        //두 객체 비교
        Assertions.assertThat(memberService1).isSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }
}

 

멤버 서비스 객체를 공유하게 됨!!

 

하지만 이러한 싱글톤 방식도 문제가 있음....ㅠ

 

싱글톤 방식의 주의점


객체 인스턴스를 하나만 생성해서 공유하므로, 객체는 상태를 유지하면 안됨!!!

(운영체제 시간의 동시성 문제를 생각해보자..)

 

즉, 무상태로 설계해야한다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다!!
  • 즉, 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.!!!
  • 따라서 가급적 읽기만 해야됨
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreaLocal등을 사용해야한다.

 

상태를 유지해서 발생하는 문제점 - 예시

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 hello.core.AppConfig;
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;

public class StatefulServiceTest {

    @Test
     void statefulServiceSingleton(){
        //스프링 컨테이너
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        //각각 price 입력해버림 = TheadA
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        statefulService1.order("userA", 10000);

        //각각 price 입력해버림 = ThreadB
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        statefulService1.order("userB", 20000);

        int price = statefulService1.getPrice();

        //뒤에서 값이 바뀌니까 같지 않을 것이다.
        Assertions.assertThat(price).isEqualTo(20000);



    }




    static class TestConfig{
        //빈 등록
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }

}

 

 

각각 Thead 2개라 가정(Order 하는 경우)

price필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경해버리게 된다.

 

싱글톤은 이런 문제가있다....

해결법으론 private int price를 사용하지 않고, int order로 price를 반환하게 해버린다.

즉 price를 지역 변수로 만들어버린다.

 

package hello.core.singleton;

public class StatefulService {
    //private int price;// 상태를 유지함, 즉, 값을 가지고 있음

    //값을 반환해서 가지고 있지 않게됨
    public int order(String name, int price){
        System.out.println("name = " + name + "price = " + price);
        return price; // 

    }
/*
    public int getPrice(){
        return price;
    }
    */
}

package hello.core.singleton;

import hello.core.AppConfig;
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;

public class StatefulServiceTest {

    @Test
     void statefulServiceSingleton(){
        //스프링 컨테이너
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        //각각 price 입력해버림 = TheadA
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        int userAPrice = statefulService1.order("userA", 10000);

        //각각 price 입력해버림 = ThreadB
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        int userBPrice =statefulService1.order("userB", 20000);

        //int price = statefulService1.getPrice();

        //뒤에서 값이 바뀌니까 같지 않을 것이다.
        Assertions.assertThat(userAPrice).isEqualTo(10000);



    }




    static class TestConfig{
        //빈 등록
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }

}

 

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

다양한 설정 형식 지원 - 자바코드, xml

스프링 설정정보를 java파일 이외에 xml, groovy 등등을 사용할 수 있다.

왜냐면

ApplicationContext(인터페이스) 구현체들로 다음과 같은 것들이 있기 때문이다.

AnnotationConfig AppliactionContext => AppConfig.class

GeneriXml ApplicationContext => appConfig.xml

Xxx ApplicationContext => appConfig.xxx =>다양한 설정형식을 지원한다.

 

XML 설정 사용해보기

요즘 잘 안쓰긴하지만 xml을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 잇다.

GenericXmlApplicationContext를 사용해서 xml 설정 파일을 넘기면 된다.

package hello.core.xml;

import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class XmlAppContext {
    @Test
    void xmlAppContext(){
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }

}

이제 appConfig.xml 파일 작성!

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="hello.core.member.MemberServiceImpl" >
        <constructor-arg name="memberRepository" ref="memberRepository" /> //생성자, 참조
    </bean>
    
    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />

    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref = "discountPolicy" />
    </bean>
    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>


</beans>

AppConfig.java와 유사한것을 알 수 있다.

 

중요한 것은 스프링이 이렇게 다양한 설정 형식을 지원한다는 것이다.

과연 어떻게???

 

 

스프링 빈 설정 메타 정보 - BeanDefinition

BeanDefinition이라는 추상화가 있다.

위에서 말한것처럼, 역할과 구현을 개념적으로 나눈것!!!

BeanDefinition을 빈 설정 메타정보라 한다.

@Bean, 당 각각 하나씩 메타정보가 생성되고, 스프링컨테이너는 이 메타정로를 기반으로 스프링 빈을 생성한다.

 

  • 스프링 컨테이너 ------> BeanDefinition <-- AppConfig.class, AppConfig.xml, AppConfig.xxx

    스프링 컨테이너는 BeanDefinition 인터페이스에만 의존한다.

    따라서 BeanDefinition에는 뭐가 들어가는지 모른다. 그냥 그 역할을 수행하면 된다.

 

 

너무 깊게 이해할 필요는 없으니 필기한거 참고!

그냥 저렇게 추상화를 사용해서 다양한 설정형식을 지원한다고 알고 있으면된다.

 

package hello.core.beandefinition;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class BeanDefinitionTest {
    //getBeanDefinition을 사용하기 위해서
    //ApplicationContext 대신 사용
    //getBeanDefinition은 왠만해선 쓸일 거의 없음 => 빈 설정 메타정보임
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 설정 메타 정보 확인")
    void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {

            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                System.out.println("beanDefinition = " + beanDefinition + "beanDefinition =" +beanDefinition);
            }
        }

    }
}

 

 

 

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

 

컨테이너에 등록된 모든 빈 조회

package hello.core.beanfind;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ApplicationContextInfoTest {
    //스프링 컨테이너 불러옴
    //구성 정보는 AppConfig에 있다.
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean(){
        //스프링 빈 이름들을 String으로 빼냄
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " Object = " + bean );

        }
    }
    //스프링에 등록된 모든 빈정보가 나옴 (스프링 내부에서 사용하는 빈까지 다 나옴)


    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean(){
        //스프링 빈 이름들을 String으로 빼냄
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            //BeanDefinition => 빈에대한 정보들, 정보들 가져옴
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            //role_appliction과 role_infrastructure(내부에서 사용하는 빈)가 있음
            //역할로 걸러서 출력함
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " Object = " + bean);
            }

        }
    }

}

 

 

스프링 빈 조회 - 기본

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회방법은

ac.getBean(빈이름, 타입)

ac.getBean(타입) => 이름 생략가능

 

만약 조회 대상이 없을 경우엔 예외가 발생한다.

NoSuchBeanDefinitionException: No bean named "xxxxx" available

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

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

public class ApplicationContextBasicFindTest {
    //스프링 컨테이너 가져옴
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName(){
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //isInstanceOf => 어떤 클래스인가?
    }

    @Test
    @DisplayName("이름 없이 빈 타입으로 조회")
    void findBeanByType(){
        //인터페이스로 조회하면 알아서 사용하는 구현체(스프링 빈에 등록된)가 조회된다.
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //isInstanceOf => 어떤 클래스인가?
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findByName2(){
        //구현체에 의존하게 된다.....
        MemberService memberService = ac.getBean(MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //isInstanceOf => 어떤 클래스인가?
    }

    //조회가 안되는 경우도 테스트 해봐야해!!
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findByNameX(){
        //MemberService memberService = ac.getBean("XXX", MemberService.class);
        //조회가 안되면 NoSuchBeanDefinitionException 터친다.
        //Junit Assertion의 assertThrows를 사용해서 예외를 확인 해야한다.
        assertThrows(NoSuchBeanDefinitionException.class, ()->ac.getBean("XXX", MemberService.class));

    }


}

 

**참고 assertThrows

org.junit.jupiter.api.Assertions.assertThrows(

 NoSuchBeanDefinitionException.class, // 발생이 예상되는 예외의 타입

 ()-> ac.getBean("xxxx", MemberService.class) // 예외가 발생될 수 있는 코드 블록

);


/*
.

먼저 해당 테스트 메소드는 존재하지 않는 빈의 이름으로 빈을 가져오려고 할 때 예외가 발생되는 상황을 테스트 하기 위한 메소드입니다.

ac.getBean("xxxx", MemberService.class); 해당 문장을 실행하면 존재하지 않는 빈의 이름(xxxx)으로 빈을 꺼내오려고 할 것입니다. 그러나 당연히 xxxx라는 이름으로 등록된 빈이 없기 때문에 NoSuchBeanDefinitionException 예외가 발생합니다.

.

assertThrows 메소드는 발생이 예상되는 예외의 타입, 예외가 발생될 수 있는 코드 블록을 파라미터로 받아서 실행됩니다.

이 때, assertThrows 내부에서는 예외가 발생될 수 있는 코드 블록을 실행합니다. 만약 해당 코드 블록을 실행 중 예외가 발생한다면 발생된 예외가 발생이 예상되는 예외의 타입과 일치하는지 아닌지 확인합니다. 이때 발생된 예외 타입과 예상되는 예외의 타입이 일치하면 테스트는 성공으로 처리됩니다.

.

아래 코드는 assertThrows의 내부 동작입니다. try 블록 안에서 코드를 실행하여 예외 발생시 catch 내에서 발생한 예외 타입과 예상되는 예외 타입을 비교하고 있습니다.
*/

 

 

스프링 빈 조회 - 동일한 타입이 둘 이상

타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류 발생!!!!

이때는 빈 이름을 지정하자

ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.

 

package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

//타입 으로 조회시 같은 타입이 둘 이상이면...
// 테스트를 위해 간단한 설정 클래스 만듬
public class ApplicationContextSameBeanFindTest {
    //테스트를 위한 sameBean 설정 정보를 가져옴
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);


    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상이면 중복오류 발생")
    void findBeanTypeDuplicate(){
        //먼저 어떤 예외를 던지는지 확인해본다.
        //MemberRepository bean = ac.getBean(MemberRepository.class);
        /*
        org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memberRepository1,memberRepository2
        NoUniqueBeanDefinitionException를 던진다.
         */
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(MemberRepository.class));

    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
    void findBeanByName(){
        //설정클래스에 메소드 둘다 MemberRepository이다.따라서 이름을 같이 지정
        MemberRepository bean = ac.getBean("memberRepository1", MemberRepository.class);
        org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(MemberRepository.class);

    }

    //특정 타입 모두 조회 => 둘 다 꺼내고 싶음
    //getBeansOfType 사용하면 된다.
    //MemberRepository형 모두 꺼냄 
    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanType(){
        //getBeansOfType은 Map형으로 반환함 <이름, 타입>
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key);            
        }
        System.out.println("beansOfType = " + beansOfType);
        org.assertj.core.api.Assertions.assertThat(beansOfType.size()).isEqualTo(2);
        //해당 타입의 개수가 2개이므로 2개 다 꺼내졌는지 확인
    }


    //구성 정보이므로 어노테이션 붙임
    //메서드 명은 다르지만, 반환하는 타입은 같음
    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

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

    }
}

 

 

스프링 빈 조회 - 상속관계

부모타입으로 조회하면 자식타입도 함께 조회된다.

예를 들어 Object타입으로 조회시 모든 스프링빈을 조회하게 된다.

 

테스트코드로 확인해보자

package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;

import java.util.Map;


public class ApplicationContextExtendsFindTest {
    //스프링 컨테이너
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시 자식이 둘 이상 있으면 중복 오류")
    void findBeanByParentTypeDuplicate(){
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        //->NoUniqueBeanDefinitionException
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(DiscountPolicy.class));

    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름 지정해서 쓰면댐")
    void findBeanByParentTypeBeanName(){
        DiscountPolicy bean = ac.getBean("fixDiscountPolicy", DiscountPolicy.class);
        org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(FixDiscountPolicy.class);

    }


    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeansBySubType(){
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(RateDiscountPolicy.class);

    }


    @Test
    @DisplayName("부모 타입으로 다 조회")
    void findBeansByParentType1(){
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key + "+ key + "value = "+ beansOfType.get(key));


        }

    }

    @Test
    @DisplayName("부모 타입으로 다 조회-object")
    void findBeansByParentType2(){
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key + "+ key + "value = "+ beansOfType.get(key));


        }

    }


    //설정 클래스이므로
    @Configuration
    static class TestConfig {

        //스프링 빈에 등록
        @Bean
        public DiscountPolicy rateDiscountPolicy(){
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy(){
            return new FixDiscountPolicy();
        }
    }
}

 

 

직접 getBean할 경우는 잘 없음!!

개발하면서 애플리케이션 컨텍스트에서 빈을 조회할 일이 거의 없음.

하지만 기본기능이고, 가끔 순수 자바애플리케이션에서 스프링컨테이너를 생성해서 쓸때 사용한다.

 


 

BeanFactory와 ApplicationContext

ApplicationContext, AnnotationConfig(구현클래스) ---> ApplicationContext(인터페이스) ---> BeanFactory(인터페이스)

 

BeanFactory

스프링 컨테이너의 최상위 인터페이스.

스프링 빈을 관리, 조회하는 역할 담당, 예시로 getBean을 제공해줌

이전 코드들에서 대부분 사용한 기능은 BeanFactory가 제공하는 기능임,

Then, 왜 BeanFactory가 아닌 ApplicationContext를 사용했을까??

 

ApplicationContext

BeanFactory 기능을 모두 상속받아서 제공한다.

차이점은, 애플리케이션 개발시, 빈은 관리하고 조회하는 기능이외에 수많은 기능들이 필요하다.

 

예를 들면 ApplicationContext가 구현하는 인터페이스들을 보면

MessageSource = 메시지 소스를 활용한 국제화기능(한국에서 들어오면 한국어, 외국에서 들어오면 영어)

EnvironmnetCapable = 환경변수이다. 로컬, 개발, 운영등 구분해서 처리 (예를 들면 로컬 개발환경, 테스트서버 개발환경, 실제운영환경 등등 사용하는 DB가 다르다면? 맞게 설정)

ApplicationEventPublisher = 애플리케이션 이벤트이다. 이벤트를 발행하고 구독하는 모델을 편리하게 지원

ResourceLoader = 편리한 리소스 조회. 파일, 클래스패스,외부 등에서 리소스를 편리하게 조회

 

일단 이런게 있다 정도만 알고 가자...

 

정리하면 ApplicationContext는 BeanFactory 기능들에 편리한 기능을 추가한것이다. 따라서 BeanFactory는 거의 잘 안쓴다.

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

스프링으로 전환


 

AppConfig 스프링 기반으로 변경

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
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 MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

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

    @Bean
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }

}

 

MemberApp에 스프링 컨테이너(객체 생성, 관리, 의존관계 주입) 적용

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

//테스트해보는 용도
public class MemberApp {
    public static void main(String[] args) {

        //AppConfig를 통하여 주입
//        AppConfig appConfig = new AppConfig();
//       MemberService memberService = appConfig.memberService();//주입!
        //멤버서비스 구현체는 생성자로 적절한 구현체들을 받게된다.
        //즉, 멤버 서비스 구현체는 어떤 구현체를 가져올지 고민안해도 됨!
        //밑에선 memberServiceImpl를 가져오고 MemberServiceImpl 내부적으로 어떤걸 쓸지 골라야됬음!
        //MemberService memberService = new MemberServiceImpl();


        /**
         * 스프링 컨테이너 적용
         */
        //ApplicationContext를 스프링 컨테이너라 보면된다. 스프링은 모든것이 이 A.C로 부터 시작된다.
        //AppConfig로부터 구성 정보를 가져온다.
        // => 스프링이 빈들을 설정해서 스프링 컨테이너에 객체를 생성해서 갖고 있는다.
        //어노테이션 기반으로 config를 하므로 AnnotationConfigApplicationContext 사용
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        
        //getBean(메서드명, 타입)으로 가져온다.
        //MemberServiceImpl.class와 같이 구체 타입을 지정해도 되나, 구현에 의존하게 되는 꼴이 되버린다.
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        
        Member member = new Member(1l, "memberA", Grade.VIP);


        //회원가입
        memberService.join(member);

        //가입한 멤버가 있는지 조회
        Member findMember = memberService.findMember(1L);

        //확인
        System.out.println("new member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());


    }
}

 

OrderApp에도 스프링 컨테이너를 적용시킨다.

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

//주문 하기
public class OrderApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        OrderService orderService = appConfig.orderService(); //주입!!\
//        MemberService memberService = appConfig.memberService();

        //스프링 컨테이너 생성
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        //빈 가져옴
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

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

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

        System.out.println("Order = " + order);
    }



}

 

 

실행 후 로그를 보면

17:06:34.789 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
17:06:34.808 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
17:06:34.810 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
17:06:34.811 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'

"Creating shared instance of singleton bean" 스프링에 등록된 것을 알 수 있다.

스프링에 등록시 (key(메서드명), value(타입))으로 저장이된다.

 

이걸 정리하면

  • ApplicationContext = 스프링 컨테이너 (Context = 문맥)
  • 기존엔 개발자가 AppConfig를 사용해서 직접 객체 생성하고 DI
  • @Configuration 어노테이션이 붙은 AppConfig를 설정정보로 사용한다.
  • @Bean 이 붙은 메서드를 모두 호출하고 반환된 객체를 스프링 컨테이너에 등록한다. (등록된 객체 = 스프링 빈)
  • 관례로 @Bean이 붙은 메서드 명을 스프링 빈 이름으로 사용한다. (@Bean (name = "AA")와 같이 변경도 가능한데 왠만하면 관례따르는게 보기 편하고 관리 편하다.)
  • 기존엔 직접 필요한 객체를 AppConfig를 이용해서 조회했다.
  • => 스프링 컨테이너를 통해서 applicaionContext.getBean()메서드를 사용해서 스프링빈, 즉 객체를 찾게되었따.

즉, 직접 자바코드로 모든 것을 하다가

  1. 스프링 컨테이너에 객체를 스프링 빈으로 등록
  2. 스프링 컨테이너에서 스프링 빈을 찾아서 사용

과 같이 바뀌게 되었다.

 

이렇게 바꾸었을 때 장점은 무엇일까?..... => 뒤에서

 

 

스프링 컨테이너와 스프링 빈


스프링 컨테이너 생성과정을 다시 보면

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext 이 스프링 컨테이너이고, 인터페이스이다.

따라서 다형성이 적용된다.

"new AnnotationConfigApplicationContext(AppConfig.class);"클래스는 ApplicationContext 인터페이스의 구현체이다.

스프링 컨테이너는 어노테이션 기반의 자바 설정 클래스로 만들수도 있고, XML을 기반으로 만들 수도 있다.

 

정리하면

  1. 스프링 컨테이너 생성

    스프링 컨테이너 생성(AppConfig.class) - 스프링 컨테이너 생성시 구성정보 지정해주어야함.

     

  2. 스프링 빈 등록

    파라미터로 넘어온 설정정보(AppConfig.class)를 사용해서 @Bean들을 다 등록함.

    빈 이름(Key) : 빈 객체(value) 로 저장된다. (빈이름은 메서드명이 사용된다.)

    ex) memberService : MemberServiceImpl@0x1

     

  3. 스프링 빈 의존관계 설정 - 준비

    빈들이 스프링 컨테이너에 등록이 되었지만 서로 연결되진 않았다. = 의존관계 주입이 안되었음

     

  4. 스프링빈 의존관계 설정 - 완료

    각 객체들이 생성되면서 의존관계가 주입되어진다.

    동적 의존 관계 연결시켜줌.

    (설정 정보를 참고해서 의존관계가 주입되어짐.)

 

** 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져있다.

위와같이 자바코드로 스프링 빈 등록시, 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.

    @Bean//스프링 빈으로 등록
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    } // -> 호출되면서 memberservice와 리포지토리가 자동으로 연결되게 된다

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

    @Bean
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    } // 호출되면서 리포지토리, 할인정책이 불려오면서 같이 연결되게 된다.

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

이제 컨테이너에 잘 등록되었는지 확인 해보자.

728x90
반응형
블로그 이미지

아상관없어

,
반응형

 

새로운 할인 정책 개발


 

10프로 할인 정책!

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

//10퍼센트만 할인 한다고 가정
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;
    
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

 

새 할인 정책 적용!

OrderService 구현체에서 직접 선택해줘야된다 => 따라서 구현체에 의존하게된다.(인터페이스가 아니라)

클라이언트(OrderServiceImpl)의 코드를 수정해야한다.

OCP => 변경하지않고 확장가능 => 깨짐

DIP => 인터페이스 뿐만 아니라 구현체에 의존하게됨.

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

따라서

클라이언트(OrderServiceImpl)를 공연의 예제에 비교하면

배우(로미오 역)이 직접 여배우(줄리엣)역을 섭외하는 격이다!!

배우는 대본에만 집중을 해야지 섭외의 역할까지 맡게되면서 다양한 책임을 가지게 된다.

 

그래서 관심사를 분리해야한다.

배우는 배우 역할에만 신경쓰게!

 

따라서 공연기획자가 필요하다.

공연을 구성, 배우 섭외, 역할에 맞는 배우를 지정하는 책일을 가지는 공연 기획자가 필요!

 

바로 공연기획자가 AppConfig이다.

 

 

 

AppConfig


 

애플리케이션의 전체 동작 방식을 구성한다.

구현 객체를 생성하고, 연결하고 책임을 가진다.

위 역할을 하는 별도의 설정 클래스이다.

 

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

//의존관계 주입!
public class AppConfig {
    
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

각 클라이언트의 생성자를 이용하여 맞는 구현체를 넣어준다.

=> 생성자 주입기법

 

그러면 이제 각 구현체에서 생성자를 통해서 받아오기만 하면 된다!!!

 

멤버서비스 구현체

package hello.core.member;

//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{

    //배역 배우로 생각할때
    //MemberRepository memberRepository = new MemoryMemberRepository();

    //생성자로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;

    //생성자
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

주문 서비스 구현체

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemoryMemberRepository;

//할인 정책을 가져와서 저장소에서 vip인지 조회해봐야됨
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //생성자 주입기법으로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

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

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

AppConfig를 통하여 주입받기 위해 MemberApp 클래스를 수정한다.

AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();//주입!

 

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;

//테스트해보는 용도
public class MemberApp {
    public static void main(String[] args) {

        //AppConfig를 통하여 주입
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();//주입!
        //멤버서비스 구현체는 생성자로 적절한 구현체들을 받게된다.
        //즉, 멤버 서비스 구현체는 어떤 구현체를 가져올지 고민안해도 됨!
        //밑에선 memberServiceImpl를 가져오고 MemberServiceImpl 내부적으로 어떤걸 쓸지 골라야됬음!
        //MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1l, "memberA", Grade.VIP);

        //회원가입
        memberService.join(member);

        //가입한 멤버가 있는지 조회
        Member findMember = memberService.findMember(1L);

        //확인
        System.out.println("new member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());


    }
}

 

주문 서비스 또한 appConfig로 인젝션을 해준다.

AppConfig appConfig = new AppConfig();
        OrderService orderService = appConfig.orderService(); //주입!!\
        MemberService memberService = appConfig.memberService();

 

package hello.core;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;

//주문 하기
public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        OrderService orderService = appConfig.orderService(); //주입!!\
        MemberService memberService = appConfig.memberService();

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

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

        System.out.println("Order = " + order); 
    }
    


}

 

각 테스트코드 또한 바꾸어 준다.

@BeforeEach를 사용하여 appconfig로 주입시켜준다.

@BeforeEach
    void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

 

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    //MemberService memberService = new MemberServiceImpl();

    MemberService memberService;
    
    @BeforeEach
    void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
    @Test
    void join(){

        //given - vip
        Member member = new Member(1L, "memberA" , Grade.VIP);

        //when - 회원가입
        memberService.join(member);
        Member findMember = memberService.findMember(member.getId());

        //then - 가입한 회원이 맞는지
        Assertions.assertThat(member).isEqualTo(findMember);


    }
}

 

 

 

AppConfig 리팩터링


 

기존

public class AppConfig {
    
    //메모리멤버리포지토리가 두번이나 생성되어 중복된다.
    //그리고 각 역할과 구현이 뚜렷하게 눈에 보이지 않는다.
    
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

 

리팩터링 후

public class AppConfig {

    //멤버 서비스는 멤버리포지토리가 필요하다.
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    //리포지토리는 멤버 리포지토리
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    //할인정책은 FixDiscountPolicy
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
        //return new FixDiscountPolicy();
    }

}

중복이 제거되었고, 각 역할과 구현이 명확하게 보인다.

 

또한 AppConfig 설정을 바꿔줌으로써 할인정책을 바꿀수 잇게 되었다.

따라서 클라이언트의 코드를 수정하지 않고 변경가능!!!

OCP = 확장에 유연, 변경에는 닫힘

을 지킬 수 있고

DIP = 인터페이스에 의존

도 가능해졌다~~!

 

AppConfig를 사용함으로써

사용영역과 구성영역으로 구분이 되었고, 변경사항시 구성영역만 변경하면 되게 되었따.

또한 구성영역을 사용함으로써 역할과 구현이 명확하게 분리 되고, 역할이 잘 보이게 되었고, 중복이 제거되었다.

 

정리하면, SRP, DIP, OCP를 적용하게 되었다.

 

SRP

한 클래스는 하나의 책임만 가져야한다.

기존 - 클라이언트가 직접 구현객체생성, 연결, 실행

AppConfig 사용 - 구현객체를 AppConfig가 생성하고 연결함, 따라서 클라이언트는 실행하는 책임만 가지게됨.

 

DIP 의존관계 역전 원칙

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다."

=> 의존관계 주입은 이 원칙을 따르는 방법 중 하나이다.

 

기존 - 새로운 할인정책을 만들고 적용시킬때, 클라이언트 코드 수정해야했음

AppConfig 사용 - AppConfig가 할인정책 객체 인스턴스를 클라이언트 코드 대신에 생성해서 클라이언트 코드에 의존관계를 주입했다.

즉, 외부에서 객체 인스턴스를 넣어주어 DIP원칙을 지켰다.

 

 

OCP

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.

다형성을 사용하고 클라이언트가 DIP를 지키면서 OCP가능성이 높아졌다.

애플리케이션 => 사용영역 / 구성영역으로 분리되었다.

AppConfig가 의존관계를 클라이언트에 주입해주어 클라이언트 코드변경X

따라서, 소프트웨어 요소를 새로 확장해도 사용 영역의 변경은 없다 => 즉 닫혀있다.!

 

 

IoC, DI, 컨테이너

IoC? => Inversion of Control(제어의 역전) , 제어를 AppConfig가 가지게 됨(외부에서 제어흐름을 가지게 됨)

 

기존 - 클라이언트 구현객체가 스스로 서버구현객체 생성, 연결, 실행 => 구현 객체가 프로그램 제어흐름 조종!

AppConfig - 구현객체는 자신의 로직만 실행, 제어흐름은 AppConfig가 해줌

(예를 들면 OrderServiceImpl은 필요한 인터페이스를 호출 하지만 어떤 구현객체가 올지 모른다. => AppConfig가 주입해줌)

 

 

프레임워크 vs 라이브러리

프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다 => ex) Junit

내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리이다. => 직접 메서드 호출 등..

 

DI(Dependecy Injection)= 의존관계 주입

OrderServiceImpl(구현체)는 DiscountPolicy(인터페이스)에 의존한다. => 어떤 DiscountPolicy가 올지 모른다.

의존관계는 정적인 클래스 의존 관계, 실행시점에 결정되는 동적인 객체 의존 관계를 분리해서 생각해야한다.

 

  • 정적인 클래스 의존 관계

    클래스가 사용하는 import 코드만 보고 의존관계 파악이 가능하다.

    즉, 애플리케이션을 실행하지 않아도 분석가능!

    하지만 어떤 구현객체가 주입되는지는 모름!

     

  • 동적인 객체 의존 관계

    애플리케이션 실행 시점에 실제 생성된 객체 인스턴스 참조가 연결된 의존 관계

    애플리케이션 실행 시점(런타임)에 외부에서 실제 구현객체를 생성해서 클라이언트에 전달!

    클라이언트와 서버 의존관계가 연결된다. => 의존관계 주입

    의존관계 주입을 통해서 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다.

 

IoC컨테이너 = DI컨테이너 (어셉블러(조립), 오브젝트 팩토리(오브젝트를 만드니까) 등으로 불리기도함)

AppConfig와 같은 역할

객체를 생성, 관리하면서 의존관계를 연결해주는 것.

 

 

살짝 정리

이때까지 순수 자바코드로 DI, 의존관계 주입을 해보았다.

다음으론 스프링을 사용하여 의존관계 주입을 해본다.

728x90
반응형
블로그 이미지

아상관없어

,
반응형

스프링 핵심 원리 이해1 - 예제


 

비지니스 요구사항과 설계

  • 회원

    회원 가입, 조회 가능

    일반, vip 등급의 회원 존재

    DB는 미정

  • 주문과 할인 정책

    회원이 상품 주문가능

    등급에 따라 할인 정책 가능

    VIP는 무조건 1000원 할인 (정책 가정, 확정은 아님)

 

=> 미정인 부분은 인터페이스로 역할/구현을 분리하면 된다!!

 

** 일단은 스프링이 아닌 순수 자바코드로 구현한다고 가정

 

회원 도메인


 

  • 클라이언트 -> 회원서비스(가입, 조회) -> 회원 저장소(미정)
  • MemberService(구현체: MemberServiceImpl) -> MemberRepository(구현체: 일단 MemoryMemberRepository)
  • 클라이언트 객체 -> 회원서비스 객체(MemberServiceImpl) -> 멤버리포지토리 객체

 

회원 도메인 개발

회원 등급, 회원 엔티티 필요

 

회원등급

package hello.core.member;
public enum Grade {
    BASIC,
    VIP
}

회원 엔티티

package hello.core.member;

//id, name, grade 필요
public class Member {
    private long id;
    private String name;
    private Grade grade;

    public Member(long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public void setId(long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Grade getGrade() {
        return grade;
    }


}

 

회원 저장소


 

회원저장소 인터페이스, 구현체

 

회원저장소 인터페이스 - 저장, 멤버찾기 기능

package hello.core.member;

//id, name, grade 필요
public class Member {
    private long id;
    private String name;
    private Grade grade;

    public Member(long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public void setId(long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Grade getGrade() {
        return grade;
    }


}

 

구현체 - 일단 메모리 저장소

package hello.core.member;

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

//저장소 필요
public class MemoryMemberRepository implements MemberRepository{

    //일단 임시 저장소 = 메모리 사용
    private static Map<Long, Member> store = new HashMap<>();

    @Override
    public void save(Member member) {
        store.put(member.getId(),member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);//store는 Map이므로 키값으로 member찾음
    }
}

 

 

회원 서비스


 

회원 가입과 조회 기능이 필요하다.

 

회원 서비스 인터페이스 (역할)

package hello.core.member;

public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}

 

구현

package hello.core.member;

//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{
    
    MemberRepository memberRepository = new MemoryMemberRepository();
    
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

일단 멤버도메인, 멤버서비스 ,멤버저장소 구현완료!!

 

일단 main으로 테스트

package hello.core.member;

//테스트해보는 용도
public class MemberApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1l, "memberA", Grade.VIP);

        //회원가입
        memberService.join(member);

        //가입한 멤버가 있는지 조회
        Member findMember = memberService.findMember(1L);

        //확인
        System.out.println("new member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());


    }
}

은 안좋으니 Junit으로 테스트

 

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    MemberService memberService = new MemberServiceImpl();

    @Test
    void join(){

        //given - vip
        Member member = new Member(1L, "memberA" , Grade.VIP);

        //when - 회원가입
        memberService.join(member);
        Member findMember = memberService.findMember(member.getId());

        //then - 가입한 회원이 맞는지
        Assertions.assertThat(member).isEqualTo(findMember);


    }
}

 

=> memberServiceImpl에서 MemberRepository memberRepository = new MemoryMemberRepository(); 로 구현체를 직접 참조하고 있다. 따라서 구현에 의존하게 되는 문제점이 발생한다.!

 

 

주문과 할인 도메인 설계


 

  • 회원은 상품 구매가능
  • 등급에따라 할인됨
  • 할인정책으론 모든 vip는 1000원 할인

 

  1. 클라이언트 - (주문생성) -> 주문 서비스 역할 - (회원조회) -> 회원저장소 역할

    주문생성(id, 상품명, 상품가격), 회원조회(id로 조회) => 상품명과 상품가격은 간단하게 객체가 아니라 data로 만듬

  2. 주문서비스 역할 - (할인 적용) -> 할인 정책역할

    할인 적용 ( vip인가? 확인)

  3. 주문서비스 역할 - (주문결과 반환) -> 클라이언트

    주문결과는 간단하게 DB에 저장이 아니라 주문결과를 반환

 

클라이언트 -> 주문서비스 -> 1. 회원저장소 2.할인정책

 

할인 정책에는 고정가 할인 정책과 비율 할인 정책이 있다.

 

할인 정책 인터페이스

package hello.core.discount;

import hello.core.member.Member;

//vip 인지 확인해서 할인된 상품가격 결과를 반환한다.
public interface DiscountPolicy {
    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);
}

 

고정가 할인 정책

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

//vip이면 1000원 할인, 할인금액 반환
public class FixDiscountPolicy implements DiscountPolicy{

    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return discountFixAmount;
        }
        else{
            return 0;
        }
    }
}

 

 

주문 서비스


 

먼저 주문 엔티티를 만든다.

(회원 id, 상품명, 상품가격)

 

package hello.core.order;

//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
//할인된 결과 필요
public class Order {
    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    //할인된 결과
    public int calculatePrice(){
        return itemPrice - discountPrice;
    }

    public void setMemberId(Long memberId) {
        this.memberId = memberId;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }

    public Long getMemberId() {
        return memberId;
    }

    public String getItemName() {
        return itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }
    
    //결과 쉽게 보기 위해서


    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

 

 

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    
    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

 

주문과 할인 테스트


 

package hello.core.order;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import net.minidev.json.JSONUtil;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

//회원등급을 조회하고 그에맞는 할인이 적용되었는가?
//주문서비스 -> 저장소, 할인정책
public class OrderServiceTest {
    //given
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();



    //주문 테스트
    @Test
    void createOrder() {

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

        //when
        memberService.join(member);
        Order order = orderService.createOrder(memberId, "itemA", 10000);

        //then

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

 

 

728x90
반응형
블로그 이미지

아상관없어

,