블로그

인프런 비노

인프런 비즈니스 서비스 이용 기업수가 2,800개를 돌파했어요!

인프런 비즈니스 서비스는기업/기관 담당자가 임직원/소속 수강생이 인프런의 콘텐츠를 학습 할 수 있도록 관리해 주는 서비스 입니다.인프런 콘텐츠의 높은 퀄리티와 간편한 서비스 이용 방식으로, 임직원과 관리자 모두의 만족도가 높은것이 그 특징인데요! 2019년 처음 서비스를 도입 한 이후로 많은 기업에서 전해주신 애정어린 피드백과 조언으로 개선을 반복한 결과, 교육 담당자님들이 실제로 꼭 필요한 기능만을 쏙쏙 담은 서비스로 거듭나게 되었습니다. 🥳🥳 ✅ 합리적인 교육 비용꼭 필요한 강의만, 수강 신청한 만큼만 정산✅ 간편한 관리손쉬운 결제수단 등록/정산 처리수강생 간편 추가/삭제✅ 내부 교육 정책에 맞는 수강 환경 커스터마이징강의 가격/카테고리별 지원 여부 설정예산에 따른 지원 금액 및 개수 설정수강 신청 기간 및 학습 기간 설정✅ 학습 현황 모니터링 및 시스템 연동수강생 대상 공지사항수강 독려 메일 전송API & SSO 연동 오랜 기간 인프런 비즈니스를 이용해 주시는 기업과 더불어 새로운 기업에서도 인프런 비즈니스를 적극 이용하시고 사랑해주심에 따라, 인프런 비즈니스 이용 기업이 2,800곳을 돌파했습니다. 🎉🎉인프런 비즈니스 팀은 앞으로도 1:1 맞춤 컨설팅을 통해 기업에 꼭 맞는 교육을 운영하실 수 있도록 적극적으로 지원하겠습니다! 😘   관련 보도 자료스타트업엔 : https://www.startupn.kr/news/articleView.html?idxno=44150비석세스 : https://www.besuccess.com/it-%EA%B8%B0%EC%88%A0-%EA%B5%90%EC%9C%A1-%EC%98%A4%ED%94%88[…]B%8B%88%EC%8A%A4-%EC%9D%B4%EC%9A%A9-%EA%B8%B0%EC%97%85-2800/플래텀 : https://platum.kr/archives/222292와우테일 : https://wowtale.net/2024/02/06/71339/

경영인프런비즈니스사내교육역량강화교육기업교육

hjoo

스타트업 투자 재무적 Log 4 (Series A)

13. Series A 투자 라운드 시작 – 2021.02지표가 좋은 편이어서, 이땐 IR 자료 들이밀면 돈다발 들고 줄 설줄 알았다. 당연히 그런일은 없었다.ㅋㅋㅋㅋㅋ 게다가 난 정형화된 발표에 매우 취약해서 본엔젤스에게서 IR에 맞는 팀을 꾸리는게 좋겠다는 조언도 받았다. 쉽게 말해서 발표 너가 하지 말라는 말씀이었다. 😂😂😂 그래서 우리팀의 고트(goat)와 팀을 짜서 같이 IR 을 준비했다. 고트는 실제로 이해력과 유연성, 전달력이 모두 좋아서 내가 만든 IR 자료와 비전을 정확하게 이해하고 잘 전달해 줬다.50억을 모을 생각이었다. 2020년도에 만난 KB가 긍정적인 신호를 보내면서 기다리고 있어서 20억정도가 확보된 느낌적인 느낌이었고, Seed 라운드에 투자해 이미 주주인 본엔젤스도 10~20억 정도 후속투자 해주신다고 해서, 남은 10~20억을 펀딩하기 위해서 미팅들을 진행했다. 근데 의외로 미팅들마다 광탈했다.ㅋㅋ 수치도 나쁘진 않고 회사 소개도 좋았는데, 그걸 표현하는 방식이 많이 서툴렀다. 만나는 VC 마다 시장 사이즈나 앞으로도 잘 할 수 있는지에 대한 확신이 없다고들 했다.역시 난 보통 VC 들이 좋아하는 스타일이 아니라고 생각했고, 그래서 세상이 잘못보는 거라며 아이유 ‘셀레브리티’많이 들으며 위로받았다. Series A 라운딩 동안 수백만번 들은듯. 아이유님한테 밥 사주고 싶다. 아이유가 거부하겠지만.ㅋㅋ14. 본격 Series A IR본격적으로 투자 IR을 시작했다.아래 인프랩은 아래 세 회사에서 투자 받았다.미래에셋만나는 VC 들이 시큰둥하니까 슬슬 짜증게이지가 올라가던 참에 미래에셋 김경모 본부장님과 미팅이 잡혔다. 2020년 초중순에 전태연 파트너님 소개받은적 있는데, 그땐 바쁘셔서 만나보지도 못했었다. 그래서 사실 별 기대 없이 갔다.근데 그 짧은 미팅 시간에 많은 인사이트를 얻었다. 온라인 플랫폼에 대해서 투자를 많이 하신 분이라 그런지 이해도도 높았고, 던지는 질문에서 공부가 많이 됐다. 그래서 그 미팅 시간에 ‘아! 이사람한테는 받아야겠다!!’ 하는 생각이 들었다. 근데 그건 내 생각이고, 시큰둥해 하신다는 생각이 들었다. 미래에셋의 질문은 날카로웠지만 우리가 제공하는 대답과 자료들은 물렁했다. 그래서 추가로 우리 서비스에서 뽑을 수 있는 임팩트 있을만한 수치들을 분석해서 이메일로 보냈다. 근데 이 수치를 보내드리고 엄청 빨리 답장이 왔다.“매우 좋은 수치인 것 같습니다. 혹시 이걸 월별로 볼 수 있을까요?”이 답장을 보고 이 지표가 엄청 좋은 지표고 투자자들이 좋아하는 것이라는걸 알게됐다. 그래서 데이터 좀 더 디벨롭해서 IR 자료에 추가하고 새로운 VC 만날때마다 이 자료에 대해서 이야기를 했다. 실제로 그 이후로 만난 모든 VC에게서 관심을 받았다. 유저 리텐션에 관한 데이터였다. 리텐션 데이터를 아래 쿠팡꺼처럼 생기게 만들었었다. 우리껀 아니지만, 인프런 유저 리텐션 데이터를 이런 형태로 만들어서 전달드렸다. 쿠팡 리텐션 데이터는 지금봐도 경이롭다.ㅋ 근데 우리꺼도 대따 좋음.이후로 계속 자료들을 검토하고 인터뷰도 하고 하다가 정식 IR 하기로 결정됐다. 이 과정에서 이진우 심사역님이 엄청 많이 도와줘서 엄청 감사하다.IR 당일에 생각보다 편안하게 진행했고, 다른 분들도 많이 들어오셨는데 엄청 다들 친절하셨다. 분위기도 나름 좋았고.한국투자파트너스한투파의 정화목 이사님을 샤플 이준승 대표님 께서 소개해줬다. 이때쯤 50억이 내정적으로는 모인 시점이라 별 생각없었는데, 이준승 대표님이 벨류 왤케 낮게 하냐고. 투자 생태계 망치는거 아니냐고, 그리고 그럴수록 많이 만나봐야 된다고 하시면서 반 강제적으로 한투파와 TBT 를 소개해 주셨다. 진짜 초 귀인..정화목 이사님은 엄청 강렬했다. 우와 나랑 동갑인데, 엄청 똑똑하고 젠틀한데 적극적이고 해서 자극이 많이 됐다. 경험이 무척 좋았다. 그리고 추진력도 장난 아니고. 고트랑 둘이 첫 미팅하고 딤섬 먹으면서 꼭 이사람에게 받고 싶다고 얘기했던 기억이 난다.3월에 IR 을 진행했는데 생각보다 분위기가 편해서 놀랐다. 한투파가 IR 분위기가 무겁고 엄숙하다고 스타트업 사이에 도는 소문을 들었는데, 상상하던 그것보다는 훨씬 부드럽고 편안했다. 우리를 많이 배려해 주신거 같았다. 결국 이번 투자는 한국투자파트너스 정화목 이사님이 리드하에 진행됐다.본엔젤스새 라운드에서 후속투자를 받기위해 IR 시간을 가지게 되니, 여러 감정이 섞여 들었다. Seed 투자때 우리를 믿어준것에 대해 의리를 지키고, 보답을 하는거 같은 느낌도 들었다. 그래서 감사함과 뿌듯함의 감정이 특히나 크게 다가왔다. 딴 예기지만 본엔젤스는 원래 Seed 단계에서의 투자가 주로 있었는데, 시리즈A 단계에서 후속투자를 하는 경우는 이전까지는 별로 없었다고 한다. 앞으론 많이 하실거라고 하네.IR 시간에 크래프톤 김창한 대표님도 계셨는데, IR 시간이라 사적으로 궁금했던거 못물어봐서 아쉽다..15. 실사 및 투심위 등등 후 60억 투자 확정 💰🎉시드투자 때와는 다르게 돈의 규모가 달라지니까 절차도 좀 많아졌다. 근데 이건 VC(투자사) 마다 절차가 다르다. 한투파, 미래 같이 규모가 큰 회사들은 보통 IR → 투심위1 → 투심위2 → 회계실사 → 계약 → 주금납입이런 순서가 많은거 같다. 투심위는 해당 VC의 담당자가 VC내부에서 동의를 이끌어내는 과정이다. 그래서 이때 VC의 담당자가 잘 준비할 수 있게 투자받는 회사도 자료들을 충실하게 지원해 줘야된다.이때 사실 엄청 일이 많다. 요청 자료들은 보통 없었던 형식이 대부분이라 데이터 뽑고 새로 만들고 검증하고, 질문들에 답해주고 하는것들이 이어진다. 그렇게 만들어진 자료들을 토대로 VC 내부에서 토론을 거치고 최종 진행 여부가 결정된다.그리고 OK 되면 VC가 선정한 회계법인이 회계 실사를 진행하게 된다. 그동안의 모든 입출금 내역, 매출, 채무 등의 건전성 적합성 등을 확인한다. 그리고 문제 없으면 계약서 도장찍고 얼마후에 주금이 납입된다. Series A 라운드 프로젝트를 준비했던 궁예방. 실사 서류로 지저분하다.결론적으로 인프랩은 4월 22일한국투자파트너스, 미래에셋캐피탈, 본엔젤스각 20억씩 총 60억원 Series A 투자 유치에 성공했다. 💰💰💰💰💰💰 🎉16. 협상과정에 대한 감상시원하고 후다닥 60억이 통장에 들어온거 같지만 완전 그렇진 않았다.가장 처음에 투자를 희망한 VC 한곳과 미래에셋 두곳에서 IR 을 가장 먼저 진행했고, 뭔가 일사천리로 슉슉 진행되는거 같았다. 근데 처음 호감을 주던 VC 에서 일이 지지부진 해졌다. 정확히 왜때문인지 모르겠는데, 큰 VC 하우스다 보니까 내부 의사결정 과정에서 시간이 지체되는거 같았다. 문제는 리딩을 하는곳이 그렇게 지체되니까 전체 일정이 다 멈춰버렸다. 그렇게 근 한달이 걍 지나가 버렸다. 원래 연초는 주주총회 시즌이니까 바뻐서 좀 딜레이 될 수도 있다고는 알고 있었는데, 이건 좀 심한데? 싶었다. 나의 마음은 갈대니까 이 과정에서 다시 투자를 받기 싫어졌다.ㅋㅋㅋ 투자 준비도 재미없고, 내가 이렇게 까지 해서 무슨 부귀영화를 누리나 싶고, 다른 더 재밌는 일을 하고 싶었다. 본엔젤스 전태연 파트너님이 뒤에 없었으면, 아마도 투자 진작에 엎어버렸을거다. 본엔젤스 내부에 인프랩에 대한 후속투자를 설득해 놓으셨다는걸 알아서 실망시킬 순 없었다. 그래서 진짜 꾹 참고 지지부진한 투자상황을 이어나갔다. 벨류 조정 협상이렇게 느리게 일이 진행되는 와중에 벨류 조정 협상까지 들어왔다. 벨류가 상대적인 거라지만 우리딴에는 벨류를 많이 낮춰서 펀딩을 하고 있었다. ‘빠른 마무리’ 와 ‘좋은 VC’. 이번 라운드는 이 두조건을 총족하는게 최우선 이라고 모토를 잡아서 벨류는 아쉬움 없이 낮게 잡았다. 이 상황에서 벨류 협상이 들어오니까 신뢰가 깨져버렸다. 이 벨류를 못받아들여져서 계속 늦어지고 있던건가? 싶기도 했고.스타트업 입장에서 생각해보면, 투자는 앞으로의 믿어야할 파트너를 정하는 것이기도 하다. 그런 관점에서 우리가 이렇게 적절한 기업가치를 잡았는데도 이런 협상이 들어왔다는것은 앞으로 파트너로서 믿어도 되는걸까 하는 원론적인 의심이 들었다.원래 뭐든 빠르게 결정하는 편인데, 이때 진짜 머리가 넘 복잡해서 혼자 양평에 다녀왔다. 주말이틀 하루종일 걸으면서 계속 생각했다. 결론짓고 돌아와서 월요일에 IR 을 같이 준비하던 고트한테 지금까지 진행된 투자 상황 모두 드랍하고 다시 시작한다고 선언했다. 그때 고트 표정이 아직도 생생히 기억난다.ㅋㅋㅋㅋㅋㅋㅋㅋ다행히 감사하게도 완전 드랍되는 일은 일어나지 않았고, 투자자의 구성이 약간 바뀌어 투자가 성공적으로 마무리 됐다.음.. VC 가 투자전 기업 벨류를 낮게 조정하는걸 나쁘게 생각하지 않는다. 오히려 VC 입장에선 당연하다고 생각한다. 원래 가격보다 싼 가격으로 물건을 사면 그만큼 실적이고 보상이 커질테니까. 자신의 이득을 위해서 당연히 시도해볼만한 일이다. 하지만 때에 따라서는 상대방의 신뢰를 깰 수 있는거라 어려운 일 같다.이때 우리가 좀 더 압도적인 설득력을 보여줬으면 어땠을까.. 하는 생각이 든다. 얼마전에 만난 이미리 대표님이 투자 협상은 ‘자본주의의 예술’ 이라고 말씀하셨는데, 난 그런점에서 예술은 잘 못하는거 같다. 🥲

창업창업인프랩인프런스타트업투자인프랩재무적기록시리즈A

일프로

