'분류 전체보기'에 해당되는 글 148건

반응형

알리 익스프레스에서 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
반응형
블로그 이미지

아상관없어

,
반응형

먼저 전원 관리 옵션에서 고급 전원 관리 옵션 설정 변경을 누르고

디스플레이 밝기 관련 설정이 보인다면

배터리 사용시 - 최대 성능이나 최대 밝기 와같이 선택을 해준다. 

 

그래도 안되면

AMD - Radeon Softwre

(Intel는 인텔 그래픽 제어판)

에 들어가서 화면 밝기 관련 설정을 찾아보자!

 

나는 AMD 그래픽을 사용해서 이걸 기준으로 보여주겠다.

 

디스플레이에 들어가서 Vari-Bright로 들어가서 최대 밝기로 바꾸어 주면 끝!!

 

참고로 전원 관리 옵션에서 

 

이처럼 각 상황에 맞는 전원 옵션을 만들어주면 배터리만 쓰는 경우, 전원이 꼽혀있는 경우, 무조건 고성능이 필요한 경우 이런식으로 맞게 고르면 편하다.

728x90
반응형
블로그 이미지

아상관없어

,
반응형

스프링 입문 복습


(자바 문법을 모를땐, https://www.youtube.com/playlist?list=PLW2UjW795-f6xWA2_MUhEVgPauhGl3xIp 여기서 해당 부분을 참고하자!)

스프링 프로젝트 생성!

"Https//start.spring.io" => 스프링 관련 프로젝트를 생성해주는 site이다.

Maven/Gradle : 라이브러리를 가져오고 빌드하는 Life Cycle까지 관리해주는 툴이며 요즘은 Gradle을 주로 사용한다.

 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.3.1.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
    
repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'//thyleaf템플릿 엔진 사용 html틀 만들어줌
	implementation 'org.springframework.boot:spring-boot-starter-web'//해당 라이브러리 안에 톰캣 서버가 있음
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
	exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }//Test를 위한 라이브러리들
}
	test {
	useJUnitPlatform()
}

 

View 환경설정

웰컴 페이지 생성 후 접속

 

localhost:8080/hello를 입력하면 내장톰캣서버가 받고 스프링컨테이너는 hello를 getmapping으로 입력받아

""@GetMapping("hello")" hello를 getmapping으로 받는 컨트롤러 hellocontroller를 실행하고

hellocontroller는 template/hello로 리턴하여 hello.html이 열리게 된다.

그리고 thymeleaf 엔진이 hello.html을 처리한다.

 

<resources/static/index.html>=> 이 경로가 기본 웰컴페이지이다. (다른 파일이 없는 경우 얘가 뜸)

<!DOCTYPE HTML>
<html>
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
</html>

 

<controller/HelloController>

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

//웹 앱에서 첫번째 진입점이다.
@Controller
public class HelloController {

    //get메소드로 hello가 들어오면
    @GetMapping("hello")
    //spring이 모델을 만들어서 넣어준다.
    //Model은 HashMap 형태를 갖고 있으며, key, value값을 가지고 있습니다. 또한 addAttribute()와 같은 기능을 통해 모델에 원하는 속성과 그것에 대한 값을 주어 전달할 뷰에 데이터를 전달할 수 있습니다.
    public String hello(Model model){
        model.addAttribute("data", "hello!!");//속성, 값
        return "hello"; //template의 hello로
        //기본적으로 resources:template/+{viewname}+.html로 매핑된다.
        //따라서 resources/tempalate/hello.html로 이동한다
    }

}

 

 

<resources/templates/hello.html>

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <!-- th는 thymeleaf 엔진을 뜻함-->
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>

data에 key값으로 hello!!를 넣어줬으므로 hello!!가 뜬다.

 

 

 

정적 컨텐츠

=> 파일을 그대로 뿌려준다. (파일 그대로 전달)

/static 폴더안의 파일을 찾는다.

 

"localhost:8080/hello-static.html"가 넘어오면 먼저 hello-static 관련 컨트롤러가 있는지 확인한다.

왜냐하면 컨트롤러가 우선순위를 가지기 때문이다.

스프링 컨테이너가 hello-static관련 컨트롤러가 없음을 확인하면 /static 폴더안을 살펴보고 있으면 그 파일을 반환한다.

 

<resources/static/hello-static.html>

<!DOCTYPE HTML>
<html>
<head>
    <title>static content</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
정적 컨텐츠 입니다.
</body>
</html>

 

 

MVC와 템플릿 엔진

Model, View, Controller로 분리하여 처리한다.

(예전에는 view에서 모든 로직을 다 처리하여 복잡했다.)

"localhost:8080/hello-mvc?name=spring"이 요청되면 @Getmapping("hello-mvc")가 받아서 뒤의 파라미터 name을 넘겨준다.

그리고 hello-template로 반환한다.

 

=> controller

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

//웹 앱에서 첫번째 진입점이다ㅣ.
@Controller
public class HelloController {

    @GetMapping("hello-mvc")
    //@RequestParam(value= "name", required = True ) ctrl+p 할 경우 필요한 파라미터 정보 볼수 있음
    //required가 기본이 트루이기 때문에 name값을 넘겨야됨
    //따라서 localhost:8080/hello-mvc?name=spring
    public String helloMvc(@RequestParam("name") String name, Model model){
        model.addAttribute("name", name);
        //모델이 name값을 넘겨줌
        return "hello-template";
    }


}

 

hello-template.html은 넘겨받은 name값을 표시한다.

<resources/templates/hello-template.html> => View

<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
    <!-- 템플릿 엔진이 model의 키 값에서 name인 것을 꺼낸다. 뒤의 hello! empty는 name값이 안넘어오면 표시된다. -->
</body>
</html>

 

 

API

주로 json포맷으로 data가 전달된다.

즉, data만 전달하는 경우이다.

"localhost:8080/hello-api?name=spring"

@ResponseBody를 사용하고 객체를 반환하면 객체가 json으로 변환된다.

(json은 key, value로 이루어진 데이터)

 

"localhost:8080/hello-api?name=spring"

요청시 @Controller가 있으므로 getmapping hello-api를 찾아서 실행한다.

@ResponseBody어노테이션이 있으므로, http의 body부에 그대로 값을 넘긴다.

(즉, viewResolver대신에 HttpMessageConverter가 동작한다.)

반환값이 단순 문자이므로 StringHttpMessageConverter가 기본문자를 처리한다.

원래는 @ResponseBody가 있으면 객체를 반환할 경우 MappingJackson2HttpMessageConverter에 의해서 객체를 자동으로 json타입으로 바꾸어서 반환한다. (key:value )와 같은 형태

-> 이것은 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입정보 두가지를 조합해서 선택한다.

(대부분은 json을 선택하여 ,json으로 반환한다.)

 

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

//웹 앱에서 첫번째 진입점이다ㅣ.
@Controller
public class HelloController {

    //문자를 넘기는 경우
     //get방식인데 body에 직접 전달함.
    //requestparam은 name으로 전달달
    @GetMapping("hello-string")
    @ResponseBody //http의 body부에 전달
    public String helloString(@RequestParam("name") String name) {
        return "hello " + name;//"hello spring" (StringConverter)
    }
    
    
    //객체를 넘기는 경우
    @GetMapping("hello-api")
    @ResponseBody//http의 body부에 전달 (viewResolver를 사용하지 않는다.), 객체반환시 json으로 반환된다.
    public Hello helloApi(@RequestParam("name") String name){
        Hello hello = new Hello();
        hello.setName(name);
        return hello;//Hello 객체를 넘기므로 json으로 바꿔짐 (json 컨버터가 바꿔줌(속성 : 값)으로)
        //jsonConverter
    }

    
   static class Hello{
        private String name;

        public String getName() {
            return name;
        }

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


}

 

 

 

 

회원 관리 예제


 

비지니스 요구 사항

  1. 데이터 = 회원id, 이름
  2. 기능 = 회원등록, 조회
  3. 데이터 저장소 = 아직 정해지지 않음

 

일반적인 웹 애플리케이션 계층 구조

컨트롤러 : 웹 MVC의 컨트롤러 역할

서비스 : 핵심 비지니스 로직

리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리

도메인 : 비지니스 도메인 객체 ex) 회원, 주문, 쿠폰 등등 주로 DB에 저장하고 관리됨

 

컨트롤러 -> 서비스 -> 리포지토리 -> DB

컨트롤러, 서비스, 리포지토리 -> 도메인

 

클래스 의존관계

멤버서비스 -> 멤버리포지토리(인터페이스) <- 메모리멤버리포지토리(구현체)

(저장소가 정해지지 않았으니까 일단 멤버리포지토리를 인터페이스로 구현해둠)

 

회원 도메인과 리포지토리 생성

<회원 객체> = 회원 도메인

id와 name을 저장해야하므로 id와 name이 필요하다.

 

<멤버 도메인>

package com.example.hellospring.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

//1. 회원 id가 필요
//2. 회원 name이 필요
public class Member {

    //id, name 생성
    private Long id;
    private String name;


    public String getName() {
        return name;
    }

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

    public Long getId() {
        return id;
    }

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

 

 

<멤버리포지토리> -인터페이스 (구현은 데이터 저장소가 정해지면 구현)

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;

import javax.swing.text.html.Option;
import java.util.List;
import java.util.Optional;

/*
1. 저장
2. 찾기
2.1. id로 찾기
2.2. name으로 찾기
2.3. 전부 다 찾기
 */
public interface MemberRepository {
    //1. 저장(도메인은 member 클래스)
    Member save(Member member);

    //2.1 id로 찾기
    //optional은 null일경우 값이 없다고 알려준다.
    Optional<Member> findbyId(Long id);
    //2.2 name으로 찾기
    Optional<Member> findbyName(String name);
    //2.3 모두다 찾기
    //Member형의 List
    List<Member> findAll();

}

 

<메모리멤버리포지토리> - 멤버리포지토리를 일단 메모리에 저장, 따라서 멤버리포지토리 인터페이스 구현

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.*;

//MemberRepository 구현
//리포지토리는 저장
//@Repository //리포지토리라고 알려줌 스프링에, 따라서 스프링이 가져옴 즉, 스프링 빈 등록
public class MemoryMemberRepository implements MemberRepository{

    //map을 이용하여 저장(키, 값)
    private static Map<Long, Member> store = new HashMap<>();
    // hashmap은 키와 값을 가지는 자료구조이다.
    //hasing을 사용하기 때문에 검색이 빠르다. 
    //A -> Hash(A) -> 값 :: A로 값이 매칭된다.
    private static long sequence = 0L;//시퀀스

    @Override
    public Member save(Member member) {
        //id값 설정
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findbyId(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findbyName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();//하나라도 찾으면
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}

 

 

회원 리포지토리 테스트 케이스 작성

자바는 JUnit이라는 프레임워크로 테스트를 실행할 수 있다.

따라서 반복실행, 여러테스트를 한꺼번에 할 수 있다.

"src/test/java" 하위 폴더에 생성한다.

 

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
//import org.junit.jupiter.api.Assertions;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Optional;

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

//테스트만을 위해쓰니까 굳이 public으로 선언하지 않아도 됌
class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();


    //테스트가 끝날때 마다 동작
    //어떤 메소드가 동작이 끝나면 실행됨
    //한번에 여러테스트 진행시 db에 이전 테스트 데이터가 남으므로 메소드가 끝나면 데이터를 지워준다,.
    //테스트는 순서가 보장되지 않으므로 서로 독립적이게 설정해야함
    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }


    @Test
    public void save(){
        Member member = new Member();
        member.setName("Spring");

        repository.save(member);
        Member result = repository.findbyId(member.getId()).get();
        //단순히 출력해보아도됨 하지만 글자로 볼순 없음
        //Assertions.assertEquals(member, result);//기대, 비교값
        //member객체와 result 객체를 비교한다.
        //사용법만 익히자
        assertThat(member).isEqualTo(result); //멤버가 result와 같으냐
    }


    @Test
    public void findbyName(){
        //멤버1 생성후 리포지토리에 저장
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        //멤버2 생성후 리포지토리에 저장
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findbyName("spring1").get();

        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll(){
        //멤버1 생성후 리포지토리에 저장
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        //멤버2 생성후 리포지토리에 저장
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll();
        assertThat(result.size()).isEqualTo(2);
    }
}

 

회원 서비스 개발

 

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;


public class MemberService {
    
    private final MemberRepository memberRepository = new MemberRepository();


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findbyName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });//ctrl + T로 람다식 변환
    }

    /**
     * 전체회원 조회
     */
    public List<Member> findMember(){
        return memberRepository.findAll();
    }

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

 

회원 서비스 테스트

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;


public class MemberService {
    
    //기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했음
    //private final MemberRepository memberRepository = new MemberRepository();
    
    /*
    멤버 서비스에서 사용하는 멤버리포지토리랑
    멤버서비스테스트에서 사용하던 멤버리포지토리랑 다르다.
    멤버리포지토리의 store가 static이므로 같은 멤버리포지토리를 가르키지만 static이 아닌 경우에는 다르게된다.
    */
    private final MemberRepository memberRepository;
    
    //멤버리포지토리를 받아서 멤버 서비스에 넘겨준다.
	public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findbyName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });//ctrl + T로 람다식 변환
    }

    /**
     * 전체회원 조회
     */
    public List<Member> findMember(){
        return memberRepository.findAll();
    }

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

멤버리포지토리를 받아서 멤버 서비스에 넘겨줫으므로 멤버서비스 테스트를 보면

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Optional;

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

class MemberServiceTest {

    //멤버 서비스, 멤버 리포지토리 객체 생성
    MemberService memberService;
    MemoryMemberRepository memberRepository;

    //같은 리포지토리를 사용하기 위해서 "멤버 서비스에 넘겨줌" => 위에서 바꿨음
    //해당 테스트 클래스를 초기화할 때 딱 한번 수행되는 메서드
    //각 테스트 실행 전에 호출된다. 
    //테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고 의존관계도 새로 맺어줌
    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        //같은 멤버 리포지토리가 사용됨
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach(){
        memberRepository.clearStore();
    }
    @Test
    void join() {

        //저장한게 리포지토리에 있는게 맞아?? => 멤버리포지토리 필요
        //given
        Member member = new Member();
        member.setName("hello");
        //만약 hello가 아니라 spring인 경우 밑 중복회원예외 메소드랑 값(db에 겹침)이 겹침

        //when
        //회원추가 할때
        Long saveId = memberService.join(member);


        //then
        //중복되는 경우 잘 걸러냄?
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void 중복_회원_예외(){
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);

        //2. assertThrows를 이용하는 방법
        //뒤 로직을 동작할때 앞의 예외가 터져야댐
        //assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        //3. IllegalStateException 객체를 받아서 사용하는 방법법
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //1. try-catch를 사용하는 방법 ->복잡
        /*
        try{
            memberService.join(member2);
            fail();//혹시 catch로 안가면
        } catch (IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."))
        }*/


    }
    @Test
    void findMember() {
    }

    @Test
    void findOne() {
    }
}

 

 

 

스프링 빈과 의존관계


컴포넌트 스캔과 자동 의존관계 설정

회원 컨트롤러가 회원서비스와 회원 리포지토리를 사용할 수 있게 의존관계를 준비!

(회원 컨트롤러가 회원서비스와 회원리포지토르 두개를 통해서 데이터 조회를 할 수 있음.)

 

 

멤버컨트롤러에서 멤버서비스를 하나만 생성해두고 공용으로 사용한다.

이때 공용으로 사용하기 위해서 Autowired를 사용하여 컨트롤러 등록시 컨테이너가 객체를 생성해서 가지고 있게 한다.

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

//스프링이 처음 실행될때 스프링 컨테이너에 컨트롤러 어노테이션이 있으면 스프링이 객체를 생성해서 들고 있음
// => 스프링 컨트롤러에서 스프링 빈이 관리된다고 함
@Controller
public class MemberController {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //new해서 새로 생성해서 사용하면 멤버 컨트롤러 외의 다른 컨트롤러들(예를들어 주문)이 가져가서 쓸 수 있음.
    //하지만 멤버서비스를 가면 별기능이 없음. 따라서 하나만 생성해서 공용으로 사용하는 것이 나음
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.   

    private final MemberService memberService;

    //컨트롤러 등록시 컨테이너가 객체를 생성(이때 생성자를 호출한다.)해서 들고 있게된다.
    //즉, 생성자에 Autowired가 선언되어 있으면, 멤버서비스를 스프링이 컨테이너에 있는 멤버서비스에 연결시켜준다.
    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }

    //회원 등록 값 입력후 회원 생성
    @PostMapping("/members/new")
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }

    //회원 조회
    @GetMapping("/members")//members로 이동(home.html에서 회원목록 누르면 /members로 이동하게 되어 있음)
    public String list(Model model){
        List<Member> members = memberService.findMember();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

즉, 스프링컨테이너가 뜰때 MemnberController가 등록(스프링빈으로 등록)되는데 Autowired로 MemberSerivce를 연결했다.

하지만 MemberSerivce(@Controller 어노테이션이 없다!)는 순수 자바 파일이므로 스프링빈으로 등록되지 않았으므로 스프링 컨테이너 실행시 등록되어있지 않는다.

따라서 MemberSerivce에 @Service 어노테이션을 붙여주면 스프링이 올라올때 스프링 컨테이너에 멤버 서비스를 등록시켜준다.

 

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;


//Service어노테이션은 MemberService를 스프링 컨테이너에 등록시켜준다
//원래 멤버 서비스 클래스는 어떠한 어노테이션도 없었다.
//보통은 컨트롤러 어노테이션을 붙이면 스프링 컨테이너가 알아서 객체를 생성해서 갖고 있지만
//멤버 서비스는 그렇지 않았다.
@Service
public class MemberService {
    
    //기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했음
    //private final MemberRepository memberRepository = new MemberRepository();
    
    /*
    멤버 서비스에서 사용하는 멤버리포지토리랑
    멤버서비스테스트에서 사용하던 멤버리포지토리랑 다르다.
    멤버리포지토리의 store가 static이므로 같은 멤버리포지토리를 가르키지만 static이 아닌 경우에는 다르게된다.
    */
    private final MemberRepository memberRepository;
    
    //멤버리포지토리를 받아서 멤버 서비스에 넘겨준다.
    @Autowired 
    //=>memberRepository가 필요하니까 연결, memberRepository는 @Repository로 스프링 빈 등록되어있음
	public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findbyName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });//ctrl + T로 람다식 변환
    }

    /**
     * 전체회원 조회
     */
    public List<Member> findMember(){
        return memberRepository.findAll();
    }

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

 

스프링 빈을 등록하는 2가지 방법

  1. 컴포넌트 스캔과 자동 의존관계 설정

    => 어노테이션을 사용한다. EX) @Service 어노테이션 안에는 @Component 어노테이션이 들어있다.

    컴포넌트 스캔 원리

    @Component 어노테이션이 있으면 스프링 빈으로 자동등록된다.

    객체를 생성해서 스프링에 등록한다. 그 후 Autowired가 각각 연결시켜준다.(각 객체들을)

    @Controller 컨트롤러가 스프링빈으로 자동등록된 이유도 컴포넌트 스캔때문이다.

    @Controller, @Service, @Repository 전부다 @Component를 포함하고 있다.

     

    따라서 MemoryMemberRepository 또한 어노테이션을 붙여준다.

    package com.example.hellospring.repository;
    
    import com.example.hellospring.domain.Member;
    import org.springframework.stereotype.Repository;
    
    import java.util.*;
    
    //MemberRepository 구현
    //리포지토리는 저장
    @Repository //리포지토리라고 알려줌 스프링에, 따라서 스프링이 가져옴 즉, 스프링 빈 등록
    public class MemoryMemberRepository implements MemberRepository{
    
        //map을 이용하여 저장(키, 값)
        private static Map<Long, Member> store = new HashMap<>();
        private static long sequence = 0L;//시퀀스
    
        @Override
        public Member save(Member member) {
            //id값 설정
            member.setId(++sequence);
            store.put(member.getId(), member);
            return member;
        }
    
        @Override
        public Optional<Member> findbyId(Long id) {
            return Optional.ofNullable(store.get(id));
        }
    
        @Override
        public Optional<Member> findbyName(String name) {
            return store.values().stream()
                    .filter(member -> member.getName().equals(name))
                    .findAny();//하나라도 찾으면
        }
    
        @Override
        public List<Member> findAll() {
            return new ArrayList<>(store.values());
        }
    
        public void clearStore(){
            store.clear();
        }
    }
    
    

     

    스프링컨테이너 안을 보면

    memberController --> memberService --> memberRepository 와 같이 연결되어있다.

    즉, memberController는 memberService, memberRepository가 필요하다.

    따라서 각각 @Controller, @Service, @Repository로 등록이 되고 사용시 @Autowired로 연결시켜준다.

     

    memberController --> memberService --> memberRepository

    이것들은 각각 스프링 빈인데, 스프링은 스프링 컨테이너에 스프링 빈을 등록할때 기본으로 싱글톤으로 등록한다.

    즉, 각 객체들을 하나만 등록해서 공유한다. => 같은 스프링빈이면 모두 같은 인스턴스이다.

    (memberController는 memberController 하나만, memberService는 memberService 하나만 등록과 같이)

    예를 들어 OrderService가 있어서 memberRepository에 접근시, 멤버 서비스나 오더 서비스는 동일한 멤버 리포지토리에 접근한다.

    따라서 메모리가 절약된다.

     

     

    만약 아무곳에나 @Component가 있어도 될까?(ex 다른곳에 아무 패키지 만들어서 demo라는 파일을 만들경우라던가)

    =>안된다.

    HelloSpring.java(main있음)를 실행시키면, Package hello.hellospring와 동일하거나 하위에 있는 것들은 스프링이 다 찾는다.

    즉, 다른 패키지에 있으면 스프링이 컴포넌트 스캔을 안하므로 등록이 안된다.

 

 

  1. 자바 코드로 직접 스프링 빈 등록하기

회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 어노테이션을 제거하고 진행한다.

 

hellospring/SpringConfig 생성

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {


    //직접 스프링 빈으로 등록한다고 알려줌으로써 DI(의존성 주입)를 해줌, 즉 연결시킴
    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    //자바 코드로 직접 멤버 서비스를 등록함/ @Service 어노테이션으로 등록하는게 아니라
    //직접 memberService 객체를 생성시켜서 등록함!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository(); 


    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

 

DI에는

  • 필드주입 = @Autowired

    @Autowired를 통한 DI는 helloController, memberService 등과 같이 스프링이 관리하는 객체에서만 동작을한다!

    스프링빈으로 등록하지 않고 직접 생성한 객체에서는 동작하지 않는다.

    왜냐하면 스프링 컨테이너가 들고 있지 않으니까!

 

  • 생성자 주입 = 생성자를 통해서 주입

  • setter 주입 = setter 설정으로 생성하고 @Autowired (말 그대로 setter로 주입됨.)

    (누군가 멤버 컨트롤러를 호출 했을때 public으로 열려 있어야함.

    => public하게 노출되어버린다.(아무나 변경가능해진다.) 중간에 변경되면 문제가 생길 수 도 있다.)

 

** 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔방식을 사용한다. 정형화 되지 않거나 상황에 따라 구현 클래스를 변경해야하마녀 설정을 통해 스프링 빈으로 등록한다.

{정형화(보통 일반적으로 자주 쓰는 틀로 만든(저장, 조회 등 일반적으로 자주 쓰는))}

 

==> 향후 메모리 리포지토리에서 다른 리포지토리로 변경할 예정이므로 컴포넌트 스캔방식이 아닌 자바코드로 스프링 빈을 설정했다.

 

 

 

회원 관리 예제 - 웹 MVC 개발


회원 웹 기능 - 홈 화면 추가

 

HomeController 추가

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    //컨트롤러가 정적파일보다 우선순위가 높음
    //도메인 "/"
    @GetMapping("/")
    public String home(){
        return "home"; //home.html 불러옴
    }
}

 

회원 관리용 홈

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <h1>Hello Spring</h1>
        <p>회원 기능</p>
        <p>
            <a href="/members/new">회원 가입</a> <!-- 링크 -->
            <a href="/members">회원 목록</a>
        </p>
    </div>
</div> <!-- /container -->
</body>
</html>

 

 

회원 웹기능 - 등록

회원 등록 폼 컨트롤러

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

//스프링이 처음 실행될때 스프링 컨테이너에 컨트롤러 어노테이션이 있으면 스프링이 객체를 생성해서 들고 있음
// =>
@Controller
public class MemberController {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    //member/new 요청시 member/createMemberForm 반환
    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }



}

 

회원 등록 폼 HTML

(resources/templates/members/createMemberForm)

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <!-- 등록을 누르면 Post방식으로 넘어온다. (데이터 전달) "/members/new"에 전달해줌-->
    <form action="/members/new" method="post">
        <div class="form-group">
            <label for="name">이름</label>
            <input type="text" id="name" name="name" placeholder="이름을
입력하세요">
        </div>
        <button type="submit">등록</button>
    </form>
</div> <!-- /container -->
</body>
</html>

 

회원 등록 컨트롤러

웹 등록 화면에서 데이터를 전달 받을 폼 객체

package com.example.hellospring.controller;

public class MemberForm {
    private String name;//name을 입력받음

    public String getName() {
        return name;
    }

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

 

그러면 이제 실제 회원을 등록하는 기능을 MemberController에 추가해준다.

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

//스프링이 처음 실행될때 스프링 컨테이너에 컨트롤러 어노테이션이 있으면 스프링이 객체를 생성해서 들고 있음
// =>
@Controller
public class MemberController {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }

    /*==========================================================*/
    //회원 등록 값 입력후 회원 생성
    //post로 /members/new를 가져옴
    @PostMapping("/members/new")
    public String create(MemberForm form){ //위에서 만든 MemberForm으로 데이터 받음
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";//홈화면으로 보냄
        
    }
    /*==========================================================*/

}

 

 

회원 웹 기능 - 조회

회원 컨트롤러에 조회기능 생성

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

//스프링이 처음 실행될때 스프링 컨테이너에 컨트롤러 어노테이션이 있으면 스프링이 객체를 생성해서 들고 있음
// =>
@Controller
public class MemberController {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }

    //회원 등록 값 입력후 회원 생성
    @PostMapping("/members/new")
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }

    /*===============================================*/
    //회원 조회
    @GetMapping("/members")//members로 이동(home.html에서 회원목록 누르면 /members로 이동하게 되어 있음)
    public String list(Model model){
        List<Member> members = memberService.findMember();
        model.addAttribute("members", members);
        return "members/memberList";//여기로 이동
    }
    /*===============================================*/
}

 

 

회원 리스트 html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <table>
            <thead>
            <tr>
                <th>#</th>
                <th>이름</th>
            </tr>
            </thead>
            <tbody>
                <!-- members를 읽어들이고 id와 name가져옴(thymeleaf버전의 for each같은 문법임)
				즉 회원들을 조회를 하는 것임-->
            <tr th:each="member : ${members}">
                <td th:text="${member.id}"></td>
                <td th:text="${member.name}"></td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

 

 

 

 

 

 

 

스프링 DB 접근 기술


H2 데이터베이스를 사용하여 스프링에서 접근해본다.

h2데이터베이스는 개발이나 테스트 용도로 가볍고 편리한 DB, 웹화면을 제공해준다.

 

drop table if exists member CASACDE;
create table member{
	id bigint getnerated by default as identity, 
--값을 입력하지 않으면 DB가 알아서 채운다는 뜻이다.(identity 방식)
	name varchar(255),
	primary key(id)
}

 

순수 Jdbc

고대의 기술이라 불린다...

대충 이렇구나만 하고 넘어가자..

 

gradle.build에 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'//자바-db 연동시 필요
runtimeOnly 'com.h2database:h2'//db가 제공하는 클라이언트

 

스프링 부트 데이터베이스 연결 설정 추가

resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

이렇게 세팅하면 스프링 부트가 DataSource를 만들어 놓는다.

즉, 데이터베이스 접속 정보를 만들어 놓는다.

 

Jdbc 회원 리포지토리

package com.example.hellospring.repository;