[쿠어클#14] ArgoCD 빠르게 레벨업 하기

Sprint2 추가 강의가 업로드 됐어요! 이번 수업에서는 ArgoCD의 아키텍처에 대해서 설명을 드리겠습니다.먼저 Argo 제품들에 대한 설명부터 드릴게요.ArgoCD는 쿠버네티스 전용 배포 툴이고, 릴리즈 파일 저장소로 Git을 반드시 필요로 해요. 그래서 ArgoCD를 쓰려면 쿠버네티스는 당연한 거고, 변경관리 저장소가 꼭 Git이어야 합니다.이전에 제가 했던 프로젝트에서는 형상관리를 SVN으로 썼거든요. SI 프로젝트에서는 개발량이 많기 때문에, 그 많은 기능들이 오픈 때까지 일단 다 돌아가는 게 하는 게 중요하지. 상대방의 코드를 리뷰 할 시간이 별로 없어요. 그래서 Git을 사용하는 문화를 정착 시키기 보다 최소한의 기능으로 누구나 빨리 익히고 쓸 수 있는 SVN을 더 선호하게 됩니다. 근데 그러면 ArgoCD는 사용할 수가 없는 거죠.다음으로 ImageUpdater는 ArgoCD의 추가 기능이에요. 이걸 사용하면 도커 허브에서 컨테이너 이미지에 대한 변경을 감지해서 배포를 할 수 있는 파이프라인을 만들 수 있습니다.그리고 BlueGreen과 Canary와 같은 고급 배포를 지원해주는 Rollouts가 있고요.다음으로 Events는 카프카와 같은 역할을 하는 솔루션이라고 보시면 되요. 통상 카프카를 이용해서 이벤트 버스 구조의 아키텍쳐를 만드는데, 이벤트 버스에 대해서는 여기서 설명을 드리기엔 큰 주제라 한번 검색을 해보시길 바라고요.  아시는 분들은 argo에서도 그런 역할을 해주는 제품이 있다고 보면 됩니다. 그리고 Workflow는 airflow나 kubeflow와 같은 역할을 하는 워크플로우 매니지먼트 도구고요. 역시 잘 모르시는 분은 추가적인 검색을 해보시면 좋은데, 결국 Argo 제품만을 가지고 아래와 같은 아키텍처가 만들어 질 수 있어요.Argo Events는 시스템들간에 이벤트를 주고 받는 메인 통로 역할을 해주고요. 이 중에 Workflow로 보내지는 이벤트가 있을 수 있고, workflow 안에는 받은 이벤트에 내부 값에 따라서 어떤 작업을 실행하라는 순서도가 있는거죠.  그래서 그 워크 플로우에 결과에 따라서 작업이 실행 되는데, 그 중에 배포를 하라는 작업이 있을 수 있고, 그럼 CD가 실행이 되요. 그리고 Rollous를 통해서 특정 배포 전략으로 쿠버네티스에 자원을 생성 시켜 주는거죠.그럼 이 제품들의 쓰임에 대해서 대략적인 맥락이 보이나요? 이제 ArgoCD가 쿠버네티스에 설치됐을 때 아키텍쳐에 대해서 설명을 드릴게요.먼저 쿠버네티스 컴포넌트들은 아래와 같이 구성돼 있고요.우리는 30000번 포트를 통해서 UI에 접근하거나, Kubectl로 CLI를 날렸었죠? 그럼 kube-apiserver가 API를 받아서 관련된 컴포넌트들 한테 트래픽을 전달을 해주는 구조였는데, 이런 구조는 다른 솔루션들도 비슷합니다. 먼저 이 Server는 API Server 와 Dashboard 역할을 동시에 합니다. 그래서 nodeport로 ArgoCD UI에 접속을 할 수 있고 kubectl처럼 argocd 라는 툴을 설치해서 CLI를 날릴 수도 있어요. 다음으로 Github가 있고, 여기에 내 App에 대해서 배포할 yaml 파일이 있다고 해볼게요. 그럼 Repo Server는 Git에 연결해서 yaml 파일을 가져오고 그걸로 ArgoCD에 배포할 yaml 매니패스트를 만들어 놓는 역할을 해요.그리고 Applicaiton Controller는 쿠버네티스에 리소스를 모니터링 하면서 이 Git에서 받은 내용과 다른 게 있는지 비교를 해주고요. 그래서 내용이 다르면, Git에 있는 내용으로 배포가 진행됩니다. 그리고 Kube API가 쿠버네티스로 리소스 생성 명령을 날려주는 역할을 하고요. 그리고 Notification은 ArgoCD에서 발생하는 이벤트를 외부로 트리거 해주는 역할을 담당해요. 그리고 Dex는 외부 인증관리를 하는 역할인데, 쿠버네티스를 하다보면 Grafana 나 이외에도 다양한 Dashobard들을 많이 쓰게 되거든요. 근데 관리자 입장에서 그 UI마다 ID/Password를 만들고 관리하기가 참 번거롭죠. 그래서 흔히 IAM 솔루션을 사용하는데, 그럼 사용자가 여기에다가만 로그인을 하면 이 IAM솔루션이랑 연결된 시스템에는 별도로 로그인을 안해도 자동 로그인이 되는 거죠. 흔히 SSO라고 하고요. 쿠버네티스에서는 대표적으로 KeyCloak이 있어요.Redis는 아시는 분은 잘 아시겠지만, 메모리 DB에요. 통상 시스템에서 캐시역할을 많이 하는 데, ArgoCD에서는 여기 Github와 연동되는 구간이랑 kube-apiserver랑 연동되는 구간에 캐시로 쓰여서. 불필요한 통신을 줄여주는 기능을 합니다. 그리고 마지막으로는 ApplicationSet Controller는 멀티 클러스터를 위한 App 패키징 관리 역할인데, 이게 뭐냐면  ArgoCD는 이렇게 클러스터마다 설치를 할 수도 있지만 이렇게 하나만 설치해서 여러 클러스터로 App을 배포할 수도 있다고 했잖아요? 그럼 배포 환경마다 ArgoCD에서 배포 구성을 만들어 줘야 하는데, 그럼 또 중복되는 구성들이 생기기 때문에, ApplicationSet Controller가 환경별로 다른 부분만 세팅해서 사용할 수 있는 템플릿을 제공해 줘요. 마치 Helm이랑 Kustomize에서 했던 것 처럼요.그래서 여기까지가 ArgoCD 아키텍쳐에 대한 설명을 드린거고, 처음 한번 정도는 이런 전체적인 구성들을 이해하면 좋은 게내가 ArgoCD에 기능에 일부만 쓰고 있더라도, 내가 알지만 안 쓰는 거지 기능을 몰라서 안 쓰는 건 아니라는 안심이 생겨요!이제 Argo App들을 어떻게 설치하는 보면,이 강의에서는 3개를 설치해 볼 거고. 이 제품들은 모두 Artifact Hub에서 Helm 패키지로 설치를 할 수가 있거든요. 그래서 이 패키지들에 특정 버전을 다운 받아서 제 강의 레파지토리에 그대로 복사를 해 놨고, 이 강의 실습 환경에 맞는 values 파일을 추가 했어요.그리고 이제 강의를 실습 하시는 분들께서는 본인에 클러스터에 설치를 하려면, CI/CD Server에 Jenkins로 Argo App들을 설치하는 잡을 하나 만들고요. 실행을 하면, 제 Github에서 패키지를 다운받은 다음에 본인에 쿠버네티스 클러스터로 설치가 됩니다. 이제 ArgoCD에서 Application을 만들어 보겠습니다. 우리는 배포를 하기 위해서 Application 이라는 걸 만들어야 되요. 이건 젠킨스에서 배포 단위 별로 프로젝트를 만들었던 거랑 똑같은 개념이고요. 하나의 app을 배포하는 단위가 Application인거고. 그럼 Application은 App별로 여러 개 만들어 지겠죠?그리고 Default라는 Project에 소속 되는데, 이 Project는 Application을 그룹핑 하는 용도고, 쿠버네티스에 네임스페이스 같이 Default라는 프로젝트가 기본적으로 만들어져 있어요.이제 이 Application을 만들 때 어떤 내용들을 넣어야 되는지 볼게요.먼저 Source라고해서 연동할 Git에 대한 정보를 입력해야 되요.  그리고 Destination이라고 해서 배포할 쿠버네티스 클러스터 정보도 줘야 됩니다. 너무 당연한 항목이죠?근데 생소할 수 있는 용어가 있는데, Refresh랑 Synchronize고요. ArgoCD UI에도 이 버튼들이 있는데,, Git에 연결을 해 놓으면 자동으로 변경 사항을 감지하지만, 3분 정도에 체크 인터벌이 있는데, 이때 Refresh 버튼을 누르면 바로 변경 사항을 체크 해줍니다. 그리고 Sync버튼을 누르면 쿠버네티스에 배포하는 실행하는 거고요.이제 또 다른 항목으로 General이 있고, 기본 정보나 배포시에 줄 옵션들이 들어가요. 여기서 주목할 옵션으로 Sync Policy가 있고요. 리소스에 대한 변경사항이 감지 됐다면, 이제 수동으로 배포할 지, 자동으로 배포할 지를 선택하는 옵션이고, 자동으로 할 경우에는 3분 이내에 배포가 됩니다.그리고 Sync Option은 배포 상세 옵션으로, 여러가지가 있는데, 예를 들어 배포할 때 네임스페이스를 자동생성 할 건지? 같은 배포시에 줄 수 있을 법한 기능들이 있고요.Prune Policy는 리소스 삭제 정책에 대한 부분이고. 그외에도 몇 가지 더 있긴 한데 대략 이정도 느낌에 설정들이 있다는 것만 알고 넘어갈게요. 마지막으로 이렇게 ArgoCD에서는 3가지 배포 방식에 배포 툴을 지원합니다. 그래서 내 릴리즈 파일이 어떤 방식 인지에 따라서 선택을 해주면 되는데, ArgoCD가 릴리즈 파일을 다운 받으면서 자동으로 인식을 해요.그럼 이번 블로그는  여기까지고요, 해당 강의에서는 실습과 더불어 추가적으로 아래 내용들에 대해서 더 다룹니다 😀[쿠버네티스 어나더 클래스] : https://inf.run/unreT ArgoCD Image Updater를 이용한 이미지 자동 배포 Argo Rollouts를 이용한 배포 - BlueGreenArgo Rollouts를 이용한 배포 - Canaryps. 매너가 좋아요♡를 만든다 :)

데브옵스 · 인프라쿠버네티스어나더클래스지상편Sprint2일프로데브옵스ArgoCDBlueGreenCanary

일프로

[쿠어클#13] Helm과 Kustomize 비교하며 사용하기 (2/2)

Sprint2 추가 강의가 업로드 됐어요!  Helm과 Kustomize 비교하며 사용하기 두 번째 시간입니다. 이번 시간에는 Kustomize 패키지 구조를 설명을 드릴 거예요.먼저 최초 패키지를 만드는 방법을 보면, Helm 패키지는 이렇게 helm create 명령을 날리면 이런 구조의 패키지가 자동으로 만들어졌는데, Kustomize는 직접 폴더를 만들어야 되고요. 그리고 이 폴더 밑에  이렇게 하위 폴더 구성도 직접 만들어야 되요.어려운 구조는 아니지만 그래도 사전에 Kustomize의 구성이 이렇다라는 건 미리 알고 있어야 되는 거죠. (물론 Kustomize 가이드에 잘 나와 있어요) 그리고 아래와 같이 각 폴더 밑에 내가 배포할 yaml 파일들도 만들어 줍니다.좀 수동으로 만들어야 할 부분이 많긴 한데, Helm은 알고 있어야 되는 파일들이 많았던 거 에 비해서 Kustomize는 파일 구조가 간단해 보이죠? 이 폴더 구조랑 이 kustomization 파일에 기능만 알면 다 예요.그래서 하나씩 보면, 메인 폴더가 있고 밑에 base는 Default 포맷이 될 yaml 파일들을 넣는 폴더입니다. 기존부터 kubectl로 배포하던 deployment yaml 파일을 그대로 여기에 넣으면, 이게 베이스 yaml 파일이 되는 거예요. 이런 식으로 밑에 배포할 파일들을 다 넣으면 되요.그리고 Kustomization.yaml이 제일 중요한 파일 입니다. 리소스 yaml 파일들 중에서 어떤 파일을 배포할 건지 선택하는 내용이 있고, 또 그 yaml 파일들에서 반복적으로 사용하는 속성들을 공통값을 설정할 수 있어요. 먼저 어떤 파일을 배포할 건지 선택하는 방법은helm의 경우, hpa.yaml 파일 내부에 이렇게 if문이 있고, values.yaml 파일에서 이렇게 false를 하면 이 hpa가 배포가 안됐고요. kustomize는 이 Kustomization 파일에 [resouce] 키가 있어서 내가 배포할 파일들만 명시적으로 정할 수 있어요.이게 배포할 파일을 선택하는데 있어서 두 패키지 매니저에 대한 차이 입니다.그리고 공통값 설정에 대한 차이는Helm은 Deployment 템플릿 내에 변수를 넣는 부분이 있고, 여러 파일들(_helpers.tpl, Chart.yaml, values.yaml)에서 변수를 주입하는 방식이 다양했었죠?반면 kustomize는 Deployment에는 특별한 내용이 없고, Kustomization.yaml 파일에 명시적으로 coommonLabels라는 키가 있어서 여기에 내용을 넣으면, 배포될 모든 yaml 파일에 label로 그 내용이 들어가 집니다.마지막으로 환경별로 배포할 때 기본값 들을 어떻게 주는지 말씀 드릴게요.kustomize는 overlays 폴더 밑에 환경별로 폴더를 만들고, 해당 폴더마다 그 환경에 맞는 yaml 파일들을 넣어 놓습니다.그리고 여기에도 마찬가지로  Kustomization.yaml 파일이 있는데, 하는 역할은 같아요.각 환경별 파일에 있는 yaml 파일들에 대해서 배포할 파일을 지정하고 공통값을 설정할 수가 있습니다. 그래서 개발 환경을 배포 하려면 [kubectl apply -k ./app/overlays/dev] 처럼, 배포 명령에 해당 환경별 폴더 경로를 주면 되고요. 반면 Helm의 경우, values.yaml 파일을 각각의 환경별로 추가하고, 배포할 때 -f 옵션으로 이 원하는 환경의 values-dev.yaml 파일을 선택하면 됩니다. 헬름은 변수를 가져다 쓰는 방법이 다양했는데, 또 한 가지가 더 늘었죠? 그래도 다 사용해야 되는 상황이 있고, 실습을 하면서 자세하게 설명을 드릴게요. 그럼 이번 블로그는  여기까지고요, 해당 강의에서는 실습과 더불어 추가적으로 아래 내용들에 대해서 더 다룹니다 😀[쿠버네티스 어나더 클래스] : https://inf.run/unreT  배포 파이프라인 구축 후 마주하게 되는 고민들ps. 매너가 좋아요♡를 만든다 :) 

데브옵스 · 인프라쿠버네티스어나더클래스지상편Sprint2일프로데브옵스HelmKustomizeJenkins

한기용

성장하는 조직의 기술 부채는 언제 갚을까?

작은 회사에서 속도에 초점을 맞추고 주니어 중심으로 개발을 하면 당연히 기술 부채가 쌓인다. 워낙 숨 가쁜 일정으로 돌아가다 보니 유닛 테스트를 만드는 건 꿈도 꿀 수 없고 최종 QA(Quality Assurance)도 대충 하고 프로덕션에서 실제 고객들이 테스트 아닌 테스트를 하는 일도 다반사다. 자잘한 버그가 자주 보고되는 것은 물론 가끔 사고도 터지며, 이런 기술 부채로 인해 생긴 누더기 같은 코드로 인해 새로운 기능 개발도 지연되기 시작한다.  그렇다고 내일도 살아있을지 아무도 모르는 작은 회사에서 항상 깔끔하고 완벽하게 테스트된 코드만 지향하거나 무모하게 새로운 프레임워크를 사용해 볼 수도 없는 노릇이다. 시작하는 단계의 스타트업이 빠른 속도와 무결점 중 꼭 하나만 선택해야 한다면 속도를 선택하는 편이 유리하다는 게 내 생각이다. 그 대신 아래의 내용을 참고하면 돈이 더 생기고 더 경험 있는 사람을 뽑기 전까지 리스크를 최소화할 수 있을 것이다. 어느 시점부터는 슬슬 사고의 빈도가 늘며 정도도 심해지기 시작할 때 그 때부터는 아래를 고민해보는 것이 좋은 듯 하다.서비스 모니터링 프로세스를 만든다. 서비스에 문제가 생기면 빨리 인지하고 해결하는데 초점을 맞추는 것이다. 이 것도 상당한 노력과 시간을 필요로 할 수 있는데 기술 부채를 많이 안고 가는 상황에서 모니터링에 대한 노력을 먼저 기울이는 것이 맞다고 본다. 어떤 지표를 바탕으로 서비스의 안정성을 측정할 것인지 그리고 서비스에 문제가 생겼을 경우 어떻게 escalate하고 문제해결을 할지 점진적으로 개선해가며 프로세스를 만드는 것이다. 또한 사고의 심각성에 따라 재발을 막을 방법을 찾아봐야 하는데 이건 뒤 문단에서 이어서 더 이야기해보겠다. 2. 매주 엔지니어링 미팅 등에서 지난 한 주간의 버그와 사고를 리뷰하고 이유를 파악한다. 유닛 테스트 코드를 붙이지는 못하고 QA를 충분히 하지 못하더라도, 이미 발견된 버그와 사고가 재발하는 것을 방지하기 위한 테스트는 붙이는 것이 필요하다고 본다. 서비스 모니터링 프로세스가 어느 정도 자리잡혀있다면 사고 리뷰는 상대적으로는 쉬워진다. 만일 버그와 사고의 빈도가 점점 높아진다면 새로운 기능 개발뿐 아니라 기존 코드의 리팩토링에도 시간을 써야 한다. 유데미 시절 데이터 엔지니어링 팀은 이슈가 늘어나면서 그러다가 대형 사고를 한번 겪고 난 뒤 최대 40%의 시간을 리팩토링에 할애했다.  3. 훌륭한 개발자는 자신이 만든 결과물이 실제 사용자들에 의해 어떻게 사용되는지 관심이 많고 실제 사용자의 피드백에 귀를 기울이는 사람이다. ‘개발 다 했으니 내 업무는 끝!’이 아니라 어떻게 쓰이는지 보고 개선하려는 의지가 있다는 말이다. 인정받는 개발자 혹은 좋은 개발 팀의 매니저가 되고 싶다면, CS/CX 팀과의 협업 채널을 만들고 주기적인 미팅을 통해 고객의 소리를 들어보고 사람이 되자. SDD(Support Driven Development)라는 말을 들어봤는가? Y Combinator 스타트업 스쿨에서 강조하는 원칙 중의 하나이다.   몇 가지 상대적으로 쉽게 붙여볼 수 있는 중요한 성능 모니터링이 있는데 첫 번째는 백엔드 데이터베이스의 오래 걸리는 쿼리를 모니터링하고 문제 되는 부분을 찾아 미리 최적화하는 것이고 두 번째는 서비스 홈페이지나 중요 API의 실행시간 (latency) 모니터링을 해보는 것이다. 이를 기술 부채를 해결하기 위해 단기적 관점에서 해볼 수 있는 일이다.  물론 위와 같은 방법을 동원해도 사고는 터진다. 그중 몇 건은 대형 사고일지도 모른다. 그럴 때 중요한 것은 사고를 바라보는 경영진의 관점인데, 특정인이나 팀을 향해 손가락질하기보다는 사고 경위서를 작성해서 사고의 원인을 이해하고 재발 방지 대책을 세운 뒤 사내에 공유하는 과정에 집중해야 한다. 여기서 포인트는 경위서 작성 자체가 아니라, 밝혀진 문제의 재발을 막기 위한 조치들이 경위서의 내용에 포함되고 이런 조치들이 실제로 구현되어야 한다는 점이다. 온라인 서비스에서 사고는 언제 일어나느냐의 문제지 피해갈 수는 없다. 회사가 망할 정도의 사고만 아니라면 조직 구성원 모두가 기술 부채 등의 다양한 이슈를 체감하고 이를 줄이기 위해 노력할 수 있는 기회가 된다는 사실을 잊지 말자. 모든 위기는 기회가 될 수 있다. 모든 사람들이 기술 부채 문제를 체감하는 회사가 망할지 않을 정도의 대형 사고를 내가 사랑하는 이유다 🙂 What doesn’t kill you makes you stronger!

기타 (개발 · 프로그래밍)기술부채사고경위서SDD

비전공 개발자가 흔히 하는 고민

지난 주에 30대 초반에 부트 캠프를 통해 엔지니어로 전직한 분과 이야기해보는 시간이 있었다. 30대 중반이 되어 주니어 개발자로 일하고 있는데 대학을 졸업한지 2-3년차 동료들과 비교하며 힘들어 하고 있었다. 부트캠프부터 치면 3년이 되어가는데 아직도 잘 모르겠다고 하면서 어떻게 살아야할지 길을 잃은 것 같다고 고민을 털어놓았다. 30-40대 경력 전환 주니어 개발자들과 이야기하다보면 항상 나오는 패턴이다.20대로 돌아가면 어떻게 살것 같냐고 했더니 조금 생각하더니 술술 잘 이야기하길래 지금부터 그렇게 살면 된다고 조언해주었다. 앞에 시간이 많기에 (이분은 앞으로 적어도 40년은 일해야한다 ^^) 늦은 것 같지만 늦은 게 아니다. 나이가 주는 강박관념과 과거에 보낸 시간이 물경력이 아닐까 하는 후회, 그러다보니 나보다 어리지만 사실은 그 분야에서 더 오랜 시간을 보낸 사람들과 비교하기 쉽다.개발에 들어온지 3년만에 잘 할 수 있다면 그건 천재다. 오랜 시간이 걸리며 디버깅은 다들 아주 사소한 걸로 몇 시간부터 며칠 보내는 일이 허다하다. 마음이 조급하다보면 회사 일을 열심히 해서 실력을 늘리기 보다는 회사 일은 회사 일이고 업무 시간 밖에서 아직은 너무 어려운 다른 공부를 하며 스트레스 받기 쉽다 (방정식을 배우는 단계에서 미분을 배우려고 하는 듯 한 느낌). 처음은 기본기다.길게 바라보며 나만의 길을 찾고 내가 있는 곳에서 잘 하려고 최선을 다 해보는 것이 커리어를 발전시키는 가장 쉬운 길이다. "나"와 "현재"에 집중해보자.커리어 멘토링에 관심있다면 https://www.inflearn.com/mentors?mentor_id=1823 :)

기타 (개발 · 프로그래밍)커리어고민비전공개발자물경력

인프런 에리얼

인프런 비즈니스 대시보드, 어떻게 써야할까? 100% 활용하기!

인프런 비즈니스 서비스, 어떻게 쓰고 있는지 궁금하신가요 ? 인프런 비즈니스에서는 IT에 특화된 강의 및 실무 강의들로 약 3,100개 이상의 콘텐츠를 제공하고 있으며, 한달 평균 65개의 강의가 새롭게 업로드 되어 최신 강의를 빠르게 필요한 실무자들에게 제공할 수 있습니다. 또한, 이미 누적 수강생 1,200만명이 인프런을 통해 학습하고 있습니다.오늘은 다양한 인프런 비즈니스 기능 중, '대시보드' 기능을 설명드리고자 합니다.대시보드 기능이란?- 학습자분들이 수강한 학습 현황, 학습 시간, 강의 평점, 인기 카테고리 및 인기 스킬태그를 관리자가 확인하기 쉬운 형태인 '대시보드' 로 제공하며,이에 맞춰 학습 독려 메일 발송 및 강의관리까지 연계 가능한 관리자 편의형 기능입니다.인프런을 사용하시는 비즈니스 관리자분들에게 받았던 피드백 중 바로 '어떤 강의를 선호하며, 어떤 데이터를 통해 강의를 제공해야 만족할까? '에 대한 부분이 유독 많았는데요.사실 관리자 분들께서도 교육 서비스 제공 시, 학습자분들에게 실질적으로 도움이 되는 강의 주제를 맞추기 어려우실 수 있을 것 같습니다. 많은 인터뷰를 듣고 난 뒤, 인프런에서 관리자님들의 편의를 위해  “대시보드” 기능을 새로 출시하였습니다! 대시보드 내 인기 카테고리 , 인기 스킬태그 등을 통해 실제로 학습하는 학습자분들의 강의 유형을 알고, 수강평을 통해 강의 만족도를 파악하여 추후 해당 카테고리의 강의들을 보강할 수 있는 중요한 역할을 하고 있습니다.TIP 하나 더!대시보드를 보고 나서, 더 자세히 “우리회사가 어떤 강의를 가장 많이 듣고 얼마나 듣지?”를 알고 싶으시다면, 담당 영업매니저에게 살짝 ‘이용보고서’를 문의해보세요! 빠르고 정확하게, 해당 기간 내 이용보고서를 공유드립니다 :) 우리 모두, 교육에 진심이니까!임직원들이 만족하고, 실제 역량에 도움이 되는 교육을 위해, 인프런 비즈니스는 계속해서 노력하겠습니다. 

업무 자동화대시보드업무자동화비즈니스교육사내교육IT교육DX교육비즈니스교육실무강의강의

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 어노테이션(Day1)

어노테이션 서론드디어 '인프런 워밍업 스터디 클럽 0기' 첫 날이 밝아왔다. 강의를 듣고 미션을 보니 어노테이션에 관련한 내용이었다.나는 이 미션을 보고 오히려 기쁜 마음이 들었다. 😆 내가 강의를 들으면서 어노테이션 부분이 많이 궁금하였는데 이렇게 공부하게 될 계기가 생긴 것 같아서 미션도 완성시키고 나 스스로 깊게 공부도 할 겸 미션을 시작할려고 한다. 미션 내용은 아래와 같다.진도표 1일차와 연결됩니다우리는 최초로 API를 만들어 보았습니다. GET API를 만들기 위해 사용했던 어노테이션에 익숙하지 않다면 자바 어노테이션에 대해서 몇 가지 블로그 글을 찾아보세요! 다음 질문을 생각하며 공부해보면 좋습니다! 😊 [질문]어노테이션을 사용하는 이유 (효과) 는 무엇일까?나만의 어노테이션은 어떻게 만들 수 있을까?내가 알아본 어노테이션의 정의나는 강의의 실습을 통하여 스프링 부트 프로젝트를 생성하고, GET API를 만들어보고 포스트맨을 통하여 테스트 작업도 해보았다. 나는 여기서 다양한 어노테이션들을 볼 수 있었다. @SpringBootApplication, @RestController, @GetMapping 등 여러 어노테이션들을 볼 수 있었다. 여기서 나는 어노테이션이 무엇일까 고민을 해보았다. 단순히 어노테이션은 @ 붙인거라고만 알고 있었기에 이번 기회에 미션도 수행할 겸 깊게 알아보는 것도 좋다 생각하여 공부해보기로 하겠다.먼저 어노테이션이 대체 어떤 정의가 있는지 구글링을 해보기로 하였다. 구글링을 해보니, 다양한 블로그들이 나왔지만 정의가 수록된 위키백과를 먼저 참조해보기로 하였다. 위키백과는 다음과 같이 정의를 내렸다. 자바 어노테이션은 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종이다. 보통 @ 기호를 앞에 붙여서 사용한다. JDK 1.5 버전 이상에서 사용 가능하다. 자바 어노테이션은 클래스 파일에 임베디드되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동한다. 그리고 강의 중에 코치님께서도 어노테이션에 대해 아래와 같이 언급해주셨다. 어노테이션은 어노테이션마다 너무 다양한 역할을 한다. 또한 마법같은 일을 자동으로 해준다는 것이다.예를 들어서, @SpringBootApplication 어노테이션은 스프링을 실행시킬 때 다양한 설정이 필요한데 이 설정을 모두 자동으로 해준다. 또한 이런것이 가장 핵심적인 마법같은 일이다.위키사전, 코치님의 설명을 통해 어노테이션의 정의를 알 수 있었다. 좀 더 내가 설명한 식으로 풀어보자면 다음과 같다.자바의 어노테이션은 코드에 추가 정보를 제공하는 데 사용되며, 컴파일 시간, 배포 시간, 또는 실행 시간에 해당 정보를 활용할 수 있습니다. 이를 통해 개발자는 코드에 메타데이터를 추가하여 코드의 가독성, 유지 보수성을 향상시키고, 다양한 도구와 프레임워크에서 활용될 수 있는 정보를 제공할 수 있습니다.좀 더 자세히 풀어보자.어노테이션은 자바 5부터 도입된 기능으로, 코드에 대한 메타데이터를 제공하는 방법입니다. 어노테이션은 주석과 비슷하지만, 실제로 코드에 영향을 줄 수 있으며, 컴파일러에게 정보를 제공하거나 실행 시간에 특정 동작을 하도록 할 수 있습니다. 어노테이션은 선언적 형태로 코드 안에 포함되어, 클래스, 메소드, 변수 등 다양한 요소에 적용될 수 있습니다.이제 위의 내용을 좀 더 정리해보겠다. 어노테이션이란?자바를 개발한 사람들은 소스코드에 대한 문서를 따로 만들기보다 소스코드와 문서를 하나의 파일로 관리하는 것이 낫다고 생각했다. 그래서 소스코드의 주석에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML 문서를 생성해내는 프로그램(javadoc.exe)를 만들어 사용했다. 그런데 여기서 의문점이 하나 든다. 🙋🏻 왜 어노테이션이라는 것을 살펴보려 하는데 주석이라는 내용이 먼저 나올까? 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 어노테이션이다.어노테이션은 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다. 📚 어노테이션(annotation)의 뜻은 주석, 주해, 메모이다.package org.example; public @interface SampleAnnotation { }위의 코드는 인텔리제이로 나의 어노테이션을 만든 코드이다.그럼 인텔리제이로 어노테이션을 만드는 것도 끝났으니 이제 끝인가? 나는 여기서 더 나아가서 이 어노테이션 코드가 .class파일로 컴파일 되었을 때 어떻게 나오는지 보고 싶어서 터미널로 컴파일을 해보았다. 컴파일 결과는 다음과 같다.public interface org.example.SampleAnnotation extends java.lang.annotation.Annotation { }컴파일 시점에 extends 한적 없는 java.lang.annotation.Annotation 이 extends 되어 있다. 이제 좀 더 자세한 어노테이션의 내용과 활용법을 알아가보자. 어노테이션은 JDK에서 기본적으로 제공하는 것과 다른 프로그램에서 제공하는 것들이 있는데, 어느 것이든 그저 약속된 형식의 정보를 제공하기만 하면 될 뿐이다.JDK에서 제공하는 표준 어노테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다. 📚 JDK에서 제공하는 어노테이션은 'java.lang.annotation' 패키지에 포함되어 있다.어노테이션은 코드에 넣는 주석이다. 완전히 주석같지는 않지만 그 비슷한 부류이다.주석이기 때문에, 실행되는 코드라고 생각하면 안된다. 어노테이션은 기능을 가지고 있는 것이라 착각을 할 수 있지만 어노테이션은 마크, 표시 해놓는 주석이다. 어노테이션은 다이나믹하게 실행되는 코드는 들어가지 않는다.즉, 런타임에 알아내야 하는 것들은 못 들어간다.위의 내용을 좀 더 풀어쓰면 컴파일러 수준에서 해석이 되야 하거나, 완전히 정적이어야 한다는 말이다.이유를 아래 코드로 보여주겠다. package me.sungbin.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { private static final String hello = "/hello"; @GetMapping(hello) public String hello() { return "hello"; } }위와 같이 hello 변수는 정적 변수이므로 @GetMapping 어노테이션에 사용할 수 있다.하지만. hello 변수가 동적인 변수라면 컴파일 에러가 발생한다.아래의 코드를 보자. 컴파일 에러가 발생하는 것을 볼 수 있을 것이다. 간략한 어노테이션 정의 방법새로운 어노테이션을 정의하는 방법은 아래와 같다.'@'기호를 붙이는 것을 제외하면 인터페이스 정의와 동일하다. package me.sungbin; public @interface SampleAnnotation { 타입요소이름(); } 📚 타입요소등, 어노테이션 정의에 대한 자세한 정의방법과 내용들은 구체적인 내용들을 확인 후, 살펴보자.자바의 표준 어노테이션자바에서 기본적으로 제공하는 어노테이션들은 몇 개 없다.그나마 이들의 일부는 '메타 어노테이션(meta annotation)' 으로 어노테이션을 정의하는데 사용되는 어노테이션의 어노테이션이다. 표준 어노테이션과 메타 어노테이션@Override: 컴파일러에게 오바리이딩하는 메서드라는 것을 알린다.@Deprecated: 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.@SuppressWarnings: 컴파일러의 특정 경고메시지가 나타나지 않게 해준다.@SafeVarags: 제네릭스 타입의 가변인자에 사용한다. (JDK 1.7)@FunctionalInterface: 함수형 인터페이스라는 것을 알린다. (JDK 1.8)@Native: native 메서드에서 참조되는 상수 앞에 붙인다. (JDK 1.8)@Target*: 어노테이션이 적용가능한 대상을 지정하는데 사용한다.@Documented*: 어노테이션 정보가 javadoc으로 작성된 문서에 포함되게 한다.@Inherited*: 어노테이션이 자손 클래스에 상속되도록 한다.@Retention*: 어노테이션이 유지되는 범위를 지정하는데 사용한다.@Repeatable*: 어노테이션을 반복해서 사용할 수 있게 한다. (JDK 1.8)*이 붙은 것이 메타 어노네이션이다.📚 메타 어노테이션: 어노테이션을 정의하는데 사용하는 어노테이션의 어노테이션 @Override현재 메서드가 슈퍼 클래스의 메서드를 오버라이드한 것임을 컴파일러에게 명시해준다.메서드가 슈퍼클래스에 없다면 에러를 발생시기 때문에 오타와 같은 실수도 잡을 수 있다. @Deprecated마커 어노테이션으로 다음 버전에 지원되지 않을 수도 있기 때문에 앞으로 사용하지 말라고 경고를 알린다.@Deprecated를 붙인 메서드는 인텔리제이에서 아래의 사진과 같이 표시해준다. @SuppressWarning경고를 제거하는 어노테이션으로 개발자가 의도를 가지고 설계를 했는데 컴파일은 이를 알지 못하고 컴파일 경고를 띄울 수 있기 때문에 이를 제거하는 목적이다. @SafeVarargsJava 7이상에서 사용가능하고 제네릭같은 가변인자 매개변수 사용시 경고를 무시한다제네릭사용할 클래스,메서드 내부에서의 데이터타입을 외부에서 지정하는 기법 @FunctionalInterfaceJava 8이상에서 사용가능하고 컴파일러에게 함수형 인터페이스라는 것을 알리는 어노테이션이다.메타 어노테이션'어노테이션을 위한 어노테이션' 쯕, 어노테이션에 붙이는 어노테이션으로 어노테이션을 정의할 때 어노테이션의 적용대상(target) 이나 유지기간(retention)등을 지정하는데 사용된다. 📚 메타 어노테이션은 java.lang.annotation 패키지에 포함되어 있다. @Target어노테이션이 적용가능한 대상을 지정하는데 사용한다.아래 예제는 '@SuppressWarnings' 를 정의한 것인데, 이 어노테이션에 적용할 수 있는 대상을 '@Target' 으로 지정한다.여러 개의 값을 지정할 때는 배열처럼 괄호{} 를 이용하여 지정할 수 있다.package me.sungbin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); } @Target으로 지정할 수 있는 어노테이션 적용대상의 종류ANNOTATION_TYPE: 어노테이션CONSTRUCTOR: 생성자FIELD: 필드(멤버 변수, ENUM 상수)LOCAL_VARIABLE: 지역변수METHOD: 메서드PACKAGE: 패키지PARAMETER: 매개변수TYPE: 타입(클래스, 인터페이스, ENUM)TYPE_PARAMETER: 타입 매개변수(JDK1.8)TYPE_USE: 타입이 사용되는 모든 곳(JDK1.8)📚 java.lang.annotation.ElementType 이라는 열거형에 정의되어 있다. static import문을 사용하면 ElementType.TYPE 이 아니라 TYPE 과 같이 간략히 사용할 수 있다. TYPE은 타입을 선언할 때 어노테이션을 붙일 수 있다는 뜻TYPE_USE는 해당 타입의 변수를 선언할 때 붙일 수 있다는 뜻이다.FIELD 는 기본형에 사용할 수 있고, TYPE_USE는 참조형에 사용된다는 점을 주의한다.타입 선언부제네릭 타입, 변수 타입, 매개변수 타입, 예외 타입...타입에 사용할 수 있으려면TYPE_PARAMETER : 타입 변수에만 사용할 수 있다.TYPE_USE : 타입 변수를 포함해서 모든 타입 선언부에 사용할 수 있다.package me.sungbin; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({FIELD, TYPE, TYPE_USE}) public @interface MyAnnotation { } package me.sungbin; import me.sungbin.controller.HelloController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MyAnnotation public class AnnotationTestApplication { @MyAnnotation int i; @MyAnnotation HelloController helloController; public static void main(String[] args) { SpringApplication.run(AnnotationTestApplication.class, args); } } @Retention어노테이션 유지되는 기간을 지정하는데 사용한다. 어노테이션 유지정책의 종류SOURCE: 소스 파일에만 존재. 클래스파일에는 존재하지 않는다.CLASS: 클래스 파일에 존재. 실행 시에 사용 불가능하다. (기본값)RUNTIME: 클래스 파일에 존재하며 실행시에 사용 가능하다.SOURCE -> CLASS -> RUNTIMESOURCE는 소스코드만 유지하겠다.컴파일 시에만 사용하겠다는 것!컴파일하고 나면 어노테이션은 없어진다. -> 바이트코드에도 남아있지 않다.CLASS애노테이션에 대한 정보를 클래스 파일까지, 즉 바이트 코드에도 남겨 두겠다.클래스 정보를 읽어들이는 방법(바이트 코드를 읽어들이는)을 바탕으로 애노테이션 정보를 읽어와서 처리할 수 있다.예) BYTE BUDDY, ASM 활용바이트 코드엔 남아 있지만, 이 클래스파일을 JVM이 실행할 때 클래스에 대한 정보를 클래스로더가 읽어서 메모리에 적재하게되고, 이후 사용 시점에 메모리에서 읽어올 때 애노테이션 정보를 제외하고 읽어옴RUNTIME위 CLASS와 동일하지만, 메모리에 적재된 클래스 정보를 읽어올 때 애노테이션 정보를 그대로 포함하는 것이다.바이트코드에서 읽어오는게 빠를까?RetentionPolicy를 CLASS로 한 이후, 바이트코드를 읽어 처리하는 라이브러리를 활용?리플렉션으로 읽어오는게 빠를까?RetentionPolicy를 CLASS로 한 이후, 바이트코드를 읽어 처리하는 라이브러리를 활용? -> 리플렉션 자체가 부하가 존재한다.-> 바이트 코드의 양에 영향을 끼친다.-> 리플렉션은 메모리에 이미 올라와 있는 정보를 읽는다. 클래스 로더가 읽고 메모리에 적재시킨 후 읽어온다. 📚 커스텀하게 만든 애노테이션이 정말로 RUNTIME 까지 필요한 정보인가? RUNTIME 까지 사용할 필요가 없다면, CLASS 레벨로 내려가거나 SOURCE 레벨로 내려갈 수도 있을 것이다. 그냥, 의례적으로 RUNTIME으로 작성하는 경우가 있었다면? 그 역할을 다시 살펴보고 명확한 Retention Policy 를 정의하자. 표준 어노테이션 중 '@Override' 나 '@SuppressWarnings' 처럼 컴파일러가 사용하는 어노테이션은 유지 정책이 'SOURCE' 이다. -> 컴파일러를 직접 작성할 것이 아니면, SOURCE 이상의 유지정책을 가질 필요가 없다. 유지 정책을 RUNTIME 으로 한다면,실행 시에 리플렉션(Reflection) 을 통해 클래스 파일에 저장된 어노테이션의 정보를 읽어서 처리 할 수 있다.Retention 정책은 RUNTIME 으로 정의하고Target은 TYPE과 FIELD로 정의한다.package me.sungbin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { } Target이 TYPE과 FIELD 임으로 클래스에도 애노테이션을 선언할 수 있고클래스 내부의 필드에도 애노테이션을 선언할 수 있다.package me.sungbin; @MyAnnotation public class TestClass { @MyAnnotation private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } TestClass 클래스에 선언된 Annotation을 리플렉션을 이용해 확인할 수 있다.package me.sungbin; import java.lang.reflect.Field; import java.util.Arrays; public class App { public static void main(String[] args) { Arrays.stream(TestClass.class.getAnnotations()).forEach(System.out::println); Field[] declaredFields = TestClass.class.getDeclaredFields(); for (Field declaredField : declaredFields) { Arrays.stream(declaredField.getAnnotations()).forEach(System.out::println); } } }  표준 어노테이션 중 '@FunctionalInterface' 는 '@Override' 처럼 컴파일러가 체크해주는 어노테이션이지만, 실행 시에도 사용되므로 유지 정책이 "RUNTIME"으로 되어 있다. @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FunctionalInterface {} 유지 정책을 "CLASS" 으로 한다면컴파일러가 어노테이션의 정보를 클래스 파일에 저장할 수 있게 하지만,클래스 파일이 JVM에 로딩 될 때는 어노테이션의 정보가 무시되어 실행 시에 어노테이션에 대한 정보를 얻을 수 없다.→ CLASS 가 유지정책의 기본값임에도 불구하고 잘 사용되지 않는 이유 지역 변수에 붙은 어노테이션은 컴파일러만 인식할 수 있으므로, 유지 정책이 RUNTIME인 어노테이션을 지역변수에 붙여도 실행 시에는 인식되지 않는다. @Documented어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다.표준 어노테이션 중 Override와 SuppressWarnings를 제외하고 모두 Documented 메타 어노테이션이 붙어 있다. @Documented애노테이션 정보가 javadoc으로 작성된 문서에 포함된다고 한다. 이것이 무슨말일까? 내 코드가 자바docs에 올라간다는 말일까?정확히 말하면 자바docs에 올라간다는 말이 아니라,직접 javadoc을 만들 수 있다는 뜻이다.이런식으로 만들 수 있는데, Local 지역입력 ko_KRother command line arguments : 한글깨짐 방지-encoding UTF-8 -charset UTF-8 -docencoding UTF-8적절하게 내용을 채운뒤 output directory에 경로를 입력해주면 끝이다.그러면 @Documented를 붙인거와 안 붙인것을 비교해보자. 코드public class Korea implements Great{ @Override @Make public String country() { return "한국"; } } 없는거 있는거JavaDoc애노테이션을 알기 전에 JavaDoc에 대해 알아보자.JavaDoc은 Java코드에서 API문서를 HTML 형식으로 생성해주는 도구이다.HTML 형식이기 때문에 다른 API를 하이퍼 링크를 통해 접근이 가능하다. JavaDoc TagsJavaDoc은 여러 Tag를 작성하여 문서를 완성한다.Java 코드에서 애노테이션으로 추가한다.IDE에서 /** 입력 후 엔터를 치면 자동으로 형식이 생성된다.Javadoc Tags의 종류들@author@deprecated@exception@param@return@see@serial@serialData@serialField@since@throws@since@throws@version@Inherited어노테이션이 자손 클래스에 상속되도록 한다.'@Inherited' 가 붙은 어노테이션을 조상 클래스에 붙이면, 자손 클래스도 이 어노테이션이 붙은 것과 같이 인식된다.MyAnnotation은 Inherited 애노테이션을 통해 자손 클래스에도 인식되도록 정의한다.package me.sungbin; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, FIELD}) @Inherited public @interface MyAnnotation { } 부모클래스인 Sungbin클래스에 MyAnnotation을 정의package me.sungbin; @MyAnnotation("hi") public class Sungbin { @MyAnnotation("yang sung bin") private String name; }  Sungbin 클래스의 자식 클래스인 Child 클래스에는 별도의 어노테이션 정의가 없다.package me.sungbin; public class Child extends Sungbin { } 리플렉션을 이용해 Child 클래스의 어노테이션을 확인해보자.→ ChildSson 클래스에는 정의한 애노테이션이 없지만,→ 부모 클래스인 Sson 클래스에 정의한 애노테이션이 확인됨을 볼 수 있다.→ Inherited 애노테이션을 통해 자식 클래스까지 전파될 수 있음을 확인할 수 있다. Inherited 애노테이션을 바탕으로 리플렉션을 활용해 자식클래스에서부모클래스에 정의되어 있는 Inherited 애노테이션을 확인할 수 있다. 📚 리플랙션의 getDeclaredFields(); 를 하면 클래스에 정의된(선언된) 것들을 가져와서 조작할 수 있다. public이던, private 이던,, Getter와 Setter에 대해 논의를 하며 큰 비용을 소모하는 것이 크게 가치가 없다.. 객체지향을 얘기하며 Getter, Setter의 정의 관련한 내용으로 얘기할 수 있겠지만, Getter와 Setter가 없더라도 리플랙션을 이용하면 충분히 가져오고 수정할 수 있기 때문이다. 중요한건 Getter, Setter 가 아닌것 같다. @Repeatable보통은 하나의 대상에 한 종류의 어노테이션을 붙이게 되는데,'@Repeatable'이 붙은 어노테이션은 여러 번 붙일 수 있다. 일반적인 어노테이션과 달리 같은 이름의 어노테이션이 어러 개가 하나의 대상에 적용될 수 있기 때문에, 이 어노테이션들을 하나로 묶어서 다룰 수 있는 어노테이션도 추가로 정의해야 한다. @Native네티이브 메서드(native method)에 의해 참조되는 '상수 필드(constant field)'에 붙이는 어노테이션이다.여기서, 네이티브 메서드는 JVM이 설치된 OS의 메서드를 말한다.네이티브 메서드는 보통 C언어로 작성되어 있는데, 자바에서는 메서드의 선언부만 정의하고 구현하지 않는다.그래서 추상 메서드처럼 선언부만 있고 구현부가 없다. 어노테이션 타입 정의어노테이션의 요소어노테이션 내에 선언된 메서드를 어노테이션의 요소라고 한다. 📚 어노테이션에도 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 있다. 어노테이션의 요소는 반환 값이 있고 매개변수는 없는 추상 메서드의 형태를 가진다.다만, 어노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다.각 요소들은 기본값을 가질 수 있으며, 기본값이 있는 요소들은 어노테이션을 적용할 때 값을 지정하지 않으면 기본값이 사용된다.어노테이션의 요소가 오직 하나 뿐이고 이름이 value 인 경우, 어노테이션을 적용할 때 요소의 이름을 생략하고 값만 적어도 된다.요소 타입이 배열인 경우, 괄호{} 를 사용해 여러 개의 값을 지정할 수 있다.하나인 경우는 괄호{} 를 생략할 수 있다.java.lang.annotation.Annotation모든 어노테이션의 조상은 Annotation이다.그러나 어노테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotation을 조상으로 지정할 수 없다. @interface TestInfo extends Annotation{ // 에러. 허용되지 않는 표현이다. int count(); String testedBy(); ... } Annotation 을 살펴보면Annotation은 어노테이션이 아니라 일반적인 인터페이스로 정의되어 있다. 모든 어노테이션의 조상인 Annotation 인터페이스가 위와 같이 정의되어 있기 때문에모든 어노테이션 객체에 대해 equals(), hashCode(), toString() 과 같은 메서드를 호출하는 것이 가능하다.리플랙션(Reflection)을 이용해 특정 클래스에 선언된 애노테이션들을 조회하여 equals, hashCode, toString 메서드를 호출해본다.어노테이션 요소의 규칙어노테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙요소 타입은 기본형, String, Enum, 어노테이션, Class 만 허용() 안에 매개변수를 선언할 수 없다.예외를 선언할 수 없다.요소를 타입 매개변수로 정의할 수 없다.마커 어노테이션 Marker Annotation값을 지정할 필요가 없는 경우,어노테이션의 요소를 하나도 정의하지 않을 수 있다.Serializable 이나 Cloneable 인터페이스처럼, 요소가 하나도 정의되지 않은 어노테이션을 마커 어노테이션이라 한다. 🙋🏻 이런 마커 어노테이션은 왜 사용될까? 글을 찾아보니 아래의 내용이 있었다.마커 어노테이션을 통해 코드 작성 시점, 컴파일 시점, 러타임 시점에 부가적인 작업을 추가할 수 있을 것이다.코드 작성 시점에 어노테이션 정보를 통해 부가적인 정보를 check 하여 컴파일에러를 발생시킬 수 있을 것이며컴파일하는 과정에서 어노테이션 정보를 바탕으로 부가적인 정보를 포함하여 컴파일된 결과를 내보낼 수도 있을 것이다.또한, 런타임 시점에 리플랙션을 이용하여 애노테이션 정보를 바탕으로 부가적인 작업을 할 수 있을 것이다.Java8 어노테이션 변화 애노테이션 관련 큰 변화 두가지자바 8 부터 애노테이션을 타입 선언부에도 사용할 수 있게 되었다.자바 8 부터 애노테이션을 중복해서 사용할 수 있게 되었다.타입 선언부제네릭 타입변수 타입매개변수 타입예외 타입...타입에 사용할 수 있으려면TYPE_PARAMETER : 타입 변수에만 사용할 수 있다.TYPE_USE : 타입 변수를 포함해서 모든 타입 선언부에 사용할 수 있다.중복 사용할 수 있는 애노테이션을 만들기@Repeatable애노테이션들을 감싸고 있을 컨테이너 애노테이션을 선언해야 한다.중복 사용할 애노테이션 만들기컨테이너 애노테이션은 중복 애노테이션과 @Retention 및 @Target 이 같거나 더 넓어야 한다.컨테이너이기 떄문에 , 이것은 접근 지시자의 범위와 유사한 개념이라고 볼 수 있다.@Retention : 애노테이션을 언제까지 유지할 것이냐?@Target : 애노테이션을 어디에 사용할 것이냐?애노테이션 프로세서애노테이션 프로세서는 소스코드 레벨에서 소스코드에 붙어있는애노테이션을 읽어서 컴파일러가 컴파일 하는 중에 새로은 소스코드를 생성하거나 기존 소스코드를 바꿀 수 있다.또는, 클래스(바이트코드) 도 생성할 수 있고 별개의 리소스파일을 생성할 수 있는 강력한 기능이다. 애노테이션 프로세서 사용 예롬복 (기존코드를 변경한다)AutoService (리소스 파일을 생성해준다.)java.util.ServiceLoader 용 파일 생성 유틸리티@Override애노테이션 프로세서 장점바이트코드에 대한 조작은 런타임에 발생되는 조작임으로 런타임에 대한 비용이 발생한다.but. 애노테이션 프로세서는 애플리케이션을 구동하는 런타임 시점이 아니라,컴파일 시점에 조작하여 사용함으로 런타임에 대한 비용이 제로가 된다.단점은 기존의 코드를 고치는 방법은 현재로써는 public 한 API 가 없다.롬복 같은 경우.. 기존 코드를 변경하는 방법이지만 public 한 api를 이용한 것이 아님으로 해킹이라고 할 수 도 있다.롬복(Lombok)의 동작원리Lombok@Getter @Setter, @Builder 등의 애노테이션과애노테이션 프로세서를 제공하여 표준적으로 작성해야 할 코드를 개발자 대신 생성해주는 라이브러리이다.사용하기의존성 추가IntelliJ Lombok 플로그인 설치Intellij Annotation Processing 옵션 활성화동작원리컴파일 시점에 "애노테이션 프로세서"를 사용하여 (자바가 제공하는 애노테이션 프로세서)소스코드의 AST(Abstract Syntax Tree) 를 조작한다.AST에 대한 참고 사이트 (아래 참조 참고)javax.annotation.processing || Interfaec Processor⇒ 소스코드의 AST를 원래는 참조만 할 수 있다. // 수정하지 못한다. 그리고 하면 안된다!⇒ 그러나 수정이 됬음을 알 수 있다.(컴파일 이후 바이트코드 확인)⇒ 참조만 해야 하는 것을 내부 클래스를 사용하여 기존 코드를 조작하는 것임으로 "해킹" 이라고 얘기하기도 한다. 논란 거리공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.특히 이클립스의 경우에는 Java Agent를 사용하여 컴파일러 클래스까지 조작하여 사용한다.해당 클래스들 역시 공개된 API가 아니다보니 버전 호환성에 문제가 생길 수도 있고 언제라도 그런 문제가 발생해도 이상하지 않다.그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며, 대안이 몇가지 있지만 롬복의 모든 기능과 편의성을 대체하지 못하는 상황이다.AutoValueImmutables기존 Getter, Setter, equals, hasCode 등의 메소드를 생성하는 순간?해당 클래스는 이미 방대해진 모습을 볼 수 있다.해당 클래스를 위한 메소드들이 선언이 되어 있더라도 위 메소드들 사이에 파묻혀 있다면?개발자 입장에서 놓칠 수도 있다. (그래서 boilerplat 코드라는 개념도 나온다.)⇒ 롬복을 이용하여 쉽게, 그리고 가독성 높게 클래스를 구현할 수 있다. package me.sungbin; import lombok.Getter; import lombok.Setter; @Getter @Setter public class Member { private String name; private int age; }  위의 롬복이 적용된 코드를 컴파일하면 아래와 같이 나온다. package me.sungbin; public class Member { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } 결론위에서 미션에 대해 다 애기한 듯 하다. 결론을 내보겠다.어노테이션을 사용하는 이유는 단순하다.코드의 가독성과 유지보수성 향상: 어노테이션을 사용하면 개발자가 코드의 의도를 더 명확하게 표현할 수 있습니다. 예를 들어, @Override 어노테이션은 메소드가 상위 클래스의 메소드를 오버라이드한다는 것을 명시합니다.컴파일 시간 검사: 어노테이션을 통해 코드에 대한 추가적인 검사를 수행할 수 있어, 잠재적인 오류를 컴파일 시간에 발견하고 수정할 수 있습니다.런타임 처리: 특정 어노테이션이 적용된 요소를 런타임에 검사하고 처리할 수 있어, 리플렉션을 사용한 동적 처리가 가능해집니다. 이는 프레임워크와 라이브러리에서 많이 활용됩니다.이런 이유로 사용이 되며 이로인하여 코드문서화, 컴파일러에 특정처리를 지시, 코드분석 도구 지원, 런타임처리등이 가능해지게 된다. 우리가 스프링의 의존성 주입을 할 때 @Autowired도 이런 기능처리를 해준다. 컴파일러에서의 처리:코드 검증: 컴파일러는 어노테이션을 사용하여 코드에 대한 추가적인 검증을 수행합니다. 예를 들어, @Override 어노테이션은 메서드가 실제로 상위 클래스나 인터페이스의 메서드를 오버라이드하는지 확인하는 데 사용됩니다. 만약 오버라이드하는 메서드가 없다면, 컴파일러는 에러를 발생시킵니다.정책 적용: 일부 어노테이션은 컴파일러에 특정 정책을 적용하도록 지시합니다. 예를 들어, @Deprecated 어노테이션이 적용된 요소를 사용하는 코드는 컴파일러 경고를 발생시키며, 이는 개발자에게 해당 요소가 더 이상 사용되지 않아야 함을 알립니다.소스 코드 변환: 어노테이션 프로세서를 사용하여 컴파일 시점에 소스 코드를 자동으로 생성하거나 수정할 수 있습니다. 이는 코드 생성 라이브러리나 프레임워크에서 흔히 사용되는 기법입니다.런타임에서의 처리:리플렉션을 통한 접근: 런타임에는 리플렉션 API를 사용하여 어노테이션 정보에 접근할 수 있습니다. 이를 통해 개발자는 실행 중인 프로그램에서 클래스, 메서드, 필드 등에 적용된 어노테이션을 검사하고, 해당 어노테이션에 지정된 정보를 바탕으로 동적인 처리를 수행할 수 있습니다.동적 처리: 런타임에 어노테이션을 기반으로 동적 처리를 하는 예로, Java EE와 Spring 프레임워크에서 의존성 주입을 구현하는 방법을 들 수 있습니다. 이러한 프레임워크는 특정 어노테이션(@EJB, @Autowired)이 붙은 필드나 메서드를 찾아, 런타임에 자동으로 의존성을 주입합니다.구성 관리: 어플리케이션의 구성 정보를 어노테이션을 통해 관리할 수 있습니다. 예를 들어, 웹 어플리케이션에서 서블릿이나 REST 엔드포인트를 정의할 때 사용되는 어노테이션들은 런타임에 웹 서버가 해당 구성 정보를 읽어들여 서비스를 구동하는 데 사용됩니다.이러한 방식으로 어노테이션은 컴파일 시점과 런타임에 다양한 목적으로 활용됩니다. 컴파일 시점에는 코드의 정확성을 보장하고, 런타임에는 코드의 동적인 행위를 제어하는 데 중요한 역할을 합니다.커스텀 어노테이션이것 또한 위에서 예제로 많이 보여드렸으므로 어노테이션 예제를 보여줌으로 이 글을 마치려고 한다. 정말 단순히 어노테이션부터 시작해서 리플렉션까지 갔는데 정말 험난한 여정이였지만 보람찬 공부가 되었다. package me.sungbin; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, FIELD}) @Inherited public @interface MyAnnotation { String value(); }  📚 참조https://b-programmer.tistory.com/264http://javaparser.org/inspecting-an-ast/

백엔드인프런워킹업스터디클럽백엔드미션어노테이션

코딩웍스(Coding Works)

[오프라인 강의 오픈] 퍼블리셔 취업 개인 포트폴리오 제작 및 JS 강좌(4주과정)

안녕하세요. 코딩웍스입니다.코딩웍스가 올해 4월부터 시작하는 오프라인 강의를 소개합니다.이번에 <퍼블리셔 취업을 위한 개인 포트폴리오 제작 및 JS 강좌(4주과정)>를 오프라인으로 진행합니다."코딩웍스만 믿고 잘 따라오시면 퍼블리셔 취업과 이직할 수 있습니다!!""2명 정원으로 밀착형 개인별 수업"이번에 오픈하는 오프라인 강의 특징은 4주과정 동안 퍼블리셔 직군으로 빠른 취업과 경쟁력 있는 면접을 볼 수 있는 역량을 기릅니다. 구체적으로 퍼블리셔 취업을 위해 필수인 개인 포트폴리오 홈페이지를 코딩웍스의 개인 지도를 받으면서 4주 동안 정말 괜찮게 완성합니다.수업내용은 코딩웍스가 지난 국비 퍼블리싱 학원에서 수료하기 전 한 달 반 동안 진행했던 <개인 포트폴리오 홈페이지 제작> 커리큘럼을 그대로 진행합니다. 물론 국비 퍼블리싱 학원 학생들처럼 학원에서 작업하는 시간을 많이 할애할 수는 없지만 이런 부분을 커버하기 위해 매주 진행해야 할 작업량을 개인별로 제시하고 다음주에 세밀하게 체크합니다. 국비 퍼블리싱 학원 때는 학생들이 많아서 개인별로 체크 & 피드백을 하는 부분이 제한적이지만 이번에는 소그룹으로 진행하기 때문에 수강생 개인에 맞게 거의 1:1로 수정 보완을 코딩웍스와 함께 하게 됩니다.특히 이번 오프라인 수업은 제이쿼리 및 바스크립트 기초부터 핵심까지 현업에서 개발자와 협업을 위한 순수 자바스크립트 실력도 갖추게 됩니다.  🌈 코딩웍스 오프라인 강의는 다릅니다.웹사이트 디자인 방향성과 체계적인 퍼블리싱 실력 갖추기디자인이 약점인 학생의 경우 디자인을 위한 타겟 사이트를 정하고 디자인 방향성을 잘 잡을 수 있도록 참고 레퍼런스를 보면서 1:1로 지도합니다.디자인을 기초로 HTML 와이어프레임 구조 설계하면서 경력자와 같은 HTML 구조 설계 방법, 효율적인 클래스 네이밍 방법 그리고 다양한 실무 팁까지 학습합니다. 학습과 동시에 큰 어려움 없이 실제 포트폴리오 웹사이트 퍼블리싱까지 완성합니다.개인 포트폴리오 홈페이지 완성(Intro, About, Skill, Publishing, Website, Mobile Web, Epilogue)개인 포트폴리오 내 퍼블리싱 결과물 완성(모바일 웹 퍼블리싱, 웹사이트 퍼블리싱, 실전 퍼블리싱, JS 작업물)7:3 비율로 개인 포트폴리오 제작 수업 중심 누가봐도 괜찮은 퍼블리셔 취업을 위한 개인 포트폴리오 홈페이지 완성할 수 있습니다.개인 포트폴리오 홈페이지 뿐만 아니라 포트폴리오 웹사이트를 반응형으로 만드는 작업도 진행하므로 반응형에 대한 이해도와 실력을 충분히 갖추게 됩니다.퍼블리셔 취업이 No.1 미션이므로 [개인 포트폴리오 홈페이지 제작] 수업과 [바닐라 자바스크립트] 수업은 7:3 비율로 포트폴리오 수업을 중심으로 진행됩니다.코딩웍스와 함께 소그룹으로 개인에 맞게 1:1로 수정 보완합니다. 특히 개인별 제작 및 취업 로드맵을 설계하고 로드맵에 맞게 체크 & 피드백하므로 시행착오를 최소화합니다.매주 미션과 테스트를 통해서 개인 포트폴리오 홈페이지 제작과 실력이 자연스럽게 완성되는 코딩웍스만의 체계적인 스케쥴링에 따라 오기만 하면 됩니다.과정 완료 시점에 취업을 위한 이력서 및 자기소개서, 채용공고, 면접요령 등 코딩웍스 학생들을 위한 철저한 마무리타 지원자와 확연히 차별화되는 경력력, 제이쿼리 및 바스크립트 기초부터 핵심까지제이쿼리 및 바스크립트 기초부터 핵심 수업은 수업 시간 내에서 거의 모두 해결할 수 있도록 커리큘럼이 준비되었습니다. 이론만 하고 넘어가는 것이 아니라 해당 이론을 공부하면 해당 이론으로 활용해서 다양한 스몰미션(Small Mission)을 만들어 봅니다. 이론 학습 → 스몰미션 제작.. 으로 이론을 더 확실히 갖출 수 있습니다.제이쿼리 및 바스크립트 기초부터 핵심 수업은 제이쿼리 수업을 중심으로 진행됩니다. 그리고 순수 자바스크립트 기초를 익히는 수업도 병행됩니다. [참고사항] 코딩웍스 오프라인 강의에서 제이쿼리 및 바스크립트 기초부터 핵심 강의는 개발자 취업을 위한 심도 있는 강의까지는 아닙니다. 퍼블리셔로서 인터렉티브한 웹을 구현하고, 실무에서 개발자와 협업에서 문제 없이 의사소통하는 수준의 강의입니다. [ 제이쿼리 및 자바스크립트(Vanilla JavaScript) PDF 교재] 소개 코딩웍스 자체 제작 교재제이쿼리(jQuery) PDF 교재 제공(79페이지 분량)제이쿼리 교재에 나오는 완성본 파일 제공자바스크립트(Vanilla JavaScript) PDF 교재 제공(210페이지 분량)자바스크립트 교재에 나오는 완성본 파일 제공퍼블리셔 취업을 준비한다면 이건 꼭! 기억하셔야 합니다.대부분의 수강생이 개인 포트폴리오 홈페이지 제작 가이드 없이 혼자서 준비합니다. 국비지원 학원 또는 독학이면 본인은 정말 열정을 다해 열심히 준비했지만, 객관적으로 경쟁력이 없는 개인 포트폴리오 홈페이지를 만드는 경우가 대부분입니다. 열심히 안 해서가 아닙니다. 처음부터 이상한 방향으로 가고 있는데 그걸 잡아주고 좋은 방향으로 갈 수 있도록 올바른 로드맵을 제시받고 체크되고 개인에게 맞는 피드백을 받지 못했기 때문입니다. 이런 개인 포트폴리오 홈페이지를 가지고 퍼블리셔 입사지원을 아무리 해도 면접 연락이 거의 오지 않거나 이상한 곳에서나 연락이 옵니다. 속상한 일이지만 다 그럴만하기 때문입니다. 회사가 퍼블리셔를 뽑을 때 인성이나 느낌으로 뽑지 않습니다. 기술자.. 곧 프로를 뽑습니다. 개인 포트폴리오 홈페이지에 아무리 자신이 열심히 열정적으로 일하는 사람이라고 어필해도 결과물이 프로답지 않으면 뽑지 않습니다..여러분이 지금까지 퍼블리셔 취업을 위해 열심히 공부하고 준비했더라도 마지막 단계를 잘못 보내면 수많은 시행착오를 겪게 되고, 이런 시행착오로 지치게 되고 결국엔 수개월 또는 그 이상 노력한 기간이 물거품이 됩니다.6주 동안 코딩웍스를 믿고 잘 따라오시면 시행착오 없이 퍼블리셔 취업을 위한 충분한 준비가 될 것이라고 확신합니다.💡 강의 개요교육요일 : 평일반(윌화), 주말반(토일)교육시간 : 12:00 - 18:00(6시간)교육일정 : 평일반 개강(2023년 4월 17일) ※ 평일반 신청이 마감되었습니다.교육일정 : 주말반 개강(2023년 4월 22일)교육과정 : 4주 과정(48시간)교육비용 : 816,000원교육장소 : 2호선 신촌역 부근(상세한 위치는 신청 완료 후 추후 공지)교육내용 : 개인 포트폴리오 홈페이지 제작, 자바스크립트 핵심 강좌, 이력서작성 및 면접요령모집정원 : 최대 3명 정원(최소 2인 이상 개강)결제방법 : 계좌이체 ※ 사업자가 아나라서 카드결제와 현금영수증 발급 불가합니다.상담문의 : webnlife@naver.com (이메일 상담 후 필요한 경우 유선으로 상세하게 상담)신청방법 : 이메일 상담 및 신청(상담 신청 제목에 오프라인 강의 상담 또는 오프라인 강의 신청이라고 써주세요.)신청기간 : 평일반 신청(2023년 4월 10일까지) / 주말반 신청(2023년 4월 16일까지)수업준비 : 수강생 개인 노트북 필수 지참(수업용 PC가 제공되는 강의장은 아닙니다.)※ 상담 메일 주실 때 현재 어떻게 학습하고 있고 얼마나 학습했는지 어느 정도 수준인지.. 국비 또는 독학 등 어디서 언제 공부했는지.. 나이 성별 등 기본적인 정보를 알려주시면서 상담하시면 제가 판단하기에 더 도움이 될 것 같습니다. 그리고 이메일 상담 후 전화 상담이 필요한 경우는 휴대폰 번호도 적어주세요.💡 교육 내용 상세개인 포트폴리오 홈페이지 제작(제작 로드맵, 디자인 컨셉, 컨텐츠 구성, 와이어프레임 구조)제이쿼리 및 바스크립트 기초부터 핵심 강좌이력서 및 자기소개서 작성 요령구직 사이트 실제 채용공고를 통해 지원회사 선정 요령퍼블리셔 취업 면접요령(모의면접 실시) 💡 참고 사항코딩웍스와 잘 맞아야 하기 때문에 코딩웍스의 수업을 경험해보신 분만 신청 대상이 됩니다.월요일 또는 화요일이 공휴일인 경우 다음 날로 자동 변경됩니다. ex) 수업일 : 월수 또는 화수퍼블리싱 기본 이론을 수업하지는 않습니다. 최소한 코딩웍스 강의 중 <HTML+CSS+JS 포트폴리오 실전 퍼블리싱(시즌1)> 강의는 완강하고 충분히 이해하는 수준이어야 합니다. 그리고 <중상급 퍼블리싱을 위한 CSS3의 모든 것>의 내용은 모두 충분히 이해하고 있는 수준이어야 합니다.해당 과정을 신청을 할 수 있는 현재 상태인지를 먼저 상담을 통해서 철저하게 체크하고 신청하시기 바랍니다.4주 단위로 강의가 진행되니 이번 일정에 맞지 않는 상태라면 좀 더 준비해서 다음 기수에 신청하시기 바랍니다. 💡 수강 혜택제이쿼리 및 자바스크립트(Vanilla JavaScript) PDF 교재 및 완성본 파일 제공개인 포트폴리오 홈페이지 제작에 필요한 다양한 교재 제공개인 포트폴리오 홈페이지 제작과 실무를 위한 각종 정보 및 다양한 웹사이트🚩 수강 대상인프런에서 코딩웍스 강의를 수강한 경험이 있는 수강생(강사와 스타일이 맞는지 체크가 중요)개인 포트폴리오 홈페이지 제작 단계이거나 제작 중인 수강생현재 퍼블리싱 공부를 시작했거나 한창 하고 있는 수강생은 대상이 되지 않습니다.신청 전에 이메일과 유선을 통해서 자신에게 맞는 과정인지 반드시 코딩웍스와 체크하고 신청하시기 바랍니다. 🌈 코딩웍스 오프라인 강의 후기코딩웍스 개인 포트폴리오 홈페이지 제작 오프라인멘토링 학생 후기 #01안녕하세요. 코딩웍스 선생님의 취업 멘토링에 참여한 학생입니다.저는 졸업 후 프리랜서 디자이너 일을 병행하면서, 퍼블리셔로 취직하고자 코딩웍스 선생님 강의를 독학으로 약 2년간 수강하였습니다.디자인 전공을 해서 그런지 내용물을 디자인하고 퍼블리싱하여 채워넣는 것, 그리고 포폴사이트를 디자인 하는것은 어렵지 않았습니다. 선생님 강의를 독학하면서 포트폴리오 웹사이트를 디자인하고, 퍼블리싱한 후 나름대로 섹션에 내용물을 채워넣고난 뒤 멘토링에 처음으로 참여했습니다.선생님의 꼼꼼한 멘토링 과정에서 제 코드를 하나하나 보시며 코딩 기본기에 대해 상세한 피드백이 이루어졌고, 기본기에 대해 다시한번 재정비하는 시간을 가질 수 있었습니다. 파일 정리법부터 폴더 구조까지 체계적으로 관리하는 법, 수많은 이미지 소스들을 어떻게 하면 효율적으로 네이밍을 할 지.. 이런 사소한 하나하나의 팁을 다 알려주셨습니다.매회 수업 이후 집에가서 코드를 수정하며 배운 내용을 적용했더니, 작업의 효율성이 저절로 올라가는 것을 느꼈습니다. 그리고 ‘내가 정말 많이 부족했구나. 그럴싸하게 퍼블리싱 결과물을 만드는것 만이 다가 아니구나. 원래의 실력으로 실무에 바로 투입되었다면 정말 팀원에게 민폐를 끼치겠구나.’ 라는 생각이 저절로 들었습니다. 결국 퍼블리셔는 협업을 해야하는 직무이기에, 내가 코딩 습관을 잘못 들여놓으면 주변 동료들이 내 코드를 수정하고 함께 작업하는 것이 정말 힘들거란 생각이 들었습니다.코딩웍스 선생님의 지도 하에, 기본기를 보완해 나가며 퍼블리싱 작업물의 갯수는 물론이고 퀄리티 또한 전에 비해 더욱 올라갈 수 있었습니다. 저는 멘토링 초반에 웹사이트 퍼블리싱 결과물이 없었던 상황 이었는데, ‘입사지원이 조금 늦어지더라도 웹사이트 퍼블리싱 섹션을 채워 이력서를 내 보자’ 는 선생님의 제안이 있었습니다. 이후 저는 선생님의 지도하에 1~2주에 한개의 웹사이트를 제작하였고, 현재는 총 3개의 퀄리티 있는 웹사이트를 만들어 입사지원 할 수 있게 되었습니다. (NGO/일반기업/쇼핑몰 이렇게 세가지 분야 웹사이트 입니다.)만약 이 웹사이트 퍼블리싱 섹션을 채우지 않고 지원한다면.. 입사 경쟁력이 떨어지는것은 물론이고, 제 스스로도 면접볼 때 떳떳하지 못할 것 같았거든요. 선생님께서 좋은 제안을 해주신 덕분에, 예전보다 더욱 자신감을 갖고 입사지원을 할 수 있게 되었습니다.그리고 웹사이트 퍼블리싱 작업물 3가지를 진행할 때, 각 웹사이트마다 체계적으로 와이어프레임 구조를 짜는 법을 디테일하게 알려주셨는데 이것이 정말 많은 도움이 되었습니다. 처음에는 박스 구조를 의미없이 대강 그리면 되겠다 생각했는데, 선생님께서 박스구조 그리는 방식에 대해 세세하게 체크해 주셨습니다. 이 훈련을 세 번 반복했더니, 혼자서도 복잡한 구조의 웹사이트를 처음부터 끝까지 만들 수 있겠다는 자신감이 생겼습니다. 이처럼 코딩웍스 선생님의 취업 멘토링은, 취업뿐만 아니라 입사 이후 실무에서도 바로 적용할 수 있는 꿀팁이 다 담겨 있다고 생각합니다. 취업도 중요하지만 실무에서 민폐를 끼치지 않는(?) 그런 기본기 충실한 퍼블리셔가 될 수 있도록 꾸준히 공부해 나가려고 합니다. 앞으로도 좋은 강의 많이 만들어 주세요. 코딩웍스 선생님 감사합니다. ^^ ▲ 코딩웍스 개인 포트폴리오 제작 오프라인 강의 수강생 면접 취업 결과 카톡을 받았습니다.코딩웍스 개인 포트폴리오 홈페이지 제작 오프라인멘토링 학생 후기 #02안녕하세요. 코딩웍스 선생님의 오프라인 강의에 참여한 학생입니다.저는 국비학원에서 백엔드 과정을 듣던 중 퍼블리싱에 관심이 생겨 과정 수료 후 선생님의 강의를 들으면서 독학을 해왔습니다.강의에 있는 여러가지 스킬들을 사용한 많은 예제들을 만들어 가며 실력을 키워왔었고 클론코딩도 큰 어려움 없이 제작할 수 있는 단계가 되었습니다.다만 디자인쪽 전공이 아니다 보니 제일 중요한 개인 포트폴리오 홈페이지와 모바일 웹, 반응형 웹 작업물을 제작하는데 어려움을 겪었습니다.클론 코딩과 달리 개인 제작 사이트는 백지상태에서 시작하려고 하니 어디서부터 손을 대야 할지 모르겠고 디자인 역시 어떤식으로 하면 좋을지 고민만 하며 제작을 하지 못하고 있었습니다.그러던 중 오프라인 강의를 개강하신다는 글을 보고 마침 교육 내용과 수강 대상이 저와 딱 맞는 조건이기에 망설임없이 신청했습니다.지금까지 제작한 결과물들의 리뷰와 퍼블리싱 기본기에 대해 상세하게 배웠으며 코딩을 하면서 생긴 잘못된 습관들을 고칠 수 있는 좋은 자리였습니다.주석처리 꼼꼼하게 하기, CSS 줄 코딩 하지 않기, 선택자 순서 지키기, 클래스 네임 일관성 있게 짓기, 폴더 구조를 체계적으로 관리하는 법 등 기초적이지만 정말 중요한 것들을 배웠고그러한 기본기들을 갖춰 갈수록 퍼블리싱이 체계적으로 이뤄지고 실력도 빠르게 성장하는걸 느낄 수 있었습니다.퍼블리싱이란 것이 단순히 웹 사이트상에 보이는 것만이 전부가 아닌 그 안에 탄탄하게 짜여진 HTML 구조, CSS 스타일링이 중요하다는 것을 배웠고 그런 탄탄함이 있어야 반응형 제작, 유지보수에 용이하고 향후 실무에서 팀원들과의 협업에서도 중요한 요소란 것을 생각해볼 수 있는 계기가 되었습니다.제가 부족하다고 느낀 디자인 부분은 코딩웍스 선생님의 조언에 따라 타겟 사이트를 잘 선정하는 것으로 극복할 수 있었습니다. 어떤 주제의 사이트를 타겟으로 잡을 것인지, 해당 타겟 사이트에서 불필요한 부분은 제외, 필요한 것은 보충하면서 디자인 구상이 완료되면 자신만의 컨텐츠를 채워넣는 식으로 반응형 웹 사이트 디자인, 와이어프레임 제작, 퍼블리싱을 하는 체계적인 프로세스를 배웠습니다.계획적으로 퍼블리싱을 하지 않으면 이도저도 아니게 되고 결국엔 제대로 된 보기 좋은 결과물을 만들 수 없다는 것을 알게 되었고 이런 프로세스로1~2주에 1개씩 웹 사이트를 제작하면서 총 3개의 누가 봐도 보기 좋은 반응형 웹사이트를 제작하게 되었습니다.앞으로는 혼자서 디자인 선정 부터 퍼블리싱까지 할 수 있을 것 같다는 생각이 들면서 점점 웹 사이트 제작에 자신감도 생겼고개인 포트폴리오 사이트에 당당하게 넣을 수 있는 결과물도 생기니 자신의 가치가 올라간다는 것을 느꼈습니다.가장 까다롭다고 생각한 개인 포트폴리오 사이트 제작의 경우에도 선생님의 거의 1:1과 다름없는 지도 하에 평범한 포트폴리오 사이트가 아닌 제 결과물들을 확실하게 보여줄 수 있고 필요한 요소들만 들어간 제가 봐도 너무나 만족스러운 개인 사이트를 제작 할 수 있었습니다.그리고 퍼블리셔에게 필수적인 제이쿼리 이론들에 대해서 학습하고 스몰미션들을 통해 어떤 경우에 사용하고 어떻게 응용할 수 있는지 배웠으며 이론들을 활용해서 제작한 간단한 연습 결과물들도 개인 사이트에 넣어서 경쟁력을 올릴 수 있는 기회가 됐습니다.끝으로 코딩웍스 선생님의 오프라인 강의는 저에게 큰 실력 향상과 높은 취업 경쟁력, 앞으로 가야할 길을 알 수 있었던 소중한 시간이였습니다.아마 선생님의 강의를 듣지 않았다면 고민만 하다가 아무것도 하지 못하고 그저 그런 퍼블리셔가 되었거나 퍼블리셔의 꿈을 접었을지도 모르겠습니다.이젠 당당하게 입사 지원을 하면서 즐겁게 퍼블리싱을 할 수 있는 사람이 됐다는게 정말 기쁩니다.앞으로도 강의에서 배운 것들을 잊지 않으며 꾸준히 성장해 나가려고 합니다. 좋은 자리를 마련해주신 코딩웍스 선생님 정말 감사합니다!

Dream

[ 인프런 워밍업 클럽 Study FE 0기 ] Week 1 발자국

들어가기 앞서...2024년의 첫 해가 밝으며, 지금까지는 단순히 관심만 갖고 있었던 웹 개발에 대한 공부를 시작해보기로 결심했습니다. 먼저 HTML과 CSS의 공부를 마치니 운이 좋게 인프런에서 JS와 ReactJS 스터디를 진행한다는 소식을 듣게 되었습니다. 이런 좋은 기회를 놓치지 않겠다고 생각하고, 워밍업 클럽에 참여하게 되었습니다.발자국OT를 참가한지 엊그제 같은데 시간은 정말 순식간에 지나가는 것 같습니다. 벌써 스터디가 시작된 지 1주차가 되었습니다. 이제 그 동안의 강의 내용을 간단하게 요약하고, 회고를 남겨 보려고 합니다. 이번 주 강의는 따라하며 배우는 자바스크립트 A-Z (섹션 0~8) 부분을 진행하였습니다.요약Section 01. 자바스크립트 기초[ Console 객체 ]자바스크립트의 console 객체는 코드를 작성하고 테스트를 할 때, 사용하기 좋은 함수를 제공해준다. 다음은 강의에서 소개한 주요 Console 객체의 함수들이다.console.log(): console에 메시지를 출력한다. console.table(): console에 배열이나 객체의 데이터를 테이블 형태로 출력한다. console.error(): console에 에러 메시지를 출력한다. console.warn(); console에 경고 메시지를 출력한다. console.time(), console.timeEnd(): 세트로 사용되며, 두 함수 사이의 코드 실행 시간을 측정한다. [ var, let, const과 스코프 ]변수/상수를 선언할 때 let, const 그리고 var 키워들 사용한다. let과 const는 ES6에 새롭게 추가된 키워드이다. var 키워드는 오래된 선언 키워드로 let과 const 사용을 권장하고 있다.let: 변수 키워드, 재선언 X, 재할당O const: 상수 키워드, 재선언 X, 재할당Xvar: 재선언 O, 재할당 O 그리고 사용한 let/const와 var 키워드에 따라 스코프가 다르게 처리된다. 스코프(scope, 유효/참조 범위)란 어떠한 변수를 참조하려고 할 때, 그 변수에 접근 가능한 유효 범위이다.let/const: 모든 코드 블록 { } 내부에서 선언된 변수는 코드 블록 내에서만 유효. var: 함수 내에서 선언된 var 변수는 함수 내에서만 유효하며, 함수 내에서 블록 내·외부에 관계없이 접근 가능. [ 호이스팅 ]코드가 실행되기 전에 변수 및 함수 선언을 로컬 범위(유효 범위)의 맨 위로 끌어 올려지는 경우를 말한다.[ 자바스크립트 타입과 타입 변환 ]자바스크립트의 데이터 타입(자료형)은 다음과 같다.원시 타입: Boolean, String, Number, null, undefined, Symbol고정된 크기로 Call Stack 메모리에 저장실제 데이터가 변수에 할당참조 타입: Object, Array데이터 크기가 정해지지 않고 Call Stack 메모리에 저장데이터의 값이 Heap에 저장되며 메모리의 주소 값이 할당 자바스크립트 변수에 저장된 값은 다른 데이터 유형으로 변환될 수 있다.명시적 데이터 변환(개발자가 직접 함수를 사용해서 변환)자동 데이터 변환(자바스크립트 자체에 의해 자동으로 변환) [ 연산 및 Math ]자바스크립트에서는 기본적인 산술 연산자, 논리 연산자, 비교 연산자를 제공한다.Math를 통해 더 많은 연산을 이용할 수 있다. [ Template Literals]Backtick(`)을 사용하여 문자열을 표현한 것을 템플릿 리터럴이라고 한다. 템플릿 리터럴을 이용하면 다음과 같은 이점이 있다.줄 바꿈이 쉽다.${}을 사용하여 내부에 표현식을 포함할 수 있게 한다. [ Loops ]for: 초기식, 조건식, 증감식을 포함하는 반복문으로 주어진 조건이 참일 경우 블록 안의 코드를 반복 실행for/in: 객체의 열거 간으한 속성들을 반복하는데 사용.while: 주어진 조건이 true일 동안 코드 블록을 계속해서 실행.do/while: 먼저 코드 블록을 실행한 후, 조건을 확인한다. 그러고 나서 조건이 true일 동안 반복 실행한다. Section 02. Window 객체 및 DOM[ Window 객체 ]브라우저에 의해 자동으로 생성된다. (자바스크립트 객체 X, 브라우저에서 제공 O)이 window 객체는 다음과 같은 역할을 수행한다.브라우저에 접근 및 조작 가능자바스크립트 코드의 전역 객체 역할 [ DOM ]문서 객체 모델(Document Object Model, DOM)요소로 이루어진 HTML 파일을 Tree 구조로 표현한 객체 모델최상단에는 document 노드가 위치해 있으며, 이를 통해 DOM 접근 및 조작 가능Critical Render Path (웹 페이지 렌더링 콰정)데이터 파싱(HTML) ➔ DOM Tree 생성 ➔ CSSOM Tree 생성 ➔ JS 실행 ➔ Render Tree 생성 ➔ Layout 생성 ➔ Paint +) 강의에서 수 많은 Property 및 Method를 소개해 주셨지만 너무 많은 관계로 생략... Section 03. Event[ Event ]만약 인프런에서 로그인 버튼을 누르면 무엇이 일어날까? 당연히 로그인 페이지로 이동할 것이다. 이렇게 웹 페이지에서 발생하는 사용자의 행동에 대응하여 브라우저에서 일어나는 특정 사건을 "이벤트"라고 한다. 자바스크립트에서는 다음과 같은 이벤트가 존재한다.UI 이벤트load, change, resize, scroll, error 키보드 이벤트keydown, keyup, keypress마우스 이벤트click, dblclick, mousedown, mouseout, mouseover, mousemove, mouseup포커스 이벤트focus, blur폼 이벤트input, change, select, reset, submit, cut/copy/paste 이벤트를 등록하기 위해서는 addEventListener()를 사용하면 된다. 또한 이벤트 흐름에는 이벤트 bubbling과 Capturing라는 2가지의 기본 모델이 존재한다.[ Event Bubbling과 Event Capturing ]이벤트 bubbling은 가장 깊게 중첩된 요소에 이벤트가 발생했을 때, 이벤트가 위로 전달 되는 것을 의미한다. 이벤트 bubbling은 target 이벤트에서 시작해서 요소를 거쳐 document 객체를 만날 때까지 각 노드에서 모두 발생한다. 만약 bubbling 중단을 원한다면 event.stopPropagation()을 호출하면 된다.event.stopPropagation()DOM Tree를 통한 이벤트 흐름 중지 가능브라우저 기본 동작은 취소 X 이벤트 Capturing은 bubbling과 다르게 제일 상단에 있는 요소에서 아래로 이벤트가 내려오는 것을 말한다.[ Event Delegation ]이벤트 bubbling과 이벤트 Capturing을 통해서 이벤트 위임을 이해할 수 있다. 이 이벤트 위임은 '하위 요소의 이벤트를 상위 요소에 위임하는 것'이다. Section 04. 자바스크립트 중급[ this]Method의 this: 해당 객체를 가리킨다.함수에서 this: window 객체를 가리킨다.constructor의 this: 빈 객체를 가리킨다. [ bind, call, apply]call():함수를 호출하는 함수.첫 번째 인자 값으로 어떠한 것을 전달해 주면 호출되는 함수의 this가 인자 값으로 지정apply(): call()과 유사하나 인수 부분에 배열을 넣어줘야함.bind(): 해당 함수가 지정한 인자 값을 가리키도록 하지만 call(), apply()와 다르게 직접 함수 실행 X [ 삼항 연산자 ]다음과 같은 구문을 갖는다.조건 ? true이면 반환 : false이면 반환 [ Event Loop]자바스크립트는 동기 언어이다. 하지만 가끔 비동기로 작동하는 setTimeout()를 사용하는 예시를 볼 수 있다. 자바스크립트는 비동기 코드를 작성하기 위해서 자바스크립트 이외의 도움을 받는다.[ Closure ]다른 함수 내부에 정의된 함수가 있는 경우, 외부 함수가 실행을 완료하고 해당 변수가 해당 함수의 외부에서 더 이상 엑세스할 수 없는 경우에도, 해당 내부 함수는 외부 함수의 변수 및 액세스가 가능하다. 이 기능을 Closure라고 부른다.[ 구조 분해 할당 ]배열이나 객체의 속성을 해제하여 그 값을 개별 변수에 담을 수 있게 해주는 표현 식이다.[ Map, Filter, Reduce ]Map, Filter, Reduce은 배열 메서드의 대표적인 예시이다.map(): 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출하고 나온 결과를 모아 새로운 배열로 만들어 반환한다.filter(): 주어진 함수의 필터를 통과하는 모든 요소를 모아 새로운 배열로 반환한다.reduce(): 배열의 각 요소에 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결괏값을 반환한다.[ 얕은 비교 VS 깊은 비교 ]숫자, 문자열 등 원시 자료형은 값을 비교하게 된다. 하지만 배열, 객체 등의 참조 자료형은 참조되는 위치를 비교하게 된다. 얕은 비교를 하게 되면 원시 값의 경우 문제가 없지만 참조 값의 경우 실제 값이 아닌 저장된 위치(참조 값)이 비교되기 때문에 문제가 된다.깊은 비교를 사용하게 되면 참조 자료형도 실제 값으로 비교할 수 있게 된댜ㅏ.객체 depth가 깊지 않은 경우: JSON.stringify() 사용객체 depth가 깊은 경우: lodash 라이브러리의 isEqual() 사용[ 얕은 복사 VS 깊은 복사 ]위에서 정리한 내용처럼 복사에도 문제가 발생하게 된다. 따라서 참조 자료형의 값의 경우 깊은 복사를 사용하면 된다.JSON.stringify()lodash 라이브러리의 deepCopy[ 함수 표현식, 함수 선언문 ]함수 선언문: 함수 선언은 함수를 만들고 이름을 지정하는 것이다.일반적인 함수 선언 방식으로 function 키워드와 식별자를 표기하여 사용한다.함수 표현식은 함수를 만들고; 변수에 할당하는 것이다.익명 함수(function 키워드는 사용했으나 식별자 X), 화살표 함수 사용 Section 05. OOP[ OOP ]OOP(Object-Oriented Programming, 객체 지향 프로그래밍)란 Java 및 C를 비롯한 많은 프로그래밍 언어의 기본이 되는 프로그래밍 패러다임이다. 완전 간단하게 말하자면 객체 지향 프로그래밍은 객체들의 모임이라고 할 수 있다.OOP 특징으로는 다음과 같다.추상화:불필요한 정보는 숨기고 중요한 정보 만을 표현함으로써 프로그램을 간단히 만드는 것.상속:새로운 클래스가 기존의 클래스의 자료와 연산을 이용할 수 있도록 해주는 것.기존 클래스: 부모 클래스, 상위 클래스새로운 클래스: 자식 클래스, 하위 클래스다형성: 하나의 틀을 가지고 여러 개의 다양한 형태를 만드는 것이다.overriding을 통하여 다형성 구현 일반적인 코드를 재사용하고 작성할 수 있다.캡슐화:클래스 안에 있는 Method, 변수 등을 하나로 묶어준다. [ class와 constructor ]class에서는 constructor라는 특별한 메서드를 제공한다. 이 constructor는 생성자로, 인스턴스화된 객체에서 다른 메서드를 호출하기 전에 수행해야 하는 사용자 지정 초기화를 할 수 있게 해준다.클래스를 new 키워드를 붙여 인스턴스 객체로 생성하면 넘겨 받은 인자 값과 함께 constructor가 가장 먼저 실행이 된다. 따라서 이 곳에 초기화를 해야 하는 작업을 수행한다.[ Super]자바스크립트에서 super는 다음과 같은 역할을 수행한다.자식 클래스 내에서 부모 클래스의 생성자를 호출할 때 사용한다.자식 클래스 내에서 부모 클래스의 메소드를 호출할 때 사용한다. Section 06. 비동기[ 동기와 비동기 ]동기(Synchronous)코드를 순차적으로 실행하는 것. 즉, 한 작업이 끝나기를 기다렸다가 끝나면 다음 작업을 수행한다.각 작업이 완료될 때까지 다음 작업이 실행되지 않는다.비동기(Asynchronous)작업이 종료되지 않아도 다음 작업을 진행할 수 있는 방식비동기적인 코드는 특정 작업을 기다리지 않고 다음 작업을 계속 수행한다. [ Callbacks, ES6 Promise, ES7 Async / Await ]callbacks콜백 함수는 특정 함수에 매개변수로 전달된 함수를 의미한다.콜백 함수는 함수를 전달 받은 함수 내부에서 호출된다.단, 콜백 지옥을 맛볼 수 있다.Promise자바스크립트 비동기 처리에 사용되는 객체이다.new 키워드와 생성자를 사용해서 만들며, 생성자의 매개변수로 실행 함수를 전달한다.new Promise(실행함수) 실행 함수의 1번째 매개변수 resolve는 비동기 작업 성공 값이다.실행 함수의 2번째 매개변수 reject는 작업 실패 값이다.Promise는 다음 중 하나의 대기 상태를 갖는다.대기, 이행, 거부단, 체인 지옥이 시작된다...Async / Await비동기 코드를 마치 동기 코드처럼 작성할 수 있도록 해준다.Promise에서 than 체인 형식으로 호출하는 것보다 가독성이 좋다.await는 async 함수에서만 사용 가능하다.동기식 코드에서 사용하는 try...catch 문을 사용할 수 있다. Section 07. Symbol, Iterator, Generator[ Symbol ]ES6에 새롭게 추가된 원시 타입으로, 유니크한 식별자를 만들기 위해서 사용.단, for...in과 getOwnPropertyNames에서 제외 된다.Symbol 사용 시 기본적으로 Property가 숨겨진다. (찾을 수 있는 방법 有)따라서 for...in과 getOwnPropertyNames에서 symbol로 만든 Property가 안보인다.[ Iterator, Generator ]Iterator대표적인 예시로 배열이 있다.반복 가능한 것을 Iterable하다고 한다. for…of를 이용할 수 있다.[Symbol.iterator()] 값을 가지고 있다.Generator사용자의 요구에 따라 일시적으로 정지할 수 있고, 다시 시작할 수 있는 특별한 기능을 가지고 있다.function다음에 Asterisk (애스터리스크)를 붙인 형태로 사용한다.function*yield 키워드를 이용한다: 제너레이터 함수의 실행을 일시적으로 정지시킴. Section 08. Design Pattern[ 디자인 패턴 ]디자인 패턴은 개발자가 응용 프로그래밍나 시스템을 디자인할 때 일반적인 문제를 히결하는 데 사용할 수 있는 공식화된 모범 사례이다.- 위키 피디아다음과 같은 장점이 있다.최고의 솔루션재사용성풍부한 표현력향상된 의사 소통필요없는 코드 리팩토링코드베이스 크기 감소[ 디자인 패턴 종류 ]Singleton Pattern: 클래스의 인스턴스화를 객체 1개로 제한하는 디자인 패턴Factory Pattern: 비슷한 객체를 반복적으로 쉽게 생성하게 해주는 디자인 패턴Mediator Pattern(중재자 패턴): 객체 그룹에 대한 중앙 권한을 제공해주는 디자인 패턴Observer Pattern: event-driven 시스템을 이용하는 디자인 패턴Module Pattern: 코드를 더 작고 재사용 가능한 조각으로 분할하게 해주는 디자인 패턴 미션완벽히 해결한 미션은 다음과 같습니다.음식 메뉴 앱음식 메뉴 앱 미션은 주어진 카테고리에 해당되는 메뉴를 출력하는 웹을 구현하는 것이었습니다. 저는 카페 메뉴를 주제로 해당 웹을 구현했습니다. 다만 출력할 아이템에 대한 DB가 없어서 직접 data.json을 작성하여 처리했습니다. 미션을 해결하면서 기능 구현에는 특별한 문제가 없었으나 기능 구현보다 데이터 파일 생성이 더 오래 걸린 미션이었습니다... (출력되는 메뉴 이미지는 스타벅스 이미지를 활용했습니다.) 가위 바위 보 앱플레이어와 컴퓨터가 가위 바위 보를 하는 게임을 구현하는 미션입니다. 총 10번의 기회가 주어지며 게임에 대한 스코어 제공 및 승패 결과를 제공해야 했습니다. UI를 어떻게 구현할까 고민하다가 이미지를 추가적으로 더 넣어 구성했습니다. 컴퓨터의 가위 바위 보 선택지는 Math.random()을 이용해 처리했으며, 기능 구현에는 특별한 문제는 없었습니다.회고자바스크립트 강의를 들으며 기초를 다지고 그 지식을 바탕으로 주어진 미션을 해결하는 한 주를 보냈습니다. 특히 웹 개발이 처음이라서 미션을 해결해 나가는 시간이 정말 흥미로웠습니다. 앞으로 워밍업 클럽 Study를 진행하며 제가 얼만큼 발전할 수 있는지 궁금해지기도 합니다. 남은 기간 최선을 다해서 임해보겠습니다!

프론트엔드워밍업클럽

윤대

[인프런 워밍업 0기 Day2] 한 걸음 더! 메서드로 코드 재사용해보기!

!! 해당 글은 독자가 인프런 워밍업 0기를 수강하고 있다는 전제 하에 작성되었습니다 !!과제 수행에 있어 스프링부트 3.2.2 버전을 사용하고 있다는 점을 미리 알려드립니다! 안녕하세요🙌! 인프런 워밍업 2일차 과제입니다!2일차에는 API를 작성하는 방법에 대해서 배우고 그에 대한 과제를 받았습니다😎과제를 자세히 살펴보는 도중, 문제 1번과 3번 모두 덧셈을 수행하는 공통로직이 있다는 사실을 발견했습니다!따라서! 오늘은 제가 한 걸음 더 성장하기 위해 메서드를 통해 공통로직을 처리한 과정을 공유할 예정입니다~ ⚙️메서드 설계하기메서드를 만들 때, 제가 중요하다고 생각하는 것은 3가지입니다!메서드가 무엇을 할 것인가 (메서드 명)메서드가 무엇을 반환할 것인가 (리턴 타입)메서드가 무엇을 필요로 하는가 (파라미터)여기서, 중요한 점은 어떻게는 생각하지 않는다는 것입니다! 🙅‍♂️위의 3가지만 만족한다면 공통로직으로 볼 수 있기 때문에 메소드의 내부 로직은 추후에 생각해도 무방합니다! 자, 그렇다면 지금부터 문제 1번과 3번에서 각 절차를 수행해보겠습니다.첫째, 무엇을 할 것인가?문제 1번과 3번 모두 2개 이상의 숫자를 더해야 하는 상황입니다! 따라서, 저는 메서드 명을 addNumbers로 정했습니다.둘째, 무엇을 반환할 것인가?우리는 두 수를 더 한 값을 반환 받아야 합니다! int와 Inteager 등 다양한 숫자가 가능하겠지만, 저는 Inteager를 택했습니다!셋째, 무엇을 필요로 하는가?우리는 숫자를 더하기 위해, 2개 이상의 숫자를 필요로 합니다! 또한, 문제 3번의 경우 몇 개의 숫자가 입력될 지 알 수가 없습니다! 따라서 저는 List<Integer> numbers로 파라미터 값을 설정했습니다.이제 위의 세 부분을 합치면 제가 만들 메서드는 아래와 같은 형식이 될 것입니다!public Integer addNumbers(List<Integer> numbers);💡 int 타입의 경우 List로 받을 수가 없어 Integer를 반환 값으로 설정하였습니다! ⚙메서드 구현하기이제 설계가 끝났으니, 메서드를 구현할 시간입니다! for each문으로 List에 담긴 값을 하나씩 꺼내 더하여 값을 반환만 해주면 구현은 어렵지 않게 끝이 납니다!😎 public Integer addNumbers(List<Integer> numbers) { Integer result = 0; for (Integer number : numbers) { result += number; } return result; }자~ 이제 구현이 끝났으니 메서드를 적용해봐야겠죠? ⚙메서드 적용하기문제 3번먼저, 문제 3번입니다! 3번부터 풀이하는 이유는 1번 문제를 풀며 발생하는 문제 때문입니다!DTO (데이터를 전송 받고 보내는 객체)@Getter @Setter public class NumbersRequest { private List<Integer> numbers; }Controller @PostMapping("/api/v1/calc") public Integer addNumbers(@RequestBody NumbersRequest numbersRequest) { return addNumbers(numbersRequest.getNumbers()); }List<Integer>를 필드 값으로 갖는 NumberRequest를 입력받아 addNumbers()에 필드값을 전달했습니다!입력받은 숫자가 몇 개던지 메서드 내부에서 값이 잘 합산되어 반환될 것입니다! 문제 1번이번엔, 문제 1번을 해결해보죠! 😎DTO@Getter @Setter public class CalcResponse { private int add; private int minus; private int multiply; }Controller @GetMapping("/api/v1/calc") public CalcResponse calc(Integer num1, Integer num2) { List<Integer> numbers = new ArrayList<>(); numbers.add(num1); numbers.add(num2); CalcResponse response = new CalcResponse(); response.setAdd(addNumbers(numbers)); // minus와 multiply도 메서드로 구현해주었습니다! response.setMinus(minusNumbers(numbers)); response.setMultiply(multiplyNumbers(numbers)); return response; }값을 Integer num1과 Integer num2를 입력받기 때문에 입력 값들을 List로 변환해줘야 합니다.이렇게 하여도 문제는 없겠지만, 코드가 지저분한 게 영 마음에 들지 않습니다!그래서, 다음과 같이 코드를 변경해보았습니다!DTO (Request) 추가@Getter @Setter public class CalcRequest { private Integer num1; private Integer num2; public List<Integer> getNumbers() { List<Integer> numbers = new ArrayList<>(); numbers.add(this.num1); numbers.add(this.num2); return numbers; } }입력 값을 각각의 값이 아닌 하나의 객체로 변환하여 받고, 내부에 getNumbers()를 구현하여 입력받은 값을 바로 List<Integer>로 변환하여 받도록 하였습니다!이 DTO를 적용하면 Controller의 코드가 다음과 같이 바뀌게 됩니다! @GetMapping("/api/v1/calc") public CalcResponse calc(@ModelAttribute CalcRequest calcRequest) { CalcResponse response = new CalcResponse(); response.setAdd(addNumbers(calcRequest.getNumbers())); response.setMinus(minusNumbers(calcRequest.getNumbers())); response.setMultiply(multiplyNumbers(calcRequest.getNumbers())); return response; }훨씬 간결해졌죠? 참고로 @ModelAttribute는 쿼리 스트링으로 입력받은 값들을 확인하여 파라미터 타입으로 지정한 타입의 필드 값과 일치하면 자동으로 객체로 변환해주는 Annotation입니다!이렇게, 메서드를 사용하면 공통로직을 단일 메서드 내부에서 관리할 수 있게 되고, 코드의 반복을 줄일 수 있게 됩니다! 여러분도 코드의 중복이 보인다면 메서드로 한 번 만들어보는 것은 어떨까요? 지금 까지의 내용을 정리하면서 2일차 포스팅을 마치도록 하겠습니다! 💡정리 💡메서드를 설계할 때 중요한 것!메서드를 설계할 때는 어떻게는 생각하지 말자!무엇을 하고, 반환하고, 필요한 지 3가지만 충족된다면 공통로직으로 만들 수 있다!객체 내부에서 변환로직 만들기!여러 개의 Integer를 Controller에서 List로 직접 변환할 시 코드가 지저분해진다! @ModelAttribute를 활용하여 객체 내부에서 값을 변환하여 반환하자!

백엔드Spring인프런워밍업메서드API인프런SpringBoot

[0일차] 인프런 워밍업 클럽 0기 백엔드 - 시작!!

어? 강의가 되게 재밌어 보이네?오랜 기간 취업 준비를 하는 도중 프로젝트 없이 공부만 하다보니 개발이 재미 없어지기 시작하였습니다.물론 오랜 취업 준비 기간으로 힘든 것도 있지만 내가 직접 만드는 서비스가 없다보니 흥미가 떨어진 것 같다고 생각하였습니다. 그렇게 어떤 프로젝트를 할까, 개인으로 할까? 팀으로 할까? 많은 고민을 하던 중 '자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지!' 라는 강의를 알게 되었습니다.강의 목차를 보니 웹 개발에 필요한 기술부터 네트워크, AWS 등 정말 백엔드로서 필요한 지식을 한 강의에서 제공해주는 것에 매력을 느꼈습니다. 하지만 돈 없는 취준생은 듣고 싶지만 부담이 되었습니다.😭 이건 꼭 해야해..!그렇게 며칠을 고민하던 중 지식 공유자께서 직접 참여를 하고, 다양한 사람들과 의견을 주고 받을 수 있는 스터디가 열린다는 것을 알게 되었습니다. 거기에 대상 강의가 바로..! 제가 구매할지 말아야할지 고민하던 '자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지!' 였습니다.저는 개발 실력도 중요하지만 네트워킹에 참여하는 것도 중요하다고 생각하였는데 이 기회는 두마리의 토끼를 동시에 잡을 수 있는 매력적인 이벤트였습니다. 강의를 사야할지 말아야할지 고민하던 저는 주저 없이 스터디를 신청하게 되었고, 강의도 바로 구매하였습니다.😁 그렇게 시작한 OT(0일차)16일 금요일 스터디의 시작을 알리는 OT를 하였습니다. 지식공유자이신 최태현님께서 대략적인 스터디 일정을 알려주셨고, 자바의 역사에 대해 알려주셨습니다. 스터디 일정을 보니 정말 타이트하고 힘들 것 같지만 저는 시간 많은 취준생 아니..백수이기 때문에 열심히 스터디 일정에 맞춰서 달려보겠습니다. 이번 기회로 참여하신 다른 러너분들과 많은 이야기와 정보를 주고 받고 싶습니다. 오랜 취업준비를 하였지만 그런 것 치고는 큰 실력향상이 있진 않았던 것 같아 이번 기회에 제대로 마음 잡고 공부해보려고 합니다..! 화이팅..! (급 종료)

백엔드백엔드인프런워밍업클럽스터디0기

양성빈

[인프런 워밍업 스터디 클럽] 출사표 및 OT 후기

출사표 및 OT 후기 출사표현재 월드 스타인 손흥민 선수의 아버지인 손웅정 선생님께서 하신 말씀으로 글을 시작하려고 합니다.손웅정 선생님께서는 아이들을 가르치실 때 이런 말씀을 하셨습니다. "세계의 벽 절대 안 높아! 할수 있어! 남자는 자신감! 일단 붙어봐야될거아니야! 저질러보고! 깨지고! 박고! 가슴만 뛰는게 축구선수가 아니라 가슴이랑 내가 같이뛰어야돼!!!" 나는 현재 작은 중소기업의 웹 프론트엔드 업무를 맡고 있는 개발자입니다. 하지만 저의 원래 꿈은 웹 백엔드 개발자였습니다. 대학교 교수님 추천으로 이 회사에 입사하게 되었고 백엔드 업무를 맡기신다고 말씀하여 입사확정을 받았지만 정작 백엔드 팀은 존재하지 않았고 프론트 업무를 맡게 되었습니다. 이에 백엔드로 이직을 염두해두며 업무시간에는 프론트 기술들을 프로젝트에 적용하며 프론트 기술 적응을 해두었으며 집에 와서는 백엔드 공부를 계속 하였습니다. 그게 어느덧 3년이라는 시간이 흐르고 점점 제 자신에게 지칠 무렵, 인프런 배너에서 엄청난 것을 보게 되었습니다! 😲 워밍업 클럽이라는 말에 신청을 바로 하려 하였지만 상세소개 글에 걸리는 부분이 있었습니다. 🧐 '부트 캠프 참가자'라는 말에 이것을 신청해도 되는지 엄청 고민을 하였습니다. 그런던 어느날 '개발바닥' 유튜브 라방 을 보게 되었다. 마침 향로님이 계셔서 조심스레 여쭤보았고 직장인도 신청해도 된다는 말씀을 남겨주셨습니다! 나는 환호성을 외쳤고 바로 신청을 하게 되었습니다. 😆 서두에 손웅정 선생님 말씀처럼 조금 나태해진 저를 '인프런 워밍업 클럽 스터디'를 통하여 한번 저질러 보고 미션이나 강의를 들으면서 한번 깨저도 보고 가슴만 뛰는게 아닌 가슴과 제 학습곡선이 같이 뛰었으면 하는 바램으로 열심히 해보겠습니다! OT 후기전 날 '인프런 워밍업 스터디 클럽'의 커뮤니티를 가입하게 되었고 당일 온라인 라이브로 OT를 진행하게 되었습니다. 간단한 일정과 방법을 코치님이 말씀을 주셨고 간단한 자바의 역사에 대해 알려주시고 간단한 질문을 받은 뒤, 라이브는 종료되었습니다. 여기서 나는 일정이 빡빡하다는 것을 알고 평일에는 직장에 소모되다 보니 과제를 미리미리 해보자는 마음을 갖게 되었고 오늘 OT 들은 자바부터 상세히 파보기 시작했습니다. 그러면서 여러 자료를 찾다가 다른 러너분들과 공유되고 싶다는 글을 찾게 되었습니다. 이렇게 첫 시작으로 지금 마음가짐으로 끝까지 완주해서 우수러너까지 노려봤으면 좋겠습니다. 다들 응원 부탁드립니다. 🥳 📚 참고자료오라클 블로그 

백엔드인프런워밍업스터디클럽출사표발자국

일코

[pyhwpx] 한/글 자동화 문의 및 요청의 90%는 ㅇㅇ 관련이었어요.

안녕하세요? 일코입니다. 유튜브와 블로그를 운영하면서 받았던 문의 중 90%는바로 "표"에 관한 내용들이었습니다.표는 비단 행정뿐만 아니라,한/글을 통해 작성되는 문서에서 가장 많이 쓰이는 컨트롤이죠.그런데 기존의 한/글 오토메이션 API를 통해 표를 읽어오거나, 표를 작성하는 메서드는다소 큰 진입장벽이 있었습니다. 예를 들어 비교적 최근 만든 pyhwpx에서5행5열의 표를 만들고 "글자처럼 취급"을 적용하는 코드는아래 세 줄이면 되지만,from pyhwpx import Hwp hwp = Hwp() hwp.create_table(5, 5, treat_as_char=True) 그런데,기존 오토메이션API를 통해 동일한 표를 직접 생성하려면무려 아래의 코드를 작성해야 합니다.(한/글 오토메이션API에 관심있으신 분은, 차근차근 읽어보셔도 좋습니다.)import win32com.client as win32 hwp = win32.gencache.EnsureDispatch("hwpframe.hwpobject") hwp.XHwpWindows.Item(0).Visible = True hwp.RegisterModule("FilePathCheckDLL", "FilePathCheckerModule") rows = 5 # 행 갯수 cols = 5 # 열 갯수 width_type = 0 # 너비: 단에 맞춤 height_type = 0 # 높이: 자동 height = 0 # height 기본값 초기화 treat_as_char = True # 글자처럼 취급 pset = hwp.HParameterSet.HTableCreation hwp.HAction.GetDefault("TableCreate", pset.HSet) # 표 생성 시작 pset.Rows = rows pset.Cols = cols pset.WidthType = width_type # 너비 지정(0:단에맞춤, 1:문단에맞춤, 2:임의값) pset.HeightType = height_type # 높이 지정(0:자동, 1:임의값) sec_def = hwp.HParameterSet.HSecDef hwp.HAction.GetDefault("PageSetup", sec_def.HSet) total_width = (sec_def.PageDef.PaperWidth - sec_def.PageDef.LeftMargin - sec_def.PageDef.RightMargin - sec_def.PageDef.GutterLen - hwp.MiliToHwpUnit(2)) pset.WidthValue = hwp.MiliToHwpUnit(total_width) # 표 너비 if height and height_type == 1: # 표높이가 정의되어 있으면 total_height = (sec_def.PageDef.PaperHeight - sec_def.PageDef.TopMargin - sec_def.PageDef.BottomMargin - sec_def.PageDef.HeaderLen - sec_def.PageDef.FooterLen - hwp.MiliToHwpUnit(2)) pset.HeightValue = hwp.MiliToHwpUnit(height) # 표 높이 pset.CreateItemArray("RowHeight", rows) # 행 m개 생성 each_row_height = total_height - hwp.MiliToHwpUnit(rows) for i in range(rows): pset.RowHeight.SetItem(i, each_row_height) # 1열 pset.TableProperties.Height = total_height pset.CreateItemArray("ColWidth", cols) # 열 n개 생성 each_col_width = total_width - hwp.MiliToHwpUnit(3.6 * cols) for i in range(cols): pset.ColWidth.SetItem(i, each_col_width) # 1열 # pset.TableProperties.TreatAsChar = treat_as_char # 글자처럼 취급 pset.TableProperties.Width = total_width # hwp.MiliToHwpUnit(148) # 표 너비 적용 hwp.HAction.Execute("TableCreate", pset.HSet) # 코드 실행 # 글자처럼 취급 여부 적용(treat_as_char) ctrl = hwp.CurSelectedCtrl or hwp.ParentCtrl pset = hwp.CreateSet("Table") pset.SetItem("TreatAsChar", treat_as_char) ctrl.Properties = pset결과는 위와 동일합니다 고작 표 하나를 만들기 위해질릴만큼 길고 복잡한 저 API코드를 작성해야 한다는 사실을 알게 되면(특히 회사원들) 어느 누가 한/글 자동화를 시도할까 싶었습니다.전부 언급할 수도 없는 상당한 분량의 백그라운드 지식을 알려주는 것도 큰 부담이었습니다.아래아한글 API의 구조와 실행방법, 파라미터셋 사용법,파라미터셋의 서브아이템셋을 정의해야 하는 경우와 정의방법,암호같은 API 문서를 읽는 방법..ㅜ 작년말에 금융감독원 연수원에서 아래아한글 자동화 강의요청을 해 주셨거든요.시간은 짧고, 할 말은 많고, 저런 무지막지한 API 코드 보여주면 재미가 없을 것 같아서현업에 쓰일 법한 함수들을 묶어서 시연 위주로 보여드렸는데,그게 계기가 되어,아래아한글 보고서작성 자동화를 위한pyhwpx라는 파이썬모듈을 개발하게 되었습니다.하여튼 이제는 나름의 버전업을 거듭해서0.8 근처까지 왔는데요. 아직 갈 길이 멉니다.그럼에도 create_table, table_to_df나,table_from_data, get_into_nth_table, 특히 put_field_text는한/글 보고서 취합 및 작성 자동화나 엑셀연동에 진심인 분들에게는정말 파워풀한 메서드일 거라고 자부합니다.개발기간도 아직 짧은 편이고, 다소 조악해 보일 수 있습니다.하지만, 정말 수백 건의 문의와 의뢰를 받고 일일이 회신을 드렸던 모든 경험을pyhwpx 모듈에 하나씩 녹여내고 있습니다.pyhwpx 사용법도 알려주세요! - 인프런현재는 블로그와 인프런 QnA에 pyhwpx 짧은 메서드 사용법 위주로 포스팅하고 있지만,오는 2월부터는 작은 프로젝트나, 의뢰 / 문의내용을 토대로pyhwpx의 활용방법도 차근차근 영상과 포스팅으로 풀어볼 예정입니다.업무자동화에 관심이 있는 파이썬 초보 분들, 특히 직장인 분들많은 관심 부탁드립니다^^ 감사합니다.행복한 하루 되세요!

업무 자동화pyhwpx아래아한글hwppython일상의코딩일코파이썬create_table한컴오피스업무자동화

최경희

[인프런 워밍업 클럽 0기] FE 1주차

1주차 학습 요약1일차 - 자바스크립트 기초, Window 객체 및 DOM, Event자바스크립트 기초 강의에서는 Console 객체, 변수 선언 방법과 참조 범위, 호이스팅에 대한 개념, 타입 종류와 타입을 변환하는 방법, 연산, Template Literals, 반복문을 배웠으며, Window 객체 및 DOM 강의에서는 Window 객체에 대한 개념과 사용법, DOM에 대한 개념과 Document 객체를 사용한 DOM 조작법을 배웠습니다. 또한 Event 강의를 통해 여러가지 이벤트 상황에 대해 동작을 설정할 수 있는 방법과 버블링, 캡처링, 위임에 대한 개념을 배울 수 있었습니다. 2일차 - 자바스크립트 중급(1)this 키워드에 대한 개념과 사용법, bind/call/apply 메서드를 사용한 this의 참조값 변경, 삼항 연산자 사용법, 간단한 동기/비동기 개념과 이벤트 동작에 대한 내부 작동 과정, Closure에 대한 개념과 사용법, 배열/객체에 대한 구조 분해 할당 방법, 전개 연산자 사용법, 배열에 대한 Map/Filter/Reduce 메서드 사용법을 배웠습니다. 3일차 - 자바스크립트 중급(2)1일차에 배웠던 undefined와 null의 공통점과 차이점, 얕은 비교/복사 와 깊은 비교/복사에 대한 개념과 사용법, 함수 표현식과 선언문의 개념과 차이점, 즉시 실행 함수(IIFF)의 개념과 사용법, Intersection Observer 사용법, pure/impure 함수의 개념, 커링 개념과 사용법, 엄격 모드에 대한 개념과 적용법, 특징을 배웠습니다. 4일차 - OOP, 비동기OOP 강의에서는 객체 지향 프로그래밍에 대한 개념과 특징, 프로토타입에 대한 개념과 사용법, 클래스에 대한 개념과 사용법을 배웠으며, 비동기 강의에서는 callbacks, promise, async/await에 대한 개념과 사용법을 배웠습니다. 5일차 - Iterator / Generator, Design PatternIterator / Generator 강의에서는 1일차 타입 설명 때 간단하게 지나갔던 심볼에 대한 개념과 사용법, Iterator와 generator에 대한 개념과 사용법을 배웠으며, Design Pattern 강의에서는 디자인 패턴에 대한 개념과 장점 그리고 가장 자주 사용되는 패턴인 Singleton 패턴과 Factory 패턴, Mediator 패턴, Observer 패턴, Module 패턴에 대한 개념을 배웠습니다. 미션 해결 과정미션 1 : 음식 메뉴 앱각 메뉴를 클릭할 때, 메뉴에 맞는 음식 리스트를 보여준다.음식 메뉴가 아닌 투썸 플레이스 카페 메뉴로 변경했고, 메뉴 부분의 스타일과 전체적인 색상을 변경했습니다.이미지는 투썸 플레이스 카페의 홈페이지에서 이미지 주소 복사를 통해 가져왔습니다. 스타일을 수정할 때 가장 시간이 오래 걸렸으며, 현재 화면 크기에 따라 음식 아이템들의 내용이 잘리는 문제가 있어 검색을 통해 수정하고 있는 중 입니다. 미션 2 : 가위 바위 보 앱컴퓨터와 진행하는 가위 바위 보 게임입니다.전체적인 색상만 변경하고, 전체적인 스타일은 과제 예시와 비슷하게 만들었습니다.스타일을 구현하는 것이 가장 고민이 됐고 시간도 가장 오래걸렸습니다. 현재 전체적인 스타일이 과제 예시와 비슷하게 만들어져 있지만 이미지를 넣어 조금 더 보기 좋게 바꾸려고 생각 중입니다. 회고스터디를 시작하기 전에는 그래도 어느 정도 자바스크립트에 대해 알고 있다고 생각했었는데, 한 주 동안 자바스크립트에 대한 강의를 들으면서 제가 알고 있었던건 기초뿐이었다는 사실을 알게 되었습니다. 그래서 자바스크립트 강의를 들으면서 처음에는 조금 이해하기 힘든 부분도 있었고, 개념은 이해했지만 이 개념을 코드에 어떻게 적용하면 좋을지 언제 사용하면 되는지는 알기 힘들었습니다. 검색을 통해 더 알아보면서 완벽하게는 아니지만 어느 정도는 이해하고 사용할 수 있게 되었습니다. 하지만 과제는 웹에 대한 공부를 올해부터 시작했던 만큼 HTML과 CSS에 대해서도 부족한 점이 많아 이번 주에 있던 모든 과제를 완성하지 못한 것이 아쉬웠습니다. 그래도 점점 속도가 붙고 있어 과제 제출 전까지 모든 과제를 완성하는 것이 목표입니다. 역시 직접 만들어 보는 것이 배운 것을 확실하게 익힐 수 있는 시간이 된 것 같습니다.

프론트엔드

학생

[1주차 발자국] 인프런 워밍업 클럽 스터디 0기 FE

발자국이번주 강의 범위는 따라하며 배우는 자바스크립트 A-Z(섹션 1~8) 까지였다.워낙에 기록을 안하는 성격이라 어떤 식으로 해야 할지 감이 안잡히지만 이제부터라도 습관을 들이는 편이 좋겠다.강의 요약이번주 강의에는 정말 많은 내용이 담겨 있었지만,이전에 혼자 책으로 공부할 때 한 번 보고 이해가 되지 않았던 부분,중요하다고 생각하는 부분,혹은 아예 처음 듣는 내용을 위주로 요약해보았다.그 목록은 아래와 같다.호이스팅HTML DOM this자바스크립트 비동기 처리 과정IIFECurryingOOP비동기Iterator & Generator자바스크립트 디자인 패턴요약을 한다고 하긴 했는데 요약이 아니라 공부노트같긴 하다. 다음 회고때는 중요한 내용 두 개 정도로 줄일까 한다.호이스팅(hoisting)변수 선언이 스코프 내의 가장 위로 끌어올려지는 것.var, let, const 모두 호이스팅된다. 그런데 방식이 다르다.var의 호이스팅console.log(a) var a = 1출력 결과: undefinedwhy? var의 경우 선언(a = undefined) -> 할당(a = 1) 됨.let, const의 호이스팅console.log(a) let a = 1결과: 에러 뜸.why? let, const의 경우 선언 -> TDZ -> 할당(a = 1) 됨.따라서 변수 선언이 스코프의 맨 위로 올라간다 해도 출력 불가. TDZ에 걸림. HTML DOMDOM(Document Object Model): 웹 페이지를 이루는 요소들을 tree구조로 만든 객체 모델.웹 페이지 빌드 과정(CRP: Critical Rendering Path)html을 DOM으로, CSS를 CSSOM으로 만든다.DOM과 CSSOM을 결합(Render Tree 생성)Layout(페이지에 표시되는 각 요소의 크기 및 위치 계산)Paint(실제 화면에 그리기)Render Tree 생성, Layout, Paint단계는 비용이 많이 든다.이 비용을 줄이기 위해 React에서는 가상 DOM을 사용해 성능을 높임. DOM Event event의 3단계 흐름: Capturing -> Target -> Bubblingevent Capturing: 이벤트가 위에서 아래로 전달되는 것event Bubbling: 가장 깊게 중첩된 요소에 이벤트가 발생했을 때 이벤트가 위로 전달되는 것event.stopPropagation(): 중첩 중지.Event Delegation(이벤트 위임)하위 요소의 이벤트를 상위 요소에 위임하는 것.하위 요소의 이벤트를 상위에서 제어. this메소드에서 this는 해당 객체를 참조.함수에서 this는 window 객체를 참조.constructor에서 this는 빈 객체를 참조.여기서 함수와 메소드의 차이가 헷갈려 알아보니, 다음과 같았다.메소드는 함수의 일종이며, 객체의 속성이 함수인 것을 메소드라고 한다.참고: https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Functionslexical this: 화살표함수(ES6)에서 this는 항상 상위스코프의 this를 가리키게 된다.bins, call, apply함수에서 this를 쓸 때 window객체 말고 다른 객체를 참조하도록 하는 방법들call: function.call(객체, 인수1, 인수2, ...)apply: function.apply(객체, [인수1, 인수2, ...])bind: const bindFunc = function.bind(객체); bindFunc(인수) // 이 때 호출. 자바스크립트의 비동기 처리 과정event loop자바스크립트는 동기 언어. 따라서 비동기를 사용하려면 브라우저 등의 도움을 받아야 한다.브라우저 내부: 자바스크립트 엔진 + Web APIs + Callback Queue + Event Loop자바스크립트 엔진: 메모리 힙 + Call Stack메모리 힙: 변수 저장 창고Call Stack: 함수 호출 시 함수가 쌓이는 곳setTimeout과 같은 비동기 기능을 처리하는 과정 (브라우저가 처리해줌)Call Stack에 setTimeout 쌓임Web APIs에게로 setTimeout이 이동 후 setTimeout의 지정한 시간만큼 대기Callback Queue에 함수가 보내지고 대기Event Loop는 Call Stack이 비면 Callback Queue에서 먼저 온 순서대로 Call Stack에 넣어줌  IIFE(즉시 호출 함수 표현식)정의가 되자마자 즉시 실행되는 자바스크립트 함수. 주된 목적: 변수 전역 선언 회피, 다른 변수의 내부 접근 막기( // 소괄호 1: 전역선언 막고 변수접근 막기 function() {} )() // 소괄호 2: 즉시 실행 함수 생성함수 이름이 없으려면이 함수를 할당받을 변수를 지정해야 함이 함수를 즉시호출해야함(IIFE) Currying커링: f(a,b,c) => f(a)(b)(c)로 변환하는 기술. 다른 언어에도 존재함.// 매개변수가 몇개든 함수를 currying해주는 함수 function curry(func) { return function curried(...args) { if(args.length >= func.length) { return func.apply(this, args); } else { return function (...args2) { return curried.apply(this, args.concat(args2)); } } } } OOP(객체 지향 프로그래밍)특징자료 추상화(Abstraction): 필요한 정보만 표현 -> 간결상속(Inheritance): 새 클래스가 기존 클래스의 기능 가져와 사용 -> 효율다형성(Polymorphism): Overriding(재정의) 사용. 인스턴스에 따라 같은 동작에 다른 결과물.코드 재사용 가능한 이점캡슐화(Encapsulation): 클래스 안에 묶어 보호 및 쉬운 관리상속부모 클래스를 자식 클래스에서 확장.extends 키워드 사용.super(): 자식 클래스에서 부모 클래스의 생성자나 메소드 호출할 때 사용. 비동기병렬로 작업. 순서를 기다리지 않음.비동기 요청이 여러개이고 한 요청이 다른 요청의 결과에 의존할때callback함수(es5): 특정 함수에 매개변수로 전달된 함수.promise(es6): 비동기처리 결과 성공, 실패 각각 resolve, reject. .then()으로 체이닝Promise.then(resolve값).catch(reject값).finally(성공실패무관)async/await(es7): 비동기식 코드를 동기식처럼 보이게 작성.async로 함수를 감싸고 그 안에서 await를 이용해 각 요청을 대기시킴동기식 코드에서 쓰는 try catch구문을 사용 가능 Iterator & Generatoriteratornext()를 호출해서 value, done 두 개의 속성을 가지는 객체를 반환하는 객체.[Symbol.iterator]()를 이용하면 반복가능한값을 반복기로 생성 가능.generatorGenerator Function은 사용자의 요구에 따라 다른 시간 간격으로 여러 값을 반환 가능.yield: 제너레이터함수의 실행을 일시적으로 정지시킴.function* 함수이름()제너레이터: 제너레이터 함수의 반환.제너레이터.next()로 사용.generator도 value, done 속성이 담긴 객체를 반환.자바스크립트 디자인 패턴디자인 패턴: 공식화된 프로그램/시스템 디자인 문제해결 모범 사례.장점최고의 솔루션: 검증됨.재사용성: 여러문제에 적용가능.풍부한 표현력향상된 의사 소통필요없는 코드 리팩토링: 검증됨코드베이스 크기 감소: 공간 보존Singleton Pattern:인스턴스를 하나의 객체로 제한.Factory Pattern: 특수함수인 팩토리함수를 사용해 비슷한 객체 많이 만들수있음.=> 비슷한 객체를 반복적으로 생성하야 하는 경우 사용. Mediator Pattern(중재자 패턴): 객체 그룹에 대한 중앙 권한 제공.Observer Pattern: observer를 이용해 객체를 관찰.Module Pattern: 더 작은 것으로 분할.export를 이용. 미션 해결 과정(과제 총합본은 따로 작성하고 있다.) https://www.inflearn.com/blogs/6758미션1 음식 메뉴 앱포인트: 각 카테고리의 버튼을 누를 때마다 메뉴들을 보여주는 공간을 비우고 해당 카테고리의 메뉴를 채움스타일: 우선은 아는 것이 별로 없고 주어진 것부터 제대로 공부하고 싶어서 과제 예시와 최대한 비슷하게 만들었다. 추가: 미디어 쿼리를 이용해 개발자 도구가 켜진 상태의 화면크기부터는 화면에 메뉴를 1열로 표시시간 많이 든 부분: CSS, 이미지 처리(선정 및 다운로드, 크기조정) 아직 HTML/CSS 공부가 많이 필요하다는 것을 깨달았다.  사진은 pixabay의 무료 이미지를 이용했으나, 다른 분의 글을 보고 음식 메뉴에 API를 이용하는 좋은 방법이 있다는 것을 깨달았다. 그런 훌륭한 문물이 있다는 것을 이제야 상기하다니! 직접 음식 메뉴를 선정하고 설명을 지어내는 것에 신경을 쓰느라 음식 API를 이용한다는 생각을 하지 못했다. 앞으로 다루어야 할 이미지가 많아질 경우 API를 사용하자.  미션2-가위바위보 앱포인트: Math.random을 이용해 컴퓨터의 가위바위보를 구현. 판마다 게임보드에 스코어 표시스타일: background에 그라데이션 효과를 적용함. 승부 결과마다 다른 색의 글귀를 표시 시간 많이 든 부분:만들 때 헷갈렸던 부분은 재도전 버튼을 눌렀을 때 기존에 써있던 플레이어와 컴퓨터의 점수를 삭제하고 새로 표시하는 부분이었다. 결국 재도전 버튼에 붙인 retry함수에서, gameDisplay에서 생성한 class를 querySelector로 찾아가 내용물을 비우고 classList에서 class 이름을 삭제하고 각 점수를 scoreboard에서 removeChild로 지웠다. 이는 gameDisplay가 실행되면 다시 생성된다. 미션3-퀴즈 앱포인트: 버튼을 누를 때마다 next버튼 생성 및 옵션 버튼 비활성화스타일: CSS의 grid 이용해 옵션 버튼 배치. position시간 많이 든 부분: CSS. 미션4-책 리스트 나열 앱포인트: 입력 후 제출 버튼을 누르면 input value를 미리 생성해둔 ul 내부에 li형태로 추가. 경고문은 setTimeout을 이용해 3초 뒤 사라지게 함스타일: 책이 추가/삭제되거나, 입력란을 빈칸으로 두고 제출했을 때의 경고문에 따라 색 다르게 표시얘는 다음주에 CSS를 공부해서 정렬을 보기 좋게 만들어줄 것이다.(거의 기능만 구현된 못생긴 화면)조만간 수정할 부분CSS 추가알림때문에 화면이 아래로 밀리는 문제 고치기 미션5-GithubFinder 앱포인트: Github API를 사용해 데이터 가져오기스타일: 우선 단순하게(아직 미해결. API 사용법 공부 후에 완성 예정)이친구는 fetch와 RESTful API 사용하는 법을 공부해야 하기 때문에 아직 해결하지 못했다. 다음 회고 때 해결 과정이 들어갈 것이다.미션6-비밀번호 생성 앱포인트: 선택된 체크박스의 값을 넣어 length란에 입력한 숫자값을 길이로 하는 문자열을 랜덤생성해야 한다.5~70의 숫자 조건에 length가 해당하지 않으면 alert를 실행시켰다.아무런 체크박스를 선택하지 않았을 경우도 alert를 실행시켰다.생성한 비밀번호를 복사하는 copy 버튼을 눌렀을 때 클립보드에 복사하는 코드navigator.clipboard .writeText(password.value) .then(() => { alert("successfully copied"); }) .catch(() => { alert("something went wrong"); });원래는 execCommand를 썼는데 deprecated되었다고 한다.then부분은, alert를 넣을 경우 then을 쓰지 않으면 복사가 되지 않아서 stack overflow사이트에서 가져온 코드이다. https://stackoverflow.com/questions/69438702/why-does-navigator-clipboard-writetext-not-copy-text-to-clipboard-if-it-is-pro문자열, 숫자, 특수문자를 랜덤으로 섞어 조합하는 것은 까다로웠지만 Math.random과 배열을 이용해 구현했다.체크박스 중 체크된 것의 value를, 생성 버튼을 클릭할때마다 배열에 넣고, 체크가 안되어있으면 배열에서 삭제하는 코드는 거의 줄글처럼 조건이 많이 작성되었다. 굉장한 하드코딩이 되었다...회고작년 말에 진로를 FE개발쪽으로 정하고 독학을 시작하였다.FE부트캠프는 참여한 적이 없었고, 따라서 미션 하나하나가 도장깨기를 하는 느낌이었다.예상대로 매일 미션 한개씩 하는데에는 시간이 꽤 걸렸다. 그리고 부족한 점을 많이 깨달았다.CSS, API 이용, 클린코드 작성 등 여러 방면에서의 지식 및 구현 경험 부족 미션을 할 때 일단 돌아가게 만들고 머리로 정리는 하지 않는 느낌  뒤로 갈수록 시간 부족 다음주 목표CSS 공부하기REST API 공부하고 미션 마저 해결하기다음주 회고 강의 요약 부분 분량 줄이기

프론트엔드인프런워밍업클럽스터디

양성빈

[인프런 워밍업 스터디 클럽] 0기 첫번째 발자국 (1 week)

발자국어느덧 인프런 워밍업 스터디 클럽을 시작한지도 1주째가 다 되간다. 부트캠프 수료자 대상으로 하는 인프런 워밍업 스터디 클럽을 이직 및 직무전환을 고려하는 저에게 기회를 주셔서 정말 최선을 다해 열심히 해서 완주는 물론이고 우수러너에도 들어볼 생각이다. 그럼 본격적으로 발자국을 작성하고 1주간 회고를 시작해보겠다.강의 요약Day0인프런 워밍업 스터디 클럽을 본격적으로 시작하기 전에 OT를 온라인으로 참석하게 되었다. 살짝 기대 반 불안감 반으로 시작을 하였다. OT에는 코치님이 인프런 워밍업 스터디 클럽에 대한 전반적인 일정과 미션 제출 방법, 완주러너, 우수러너 선정방법등 전반적인 일정과 규칙에 대해 설명해주셨다. 또한 남은 시간 동안 코치님은 자바의 역사에 대해서 설명해주셨다. OT의 짧은 특강 (feat. 자바의 역사)자바 도대체 어떤 역사를 갖고 있는가?자바가 처음 발표되고 많은 사람들에게 관심을 받았지만 자바7이 되던 시점, 자바의 암흑기가 잠깐 찾아오게 되었다. 그 이유는 그 당시에 nodejs라던지, python이라던지 이런 언어들이 관심을 받게 되었고, 자바는 너무 오래된 언어, 뭔가 부족하다라는 인식이 생기게 되었습니다.그리고 2014년쯤 자바8이 발표가 되었다. 자바8은 대격변의 버전이였다. 아주 중요한 역할을 했으며 자바의 안 좋은 인식들을 걷어내게 된 계기가 된 버전이였다. 간략하게 보면, 자바8에는 람다, @FunctionalInterface등이 등장하며 함수형 프로그래밍이 가능하게 되었다. 또한 Stream이 등장하며 Stream 연산이 가능하게 되었다. 그리고 우리가 요즘 많이 쓰는 Optional도 자바8에 등장하였다. 이 외에도 날짜, 시간을 다루는 방법이 확장되게 되었다. 자바의 이후 역사이후 자바는 현재 자바21까지 나왔으며 현재 LTS 버전은 21이다. 그리고 JDK17이후부터 LTS 버전 주기가 2년으로 변경되었다. 아래의 오라클 블로그를 참고하기 바란다. 📚 오라클 블로그https://blogs.oracle.com/java/post/moving-the-jdk-to-a-two-year-lts-cadence 이렇게 OT 라이브 세션이 등장하였고 직장인인 저에게 이런 기회를 주셔서 정말 감사했으며 이에 부응하게 더욱 열심히 해서 완주러너를 넘어서 우수러너가 되기를 목표로 삼으며, 이 스터디 클럽이 끝날 때 성장이라는 것을 했으면 하는 마음이다. 아래는 내가 미리 작성해둔 출사표에 관련한 블로그이므로, 참고바랍니다. 📚 나의 출사표https://inf.run/jR96P Section01일차가 시작되기 전에 Section0 강의를 미리 수강해두기로 하였다. 그에 대한 내용을 요약해보겠다. Java, IntelliJ, Postman, MySQL, git 설치처음에는 Java, IntelliJ, Postman, MySQL, git을 운영체제별로 설치하는 방법을 알려주셨다. 나는 이전에 설치가 되었기에 이 부분은 쉽게 수강을 할 수 있었다. 스프링 프로젝트를 시작하는 첫 번째 방법강좌에 강의자료를 다운로드 받으면 PPT와 소스코드 압축파일이 있다. 압축파일을 해제 후에 인텔리제이의 open을 눌러서 해당 소스코드를 열었다. 초기에는 많은 것들을 다운로드 하기 때문에 조금 기다려줘야 한다. 다운로드가 완료되면 LibraryAppApplication 클래스를 찾는다. (src/main/java/패키지명/LibraryAppApplication.java) 이후에 이 클래스를 실행시킨다.이 부분 또한 이전에 학습을 개인적으로 했던 부분이라 쉽게 수강을 하였다. Java를 공부하기 전에 알아두면 좋을 것들! #1 (JVM, JDK - 유튜브)자바라는 언어를 어떻게 컴퓨터가 알아먹을까?컴퓨터는 0과 1밖에 모르는 바보다. 그래서 코드를 알아먹지 못한다. 이를 위해 코드를 컴파일이라는 과정을 거쳐서 컴퓨터가 알아먹을 수 있는 바이트코드(0과 1로 된 코드)로 번역해줘야 한다.컴파일: 인간이 이해하기 쉬운 언어를 기계어(바이트 코드)로 바꾸는 과정컴파일러: 컴파일하는 프로그램바이트코드: 0과 1로 이루어진 코드, 컴퓨터가 이해할 수 있는 코드0과 1은 운영체제마다 다르다. C언어 같은 경우 각각 운영체제별 다른 컴파일러가 필요하다. 하지만 자바는 특별하다. 자바는 하나의 컴파일러로 똑같은 바이트코드를 만든다. 그 이후, 운영체제 별 JVM에게 전달하고 이 JVM이 또 번역해서 각 운영체제에게 전달해준다. 원래는 운영체제마다 다른 '컴파일러'가 필요하지만 자바는 JVM이 0과 1을 운영체제에 맞게 번역을 해준다. 이 JVM은 인기가 상당해서 자바외에도 다른 언어들에도 사용된다.(ex. kotlin, groovy...) JVM자바 가상머신운영체제별 존재바이너리 코드를 읽고 검증 및 실행JRE자바 실행환경JRE = JVM + 자바 프로그램 실행에 필요한 라이브러리JVM의 실행환경 구현JDK자바 개발도구JDK = JRE + 개발도구컴파일러, 디버그등이 포함JDK를 설치하는 행위는 JDK만 설치되는 것이 아니라 그 안에 포함한 JRE + JVM이 같이 설치되는 것이다.LTS 버전LTS버전이란 오래써도 되는 버전을 말한다.JDK 종류Oracle: 개인은 무료, 기업은 유료open JDK: oracle JDK와 비슷한 성능, 언제나 무료Java를 공부하기 전에 알아두면 좋을 것들! #2 (빌드, 빌드툴 - 유튜브)빌드소스코드 파일을 컴퓨터에서 실행할 수 있는 독립 소프트웨어 가공물(Artifact)로 변환시키는 과정즉, 소스코드 파일을 Artifact로 만드는 과정1-1. 빌드 과정소스코드 컴파일테스트코드 컴파일테스트코드 실행테스트코드 리포트기타 추가 설정한 작업들 진행패키징최종 소프트웨어 결과물을 만들어낸다.🙋🏻 테스트코드란?내가 작성한 코드를 자동 테스트해주는 코드를 추가로 작성하는 코드실행내가 작성한 코드 (혹은 테스트 코드)를 컴파일을 거쳐 작동시켜 보는 것Artifact가 나올 수도 있고 안 나올 수도 있다.⚠ 주의인터프리터 언어는 컴파일이 필요 없다. 인터프리터의 대표적인 언어로는 파이썬, 자바스크립트가 있다.그런데 이런 빌드 과정이 이렇게 긴데 이것을 사람이 수동으로 하면 무조건 실수가 나오기 마련이다. 내가 생각해도 그럴 것이다. 현재 회사에선, 이런 과정을 일일이 한 경험이 있기 때문이다. 이런 경험 기반으로 간절했던 마음은 빌드 툴이라는 것을 사용했으면 하는 마음이었다. 물론 사내 보안 규칙으로 빌드툴은 사용이 안되었지만 이런 빌드툴로 인하여 우리가 이런 일련의 과정은 일일이 하지 않아도 되기 때문이다.빌드툴소스코드의 빌드 과정을 자동으로 처리해주는 프로그램외부 소스코드 자동 추가 관리빌드툴에는 Ant, Maven, Gradle이 있지만 유연함과 성능으로 Gradle이 압승으로 많은 사람들이 Gradle을 사용한다.Day1스프링 프로젝트를 시작하는 두 번째 방법스프링 프로젝트를 시작하는 두 번째 방법은 start.spring.io를 이용하는 방법이다. 즉, spring initializr를 이용하는 방법이다.이 방법 또한 나는 많이 사용해봤기 때문에 쉽게 수강을 할 수 있었다. 그래도 복습겸 열심히 들어봤다.처음에 빌드 툴을 설정하는게 나온다. 신규 프로젝트는 Gradle을 사용한다. 언어는 자바, 코틀린, 그루비를 선택하게 되어있는데 최신에는 코틀린을 많이 선택한다. 다음으로 스프링 부트 버전을 선택하는게 나오는데 여기서 알파벳이 붙은 버전은 오픈베타버전으로 가급적 알파벳을 붙이지 않는 것을 선택하는게 좋다. 다음으로 프로젝트 메타데이터를 작성하는게 나오는데 각각은 아래의 의미를 가진다.Group : 프로젝트 그룹Artifact : 최종 결과물의 이름Name : 프로젝트 이름Description : 프로젝트 설명Package name : 패키지 이름다음으로 패키징 방법을 선택하는게 나오는데 우리는 jar를 선택했다. 일종의 압축파일이다. 요즘 많이 사용하며, 특정 SI 프로젝트의 경우 War를 많이 사용하기도 한다. (내 경험담...)다음으로 자바 버전을 선택하는데 코치님은 자바17을 선택하셨지만 나는 21이 나온 시점이라 21을 선택하였다.다음으로 의존성 설정한다. 여기서 의존성이란, 프로젝트에서 사용하는 라이브러리 / 프레임워크를 의미한다. 📚 라이브러리란?프로그래밍을 개발할 때 미리 만들어져 있는 기능을 가져다 사용하는 것!코치님은 일종의 김치찌개로 비유하셨다. 김치찌개를 끓일 때 김치를 직접 농사해서 할 수 있고 마트에 살 수 있다. 여기서 마트의 김치를 라이브러리에 비유하셨다.나는 비유를 밀키트로 비유해보겠다. 떡볶이 밀키트가 있다하면 떡볶이를 직접 재료를 사서 조리를 할 수 있지만 밀키트를 사서 쉽게 끓여먹을 수 있다. 📚 프레임워크란?프로그래밍을 개발할 때 미리 만들어져 있는 구조에 코드를 가져다 끼워 넣는 것!이것도 김치찌개로 비유하셨는데, 여러 재료를 사서 만들 수도 있고 원데이 클래스에 가서 선생님이 시키는 것만 편하게 할 수도 있다. 여기서 원데이 클래스가 프레임워크라 하셨다. 마지막으로 의존성을 설정했으면 generate을 눌러서 압축파일을 다운 받고 아까 설명한 첫번째 방법을 이용하여 인텔리제이로 생성한 프로젝트를 켠다. @SpringBootApplication과 서버서버란? 내가 생각하는 서버는 영어로 serve는 "제공하다"라는 의미를 지닌다. 어떤 것을 제공하는 사람을 서버라고 부른다. 우리는 식당에 가면 종업원이 서빙을 한다. 즉 서버가 서빙을 하는 것이다. 즉, 기능을 제공하는 프로그램이여, 그 프로그램을 실행시키고 있는 컴퓨터를 서버라고 한다. 여기서 이런 의문사항이 있을 수 있다.🙋🏻 서버를 들었을 땐 엄청 크고 멋진 장치인데 그거랑 뭐가 다를까?서버라고 하면 엄청나게 큰 장치만 생각하며 막연하게 생각하신 분들이 많을 것이다. 컴퓨터의 외형으로 서버와 클라이언트를 나누는 것이 아니다. 서버는 단지 서비스를 제공해준다는 것만 기억하면 될 것이다. 우리가 사용하는 컴퓨터도 언제든지 서버가 될 수 있다.  나는 대학생 때 캡스톤 디자인으로 라즈베리파이라는 초소형 컴퓨터를 구입하여 서버로 이용하기도 했다. 손바닥만한 작은 크기지만 서버의 역할을 잘 수행하였다. 다만 대부분의 서버는 많은 클라이언트의 요청을 처리해야 하므로 성능이 중요하다. 따라서 하드웨어의 크기도 커진 것이다. 하지만 서버와 클라이언트에서 중요한 것은 하드웨어의 크기가 아니라 "누가 요청을 하고 누가 응답을 받는가"이다.여기서 클라이언트라는 말도 나온다. 클라이언트는 요청하는 사람 혹은 컴퓨터라고 한다. 그럼 이 클라이언트는 어떻게 서버에게 요청을 할까? 바로 인터넷을 통해 한다. 네트워크란 무엇인가?!네트워크를 이세계의 부족으로 설명해주셨다. 이세계 부족에는 주소체계와 택배시스템이 잘 되어 있다. 그래서 우리가 택배보내는 것처럼 아래와 같이 택배를 보낼 수 있다고 하셨다. B부족 감자동 곰로 13번길 2에 사는 둘째하지만 이렇게 주소체계를 우리도 마찬가지로 기억하는 사람은 많지 않을 것이다. 그냥 '파란색 집에 사는 둘째'라고 편히 부른다. 이제 현실세계도 마찬가지다. 현실세계에 컴퓨터는 고유의 IP를 가진다. 그리고 현실세계는 택배시스템처럼 인터넷이 잘 발달되어 있다. 그래서 우리는 인터넷을 통해 데이터를 주고 받을 수 있다. 아래와 같이 말이다.210.210.210.210 IP를 가진 PC에서 port 8080번으로 데이터 보내줘!파란색집 둘째가 port이고 자세한 주소가 IP 주소이다. 하지만 우리는 인터넷 접속할 때 일반적으로 IP주소와 port를 입력하지 않는다. 아래와 같이 도메인을 입력하고 접속할 것이다.https://www.spring.io:3000여기서 210.210.210.210이 spring.io일 것이다. HTTP와 API란 무엇인가?!HTTP와 API를 설명을 위해 또 다시 이 세계를 비유해주셨다. 택배를 보내려면 우리는 운송장이란 표준을 이용한다. 이세계의 운송장은 아래와 같다.내놓아라 파란집 둘째, 포션 빨강색 2개여기서 '내놓아라'는 운송장을 받는 사람에게 요청하는 행위이며, '파란 집'은 운송장이 가는 집을 말하고, '둘째'는 운송장을 실제 받는 사람, '포션'은 운송장을 받는 사람에게 원하는 자원이며, '빨강색 2개'는 자원의 세부조건을 의미한다. 여기서 행위와 자원은 빨간집에 운송장을 보내기 전에 약속해야 한다.현실세계에도 데이터를 받는 표준이 있는데 바로 HTTP이다. 일종의 약속이다. 아래와 같이 약속을 지켜 우리는 데이터를 보낸다.GET /portion?color=red&count=2Host: spring.io:3000여기서 GET은 HTTP 요청을 받는 컴퓨터에게 요청하는 행위이며, HTTP method라고 부른다. Host 부분은 HTTP 요청을 받는 컴퓨터와 프로그램 정보를 뜻한다. /portion은 HTTP 요청을 받는 컴퓨터에게 원하는 자원을 의미하며, path라고 부른다. ?은 구분기호이며 color=red는 자원의 세부조건, &는 구분기호, count=2 또한 자원의 세부조건을 뜻한다.행위와 자원은 HTTP 요청을 보내기 전에 약속해야 한다.그리고 이런 세부조건들을 고급용어로 쿼리스트링라고 부른다. 또한 이세계에서 아래와 같이 운송장을 작성할 경우도 있다.창고에 넣어라, 오크가죽, 창고에이것을 현실세계로 표현하면 다음과 같다.POST /oak/leatherHost: spring.io:3000오크가죽정보여기서 다른 것은 위와 동일하지만 '오크가죽정보'는 body라고 하고 호스트 부분과 한줄 내리고 시작을 한다. 요약을 하면 GET HTTP method는 데이터를 요청하는것으로 보통 쿼리스트링을 이용한다. (없는 경우도 있음) 하지만 POST는 데이터를 저장을 하는 것으로 바디를 이용한다. 이외에 PUT과 DELETE가 있는데 PUT은 데이터 수정을 요청하는 것으로 바디를 이용하고, DELETE는 데이터 삭제요청을 하는 것으로 쿼리스트링을 이용한다. 그럼 API는 무엇일까? API란, 정해진 약속을 하여, 특정 기능을 수행하는 것이다. 그래서 이 약속은 이전까지 썼던 방식으로 첫줄에는 HTTP method와 path, (쿼리)를 작성한다. 추가적으로 어디로 보낼 지 Host를 작성한다.(도메인 + 포트) 이런것을 헤더를 작성한다고 하고 헤더는 여러줄이 가능하다. 그 다음 body가 있을 경우 한 줄 띄고 body를 작성하며 여러줄 작성이 가능하다. 그래서 https://spring.io/portion?color=red&count=2 이런 형식을 URL이라고 부르고 작성 순서는 아래와 같다.프로토콜://도메인(혹은 IP:포트)/자원경로?쿼리(추가정보)그럼 요청을 보냈으니 응답을 보내줘야 한다. 예를 들어 200 OK 이런식으로 말이다. 요청에 대한 응답을 보내주는 컴퓨터를 서버라고 부른다. 그리고 요청을 한 컴퓨터를 클라이언트라고 부른다. 또한 응답에는 body를 담을 수도 있다. 응답은 요청 구조와 동일하다. 그리고 응답의 핵심은 상태코드인데 200, 201, 400, 404, 500등이 존재한다. GET API 개발하고 테스트하기API를 개발 전에는 항상 API Spec을 살펴봐야 한다. 즉, HTTP method와 path, 쿼리를 봐야하고 이에 대한 응답에 결과도 확인을 해봐야 한다. 그래서 실제 더하는 GET API를 실습을 해보았다.여기서 실습중에 @RestController라는 어노테이션도 학습을 했는데, 해당 클래스를 API의 진입점으로 만드는 어노테이션이라고 볼 수 있다. 그리고 @GetMapping("/path")이라는 어노테이션도 학습을 했는데 해당 메서드를 HTTP method가 GET이고 path가 /path인 API로 지정한다는 의미이다. 마지막으로 @RequestParam을 배웠는데 쿼리가 있을 시, 주어진 쿼리를 함수 파라미터로 넣을 수 있다. 그래서 단일 타입으로 넣을수도 있지만 request DTO를 만들어 객체를 넣을 수도 있는데 객체를 넣을 시, 어노테이션은 생략할 수 있다. 단, Spring Boot 3.2 이후 버전은 생략이 불가능했는데 빌드 툴을 Gradle로 변경하면 가능했었다. 왜 그런지는 내가 작성한 미션1에 대한 내용을 살펴보자. 📚 미션1https://inf.run/QKGsfDay2POST API 개발하고 테스트하기POST에는 어떻게 데이터를 전송하고 받을지에 대해 학습을 했다. POST에서는 GET과 달리 HTTP Body를 이용하였다. 그리고 HTTP body는 JSON 형태로 보낸다. 객체 표기법, 즉 무언가를 표현하기 위한 형식이다! Java로 비유해보자면, Map<Object, Object> 느낌이다.JSON의 표기 예는 아래와 같다.{ “name”: “양성빈”, “age”: 29, "stack": ["java", "javascript"], "house": { "address": "대한민국 경기도 시흥", "hasDoor": true } }그래서 POST HTTP method로 body를 넘겨 보낼 때 이런 형식으로 보낸다.그리고 실습을 통해 POST method를 실습했다. 여기서 나온 주요 어노테이션은 아래와 같다.@PostMapping("/path") : 아래 함수를 HTTP Method가 POST이고 Path가 /path인 API로 만든다!@RequestBody: HTTP Body로 들어오는 JSON을 파리마터로 넘긴 객체(DTO)로 바꿔준다. 그리고 DTO에는 json의 key값이 명시되어야 하며, 각 속성은 key값과 동일하게, 타입도 value에 타입에 따라서 작성한다.유저 생성 API 개발실제 프로젝트에 대한 기능 스펙을 제시해주셨으며 웹 UI까지 제공해주셨다. 그리고 우리가 배운 POST를 이용해 유저생성 API를 개발해보았다. 이 부분도 내가 아는거라 간단히 편하게 실습을 할 줄 알았지만, 내가 미쳐 생각지 못한 부분이 있었는데 이 부분을 다시한번 복습하는 계기로 실습을 하였다. 유저 조회 API 개발과 테스트이제 유저 조회 API를 실습해보았다. 이전에 배운 GET HTTP method를 이용하여 개발했다. GET API에서는 응답 반환이 있었는데 이 형태는 json이였으며 json으로 반환받으려면 파라미터로 넘기는 객체(DTO)에 getter가 반드시 있어야 json으로 받을 수 있다. 이 부분이 내가 배운 사실이었다.  📚 참고한 클래스 안에는 여러 API 추가 가능 정리. 다음으로!우리가 이렇게 GET과 POST API를 설계하고 개발하고 테스트까지 해보았다. 하지만 지금까지 만든 프로젝트에 큰 문제가 있다. 서버를 재시작하면 데이터가 날라갔다. 그 이유를 나는 잘 몰랐고 DB를 안 써서 그랬겠지라는 생각이었는데 코치님께서 아래와 같이 설명해주셨다.컴퓨터에는 1차 메모리와 2차 메모리가 있고 데이터가 날라가는 이유는 1차 메모리에 있었기 때문이다. 그래서 서버를 재시작해도 데이터가 남아있으려면 2차 메모리에 저장을 해두어야 하는데 우리는 2차 메모리에 저장보단, DB에 저장한다고 하셨다.Day3Database와 MySQL지난번에 우리는 서버를 재가동하면 데이터가 남아있지 않고 사라졌다. 그 이유는 유저 데이터가 램에 저장되어 있기 때문이다. 그래서 우리는 2차 메모리등에 저장하는 방법을 생각할 수 있다. 자바의 File이라는 클래스를 이용해 직접 디스크에 접근을 할 수 있지만 보통은 Database를 이용한다.Database란 데이터를 구조화시켜 저장하는 하는 것이라고 볼 수 있다. 마치 엑셀과 비슷하다고 생각하면 좋을 것 같다. 엑셀처럼 데이터를 표처럼 구조화하여 저장한다. 대표적으로 RDB의 MySQL이 그렇다. 그리고 이 표처럼 구조화된 데이터를 조회하는 언어를 SQL이라고 한다.MySQL에 접근하는 방법은 먼저 MySQL을 시작해야 한다. 인텔리제이 얼티밋 유료버전을 이용하면 IDE에서 직접 접근이 가능하지만 이 IDE를 이용할 수 없는 분들은 윈도우의 cmd창이나 유닉스의 터미널을 이용해야 한다. 동일하게 아래의 명령어를 작성하면 된다. $> mysql -uroot -p MySQL에서 테이블 만들기테이블 하나를 만든 다는 것은 엑셀파일을 만드는 것인데, 엑셀파일을 만들려면 엑셀파일을 담을 폴더를 생성 후에 폴더에 들어가 엑셀파일을 생성해야 한다. 그리고 엑셀에 헤더를 작성해야 한다. 그리고 헤더별로 서식을 설정한다. MySQL 테이블 생성도 이와 유사하다. 과정은 아래와 같다. 여기서 폴더는 데이터베이스(스키마)를 엑셀파일은 테이블을 엑셀파일의 헤더는 테이블의 필드를 정의한 것이다. 그리고 엑셀파일의 서식은 테이블의 필드의 타입을 설정하는 것이라 볼 수 있다. 그럼 이제 DB의 테이블을 직접 만들어보자.데이터베이스 만들기$> create database [데이터베이스 이름];데이터베이스 목록보기$> show databases;데이터베이스 지우기$> drop database [데이터베이스 이름];데이터베이스 접속하기$> use [데이터베이스 이름];테이블 목록보기$> show tables;테이블 만들기$> create table [테이블 이름] ( [필드1 이름] [타입] [부가조건], [필드2 이름] [타입] [부가조건], ... primary key ([필드이름]). );테이블 제거하기$> drop table [테이블 이름];💡 꿀팁1. auto_increment: 데이터를 명시적으로 넣지 않더라도 1부터 1씩 증가하며 자동 기록된다. 단, 데이터를 생성하고 삭제를 한 후 다시 생성시 1부터 생성되는게 아니라 삭제한 컬럼의 id값 다음 값으로 생성된다. 그리고 데이터 추가시에, auto_increment로 설정한 필드는 안 넣어도 자동으로 들어간다.2. primary key: 유일한 필드를 지정할 때 사용MySQL 타입정수타입: tinyint(1 byte), int(4byte), bigint(8byte)실수타입:double(8 byte)decimal(A, B): 소수점 B개를 가지고 있는 전체 A자릿 실수문자열 타입:char(A): A글자가 들어갈 수 있는 문자열varcher(A): 최대 A 글자가 들어갈 수 있는 문자열날짜, 시간 타입date : 날짜, yyyy-MM-ddtime : 시간, HH:mm:ssdatetime : 날짜와 시간을 합친 타입, yyyy-MM-dd HH:mm:ss지금까지 배운 SQL을 DDL(Data Definition Language)이라고 한다. 즉, 데이터 정의 언어라고 말한다. 테이블의 데이터를 조작하기데이터 넣기$> INSERT INTO [테이블 이름] (필드1이름, 필드2이름, ...) VALUES (값1, 값2, ...)데이터 조회하기$> SELECT * FROM [테이블 이름]; // * 대신에 필드 이름 여러개 넣을 수 있다.$> SELECT * FROM [테이블 이름] WHERE [조건]; // 특정 조건을 통해 조회. AND 또는 OR을 이용해 조건을 이어 붙일 수 있다! 조건에는 =, <= 외에도 !=, <, >, >=, between, in, not in 등이 있다.데이터 업데이트$> UPDATE [테이블 이름] SET 필드1이름=값, 필드2이름=값, ... WHERE [조건];⚠ 주의만약 [조건]을 붙이지 않으면, 모든 데이터가 업데이트된다!!!데이터 삭제하기$> DELETE FROM [테이블 이름] WHERE [조건];⚠ 주의만약 [조건]을 붙이지 않으면, 모든 데이터가 삭제된다!!지금까지 배운 SQL을 DML(Data Manipulation Language)이라고 한다. 즉, 데이터 조작 언어이다. Spring에서 Database 사용하기지금까지 사람이 DB에 직접 접근했으니 웹 어플리케이션이 DB에 접근하도록 하겠다.먼저 src/main/resources의 경로에 application.properties가 있을 것이다. 이것을 application.yml로 변경해준다. 단, 강의에 따라 변경을 한것이지 properties가 더 익숙하신 분이면 여기다가 DB설정정보를 기입해도 된다.아래와 같이 DB 설정정보를 기입한다.spring: datasource: url: "jdbc:mysql://localhost/library" username: "root" password: "1234" driver-class-name: com.mysql.cj.jdbc.Driverjdbc:mysql:// - jdbc를 이용해 mysql에 접근localhost – 접근하려는 mysql은 localhost에 있다./library – 접근하려는 DB는 library이다.root는 MySQL에 접근하려는 계정명1234는 MySQL에 접근하기 위한 비밀번호마지막으로 driver-class-name은 데이터베이스에 접근할 때 사용할 프로그램이 적혀있다.그럼 이전에 만든 프로젝트에 DB를 입히기 위해 유저 정보 테이블을 만든다.그후에, 유저 생성 API를 JdbcTemplate을 이용하여 SQL을 날린다. 생성자를 만들어 jdbcTemplate을 파라미터로 넣으면, 자동으로 들어온다. 그리고 SQL을 문자열로 입력 후, 값이 들어갈 부분에 ?을 넣는다. ?를 사용하면 값을 유동적으로 변경이 가능하다. 그리고 이 문자열을 JdbcTemplate의 update 메서드에 담는다. update 메서드는 insert, update, delete 쿼리에 적용이 가능하다.다음 유저 조회 API도 변경한다. 아래와 같이 변경이 가능하다.jdbcTemplate.query(sql, RowMapper 구현 익명클래스)구현 익명클래스 안에는 ResultSet에 getType(“필드이름”)을 사용해 실제 값을 가져올 수 있다. 그리고 이 익명클래스는 람다식을 이용하면 더 간단하게 표현이 가능하다.Day4유저 업데이트 API, 삭제 API 개발과 테스트이제 DB를 이용해 유저 업데이트와 삭제 API를 개발해보았다. 업데이트는 UPDATE 쿼리를 사용하여 jdbcTemplate의 update 메서드에 넘겨주어 실행을 하였고, 업데이트는 body를 넘기므로 @RequestBody를 사용하였다. 그리고 아래와 같은 추가 어노테이션도 학습하였다.@PutMapping("/path"): /path 경로로 PUT HTTP method를 전송한다.삭제 또한 마찬가지다. DELETE 쿼리를 이용하여 jdbcTemplate의 update 메서드에 넘겨주어 실행을 하였고 삭제는 파라미터를 넘기므로 @RequestParam을 사용하였다. 그리고 아래와 같은 추가 어노테이션도 학습을 하였다.@DeleteMapping("/path"): /path 경로로 DELETE HTTP method를 전송한다.유저 업데이트 API, 삭제 API 예외 처리 하기여기서 또 하나 꿀팁은 존재하지 않는 유저를 업데이트하고 삭제해도 200OK 응답이 나오는 것이 문제였다. 그래서 Exception을 던져서 500 INTERNAL SERVER ERROR가 나오게 하는것으로 변경을 하였다. 그래서 업데이트든 삭제든, select 쿼리를 전에 날려서 유저가 존재하는지 유무를 판단후, 있으면 각각 업데이트, 삭제 쿼리를 날리고 없다면 IllegalArgumentException을 날라기로 변경하였다. 아래와 같이 select 쿼리를 날리 수 있다.String readSql = "select * from user where id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();이렇게 return된 boolean 변수를 통해 유무 판단을 하는 것이다. Section2 정리. 다음으로!지금 코드에도 문제가 존재한다. 바로 한 클래스인 Controller에 많은 역할을 하며 여러 비즈니스 로직이 통합되어 있다.이 문제는 만약 이 코드가 1000줄 이상만 되도 어느 기능을 수정할 때 상당한 워킹타임이 들 것이다. 이런 문제를 어떤 방법론으로 어떻게 변경할지 알아보자.Day5좋은 코드(Clean Code)는 왜 중요한가?!코드는 요구사항을 표현한 언어이다. 개발자는 요구사항을 구현하기 위해 코드를 읽고 작성한다. 여기서 핵심은 읽는다는 것이다. 예를 들어 몇천줄의 코드에 변수도 의미없는 이름을 짓고 로직도 한 곳에 모여있다면 유지보수하는 개발자는 읽기도 힘들 것이다. 또한 동시에 여러명이 수정이 힘들고, 어느 부분을 수정하더라도 다른 곳에 영향을 끼칠 수 있기에 지뢰코드가 된다. 당연히 단위테스트는 힘들 것이다. 또한 안 좋은 코드가 쌓이면 시간이 지날수록 생산성이 떨어진다. 즉, 유지보수 시간이 늦어지고 이것은 바로 돈과 관계가 되기에 클린코드는 정말 중요하다. 그래서 클린코드를 정의하면 아래와 같다.함수는 최대한 작게 만들고 한 가지 일만 수행하는 것이 좋다.클래스는 작아야 하며 하나의 책임만을 가져야 한다.우리의 컨트롤러 클래스도 API 진입점 역할, 유저의 유무를 판단하는 예외로직, SQL을 통한 DB통신으로 무려 3가지 역할을 한다. Controller를 3단 분리하기 – Service와 Repository우리의 컨트롤러는 API 진입점 역할, 유저의 유무를 판단하는 예외로직, SQL을 통한 DB통신으로 무려 3가지 역할을 한다. 이것을 분리해보는 시간을 가졌다. API 진입점 역할은 컨트롤러 레이어 역할로 예외로직은 서비스 레이어 역할로, SQL을 통한 DB통신은 레파지토리 레이어 역할로 분리하였다. 이렇게 레이어 분할을 한 구조를 Layered Architecture라고 한다.Live Q&A금요일에 Live Q&A를 참석했다. 커뮤니티에 올린 질문들을 코치님께서 성심 성의껏 말씀을 해주셔고 약간의 시간이 남아서 웹 어플리케이션 서버와 웹서버 차이를 역사를 통해 알려주셨다.초기에는 원격으로 메세지를 보내는 방식에서 시작했다가 이후에 클라이언트가 서버에게 정적 리소스를 요청하는 걸로 발전했다. 즉, 어떤 클라이언트가 요청을 하든지 똑같은 내용이 오는 것이다. 이것을 웹 서버라고 하고 대표적으로 Apache와 NginX가 있다. 이러다가 이런 생각도 하게 되었다. 클라이언트마다 다른 리소스를 받고 싶다는 생각을 하게 되었다. 그래서 클라이언트가 서버에 요청을 하면 서버는 요청을 확인해 그에 맞는 프로세스를 실행하여 파일을 그때 그때 바꾼다. 하지만 이런 과정은 성능적으로 좋지 않다. 그래서 이런식으로 변경을 했다. 클라이언트가 요청을 하면 서버는 요청을 받고 쓰레드를 생성한다. 쓰레드는 서블릿이라는 인터페이스 통해 알려준다. 즉, 여기서 서버가 생성한 프로세스는 Servlet Container라고 하고 쓰레드를 쓰레드 풀에 담아 관리한다.여기서 또 생각한 것은 서블릿은 여러 공통코드가 많아 우리가 개발을 할때 공통코드를 적느라 비효율적이라 느껐다. 그래서 서블릿을 그때 그때 사용하지 않고 하나로 퉁 치는 개념이 등장했는데 그것을 Dispatcher Servlet이 등장하게 되었다.미션 해결 과정Day1첫번째 미션은 어노테이션에 대한 학습을 하는 것이였다. 아래 질문을 통해서 말이다.어노테이션을 사용하는 이유 (효과) 는 무엇일까?나만의 어노테이션은 어떻게 만들 수 있을까?여기서 나는 이런 질문도 질문이지만, 단순히 @붙이는 걸로 파악하고 있었다. 그래서 이 어노테이션에 대한 기본 문법, 커스텀 어노테이션에 대해 알아보면서, 자바 표준 어노테이션은 무엇이 있고 각각 무슨 의미를 하는지 학습을 찾아봤으며, 찾다보니 자바의 리플렉션 개념까지 연관이 되었다. 그래서 리플렉션에 대한 학습까지 이어갔다. @Documented를 붙은 어노테이션과 아닌 어노테이션이 어떤 차이가 있는지 java doc을 직접 만들면서 확인을 해보았고 자바8에 어노테이션의 변화에 대해서도 학습을 마쳤다. 그리고 자주 사용하는 롬복 어노테이션들이 어떤식을 동작을 해보는지 궁금하여 찾아보고 어노테이션 프로세서를 이용하여 조작을 하는 걸 알게 되었다. 즉, 나의 학습방식은 아래와 같았다.어노테이션이 뭐야? 어떻게 사용해? 동작원리는 뭐야? 각각의 어노테이션이 붙은거랑 아닌거랑 어떻게 달라?📖 학습 방법 및 반성할 점위의 물음을 재차 물으며 학습했다. 하지만, 반성할 점도 있었다. 하나의 개념으로 여러 개념들을 파보는 것은 좋지만 뭔가 실습을 많이 해보면서 익히면 체득이 될텐데 그러지 못했다는 점을 반성하게 된다. ㅠㅠ📋 미션 블로그https://inf.run/QKGsfDay2두번째 미션은 GET과 POST API에 대한 실습을 문제로 내주셨다. 여기서 나는 이런 생각을 했다. 단순히 문제 푸는것에 의의를 두셔서 문제를 내주신게 아닐 것이다. 좀 더 깊이 파보았다.문제를 풀 때 일단 먼저 풀고, 비즈니스 로직들을 서비스 레이어로 분리하여 해보고, DTO를 클래스가 아닌 JDK17에 나온 record를 이용도 해보고, 이에 Spring Boot 3.2에 나타는 트러블 슈팅도 겪었다. 그리고 검증에 대한 로직을 spring boot starter validation을 이용해 예외를 처리하며, 테스트코드까지 작성함으로 조금 더 깊이있게 해보았다. 📖 학습 방법 및 반성할 점나는 미션을 제출하면서 완벽히 진행을 했다고 느꼈다. 그리고 다른 러너분들이 제출한 글을 보니 의외로 나와 비슷한 부분도 있지만 또 다른 방법으로 제출하신 러너분들을 볼 수 있었다. 이에 나는 아직 부족하다라는 생각을 하며 좀 더 열심히 해서 성장해야겠다는 생각을 하게 되었다. 📋 미션 블로그https://inf.run/fJXgxDay3세번째 미션은 익명 클래스 / 람다 / 함수형 프로그래밍 / @FunctionalInterface / 스트림 API / 메소드 레퍼런스 라는 키워드를 생각하여 람다식과 익명클래스를 공부하는 것이였다.[질문]자바의 람다식은 왜 등장했을까?람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?이 또한 나는 익명클래스의 어떠한 불편함때문에 왜 람다식이 등장한지를 질문사항으로 공부를 먼저 시작했다. 다음 람다식이 무엇인지 정의를 내려보았다. 또한 람다식을 공부하니 함수형 프로그래밍과 함수형 인터페이스가 연관되었으며 이에 대해 또 문법을 공부하고 이번엔 실습도 해보았다. 그리고 익명클래스와 람다가 어떻게 다른지를 코드로만 보는게 아니라 바이트코드를 확인하여 살펴봤다. 또한 더 깊게 파다보니 INVOKEDYNAMIC 내부 동작을 확인하게 되었다. 정말 깊게 팔 수록 한도 끝도 없다고 느끼게 되었다. 📖 학습 방법 및 반성할 점람다에 대해 처음에는 왜 등장했을까? 부터 시작해서 익명클래스와 람다가 어떤 차이가 있을지 코드뿐만 아니라 바이트코드로 확인을 했으며 더 깊이 들어가 INVOKEDYNAMIC 내부 동작을 학습해보는 계기가 되었다. 이런 미션을 하면서 "나는 이제까지 아무것도 아니었구나"라는 생각을 하며 더욱 더 열심히 하게되는 계기였다.Day4네번째 미션은 DB를 연동하여 API를 생성하고 수정하고 조회하는 것을 해보았다.당연히, 일단 나는 문제를 컨트롤러 클래스에 비즈니스, 예외로직을 넣고 해결했다. 이후에 나는 리팩토링 작업을 거쳤다. 이런 로직들을 서비스, 레파지토리로 분리하고 나는 엔티티라는 것을 따로 만들어 request와 response는 dto로 처리하고 엔티티는 순수히 데이터를 받는 걸로만 처리하여 더욱 견고히 했다. 이렇게 작성한 이유는 아래와 같다.보안: DTO를 사용하면 민감한 정보를 숨기고 필요한 데이터만 클라이언트에 전달할 수 있습니다.추상화: DTO는 엔티티의 구조를 클라이언트에 그대로 노출하지 않고, API 응답을 통해 데이터의 표현 방식을 커스터마이징할 수 있게 해줍니다.유연성: 엔티티와 API 사이의 계약을 DTO를 통해 정의함으로써, 엔티티의 변경이 API 스펙에 직접적인 영향을 미치지 않도록 합니다.또한 당연히, 테스트코드로 검증까지 완료하였다. 📖 학습 방법 및 반성할 점문제3번에 SUM이라든지 GROUP BY를 알긴 알았지만 이런 집계함수에 대해 정확히 뭔지가 헷갈렸던 부분이 많았다. 그래서 나름 검색도 해보고 사용법도 익혀보았다. 이로 인해 내가 SQL 부분을 완전히 아는게 아니라는 생각을 가졌고 시간날때 틈틈이 SQL 공부도 해보면서 자격증 시험(SQLD)도 준비해보면 좋지 않을까라는 생각을 가지게 되었다.Day5다섯번째 미션은 하나의 코드를 클린코드 개념을 도입해 리팩토링 하는 것이였다.나는 그래서 클린코드에 대해 검색을 하면서 다른 유튜브 영상을 통해 학습을 했고 이를 바탕으로 총 4~5단계에 걸쳐서 리팩토링을 하였다. 1단계는 단순히 변수이름 변경 및 메서드로 분리였지만, OOP 개념을 도입하고 단일책임의 원칙을 적용하였으며 마지막에는 팩토리 디자인패턴과 테스트 코드로 마무리하였다. 📖 학습 방법 및 반성할 점정말 이번 미션이 나를 반성하게 하는 점이였다. 현업을 뛰는 나로서 현업(프론트엔드)에서 내가 얼마나 더러운 코드를 짰다는 생각이 많이 들었다. 그 동안 나는 쓰레기를 생산했다고 할 정도로... 그래서 나는 다음주 출근하자마자 시간이 된다면 바로 리팩토링 작업을 시작해야겠다고 느끼게 된 하루였다.회고이번주부터 정말 정신이 없었다. 직장다니면서 끝나자마자 회사 근처 카페에 가서 미션 수행하고, 정말 정신이 없었다. 심지어 어느 하루는 날밤을 세서 한 적도 있었다. 하지만 오히려 힘들고 불행했다기 보단 행복했다. 다른 분들은 이상하게 느낄 지 모르지만 뭔가 해결했다는 쾌감이 정말 감명 깊었고 지금의 마인드를 기억하면 다음주도 화이팅해서 성장해보겠다.

백엔드인프런워밍업스터디클럽발자국

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 람다식 (Day3)

과제진도표 3일차와 연결됩니다우리는 JdbcTemplate을 사용하는 과정에서 익명 클래스와 람다식이라는 자바 문법을 사용했습니다. 익명 클래스는 자바의 초창기부터 있던 기능이고, 람다식은 자바 8에서 등장한 기능입니다. 다음 키워드를 사용해 몇 가지 블로그 글을 찾아보세요! 아래 질문을 생각하며 공부해보면 좋습니다! 😊 [키워드]익명 클래스 / 람다 / 함수형 프로그래밍 / @FunctionalInterface / 스트림 API / 메소드 레퍼런스 [질문]자바의 람다식은 왜 등장했을까?람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?람다벌써 스터디 클럽 3일차가 되었다. 이번 강의에서는 DB 쿼리들에 대해서 배우고 DB를 Spring Boot 프로젝트와 연동하여 JdbcTemplate을 이용하여 실습을 해보았다. 이 과정에서 람다식이 나왔고, 오늘은 람다식에 대해 다뤄보도록 하겠다. 자바 개발자를 위한 코틀린 입문 - 17강(람다)람다를 본격적으로 다루기 전에 강의 중에 코치님이 '자바 개발자를 위한 코틀린 입문편'에 람다를 보는 것을 추천드린다고 하셔서 학습을 해보았다. Java에서 람다를 다루기 위한 노력먼저 예시 코드를 살펴보자.package me.sungbin.lecture; public class Fruit { private final String name; private final int price; public Fruit(String name, int price) { this.name = name; this.price = price; } public String getName() { return name; } public int getPrice() { return price; } }  그리고 main 함수에 아래와 같이 작성한다. List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); 여기서 어느 손님이 와서 "사과만 보여주세요~"라고 말한다. 그러면 우리는 개발자로서 이에 해당하는 메서드를 만들어 작성할 것이다. 그런데 갑자기 조건이 붙기 시작한다. 사과뿐만 아니라, 바나나도 보여주고 각각 가격은 5000원 이상인 것들만 보여달라고 주문한다. 🤔 그래서 우리는 고민을 하다가 일일이 메서드를 만드는 것은 불필요하기에 인터페이스를 이용하기로 한다. package me.sungbin.lecture; public interface FruitFilter { boolean isSelected(Fruit fruit); }  위와 같이 인터페이스를 만들고 아래와 같이 메서드 안에 인터페이스를 적용하고 이 메서드를 호출하는 쪽에서 익명클래스로 구현해주면 된다. package me.sungbin; import me.sungbin.lecture.Fruit; import me.sungbin.lecture.FruitFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author : rovert * @packageName : org.example * @fileName : ${NAME} * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, new FruitFilter() { @Override public boolean isSelected(Fruit fruit) { return Arrays.asList("사과", "바나나").contains(fruit.getName()) && fruit.getPrice() >= 5_000; } }); } private static List<Fruit> filterFruits(List<Fruit> fruits, FruitFilter fruitFilter) { List<Fruit> results = new ArrayList<>(); for (Fruit fruit : fruits) { if (fruitFilter.isSelected(fruit)) { results.add(fruit); } } return results; } } 🥲 익명 클래스 아쉬운 점1. 익명클래스를 사용하는 것은 딱 봐도 복잡해 보인다.2. 다양한 Filter가 필요할 수도 있다. ex) 과일 간의 무게 비교, n개의 과일을 한번에 비교등등... 이러한 이유로 JDK8부터 람다(이름 없는 함수) 등장하였다. 또한 FruitFilter와 같은 인터페이스와 같은 Predicate, Consumer등을 많이 만들어 두었다. 그래서 위의 코드는 아래와 같이 변경되었다. package me.sungbin; import me.sungbin.lecture.Fruit; import me.sungbin.lecture.FruitFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; /** * @author : rovert * @packageName : org.example * @fileName : ${NAME} * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, fruit -> fruit.getName().equals("사과")); } private static List<Fruit> filterFruits(List<Fruit> fruits, Predicate<Fruit> fruitFilter) { List<Fruit> results = new ArrayList<>(); for (Fruit fruit : fruits) { if (fruitFilter.test(fruit)) { results.add(fruit); } } return results; } } 💡 람다로 변경되면서 바뀐 점1. 호출하는 부분이 filterFruits(fruits, fruit -> fruit.getName().equals("사과")); 처럼 바뀌었다.2. 그 다음에 함수는 Predicate로 통하여 리팩토링을 할 수 있다. 변수 -> 변수를 이용하는 함수 혹은 (변수1, 변수2) -> 변수1과 변수2를 이용한 함수 이런 형태가 람다다. 여기서 또 JDK8에 위의 for문과 if문을 간결하게 처리하기 위해 간결한 스트림이 등장했다. (병렬처리에도 강점)그래서 코드가 아래와 같이 변경되었다. package me.sungbin; import me.sungbin.lecture.Fruit; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, fruit -> fruit.getName().equals("사과")); } private static List<Fruit> filterFruits(List<Fruit> fruits, Predicate<Fruit> fruitFilter) { return fruits.stream().filter(fruitFilter).collect(Collectors.toList()); } } 또한 람다는 아래와 같이 메서드 레퍼런스를 활용이 가능하다. package me.sungbin; import me.sungbin.lecture.Fruit; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; /** * @author : rovert * @packageName : org.example * @fileName : ${NAME} * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, Fruit::isApple); } private static List<Fruit> filterFruits(List<Fruit> fruits, Predicate<Fruit> fruitFilter) { return fruits.stream().filter(fruitFilter).collect(Collectors.toList()); } }비즈니스 로직을 도메인 메서드로 빼주고 이런 형식으로도 관리가 가능하다. 이렇게 자바에서 메서드 자체를 직접 넘겨주는 것처럼 보이지만 실제로는 인터페이스를 받기 때문이다.이 말은 메서드는 변수에 할당하거나 파라미터로 전달할 수 없고 2급시민으로 간주한다.위의 설명으로 잔도표에 3일차에 미션은 금방 끝나게 될 것이다. 하지만 단순 강의로 과제를 할 수는 없기에 내가 한번 더 찾아보고 학습한 내용을 공유드린다. 익명 클래스익명 클래스가 등장한 이유는 위의 강의처럼 사용자의 요구 조건이 메서드로 처리하기엔 너무 많아지고 조건이 복잡해지면서 인터페이스 혹은 추상클래스를 이용할 때 조금 간편하게 하기 위해 등장한 것이다. 조금 더 간결히 이야기를 하면 익명 클래스는 인터페이스나 추상 클래스의 인스턴스를 간편하게 생성하기 위해 등장했습니다.익명 클래스는 이름 없이 선언과 동시에 객체를 생성할 수 있는 클래스로, 주로 단일 사용 인스턴스에 대한 정의에 사용됩니다. 이러한 클래스는 GUI 이벤트 처리나 작은 콜백 객체 같은 곳에 유용하게 쓰입니다. 익명 클래스의 주된 목적은 코드의 간결성을 높이고, 즉석에서 필요한 구현을 제공하여 별도의 클래스 파일을 만들지 않아도 되게 하는 것입니다.그럼 예시 코드를 보자. package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : AnonymousClass * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class AnonymousClass { public static void main(String[] args) { Thread myThread = new Thread(new Runnable() { @Override public void run() { System.out.println("익명 클래스를 사용한 스레드 실행"); } }); myThread.start(); // Thread start } } 람다식(Lambda Expression)자바가 1996년 등장안 이후 두번의 큰 변화가 있었다. 첫번째 JDK 1.5 부터 추가된 제네릭의 등장이고, 두번째 JDK 1.8 부터 추가된 람다식의 등장이다. 람다의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다. 람다식이란?람다식은 간단히 말해 메서드를 하나의 식(expression) 으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)' 이라고도 한다. package me.sungbin.blog; import java.util.Arrays; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : LambdaBlogEx1 * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class LambdaBlogEx1 { public static void main(String[] args) { int[] arr = new int[5]; Arrays.setAll(arr, (i) -> (int)(Math.random() * 5) + 1); } }  위의 코드에서 () → (int)(Math.random() * 5) + 1 구문이 람다식이다. 이 람다식이 하는 일을 메서드로 굳이 표현하면 아래와 같다. int method(){ return (int)(Math.random() * 5) + 1; } 메서드 형태보다 람다식이 간결하고 이해가 쉽다.게다가 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야 이 메소드를 호출할 수 있지만, 람다식은 이 과정 없이 오직 람다식 자체만으로 이 메서드의 역할을 수행할 수 있는 것이 큰 장점이다. 🙋🏻 메서드와 함수 차이함수는 수학에서 따온 것이다. 수학의 함수와 개념이 유사하다. 그러나 OOP에서는 함수대신 객체의 행위나 동작을 의미하는 메서드라는 용어를 사용한다.메서드는 함수와 같은 의미지만, 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미를 다른 용어를 선택해서 사용한 것이다. 그러나 이제 다시 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 함수라는 용어를 사용하게 되었다. 람다식 작성하기람다식은 '이름 없는 함수'답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 {} 사이에 '->'를 추가한다. AS-IS 반환타입 메서드이름(매개변수 선언){ 문장들 } TO-BE (매개변수 선언) -> { 문장들 } AS-IS int max(int a, int b){ return a> b ? a: b; } TO-BE (int a, int b) -> { return a > b ? a: b; } TO-BE에서 반환 값이 있는 메서드의 경우 return문 대신 '식(expression)'으로 대신할 수 있다.식의 연산결과가 자동적으로 반환된다.문장이 아닌 식으로 끝에 ';'를 붙이지 않는다.(int a, int b) -> a > b ? a: b // TO-BE 선언된 매개변수의 타입은 추론이 가능한 경우 생략할 수 있다.람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문이다.(a, b) -> a > b ? a : b // TO-BE 선언된 매개변수가 하나인 경우 괄호() 를 생략할 수 있다.단, 매개변수의 타입이 있으면 괄호()를 생략할 수 없다.TO-BE a -> a * a // 올바른 예 int a -> a * a // 잘못된 예괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있다.문장의 끝에 ';'을 붙이지 않아야 한다는 것에 주의한다.AS-IS (String name, int i) -> { System.out.println(name+"="+i); } TO-BE (String name, int i) -> System.out.println(name+"="+i) 함수형 인터페이스자바에서 모든 메서드는 클래스 내에 포함되어야 하는데, 람다식은 어떤 클래스에 포함되는 것일까?람다식은 익명 클래스의 객체와 동등하다. (int a, int b) -> a > b ? a : b // 위(람다식)와 아래(익명 클래스의 객체 내부 메소드)와 같다 new Object(){ int max(int a, int b){ return a > b ? a : b; } } 그렇다면, 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을 것인가?참주변수가 있어야 객체의 메서드를 호출할 수 있으니 이 익명 객체의 주소를 f 라는 참조변수에 저장해본다.타입 f = (int a, int b) -> a > b ? a : b; // 여기서 참조변수 f의 타입은??? 참조변수 f의 타입은 어떤 것이어야 할까?참조형이니깐 클래스 또는 인터페이스가 가능하다.그리고 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.위 내용을 바탕으로 예를 들어 max()라는 메서드가 정의된 MyFunction 인터페이스가 정의되어 있다고 가정해본다. public interface MyFunction{ public abstract int max(int a, int b); } 위 인터페이스를 구현한 익명 클래스의 객체는 아래와 같이 생성가능하다. MyFunction f = new MyFunction() { @Override public int max(int a, int b) { return a > b ? a: b; } }; int big = f.max(5, 3); System.out.println(big); 여기서 MyFunction인터페이스에 정의된 메서드 max() 는람다식 '(int a, int b) → a > b ? a: b' 와 일치한다.→ 익명 객체를 담다식으로 아래와 같이 대체할 수 있다.MyFunction f = (a, b) -> a > b ? a: b; int big = f.max(5, 3); System.out.println(big); 위와 같이 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체할 수 있는 이유는람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 개맥변수 타입과 개수 그리고 반환값이 일치하기 때문이다. 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다.그렇기 때문에 인터페이스를 통해 람다식을 다루기로 결정되었으며,람다식을 다루기 위한 인터페이스를 함수형 인터페이스(functional interface)라 부르기로 했다.@FunctionalInterface public interface MyFunction{ public abstract int max(int a, int b); }단, 함수형 인터페이스에서는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다.그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다.다만, static 메서드와 default 메서드의 개수에는 제약이 없다.함수형 인터페이스로 구현한 인터페이스라면 반드시 '@FunctionalInterface' 애노테이션을 정의하도록 하자.컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해주니깐📚 바이트코드로 확인 (참고. https://dreamchaser3.tistory.com/5)람다는 익명 내부 클래스와 다르다.예제로 살펴보자.FCOnline, Readypackage me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : FCOnline * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ @FunctionalInterface interface Ready { void setup(); } public class FCOnline { public void playing(Ready ready) { ready.setup(); System.out.println("FC Online is playing"); } }FCOnlineExamplepackage me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : FCOnlineExample * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class FCOnlineExample { public static void main(String[] args) { FCOnline fcOnline = new FCOnline(); fcOnline.playing(new Ready() { @Override public void setup() { System.out.println("FC Online is ready??"); } }); } }위의 기본 예제와 람다 예제로 작성하였을 시 결과는 동일한데 어떻게 실행되고바코드가 동일한지 궁금한지 확인해보았다. 예제에서 익명클래스인 FCOnlineExample$1 새로운 클래스를 생성하여 초기화를 해주고 Ready 인터페이스를 실행하는 것과 같이 확인이 된다.컴파일 시, 익명내부클래스는 $과 같은 클래스 파일이 생긴다.익명클래스는 INVOKESPECIAL 이란 OPCODE로 생성자를 호출하고, INVOKEVIRTUAL로 Setting을 호출한다. 익명클래스 & Function Type자바에서는 왜 람다를 내부적으로 익명클래스로 컴파일하지 않을까?Java8 이전 버전에서 람다를 쓰기 위한 retrolambda 같은 라이브러리나, kotlin 같은 언어에서는 컴파일 시점에 람다를 단순히 익명클래스로 치환이 된다.다만, 익명 클래스로 사용할 경우 아래와 같은 문제가 발생할 수 있다.항상 새로운 인스턴스로 할당한다.람다식마다 클래스가 하나씩 생기게 된다.람다 예제 바이트 코드 람다예제의 바이트코드에서는 기본예제의 바이트코드와 다른점이 있었다.새로운 메서드를 static으로 생성하고 있는 부분을 볼 수 있다.중간즈음 INVOKEDYNAMIC setup() ... 이라는 구문을 볼 수 있는데 이 부분이 바로INVOKEDYNAMIC CALL → INDY 이다.INDY가 호출되게되면 bootstrap 영역의 lambdafactory.metafactory()를 수행하게 된다.lambdafactory.metafactory() : java runtime library의 표준화 method어떤 방법으로 객체를 생성할지 dynamically 를 결정한다.클래스를 새로 생성, 재사용, 프록시, 래퍼클래스 등등 성능향상을 위한 최적화된 방법을 사용하게 된다.java.lang.invoke.CallSite 객체를 return 한다.LambdaMetaFactory ~ 이렇게 되어 있는 곳의 끝에 CallSite 객체를 리턴하게 된다.해당 lambda의 lambda factory, MethodHandle을 멤버변수로 가지게 된다.람다가 변환되는 함수 인터페이스의 인터페이스를 반환한다.한번만 생성되고 재호출시 재사용이 가능하다. 📚 더 알아보기INVOKEDYNAMIC으로 구현되어 있는 이유는 여러가지가 있지만 자바의 버전이 올라갈 때 인보크 다이나믹으로 구현한 부분은 하위호환성을 유지하면서 개선할 여지를 가지고 있다.중간 정리 함수형 인터페이스 (Functional Interface)추상 메소드를 딱 하나만 가지고 있는 인터페이스SAM(Single Abstract Method) 인터페이스@FunctionalInterface 에노테이션을 가지고 있는 인터페이스 람다 표현식 (Lambda Expressions)함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다.코드를 줄일 수 있다.메소드 매개변수, 리턴파입, 변수로 만들어 사용할 수 있다.자바에서 함수형 프로그래밍함수를 First Class Object로 사용할 수 있다.순수 함수 (Pure Function)사이드 이팩트 만들 수 없다. (함수 밖에 있는 값을 변경하지 못한다.)상태가 없다. (함수 밖에 정의되어 있는)고차 함수 (High-Order Function)함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수 있다.추상 메서드 하나만 있으면 함수형 인터페이스다.MyFunction.java Interface는 추상메서드 하나만 가지고 있기 때문에 함수형 인터페이스@FunctionalInterface public interface MyFunction { int max(int a, int b); }-> @FunctionalInterface 애노테이션을 정의하고 나서 추가적인 추상 메서드를 입력하면, 컴파일 시 에러가 발생된다. (why? 함수형 인터페이스가 아니게 됨으로) 인터페이스에 static, default 메소드를 선언할 수 있다.아래와 같이 다른 형태(static, default) 메소드가 있더라도, 추상 메소드 하나만 있다면 FunctionalInterface 이다.@FunctionalInterface public interface MyFunction { int max(int a, int b); static void printNumber() { System.out.println(1); } default void printDefaultNumber() { System.out.println(0); } } 위에서 정의한 함수형 인터페이스를 이용해보자. public class App { public static void main(String[] args) { MyFunction myFunction = new MyFunction() { @Override public void printAnyThing() { System.out.println("anything"); } }; } }위의 코드는 익명내부클래스를 정의하는 방식이다.-> 람다로 변경 public class App { public static void main(String[] args) { MyFunction myFunction = () -> System.out.println("anything"); } } public class App { public static void main(String[] args) { MyFunction myFunction = () -> { System.out.println("anything"); System.out.println("Lambda"); }; myFunction.printAnyThing(); } }함수형 인터페이스를 인라인으로 구현한 오브젝트로 볼 수 있다.위 예시와 같이 구현한 자체를 Return 하거나, 메소드의 파라미터로 전달할 수도 있다.@FunctionalInterface public interface MyFunction { void printAnyThing(String name); static void printNumber() { System.out.println(1); } default void printDefaultNumber() { System.out.println(0); } }package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { MyFunction myFunction = name -> System.out.println(name); myFunction.printAnyThing("양성빈"); myFunction.printAnyThing("인프런"); } } 같은 값을 넣었을 때 같은 값이 나오는 것. pure한 함수그렇지 않으면, 함수형 프로그래밍이 X그렇지 않은 경우가 어떻게? → 함수 밖에 있는 값을 참조해서 사용하는 경우 (상태값에 의존한다는 의미) → 외부에 있는 값을 변경하려는 경우package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { MyFunction myFunction = new MyFunction() { int baseNumber = 100; @Override public void printAnyThing(String name) { baseNumber++; System.out.println(baseNumber + name); } }; } }아래와 같은 경우 참조는 할 수 있지만, 변경할 수 없다.final 이라 가정하고 사용될 수 있는 경우이다.package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { int baseNumber = 100; MyFunction myFunction = name -> System.out.println(baseNumber + name); } }  이러한 경우들은 함수형 프로그래밍과 거리가 멀다.Java에서 기본으로 제공하는 함수형 인터페이스ava.lang.function 패키지자바에서 미리 정의해둔 자주 사용할만한 함수 인터페이스Function<T, R>BiFunction<T, U, R>Consumer<T>Supplier<T>Predicate<T>UnaryOperator<T>BinaryOperator<T>....Function<T, R>값을 하나 받아서 리턴하는 일반적인 함수R apply<T>Plus.java → implements Function<Integer, Integer>Integer값을 받아서 Integer 값으로 반환하고자 함.package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : Plus * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Plus implements Function<Integer, Integer> { @Override public Integer apply(Integer integer) { return integer + 100; } } package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { int baseNumber = 100; MyFunction myFunction = name -> System.out.println(baseNumber + name); Plus plus = new Plus(); System.out.println(plus.apply(1)); } } 위와 같은 동작을 하는 함수를 Plus라는 별도 클래스 없이도 사용할 수 있다.Function<Integer, Integer> 함수형 인터페이스를 바로 구현package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { int baseNumber = 100; MyFunction myFunction = name -> System.out.println(baseNumber + name); Function<Integer, Integer> plus10 = (number) -> number + 100; System.out.println(plus10.apply(1)); } }  함수의 조합도 가능하다.compose입력값을 가지고 먼저 뒤에 오는 함수를 적용한다.그 결과값을 다시 입력값으로 사용하는 것이다.package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Function<Integer, Integer> plus10 = (number) -> number + 10; Function<Integer, Integer> multiply2 = (number) -> number * 2; System.out.println(plus10.apply(1)); System.out.println(multiply2.apply(1)); Function<Integer, Integer> multiply2AndPlus10 = plus10.compose(multiply2); System.out.println(multiply2AndPlus10.apply(2)); } }andThencompose와 반대로 먼저 적용하고 뒤에 오는 함수를 적용한다.package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Function<Integer, Integer> plus10 = (number) -> number + 10; Function<Integer, Integer> multiply2 = (number) -> number * 2; System.out.println(plus10.apply(1)); System.out.println(multiply2.apply(1)); Function<Integer, Integer> plus10AndMultiply2 = plus10.andThen(multiply2); System.out.println(plus10AndMultiply2.apply(2)); } } BiFunction<T, U, R>Function<T, R> 과 유사하지만, 입력값을 2개를 받는 것이다.(T, U) → RR apply(T t, U u) Consumer<T>리턴이 없다. | 함수 조합용 메소드 : andThenvoid Accept(T t)Consumer<T> 함수형 인터페이스 사용 예시package me.sungbin.blog; import java.util.function.Consumer; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Consumer<Integer> printT = System.out::println; printT.accept(100); } } Supplier<T>T 타입의 값을 제공해주는 함수형 인터페이스T get() Supplier<T> 사용 예시package me.sungbin.blog; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Supplier<Integer> get100 = () -> 100; System.out.println(get100.get()); } } Predicate<T>T 타입의 값을 받아서 boolean 을 반환하는 함수 인터페이스boolean test(T t) 함수 조합용 메소드And, Or, NegatePredicate<T> 사용 예시package me.sungbin.blog; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Predicate<String> startsWithYang = (str) -> str.startsWith("yang"); Predicate<Integer> isEven = (i) -> i % 2 == 0; } } UnaryOperator<T>Function<T, R>의 특수한 형태입력값 하나를 받아서 동일한 타입을 리턴하는 함수 인터페이스입력/리턴 값이 같으므로 이전의 Function<T, R> 을 아래와 같이 변경할 수 있다.package me.sungbin.blog; import java.util.function.*; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { UnaryOperator<Integer> plus10 = (number) -> number + 10; UnaryOperator<Integer> multiply2 = (number) -> number * 2; } } BinaryOperator<T>BiFunction<T, U, R> 의 특수한 형태동일한 타입의 입력값 2개를 받아서 리턴하는 함수 인터페이스3개의 타입이 다 같을 것이라는 가정으로 작성됨. 📚 자바 api에서 제공해주는 함수형 인터페이스의 별명Function별명 : 트랜스포머(변신로봇)이유 : 값을 변환하기 때문에!Consumer별명 : Spartan (스파르탄!)이유 : 모든 것을 빼앗고 아무것도 내주지 마라 !Predicate별명 : 판사이유 : 참 거짓으로 판단하기 때문Suppliers별명 : 게으른 공급자.이유 : 입력값이 존재하지 않는데, 내가 원하는 것을 미리 준비하기 때문람다(인자 리스트) → {바디}인자 리스트인자가 없을 때 : ()인자가 한개 일 때 : (one) 또는 one인자가 여러개 일 때 : (one, two)인자 타입은 생략 가능→ 컴파일러가 추론(infer)하지만 명시할 수도 있다. → (Integer one, Integer two)바디화살표 오른쪽에 함수 본문을 정의여러 줄인 경우 { } 을 사용하여 묶는다.한 줄인 경우 생략 가능, return 또한 생략 가능하다.변수 캡쳐 (Variable Capture)로컬 변수 캡쳐final이거나 effective final인 경우에만 참조할 수 있다.변수를 변경되도록 수정해보면 이것은 effective final 인지 아닌지 확인가능하다.변경이 되면 effective final 이 아니게 되며, 람다에서 사용할 수 없게 된다.그렇지 않을 경우 concurrency 문제가 생길 수 있어서 컴파일이 불가능하다.effective final이것 역시 자바 8 부터 지원하는 기능으로 "사실상" final인 변수.final 키워드 사용하지 않은 변수를 익명 클래스 구현체 또는 람다에서 참조할 수 있다.익명 클래스 구현체와 달리 "쉐도윙" 하지 않는다.익명 클래스는 새로 스콥을 만들지만, 람다는 람다를 감싸고 있는 스콥과 같다.변수 캡쳐 케이스 예.→ run() 메소드의 int baseNumber 는 IntConsumer 람다에서 참조되고 있다.package me.sungbin.blog; import java.util.function.*; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : BlogExample * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class BlogExample { public static void main(String[] args) { /** * 순수함수프로그래밍을 할려면 외부에 있는 값을 변경하거나 참조하면 안된다. 함수 내부 및 파라미터만 가지고 써야한다. */ // int baseNumber = 10; // RunSomething runSomething = number -> number + baseNumber; // System.out.println(runSomething.doIt(2)); // // UnaryOperator<Integer> plus10 = (i) -> i + 10; // UnaryOperator<Integer> multiply2 = (i) -> i * 2; // Function<Integer, Integer> multiply2AndPlus10 = plus10.compose(multiply2); // // Consumer<Integer> printT = System.out::println; // Supplier<Integer> get10 = () -> 10; // // Predicate<String> startsWithSungbin = (str) -> str.startsWith("sungbin"); // // System.out.println(plus10.apply(1)); // System.out.println(multiply2.apply(1)); // System.out.println(multiply2AndPlus10.apply(2)); // System.out.println(plus10.andThen(multiply2).apply(2)); // printT.accept(10); // System.out.println(get10.get()); // System.out.println(startsWithSungbin.test("sungbin")); UnaryOperator<Integer> plus10 = (i) -> i + 10; UnaryOperator<Integer> multiply2 = (i) -> i * 2; System.out.println(plus10.andThen(multiply2).apply(2)); Supplier<Integer> get10 = () -> 10; BinaryOperator<Integer> sum = Integer::sum; BlogExample example = new BlogExample(); example.run(); } private void run() { int baseNumber = 10; // 내부 클래스 :: 쉐도잉 가능 class LocalClass { void printBaseNumber() { int baseNumber = 11; System.out.println(baseNumber); } } // 익명 클래스 :: 쉐도잉 가능 Consumer<Integer> integerConsumer = new Consumer<Integer>() { @Override public void accept(Integer baseNumber) { System.out.println(baseNumber); } }; // 람다 IntConsumer printInt = (i) -> { System.out.println(i + baseNumber); }; printInt.accept(10); LocalClass localClass = new LocalClass(); localClass.printBaseNumber(); integerConsumer.accept(11); } }  로컬 클래스와 익명 클래스 <> 람다와 다른 점→ 쉐도윙 (가려지는 것, 덮어지는 것)로컬 클래스와 익명 클래스는 메소드 내에서 새로운 Scope 이다.→ 쉐도잉 발생-> 쉐도잉 발생 int baseNumber = 10; // 1. 로컬 클래스 class LocalClass{ void PrintBaseNumber(){ int baseNumber = 11; // baseNumber 값은 11이 찍힐 것이다. (scope) // run 메소드에서 선언한 baseNumber에 대해 쉐도잉이 발생 System.out.println(baseNumber); } } // 2. 익명 클래스 Consumer<Integer> integerConsumer = new Consumer<Integer>() { @Override public void accept(Integer baseNumber) { // 파라미터로 전달받은 baseNumber 가 찍힐 것이다. // run 메소드에서 선언한 baseNumber에 대해 쉐도잉이 발생 System.out.println(baseNumber); } }; 람다는 람다를 감싸고 있는 메소드와 같은 Scope이다. → 같은 이름의 변수를 선언할 수 없다.→ 람다에 들어있는 변수와 람다가 사용되고 있는 클래스의 변수들은 같은 Scope이다.int baseNumber = 10; // 3. 람다 IntConsumer printInt = (baseNumber) -> { System.out.println(baseNumber); };위와 같이 선언하게 되는경우 에러가 발생하게 된다.→ Variable 'baseNumber' is already defined in the scope Variable Capture의 자세한 설명 추가람다식의 실행 코드 블록 내에서 클래스의 멤버 필드와 멤버 메소드, 그리고 지역 변수를 사용할 수 있다.클래스의 멤버 필드와 멤버 메소드는 특별한 제약없이 사용 가능하지만, 지역변수를 사용함에 있어 제약이 존재한다.이 내용을 이해하기 위해서는 jvm 메모리에 대해 알아야 한다. 잠시 람다식이 아닌 다른 얘기를 해보자.멤버 메소드 내부에서 클래스의 객체를 생성해서 사용할 경우 다음과 같은 문제가 있다.익명 구현 객체를 푸함해서 객체를 생성할 경우 new 라는 키워드를 사용한다.new라는 키워드를 사용한다는 것은 동적 메모리 할당 영역(이하 heap)에 객체를 생성한다는 의미이다.이렇게 생성된 객체는 자신을 감싸고 있는 멤버 메소드의 실행이 끝난 이후에도 heap영역에 존재하므로 사용할 수 있지만, 이 멤버 메소드에 정의된 매개변수나 지역 변수는 런타임 스택 영역(이하 stack)에 할당되어 메소드 실행이 끝나면 해당 영역에서 사라져 더 이상 사용할 수 없게 된다.그렇기 때문에 멤버 메소드 내부에서 생성된 객체가 자신을 감싸고 있는 메소드의 매개변수나 지역변수를 사용하려 할 때 문제가 생길 수 있다.클래스의 멤버 메소드의 매개변수와 이 메소드 실행 블록 내부의 지역변수는 JVM의 STACK에 생성되고 실행이 끝나면 STACK에서 사라진다.new 연산자를 사용해서 생성한 객체는 JVM의 HEAP영역에 객체가 생성되고 GC(Garbage Collector)에 의해 관리되며, 더 이상 사용하지 않는 객체에 대해 필요한 경우 메모리에서 제거한다.heap에 생성된 객체가 stack의 변수를 사용하려고 하는데, 사용하는 시점에 stack에 더 이상 해당 변수가 존재하지 않을 수 있다는 것이다.왜냐하면 stack은 메소드 실행이 끝나면 매개변수나 지역변수에 대해 제거하기 때문이다.그래서 더 이상 존재하지 않는 변수를 사용하려 할 수 있기 떄문에 오류가 발생한다.→ 자바는 이 문제를 Variable Capture 라고 하는 값 복사를 사용해서 해결한다.즉, 컴파일 시점에 멤버 메소드의 매개변수나 지역변수를 멤버 메소드 내부에서 생성한 객체가 사용할 경우 객체 내부로 값을 복사해서 사용한다.하지 모든 값을 복사해서 사용할 수 있는 것은 아니다.여기에도 제약이 존재하는데 final 키워드로 작성되었거나 final 성격을 가져야 한다.final 키워드는 알겠는데 final 성격을 가져야한다는 것은 왜그럴까?final 성격을 가진다는 것은 final 키워드로 선언된 것은 아니지만 값이 한번만 할당되어 final 처럼 쓰이는 것을 뜻한다.복잡한 내용과 예제가 존재하지만, 쉽게 생각한다면 익명 구현 객체를 사용할 때와 람다식을 사용했을 때 다음과 같은 차이점이 있다는 것만이라도 기억해보자.람다식은 익명 구현 객체 처럼 별도의 객체를 생성하거나 컴파일 결과 별도의 클래스를 생성하지 않는 다는 것이다.람다식 내부에서 사용하는 변수는 Variable Capture가 발생하며, 이 값은 final이거나 final처럼 사용해야 한다는 것이다.익명 구현 객체에 대해서는 new를 사용해서 객체도 생성된 것으로 보이고 별도 클래스 파일이 생긴 것을 확인할 수 있을 것이다.람다식이 쓰인 부분에서는 INVOKEDYNAMIC 이라는 OPCODE를 사용했는데, JAVA8 부터 생긴 것으로 interface의 default method와 lambda 식에서 사용된다고 한다.메소드 레퍼런스람다가 하는 일이 기준 메소드 또는 생성자를 호출하는 것이라면, 메소드 레퍼런스를 사용해서 매우 간결하게 표현할 수 있다. 메소드 참조하는 방법스태틱 메소드 참조 → 타입::스태틱 메소드특정 객체의 인스턴스 메소드 참조 → 객체 래퍼런스::인스턴스 메소드임의 객체의 인스턴스 메소드 참조 → 타입::인스턴스 메소드생성자 참조 → 타입::new메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다.리턴값 또는 생성한 객체는 람다의 리턴 값이다. package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : Eating * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Eating { private String food; public Eating() { } public Eating(String food) { this.food = food; } public String eat(String food) { return "eating " + food; } public static String keepEat(String food) { return "keep eating " + food; } } Function<T, R> 을 이용해 구현 가능하지만, 동일한 작업을 하는 Greeting 객체의 메소드를 활용하여 아래와 같이 작업해볼 수 있다.메소드 레퍼런스 -> Eating::keepEatpackage me.sungbin.blog; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { UnaryOperator<String> keepEatFunction = (s) -> "hi " + s; UnaryOperator<String> keepEatingObject = Eating::keepEat; System.out.println(keepEatingObject.apply("햄버거")); } }인스턴스 메서드 사용package me.sungbin.blog; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Eating eating = new Eating(); UnaryOperator<String> printEat = eating::eat; System.out.println(printEat.apply("햄버거")); } }  생성자 사용Supplier를 이용한 것과 Function을 이용한 생성자 호출은 엄연히 다르다. Supplier는 인자가 없고 Function은 인자가 있다.사용하는 부분인 메소드 레퍼런스만 보면 "Eating::new" 와 동일하지만 다르다는 점!package me.sungbin.blog; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { // 입력값은 없는데 반환값은 있는 함수형 인터페이스 > Supplier Supplier<Eating> newEating = Eating::new; Eating eating = newEating.get(); } }package me.sungbin.blog; import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { // 입력값 T 를 받아 R 반환 함수형 인터페이스 > Function Function<String, Eating> hamburgerEating = Eating::new; Eating hamburger = hamburgerEating.apply("햄버거"); } } 임의의 객체를 참조하는 메소드 레퍼런스package me.sungbin.blog; import java.util.Arrays; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { String[] names = {"A", "B", "C", "D"}; Arrays.sort(names, String::compareToIgnoreCase); System.out.println(Arrays.toString(names)); } }람다식의 타입과 형변환 정리!함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐이지 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.람다식은 익명 객체이고 익명 객체는 타입이 없다.정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다.그래대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요하다. MyFunction f = (MyFunction)(() -> {...});람다식은 MyFunction 인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생략가능하다.람다식은 이름이 없을 뿐 분명히 객체인데도, Object 타입으로 형변환할 수 없다.람다식은 오직 함수형 인터페이스로만 형변환이 가능하다. Object obj = (Object)( () -> { ... }); // ERROR. 함수형 인터페이스로만 가능하다. 굳이 변경하고자 한다면, 함수형 인터페이스로 변환하고 난 후 가능하다.다음 예제를 통해 컴파일러가 람다식의 타입을 어떤 형식으로 만들어내는지 알아보자. package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : MyFunction02 * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ @FunctionalInterface public interface MyFunction02 { void myMethod(); }package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { MyFunction02 f = () -> {}; Object obj = (MyFunction02) (() -> {}); String str = ((Object) (MyFunction02) (() -> {})).toString(); System.out.println(f); System.out.println(obj); System.out.println(str); System.out.println((MyFunction02) (() -> {})); System.out.println(((Object) (MyFunction02) (() -> {})).toString()); } } 일반적인 익명 객체라면, 객체의 타입이 외부클래스이름$번호 와 같은 형식으로 타입이 결정되었을텐데, 람다식의 타입은 외부클래스이름$Lambda$번호 와 같은 형식으로 되어 있는 것을 확인할 수 있다. INDY 상세한 정리!INVOKEDYNAMIC 의 내부 동작 분석Java SE7 부터 등장한 새로운 바이트코드 셋이다.기존에는 invoke 시리즈는 4가지만 존재하였다. (invoke + virtual/static/interface/special)invokevirtual : instance 메소드를 디스패치 하기 위한 명령어invokestatic : static 메소드를 디스패치 하기 위한 명령어invokeinterface : 인터페이스를 통해서 method를 디스패치 하기 위한 명령어invokespecial : 생성자, 수퍼클래스, private method등 invoke-virtual이 아닌 메소드들을 디스패치 하기 위한 명령어저 명령어들을 보면 어느 한 메소드를 부르기 위해서는 아래와 같은 4가지 메소드 정보가java에 명확하게 선언되어 있어야 한다.메소드의 이름메소드 시그니처 + return type메소드가 정의되어 있는 클래스메소드를 실행할 수 있는 바이트 코드.즉, 메소드를 부르기 위해서는 위와 같은 정보들이 자바 lang을 통해 고정되어 있고 이미 모든 것이 구현되어 있고 컴파일까지 완벽하게 되어 있어서 JVM에서 바이트코드를 읽기만 하면 된다.Java SE7 에서는 언어의 로직들이 컴파일러가 번역해준 메소드 실행 로직이 아닌, 런타임에서 메소드를 실행할 CALL TARGET을 정할 수 있도록 INVOKEDYNAMIC이라는 바이트코드를 추가하였다. Java 코드로 작성된 Lambda랑 INVOKEDYNAMIC 과 무슨 상관일까?invokedynamic의 예시를 보면, 마치 다양한 언어를 JVM 위에서 돌릴 수 있도록 추가된 바이트코드처럼 보인다.람다는 Java로 쓰여져있고, 람다를 통해 생성된 바이트코드를 보면 모두 컴파일 시점에 자바 메소드로 재분해(desugar) 확인할 수 있다.그렇다면 람다가 굳이 invokedynamic을 사용하도록 컴파일되는 이유는 무엇일까? 특정 translation strategy (람다를 → 가용가능한 자바 로직으로 변환하는 과정 및 전략) 에는 두가지 고려사항이 존재한다.미래의 최적화를 위해서 특정 전략으로 고정하지 않는 것. (즉, 최대한 바이트코드로 고정되어 컴파일 되지 않게 하는 것)특정 translation strategy (실제 자바 실행 로직으로 변환하는 과정 및 전략)을 런타임에서 결정할 수 있다. 따라서, 전략이 미래에 변해서 JVM 스팩이 업데이트 되었다고 하더라도 소스코드 수정이나, 재컴파일 없이 그대로 실행 가능하다.만약 람다가 컴파일 타임에서 완벽하게 변환되었다고 한다면, 나중에 수정사항이 있을 경우 프로젝트를 모두 재컴파일 해야 하는 일이 발생한다.클래스 파일 표현에 안정성을 가지는 방법람다는 두가지의 이점을 가지기 위해,'람다라는 표현 () → {}' 을 '실제 자바 메소드 실행 로직' (바이트코드로 나온 실행 메소드)로 컴파일 타임이 아닌 INVOKEDYNAMIC 을 통해서 런타임에서 로직을 연결하고 실행한다.이를통해서, 실제 람다 표현을 실행하는 로직을 결정하는 전략을 런타임에서 LAZY 하게 셜정할 수 있다는 것이 장점이라고 생각된다. Invokedynamic + lambda 실행public class SimpleLambda{ public static void main(String[] args){ Runnable lambda = invokedynamic( bootstrap = LambdaMetafactory, staticargs = [Runnable, lambda$0], dynargs=[]); lambda.run(); } private static void lambda$0(){ System.out.println(1); } }📚 참고: https://d2.naver.com/helloworld/4911107?fbclid=IwAR2KrFe7ksfRr4cDQWWGqvFpQDB6B4MVCh_zlMjZFZh5NVD5KSWHg8nV46U invokedynamic()Bootstrap Method (or BSM)부트스트랩 메소드는 invokedynamic() + bootstrap method()를 통해 들어온 정보를 기반으로, 호출 대상을 찾아서 연결하고 CallSite 객체를 반환한다.CallSite가 반환되기 전을 unlaced 상태(실제 로직이 연결되지 않은 상태)라고 하며, invokedynamic() 콜이 제대로 불려 실제로 linkage가 일어나면 **laced(연결된) 상태**라고 불린다.VM이 bootstrap method를 부를 때에, CallSite를 lazy 하게 반환하며, 반환 후에는 CallSite를 통해 람다와 실제 함수 구현 부분이 연결된다.연결을 한번 한 뒤에는 계속 연결상태가 되기 때문에, 따로 링킹을 시도하지 않는다.Lambda는 LambdaMetafactory.metafactory 라는 부트스트랩 메소드를 부른다.1-1). CallSite & MethodHandle이 CallSite 객체에는 실제 메소드를 실행시킬 메소드 (아까 바이트코드에서 봤던 private static method) 포함되어 있다.📚 참조 https://www.slideshare.net/DanHeidinga/invokedynamic-evolution-of-a-language-feature CallSite 가 멤버변수로 가지고 있는 실제 로직을 가리키고 있는 메소드는 MethodHandle로 표현된다.아래의 코드 예제를 보면, 일반적으로 Reflection API를 통해 메소드를 Method 객체를 얻은것과 비슷해 보이지만 훨씬 더 VM 레벨에서 작동하는 메소드이다. public MethodHandle getToStringMH() { MethodHandle mh = null; // 메소드 타입 부터 첫번째 파라미터는 Return type, 두번째부터는 메소드 파라미터들.. MethodType mt = MethodType.methodType(String.class); // 메소드 handles의 lookup() = "lookup context" MethodHandles.Lookup lk = MethodHandles.lookup(); try { // mh를 통해 toString메소드를 찾음. mh = lk.findVirtual(getClass(), "toString", mt); } catch (NoSuchMethodException | IllegalAccessException mhx) { throw (AssertionError)new AssertionError().initCause(mhx); } return mh; } 2) staticargs, dynargsstaticargs : 바이트 코드로 변환된 람다 실제 메소드 정보dynargs : 람다 스코프 외 외부변수 참조 시, dynargs에 포함됨. invokedynamic 이 부트스트랩 메소드를 여러 인자 요소들(메소드 정보, 메소드 구현체 정보, 타입 정보 등..)과 함께 부른다. BootstrapMethod는 CallSite를 반환한다. CallSite는 target method의 정보를 가지고 있는 method handler를 멤버 변수로 가지고 있고, 이 method handler를 통해 실제 동작하는 메소드와 연결된다.즉, lambda로 인해 컴파일 시점에 추가된 static/instance 함수 (예 : private static void lambda$0)를 LambdaMetaFactory가 MethodHandler를 통해 관리해주고 있는 형태로 볼 수 있다.Stream 소개Streamsequence of elements supporting sequential and parallel aggregate operations데이터를 담고 있는 저장소(컬렉션)이 아니다.Functional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.→ Functional 하다.→ 결과가 또다른 stream이 되는 것이지, 전달받은 데이터 자체가 변경되는 것이 아니다.스트림으로 처리하는 데이터는 오직 한번만 처리한다.→ 컨베이어 밸트에 항목이 한번 지나가는 것이라고 보면된다. (한번 지나면 끝)무제한일 수도 있다. (Short Circuit 메소드를 사용해서 제한할 수 있다.)중개 오퍼레이션은 근본적으로 lazy 하다.→ stream에 사용하는 것은 2개로 나눌 수 있다. (중개, 종료)→ 중개 오퍼레이션은 lazy.. ???→ 중개형 오퍼레이션은 종료 오퍼레이션이 오기 전까지 실행되지 않는다. package me.sungbin.blog; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("손흥민"); names.add("박지성"); names.add("이강인"); names.add("김민재"); names.add("조규성"); names.stream().map(x -> { System.out.println(x); return x.toUpperCase(Locale.ROOT); }); System.out.println("=========="); names.forEach(System.out::println); } }.map() 바디에 선언한 System.out.println 은 찍히지 않는 것을 볼 수 있다.종료형 오퍼레이션이 반드시 한번 와야 하며, 종료형 오퍼레이션이 오지 않으면 중계형 오퍼레이터는 의미가 없다. (실행x) 손쉽게 병렬 처리 할 수 있다.병렬처리를 하는 것이 모두 빠른 것이 아니다. 더 느릴 수 있다. Thread를 만들어서 Thread별로 병렬로 처리하고 수집하는 일련의 과정이 발생된다.데이터가 정말 방대하게 큰 경우 유용하게 사용될 수 있으나, 그게 아니라면 stream 권장Stream Pipline0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션(terminal operation)으로 구성한다.스트림의 데이터 소스는 오직 터미널 오퍼레이션을 실행할 때만 처리한다.중개 오퍼레이션Stream을 리턴한다.Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다.대부분 Stateless 지만 distinct나 sorted 처럼 이전 소스 데이터를 참조해야 하는 오퍼레이션은 Stateful 오퍼레이션이다.filter, map, limit, skip, sorted ...종료 오퍼레이션Stream을 리턴하지 않는다.collect, allMatch, count, forEach, min, maxStream API 걸러내기Filter(Predicate)예) 이름이 3글자 이상인 데이터만 새로운 스트림으로변경하기Map(Function) 또는 FlatMap(Function)예) 각각의 Post인스턴스에서 String Title만 새로운 스트림으로예) List<STream<String>> 을 String의 스트림으로생성하기generate(Supplier) 또는 Iterate(T seed, UnaryOperator)예) 10부터 1씩 증가하는 무제한 숫자 스트림예) 랜덤 int 무제한 스트림제한하기limit(long) 또는 skip(long)예) 최대 5개의 요소가 담긴 스트림을 리턴한다.예) 앞에서 3개를 뺀 나머지 스트림을 리턴한다.package me.sungbin.blog; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { List<Blog> developBlogs = new ArrayList<>(); developBlogs.add(new Blog(1L, "자바", "스트림 API", true)); developBlogs.add(new Blog(1L, "mysql", "롤백", true)); developBlogs.add(new Blog(1L, "spring boot", "DI", false)); developBlogs.add(new Blog(1L, "spring data jpa", "트랜잭션", false)); developBlogs.add(new Blog(1L, "spring security", "cors", false)); System.out.println("spring 으로 시작하는 블로그"); System.out.println("공개 블로그"); System.out.println("블로그 이름만 만들어서 스트림 만들기"); } } 1. spring으로 시작하는 수업 만들기System.out.println("spring 으로 시작하는 수업"); List<Blog> blogsByTitleIsSpringStartWith = developBlogs.stream() .filter(x -> x.getTitle().startsWith("spring")) .collect(Collectors.toList()); blogsByTitleIsSpringStartWith.forEach(x -> System.out.println(" > " + x.getTitle())); 2. secret되지 않는 블로그System.out.println("공개 블로그"); List<Blog> notSecretBlog = developBlogs.stream() .filter(x -> !x.isSecret()) .collect(Collectors.toList()); notSecretBlog.forEach(x -> System.out.println(" > " + x.getTitle())); 3. 블로그 이름만 모아서 스트림 만들기List<String> blogsByTitle = developBlogs.stream() .map(Blog::getTitle) .collect(Collectors.toList()); blogsByTitle.forEach(x -> System.out.println(" > " + x));연습 https://www.inflearn.com/course/the-java-java8public class StreamApp { public static void main(String[] args) { List<OnlineClass> springClasses = new ArrayList<>(); springClasses.add(new OnlineClass(1, "spring boot", true)); springClasses.add(new OnlineClass(2, "spring data jpa", true)); springClasses.add(new OnlineClass(3, "spring mvc", false)); springClasses.add(new OnlineClass(4, "spring core", false)); springClasses.add(new OnlineClass(5, "rest api development", false)); List<OnlineClass> javaClasses = new ArrayList<>(); javaClasses.add(new OnlineClass(6, "The Java, Test", true)); javaClasses.add(new OnlineClass(7, "The Java, Code manipulation", true)); javaClasses.add(new OnlineClass(8, "The Java, 8 to 11", false)); List<List<OnlineClass>> ssonEvents = new ArrayList<>(); ssonEvents.add(springClasses); ssonEvents.add(javaClasses); System.out.println("두 수업 목록에 들어있는 모든 수업 아이디 출력"); // todo System.out.println("10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만"); // todo System.out.println("자바 수업 중에 Test가 들어있는 수업이 있는지 확인"); // todo System.out.println("스프링 수업 중에 제목에 spring이 들어간 것만 모아서 List로 만들기"); // todo } } 1. 두 수업 목록에 들어있는 모든 수업 아이디 출력리스트를 항목으로 갖고 있는 것을 Flat 하게 변형한다. → 안에 있는 것들을 다 꺼낸다(?)FlatMap → 모든 항목들을 풀어내는 것System.out.println("두 수업 목록에 들어있는 모든 수업 아이디 출력"); ssonEvents.stream().flatMap(Collection::stream) .forEach(oc -> System.out.println(oc.getId())); 2. 10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만Stream.iterator → skip, limitSystem.out.println("10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만"); Stream.iterate(10, i -> i + 1) .skip(10) .limit(10) .forEach(System.out::println); 3. 자바 수업 중에 Test가 들어있는 수업이 있는지 확인Match (any, all ,...)System.out.println("자바 수업 중에 Test가 들어있는 수업이 있는지 확인"); boolean isTestClasses = javaClasses.stream().anyMatch(x -> x.getTitle().contains("Test")); System.out.println("isTestClasses : " + isTestClasses);4. 스프링 수업 중에 제목에 spring이 들어간 타이틀만 모아서 List로 만들기System.out.println("스프링 수업 중에 제목에 spring이 들어간 제목만 모아서 List로 만들기"); List<String> springTitleClasses = springClasses.stream() .filter(x -> x.getTitle().contains("spring")) .map(OnlineClass::getTitle) .collect(Collectors.toList()); springTitleClasses.forEach(x -> System.out.println(" > " + x));🛠 마무리이렇게 익명클래스, 람다, 함수형 프로그래밍, @FunctionalInterface, 스트림 API, 메서드 레퍼런스에 대해 알아보고 깊게 파보았다.그러면 질문에 대해 대답해보자. Q.자바의 람다식은 왜 등장했을까?자바의 람다식은 함수형 프로그래밍을 자바에 통합하기 위해 자바 8에서 도입됐다. 이는 한 개의 추상 메소드를 가진 인터페이스를 간결하게 구현할 수 있게 해주며, 코드 양을 줄이고 가독성을 향상시키는 이점을 제공한다. 람다식과 익명 클래스는 모두 익명 함수를 구현하는 방식이지만, 람다식은 더 간결한 문법과 함수형 인터페이스의 직접적 사용을 가능하게 한다. 람다식의 도입으로 자바에서도 함수형 프로그래밍 패러다임을 효율적으로 적용할 수 있게 됐다.람다식은 자바 8에서 처음 소개됐으며, 함수형 프로그래밍 개념을 자바에 도입하기 위한 목적으로 만들어졌다. 익명 클래스에 비해 람다식은 코드를 더 간결하게 만들고, 함수형 인터페이스를 이용해 메소드를 보다 직관적으로 표현할 수 있다. 람다식은 (매개변수) -> { 표현식 } 형태로 작성되며, 이를 통해 인터페이스의 구현체를 더욱 간단하게 작성할 수 있다.함수형 인터페이스는 단 하나의 추상 메소드를 가지는 인터페이스로, @FunctionalInterface 어노테이션을 사용해 명시적으로 정의할 수 있다. 이 인터페이스는 람다식을 통해 구현될 수 있으며, 자바는 java.util.function 패키지를 통해 다양한 함수형 인터페이스를 제공한다. 이를 통해 개발자는 보다 함수적인 프로그래밍 방식을 자바에서도 적용할 수 있게 됐다.람다식의 도입은 자바의 내부적인 동작 방식에도 영향을 미쳤다. 람다식은 invokedynamic 바이트코드 명령을 사용해 동적으로 메소드 타입과 메소드 핸들을 결정한다. 이는 람다식이 실행될 때마다 인터페이스의 메소드 호출이 아니라, 실제로 실행되는 함수형 인터페이스의 구현체를 동적으로 생성하고 호출하는 메커니즘을 가능하게 한다. 이 과정은 자바 가상 머신(JVM)의 성능 최적화와 밀접하게 관련되어 있으며, 람다식을 통한 함수형 프로그래밍의 효율적인 실행을 지원한다.Q. 람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?문법은 위에서 설명을 했으니 위의 질문 혹은 상단 블로그에 참조바랍니다.람다식과 익명 클래스 모두 인터페이스의 구현체를 생성하는데 사용될 수 있지만, 람다식은 함수형 인터페이스에 한정된다는 점에서 차이가 있다. 람다식은 문법적으로 더 간결하며, 코드를 더 읽기 쉽게 만들어 준다. 반면, 익명 클래스는 여러 메소드를 오버라이드해야 할 때 또는 함수형 인터페이스가 아닌 경우에 여전히 유용하다.람다식은 자바의 함수형 프로그래밍 패러다임을 강화하는데 기여했으며, 익명 클래스보다 더 간결하고 표현력 있는 코드 작성을 가능하게 한다. 하지만, 익명 클래스는 람다식으로 대체할 수 없는 경우에 여전히 그 가치를 지닌다. 예를 들어, 여러 메소드를 구현해야 하거나, 슈퍼 클래스의 생성자를 호출해야 하는 경우 익명 클래스를 사용해야 한다.람다식의 도입으로 자바 개발자들은 보다 함수적인 접근 방식을 취할 수 있게 되었고, 이는 자바 프로그래밍 언어의 발전에 중요한 역할을 했다. 익명 클래스와 람다식은 각각의 사용 사례에 따라 선택적으로 사용되어, 자바 프로그래밍의 유연성을 높이는 데 기여한다.📚 참조https://inf.run/XKQg)https://inf.run/r9oUhttps://dreamchaser3.tistory.com/5https://d2.naver.com/helloworld/4911107?fbclid=IwAR2KrFe7ksfRr4cDQWWGqvFpQDB6B4MVCh_zlMjZFZh5NVD5KSWHg8nV46Uhttps://www.slideshare.net/DanHeidinga/invokedynamic-evolution-of-a-language-featurehttps://www.inflearn.com/course/the-java-java8#https://tourspace.tistory.com/11?category=788398