import com.example.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;//스프링에게서 주입받아야한다.
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository {
    private final DataSource dataSource;//스프링에게서 주입받아야한다.
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    //저장을 위해서 insert 쿼리가 필요하다 따라서 직접 작성
    @Override
    public Member save(Member member) {
        String sql = "insert into member(name) values(?)";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection(); //DB 연결 만듬
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);//sql 입력
            pstmt.setString(1, member.getName()); // sql의 ?에 매칭됨
            pstmt.executeUpdate(); //DB에 쿼리 날림
            rs = pstmt.getGeneratedKeys(); 
            if (rs.next()) {
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs); //외부에 연결을 했으므로 다 끊어줘야한다. 아니면 위험하다.
        }
        //예외를 많이 던져서 복잡하다..
    }
    @Override
    public Optional<Member> findbyId(Long id) {
        String sql = "select * from member where id = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1, id);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public List<Member> findAll() {
        String sql = "select * from member";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();
            List<Member> members = new ArrayList<>();
            while(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                members.add(member);
            }
            return members;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public Optional<Member> findbyName(String name) {
        String sql = "select * from member where name = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, name);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            }
            return Optional.empty();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }

    private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
    {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (conn != null) {
                close(conn);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

 

 

스프링 설정 변경

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;


    @Autowired //생성자가 하나라면 생략가능
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        //다른 코드를 변경하지 않고 저장소를 단 한줄로 바꾸었따.!!
 

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.

스프링 부트가 DataSource를 생성하고 스프링 빈 등록하니까 주입받아서 쓸수 있음

 

OCP (Open-Closed 원칙)

개방-폐쇄 원칙.

확장에는 열려있고 수정 변경에는 닫혀있다.

스프링의 DI를 사용하면 기존코드를 손대지않고 설정만으로 구현클래스를 변경할 수 있다.

위의 예시서첨 Memory멤버리포지토리에서 코드만 변경하여 jdbc멤버 리포지토리로 변경한것처럼.

 

스프링 통합 테스트

스프링 컨테이너와 DB까지 연결한 통합 테스트!

(이전 테스트코드들은 그냥 자바파일들이엿지만, 지금은 데이터가 DB에 저장되고 DB연결 정보도 스프링이 가지고 있따.)

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

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

@SpringBootTest//테스트시 스프링부트 가져옴 (스프링 컨테이너와 테스트를 함께 실행한다.)
@Transactional//데스트시 DB 트랜잭션 롤백시킴(테스트가 끝나면 커밋 안함), 따라ㅓㅅ 다음 테스트에 영향안줌
public class MemberServiceIntegrationTest {
    
    //Test 케이스 이므로 그냥 필드 주입시킴
    //new 로 생성하지 않고 스프링에게 달라고 요청함
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findbyId(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    @Test
    public void 중복_회원_예외() throws Exception {//thorws Exception => 이 메소드가 예외를 던질 수 도 있음
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}


 

 

스프링 JdbcTemplate

순수 jdbc에서 봣던 반복 코드들을 대부분 제거해준다.

하지만 SQL은 직접 작성해야한다.

(Jdbc => DB와 스프링 중간에서 번역해서 아무 DBMS에 다 맞게 번역해줌)

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import javax.print.attribute.HashPrintJobAttributeSet;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository {

    //JdbcTemplate 사용!!
    private final JdbcTemplate jdbcTemplate;//인젝션 받을 수 있는건 아님

    //JdbcTemplate 사용시 DataSource가 필요하다.
    @Autowired //생성자가 하나인 경우에 생략가능하다.
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);//datasource = db접근 정보 넘겨줌
    }

    //save하는 경우는 DB에 Insert하는 경우이다. 따라서 명시적으로 insert하기 위해 SimpleInsert사용
    @Override
    public Member save(Member member) {
        /*
        SimpleJdbcInsert은 알아서 insert문을 생성해준다.
        테이블명과 키 컬럼을 명시해주면 알아서 insert해준다.
        또한 이전 jdbc와 다르게 ?에 값을 매칭시키는게 아니라 명시적으로 이름을 지정하여 의존성을 높여준다.
        만약 ?, ?로 두개의 값을 매칭할때 두 값이 순서가 바뀌면 문제가 되버리니 명시적으로 지정한다.
        */
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
        //테이블명은 member이고 키 컬럼은 id이다.

       
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName()); //parameters는 Map타입이므로 각 값들 put해줌

        //key를 받고 넘겨줌, key는 sql 결과
        //이구문은 다시한번 봐야할듯!!!!
        //파라미터를 넘겨서 실행하고 생성된 키 값을 받아옴
        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());//member에 setID해서 넣어줌 id값
        return member;
    }

    @Override
    public Optional<Member> findbyId(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
        return result.stream().findAny();//stream으로 변환해서 하나라도 있으면 반환
    }

    @Override
    public Optional<Member> findbyName(String name) {
        //Member객체들의 List이므로 List<Member>라고 사용
        List<Member> result = jdbcTemplate.query("select * from member where name=?", memberRowMapper(), name);
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        //memberRowMapper는 member객체를 반환하므로 그대로 씀
        //해당 쿼리 결과(리스트로 반환됨)을 memberRowMapper()가 매핑해준다. 그리고 그걸 반환함
        // sql, 반환타입, 인자 (현재 sql문에는 인자가 필요없으므로 없다.)
        return jdbcTemplate.query(("select * from member"), memberRowMapper());
    }

    //ResultSet rs를 받아서 rowNum만큼 반복해서 member객체에 id와 name을 설정해준다.
    private RowMapper<Member> memberRowMapper(){
            return (rs, rowNum) -> {
                Member member = new Member();
                //rs로 결과를 받아서 member에 매핑해서 member를 반환한다.
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;

            };
        
     /*
     템플릿으로부터 ResultSet을 받고 필요한 정보를 추출해서 리턴한다.
     ResultSet의 row하나만 매핑하기 위해 사용한다.
     */
            /*
            Alt+Enter로 람다식으로 바꿀 수 있음
            return new RowMapper<Member>() {
                @Override
                public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
                    Member member = new Member();
                    member.setId(rs.getLong("id"));
                    member.setName(rs.getString("name"));
                    return member;

                }
            };
             */
    }
}

 

RowMapper

기존에는

ResultSet rs = stat.executeQuery("select ~~~ ")
while(rs.next()){//다음 row로
    user = new User();
    ~~setId
    ~~setName
        ...
}

위와 같은 방식이였다.

즉, ResultSet으로 값을 받고 그 다음 User 객체에 담아서 반환하는 방식이였다.

(

ResultSet(java.sql.ResultSet)은 즉 결과임 쿼리문에 대한 결과임.

Select문 결과 조회할 수 있는 방법을 정의한 인터페이스이다.

즉, 결과 집합이다.

next(), getString(), getInt() ...등을 사용해서 값을 가져올수 잇다.

)

JdbcTemplate는 이것을 줄여서 쓴다.

//ResultSet rs를 받아서 rowNum만큼 반복해서 member객체에 id와 name을 설정해준다.
    private RowMapper<Member> memberRowMapper(){
            return (rs, rowNum) -> {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;

            };

rs( = ResultSet)이 값을 가져오고

rowNum만큼 반복해서

객체에 저장해서 반환한다.

 

스프링 설정에서 기존 jdbc에서 jdbcTemplate으로 리포지토리를 바꾼다.

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;


    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        //return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        return new JdbcTemplateMemberRepository(dataSource);//jdbc 템플릿으로 바꿈 레포지토리를

    }

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

 

 

JPA


jpa는 개 편한방식이다...

기존 반복코드도 줄여주고, 기본적인 sql문도 자기가 직접 다 만들어준다.

따라서 개발 생산성이 크게 높아진다.

 

gradle에 관련 라이브러리 추가

plugins {
	id 'org.springframework.boot' version '2.5.10'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	//implementation 'org.springframework.boot:spring-boot-starter-jdbc'//자바-db 연동시 필요
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'//jpa, jdbc다 포함함
	runtimeOnly 'com.h2database:h2'//db가 제공하는 클라이언트
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

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

 

스프링 부트에 JPA 설정 추가

resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

spring.jpa.show-sql=ture//jpa가 날리는 쿼리를 볼수잇게 해줌
spring.jpa.hibernate.ddl-auto=none
    //JPA를 사용하면 member객체를 보고 자기가 table을 자동으로 만든다.
    //하지만 위에서 이미 테이블을 다 만들었으므로 이 옵션은 끔
    //자동 생성을 원하면 create로 설정해두면됨

 

JPA는 인터페이스(자바 표준 인터페이스, 구현은 여러업체에서한다.)이다. 인터페이스만 제공해준다.

구현체로 hibernate ...등이 있다. (대부분 hibernate만 사용한다.)

JPA는 객체와 ORM(object, Relational(관계형DB), Mapping(어노테이션으로))

 

JPA 엔티티 매핑

package com.example.hellospring.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

//1. 회원 id가 필요
//2. 회원 name이 필요
@Entity//jpa가 관리하는 엔티티라고 알림
public class Member {

    //id -> p.k라고 매핑시킴, 기본키 지정시 설정해야됨 db에서 자동으로 id값을 생성해주므로 identiy라고 적음
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    //id, name정보로 JPA는 sql문을 자동으로 작성해준다.
    //ex) @Column (name = "username") -> 만약 DB컬럼명이 username인경우, 매핑시킴 (DB에 있는 컬럼명은 username이라고 매핑됨))

    public String getName() {
        return name;
    }

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

    public Long getId() {
        return id;
    }

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

 

JPA 회원 리포지토리

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.swing.text.html.parser.Entity;
import java.util.List;
import java.util.Optional;


public class JpaMemberRepository implements MemberRepository {

    /*
    gradle에서 "~~-data-jpa"라이브러리를 받으면
    스프링부트가 자동으로 EntityManager(DB랑 연결)를 생성해준다.
     */
    private final EntityManager em;

    //injection 받음
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        //persist => 영구 저장하다.
        em.persist(member);//쿼리는 알아서 다 작성해줌
        return member;
    }

    @Override
    public Optional<Member> findbyId(Long id) {
        //find(타입, 식별값=p.k)
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findbyName(String name) {
        //JPQL이다. 객체대상으로 쿼리를 날리면 sql로 번역이된다. P.K가 아닌 경우에 사용된다.
        //JPQL, 반환Type
        List<Member> result = em.createQuery("select m from Member m where m.name= :name", Member.class)
                .setParameter("name", name) //name컬럼에서 name값 설정
                .getResultList();
        return result.stream().findAny();//해당 name을 찾으면 반환함
    }

    @Override
    public List<Member> findAll() {
        //기본키가 아니므로 JPQL문을 사용하여 가져옴
        //m은 member Entity 자체를 select(이미 다 매핑되어있음)
        //결과를 list로 가져옴
        return em.createQuery("select m from Member m", Member.class).getResultList();
    }
}

 

그리고 서비스 계층에 트랜젝션 어노테이션을 추가해야된다.

왜냐면 DB에 접근하기때문에.

스프링은 해당 클래스의 메서드를 실행할때 트랜잭션을 시작하고 메서드가 종료되면 트랜잭션을 커밋함.

만약 런타임 예외가 발생하면 롤백한다.

JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

//Service어노테이션은 MemberService를 스프링 컨테이너에 등록시켜준다
//원래 멤버 서비스 클래스는 어떠한 어노테이션도 없었다.
//보통은 컨트롤러 어노테이션을 붙이면 스프링 컨테이너가 알아서 객체를 생성해서 갖고 있지만
//멤버 서비스는 그렇지 않았다.
//@Service
@Transactional //data를 저장하거나 변경하므로 필요하다.
//해당 클래스의 메서드를 실행할때 트랜잭션을 시작하고 종료되면 트랜잭션을 커밋함, 런타임 예외시는 롤백
public class MemberService {
    
    //private final MemberRepository memberRepository = new MemberRepository();
    private final MemberRepository memberRepository;

    //alt+ insert => constructor 생성
    //외부에서 멤버리포지토리를 넣어주게 만듬 //테스트환경에서 필요해짐
    //생성자에서 멤버리포지토리가 필요하므로 스프링이 가져와서 넣어줌(MemberRepository는 MemoryMemberRepository가 구현함)
    //따라서 MemoryMemberRepository를 연결시켜준다.(서비스에 주입시켜줌)
    //@Autowired //스프링 빈에 등록
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /*
    setter 주입법
    private memberRepository memberRepository; //주의! -> final 선언이 아님

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

    이러면 다른곳에서 memberService.setMemberService()로 변경가능해진다.
    따라서 별로 잘 안씀

    의존관계가 실행중에 동적으로 변하는 경우는 잘 없다.!
     */


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findbyName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체회원 조회
     */
    public List<Member> findMember(){
        return memberRepository.findAll();
    }

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

JPA를 사용하도록 스프링 설정을 변경해준다.

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;


    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        //return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        //return new JdbcTemplateMemberRepository(dataSource);//jdbc 템플릿으로 바꿈 레포지토리를
        return new JpaMemberRepository(em);//jpa는 EntityManager가 필요하다.
    }

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

 

 

 

스프링 데이터 JPA


스프링 부트와 jpa를 사용하면 편하지만, 여기에 스츠링 데이터 jpa를 사용하면 더욱 편해진다.

예를 들어 리포지토리에 구현 클래스 없이 인터페이스만으로도 개발을 완료 할 수 있다.

즉, 단순 반복도 줄이고 기본 CRUD기능도 스프링 데이터 jpa가 모두 제공한다.

 

crud => 데이터 생성(Create), 검색(Read), 갱신(Update), 삭제(Delete)

 

스프링 데이터 JPA 회원 리포지토리

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;//스프링 데이터 jpa가 제공함(인터페이스에 대한 구현체를 자동으로 만든다.)

import java.util.Optional;

//인터페이스 다중상속
//JpaRepository 와 MemberRepository 인터페이스를 가져옴
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    Optional<Member> findByName(String name);// 구현 완료!!! 나머진 데이터 JPA CRUD 다 만들어준다.

}

 

스프링 데이터 jpa 회원 리포지토리를 사용하도록 스프링 설정 변경

package com.example.hellospring;


import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.swing.*;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {
    
    private final MemberRepository memberRepository;

    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    //스프링 컨테이너가 멤버리포지토리를 찾는다.
    //하지만 등록한게 없다. 킹치만.. => SpringDataJpaMemberRepository 가 있음
    //스프링 데이터 JPA가 자동으로 SpringDataJpaMemberRepository를 스프링 빈으로 자동등록해줌
    
    //memberRepository를 인젝션 받아서 등록해준다.
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository);
    }
    //스프링빈 등록해줌
    
    
/*
    private final DataSource dataSource;
    private final EntityManager em;

    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        //return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        //return new JdbcTemplateMemberRepository(dataSource);//jdbc 템플릿으로 바꿈 레포지토리를
        return new JpaMemberRepository(em);//jpa는 EntityManager가 필요하다.
    }

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */

*/
}

 

 

스프링 데이터( Repository(interface) <- CrudRepository(interface) <- PagingAndSortingRepository(interface )) <- 스프링 데이터 JPA(JpaRepository(interface)) 와 같은 관계를 가진다.

각 리포지토리들에 기본적인 CRUD 메소드들과 자주 사용하는 기본 메소드들이 있다.

따라서 자동으로 다 생성해준다.

그러므로 가져다쓰기만 하면됌!

findByName()이나 findByEmail()처럼 메서드 이름만으로 조회 기능을 제공해준다.

 

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;//스프링 데이터 jpa가 제공함(인터페이스에 대한 구현체를 자동으로 만든다.)

import java.util.Optional;

//인터페이스 다중상속
//JpaRepository 와 MemberRepository 인터페이스를 가져옴
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    //JPA는 select m from Member m where m.name=?로 JPQL을 짠다.
    //메소드 이름이 findBy"Name"이니까!!
    //따라서 name이 아니라 username이라던가 nameAndid라고 되어잇는경우는
    //By뒤에만 바꾸어 주면 나머지는 스프링 데이터 JPA가 알아서 다 짜준다.
    Optional<Member> findByName(String name);

}

 

복잡한 동적쿼리는 Querydsl이라는 라이브러리를 사용하면 된다.

이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나 스프링 JdbcTemplate를 사용하면된다.

 

728x90
반응형
블로그 이미지

아상관없어

,
반응형

회원관리 예제


  • 회원 객체
package hello.hellospring.domain;

public class Member {
    //id식별자와 이름이 요구됨
    private Long id;
    private String name;

    //getter, setter생성
    public Long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

 

  • 회원 리포지토리 인터페이스
package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;

//회원 객체 저장소
public interface MemberRepository {
    Member save(Member member);//저장소에 회원 저장
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    //optional 가져오는 값이 없으면 null일 수 잇음(NPE 방지)
    List<Member> findAll();//저장된 모든 회원 리스트 반환
}

 

  • 회원 리포지토리 메모리 구현체
package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{

    //key는 long 값은 Memeber로
    private  static Map<Long, Member> store = new HashMap<>();//hashmap객체 map 생성(key는 long, value는 member
    private static long sequence = 0L;//0부터 키값 생성해줌

    @Override
    public Member save(Member member) {
        //id값 세팅후 store에 저장
        member.setId(++sequence);//save시 시퀀스값 올려줌
        store.put(member.getId(),member);

        return member;
    }

    @Override
    //store에서 꺼내서 찾으면됨
    public Optional<Member> findById(Long id) {
        //return store.get(id); => null이 반환될수도 있음
        return Optional.ofNullable(store.get(id));//NPE가 발생되지 않게 Optional로 감싸줌
    }

    @Override
    //store에서
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                //stream은 iterator와 유사 => 요소를 순차적 처리, stream()은 stream객체로 반환
                .filter(memeber -> memeber.getName().equals(name))//member 객체를 받아서 equal하면 리턴
                .findAny();//filter로 찾은 값 중 아무값이나 하나 선택
        //루프를 돌면서 찾아지면 반환함, 끝까지 돌려서 없으면 Optional에 null이 포함되어 반환
    }

    @Override
    public List<Member> findAll() {
        //리스트로 반환
        //매개변수로 넘어온 컬렉션 객체가 저장되어 있는 ArrayList를 만듦
        //values는 값의 목록을 Collection 타입으로 리턴함
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}

 

 

  • 회원 리포지토리 메모리 구현체 테스트
package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Test;

//굳이 public하지 않아도됨 어디서 가져다 쓰지 않기 때문에
class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository= new MemoryMemberRepository();

    @Test// 테스트를 수행하는 메소드가 된다.
    public void save(){
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        //반환타입이 Optional이므로 값을 꺼낼때 get으로 꺼냄냄
        //get() 메소드를 사용하면 Optional 객체에 저장된 값에 접근할 수 있습니다.
        //Optional<Member>이므로
        Member result = repository.findById(member.getId()).get();
        System.out.println("result = "+(result == member));

    }
}

 

 

출력하여서 확인하여도 된다.

 

출력하지 않고서 확인하는 법은

Assertion을 사용하는 것이다.

 

Assertions.assertThat(member).isEqualTo(result);

를 사용하여도 된다.(org.assertj.core.api)

 

 

    @Test
    public void findByName(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findByName("spring1").get();

        Assertions.assertThat(result).isEqualTo(member1);

 

정리하면

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

//굳이 public하지 않아도됨 어디서 가져다 쓰지 않기 때문에
class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach//메소드가 끝날때 마다 동작
    //test시 member객체를 계속 만들어서 중복이 되버리므로
    public void afterEach(){
        repository.clearStore();
    }

    @Test// 테스트를 수행하는 메소드가 된다.
    public void save() {
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        //반환타입이 Optional이므로 값을 꺼낼때 get으로 꺼냄냄
        //get() 메소드를 사용하면 Optional 객체에 저장된 값에 접근할 수 있습니다.
        //Optional<Member>이므로
        Member result = repository.findById(member.getId()).get();
        //System.out.println("result = "+(result == member));
        //Assertions.assertEquals(member, null);
        Assertions.assertThat(member).isEqualTo(result);

    }

    @Test
    public void findByName() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findByName("spring1").get();

        Assertions.assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll() {
        Member member1= new Member();
        member1.setName("Spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll();
		//크기가 2인지 확인한다.
        Assertions.assertThat(result.size()).isEqualTo(2);
    }

}

=> 자바는 Junit이라는 프레임워크로 테스트를 실행한다.

(main메소드나 웹 애플리케이션의 컨프롤러를 통하여 테스트할 경우 오래걸리고 반복실행, 여러테스트를 한번에 실행하기 어렵다.)

  • JUnit

    자바에서 독립단위 테스트(Unit Test)를 지원해주는 프레임워크

    assert메소드로 테스트케이스의 수행결과를 판별한다.

    @Test : 테스트를 수생하는 메소드 (각각 테스트가 서로 영향을 주지 않고 독립적으로 실행, @Test마다 객체를 생성)

    @AfterEach, BeforeEach : Test메소드가 실행될때 호출됨(각각 메소드 실행 후, 실행전 호출)

    @AfterAll, BeforeAll : 클래스에 존재하는 모든 메소드를 실행한다고 할때, 메소드 시작전, 끝난후에 실행됨

    메소드설명
    assertEquals(x, y)· 객체 x와 y가 일치함을 확인합니다.· x(예상 값)와 y(실제 값)가 같으면 테스트 통과
    assertArrayEquals(a, b);· 배열 A와 B가 일치함을 확인합니다.
    assertFalse(x)· x가 false 인지 확인합니다.
    assertTrue(x)· x가 true 인지 확인합니다.
    assertTrue(message, condition)· condition이 true이면 message표시
    assertNull(o)· 객체o가 null인지 확인합니다.
    assertNotNull(o)· 객체o가 null이 아닌지 확인합니다.
    assertSame(ox, oy)· 객체 ox와 oy가 같은 객체임을 확인합니다.· ox와 oy가 같은 객체를 참조하고 있으면 테스트 통과· assertEquals()메서드는 두 객체의 이 같은지 확인하고, assertSame()메서드는 두 객체의 레퍼런스가 동일한가를 확인합니다. (== 연산자)
    assertNotSame(ox, oy)· ox와 oy가 같은 객체를 참조하고 있지 않으면 통과
    assertfail()· 테스트를 바로 실패처리

 

728x90
반응형

'공부 > Spring' 카테고리의 다른 글

스프링 웹개발 기초  (0) 2021.10.20
자바 - Map  (0) 2021.10.19
자바 - Set  (0) 2021.10.18
자바 - 컬렉션(List)  (0) 2021.10.15
자바 - 제네릭  (0) 2021.10.14
블로그 이미지

아상관없어

,
반응형

스프링


스프링은 자바의 웹 프레임워크이다.

자바로 다양한 앱을 만들기 위한 프로그래밍 툴

 

thymeleaf : html 템플릿 엔진

 

.idea : intellij가 사용하는 설정파일

 

gradle : gradle관련 폴더

 

src : main, test가 있음 main이랑 test가 나뉘어져있음

main밑에 가면 자바랑 리소스(실제 자바코드파일을 제외한 어떤 xml, properties, 설정파일, html등)가 있음, 자바 밑에 실제 소스파일

test는 테스트코드들과 관련된 소스들이 있음

테스트코드가 중요하단 소리 요즘 개발 트렌드

 

build.gradle 이 중요

버전설정, 라이브러리 땡겨옴

plugins {
   id 'org.springframework.boot' version '2.5.4'
   id 'io.spring.dependency-management' version '1.0.11.RELEASE'
   id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11' //자바 11버전

repositories {
   mavenCentral() //mavencentral싸이트에서 라이브러리를 다운받음
}

//라이브러리들
dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'//thymeleaf
   implementation 'org.springframework.boot:spring-boot-starter-web'//web
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
   //테스트라이브러리가 자동으로 들어감 
}

test {
   useJUnitPlatform()
}

 

.gitignore

깃에는 필요한 소스코드만 올라감, start.spring.io에서 관리해줌

 

gradlew

gradlew.bat

 

settings.gradle

 

 

 

콘솔로 빌드 하기

해당 디렉토리로 이동후 "gradlew.bat build" 입력

image-20210928173216290

cd build

cd libs

java -jar hello-spring-0.0.1-SNAPSHOT.jar

image-20210928173505805image-20210928173523377

 

서버배포시 "hello-spring-0.0.1-SNAPSHOT.jar" 파일만 복사해서 옮기고 실행하면 된다.

그러면 서버에서 스프링이 동작하게 된다.

 

"gradle clean" or "gradle clean build" 명령어 입력시 build폴더가 없어진다.

 

 

스프링 웹 개발 기초


정적 컨텐츠 : 파일을 그대로 웹브라우저에 내려줌. 그냥 파일을 그대로 전달해줌

MVC와 템플릿 엔진 : 가장많이 하는 방식, (jsp, php 등 템플릿 엔진), 서버에서 프로그래밍해서 동적으로 웹브라우저에 내림, 서버에서 변형을 해서 전달해줌

API : json데이터 포맷으로 클라이언트에게 데이터를 전달해줌, 뷰나 리액트에서 사용(데이터만 주면 화면은 클라이언트가 알아서 그림), 서버끼리 통신할때(데이터만 왓다갓다)

 

 

- 정적컨텐츠

image-20210928174256185

resources/static 폴더 안 아무 html파일 하나 생성후 작성

image-20210928174427627

정적파일이 그대로 반환이 됨.

따로 프로그래밍을 할 수는 없다.

 

thymeleaf엔진은 서버없이 html파일을 열어볼 수 있음

image-20210930220333376image-20210930220344251

 

 

image-20210930220846830

동작하지 않음

 

2021-09-30 22:08:36.038 WARN 3676 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'name' for method parameter type String is not present]

"Required request parameter 'name' for method parameter type String is not present" 임

 

image-20210930221110958image-20210930221354214

변환된 html이 넘어간다.

템플릿 방식은 뷰가 있고 거기서 화면을 조작함

 

 

api사용법

@GetMapping("hello-string")
    @ResponseBody //http에서 body부분의 데이터를 직접 넣어주겟다 라는 뜻
    public String helloString(@RequestParam("name") String name){
        return "hello" + name;//문자가 그대로 내려감,
        // 템플릿 엔진과의 차이점은 뷰 엔진이 없고 문자가 그대로 내려감

    }

 

http://localhost:8080/hello-string?name=string!!!

image-20210930221801049image-20210930221809163

문자만 그대로 내려감

728x90
반응형

'공부 > Spring' 카테고리의 다른 글

회원관리 예제 - 회원도메인, 리포지토리, 테스트케이스  (0) 2021.10.20
자바 - Map  (0) 2021.10.19
자바 - Set  (0) 2021.10.18
자바 - 컬렉션(List)  (0) 2021.10.15
자바 - 제네릭  (0) 2021.10.14
블로그 이미지

아상관없어

,

자바 - Map

공부/Spring 2021. 10. 19. 15:49
반응형

Map

key와 value로 이루어져있다.

키는 해당 map에서 중복되지않고 키와 값이 1:1로 저장된다.

(키가 다르고 값이 같은 경우 서로 다른 것으로 간주한다.)

 

모든 데이터는 키와 값이 존재

키없이 값만 저장 불가

값 없이 키만 저장 불가

키는 해당 맵에서만 고유함

값은 중복되어도 상관없음

 

java.util.Map 인터페이스로 선언되어 있다.

 

주요 메소드는

V put(K key, V value) : 키와 값을 데이터에 저장한다.

void putAll(Map<? extends K, ? extends V> m) : 매개변수로 넘어온 Map의 모든 데이터를 저장

V get(Object key) : key에 해당하는 값을 넘겨줌

V remove(Object key) : key에 해당되는 값을 넘기고, 해당 키와 값을 삭제한다.

Set keySet() : 키의 목록을 Set타입으로 리턴함

Collection values() : 값의 목록을 Collection 타입으로 리턴함

Set<Map.Entry<K,V>> entrySet() : Map안에 Entry라는 타입의 Set을 리턴함

int size() : Map 크기 리턴

void clear() : Map내용 지움

 

Map을 구현한 주요 클래스들은 HashpMap, TreeMap, LinkedHashMap, Hashtable 등이 있다.

 

Hashtable 클래스는 Map 인터페이스를 구현하였지만, 다른 Map을 구현한 클래스와는 조금 다르다.

Map은 키,값, 키-값 쌍으로 데이터를 순환하여 처리 가능 // Hashtable은 키-값 쌍으로 데이터를 순환하여 처리 불가

Map은 iteratrion을 처리하는 도중에 데이터를 삭제하는 안전한 방법 제공 // Hashtable은 제공 X

Map은 Collection view 사용 // Hashtable은 Enumeration 객체를 통하여 데이터 처리

 

HashMap과의 차이는

Hashtable은 키나 값에 null을 저장할 수 없고

HashMap과 다르게 여러 쓰레드에서 동시 접근이 가능하다.

 

=> 중요한 것은 어떤 작업에 어떤 클래스가 어울리는지 알고 사용하는 것이다.

 

HashMap

Object - AbstractMap - HashMap

 

생성자

HashMap() : 16개의 저장곤간을 갖는 HashMap 객체 생성

HashMap(int initialCapacity) : 매개변수만큼의 저장공간을 갖는 HashMap객체 생성

HashMap(int initialCapactiy, float loadFactor) : 첫 매개변수의 저장공간을 갖고, 두번째 매개 변수의 로드 팩터를 가지는 객체 생성

HashMap(Map<? extends K, ? extends V> m) : 매개 변수로 넘어온 Map을 구현한 객체에 있는 데이터를 갖는 HashMap객체 생성

 

대부분 HashMap 객체 생성시에는 매개변수가 없는 생성자를 사용한다.

 

import java.util.HashMap;
import java.util.Set;
import java.util.Map.Entry;