백엔드인프런워킹업스터디클럽람다함수형프로그래밍

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - API 실습 (Day2)

API 실습벌써 2번째 미션을 진행할 차례가 되었다. 강의 중에 GET, POST API 개발을 해보았고 포스트맨으로 테스팅도 해보았다.또한 실제 프로젝트처럼 ui가 존재하는 화면과 연동하는 유저 생성 및 조회 API를 개발하면서 뭔가 실무를 체험하는 것과 같은 느낌이 들었다. 하지만, 아직 조금 부족하다고 많이 느끼게 되었다. 또한 많은 연습이 필요하다고 느꼈다. 그런데 마침 코치님께서 친절하게 미션을 통하여 API 연습을 하게 도와주셨다. 😆 그럼 미션을 통하여 나의 코드를 글로 표현해보겠다. 문제1요구조건해결과정당연하겠지만 스프링 프로젝트를 만든다. 나는 IntelliJ Ultimate를 사용하고 있는 관계로 start.spring.io를 통하여 프로젝트를 생성하지 않고 직접 인텔리제이를 통하여 프로젝트를 생성할 수 있다. 아래는 프로젝트를 세팅한 화면이다.controller 패키지 생성 후, 문제1에 대한 컨트롤러 클래스 생성package me.sungbin.mission.controller; public class MissionController { }API를 만들기 위해 코드를 작성한다. 나는 아래와 같이 작성하였다.package me.sungbin.mission.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1") public class MissionController { }📚 문제1~3까지 제시된 api는 /api/v1/으로 시작한다. 따라서 @RequestMapping을 통하여 공통된 api url부분을 제시해준다. 문제 1에 대한 API를 정의해야 한다. 제시된 조건은 /api/v1/calc의 path를 가지며, 쿼리 파리미터로 num1과 num2를 가진다. 이에 따라 정의를 해볼려고 한다. 그런데 문제는 응답하는 값이 json 형태로 반환되므로 DTO 객체를 통하여 반환하도록 하자. 그러면 DTO 응답 객체부터 만들자. DTO 응답 객체는 다음과 같다.package me.sungbin.mission.dto.response; public class CalculationResponseDto { private final int add; private final int minus; private final int multiply; public CalculationResponseDto(int add, int minus, int multiply) { this.add = add; this.minus = minus; this.multiply = multiply; } public int getAdd() { return add; } public int getMinus() { return minus; } public int getMultiply() { return multiply; } }  롬복을 통하여 생서자와 getter를 만들 수도 있고, JDK17 이상부터는 record를 이용하여 만들 수도 있다.하지만, 미션의 취지와 강의에 설명한 데로 생성해보겠다. 💡 record를 통하여 DTO 생성package me.sungbin.mission.dto.response; public record CalculateResponseRecordDto(int add, int minus, int multiply) { @Override public int add() { return add; } @Override public int minus() { return minus; } @Override public int multiply() { return multiply; } }  parameter를 객체를 통하여 전달주려고 한다. 물론 @RequestParam을 통하여 전달줄 수 있다. 아래와 같이 DTO 요청 객체를 만들었다.  package me.sungbin.mission.dto.request; public class CalculationRequestDto { private final int num1; private final int num2; public CalculationRequestDto(int num1, int num2) { this.num1 = num1; this.num2 = num2; } public int getNum1() { return num1; } public int getNum2() { return num2; } }  그리고 컨트롤러 클래스를 마저 작성하면 아래와 같다.package me.sungbin.mission.controller; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.response.CalculationResponseDto; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1") public class MissionController { @GetMapping("/calc") public CalculationResponseDto calculate(CalculationRequestDto requestDto) { return new CalculationResponseDto( requestDto.getNum1() + requestDto.getNum2(), requestDto.getNum1() - requestDto.getNum2(), requestDto.getNum1() * requestDto.getNum2() ); } } 그리고 포스트맨으로 테스트를 해보니 아래와 같이 에러가 발생한다. 그래서 에러 내용을 보니 아래와 같다. ⚠ 트러블 슈팅그래서 대체 이유가 뭘까 고민을 하다가 name 속성을 줘서 풀어보니 정상동작을 하였다. 그래서 이런 문제는 공식문서에 있을법해서 구글링 및 공식문서 이슈사항을 보았다. 위의 공식문서에서 업데이트 기록에 나와있었다. Spring Boot 3.2에서 사용되는 Spring Framework 버전은 더 이상 바이트코드를 구문 분석하여 매개변수 이름을 추론하려고 시도하지 않습니다. 즉, 스프링 부트 3.2부터 자바 컴파일러에 -parameters 옵션을 넣어주어야 애노테이션의 이름을 생략할 수 있다. 또 한 가지 방법으로는 gradle을 사용해 빌드를 하고 실행하는 방법이 있다. 나는 Build and run using를 IntelliJ IDEA로 선택하였습니다. (체감상 Gradle보단 빨라서...) Gradle로 선택한 경우에는 Gradle이 컴파일 시점에 해당 옵션을 자동으로 적용해준다. 그래서 인텔리제이의 세팅에 Build, Execution, Deployment > Build Tools > Gradle 을 들어가서 아래 세팅처럼 Gradle로 변경한다. 초기세팅은 Gradle이다. 나처럼 IntelliJ로 변경한 사람만 적용하면 된다. 그 후에 다시 실행하면 정상적으로 결과가 나온다. 결과 확인리펙토링이제 컨트롤러에 있는 비즈니스 로직을 좀 더 리팩토링해보자. 여기서 든 생각은 더하기, 빼기, 곱하기 로직은 다른 클래스로 분리하면 좋을 것 같다는 생각이 들었다. 🤔 먼저 서비스 패키지를 구성하고 서비스 클래스를 만들어보자. 그리고 거기다가 로직을 추가해보자. package me.sungbin.mission.service; import me.sungbin.mission.dto.request.CalculationRequestDto; import org.springframework.stereotype.Service; @Service public class CalculationService { /** * 더하기 로직 * @param requestDto * @return */ public int add(CalculationRequestDto requestDto) { return requestDto.getNum1() + requestDto.getNum2(); } /** * 빼기 로직 * @param requestDto * @return */ public int minus(CalculationRequestDto requestDto) { return requestDto.getNum1() - requestDto.getNum2(); } /** * 곱하기 로직 * @param requestDto * @return */ public int multiply(CalculationRequestDto requestDto) { return requestDto.getNum1() * requestDto.getNum2(); } }  다음으로 컨트롤러 코드를 수정하자. package me.sungbin.mission.controller; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.response.CalculationResponseDto; import me.sungbin.mission.service.CalculationService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1") public class MissionController { private final CalculationService calculationService; public MissionController(CalculationService calculationService) { this.calculationService = calculationService; } @GetMapping("/calc") // GET /api/v1/calc public CalculationResponseDto calculate(CalculationRequestDto requestDto) { return new CalculationResponseDto(calculationService.add(requestDto), calculationService.minus(requestDto), calculationService.multiply(requestDto)); } }  그리고 포스트맨으로 실행해보면 정상적으로 결과가 나온다.  테스트 코드그러면 포스트맨으로 테스팅을 해보았지만, 테스트 코드를 통해 확실한 검증을 가보자.다만, 테스트 코드는 실패하는 로직과 성공하는 로직을 작성해야하지만 이번 문제는 성공하는 로직만 작성해보겠다.또한 비즈니스 로직은 단순 연산이므로 통합테스트로 과정설명없이 아래와 같이 작성했다. package me.sungbin.mission.controller; import me.sungbin.mission.dto.request.CalculationRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc class MissionControllerTest { @Autowired private MockMvc mockMvc; @Test @DisplayName("문제 1번 통합 테스트 - 성공") void calculate_test_success() throws Exception { CalculationRequestDto calculationRequestDto = new CalculationRequestDto(10, 5); this.mockMvc.perform(get("/api/v1/calc") .param("num1", String.valueOf(calculationRequestDto.getNum1())) .param("num2", String.valueOf(calculationRequestDto.getNum2()))) .andDo(print()) .andExpect(status().isOk()); } }결과는 아래와 같다. 문제2요구사항문제풀이기본적으로 IDE 열고 프로젝트 세팅은 생략하겠다.컨트롤러 클래스에 경로를 지정해주기 전에, 응답객체부터 먼저 만들어보자.응답객체는 아래와 같다. package me.sungbin.mission.dto.response; public class DayOfTheWeekResponseDto { private final String dayOfTheWeek; public DayOfTheWeekResponseDto(String dayOfTheWeek) { this.dayOfTheWeek = dayOfTheWeek; } public String getDayOfTheWeek() { return dayOfTheWeek; } } 다음으로 컨트롤러 코드의 비즈니스 로직 부분을 작성해보자. package me.sungbin.mission.controller; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.response.CalculationResponseDto; import me.sungbin.mission.dto.response.DayOfTheWeekResponseDto; import me.sungbin.mission.service.CalculationService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; import java.time.format.TextStyle; import java.util.Locale; @RestController @RequestMapping("/api/v1") public class MissionController { private final CalculationService calculationService; public MissionController(CalculationService calculationService) { this.calculationService = calculationService; } @GetMapping("/calc") // GET /api/v1/calc public CalculationResponseDto calculate(CalculationRequestDto requestDto) { return new CalculationResponseDto(calculationService.add(requestDto), calculationService.minus(requestDto), calculationService.multiply(requestDto)); } @GetMapping("/day-of-the-week") // GET /api/v1/day-of-the-week public DayOfTheWeekResponseDto findDayOfTheWeek(@RequestParam LocalDate date) { return new DayOfTheWeekResponseDto(date.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase()); } }  결과 확인🙋🏻 처음에는 당황했다. 분명 제대로 로직이 갔는데 예시랑 다르기 때문이다. 하지만 달력을 확인해도 2023년 01월 01일은 일요일이 맞다! 📚 나는 문제를 단순 속성이 1개이기 때문에 단순 타입으로 받았지만 만약에 단순 타입이 아니라 객체로도 넘길 수 있다. @DateTimeFormat : 객체로 받을 시, 필드에다가 이 어노테이션을 붙여주고 패턴을 지정해줘야 한다. 왜냐하면 스프링의 기본 날짜/시간 파싱 규칙은 LocalDate의 경우 ISO 형식(예: yyyy-MM-dd)을 사용합니다. 따라서, 클라이언트 요청이 이 형식을 따른다면 @DateTimeFormat 어노테이션이 없어도 문제없이 파싱될 수 있습니다. 다만, 아래의 경우에 문제가 발생한다.날짜 형식 불일치: 클라이언트가 다른 형식(예: dd-MM-yyyy)을 사용하여 데이터를 보내면, 스프링은 이를 올바르게 파싱하지 못하고 오류를 반환합니다.명확성 부족: @DateTimeFormat 어노테이션을 사용하지 않으면, API를 사용하는 클라이언트 개발자들이 요구되는 정확한 날짜 형식을 명확하게 알 수 없습니다. 이는 API의 사용성을 저하시킬 수 있습니다.그러면 객체로 받는 예시도 보여주겠다. DTOpackage me.sungbin.mission.dto.request; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; public class DayOfTheWeekRequestDto { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate date; public DayOfTheWeekRequestDto(LocalDate date) { this.date = date; } public LocalDate getDate() { return date; } }  Controllerpackage me.sungbin.mission.controller; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.request.DayOfTheWeekRequestDto; import me.sungbin.mission.dto.response.CalculationResponseDto; import me.sungbin.mission.dto.response.DayOfTheWeekResponseDto; import me.sungbin.mission.service.CalculationService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; import java.time.format.TextStyle; import java.util.Locale; @RestController @RequestMapping("/api/v1") public class MissionController { private final CalculationService calculationService; public MissionController(CalculationService calculationService) { this.calculationService = calculationService; } @GetMapping("/calc") // GET /api/v1/calc public CalculationResponseDto calculate(CalculationRequestDto requestDto) { return new CalculationResponseDto(calculationService.add(requestDto), calculationService.minus(requestDto), calculationService.multiply(requestDto)); } @GetMapping("/day-of-the-week") // GET /api/v1/day-of-the-week public DayOfTheWeekResponseDto findDayOfTheWeek(DayOfTheWeekRequestDto requestDto) { return new DayOfTheWeekResponseDto(requestDto.getDate().getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase()); } }  리팩토링이제 비즈니스 로직을 서비스 클래스에 넣어서 좀 더 리팩토링 해보자. validation도 적용할려나 @DateTimeFormat을 이용하면 스프링에서 알아서 TypeMismatchException을 발생시켜준다. 따라서 @RestControllerAdvice를 이용하여 할 수 있다. /** * 요일 찾기 비즈니스 로직 * @param requestDto * @return */ public String findDayOfTheWeek(DayOfTheWeekRequestDto requestDto) { return requestDto.getDate().getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase(); }위의 코드는 요일 찾기 로직을 서비스 클래스에 옮긴 것이다. 다음으로 컨트롤러 클래스를 아래와 수정하자. package me.sungbin.mission.controller; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.request.DayOfTheWeekRequestDto; import me.sungbin.mission.dto.response.CalculationResponseDto; import me.sungbin.mission.dto.response.DayOfTheWeekResponseDto; import me.sungbin.mission.service.CalculationService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; import java.time.format.TextStyle; import java.util.Locale; @RestController @RequestMapping("/api/v1") public class MissionController { private final CalculationService calculationService; public MissionController(CalculationService calculationService) { this.calculationService = calculationService; } @GetMapping("/calc") // GET /api/v1/calc public CalculationResponseDto calculate(CalculationRequestDto requestDto) { return new CalculationResponseDto(this.calculationService.add(requestDto), calculationService.minus(requestDto), calculationService.multiply(requestDto)); } @GetMapping("/day-of-the-week") // GET /api/v1/day-of-the-week public DayOfTheWeekResponseDto findDayOfTheWeek(DayOfTheWeekRequestDto requestDto) { return new DayOfTheWeekResponseDto(this.calculationService.findDayOfTheWeek(requestDto)); } }📚 LocalDate 참고아래의 코드는 블로그를 통하여 유용한 LocalDate 함수를 사용한 것이다. 확인해보자.// 특정 날짜의 요일 구하기 LocalDate.of( 2022, 12, 12 ).getDayOfWeek(); // MONDAY // 특정 날짜로부터 일, 월, 주, 연 차이 나는 날짜 구하기 localDate.minusDays( 5 ); // @param long daysToSubtract localDate.minusMonths( 5 ); // @param long daysToSubtract localDate.minusWeeks( 5 ); // @param long daysToSubtract localDate.minusYears( 5 ); // @param long daysToSubtract // 특정 날짜로부터 몇 일 이후 날짜 구하기 ( 위와 유사 ) localDate.plusDays( 7 ); // @param long amountToAdd // 특정 날짜가 해당하는 주의 특정 요일 일자 구하기 localDate.with( DayOfWeek.FRIDAY); // 2022-12-16 ( @param DayOfWeek ) // 특정 날짜에서 특정 부분만 바꾸기 LocalDate localDate = LocalDate.now(); // 2022-12-12 localDate.withDayOfMonth( 31 ); // 2022-12-31 ( @param int dayOfMonth ) localDate.withMonth( 1 ); // 2022-01-12 ( @param int month ) localDate.withYear( 2023 ); // 2023-12-12 ( @param int year ) // 윤년 여부 localDate.isLeapYear(); // false // 해당 월의 첫째 날 구하기 localDate.withDayOfMonth( 1 ); // 해당 월의 마지막 날 구하기 localDate.withDayOfMonth( localDate.lengthOfMonth() ); // 두 날짜 사이의 간격 구하기 LocalDate start = LocalDate.of( 2021, 10, 1 ); LocalDate end = LocalDate.of( 2022, 12, 31 ); Period diff = Period.between( start, end ); diff.getYears(); // 1 diff.getMonths(); // 2 diff.getDays(); // 30 // ChronoUnit 을 이용한 두 날짜 사이 간격 구하기 long diffMonth = ChronoUnit.MONTHS.between( start, end ); // 14 long diffWeek = ChronoUnit.WEEKS.between( start, end ); // 65 long diffDay = ChronoUnit.DAYS.between( start, end ); // 456 테스트 코드이번엔 테스트 코드를 작성하자. 성공과 실패 케이스 둘 다 작성해보겠다.package me.sungbin.mission.controller; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.request.DayOfTheWeekRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc class MissionControllerTest { @Autowired private MockMvc mockMvc; @Test @DisplayName("문제 1번 통합 테스트 - 성공") void calculate_test_success() throws Exception { CalculationRequestDto calculationRequestDto = new CalculationRequestDto(10, 5); this.mockMvc.perform(get("/api/v1/calc") .param("num1", String.valueOf(calculationRequestDto.getNum1())) .param("num2", String.valueOf(calculationRequestDto.getNum2()))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 2번 통합 테스트 - 성공") void find_day_of_the_week_test_success() throws Exception { DayOfTheWeekRequestDto requestDto = new DayOfTheWeekRequestDto(LocalDate.of(2023, 1, 1)); this.mockMvc.perform(get("/api/v1/day-of-the-week") .param("date", String.valueOf(requestDto.getDate()))) .andDo(print()) .andExpect(status().isOk()); } }결과를 보자. 문제3문제풀이이제는 익숙해졌을거라 보고 최종 코드만 확인해보겠다. DTOpackage me.sungbin.mission.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; public class ListNumberDataRequestDto { private final List<Integer> numbers; public ListNumberDataRequestDto(@JsonProperty("numbers") List<Integer> numbers) { this.numbers = numbers; } public List<Integer> getNumbers() { return numbers; } }  Controllerpackage me.sungbin.mission.controller; import jakarta.validation.Valid; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.request.DayOfTheWeekRequestDto; import me.sungbin.mission.dto.request.ListNumberDataRequestDto; import me.sungbin.mission.dto.response.CalculationResponseDto; import me.sungbin.mission.dto.response.DayOfTheWeekResponseDto; import me.sungbin.mission.service.CalculationService; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; import java.time.format.TextStyle; import java.util.Locale; @RestController @RequestMapping("/api/v1") public class MissionController { private final CalculationService calculationService; public MissionController(CalculationService calculationService) { this.calculationService = calculationService; } @GetMapping("/calc") // GET /api/v1/calc public CalculationResponseDto calculate(CalculationRequestDto requestDto) { return new CalculationResponseDto(this.calculationService.add(requestDto), calculationService.minus(requestDto), calculationService.multiply(requestDto)); } @GetMapping("/day-of-the-week") // GET /api/v1/day-of-the-week public DayOfTheWeekResponseDto findDayOfTheWeek(DayOfTheWeekRequestDto requestDto) { return new DayOfTheWeekResponseDto(this.calculationService.findDayOfTheWeek(requestDto)); } @PostMapping("/sum-of-numbers-in-list") public Integer sumOfNumbersInList(@RequestBody @Valid ListNumberDataRequestDto requestDto) { return requestDto.getNumbers().stream().mapToInt(Integer::intValue).sum(); } }📚 비즈니스 로직 부분은 Java8에 나온 Stream API와 메서드 레퍼런스를 이용하여 만들었다. 이 API는 다음 미션때 자세히 보도록 하겠다. ⚠ 또한 나처럼 DTO의 필드를 final로 설정하면 생성자 부분에 @JsonProperty를 빼고 진행하면 에러가 발생한다.Cannot construct instance of me.sungbin.mission.dto.request.ListNumberDataRequestDto (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator) 트러블 슈팅이 오류 메시지는 스프링 부트와 Jackson 라이브러리가 ListNumberDataRequestDto 클래스의 인스턴스를 JSON 데이터로부터 역직렬화할 때 발생한다. 오류 메시지는 ListNumberDataRequestDto에 기본 생성자가 없거나, Jackson이 JSON 데이터를 객체의 필드에 매핑하기 위해 사용할 수 있는 적절한 생성자나 세터 메서드가 없음을 나타낸다. 이 경우, 클래스에는 파라미터를 받는 생성자만 정의되어 있으며, final 키워드로 선언된 numbers 필드 때문에 수정자(setter) 메서드를 추가할 수 없다. 따라서 Jackson이 객체를 역직렬화할 때 사용할 수 있는 "속성 기반 생성자"를 제공하기 위해, 생성자 파라미터에 @JsonProperty 어노테이션을 사용할 수 있다. 이 방법은 Jackson에게 JSON 데이터의 어떤 필드가 클래스 생성자의 어떤 파라미터와 매핑되는지 명확하게 지시한다. 아니면 final 키워드를 없애는 방법이 있다. 나는 이 예시를 보이기 위해 의도적으로 이렇게 작성하겠다. 결과를 보자.  리팩토링비즈니스 로직을 서비스 클래스에 옮기자. /** * 배열의 합 구하는 로직 * @param requestDto * @return */ public Integer sumOfNumbersInList(ListNumberDataRequestDto requestDto) { return requestDto.getNumbers().stream().mapToInt(Integer::intValue).sum(); }컨트롤러 코드도 수정하자. package me.sungbin.mission.controller; import jakarta.validation.Valid; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.request.DayOfTheWeekRequestDto; import me.sungbin.mission.dto.request.ListNumberDataRequestDto; import me.sungbin.mission.dto.response.CalculationResponseDto; import me.sungbin.mission.dto.response.DayOfTheWeekResponseDto; import me.sungbin.mission.service.CalculationService; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; import java.time.format.TextStyle; import java.util.Locale; @RestController @RequestMapping("/api/v1") public class MissionController { private final CalculationService calculationService; public MissionController(CalculationService calculationService) { this.calculationService = calculationService; } @GetMapping("/calc") // GET /api/v1/calc public CalculationResponseDto calculate(CalculationRequestDto requestDto) { return new CalculationResponseDto(this.calculationService.add(requestDto), calculationService.minus(requestDto), calculationService.multiply(requestDto)); } @GetMapping("/day-of-the-week") // GET /api/v1/day-of-the-week public DayOfTheWeekResponseDto findDayOfTheWeek(DayOfTheWeekRequestDto requestDto) { return new DayOfTheWeekResponseDto(this.calculationService.findDayOfTheWeek(requestDto)); } @PostMapping("/sum-of-numbers-in-list") public Integer sumOfNumbersInList(@RequestBody @Valid ListNumberDataRequestDto requestDto) { return this.calculationService.sumOfNumbersInList(requestDto); } }  그리고 마지막으로 validation을 추가하자!물론 서비스 클래스에 아래와 같은 로직을 넣을 수 있지만 if (requestDto.getNumbers() == null || requestDto.getNumbers().isEmpty()) { throw new IllegalArgumentException("리스트는 공란이거나 null일 수 없습니다."); } 좀 편하게 spring-boot-starter-validation을 이용하여DTO 클래스를 변경해보자. package me.sungbin.mission.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; public class ListNumberDataRequestDto { @NotEmpty(message = "리스트의 적어도 하나의 원소가 존재해야 합니다.") @NotNull(message = "리스트는 null일 수 없습니다.") private final List<Integer> numbers; public ListNumberDataRequestDto(@JsonProperty("numbers") List<Integer> numbers) { this.numbers = numbers; } public List<Integer> getNumbers() { return numbers; } }  테스트 코드이번에는 테스트 실패와 성공케이스 둘다 적어보자. 실패@Test @DisplayName("문제 3번 통합 테스트 - 실패 (빈 List)") void sum_of_the_list_numbers_test_fail_caused_by_list_empty() throws Exception { List<Integer> list = new ArrayList<>(); ListNumberDataRequestDto requestDto = new ListNumberDataRequestDto(list); this.mockMvc.perform(post("/api/v1/sum-of-numbers-in-list") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("문제 3번 통합 테스트 - 실패 (null인 List)") void sum_of_the_list_numbers_test_fail_caused_by_list_null() throws Exception { List<Integer> list = null; ListNumberDataRequestDto requestDto = new ListNumberDataRequestDto(list); this.mockMvc.perform(post("/api/v1/sum-of-numbers-in-list") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); }결과 (1,2번 실패 테스트는 response 동일)성공package me.sungbin.mission.controller; import com.fasterxml.jackson.databind.ObjectMapper; import me.sungbin.mission.dto.request.CalculationRequestDto; import me.sungbin.mission.dto.request.DayOfTheWeekRequestDto; import me.sungbin.mission.dto.request.ListNumberDataRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc class MissionControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test @DisplayName("문제 1번 통합 테스트 - 성공") void calculate_test_success() throws Exception { CalculationRequestDto calculationRequestDto = new CalculationRequestDto(10, 5); this.mockMvc.perform(get("/api/v1/calc") .param("num1", String.valueOf(calculationRequestDto.getNum1())) .param("num2", String.valueOf(calculationRequestDto.getNum2()))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 2번 통합 테스트 - 성공") void find_day_of_the_week_test_success() throws Exception { DayOfTheWeekRequestDto requestDto = new DayOfTheWeekRequestDto(LocalDate.of(2023, 1, 1)); this.mockMvc.perform(get("/api/v1/day-of-the-week") .param("date", String.valueOf(requestDto.getDate()))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 3번 통합 테스트 - 실패 (빈 List)") void sum_of_the_list_numbers_test_fail_caused_by_list_empty() throws Exception { List<Integer> list = new ArrayList<>(); ListNumberDataRequestDto requestDto = new ListNumberDataRequestDto(list); this.mockMvc.perform(post("/api/v1/sum-of-numbers-in-list") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("문제 3번 통합 테스트 - 실패 (null인 List)") void sum_of_the_list_numbers_test_fail_caused_by_list_null() throws Exception { List<Integer> list = null; ListNumberDataRequestDto requestDto = new ListNumberDataRequestDto(list); this.mockMvc.perform(post("/api/v1/sum-of-numbers-in-list") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("문제 3번 통합 테스트 - 성공") void sum_of_the_list_numbers_test_success() throws Exception { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); ListNumberDataRequestDto requestDto = new ListNumberDataRequestDto(list); this.mockMvc.perform(post("/api/v1/sum-of-numbers-in-list") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } } 결과📚 참조자바의 정석https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes

백엔드인프런워밍업스터디클럽백엔드API

[1일차] 인프런 워밍업 스터디 클럽 0기- BE

[질문]어노테이션을 사용하는 이유 (효과) 는 무엇일까?나만의 어노테이션은 어떻게 만들 수 있을까?1일차에 위와 같은 질문을 받았습니다.어노테이션을 사용하는 이유는 어노테이션 하나로 마법과 같은 일이 일어나기 때문이라고 하셨습니다. ( 강의 중에.. )예를 들면 @PostMapping의 경우 이 어노테이션 하나로 http통신 방법을 정의할 수 있습니다.아래는 @PostMapping 어노테이션의 일부 코드입니다. ( 솔직히 어떻게 작동하는지 모르겠습니다 )@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @RequestMapping(method = RequestMethod.POST) public @interface PostMapping { /** * Alias for {@link RequestMapping#name}. */ @AliasFor(annotation = RequestMapping.class) String name() default ""; /** * Alias for {@link RequestMapping#value}. */ @AliasFor(annotation = RequestMapping.class) String[] value() default {}; /** * Alias for {@link RequestMapping#path}. */ @AliasFor(annotation = RequestMapping.class) String[] path() default {}; /** * Alias for {@link RequestMapping#params}. */ @AliasFor(annotation = RequestMapping.class) String[] params() default {}; 그럼 이제 나도 마법같은 나만의 어노테이션을 만들어 볼까? 라는 생각이 들었습니다.@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AnnotationCal { int[] numbers() default {}; }어떤 어노테이션을 만들어 볼까 하다가 배열을 넣으면 계산을 해주는 어노테이션을 만들기로 하였습니다.위와 같이 어노테이션을 선언 하였습니다. 간단하게 numbers 배열로 만들었습니다.public class AnnotationUtil { static int calculateSumForAnnotation(Class<?> clazz, String methodName) throws NoSuchMethodException { int sum = 0; Method method = clazz.getMethod(methodName); AnnotationCal annotationCal = method.getAnnotation(AnnotationCal.class); for (int number : annotationCal.numbers()) { sum += number; } return sum; } }어노테이션 유틸에 덧셈 계산을 하는 로직을 만들어 주었습니다. 근데 @PostMapping의 경우는 저런 구현체가 없던데...@RestController public class AnnotationCalculator { @GetMapping("/annotationSum") @AnnotationCal(numbers = {1, 2, 3, 4, 5}) public int annotationSum() throws NoSuchMethodException { return AnnotationUtil.calculateSumForAnnotation(getClass(), "annotationSum"); } }위와 같이 어노테이션 배열로 숫자를 넣어 주면 자동으로 합계를 구해주도록 만들었습니다!!위 API를 호출하면 15가 출력됩니다![ 깃 허브 ]여기서 더 궁금한 점이 생겼습니다. 위에서 말한 구현체 관련된 부분입니다.스프링 어노테이션의 경우 위와같이 어노테이션을 불러서 로직을 만들어 주는 부분이 어디 있는지 모르겠습니다. 저처럼 어딘가에 만들어 놓지 않았다면 절대 동작할 일이 없습니다.저도 어노테이션만 선언하면 사용자는 세부 로직을 모르고 단순히 사용만 가능하도록 로직을 만들어 보고 싶습니다.그렇지만 저는 검색 바보라서 그런 것을 찾는 것이 너무 어렵습니다ㅠㅠ아시는 분은 좀 알려주세요~ 1일차 과제 끝 ~~

백엔드

jhc 10일 전
코딩웍스(Coding Works)

[필독] 웹디자인 기능사 실기시험 학습을 처음 시작하는 분들

안녕하세요. 코딩웍스입니다.웹디자인 기능사 실기시험 학습을 처음 시작하는 분들이 자주 질문하시는 것들이 있어서 블로그에 정리하려고 합니다.웹디자인 기능사 실기시험은 실무를 위한 학습과는 거리가 있기 때문에 시험에 꼭! 필요한 부분만 준비하고 학습하셔야 합니다. 1) 비주얼 스튜디오 코드(Visual Studio Code) 사용법 및 환경설정 웹디자인 기능사 실기시험 학습을 위해 1~4번 중에서 1)번과 2)번만 학습하시면 됩니다.2)번은 모두 확실히 학습합니다.1)번에서는 비주얼 스튜디오 코드 사용법은 모두 확실히 학습합니다. 비주얼 스튜디오 코드 환경설정에서 자동저장 기능 활성화 하기는 체크하고 연습하세요.환경설정 부분은 하실 필요없습니다. 왜냐면 실기 시험장에 가면 초기화 되어 있어서 환경설정을 다시 해야 합니다. 만약 환경설정을 하고 싶다면 단축키 설정 정도만 하시면 됩니다. 실기 시험장에서 단축키 설정은 몇개 안되니 1~2분이면 충분하니까 작업 속도를 위해서 단축키 설정은 하시면 좋습니다.웹디자인 기능사 실기시험 학습을 위해 3)번과 4번)은 학습하실 필요 없습니다. 시험장은 인터넷이 안되는 환경이므로 익스텐션을 설치할 수 없으니 익스텐션을 사용해서 학습하시면 실기시험과 환경이 달라서 혼란스러울 수 있습니다. 3)번은 나중에 시험 끝나고 퍼블리싱을 더 학습해야 할 경우 학습합니다. 

웹 개발웹디자인기능사실기시험