public class MapSample {
	public static void main(String[] args){
		MapSample sample=new MapSample();
		sample.checkHashMap();
	}
	//HashMaP객체 생성 후 값 넣기
	public void checkHashMap(){
		HashMap<Strig, String> map = new HashMap<String, String>();
		map.put("A", "a"); //키-값 순서이다.
		
		System.out.println(map.get("A"));
		System.out.println(map.get("B")); // ->null을 리턴 없는값이므로\
		
		map.put("A", "1"); // A의 값이 1로 변경된다.(값 입력, 수정 모두 put을 이용)
		
		//키와 값 모두 확인하려면??
		map.put("C", "c");
		map.put("D", "d");
		Set<String> keySet=map.keySet();//keySet()메소드 사용
		//출력의 순서는 뒤죽박죽이다. List와 Queue외에는 저장순서가 중요하지 않다
		for(String tempKey:keySet){
			System.out.println(tempkey+"="+map.get(tempKey));//값은 get으로 가져옴
		}
		
		//객체에 담겨있는 값만 확인하려면?? => value()
		Collection<String> values=map.values();//HashMap 객체에 담긴 값을 Collection 타입의 목록으로 반환
		for(String tempValue:values){
			System.out.println(tempValue);
		}
		
		
		//Entry 객체를 사용하여 키와 값을 얻기 => getKey(), getValue()사용
		Set<Entry<String, String>> entries= map.entrySet();
		for(Entry<String, String> tempEntry:entries){
			System.out.println(tempEntry.getKey()+"="+tempEntry.getValue())
		}
		
		//키나 값이 존재하는지 확인 => containsKey(), containsValue() 리턴값은 참 거짓
		map.containsKey("A");
		map.containsValue("a");
		
		//데이터 삭제 => remove()
		map.remove("A"); //A를 키로 갖는 데이터가 삭제됨

 

728x90
반응형

'공부 > Spring' 카테고리의 다른 글

회원관리 예제 - 회원도메인, 리포지토리, 테스트케이스  (0) 2021.10.20
스프링 웹개발 기초  (0) 2021.10.20
자바 - Set  (0) 2021.10.18
자바 - 컬렉션(List)  (0) 2021.10.15
자바 - 제네릭  (0) 2021.10.14
블로그 이미지

아상관없어

,

자바 - Set

공부/Spring 2021. 10. 18. 18:15
반응형

Set

순서 상관없이 어떤 데이터가 조재하는지 확인하기 위한 용도로 사용

중복되는 것을 방지하고 원하는 값이 포함되어 있는지 확인하는 용도

 

Set 인터페이스를 구현한 주요 클래스는 HashSet, TreeSet, LinkedHashSet가 있다

 

  • HashSet

순서가 필요없는 데이터를 해시테이블에 저장함. 성능이 가장 좋음

Object - AbstractCollection - AbstarctSet - HashSet 구조로 상속받는다.

 

HashSet의 생성자에는

HashSet() : 데이터를 저장할 수 있는 16개의 공간과 0.75의 load factor를 갖는 객체를 생성한다.

HashSet(Collection<? extends E> c) : 매개 변수로 받은 컬랙션 객체의 데이터를 HashSet에 담는다.

HashSet(int initialCapacity) :매개 변수로 받은 개수만큼의 데이터 저장 공간과 0.75의 load factor를 갖는 객체를 생성

HashSet(int initialCapacity, float loadFactor) : 첫번째 매개변수로 받은 개수만큼의 데이터 저장공간과, 두번째 매개변수로 받은 만큼의 load factor를 갖는 객체를 생성한다.

 

load factor : 데이터 개수/ 저장공간

데이터 개수가 증가하여 load factor 보다 커지면 저장공간의 크기는 증가되고 해시 재정리 작업을 해야한다.

=> 자료구조를 다시 생성하므로 "성능에 영향"

load factor가 클수록 공간은 넉넉해지지만, 데이터를 찾는 "시간은 증가"하게 된다.

=> 초기 공간 개수와 load factor는 데이터의 크기를 고려하여 산정하는 것이 좋음

 

HashSet의 주요 메소드들

boolean add(E e) : 데이터 추가

void clear() : 모든 데이터 삭제

Object clone() : HashSet 객체를 복제, 담겨잇는 데이터는 복제X

boolean isEmpty() : 데이터가 있는지 확인함

boolean contains(Object o) : 해당 객체가 있는지 확인

Iterator iterator() : 데이터를 꺼내기 위한 Iterator 객체를 리턴함

boolean remove(Object o) : 매개 변수로 넘어온 객체를 삭제함

int size() : 데이터의 개수

 

import java.util.HashSet;
import java.util.Iterator;

...
... main ...
String[] cars = new String[]{"Hyundai", "Kia", "Samsung", "BMW", "Audi", "Kia", "Samsung"};

...

//차종의 개수를 알아내려는 메소드
public int getCarKinds(String[] cars){
	if(cars == null) return 0;
	if(cars.length==1) return 1;
	
	//HashSet을 생성하여 add해줌 => 중복된 값이 없어짐
	HashSet<String> carSet=new HashSet<String>();
	for(String car : cars){
		carSet.add(car);
	}
	//size를 반환하면 차종의 수를 알 수 있음
	return carSet.size();
}

//값을 꺼낼경우 1=> 데이터의 저장순서는 뒤죽박죽이다. Set은 데이터의 저장순서가 중요하지않은 경우 사용한다.
public void printCarSet(HashSet<String> carSet){
	for(String temp : carSet){
		System.out.print(temp + " ");
	}
	System.out.println();
}

//값을 꺼낼 경우 2 => Iterator의 hasNext, next이용
public void printCarSet2(HashSet<String> carSet){
	Iterator<String> iter=carSet.iterator();
	while(iter.hasNext()){
		System.out.print(iter.next()+" ");
	}
}

 

 

TreeSet

저장된 데이터의 값에 따라서 정렬됨

red-black(이진트리)이라는 트리 타입으로 저장되며 HashSet보다 약간 느리다.

 

LinkedHashSet

연결된 목록타입으로 구현된 해시 테이블에 데이터 저장

저장된 순서에 따라 값이 정렬됨. 성능이 가장 나쁘다.

728x90
반응형

'공부 > Spring' 카테고리의 다른 글

스프링 웹개발 기초  (0) 2021.10.20
자바 - Map  (0) 2021.10.19
자바 - 컬렉션(List)  (0) 2021.10.15
자바 - 제네릭  (0) 2021.10.14
자바 문법  (0) 2021.10.13
블로그 이미지

아상관없어

,
반응형

컬렉션

자바 컬렉션은 목록성 데이터를 처리하는 자료구조를 통칭함

 

만약 여러개의 String 객체를 하나의 객체에 담으려면? => 배열을 이용하면된다.

하지만 담으려는 데이터의 크기를 모르면?

 

자바에선 이러한 문제를 해결하는 클래스가 미리 만들어져있다.

 

순서가 있는 목록인 List형

순서가 중요하지 않은 목록인 Set형

먼저 들어온 것이 먼저 나가는 Queue형

키-값(key-value)로 저장되는 Map형

 

List, Set, Queue는 Collection이라는 인터페이스를 구현한다.

Collection 인터페이슨느 java.util 패키지에 선언되어있고,

여러 개의 객체를 하나의 객체에 담아 처리할때 공통적으로 사용되는 여러 메소드를 선언해두었다.

 

Map은 Collection과 관련없는 별도의 인터페이스로 선언되어있다.

 

public interface Collection<E> extends Iterable<E>

Collection인터페이스는 Iterable라는 인터페이스를 확장했다.

해당 인터페이스는 iterator()메소드만 선언되어있고 Interator라는 인터페이스를 리턴한다.

 

Iterator라는 인터페이스에는

hasNext() : 추가데이터가 잇는지 확인

next() : 현재 위치를 다음 요소로 넘기고 그 값을 리턴해줌

remove() : 데이터를 삭제

라는 메소드가 있다.

 

Collection인터페이스가 Iterable 인터페이스를 확장했으므로 Iterator 인터페이스로 데이터를 순차적으로 가져올 수 있다.


 

 

List

List 인터페이스는 Collection인터페이스를 확장한다.

Collection 인터페이스를 확장하는 다른 인터페이스와 다른점은 배열처럼 순서가 있다는 점이다.

 

List 인터페이스를 구현한 클래스들 중 많이 사용하는 것은

  • ArrayList
  • Vector
  • Stack
  • LinkedList

이다.


 

ArrayList

크기 확장이 가능한 배열이다.

 

상속관계를 보면

java.lang.Object

jaba.util.AbstractCollection

java.util.AbstractList

java.util.ArrayList

이다.

=> AbstractCollection,AbstractList는 각각 Collection, List 인터페이스 중 일부만 구현했다.

 

ArrayList가 구현한 inerface들은

Serializable, Cloneable, Iterable, Collection, List, RandomAccess

이다.

 

public class ListSample{
	public static void main(String[] args){
		ListSample sample = new ListSample();
		sample.checkArrayList1();
	}
	
	public void checkArrayList1(){
		//list1 객체 생성 => ArrayList객체인 list1에 객체들을 넣을 수 있음
		ArrayList list1=new ArrayList();
		
		//list1에 객체들을 넣음
		list1.add(new Object());
		list1.add("ArrayListSample");
		list1.add(new Double(1));
	}
}

컴파일 되지 않는다. => ArrayList는 java.lang에 속한 클래스가 아니기 때문이다.

따라서

import java.util.ArrayList //를 해주어야한다.

 

ArrayList 객체에 여러 객체들을 넣을 수 있지만, 한가지 종류의 객체만을 주로 저장한다.

여러 종류를 하나의 객체에 담을 때는 DTO라는 객체를 만들어서 담는것이 좋다.

따라서 컬렉션 관련 클래스의 객체를 선언할때는 제네릭을 사용하여 선언하는것이 좋다.

=> 한가지 종류의 객체만을 저장

ArrayList<String> list1=new ArrayList<String>();

위와 같이 선언하면 list1은 String 타입의 객체만 넣을 수 있다.

(제네릭을 사용하여, 컴파일 시점에 다른 타입이 잘못 지정되는 것을 막을 수 있다.)

 

ArrayList 객체를 선언할 때 매개변수를 넣지 않으면 기본 초기값은 10이다.

(10개 이상의 데이터가 들어가면 크기를 늘리는 작업은 ArrayList내부에서 자동으로 수행된다.)

ArrayList<String> list1 = new ArrayList<String>(100);

 

 

ArrayList에 데이터를 담기위해선 add와 addAll메소드를 사용하면된다.

 

boolean add(E e) : 매개 변수로 넘어온 데이터를 가장 끝에 담음(리턴값은 제대로 추가되었는지 확인)

void add(int index, E e) : 매개 변수로 넘어온 데이터를 지정된 index위치에 담는다.

boolean addAll(Collection<? extends E> c) : 매개 변수로 넘어온 컬렌션 데이터를 가장 끝에 담는다.

boolean addAll(int index, Collection<? extends E> c) : 매개 변수로 넘어온 컬렉션 데이터를 index에 지정된 위치부터 담는다.

 

만약 list1의 내용을 list2에 복사하고 싶으면

ArrayLsit<String> list2 = new ArrayList<String>(list);

와 같이 사용하면 된다.

ArrayList에는 Collecion 인터페이스를 구현한 어떠한 클래스도 포함시킬 수 있는 생성자가 있기 때문

 

list2=list1

을 해버리면 주소가 같으므로 위와같이 생성자를 이용하여 복사하거나

addAll()을 이용하여 복사하여야 한다.

 

데이터를 꺼내는 법

size() : ArrayList객체에 들어가 있는 데이터의 개수를 가져오는 메소드

=> ArrayList의 크기를 말하는 것이 아님

get(int index)메소드를 사용하여 데이터를 가져올 수 있다.

 

중복된 값이 잇을경우에는

int indexOf(Object o) : 매개 변수로 넘어온 객체와 동일한 데이터의 위치를 리턴한다.

=>앞에서부터 찾을 경우

int lastIndexOf(Object o) : 매개 변수로 넘어온 객체와 동일한 마지막 데이터의 위치를 리턴한다.

=> 뒤에서부터 찾을 경우

 

ArrayList 객체에 있는 데이터들을 배열로 뽑아낼 경우엔 toArray()메소드를 사용한다.

Object[] toArray() : 객체에 있는 값들을 Object[] 타입의 배열로 만듬

T[] toArray(T[] a) : 객체에 있는 값들을 매개변수로 넘어온 T타입의 배열로 만든다.

 

매개변수가 없는 toArray는 Object타입의 배열로만 리턴하므로, 제네릭을 사용하여 선언한 ArrayList 객체를 배열로 생성할 때는 매개변수가 있는 toArray를 사용하는것이 좋다.

String[] temp = new String[5];
String[] strList=list.toArray(temp);

만약 list의 size가 3이라면 (데이터는 1, 2, 3이라 가정)

strList를 출력하면 1, 2, 3, null, null로 출력될것이다.

 

ArrayList에 저장된 데이터 size > 매개변수로 넘어온 배열의 크기

즉, size(list)=3 > temp=0 크기이면

새로운 배열을 생성하여 넘겨주므로

String[] strList=list.toArray(new String[0]);

와 같이 크기가 0인 배열을 넘겨주는 것이 좋다.

 

데이터 삭제

void clear() : 모든 데이터 삭제

E remove(int index) : 매개 변수에서 지정한 위치에 있는 데이터를 삭제하고 리턴

boolean remove(Object o) : 매개변수에 넘어온 객체와 동일한 "첫번째" 데이터를 삭제한다.

boolean removeAll(Collection<?> c) : 매개 변수로 넘어온 "컬렉션 객체에 있는 데이터"와 "동일한 모든 데이터"를 삭제한다.

 

데이터 변경

E set(int index, E element) : 지정한 위치에 있는 데이터를 두번째 매개변수로 넘긴 값으로 변경, 해당위치에 있던 데이터를 리턴

=> 원래 값 반환하고 설정값으로 값 바꿈

=> remove, add 두 과정을 거칠필요 없음

 

trimToSize() : 객체 공간의 크기를 데이터의 개수만큼으로 변경함, 즉 앞뒤 공백을 없앰

 

728x90
반응형

'공부 > Spring' 카테고리의 다른 글

스프링 웹개발 기초  (0) 2021.10.20
자바 - Map  (0) 2021.10.19
자바 - Set  (0) 2021.10.18
자바 - 제네릭  (0) 2021.10.14
자바 문법  (0) 2021.10.13
블로그 이미지

아상관없어

,
반응형

스택 : 후입선출

큐 : 선입선출

 

리스트는 동적할당으로 구현되어있어서 큐의 연산을 수행하기에는 효율적이지 않다.

데크를 사용하면 좋은 성능을 낼 수 있다.

 

스택 구현

class Node:
	def __init__(self, item, next):
		self.item = item
		self.next = next
class Stack:
	def __init__(self):
		self.last = None
	
    #맨 위에 집어넣음
	def push(self,item):
		self.last = Node(item, self.last)
	
	#맨 위값 꺼냄
	def pop(self):
		item = self.last.item
		self.last = self.last.next
		return item

 

  1. 유효한 괄호
'''
https://leetcode.com/problems/valid-parentheses
괄호로 입력된 값이 올바른지 판별하라.

ex)
Input: s = "()[]{}"
Output: true

Input: s = "([)]"
Output: false

Input: s = "{[]}"
Output: true

방법1)
last in fisrt out. 
열린 괄호를 만날때 스택에 push하고
닫힌 괄호를 만날때 pop
pop했을때 닫힌 괄호와 일치하는지 확인

'''



class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        table = { ')':'(', 
                 '}' : '{',
                ']' : '['
                }
        for char in s:
            #괄호에 해당되지 않을 경우 stack에 추가 X
            if char not in table:
                stack.append(char)
            #stack이 비어있지 않고, pop한 결과와 다르다면
            elif not stack or table[char] != stack.pop():
                return False
            
        #스택이 비어있지 않다면 쌓이기만 했으므로 false
        return len(stack) == 0

 

728x90
반응형

'공부 > 파이썬 알고리즘' 카테고리의 다른 글

문자열  (0) 2021.09.16
연결리스트  (0) 2021.09.15
배열  (0) 2021.09.15
etc  (0) 2021.07.01
파이썬 문법  (0) 2021.06.28
블로그 이미지

아상관없어

,

자바 - 제네릭

공부/Spring 2021. 10. 14. 18:33
반응형

제네릭

import java.io.Serializable;

public class CastingDTO implements Serializable{
	//타입이 Object이므로 어떤 타입이든 사용할 수 있다.
	priavet Object object;
	public void setObject(Object object){
		this.object=object;
	}
	
	public Object getObject(){
		return object;
	}
}

위 경우 Object타입을 인자로 받으므로 생성시 String이나 StringBuffer등 여러타입으로 생성할 경우 각각 타입으로 형 변환을 해야한다. 또한 여러 타입으로 생성시 각 객체가 어떤 타입인지 혼동 될 수 있다.

 

따라서 

import java.io.Serializable;

public class CastingGenericDTO<T> implements Serializable{
	priavet T object;
	public void setObject(T object){
		this.object=object;
	}
	
	public T getObject(){
		return object;
	}
}

T는 아무런 이름을 지정하여도된다.

<>안에는 현재 존재하는 클래스를 사용해되 되고, 존재하지 않는 것을 사용해도 된다.

되도록이면 클래스 이름의 명명 규칙과 동일하게 지정하는 것이 좋다.

선언 후 클래스 안에서 하나의 타입 이름처럼 사용하면된다.

만약 <>안의 이름이 현재 존재하는 클래스여야한다면, 컴파일시 에러가 발생할 것이다.

따라서, 가상 타입의 이름이라고 생각하면 된다.

 

 

제네릭을 사용하지 않을 경우 예시를 보면,

public class GenericSample{
	public static void main(String[] args){
		GenericSample sample = enw GenericSample();
		sample.checkCastingDTO();
	}
	
	public void checkCastingDTO(){
		CastingDTO dto1=new CastingDTO();
		dto1.setObject(new String());
		
		CastingDTO dto2=new CastingDTO();
		dto2.setObejct(new StringBuffer());
		
		CastingDTO dto3=new CAstingDTO();
		dto3.setObject(new StringBuilder());
	}
}

 

String temp1 = (String)dto1.getObject();
StringBuffer temp2 = (StringBuffer)dto2.getObject();
StringBuilder temp3 = (StringBuilder)dto3.getObject();

리턴 값으로 넘어는 타입은 Object이므로 형변환을 해주어야한다.

dto2의 인스턴스 변수 타입이 StringBuilder인지 StringBuffer인지 혼동된다면??

instanceof를 사용하여서 점검해도 되지만,

 

CastingGenericDTO클래스를 이용하여

public void checkGenericDTO(){
	CastingGenericDTO<String> dto1 = new CastingGenericDTO<String>();
	dto1.setObject(new String());
	
	CastingGenericDTO<StringBuffer> dto2 = new CasintgGenericDTO<StringBuffer>();
	dto2.setObject(new StringBuffer());
	
	CastingGenericDTO<StringBuilder> dto3 = new CasintgGenericDTO<StringBuilder>();
	dto3.setObject(new StringBuilder());
 }
String temp1 = dto1.getObject();
StringBuffer temp2 = dto2.getObject();
StringBuilder temp3 = dto3.getObject();

로 사용할 수 있다.

<>안에 타입을 명시적으로 적어주게되어 헷갈리지 않는다.

실행시에 타른 타입으로 잘못 형변환되어 예외가 발생하는 일이 없어진다

또한 형변환을 해주지 않아도 된다.

결과적으로 실행시 다른 타입으로 잘 못 형변환한하여 예외가 발생하는 일이 없어진다.

 

제네릭한 클래스의 타입만 바꾼다고 Overriding이 불가능하다.

public class WildcardGeneric<W>{
	W wildcard;
	public void setWildcard(W wildcard){
		this.wildcard = wildcard;
	}
	pubilc W getWildcard(){
		return wildcard;
	}
}

set, get하는 간단한 class이다

 

public class WildcardSample{
	public static void main(String[] args){
		WhildcardSample sample = new WildcardSample();
		sample.callWildcardMethod();
	}
	
	public void callWildMethod(){
		//String을 사용하는 제네릭한 객체를 생성함
		WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
		wildcard.setWildcard("A");
		wildcardMethod(wildcard);
	}
	
	public void wildcardMethod(wildcardGeneric<String> c){
		String value = c.getwildcard();
		System.out.println(value);
	}
}

 

 

만약 <String>이 아니라 WildcardGeneric<Integer>과 같이 선언된 객체를 받으려면??

불가능하다.

제네릭한 클래스의 타입만 바꾼다고 오버라이딩이 가능하진 않다.

public void wildcardMethod(WhildcardGeneric<?> c){ 
//<- ?로 적어주면 어떤 제너릭 타입이 되더라도 상관없다.
	Object value=c.getWildcard(); //정확한 타입을 모르므로, Object로 값을 받아야한다.
	System.out.println(value);
}

 

이렇게는 사용 불가하다.

	public void callWildMethod(){
		//알수 없는 타입에 String을 지정할 수 없다고 에러가 난다.
		WildcardGeneric<?> wildcard = new WildcardGeneric<String>();
		wildcard.setWildcard("A");
		wildcardMethod(wildcard);
	}

객체에 제너릭 타입으로 값을 지정하는 것은 불가능하다.

 

제네릭 선언에 사용하는 타입의 범위도 지정할 수 있다.

<? extends 타입>으로ㅓ 선택한다.

 

//Car

public class Car{
	protected String name;
	public Car(String name){
		this.name=name;
	}
	
	public String toString(){
		return "Car name="+name;
	}
}
//Bus

public class Bus extends Car{
	public Bus(String name){
		super(name);
	}
	public String toString(){
		return "Bus name="+name;
	}
}
//WildcardSample

public class WildcardSample{
	public static void main(String[] args){
		WhildcardSample sample = new WildcardSample();
		sample.callWildcardMethod();
	}
	
	public void callWildMethod(){
		//String을 사용하는 제네릭한 객체를 생성함
		WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
		wildcard.setWildcard("A");
		wildcardMethod(wildcard);
	}
	
	public void wildcardMethod(wildcardGeneric<String> c){
		String value = c.getwildcard();
		System.out.println(value);
	}
	
	public void callBoundedWildcardMethod(){
		//Car을 사용하는 제네릭 객체 생성
		WildcardGeneric<Car> wildcard = new WildcardGeneric<Car>();
		wildcard.setWildcard(new Car("BMW"));
		wildcardMethod(wildcard);
	}
	
	//?는 어떠한 타입이 와도 상관이 없다. 
	//제네릭 타입으로 Car를 상속받은 모든 클래스를 사용할 수 있다는 의미가 된다.
	//즉 반드시 Car클래스를 확장한 클래스가 넘어와야한다.
	public void boundedWildcardMethod(WildcardGeneric <? extends Car> c){
		Car value=c.getWildcard();
		System.out.println(value);
	}
}
public void callBoundedWildcardMethod(){
		//Bus을 사용하는 제네릭 객체 생성
		WildcardGeneric<Bus> wildcard = new WildcardGeneric<Bus>();
		wildcard.setWildcard(new Bus("Bus"));
		wildcardMethod(wildcard);
	}

callBoundedWildcardMethod()를 호출하면, Bus를 사용하는 제네릭한 객체를 넘겨주어도 실행이 잘 된다.

Bus클래스는 Car클래스를 상속받기 때문이다.

 

public class WildcardGeneric<W>{
	W wildcard;
	public void setWildcard(W wildcard){
		this.wildcard = wildcard;
	}
	pubilc W getWildcard(){
		return wildcard;
	}
}
//wildcardSample
...
public <T> void genericMethod(WildcardGeneric<T> c, T addValue){
	c.setWildcard(addValue);//값을 할당함
	T value = c.getWildcard();
	System.out.println(value);
}

//genericMethod 사용
public void callGenericMethod(){
	WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
	genericMethod(wildcard, "Data");//string을 사용하는 제네릭한 객체와 T타입의 변수
}

 

public <S, T extends Car > void genericMethod(WildcardGeneric<T> c, T addValue, S another){
	...
}
//도 가능하다.
//S와 T라는 제네릭 타입을 메소드에서 사용할 수 있다.

 

리턴타입 앞 로 제네릭 타입을 선언했다.

매개변수에는 제네릭타입이 포함된 객체를 받아서 처리했다.

메소드 선언시 리턴타입 앞에 제네릭한 타입을 선언해주고 그 타입을 매개 변수에서 사용하면 컴파일할 때 문제가 없다.

 

728x90
반응형

'공부 > Spring' 카테고리의 다른 글

스프링 웹개발 기초  (0) 2021.10.20
자바 - Map  (0) 2021.10.19
자바 - Set  (0) 2021.10.18
자바 - 컬렉션(List)  (0) 2021.10.15
자바 문법  (0) 2021.10.13
블로그 이미지

아상관없어

,

자바 문법

공부/Spring 2021. 10. 13. 21:10
반응형

배열

int [] nums;
int nums[];

int [] nums;
nums = new int[5];

int nums[] = {1, 2, 3}


 

생성자

생성자는 자바 클래스의 객체를 생성하기 위해 존재한다.

Test test = new Test();

생성자의 리턴타입은 클래스의 객체이다.

다른 생성자가 없을 경우 기본으로 컴파일할 때 만들어진다.

-> 다른 생성자가 있을 경우, 기본 생성자는 만들어지지 않음


 

메소드 overloading

이름은 같지만 매개변수가 다르다. 매개변수의 종류, 개수, 순서가 다르다.


 

static 메소드

객체를 생성하지 않아도 호출할 수 있다.

static => 클래스가 메모리에 올라갈때 할당된다.

 

자바에서는 하나의 클래스를 컴파일할 때, 관련된 클래스가 컴파일 되어있지않으면 알아서 컴파일해준다.


 

static 블록

static {
//딱 한번만 실행되는 코드
}

객체가 생성되기 전에 한번만 호출됨.

메소드 내에 선언 불가 => static 이므로

생성자가 호출되기 전에 static 블록들이 호출된다.

static한 변수, 메소드를 사용할 수 있다.


 

String

string은 참조형이지만

String b="b";//new를 쓰지않았지만 객체를 생성한것임
b="z";

String b=new String("b");
b=new String("z");

String b="b";
b=b+"z" //더하기 연산시 기존의 객체는 버리고 새로운 객체를 만든다.

 

매개변수 개수가 몇개일지, 호출될때마다 바뀔때 방법

public void calc(int... nums){
}

//호출시
calc(1, 2, 3, 4)
calc(1, 2)


OR

public void calc(String op, int... nums){
}

**여러 매개변수가 있을때, "타입... 변수명"은 항상 맨 뒤에 와야함.
**"타입... 변수명"다음엔 메소드의 선언을 닫는 소괄호가 와야한다.

 

패키지

클래스들을 구분짓는 폴더

어떤 클래스가 어떤 일을 하는지 혼동되는 것을 방지

  • 패키지 이름을 java로 시작하면 안된다.
  • 패키지 선언은 소스 하나에는 하나만 있어야한다.
  • 패키지 이름과 위치한 폴더이름이 같아야한다.
  • (자바 파일을 만든 폴더 이름과 선언된 패키지 이름이 다르면 파일을 찾지 못한다.)
  • 패키지 이름은 모두 소문자로 지정
  • 자바의 예약어를 사용하지 않는다.(com.int.util과 같이 int가 없어야함 )
package c.javapackage;//패키지 선언문

public class Package{
...
}

컴파일시 소스코드와 컴파일된 클래스가 같은 디렉토리에 존재하게 된다.

 

import를 이요하여 다른패키지에 접근할 수 있다.

import 패키지이름.클래스이름


 

import static

static한 변수와 메소드를 사용할때 용이

package c.sub;

public class Sub{
...

	public final static String CLASS_NAME="sub";
	public static void method(){
		System.out.println('sub method');
	}
}
//1. 그냥 import하여 사용
package c;

immport c.sub;

...
public static void main(Strings[] args){
	Sub.method();
	System.out.println(Sub.CLASS_NAME);
}
//2. import static 사용

package c;

import static c.sub.Sub.CLASS_NAME;
import static c.sub.Sub.method;

...
~~ main ~~ {
	method();
	System.out.println(CLASS_NAME);
}
...

static 변수나 메소드가 중복될때는 자신의 클래스에 있는 static 변수나 메소드가 import static으로 가져온 것보다 우선이다.


 

접근제어자

  • public : 누구나 접근
  • protected : 같은 패키지내에있거나 상속받은 경우 접근
  • package-private : 같은 패키지내에 잇을때 (접근제어자 없을경우 기본)
  • private : 해당 클래스내에서만 접근 가능

=> 말그대로 접근을 제어하기 위해 사용, 주로 직접 접근하지 않고 메소드를 통하여 변경이나 조회를 할 수 있도록 할 때 사용

static => 클래스 변수/ 메소드 밖, 클래스 안

자바에서는 하나의 클래스를 컴파일할 때 관련된 클래스가 컴파일 되어 있지 않다면, 알아서 컴파일 해줌

한번만 호출되어야 하는 코드가 있따면 static 블록 사용

public으로 선언된 클래스가 소스내에 있으면, 그 소스 파일의 이름은 public 인 클래스 이름과 동일해야함

하나의 클래스 소스에서 여러 클래스가 선언가능하다 (단, 같은 패키지 내에 있는 클래스만 이 클래스의 객체를 생성하고 사용할 수 있다.)

 

public으로 선언된 클래스가 소스내에 있으면, 소스파일의 이름은 public인 클래스 이름과 동일해야한다.


 

상속

부모 클래스에 선언된 public, protected로 선언된 모든 변수와 메소드를 내가 갖고 잇는 것처럼 사용

자바에서 아무런 상속을 받지 않으면, java.lang.Object 클래스를 확장한다.

자바는 이중상속을 받을 수 없지만, 여러 단계로 상속을 받을 수 있다.

ex) Object -> Parent -> Child

Object 클래스에 있는 메소드를 통해서 클래스의 기본적인 행동을 정의할 수 있음.

(기본적으로 갖추어야될 메소드들 ex) equals, toString, getClass ... )

자식 클래스의 생성자가 호출되면 자동으로 부모클래스의 매개변수 없는 생성자가 실행된다.

=>하나를 만들고 파생되는 것을 조금씩 바꾸면 편리함

 

parent 클래스에 매개변수를 받는 생성자만 있을경우(기본생성자는 자동으로 생성되지 않음) => 에러

child 클래스의 모든 생성자가 실행될때 parent의 기본 생성자를 찾기 때문이다.

따라서 super(매개변수)와 같이 부모 클래스의 생성자를 호출 시켜주면된다.

super은 반드시 생성자의 첫줄에 있어야한다.


 

메소드 오버라이딩(덮어씀)

접근제어자, 리턴타입, 메소드 이름, 매개변수 타입 및 개수 모두 동일

접근제어자는 확장되는것은 괜찮지만 축소되는것은 문제가 된다.

 

참조 자료형의 형변환

Parent parent = new Parent();
Child child = new Child();

//부모 클래스에선 자식 클래스의 메소드와 변수를 사용할 수 없다.
Child obj1 = new Parent();//parent로 생성시 child의 기능을 사용할 수 없다.

//자식클래스에선 부모 클래스의 메소드와 변수 사용가능
Parent obj = new Child();//child로 생성시 parent의 기능을 사용할 수잇다.


Child child = new Child();
Parent parent = child; //child는 paret의 기능을 사용할 수 있다, parent는 실제로 child 객체이다.
Child child2 =(Child)parent;//따라서 형변환이 가능하다.

부모 타입의 객체를 자식 타입으로 형 변환을 할 때에는 명시적으로 타입을 지저해 주어야 한다. 부모타입의 실제 객체는 자식 타입이어야만 한다.


 

다형성

Parent parent1 = new Parent();
Parent parent2 = new Child();
Parent parent3 = new Child2();

//printName메소드는 각 클래스별로 있으며 해당 타입의 이름을 출력
parent1.printName();
parent2.printName();
parent3.printName();

//출력: 각각 타입
parent1 - parent
parent2 - child
parent3 - child2

모두 parent 타입으로 선언되었는데 각 객체의 실제 타입은 다르다.

그리고 형변환을 하더라도 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다.

=> 원래 객체의 printName 메소드를 호출하여 각각 다른 결과가 나온다.


 

java.lang.Object

아무런 상속을 받지 않으면 java.lang.Object 클래스를 확장한다.

java.lang.Object는 모든 자바 클래스의 부모이다. => Object에 있는 클래스의 메소드를 통해서 클래스의 기본 행동을 정의할 수 있기 때문(이정도 메소드는 잇어야한다~)

다중 상속은 되지 않으나, 여러단계로 상속을 받을 수 있다.


 

인터페이스

자바에선 클래스 파일 이외에 interface, abstract 클래스가 있다.

존재이유 => 프로그램 설계단계에서 인터페이스를 정해놓으면 메소드 이름, 매개변수 등등을 고민하지 않아도 된다. 인터페이스형식에 맞추어 메소드를 구현하면 된다.

구현시 implements로

정의된 메소드를 모두 구현하여야 컴파일이 된다.

인터페이스의 변수는 public static final로 자동 선언된다.

메소드의 경우도 public abstract로 자동 선언된다.

선언시 class 대신 interface를 사용

public interface test {
	methods....
}
MemberManagerInterface manager = new MemberManagerInterface();
//아무것도 구현해놓지 않은 인터페이스로 초기화 하려고하여 에러가남

MemberManagerInterface manager = new MemberManager();
//실제 구현은 MemberManager에 되어있으므로 실제 타입은 MemberManager에

 

abstract 클래스

마음대로 초기화하고 실행할 수 없다.

abstarct 클래스를 구현해 놓은 클래스로 초기화 및 실행이 가능하다.

선언시 class앞에 abstract를 사용

public abstarct class test{
	public abstract boolean addMemeber(MemberDTO member);
	public void test(){
		System.out.println("test");
	}
	...
}

abstract는 추상 메소드가 하나라도 있으면 사용한다.

인터페이스와 달리 구현되어 있는 메소드가 있어도 상관없다.

구현이 아니라 상속을 하여 사용하면된다.

extends하여 abstract한 메소드를 사용하면된다.

사용하는 이유는 어떤 메소드는 미리 만들어 놓아도 문제가 없는 경우가 발생한다.

하지만 해당 클래스를 만들기는 애매할 때, 공통적인 기능을 미리 구현하면 도움이 된다.

 

abstract의 경우 abstract 메소드(구현안된 메소드)가 하나라도 있을경우 class 예약어 앞에 붙인다.

상속과 동일하게 extends를 사용하면 된다.

어떤 메소드는 미리 만들어도되지만 클래스를 만들기는 애매할때 공통적인 기능을 미리 구현해놓으면 도움이 된다.


 

 

final

final의 경우 상속해줄수 없다.

확장해서는 안되는 클래스, 상속받아서 내용을 변경해서는 안되는 클래스 선언시 사용

기본 속성이 변경되면 안되는 클래스에 사용한다.

메소드 또한 final로 선언시 overriding할 수 없게 된다.

인스턴스 변수나 static으로 선언된 클래스 변수의 경우 생성과 동시에 초기화를 해주어야한다. 생성자나 메소드에서 초기화시, 중복되어 변수값이 선언될 수 있다.

하지만, 매개변수의 경우 이미 초기화가 되어 값이 넘어오고

지역변수의 경우 메소드를 선언하는 중괄호 내에서만 참조되므로 다른곳에서 변경할 일이 없으므로 컴파일시 문제발생X

 

객체를 final로 선언시 객체안의 변수들은 제약이 없다. final로 선언되어 있지 않으면

=> 해당 클래스가 final이라고 그 안의 인스턴수 변수나 클래스 변수가 무조건 final로 선언된건 아니다.


 

enum

상수의 집합이다.

클래스의 일종

public enum Time{
	THREE,
	FIVE,
	EIGHT;
}
...
	public int timecheck(Time value){
		switch(value){
			case THREE:
				System.out.println("3")
			...
		}
	}

 

main

~~ ~~~ main(~~){
	Time test = new Time();
	test.timecheck(Time.THREE);
}

클래스이름.상수이름 을 넘겨준다.

생성자를 만들 수 있지만, 생성자를 통하여 객체를 생성할 수는 없다.

 

enum클래스 선언시 각 상수의 값을 지정할 수 있다.

public enum Time{
	THREE(1000),
	FIVE(3000),
	private final int amount;
	
	Time(int amount){
		this.amount= amount;
	}
}

enum클래스의 생성자는 아무것도 명시하지 않는 pacakge-private와 private만 접근 제어자로 사용할 수 있다.

=> 각 상수를 enum 클래스내에서 선언할 때에만 이 생성자를 사용할 수 있다.

 

  • enum 클래스는 다른 클래스와 같이 컴파일 할때 생성자를 자동을 만들어준다.

 

 

Nested 클래스

클래스 안의 클래스

코드를 간단하게 표현하기 위해서

주로 UI처리시 사용자의 입력이나 외부의 이벤트에 대한 처리를 하는 곳에서 많이 사용됨

 

static으로 선언시 static nested 클래스 => 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을때

아닌경우 내부클래스이다. => 캡슐화가 필요할 때(A클래스의 private 변수접근을 위한 B클래스를 선언하고, B클래스를 외부에 노출시키고 싶지 않을 경우), 다른클래스에서 전혀 필요가 없을 경우

내부클래스는 로컬내부클래스와 익명내부클래스로 구분된다.


 

 

static nested class

내부 클래스는 감싸고 있는 외부 클래스의 어떤 변수도 접근할 수 있다. 심지어 private 변수까지

하지만 "Static"하기 때문에 그렇게 사용하는 것은 불가능하다.

public class Test{
	staic class StaticTest{
		public void setVal(int val){
			...
		}
		...
	}
}

내부 static 클래스 객체 생성?

public class InterClassTest{

	public static void main(String[] args){
		InterClassTest sample = new InterClassTest();
		sample.StaticObject();
	}
	
	public void StaticObject(){
		Test.StaticTest staticNested = new Test.StaticTest();
		staticNested.setVal(1230);
	}
}

감싸는 클래스.내부클래스 로 생성한다.

 

사용하는 이유

예로 학교를 관리하는 school 클래스를 만들고 대학을 관리하느 univ클래스를 만들면,

student는 어디 학생인지 모름

따라서 school안에 static nested 클래스 student를 만들면 용도가 명확해짐

또한 해당 클래스는 univ에서 사용 불가함.


 

 

내부 클래스와 익명클래스

public class Test{
	 class Inner{
		public void setVal(int val){
			...
		}
		...
	}
}

 

생성시 감싸는 클래스의 객체를 만들어야한다.

public class InterClassTest{

...
	public void InnerObject(){
		Test outer = new Test();//감싸는 클래스 객체 생성
		Test.Inner inner = outer.new Inner();//객체를 통해서 생성
	}
...

}

=>다른 클래스에서는 그 클래스가 전혀 필요 없을때 이러한 내부클래스 사용

 

예를 들어 gui에서 버튼 클릭 이벤트 처리시 내부 클래스를 만드는 것이 편이함(따로 클래스를 만드는것 보다)

하지만 익명클래스를 사용하면 더 편리함

 

  1. 내부 클래스 사용방법
package a.inner;

public interface EventListener{
	public void onClick();
}

 

package c.inner;

public class MagicButton{
	public MagainButton(){
	
	}
	//EventListener는 인터페이스이다.
	private EventListener listener;
	public void setListener(EventListener listener){
		this.listener=listener;
	}
	public void onClickProcess(){
		if(listener!=null){
			listener.onClick();
		}
	}
}
package c.inner;

public class NestedSample{
	...
	public void setButtonListner (){
		MagicButton button = new MagicButton();
		MagicButtonListener listener = new MagicButtonListener();
		button.setListener(listener); //<- 별도 클래스를 넘기지 않고 밑의 내부 클래스를 넘김
		button.onClickProcess();
	}
}
package c.inner;

public class NestedSample{
	...
	//내부 클래스
	class MagicButtonListener implements EventListener {
		public void onClick(){
			System.out.println("clicked")
		}
	}
}

 

  1. 익명 클래스 사용방법
package c.inner;

public class NestedSample{
	...
	public void setButtonListner (){
		MagicButton button = new MagicButton();
		//MagicButtonListener listener = new MagicButtonListener();
		//button.setListener(listener); //<- 별도 클래스를 넘기지 않고 밑의 내부 클래스를 넘김
		
		//익명 클래스 생성로 인자 넘김
		button.setListener(new EventListener(){
			public void onClick(){
				System.out.println("button clicked");
			}
		});
		
		button.onClickProcess();
	}
}

new EventListener로 생성자 호출 후 바로 중괄호를 열고 메소드 구현

클래스 이름도 없고 객체 이름도 없어서 다른곳에서 참조할 수는 없다.

 

사용이유

클래스를 많이 만들면 메모리가 많이 필요해짐. 따라서 내부, 익명클래스로 간단하게 객체를 생성할 수 잇다.

클래스 개수를 줄일 수 있으면 좋으니까

또한 다른 클래스에서 재사용할 일이 없을때 유용하다.


 

 

예외

예외적인 일이 발생한 경우

잘못을 컴파일할 때 점검해주지 않으므로 예외처리 해야한다.

try{
...
}
catch (Exception e){
...
}

예외가 발생하는 부분만 묶어주면 된다.

try 블록 안에서 예외가 발생되면 그 이하의 문장은 실행되지 않고 바로 catch로 넘어간다.

그리고 try-catch 구문 밖에 잇는 문장이 실행된다.

 

try에서 선언한 변수를 catch에서 사용할 수 없다.

따라서 일반적으로 catch 문장에서 사용할 변수는 try 앞에 선언한다.

 

모든 예외 객체의 부모 클래스는 java.lang.Exception 클래스이다.

따라서 만약 catch가 여러개인 경우,

부모 예외 클래스가 잡고 자식 클래스가 잡도록 되어있으면 자식 클래스가 예외를 처리할 수 없다.

즉, 위에서 먼저 catch해버리므로 그 다음에서 catch할 수 없다.


 

Throw

모든 예외는 java.lang.Throwable 클래스를 상속받는다.

이러한 이유는 모두 동일한 이름의 메소드를 사용하여 처리할 수 있게 하기 위함이다.

 

예외를 발생시키기 위해서 Throw를 사용한다.

 

try{
	throw new Exception("wrong input");
	...
}

catch (Exception e){
	e.printStrackTrace();
}

try블록 내에서 throw라고 명시한 수 개발자가 예외 클래스의 객체를 생성하면 된다.

throw한 문장 이후에 있는 모든 try 블록 내의 문장들은 수행되지 않고 catch로 이동한다.

 

또한 예외가 발생된 메소드를 호출한 메소드로 던질수도 있다.

public class TestException {
	public void throwException(int num) throws Exception{

			if(num<0){
				throw new Exception("wrong");
			}

	}
}

try-catch로 묶지 않았지만,

throws가 선언되어 있기 때문에 throwException 메소드를 호출한 메소드에선 try-catch블록으로 감싸어서 예외를 받아서 처리해야한다.

 

try{
	test.throwException(-1);
}
catch(Exception e){
  ....
}

 

여러가지 예외를 던질수도 있다.

public void multiThrow() throws NullPointerException, ArrayIndexOutOfBoundsExceptioin{
	...
} 

 

예외를 만들수도 잇다.

public class MyException extends Exception{
	...
}

 

finally

어떠한 경우에도 반드시 실행

try{

}
catch (Exception e){

}
finally{

}

예외가 발생하면 catch가 실행되고 finally 블록이 실행되고 try-catch 이후 문장이 실행된다.

finally는 예외와 상관없이 실행된다.

코드의 중복을 피하기 위해서 반드시 필요하다.


 

어노테이션

클래스나 메소드 선언시 @를 사용함

메타데이터라고도 함

  • 컴파일러에게 정보를 알려줌
  • 컴파일할 때와 설치시의 작업을 지정
  • 실행시 별도의 처리가 필요할때

 

미리정해진 어노테이션은 3개

  • @Override즉, 메소드가 override되엇으니 잘못코딩되면 컴파일ㄹ러에게 알려달라고 한다.
  • 해당 메소드가 부모 클래스에 잇는 메소드를 override 했다고 명시적으로 선언
  • @Deprecated
  • 미리 만들어진 클래스나 메소드가 더이상 쓰이지 않을때 알려줌
  • @SuppressWarnings
  • 컴파일러에서 경고를 주는 경우, 일부러 이렇게 코딩햇으니 괜찮다고 알려줌

 

메타 어노테이션

어노테이션을 선언할때 사용

  • @Target
  • 어노테이션을 어떤 것에 적용할지 선언할때 사용
  • @Retention
  • 얼마나 오래 어노테이션 정보가 유지되는지 선언
  • @Documneted
  • 해당 어노테이션에 대한 정보가 javadocs 문서에 포함된다는 것을 선언
  • @Inherited
  • 모든 자식 클래스에서 부모 클래스의 어노테이션을 사용할 수 있다는 것을 선언
  • @interface
  • 어노테이션을 선언할때 사용

 

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
//이 어노테이션은 메소드에 사용할 수 있다고 지정

@Retention(RetentionPolicy.RUNTIME)
//실행시에 이 어노테이션을 참조하게 됨. 

//UserAnnotation이라는 어노테이션 선언
//@interface를 앞에 붙여서 사용시 @UserAnnotation으로 사용할 수 있다.
public @interface UserAnnotation {
	public int number();
	//메소드처럼 어노테이션 안에 선언해두면, 이 어노테이션을 사용할 때 해당항목에 대한 타입으로 값을 지정해야한다.
	public String text() default "This is first annotation";
	//default를 사용할 경우 뒤에 있는 값이 이 어노테이션을 사용할때 별도로 값을 지정해 주지 않을 때의 값이 된다.
}



 

사용시

만든 어노테이션의 target이 메소드였으므로 대상은 메소드 뿐이다.

public class Sample{
	@UserAnnotation(number=0)
	public static void main(String args[]){
		Sample sample = new Sample();
	}

	@UserAnnotation(number=3, text="Test")
	public void annotationexam(){
		
	}
}

 

 

 

 

728x90
반응형

'공부 > Spring' 카테고리의 다른 글

스프링 웹개발 기초  (0) 2021.10.20
자바 - Map  (0) 2021.10.19
자바 - Set  (0) 2021.10.18
자바 - 컬렉션(List)  (0) 2021.10.15
자바 - 제네릭  (0) 2021.10.14
블로그 이미지

아상관없어

,
반응형

https://leetcode.com/problems/reverse-string/

  1. Reverse String
class Solution:
    def reverseString(self, s: List[str]) -> None:
        """
        Do not return anything, modify s in-place instead.
        """
        left, right = 0, len(s)-1
        while left < right:
            s[left], s[right] = s[right], s[left]
            left +=1
            right -=1

https://leetcode.com/problems/reorder-data-in-log-files/

  1. Reorder Data in Log Files
class Solution:
    def reorderLogFiles(self, logs: List[str]) -> List[str]:
        '''
        로그의 가장 앞 부분은 식별자
        문자로 구성된 로그가 숫자 로그보다 앞에 온다.
        식별자는 순서에 영향을 끼치지 않지만, 문자가 동일할 경우 식별자 순으로 한다.
        숫자로그는 입력 순서대로 한다.
        => 1. 숫자보다 문자 로그 먼저
        => 2. 문자 동일할 경우 식별자 순(식별자는 로그 가장 앞)
        '''

        letters, digits=[], []


        for log in logs:
            #로그가 숫자라면 (log.split()는 string을 반환)
            if log.split()[1].isdigit():
                digits.append(log)

            #로그가 문자라면
            else:
                letters.append(log)
        #문자일 경우 로그기준으로 정렬, 식별자를 뒤로, 문자 동일할 경우 식별자 순으로 정렬   
        letters.sort(key=lambda x: (x.split()[1:], x.split()[0]))

        #문자+숫자
        return letters+digits

https://leetcode.com/problems/most-common-word/

\19. Most Common Word

'''
금지된 단어를 제외한 가장 흔하게 등장하는 단어를 출력하라
대소문자를 구분하지 않으며, 구두점(마침표, 쉼표) 또한 무시한다.

Input: paragraph = "Bob hit a ball, the hit BALL flew far after it was hit.", banned = ["hit"]
Output: "ball"

1. 데이터 처리 => 대소문자 없애고, 구두점 없앰
2. 빈도수 체크
'''

class Solution:
    def mostCommonWord(self, paragraph: str, banned: List[str]) -> str:

        #cleaning
        #\w은 단어문자를 뜻하며 ^은 not을 의미한다.
        #문자열 앞에 r이 붙으면 해당 문자열이 구성된 그대로 문자열로 반환
        #즉 문자가 아닌 모든 문자를 공백으로 치환한다.
        #re.sub('패턴', '바꿀문자열', '문자열', 바꿀횟수)
        #words에는 cleaning된 word가 순차적으로 들어가게됨
        '''
        # 리스트 안에서 for 문 사용하기 1
        list = [ num * 3 for num in list ]
        '''

        #\w, 즉 단어 문자가 아니면 공백으로 치환하고 lower, split함
        words =[word for word in re.sub(r'[^\w]',' ', paragraph).lower().split()
            if word not in banned] #금지된 단어에 당되는 경우엔 포함X


        #개수 세기
        #딕셔너리 사용하여 횟수 셈
        #int형 딕셔너리 생성
        counts = collections.defaultdict(int)
        for word in words:
            counts[word] += 1 #해당되는 word 수 셈

        #딕셔너리 변수 counts에서 값이 가장 큰 키를 가져옴
        #max함수에 key를 지정하여 argmax를 간접적으로 추출
        '''
        >>> a = {'name':'pey', 'phone':'0119993323', 'birth': '1118'}
        >>> a.get('name')
        'pey'
        '''

        return max(counts, key=counts.get)

        #most_common(1)사용 가능 => 최빈값 1개 반환(리스트에 담긴 튜플 형태로
        '''
        counts=collections.Counter(words)
        return counts.most_common(1)[0][0] #=> [('ball', 2)]이므로
        '''

https://leetcode.com/problems/group-anagrams/submissions/

'''
애너그램 = 재배열하여 다른 뜻을 가진 단어로 바꾸는것

Input: strs = ["eat","tea","tan","ate","nat","bat"]
Output: [["bat"],["nat","tan"],["ate","eat","tea"]]

방법
1. strs의 단어들을 정렬하여 같은지 비교



'''


class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        #딕셔너리 생성
        anagrams = collections.defaultdict(list)

        for word in strs:
            #정렬하여 key, value딕셔너리에 삽입
            #list에서 문자열로 변환 -> 리스트(List)를 특정 구분자를 포함해 문자열(String)으로 변환
            anagrams[''.join(sorted(word))].append(word)

        #values() -> 모든 value들을 출력
        return list(anagrams.values())

https://leetcode.com/problems/longest-palindromic-substring/

'''
가장 긴 펠린드롬 부분 문자열을 출력

Input: s = "babad"
Output: "bab"

방법
1. 처음, 끝 비교 => 안맞으면 끝에서 2번째부터 계속 비교
(대칭되게 비교해야함 => aa, aaa, aaaa, aaaaa)
(1)b - d (x)
(2)b - a (x)
(3)b - b (O)
(4)길이가 3이므로 비교  X => 끝

(1)a - d
(2)a - a
.
.
.

2. 홀수칸, 짝수칸을 처음부터 이동 => 대칭되면 확장
더 효율적
'''



class Solution:
    def longestPalindrome(self, s: str) -> str:

        #s[left]==s[right]일 경우 범위 늘려가며 비교
        def expand(left:int, right:int)->str:
            while left >=0 and right < len(s) and s[left]==s[right]:
                left -= 1
                right += 1
            #left -= 1 했으므로 left+1, list범위는 right-1까지이므로 right
            return s[left+1:right]

        '''
        Input: s = "ac"
        Output: "a"
        '''
        #s가 짧거나 s자체가 펠린드롬일 경우 처리
        if len(s) < 2 or s==s[::-1]:
            return s


        #펠린드롬이 없는 경우
        result = ''

        for i in range(len(s) - 1):
            #max기준 len
            #홀수인 경우, 짝수인 경우
            result = max(result, expand(i, i+1), expand(i, i+2), key=len)

        return result
728x90
반응형

'공부 > 파이썬 알고리즘' 카테고리의 다른 글

스택과 큐  (0) 2021.10.14
연결리스트  (0) 2021.09.15
배열  (0) 2021.09.15
etc  (0) 2021.07.01
파이썬 문법  (0) 2021.06.28
블로그 이미지

아상관없어

,
반응형

* 펠린드롬 연결리스트

연결리스트가 펠린드롬 구조인지 판별하여라

https://leetcode.com/problems/palindrome-linked-list/


'''
펠린드롬 = 뒤집어도 같아야함

연결리스트가 펠린드롬 구조인지 판별하여라

1. 리스트를 이용하여 앞뒤 비교 = pop(0)?pop()
2. fast, slow runner활용
2.1 fast가 끝까지 가면 slow는 절반만큼 가게됨
2.2 slow는 이동하면서 역순으로 rev에 값을 저장
2.3 rev와 남은 slow가 가야하는 부분을 비교
2.4 홀수개일 경우 중앙값을 포함하면 안된다. 따라서 slow를 한칸 더 옮긴다.

'''
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        '''
        q = []
        q: Deque = collecitons.deque()

        if not head:
            return True

        node = head

        #리스트 변환
        while node is not None:
            q.append(node.val)
            node = node.next

        #펠린드롬 변환
        while len(q) > 1:
            if q.pop(0) != q.pop():
                return False
        return True
        '''

        if not head:
            return True

        fast = slow = head
        rev = None

        #절반만큼 이동/ 펠린드롬이 짝수 or 홀수 이므로 fast, fast.next값을 비교
        while fast and fast.next:
            #fast는 두칸씩 이동
            fast = fast.next.next
            #slow는 한칸씩 이동, rev에 역순으로 값 저장
            rev, slow, rev.next = slow, slow.next, rev

        #짝수일 경우 fast의 값은 None이 되고 홀수일 경우 값을 가지게 된다.
        if fast:
            slow = slow.next
        #rev와 남은 값 비교
        while rev and rev.val == slow.val:
            slow, rev = slow.next, rev.next

        #모든 값이 맞다면 rev는 none일 것이다.
        return not rev

 

 

 

 

* 정렬되어 있는 두 연결 리스트를 합쳐라

https://leetcode.com/problems/merge-two-sorted-lists/


'''
정렬되어 있는 두 연결 리스트를 합쳐라

Input: l1 = [1,2,4], l2 = [1,3,4]
Output: [1,1,2,3,4,4]

방법1)
1번째 연결리스트를 기준으로 두고 2번째 연결리스트의 값을 비교하여 삽입
1번째 연결리스트의 값과 같거나 크면 뒤로, 작으면 앞으로 => n^2만큼 비교해야..

방법2)
두 연결리스트를 합친뒤 정렬하기
정렬방법.. merge sort로 (nlogn)

'''

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def mergeTwoLists(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:

        #l1이 l2보다 크면 swap => 재귀로 반복
        if (not l1) or (l2 and l1.val > l2.val):
            l1, l2 = l2, l1

        #l1이 none 아니면, 재귀 호출
        if l1:
            l1.next = self.mergeTwoLists(l1.next, l2)

        return l1

 

 

 

 

* 연결리스트를 뒤집어라

https://leetcode.com/problems/reverse-linked-list/


'''
연결리스트를 뒤집어라

Input: head = [1,2,3,4,5]
Output: [5,4,3,2,1]

방법1)
러너문제 활용
rev, rev.next, slow = slow, rev, slow.next

방법2)
재귀활용
rev에 저장하게..

반복문을 사용하는것이 공간복잡도가 낮고 시간도 빠르다..

'''


# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:

        result = None
        temp = head

        while temp:
            temp, result, result.next = temp.next, temp, result

        return result


        '''
        def reverse(temp: ListNode, rev: ListNode = None):

            if not temp:
                return rev

            rev, temp, rev.next = temp, temp.next, rev

            return reverse(temp, rev)

        return reverse(head)
        '''

https://leetcode.com/problems/add-two-numbers/

다시....

 

 

 

 

* 홀짝 연결 리스트

연결리스트를 홀수 노드 다음에 짝수 노드가 오도록 재구성하라. 공간 복잡도 O(1), 시간 복잡도 O(n)에 풀이하라.

https://leetcode.com/problems/odd-even-linked-list/


nput: head = [1,2,3,4,5]
Output: [1,3,5,2,4]

Input: head = [2,1,3,5,6,4,7]
Output: [2,3,6,7,1,5,4]

홀수번째 노드가 짝수번째 노드 뒤에 와야한다.

방법1) 리스트로 바꾸고 슬라이싱으로 처리...는 파이썬 내장 함수로 간단...

방법2) 홀수, 짝수 노드 따로 구분하여 붙이기
1->2->3->4->5

  1. 1->3
  2. 2->4
  3. 3->5
  4. 5->2
    odd = 1->3->5
    even = 2->4
  5. 1->3->5->2->4

체크
=> a.next 가 null인가? 확인
=> 2->4노드에서 4->null이 되어야함

홀: odd.next = (even.next = odd.next.next)
짝: even.next = (odd.next = even.next.next)

시작
odd : head
even : head.next

마지막

  # Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:

        if head is None:
            return head

        #odd, even으로 구분
        odd = odd_start = head
        even = even_start = head.next


        #while odd and odd.next:이 아니라 even을 비교하여야한다... odd일 경우 None.next에 값을 넣게 되버림(한번에 두개씩 바꾸므로)

        while even and even.next:
            odd.next, even.next = even.next, even.next.next
            #odd를 다음 odd로, even을 다음 even으로
            odd = odd.next
            even = even.next

        #odd->even 연결
        odd.next = even_start

        return odd_start   

728x90
반응형

'공부 > 파이썬 알고리즘' 카테고리의 다른 글

스택과 큐  (0) 2021.10.14
문자열  (0) 2021.09.16
배열  (0) 2021.09.15
etc  (0) 2021.07.01
파이썬 문법  (0) 2021.06.28
블로그 이미지

아상관없어

,
반응형

https://leetcode.com/problems/trapping-rain-water/

'''
    높이를 입력받아 비 온 후 얼마나 많은 물이 쌓일 수 있는지 계산
    Input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
    Output: 6

    방법1)
        최대 높이 - 현재 높이 = 물이 고인 양

    방법2)
        현재 높이가 이전 높이 보다 높을 때 

'''
class Solution:
    def trap(self, height: List[int]) -> int:
        if not height:
            return 0

        #양쪽에서 시작하여 최대높이 까지
        volume = 0
        left, right = 0, len(height) - 1
        left_max, right_max = height[left], height[right]


        #좌우가 만날때 종료
        while left < right:
            #좌우 최대값 입력
            left_max, right_max = max(height[left], left_max), max(height[right], right_max)

            #좌우 높은 쪽으로 이동하므로,오른쪽이 크면 왼쪽이 이동, 왼쪽이 크면 오른쪽이 이동 
            #따라서 최대 지점에서 만나게됨
            if left_max <= right_max:
                volume += left_max - height[left]
                left += 1
            else:
                volume += right_max - height[right]
                right -= 1
        return volume

https://leetcode.com/problems/3sum

'''
배열을 입력받아 합으로 0을 만들 수 있는 3개의 요소를 출력하라
Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]

방법1)
두요소의 합*(-1) 에 해당되는 값이 있는지 확인한다.
-1 => [0, 1, 2, -1, -4]
=> 모든 요소를 다 더해봐야함 = n^3

방법2)
정렬을 한 뒤 비교(투포인터 방식으로 비교) => nlog(n) + n^2
1. 정렬
2. 기준점을 둠, 나머진 투포인터 방식으로 비교(left, right지정)
3. 중복값이 있을경우 넘어감(계산낭비)

'''

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        result=[]
        #1.정렬
        nums.sort()

        #2.기준값 두고 비교(마지막 값 2개는 사용하지 않음)
        for i in range(len(nums)-2):
            #중복값일 경우 제외
            if i > 0 and nums[i] == nums[i-1]:
                continue    

            left, right = i + 1, len(nums)-1

            while left < right:
                sum = nums[i] + nums[left] + nums[right]
                if sum < 0:
                    left += 1
                elif sum > 0:
                    right -= 1
                else:
                    result.append([nums[i], nums[left], nums[right]])

                    #현재 값과 같은 값을 가질 경우 중복 제거
                    #왼쪽확인
                    while left < right and nums[left] == nums[left + 1]:
                        left += 1
                    #오른쪽 확인
                    while left < right and nums[right] == nums[right - 1]:
                        right -= 1
                    #확인이 끝난 후 각 값을 다음번째 값으로 옮김
                    left += 1
                    right -= 1


        return result

https://leetcode.com/problems/array-partition-i/

'''
n개의 페어를 이용한 min(a,b)의 합으로 만들 수 있는 가장 큰 수를 출력하라.
Input: nums = [1,4,3,2]
Output: 4

Explanation: All possible pairings (ignoring the ordering of elements) are:
1. (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3
2. (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3
3. (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4
So the maximum possible sum is 4.

방법1) 
정렬한 뒤, 뒤에서부터 2개씩 묶어 계산 = nlogn

방법2)
짝수번째 값이 항상 min값이 됨

방법3)
슬라이싱 사용

'''
class Solution:
    def arrayPairSum(self, nums: List[int]) -> int:
        '''
        nums.sort()
        result = 0
        pair = []
        '''

        '''
        =====================================================
        방법 1)
        =====================================================
        '''
        '''
        387ms = 속도 더 나음
        for i in range(len(nums)-1, 0, -2):
            #배열의 개수가 홀수인 경우
            if i != 0:
                result += min(nums[i], nums[i-1])
        '''


        '''
        514ms = 가독성 좋음
        for n in nums:
            pair.append(n)
            if len(pair) == 2:
                result += min(pair)
                pair = []
        '''

        '''
        =====================================================
        방법2) 276ms
        =====================================================
        '''
        '''
        if len(nums)%2 == 0:
            for i, n in enumerate(nums):
                if i%2 == 0:
                    result += n
        else:
            for i, n in enumerate(nums):
                if i%2 != 0:
                    result += n

        return result
        '''

        '''
        =====================================================
        방법3) 314ms
        =====================================================
        return sum(sorted(nums)[::2])
        '''

https://leetcode.com/problems/product-of-array-except-self/

'''
배열을 입력받아 output[i]가 자신을 제외한 나머지 모든 요소의 곱셈 결과가 되도록 출력하라.
단 나눗셈을 하지 않고 O(n) 에 풀어라

Input: nums = [1,2,3,4]
Output: [24,12,8,6]

방법1)
자기자신 제외하고 나머지 값들 곱함
자기 자신 제외하고 왼쪽 오른쪽 구분하여 서로 곱함

1 2 3 4

1. 1*(2*3*4)
2. (1)*(3*4)
3. (1*2)*(4)
4. (1*2*3)*1

왼쪽 1, 1, 2, 6
오른쪽 24, 12, 4, 1


'''

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        result = []
        tmp = 1

        #왼쪽
        for i in range(len(nums)):
            result.append(tmp)
            tmp = tmp * nums[i]

        tmp = 1
        #오른쪽 => 반대로
        for i in range(len(nums)-1, -1, -1):
            result[i] = result[i] * tmp
            tmp = tmp * nums[i]

        return result

https://leetcode.com/problems/best-time-to-buy-and-sell-stock/

'''
주식을 사고팔기 가장 좋은 시점

한번의 거래로 낼 수 있는 최대 이익을 산출하라.
Input: prices = [7,1,5,3,6,4]
Output: 5
2일날 1에 사서 5일날 6에 팔면 최대의 이익이다.

방법1)
현재 값보다 다음값이 크면, 현재 이후의 값중 최대값을 구하여 이익을 계산 => 예시의 경우 
1->5, 3->6 두가지의 경우를 계산해야한다.

방법2) 방법1 복잡도 줄여야
최소값과 최대 이익값을 갱신하면서 마지막에 차이를 구함


'''


class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        '''
        방법1) 시간 초과남
        max_profit = -1
        profit = 0

        leng = len(prices)

        for i in range(leng):
            if i < leng-1 and prices[i] < prices[i+1]:
                profit = max(prices[i:]) - prices[i]

            if max_profit < profit:
                max_profit = profit
        '''

        profit = -sys.maxsize-1
        min_price =  sys.maxsize

        #갱신
        for i in range(len(prices)):
            min_price = min(min_price, prices[i])
            profit = max(profit, prices[i]-min_price)

        return profit
728x90
반응형

'공부 > 파이썬 알고리즘' 카테고리의 다른 글

문자열  (0) 2021.09.16
연결리스트  (0) 2021.09.15
etc  (0) 2021.07.01
파이썬 문법  (0) 2021.06.28
파이썬 기본 문법  (0) 2021.05.18
블로그 이미지

아상관없어

,
반응형

isdigit(), isnumeric() => 글자가 숫자모양이면 true (3²같은 제곱과 같은 형태 가능)

isdecimal() => 주어진 무나졍리 int형으로 변환 가능한지 (3²같은 제곱과 같은 형태 불가능)

 

 

for _ in range(10):

_뜻은 변수가 없다는 뜻

 

 

input().split()시

split의 기본값이 ' ' 공백이다.

 

list생성시

a = []

a = list(range(10)) 과같이 가능

 

 

SWAP 시 temp 불필요

a = 3

b = 'abc'

temp = a

a = b

b = temp

파이썬에서는 다음과 같이 한 줄로 두 값을 바꿔치기할 수 있다.

a = 3 b = 'abc'

a, b = b, a 

 

 

is => ID비교

== => 값 비교

 

728x90
반응형

'공부 > 파이썬 알고리즘' 카테고리의 다른 글

문자열  (0) 2021.09.16
연결리스트  (0) 2021.09.15
배열  (0) 2021.09.15
파이썬 문법  (0) 2021.06.28
파이썬 기본 문법  (0) 2021.05.18
블로그 이미지

아상관없어

,
반응형

def fn(a) :

a의 타입이 무엇인지 몰라 버그 유발 가능

def fn(a: int) -> bool:

a가 정수형임을 알리고 리턴값이 true or false임을 알림

리스트 컴프리헨션 : 기존 리스트를 기반으로 새로운 리스트를 만들어내는 구문

[n*2 for n in range(1, 10+1) if n&2==1]
# [2,6,10,14,18]

#리스트 컴프리헨션을 사용하지 않는 경우
a=[]
for n in range(1, 10+1):
    if n % 2 == 1:
        a.append(n*2)

print(a)
# [2,6,10,14,18]

리스트외에도 딕셔너리등이 가능함

a={}
for key, value in original.items():
    a[key]=value


 #한줄로 표현 가능
 a = {key: value for key, value in original.items()}


제너레이터 : 루프의 반복 동작을 제어할 수 있는 루틴


ex) 숫자 1억개를 만들어내 계산하는 프로그램

! 제너레이터가 없으면 메모리 어딘가에 1억개의 숫자를 보관해야함

제너레이터를 생성해두고 필요할때 언제든 숫자를 만들 수 있음

1억개 숫자중 100개만 사용하는 경우, 제너레이터를 사용하는 것이 나음

#함수의 리턴값은 제너레이터가 됨
def get_natural_number():
    n = 0
    #while true이므로 계속해서 값을 내보냄
    while True:
        n+=1
        yield n

yield구문을 사용하면 제너레이터를 리턴할 수 있다.

(기존함수는 return 구문을 만나면 값을 리턴하고 함수의 동작을 종료함)

yield는 제너레이터가 여기까지 실행중이던 값을 내보낸다는 뜻

중간값을 리턴한 다음 함수는 종료되지 않고 끝까지 실행됨

g = get_natural_number()
for _ in range(0. 100):
    print(next(g)) #next로 값을 추출 할 수 있다.

 #1
 #2
 #.
 #.
 #.
 #99
 #100

여러 타입의 값을 하나의 함수에서 생성하는 것도 가능

def generator():
    yield 1
    yield 'string'
    yield True


g = generator()

next(g) #1
next(g) #'string'
next(g) #True


range : 제너레이터의 방식을 활용하는 대표적인 함수


list(range(5))
#[0,1,2,3,4]

range(5)
#range(0, 5)


type(range(5))
#<class 'range'>

for i in range(5):
    print(i, end=" ")
 #0 1 2 3 4

range()는 range클래스를 리턴

for문에서는 내부적으로 제너레이터의 next를 호출하듯 숫자 생성

만약) 숫자가 100만개라면?

# 1번
a = [n for n in range(1000000)]

#2번
b = range(10000000)


len(a) #1000000
len(b) #1000000

len(a) == len(b) #True

a #range(0, 1000000) => 생성된 값이 담겨있음
b #<class 'range'> => 생성해야한다는 조건만 있음


#따라서 크기를 비교해보면
sys.getsizeof(a) #8697464
sys.getsizeof(b) #48 => 1억개라도 메모리 점유율은 동일함, 생성조건만 보관하고 있기 때문이다.
#인덱스로 접근 시에는 바로 생성하도록 구현되어 있음
b[999] #999


enumerate : "열거하다" 여러가지 자료형을 인덱스를 포함한 enumerate객체로 리턴함


a = [1,2,3,2,45,2,5]
a #[1,2,3,2,45,2,5]

enumerate(a)
#<enumerate object at ~~~>


#list로 결과를 추출할 경우, 인덱스를 자동으로 부여해줌
list(enumerate(a))
#[(0,1), (1,2), (2,3), (3,2), (4,45), (5,2), (6,5)]


#인덱스와 값을 함께 출력??
a = ['a1', 'b2', 'c3']

#1번 => range사용
for i in range(len(a)):
    print(i, a[i])


#2번 => 인덱스 위한 변수 따로 선언
i = 0
for v in a:
    print(i, v)
    i += 1

#3번 => 인덱스와 값 함께 처리
for i, v in enumerate(a):
    i += 1



나눗셈


// => 정수형으로 유지

5/3
#1.66666666666666667

5//3
#1

5//3 = int(5/3)

#몫과 나머지 한번에 구함
divmod(5, 3)
#(1, 2)

print


콤마로 구분

print('A1', 'B2')
#>>A1 B2

파라미터로 구분자 지정

print('A1', 'B2', sep=',')
#구분자를 ,로 지정
#>>A1, B2

print 함수는 항상 줄바꿈을 한다. 따라서 루프의 값을 반복적으로 출력하려면 end 파라미터를 공백처리한다.

print('aa', end =' ')
print('bb')
#>> aa bb

리스트 출력시 join으로 묶어서 처리

a = ['A', 'B']
print(' '.join(a))
#>>A B

문자열 함께 출력

idx = 1
fruit = "Apple"
print('{0}: {1}'.format(idx + 1, fruit))
#>>2: Apple

#인덱스 생략도 가능
idx = 1
fruit = "Apple"
print('{}: {}'.format(idx + 1, fruit))
#>>2: Apple

f-string(formated string literal)

print(f'{idx+1}: {fruit}')
#>>2: Apple

pass


class MyClass(object):
    def method_a(self):

    def method_b(self):
        print("Method B")

c = MyClass()

위 코드는 실행되지 않는다.

method_a()가 아무런 처리를 하지 않았기 때문에 method_b()에서 오류가 발생한다.

pass는 이런 오류를 막는다.

class MyClass(object):
    def method_a(self):
        #pass 삽입
        pass


    def method_b(self):
        print("Method B")

c = MyClass()

pass는 null연산으로 아무 작업도 하지 않는다. 따라서 인덴트 오류같은 불필요한 오류를 방지한다.

pass를 사용하여 mockup 인터페이스부터 구현한 다음 추후 구현을 할 수 있게 된다.


locals


locals()는 로컬 심볼 테이블 딕셔너리를 가져오는 메소드이다.

로컬에 선언된 모든 변수를 조회할 수 있다.

...
import pprint
pprint.pprint(locals())
...

pprint로 출력하게 되면 클래스 메소드 내부의 모든 로컬변수를 출력해준다.

{'num':[2,7,11,15].
'pprint': >module 'pprint' from '/user/lib/python3.8/pprint.py'>,
'self':<__main__.Solution object at 0x~~~~>,
'target; : 9'}

int


숫자 정수형 int만 제공

int가 충분하지 않으면 자동으로 long 타입으로 변환됨

bool은 논리 자료형이지만 내부적으로 int로 처리된다.

Ture == 1
>>True
False == 0
>>True

매핑


키와 자료형으로 구성된 복합 자료형

유일한 매핑 자료형은 딕셔너리이다.


집합


set은 중복된 값을 갖지 않는 자료형이다.

입력순서가 유지되지 않으며 중복된 값이 있을 경우 하나의 값만 유지한다.

a=set()
a
>>set()

type(a)
>><class 'set'>

{} 중괄호를 사용하여 선언함

a={'a','b','c'}

시퀀스


특정 대상의 순서있는 나열

str은 문자의 순서 있는 나열로 문자열을 이루는 자료형이고, list는 다양한 값들을 배열 형태의 순서있는 나열로 구성하는 자료형이다.

시퀀스는 불변(immutable)과 가변(mutable)로 구분한다.

불변 : 값을 변경할 수 없다. => str, tuple, bytes

a='abc' #abc할당
a='def' #def할당
type(a)
>><class 'str'>

a 변수에 처음 abc가 할당되고, 이후 다른 str타입인 def를 다시 참조했다.

a='abc'
id('abc')
>>4317530408
id(a)
>>4317530408

a='def'
id('def')
>>4318831648
id(a)
>>4318831648

각 메모리 주소를 보면 참조하는 주소가 바뀐 것을 알 수 있다.

변경되려면 다음과 같은 할당자가 처리되어야한다.

a[1]='d'
>> 오류 발생

하지만 오류가 발생한다.

가변 : 값을 변경할 수 있다. => list

list는 값을 추가/삭제할 수 있는 동적 배열이다.

728x90
반응형

'공부 > 파이썬 알고리즘' 카테고리의 다른 글

문자열  (0) 2021.09.16
연결리스트  (0) 2021.09.15
배열  (0) 2021.09.15
etc  (0) 2021.07.01
파이썬 기본 문법  (0) 2021.05.18
블로그 이미지

아상관없어

,