블로그

코드캠프

Github Copoilot vs 주니어 개발자 (feat. 뭐야 내 직업 돌려줘요)

Github Copilot에 대한 의견이 분분하다는 걸 목격한 코캠, 지난 블로깅에서는 토론에 참여해 의견을 끼워넣어봤습니다.코캠 내부의 토론 결과, '주니어는 대체될 수도 있을 것 같다.' 라는 의견이 많았습니다.이 토론 결과에 동공이 흔들리는 주니어 바로 나야나... (내 직업 못잃어...)코드캠프 선배님들은 거침없이 왜 주니어가 대체 될 수 있는지에 대해 말씀을 시작하고,(그만..그만 말해..)주니어 막냉이는 오랜만에 심장에 떨림이 느껴지기 시작하고. 나 떨고있니..직업이 한순간 백수가 될까 불안에 떨던 저는 마음 먹었습니다.copilot을 직접 사용해보고 정말 날 대체할 수 있나 판단하기로.비교대상인 저는 코드캠프 2년차 주니어 개발자이며 언어는 javascript를 사용하고 있습니다.스택은 코드캠프 스택 + a 정도의 스택을 가지고 있습니다. 자, 그럼 [ 코파일럿 vs 주니어 개발자 ] 비교 가 보 자 구 💡주니어의 Copoilot 사용기1️⃣ 코파일럿 사용 세팅하기 [ Docs ]1 ) 깃허브 settings에 들어갑니다.2) 왼쪽의 사이드 바 'code,planning, and automation' 섹션에서 copilot을 클릭합니다.3) copilot 설정을 해줍니다.* 60일동안은 무료지만, 이후부터는 유료 서비스로 전환됩니다.그 전에 구독 취소하시면 비용을 지불하지 않아도 됩니다.모두 입력 후 설정값을 저장해주시면 됩니다!4) vsCode에 Github Copilot이라는 extension을 설치합니다.5) 설치하고 나면 깃허브 로그인,권한부여 등을 묻는 창이 작게 뜰텐데 로그인과 권한부여에 모두 동의해주시면 됩니다.6) 다 했는데 안될때는?설정 버튼을 눌러 [ 확장 설정 ]을 눌러주세요.위의 버튼을 눌러 setting.json 파일에 들어가 copilot 사용설정을 해주세요! 2️⃣ copilot VS 주니어 개발자⛔️ 본격적으로 주니어 개발자와 copilot의 비교에 앞서, 이 견해는 코드캠프의 주니어 개발자의 의견일 뿐 모두를 대표하지 않습니다. 비교를 위해 주니어 개발자인 저와 코파일럿이 같은 기능을 만들어 보기로 했습니다.기능 : unix 시간을 자바스크립트 Date객체로 변환해 화면에 예쁘게 출력하기[ 주니어 개발자 ]만드는 데 걸린 시간 : 30분직접 코딩시 장단점장점 : 구조를 내 마음대로 그때 그때 바꾸면서 사용할 수 있다.단점 : 모르는 파트가 나오면 구글링 + 구현시간이 꽤나 걸린다.[ 코파일럿 ]위 사진은 코파일럿이 만들어준 기능입니다.너무 신기하게도 제가 직접 작성했던 코드를 러닝해서 비슷하게 진행되면 제가 적었던 코드를 추천 합니다.위의 주석처리 된 함수는 제가 함수 이름만 만들어도 전에 적었던 함수를 추천해줬습니다.그럼 주석으로 한줄씩 만드는 데는 차이가 있을까하여 주석묘사로도 만들어봤는데, 주석도 추천해줍니다.(너무 신기)만드는데 걸린시간 : 5분신기하게도 내가 적은 코드도 러닝을 해서 내 코드를 추천해줌..한글 주석도 된다코파일럿으로 코딩시 장단점장점 : 누구보다 빠르게 코딩이 가능함단점 : 본인 실력이 아니기때문에 코드수정이 필요할 때 난감할 수 있음.기본 틀 자체를 추천하기 때문에 수정에 불리함.그 기능 만들거 아닌데 설레발로 자동완성 해주려고 하니까 불편함. 주니어 개발자의 전반적인 평가평가는 개인적인 견해이기 때문에 주니어 By 주니어지만, 제가 사용해본 코파일럿은 생각보다 똑똑했습니다.언어를 선택할 수 있다고 하길래 당연히 JS 에서만 자동완성이 되는 줄 알았는데, HTML 코드에서도 자동완성을 추천해줍니다.HTML 코드 추천은 제가 위에 적어놓은 코드+내용 바탕으로 추천하는 것 같은데 생각보다 정확합니다.사용하면서 계속 들었던 생각은 '어..이게 되네?' 와 '어..생각보다 똑똑한데...?' 입니다.물론 제가 복잡한 프로젝트내에서 적용하게 아니고, 간단한 기능을 만들었기 때문에 그럴 수 있지만 간단한 함수들을 만드는데는 전혀 무리가 없어보입니다.또한 깃허브에서 만든거라 주석묘사시 영문으로만 입력해야하나 걱정했는데, 한글로도 작성이 가능합니다.코파일럿을 이용해서 회사 내부 템플릿을 만들수도 있겠다는 생각이 드는게, 주석을 자세히 달고 해당 주석을 그대로 입력하면 템플릿을 복사해오지 않을까 싶습니다.이렇게 되면 공통 컴포넌트를 작업하는 시니어는 코파일럿을 만지고, 주니어는 크게 할 일이 없어질 수도 있을 것 같습니다. 결론은 아주 훗날, 라이선스 문제도 해결되고 회사에서 상용화 된다면 정말 주니어 개발자의 입지가 좁아질 수 있을 것 같다는 생각이 들 정도로 잘 됩니다. 그럼, 주니어 개발자는 어떤 걸 더 공부하고 성장해야 코파일럿에게 밀리지 않고 개발자 수명을 연명해 나갈 수 있을까요?(직업을 잃을 순 없잖아요)주니어 개발자가 공부하면 너~무 좋은 기술! 다음 블로그에서 만나보도록 하시죠!모두 코캠과 함께 공부하고 성장하는 개발자가 되어 직업을 잃지 않기로해요. 약속~👍🏻 

웹 개발코파일럿주니어대체GithubCopliot개발자대체프론트엔드백엔드코드캠프

코드캠프

입문자도 가넝한 8주만에 개발자 되는 법

안녕하세요! 실무 코딩부트캠프, 코드캠프입니다 :)날씨가 조금씩 따뜻해지면서 식곤증이 부쩍 늘어나고 있지 않나요?(전지적 컴퓨터 시점)그래서 코드캠프가 눈이 번쩍! 뜨이는(👀) 강의 업데이트 소식을 준비해왔어요!특히 비전공이지만 개발 커리어에 관심이 있는 분들이라면, 주목해주셔도 좋아요 :)코딩을 몰라도(NEW!) 2개월만에 개발자가 되는 '관리형' 코딩 부트캠프인프런에 입성한지 얼마 되지 않았지만 뜨거운 관심과 애정 어린 피드백을 받으며, 더 나은 컨텐츠를 제공하기 위해 코드캠프도 열심히 성장 중인데요. (정말 감사합니다)그 중 사전 기초 지식 없이는 부트캠프 합류가 어려워 신청을 망설였던 분들을 많이 보았어요.그래서 상담 후 등록을 완료해주신 모든 분들께,[시작은 프리캠프], [강력한 CSS], [훈훈한 Javascript] 기초 강의를 무료로 제공합니다!사전 지식을 필요로 하시는 분들은 웹 개발의 기초를 습득하시고사전 지식이 이미 있으신 분들은 기초 내용을 복습하면서 튼튼하게 다져보세요 :)⚠️ 단, 기초 강의 수강 기간은 부트캠프 기간(8주)에 포함되지 않으므로 등록하신 기수의 개강 일정에 맞춰 수강해 주셔야 합니다!(MBTI가 P인 분들)개강 전 기초 강의 수강 계획을 짜기 힘드신 분들은 언제든 코드캠프에 문의해주세요!개강일에 맞춰 수강할 수 있도록 체계적인 시간표를 제공해드리겠습니다 😊👉 2기 일정 : 03.06 - 04.28 (선착순 모집) ▶ 20% 할인 중!👉 3기 일정 : 04.03 - 05.29 (선착순 모집) ▶ 20% 할인 중!

웹 개발웹개발프론트엔드백엔드부트캠프비전공자JavascriptNode.jsReactNext.jsNest.js

조성호

[인프런 비즈니스] 교육담당자의 개발직무 파헤치기: 백엔드편

안녕하세요, 인프런 비즈니스팀 호야 입니다😀 혹~싀!개발/프로그래밍 관련 용어들이 외계어처럼 느껴지는 분들 계시나요?!(제가 사실 그랬습니다,,허허허) 개발/프로그래밍 관련 용어들이 낯설고, 개발 직무가 여전히 생소한 우리 교육 담당자님들을 위해 준비했어요!앞으로 연재형 포스팅으로 백엔드부터 데이터 사이언스까지 개발 직무와 관련된 모든 것들을 낱낱이 파헤쳐 볼 생각입니다 :) 자! 그럼 이제 다들 인프런 비즈니스팀의 성장일기 열차에 탑승하실 준비가 되셨나요~?준비가 되셨다면 백엔드부터 본격적으로 파헤쳐 보는 성장일기 열차 출발하도록 하겠습니다!! 출~바알! 본격 백엔드 파헤치기백엔드란? 말 그대로! "뒷단"이란 뜻인데요!사용자가 볼 수 없는 영역인 데이터베이스나 서버를 관리하는 분야를 말합니다.예시로 쉽게 설명해드리면, 우리가 로그인할 때 정보를 확인하고, 일치 여부를 결정하는 역할을 하기도 하구요!상품을 구매할 때, 주문을 처리하고 결제를 완료하는 것도 백엔드의 영역이라고 생각하시면 됩니다😀 그렇다면! 백엔드에서 가장 많이 쓰이는 대표적인 스킬태그는 뭐가 있을까요?! 백엔드 대표 스킬태그Spring: Java 백엔드 개발에 떼어놓을 수 없는 Java 기반 오픈소스 경량급 애플리케이션 프레임워크 *프레임워크란? 목적 달성을 위해 복잡하게 얽혀있는 문제 해결 구조로 소프트웨어 개발 뼈대 역할*오픈소스란? 모든 개인 및 기업에게 무료로 오픈되어 있는 것을 뜻함*경량급이란? 기존에 사용하던 기술들과 비교하여, 상대적으로 코드가 단순함을 뜻함 Spring Boot: Spring으로 애플리케이션을 만들 때 필요한 설정을 간편하게 처리해주는 별도의 프레임워크*URL 이 길어서 'URL 줄이기' 를 사용하는 것과 같은 맥락이랍니다 😀Java: 무려 1995년에 개발된 객체 지향 프로그래밍 언어로, 수백만 개에 달하는 엔터프라이즈 소프트웨어에 활용되고 있는 백엔드 개발에 근간이 되는 언어 MVC: Model View Controller의 약자로, 소프트웨어 개발에서 흔히 사용되는 설계 패턴*마치 문과생에게 있어 PPT/기획서 작성 패턴과 동일한 느낌이랄까요 😀JPA: Java Persistence API의 약자로, 현재 자바 진영의 ORM 기술 표준(인터페이스 모음)*API는 한 번 쯤 들어보셨을 수도 있는데, 어렸을 때 갖고 놀던 다마고치 / 팬들럼과 비슷하다고 생각하시면 돼요!(서로 다른 기기를 붙이면 연동돼서 같이 놀 수 있었던 그 때 그 갬성이랄까요,,ㅋㅋㅋ)*API란? 각각 다른 종류의 소프트웨어를 연결시켜주는 서비스*ORM이란? Object Relational Mapping의 약자로, 애플리케이션과 데이터베이스를 개발 언어로 연결시켜주는 툴지금까지 가장 대표적인 다섯 가지 백엔드 스킬태그에 대해 살펴보았는데요!굉장히 생소한 용어들이 많이 들어가있죠😅아무래도 전공자가 아니라면 백엔드에 대해 모르시는게 어찌 보면 당연한 거라고 생각해요!다만, 이번 기회를 통해 조금이나마 백엔드에 대한 기초 지식을 쌓을 수 있었다는 것 자체로너무 귀중한 시간이 되지 않았나 생각이 듭니다!앞으로 우리 교육 담당자님들이 IT교육 커리큘럼을 기획하고, 개발자분들과 원활히 소통하시는데 조금이나마 도움이 될 수 있도록 개발직무 파헤치기 컨텐츠를 계속적으로 연재할테니까요! 많은 관심 부탁드립니다 😀우리 함께 배우고, 나누고, 성장해요!교육담당자님을 위한 Tip! (개발자에게 아는 척하면 붐업킹 되는 추천 강의)Spring 추천 로드맵: 우아한형제들 최연소 기술이사 출신 김영한의 스프링 완전 정복Java 추천 로드맵: 누적 수강생 30만명 최다 학습 지식공유자, 김영한의 실전자바Spring Boot 와 JPA 추천 로드맵: 스프링 부트와 JPA 실무 완전 정복

경영인프런비즈니스비즈니스인프런기업교육기업교육DT교육DX교육IT교육개발교육교육컨텐츠백엔드

코드캠프

Github Copilot, 진짜 개발자 대체가 가능할까?

요즘 Github Copilot에 관해 여러가지 의견들이 충돌하며 의견이 분분하다는 것을 본 코캠.어떤분들은 '미래에 개발자는 Copilot이 대체할 거다' 라는 의견이 있는가 하는 반면, '치와와랑 머핀도 구분 못하는 AI가 어떻게 대체하냐,아직 미흡하다.' 라는 의견도 있었습니다.이런 토론은 개발자로서 굉장히 참을 수 없는 의견대립이죠.그래서 코캠측에서도 슬그머니 의견을 끼워 넣어보기 위해 Github Copilot에 대해 알아보았습니다. 🛠 Github Copoilot?깃허브 코파일럿은 내가 원하는 기능을 주석으로 묘사하면, 묘사에 맞는 기능을 자동으로 완성시켜주는 자동 코딩 시스템입니다.Copilot 빠르게 시작하기1️⃣ 작동방식1. 내가 원하는 기능을 주석으로 묘사합니다.2. 코파일럿 AI가 딥러닝 한 내용을 바탕으로 '대부분 이렇게 쓰던데?' 하는 코드들을 완성합니다. 2️⃣ Github Copoilot의 장단점[ 장점 ]웬만큼 연차가 쌓인 개발자가 아니고서야 라이브 코딩을 하는 개발자는 생각 보다 많이 없습니다.주니어 개발자의 대부분은 다른 사람들이 쓴 코드를 참고하고 긁어와 사용하는 경우가 더 많죠.이런 부분에 있어서는 코파일럿이 도움이 될 수 있습니다.[ 단점 ]코파일럿의 학습이 완벽하지 않기 때문에 개발자의 의도가 정확하게 컴퓨팅 사고를 기반으로 제시되지 않으면, AI는 갈 길을 잃고 의도와 다른 코드를 제시할 수 있습니다.개발자의 의도와 다른 코드는 결국 불필요한 코드를 늘리는 것과 같기 때문에 비효율적일 수 있습니다. 3️⃣ Copoilot의 궁극적인 문제?copoilot의 궁극적 문제는 라이선스 문제가 되지 않을까 싶습니다.코파일럿의 AI가 어떤 라이선스인가를 따지지 않고 학습하기 때문에 뱉어낸 결과물에 제한된 라이선스 코드가 있다면 해당 코파일럿 코드 또한 제한되어야 하는지, 적용한다면 어느 범주까지 적용해야 하는지 애매한 부분이 있다고 합니다.  ❓ 그래서 진짜 개발자 대체가 가능해?여기부터는 코드캠프 일부 개발자들의 의견으로 반박시 여러분들 의견이 맞습니다.😁코드캠프에게 여러분들의 의견을 알려주세요! 코캠측 개발자들의 의견을 정리해보았는데요, 생각보다 코캠 내부에서도 파가 나뉘었습니다!🧑🏻‍💻 백엔드 개발자들Captain( 팀 내 그저 빛을 맡고 계신 9년차 풀스택 개발자 )- 설계를 하는 시니어들은 대체 불가능, 단순 업무를 하는 주니어는 대체 가능.틀을 설계하는 건 인공지능이 발전해도 인간의 창의성까지 가지고 올 수 없는 부분이 있기 때문에 무리라고 생각.Quokka- 비슷한 거 써봤는데 대체 안됨.( 일단 돈을 안냈음. - 무료판 유저 )Otter- 회사의 도입이 대중화 되느냐에 따라 다를 것 같은데, 주니어는 대체 가능하지만 로직의 틀을 짜야 하는 시니어는 대체가 불가하다.Bommy- 상용화 시기가 중요하다고 생각, 과도기동안은 주니어도 대체가 안되지만 상용화 된 이후에는 주니어는 대체가 가능하다고 생각.🧑🏻‍💻 프론트엔드 개발자들Eunny- 주니어, 시니어 모두 대체가 불가하다.공부는 가능하겠지만, 모든 코드는 각자 코드 상황에 따라 다르게 적용되기 때문에 대체 불가.Hoony( 공상과학에 빠져있는 디지털노마드 선두주자 )- 주니어, 시니어 모두 대체가 가능하다. ( 터미네이터와 아이언맨 자비스를 너무 감명 깊게 봄. - AI가 인류를 대체할 수 있다 주의 )시간이 흐를 수록 데이터는 누적될 것이고 대부분의 코드 설계가 가능한 수준까지 올라 갈 수 있을 것, 이를 통해 인건비 절감을 위해 개발자 보다 AI를 선호하는 상황이 생길 수 있다.Jenny- 주니어 정도는 코파일럿으로 대체가 가능하다. 하지만 코파일럿 설계를 해야 하는 엔지니어나 서비스의 큰 틀을 짜야하는 시니어는 대체가 불가능하다. 큰 틀은 언제나 상황에 따라 짜야 하는데 통상적인 부분으로는 커버 불가능함.Gee- 코파일럿이 상용화되어서 많은 사람이 사용하게 된다면 나중에는 코파일럿이 작성한 코드가 트렌드를 반영한 정석 로직으로 여겨지는 날이 올 것 같음. 하지만 상황에 맞게 배치하고 개선하는 작업을 하는 개발자는 반드시 필요하다고 생각. ( 근데 코파일럿 되게 좋은데? ) 코드캠프에서도 총 3가지 의견으로 나뉘었는데요,1. 주니어만 대체가 가능하다.2. 주니어, 시니어 둘 다 대체 가능하다.3. 주니어, 시니어 둘 다 대체 불가능하다.이렇게 총 세가지 의견 중 가장 우세한 의견은 [ 주니어는 대체가 가능하나, 시니어는 불가능하다! ] 입니다. 코드 캠프의 개발자들(일부)은 위와 같이 생각하는데, 여러분들은 어떻게 생각하시나요?여러분들의 의견도 들려주세요!

웹 개발개발자개발자대체GithubCopilotAI기술토론프론트엔드백엔드웹개발시니어개발자주니어개발자

코드캠프

코플소 |코캠러의 프로젝트를 소개합니다!

안녕하세요! 실무 코딩 부트캠프, 코드캠프입니다 :)어쩐지 요즘은 퇴근 시간에도 해가 안 진다 싶더니 오늘이 바로 낮🌞과 밤🌚의 길이가 같다는 ‘춘분’이래요!새 학기가 시작하는 계절이기도 한 지금, 여러분은 어떻게 커리어를 준비하고 계신가요?오늘은 코드캠프의 '코플소:코캠러의 프로젝트를 소개합니다'를 처음 소개하는 날이에요.코플소 시리즈는 코드캠프 코캠러의 다양한 프로젝트를 직접 볼 수 있는 유익한 소식지입니다😊자! 그럼 지금부터 첫번째 프로젝트를 확인해볼까요? 코캠러가 만든 세상에 단 하나뿐인 플랫폼!바로 '댕더(Danger)' 입니다!혹시... 댕더세요? 야나두!🙌기획 의도860만 반려동물 시대를 맞이해 반려견의 Play-Mate를 찾아 '멍라밸'의 질을 높이고 싶었어요!플랫폼을 통해 산책, 간식, 애견카페 탐방 등을 함께 할 친구를 찾는 애견인과 댕댕이를 위한 프로젝트입니다 :) 💫구현 기능✔️ 회원가입 & 초기 프로필 설정웹앱 개발의 기본인 회원가입에서 이메일로 가입할 수 있도록 구현했어요. 본인인증을 위해 가입할 이메일 계정으로 인증 이메일이 전송되어요. 또 동물보호관리시스템을 연동 시켜 반려동물을 등록하고 정보를 검증, 조회할 수 있어요!✔️로그인 페이지 & 비회원 페이지로그인 페이지에서는 회원가입. 비밀번호 재설정, 비회원으로 둘러보기를 차례로 넣어 직관적으로 보이게 만들었어요. 또한, 꼭 회원가입을 하지 않아도 비회원으로 입장해서 등록된 회원들의 강아지를 메인페이지에서 확인할 수 있답니다.(하지만 회원가입 유도를 위해 좋아요 스와이프, 상세 페이지, 댕더패스, 채팅 기능은 비활성화로 설정했답니다!)✔️오늘의 댕댕이 & 상세 페이지좋아요 기능을 적극 활용하여 '오늘의 댕댕이' 페이지를 기획해보았어요! 하루 동안 받은 좋아요 수를 기준으로 12마리까지 나타날 수 있게 구현해 놓고 강아지를 누르면, 댕댕이 상세 정보를 직접 확인할 수 있어요👀추가적인 강아지 사진들과 강아지 이름, 나이, 거리 정보, 설명, 성격, 관심사, 기피 견종까지! 모두 확인할 수 있고 상세페이지에서도 skip, 좋아요 스와이프가 가능하답니다 :)✔️채팅 목록 & 채팅방 페이지.채팅 목록에서는 매칭된 채팅방들을 확인할 수 있고 가장 최근 대화를 나눈 채팅방이 가장 위로 정렬될 수 있게 구현했어요. 또 채팅을 종료하고 싶을 경우 스와이프를 통해 나가기 버튼을 통해 퇴장이 가능해요. 상대 강아지와 채팅이 가능해서 장소와 약속 시간을 공유할 수 있는 기능을 부여해서 유저들이 편리하게 서비스를 이용할 수 있도록 구현했어요 :)🗣️팀 프로젝트 소감(View Point 팀)각자의 과정에서 공부만 하다 처음으로 프론트엔드와 백엔드가 만나는 시간이었던 만큼 긴장도 많이 됐고 서로가 배운 기술로 어떻게 협업을 해야 하는지 감도 잘 오지 않았어요. 하지만 팀원들이 정해지고 팀의 전반적인 룰을 정하기 위해 회의를 진행하거나 기획에 대해 논의를 계속 하면서 하나부터 열까지 협력이 필요하다는 것을 알게 되었습니다!코드캠프는 소수정예로 수강생을 받기 때문에 그만큼 집중 관리를 받을 수 있어서 프로젝트의 퀄리티가 좋은 것 같아요!바쁘고 힘들었던 만큼 가장 뿌듯한 과정이었기도 했습니다! View Point 팀 파이팅!!👊

웹 개발코드캠프팀프로젝트코드캠프후기부트캠프프론트엔드백엔드협업프로젝트플랫폼비전공자개발자

양성빈

[인프런 워밍업 스터디 클럽] 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/

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

왜 자바 백엔드 실무에선 스프링 부트가 중요할까?

한국은 물론, 세계적으로도 가장 인기 있는 서버 개발 스택은 자바(Java) 언어 기반의 스프링(Spring) 프레임워크를 이용한 백엔드 기술입니다. 스프링은 불필요하거나 반복적인 코드를 줄임으로써 코드의 복잡성을 낮추고, 개발자가 핵심 비즈니스 로직에 집중할 수 있도록 돕는 역할을 합니다.하지만 스프링을 사용하려면 초기 환경을 일일이 설정해야 하는 등 번거롭고 어려운 면이 있었는데요. 이런 스프링의 복잡한 부분을 개선하고 보다 손쉬운 웹 애플리케이션 개발을 가능하게 한 게 바로 스프링 부트(Spring Boot)입니다. 스프링 부트를 통해 XML 구성을 할 필요도 없고, Tomcat 등의 기본 HTTP 서버가 내장되어 있어 편의성은 높으면서도 더 빠른 개발이 가능하게 되었죠.이러한 스프링 부트를 통해 자바/스프링 개발자들은 초기 설정처럼 핵심적인 부분은 아니지만 빼놓을 수 없는 공정의 부담을 덜어내고, 프로그램 및 시스템 운용이라는 관점에 집중하여 개발할 수 있게 된 셈입니다.•••베테랑 시니어 개발자들이 알려주는 스프링 부트 노하우가 궁금하신가요?지금 인프런 프리즘 [스프링 부트 로드맵]을 통해 학습해보세요. https://www.inflearn.com/roadmaps/649•••인프런 프리즘 브랜드 스토리 읽어보기 >>

백엔드SpringSpringBootJava스프링스프링부트백엔드Back-End인프런프리즘InflearnPrism

요즘 백엔드 취업 시장에서 코프링이 핫하다던데?

코틀린(Kotlin)은 젯브레인즈(JetBrains)에서 개발한 크로스 플랫폼 범용 프로그래밍 언어입니다. JVM 기반의 언어이면서 자바(Java)와 100% 호환되도록 설계되었습니다. 구글은 2019년부터 코틀린을 안드로이드 개발 공식 언어로 지정했어요. 간결한 문법, 안정성, 다양한 기능이 있다는 장점과 함께 전 세계적으로 사랑받고 있는 언어입니다.그동안 백엔드에선 자바 언어와 스프링 프레임워크의 조합이 가장 압도적인 점유율을 차지하고 있었는데요. 최근엔 코틀린을 도입하거나 자바를 코틀린으로 대체하려는 기업이 늘면서 코틀린 언어와 스프링 프레임워크의 조합, 일명 '코프링'이 주목받기 시작했습니다. 실제로 현재 취업 시장을 살펴보면 코틀린 언어를 다루는 능력을 자격이나 우대 사항으로 기재해 두는 기업을 어렵지 않게 찾아볼 수 있어요. 하지만 비교적 최근에 주목받고 있는 만큼 백엔드 현업에서의 코틀린 혹은 코프링 관련 사례나 자료를 찾는 건 쉽지 않죠.앞으로 사용이 더 늘어날 것으로 전망되는 코틀린, 코틀린과 코프링의 세계에 발 빠르게 뛰어들고 싶다면 지금 시도해 보는 건 어떨까요?•••Java 개발자를 위한실무밀착형 코프링을 배우고 싶다면?지금 인프런 프리즘 [자바 개발자를 위한 실전 코프링 입문 (Kotlin + Spring)]을 통해 학습해보세요.https://www.inflearn.com/roadmaps/703•••인프런 프리즘 브랜드 스토리 읽어보기 >>

백엔드코틀린Kotlin스프링SpringSpringBoot스프링부트코프링백엔드인프런프리즘InflearnPrism

저니

스프링 가이드 목록 2022

* page : https://spring.io/guides     클릭하시면 해당 페이지로 넘어갑니다. 🍃 Building a RESTful Web Service Scheduling Tasks Consuming a RESTful Web Service Building Java Projects with Gradle Building Java Projects with Maven Accessing Relational Data using JDBC with Spring Uploading Files Authenticating a User with LDAP Messaging with Redis Messaging with RabbitMQ Accessing Data with Neo4j Validating Form Input Building a RESTful Web Service with Spring Boot Actuator Messaging with JMS Creating a Batch Service Securing a Web Application Building a Hypermedia-Driven RESTful Web Service Accessing Data in Pivotal GemFire Integrating Data Caching Data with Pivotal GemFire Managing Transactions Accessing Data with JPA Accessing Data with MongoDB Serving Web Content with Spring MVC Converting a Spring Boot JAR Application to a WAR Creating Asynchronous Methods Handling Form Submission Building an Application with Spring Boot Using WebSocket to build an interactive web application Working a Getting Started guide with STS Consuming a RESTful Web Service with AngularJS Consuming a RESTful Web Service with jQuery Enabling Cross Origin Requests for a RESTful Web Service Consuming a SOAP web service Accessing JPA Data with REST Accessing Neo4j Data with REST Accessing MongoDB Data with REST Accessing Data in Pivotal GemFire with REST Producing a SOAP web service Caching Data with Spring Deploying to Cloud Foundry from STS Spring Boot with Docker Working a Getting Started guide with IntelliJ IDEA Creating CRUD UI with Vaadin Service Registration and Discovery Centralized Configuration Testing the Web Layer Accessing data with MySQL Creating a Multi Module Project Creating API Documentation with Restdocs Messaging with Google Cloud Pub/Sub Building a Reactive RESTful Web Service Consumer Driven Contracts Accessing Vault Vault Configuration Accessing Data Reactively with Redis Deploying a Spring Boot app to Azure Building a Gateway Client-Side Load-Balancing with Spring Cloud LoadBalancer Spring Cloud Stream Spring Cloud Data Flow Spring Cloud Task Spring Boot Kubernetes Accessing data with R2DBC Spring Cloud Circuit Breaker Guide Observability with Spring Building a Guide with VS Code Accessing Data with Cassandra Spring Security Architecture Spring Boot Docker Spring on Kubernetes Building REST services with Spring Spring Security and Angular React.js and Spring Data REST Spring Boot and OAuth2 Building web applications with Spring Boot and Kotlin Spring Boot with Kotlin Coroutines and RSocket Metrics and Tracing with Spring     var list = Array.from(document.getElementsByClassName('guide-link')); list.forEach((e) => { console.log('* [' + e.innerHTML + '](http://spring.io' + e.getAttribute('href') + ')'); }); 출처 : https://okky.kr/article/1209730

백엔드스프링프레임워크가이드springframeworkbackendguides

lwisekiml

[인프런 워밍업 클럽 0기] BE 후기

여느 때처럼 인프런 강의를 들으러 사이트에 들어왔더니 인프런 워밍업 클럽을 모집한다는 배너가 보였습니다. 혼자 공부를 하다 보니 목표를 세워도 미루기도 하는 식이였는데 인프런 워밍업 클럽에 참여하면 미션도 있고 미니 프로젝트도 있다고 하여 참여하였습니다. 앞부분은 아는 내용도 많아서 쉬웠지만 뒤로 갈 수로 공부하며 진행하였고 내가 궁금했던 부분들은 이미 다른 분들이 질문을 해서 답변을 얻은 것들을 보면서 공부했습니다. 중간중간 코치님의 라이브를 통해 현재 회사에서는 어떻게 일을 하고 어떤 것을 사용하고 문제를 어떻게 해결하는지 등 많은 내용을 공유해 주셨고 마지막 라이브 때는 열정적으로 2시간 40분 정도 라이브를 해주셨습니다. 열정적으로 알려주셔서인지 시간이 너무 빨리 간 느낌입니다. 수료식 때는 온라인으로 참여하였고 여기서도 많은 것을 알 수 있었고 많은 도움을 받을 수 있었습니다. 러너 분들이 포기하지 않고 완주 목표를 달성할 수 있게 도와주시고 배려 해주시는 운영진분들, 하나라도 더 알려주시는 코치님 그리고 열심히 참여하셨던 러너 분들과 함께할 수 있어서 뜻깊은 시간이 된 것 같습니다. 다음에도 기회가 된다면 참여하고 싶습니다.

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

7마리상어

[인프런 워밍업 클럽 BE 0기] 후기

인프런 워밍업 클럽 후기 이제 막 스프링 부트에 대한 지식을 쌓기 시작한 단계라서 이것저것 어려운 점들이 많았는데 이번에 인프런 워밍업 클럽 스터디를 진행한다는 소식을 듣고 지원하게 되었습니다. 솔직히 처음에는 제가 이 스터디를 잘 따라갈 수 있을지도 의문이었고, 중간에 뒤쳐지진 않을까 걱정도 되었습니다. 그런데 의외로 생각했던 것 만큼 어렵지 않았고, 강사님께서도 정말 쉽게 설명해주셔서 이해하는 데 큰 무리가 없었습니다. 또한 7일차에 거친 과제들과 8일차부터 진행한 미니 프로젝트를 통해 배우는 것에서 그치지 않고 직접 실습 하여 조금 더 제 것으로 만들 수 있었고, 이러한 과제들이 제가 강의를 듣는 데에 소홀해지지 않도록 도움을 준 것 같습니다. 또한 중간중간에 진행했던 Q&A를 통해서 공부를 하다가 막힌 부분이나 더 궁금했던 부분에 대하여 강사님께 직접 물어보고 바로 답을 얻음으로써 몰랐던 지식들을 채워나갈 수 있었고, 특히 다른 러너분들께서 질문 하시는 것들이 궁금했던 것 이외에도 도움이 될 만한 질문들이 많았어서 덕분에 여러 지식을 얻는데 큰 도움을 얻었습니다. 이번 3주간의 인프런 스터디의 경험은 스터디를 시작하기 전과 후의 저가 많이 바뀌게 해주었고, 그만큼 저에게 매우 유익한 시간이었습니다. 다른 러너분들과 함께 지식도 공유하고, 계속해서 스터디를 진행한 결과 이전에는 이해하지 못했던 이론이나 깨닫지 못하는 것들을 시간이 지나고서 이해하게 되었고, 스프링 부트의 전반적인 구조에 대해 익힐 수 있었습니다. 덕분에 이제까지는 들어도 무슨 소리지 했던 내용들이 지금에서는 완벽하게는 아니지만 어떤 내용인지에 대해서는 이해할 수 있게 되었을 정도로 성장했음을 느낍니다! 솔직히 가끔 나태해 질 때가 몇 번 있었는데 다른 러너분들께서 적극적으로 질문도 하시고, 과제나 발자국 제출 등 열심히 하는 모습들을 보니 저도 열정을 넘겨받아 더욱 열심히 참여하게 되었습니다. 평소에 혼자 공부하는 편이었어서 하기 싫을 때 그냥 던져 둘 때가 많았었는데 함께하는 러너분들이 있어서 저도 의지가 계속해서 생겨나서 끝까지 도달할 수 있었습니다. 짧지만 정말 좋은 경험이었고, 다음에도 인프런 스터디를 진행하게 된다면 꼭 참여하고 싶습니다!!

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

또니

[인프런 워밍업 클럽 0기 BE] 오프라인 수료식 후기

[오프라인 수료식 후기]3주간의 시간이 지나고, 워밍업 클럽 스터디가 끝나면서 오프라인 수료식을 다녀왔다.[첫번째 순서]낯을 조금 가리는 탓에 처음 도착해서는 앉아서 멀뚱멀뚱 사무실만 구경하고 있었다.감사하게도 같은 테이블에 계신 러너분께서 먼저 대화를 걸어주셔서 수료식 시작 전까지 신나게 대화하였고,다른 러너 두분께서 추가로 오셔서 첫번째 순서인 저녁 식사를 하며 또 다시 수다 타임을 가졌다.[두번째 순서]식사를 마치고 두번째 순서는 지식 공유자분들의 Q&A 타임이었다.질문은 사전에 러너분들께서 작성해주신 질문에서 몇개 골라 선정된 질문들이었다.취업에 관한 질문, 개발자에 대한 질문 등등 여러가지 질문들이 있었는데, 그동안 내가 많이 고민했던 질문들이 많았고내가 고민했던 것과 지식공유자분들이 답변해주신 것과 일치해서 조금은 안도(?) 하였다.🤣[세번째 순서]이후 세번째 순서는 우수러너 시상식이었다. 나는 처음부터 우수러너에 대한 기대는 크게 없었다.블로그도 누군가에게 보여주기 보다는 내가 공부하고 기록하는 곳으로 사용했고 깔끔하게 작성하는 편도 아니다.열심히 해보겠다는 의지는 강했지만 실력자분들이 많아 이런 쉬운 질문을 해도 될까? 라는 생각에 질문을 망설였다.(조금 더 찾아보니 혼자서도 충분히 해결 가능했던 질문들이 많았다ㅎㅎ)위와 같은 이유로 우수러너는 포기하였고, 완주와 내 실력 향상에 초점을 맞추어 묵묵히 열심히 하였다.그런데!!우수러너 명단에 내 이름이 있던 것이었다!!감사하게도 우수러너로 선정되어 인프런의 귀여운 굿즈를 받게 되었다!다시 한번 감사드립니다.[마지막 순서]그렇게 기쁜마음으로 시상식을 마치고 마지막 순서인 네트워킹을 가졌다.조별로 나누어 앉았고 각 조별로 코치님과 인프런 개발자 한분씩 들어가 네트워킹을 가졌다.내가 있던 테이블에는 인프런에서 5년째 일하고 계신 프론트엔드 개발자분이셨고,현재 취업 시장이 어떤지, 현업에서는 어떻게 일하는지 등등 유익한 이야기를 나누며 수료식은 끝이 났다.[스터디를 하면서 느낀점]그동안 공부하면서 이해되지 않았던 부분이 이번 강의를 통해 이해가 되어 고구마 100개 먹은 가슴이 뻥 뚫렸고,궁금한 부분에 대해 해결하고 고민하는 방법을 얻을 수 있어서 좋았다.이전엔 오류가 나고 해당 매서드에 대해 궁금하면 무작정 인터넷을 먼저 찾았지만이제는 해당 매서드에 들어가서 어떤 것들로 구성되어 있고, 해당 오류는 어떤 오류인지 공식문서나 해당 코드를 통해먼저 생각하고 고민해보는 습관이 형성되어 좋았다.짧다면 짧고 길다면 긴 3주동안 재미있게 공부하며 즐겼던 스터디였다.스터디를 진행해주시고 늦은 시간까지 수료식을 위해 자리를 만들어주신 인프랩 임직원분들,바쁘실텐데 중간 중간 깜짝 특강을 진행해주시고 예정된 시간이 지나도 목소리가 쉬어가며 질문과 정보 전달을 위해열정을 보여주신 최태현 코치님께 감사인사 드립니다.그리고 3주간 함께 달리신 러너분들 고생 많으셨습니다!😁

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

양성빈

[인프런 워밍업 스터디 클럽] 0기 세번째 발자국 (feat. 마지막 발자국 ㅠㅠ)

발자국어느덧 인프런 워밍업 스터디 클럽 마지막 주차가 다가왔다. 매우 즐겁기도 했지만 한편으로는 매우 아쉬운 마음이 너무 걸렸다. 이런 기회가 자주 있었으면 하는 마음으로 마지막 발자국(회고)를 시작해보겠다.강의 요약Day10. 객체지향과 JPA 연관관계조금 더 객체지향적으로 개발할 수 없을까?우리는 지난 시간까지 책 생성 API를 개발하고 대출과 반납기능까지를 개발완료하였다. 하지만 여기서 이런 의문사항이 들 수 있다. SQL 대신에 ORM을 사용하게 된 계기는 "DB 테이블과 객체는 패러다임이 다름" 때문이다. 우리가 사용하는 Java는 객체지향 언어이고 요즘 서비스 진행중인 웹 어플리케이션도 절차지향적이기 보단 객체지향적으로 구성되어 있는 코드들이 많을 것이다. (개인적인 뇌피셜) 그래서 우리가 20강에서 배운 스프링 컨테이너도 객체지향 설계라는 지점에서 출발하게 되었다. 즉, User 객체와 UserLoanHistory를 협업시킬 수 없을까? 즉, 대출기능을 개발할때 BookService가 UserLoanHistory 객체를 만들어 저장하고, 그것을 User객체가 가져오는 방식이였다. 뭔가 BookService를 거쳐가야한다는게 걸린다. 즉, BookService로직은 User객체가 가져와 사용하고 User객체가 직접 UserLoanHistory와 상호작용을 하면 좋을 것 같다. 반납기능도 대출기능과 동일하게 바꾸면 좋을 것 같다. 이렇게 바꾸려면 조건이 존재한다. User객체와 UserLoanHistory가 서로 존재한다는 것을 인지해야 한다. 이것을 위해 연관관계 개념이 등장하였다. 대표적으로 N:1 관계가 존재한다.🙋🏻 N:1 관계란?예시로 들어보자. 어느 한 교실에 여러명의 학생이 존재할 수 있다. 이 때 학생은 N이고 교실은 1이다 이것을 N:1관계라고 부를 수 있다.그럼 관계를 설정하고 나서 다음으로 할 일은 연관관계 주인이 누구인지 알아야한다. 현재 우리의 실습 소스에서 user와 user_loan_history의 테이블을 보면 아래와 같다.create table user ( id bigint auto_increment, name varchar(25), age int, primary key (id) );create table user_loan_history ( id bigint auto_increment, user_id bigint, book_name varchar(255), is_return tinyint(1), primary key (id) );여기서 연관관계 주인을 누구로 할까? 쉽게 생각해서 N:1관계에서 N쪽이 보통은 연관관계 주인이라고 생각하면 쉽다.그리고 연관관계 주인이 아닌쪽에는 mappedBy 속성을 추가해줘야 한다. mappedBy의 속성의 값으로는 관계에 설정된 클래스에 선언된 자신의 객체의 변수명을 적어주면 된다. 실제 코드를 살펴보면 아래와 같이 변경이 가능하다.User.java@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @Column(nullable = false, length = 20) private String name; private Integer age; @OneToMany(mappedBy = "user") private List<UserLoanHistory> userLoanHistories = new ArrayList<>(); protected User() { } public User(String name, Integer age) { if (name == null || name.isBlank()) { throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다.", name)); } this.name = name; this.age = age; } public Long getId() { return id; } public String getName() { return name; } public Integer getAge() { return age; } public void updateName(String name) { this.name = name; } }UserLoanHistory.java@Entity public class UserLoanHistory { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @ManyToOne private User user; private String bookName; private boolean isReturn; protected UserLoanHistory() { } public UserLoanHistory(User user, String bookName) { this.user = user; this.bookName = bookName; this.isReturn = false; } public void doReturn() { this.isReturn = true; } }위의 코드처럼 N쪽에 @ManyToOne 어노테이션을 붙여주고 관계를 맺는 객체를 선언해준다. 그리거 1쪽도 마찬가지로 관계를 맺는 객체를 선언해주고 위에 @OneToMany 어노테이션을 선언해준다. 이런 방식을 양방향 연관관계라고 부르며, 한쪽만 연관관계를 맺을 시 단방향 연관관계라고 부른다. 이렇게 연관관계의 주인의 값이 설정되어야만 진정한 데이터가 저장된다.그럼 BookService는 어떻게 변경을 하는지 살펴보자.BookService.java// 5. 유저와 책 정보를 기반으로 UserLoanHistory를 저장. this.userLoanHistoryRepository.save(new UserLoanHistory(user, book.getName()));이제 위와 같이 user의 id값을 저장하는게 아닌 user 객체를 직접 저장할 수 있다.JPA 연관관계에 대한 추가적인 기능들1:1 관계예를 들어 한 사람과 실거주지의 관계가 딱 1:1 관계이다. 그러면 연관관계 주인은 어느 객체일까? 설정하기 나름이지만 주어진 상황은 사람이 연관관계 주인이라 생각하는게 좋을 것이다. 그러면 코드로 표현하면 아래와 같을 것이다.Person.java@Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; private String name; @OneToOne private Address address; public Long getId() { return id; } public String getName() { return name; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; this.address.setPerson(this); } }Address.java @Entity public class Address { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String city; private String street; @OneToOne(mappedBy = "address") private Person person; public Long getId() { return id; } public String getCity() { return city; } public String getStreet() { return street; } public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } }🔥 연관관계 주인 효과연관관계 주인을 설정하는 것은 객체가 연결되는 기준이 된다.1. 상대 테이블을 참조하고 있으면 연관관계의 주인2. 연관관계의 주인이 아니면 mappedBy를 사용3. 연관관계의 주인의 setter가 사용되어야만 테이블 연결즉, 아래처럼 setter를 이용하여 연결이 가능하다.@Transactional public void savePerson() { Person person = this.personRepository.save(new Person()); Address address = this.addressRepository.save(new Address()); person.setAddress(address); }⚠ 주의만약 트랜잭션이 끝나지 않았을 때 한쪽만 연결해두면 반대쪽은 알 수 없다. 그래서 위의 코드에서 address.getPerson()을 출력을 하면 null이 뜰 것이다. 왜냐하면 지금은 현재 person만 address를 연결해줬기 때문이다. address는 person을 연결해주지 않았기 때문이다.그럼 해결책은 없을까? 객체안에 연관관계 편의 메서드를 만들어 두 객체의 setter를 호출하면 해결이 된다.N : 1 관계 - @ManyToOne과 @OneToMany위에서 언급을 했지만 @ManyToOne과 @OneToMany는 둘다 양방향으로 연결을 할 수 있지만 단방향 연결도 가능하다. 또한 이 어노테이션들을 이용하면서 새롭게 배우는 어노테이션이 있는데 바로 @JoinColumn이다.@JoinColumn- 연관관계의 주인이 활용할 수 있는 어노테이션.- 필드의 이름이나 null 여부, 유일성 여부, 업데이트 여부 등을 지정- 일종의 @Column 어노테이션과 유사하다고 생각하면 좋다.N : M 관계 - @ManyToMany구조가 복잡하고, 테이블이 직관적으로 매핑되지 않아 사용하지 않는 것을 추천한다고 하셨다. 실제로 실무에 근무하는 분들한테 이야기를 들으면 N:M은 많이 사용하지 않고 꼭 이런식으로 처리해야할 경우면 N:1과 1:N으로 풀어쓴다고 하셨다.cascade 옵션 & orphanRemoval 옵션한 객체가 저장되거나 삭제될 때, 그 변경이 폭포처럼 흘러 연결되어 있는 객체도 함께 저장되거나 삭제되는 기능.JPA에는 Entity들 사이의 연관관계를 정의할 때 사용할 수 있는 영속성 전이라고 하는 Cascade 옵션이 있다. 이 옵션을 이용해서 부모에 가해지는 변화를 자식에게 전파할지에 대해 설정할 수 있다.@OneToMany로 자식들을 갖고 있는 부모 객체만 저장/삭제 해도 자식 객체도 함께 저장/삭제 된다던지, 하는 효과를 누릴 수 있다.JPA에는 Entity들 사이의 연관관계를 정의할 때 사용할 수 있는 옵션 중에 orphanRemoval 라는 것이 있다. 이 옵션을 이용하면 부모가 자식에 대한 참조를 끊을 때, 참조가 끊어진 자식 Entity(고아 객체)를 DB에서 삭제하도록 설정할 수 있다.만약 어떤 회원이 책 2권을 대출했다고 하자. 그리고 그 회원이. 갑자기 회원탈퇴를 해서 DB에서 사라졌다. 그럴 경우 많이 이상하게 책 2권이 연결되어 있던게 끊어진 상태가 된다. 이상한 구조일 것이다. 즉, 회원이 삭제될 때 유저 대출기록도 같이 삭제해두는게 좋을 것이다. 그리고 이와 같이 쓰는 옵션이 바로 orphanRemoval 옵션이다.@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List<UserLoanHistory> userLoanHistories = new ArrayList<>();위의 코드처럼 사용하면 부모객체의 저장/삭제해도 자식 객체도 함께 전파되면 삭제시, 자식 객체도 같이 삭제된다.책 대출/반납 기능 리팩토링과 지연로딩이제 우리가 만든 대출과 반납기능을 리팩토링 해보자. 리팩토링 할 부분은 무엇일까? 현재 코드를 보면 도메인 계층에 비즈니스 로직이 들어가져 있다. 또한 여기서 영속성 컨텍스트 4번째 옵션이 나오는데 바로 지연로딩이다.데이터를 처음에 한번에 로딩을 안하고 꼭 필요한 순간에 데이터를 로딩시킨다. 바로 @OneToMany의 fetch옵션의 default 값이다. 지연 로딩을 사용하게 되면, 연결되어 있는 객체를 꼭 필요한 순간에만 가져온다.그러면 우린 이제까지 연관관계를 맺고 주인을 정하고 지연로딩, cascade, orphanRemoval옵션을 이용해서 리팩토링과정을 거쳐보았다. 이렇게 연관관계를 이용하면 뭐가 좋을까?📖 연관관계 장점1. 각자의 역할에 집중할 수 있다. = 응집성2. 새로운 개발자가 코드를 봤을 때 이해가 쉬워진다.3. 테스트 코드 작성에 용이하다.그러면 무조건 연관관계 맺는것이 좋을까? 그렇지는 않다! 연관관계를 남발해서 사용하면 지나치게 사용하면, 성능상의 문제가 생길 수도 있고 도메인 간의 복잡한 연결로 인해 시스템을 파악하기 어려워질 수 있다. 또한 관계가 복잡하면 하나의 테이블 수정 시, 다른 테이블까지 영향을 끼칠 수 있다. 강의중에서도 코치님께서 아래와 같이 말씀주셨다.비즈니스 요구사항, 기술적인 요구사항, 도메인 아키텍처 등 여러 부분을 고민해서 연관관계 사용을 선택해야 한다.Day11. 기본적인 배포를 위한 준비배포란 무엇인가?배포란 무엇일까? 배포는 제 3자의 사용자가 우리가 만든 서비스를 전달하는 과정이라고 볼 수 있다. 우리는 지금 현재 우리의 개인 PC에다가 개발을 하고 웹을 띄워보며 테스트를 해보았다. 하지만 영희가 우리의 서비스를 이용할려하면 어떻게 할까? 현재 상황에서는 나한테 연락을 하고 우리집에 방문해서 사용하고 가야할 것이다. 물론 영희 1명이고 내가 집에 있다고 한다면 가능하다. 하지만 영희 혼자가 아니라 100만명이 우리의 서비스를 이용한다면 정말 고민이 많을 것이다. 또한, 내가 잘때 갑자기 철수가 오겠다고 하면 나는 잠을 자지도 못하고 철수가 우리집에 올 때까지 기다려야 할 것이다.그래서 나는 좋은 생각을 한다. 제3의 컴퓨터를 빌려서 우리의 웹 어플리케이션을 띄우는 것이다. 그리고 나의 친구들에게 그 컴퓨터 IP주소를 알려주면 된다. 이 과정을 배포과정이라고 한다. 그러면 이 컴퓨터는 누구한테 빌릴까? 네이버, 구글등 다양한 컴퓨팅 서비스를 해주는 곳은 많지만 대부분 아마존을 이용한다. 또한 배포를 위해 컴퓨터를 빌릴때 운영체제를 선택도 해야한다.profile과 H2 DB여기서 우리는 문제를 직면한다. 우리의 코드를 제3의 컴퓨터에서 실행시킬 때 DB같은 자원정보를 변경해줘야 한다. 이런 불편함에 이런 생각을 하게 된다. 코드변경 없이 우리의 컴퓨터에서 실행할때 우리의 DB가 연결이 되고 제3의 컴퓨터에서 실행할때는 제3의 컴퓨터에 설치된 DB가 연결되어야 한다. 즉, 똑같은 코드로 실행환경에 따라 설정을 다르게 하고 싶다. 이때 바로 profile을 이용하는 것이다. 현재 우리는 지금 profile이라는 것을 사용하고 있다. 바로 "default" profile을 사용한다. 아무것도 설정을 안하면 해당 프로필이 자동으로 올라온다. 그럼 실제 우리의 코드에 profile을 적용해보자. 똑같은 서버 코드를 실행시키지만, local 이라는 profile을 입력하면, H2 DB를 사용하고 dev 라는 profile을 입력하면 MySQL DB를 사용하게 바꾸자.🤔 H2 DB란?경량 Database로, 개발 단계에서 많이 사용하며 디스크가 아닌 메모리에 데이터를 저장할 수 있다. 또한, 개발 단계에서는 테이블이 계속 변경되는데 어차피 데이터가 휘발되기 때문에 ddl-auto 옵션을 create로 주면 테이블을 신경쓰지 않고 코드에만 집중할 수 있다! 그래서 개발단계나 테스트에서 H2 DB를 많이 사용한다.그러면 적용한 yml은 아래와 같이 될 수 있다.pring: config: activate: on-profile: local datasource: url: "jdbc:h2:mem:library;MODE=MYSQL;NON_KEYWORDS=USER" username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create properties: hibernate: show_sql: true format_sql: true h2: console: enabled: true path: /h2-console --- spring: config: activate: on-profile: dev datasource: url: "jdbc:mysql://localhost/library" username: "root" password: "" driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true여기서 --- 은 프로필을 구분하는 표시선이라 생각하면 좋다. 그리고 DB 접속 url에 MODE=MYSQL;NON_KEYWORDS=USER 해당 옵션을 붙인 이유는 DB의 키워드중에 USER라는 것이 있기에 키워드로 설정 안하고 모드를 MySQL과 유사하게 만들기 위한 옵션이다. 또한 h2.console.enabled와 h2.console.path 옵션은 해당 경로로 접속했을 때 h2 console을 사용할 수 있기 위해서이다.git과 github이란 무엇인가?!개발 관련 서적이나 자료를 찾다보면 한번쯤 보이는 주소가 있다. 바로 git이다. git이란 코드를 쉽게 관리할 수 있도록 해주는 버전 관리 프로그램이다. 이런 상황이 있다하자. A개발자와 B개발자가 협업을 하고 있다 하자. 그리고 각자 개발 후 소스코드를 합칠때 문제가 생긴다. 다른 코드들은 상관없지만 같은 파일의 코드들을 다르게 수정할 우려가 있기 때문이다. 그래서 이것을 일일이 수작업으로 확인하기엔 너무 힘들다. 이래서 git이 등장한 것이다. 또한 버전을 관리하기에 아래와 같은 사태 또한 일어나지 않을 것이다.그러면 github는 무엇일까? git으로 관리되는 프로젝트들을 관리해주는 저장소이다. 우리는 git으로 관리하는 프로젝트를 github에 저장할 수 있다. 그럼 왜 github에 저장할까? 자랑용, 공유로 저장할 수 있지만 배포가 가장 큰 이유로 볼 수 있다. 제3자의 컴퓨터에 우리의 서비스를 배포해야하는데 우리의 소스코드를 usb나 외장하드에 담아 제3자의 컴퓨터까지 가서 복사해서 할 수는 없을 것이다. 만약 집 근처면 참고 갈테지만 만약 미국의 제3자의 컴퓨터가 있다면 비행기값이 더 나올 것이다. 깃 명령어그럼 간단하게 깃 명령어를 알아보자.📚 용어git init : git 프로젝트 시작하기git remote add origin [각자 저장소 주소]: git 프로젝트의 github 저장소 설정하기git add . : 코드들을 담는다. 일종의 택배상자에 담는다고 보면 된다.git status: 현재 택배상자에 코드들이 잘 담겨져 있는지 확인하는 명령어git commit -m "메세지" : 택배상자에 송장 붙이는 명령어git push : 택배상자를 github에 보내기 택배상자를 github에 보낼 때 git push –set-upstream origin master 명령어를 최초 1번 해줘야 한다. AWS의 EC2 사용하기AWS의 회원가입 로그인 과정을 거쳐서 제3자의 컴퓨터를 빌려보는 실습을 해보았다.Day12. AWS와 EC2 배포EC2에 접속을 하려면 아래와 같은 준비물이 필요하다.1) 우리가 접속하려는 EC2의 IP 주소2) 이전 시간에 다운로드 받았던 키 페어3) 접속하기 위한 프로그램 (git CLI 혹은 Mac terminal)다운로드 받은 키 페어를 이용하는 방법ssh –i 경로/키페어이름.pem ec2-user@IP다음으로 키페어 권한을 변경해주자.chmod 400 경로/키페어이름.pem아니면 위와같은 과정이 불편하다면 AWS의 콘솔을 이용하는 방법도 있다.리눅스 명령어mkdir : 폴더를 만드는 명령어ls : 현재 위치에서 폴더나 파일을 확인하는 명령어ls –l : 조금 더 자세한 정보를 확인할 수 있다!cd : 폴더 안으로 들어가는 명령어pwd : 현재 위치를 확인하는 명령어cd .. : 상위 폴더로 올라가는 명령어rmdir : 비어 있는 폴더(디렉토리)를 제거하는 명령어프로그램 설치이제 EC2에 접속했으니 git, java, mysql을 설치해보자. 먼저 아래와 같이 리눅스 터미널에 명령어를 입력하자.sudo yum update위의 명령어의 sudo는 관리자 권한으로 실행한다는 의미이고 yum은 리눅스 패키지 관리 프로그램 (gradle과 비슷한 역할)이다. update는 현재 설치된 여러 프로그램들을 최신화한다는 의미이다.깃 설치sudo yum install gitJDK11 설치sudo yum install java-11-amazon-corretto -ymysqlsudo yum install mysql-community-server // 설치 sudo systemctl status mysqld // 현재 보이지 않는 프로그램을 관리하는 명령어 + mysql 상태 확인 sudo systemctl restart mysql // mysql 재시작 sudo cat /var/log/mysqld.log | grep “A temporary password” // mysql 임시 비밀번호 확인 mysql –u root –p // mysql 접속빌드와 실행git clone 명령어로 우리가 깃헙에 올린 프로젝트를 가져오자.git clone [github 저장소 주소]이제 빌드준비를 위해 gradlew의 권한을 변경하자chmod +x ./gradlew이제 빌드를 하자 (단, 테스트는 제외)./gradlew build –x test그럼 jar파일이 생겼을텐데, 아래와 같은 명령어로 실행시킨다.java –jar build/libs/library-app-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev그럼 서버가 정상적으로 실행된다. 다음으로 서버를 중단해보자. ctrl + c를 누르면 중단된다.하지만 우리는 터미널을 닫아도 서버는 계속 실행되고 싶다. 즉, 백그라운드 재생을 하고 싶어한다. 아래와 같이 입력한다.nohup [명령어] &그러면 백그라운드로 재생된 우리의 프로그램을 어떻게 종료할까?ps aux | grep java위와 같이 현재 실행중인 프로그램 중 java가 들어가는 프로그램을 확인해서 pid값을 알아내 아래와 같이 입력한다.kill -9 프로그램번호또한 파일의 내용을 확인해 볼 수 있는 명령어도 알아보자.vi : 리눅스 편집기인 vim을 사용하여 파일을 연다.또한 vi말고도 cat 명령어도 있다.cat : 파일에 있는 내용물을 모두 출력하는 명령어또한 끝부분만 확인하고 싶을때 아래와 같이 입력한다.tail : 현재 파일의 끝 부분을 출력하는 명령어여기서 실시간으로 확인하고 싶을 경우 f옵션만 주면 된다.가비아를 이용한 도메인 구입가비아를 통해 도메인을 구입해보았다. 실제 과정은 매우 단순함으로 생략한다.Day13. SpringBoot 설정 및 버전업여기서는 내용을 조금 축약해서 작성해보겠다. 이전에 배웠던 개념이기도 하고 중요하지만 실습적인 부분은 아니기에 간략히 작성한다. 우리는 여기서 gradle의 구성에 대해 알아보았다. 이 gradle 파일안에는 플러그인 설정, 의존성 설정, 저장소 설정등을 확인할 수 있었다. 또한 스프링이 어떻게 생겨났는지 스프링부트는 또 어떻게 생겨났는지 이 둘의 차이는 무엇인지 알 수 있었다. 그리고 yaml 문법과 properties 문법에 대해 알아보았다. 다음 우리의 프로젝트에서 롬복을 적용해서 리팩토링과정도 알아보았다. 마지막으로 스프링부트 버전을 3.x로 바꾸어보았다. gradle에서 스프링부트 버전을 변경하고 빌드할때 달라진 부분들을 고쳐주는 작업을 해보았다.Day14. 마무리 및 꿀팁우리는 여기서 앞으로의 공부 방향성, AWS 비용계산방법, myBatis 적용, 정적파일 처리방법을 배웠다. 나는 여기서 느꼈던 점은 공부 방향성에 있어서 코치님 말씀대로 코틀린 및 스프링의 다양한 모듈에 대해 접근해볼 예정이다.또한 이 수업에서 myBatis를 적용해보았는데 개인적으로 내 스프링부트 버전이 myBatis starter 몇 버전을 쓰는지 복잡함을 느꼈다. 또한 여전히 문자열로 쿼리를 작성한다는게 나한테에 있어서 많은 불편함을 느꼈다. 하지만 여기서 코치님은 대용량 데이터를 insert할때 jdbcTemplate을 이용한 batch 쿼리 실습을 해주셨는데 시간차이를 보니 완전 신세계였다. 이 부분에 대해 좀 더 자세히 알아봐야겠다.미니 프로젝트나는 미니프로젝트를 개발해가면서 많은 어려움과 좌절을 맛 보았다. 1단계는 나름 간단해서 별거 없네라는 식으로 넘어갔다. 하지만 다른 러너분들과 리뷰과정에서 많이 고쳐야 할 점을 보았다. 또한 단계가 갈수록 코드가 점점 개판이 되어간다는 나 자신이 너무 싫었고 특히 마지막 단계는 밤을 꼬박 새워서 해결할 수 있었다. 공공데이터포털로 법정공휴일 api를 가져오려 했지만 이 api가 몇번 타임아웃이 발생한다. 이 문제때문에 시간을 쏟은것은 안 비밀!리뷰중에는 왜 이렇게 작성했냐부터 이렇게 바꾸는 것이 어떤가의 대해 의문점을 던져주셨고 이것을 깊이 통찰하는 시간이 나를 성장하는 계기를 만든 것 같다. 자세한 개발과정은 1단계와 똑같은 절차로 해결했으니 1단계 개발일지를 참조해보시면 좋을 것 같습니다. 개발일지https://inf.run/rF31s PR1단계: https://github.com/crispindeity/warming-up-study-mini/pull/82단계: https://github.com/crispindeity/warming-up-study-mini/pull/93단계: https://github.com/crispindeity/warming-up-study-mini/pull/134단계: https://github.com/crispindeity/warming-up-study-mini/pull/15 최종 머지한 내 프로젝트https://github.com/SungbinYang/warming-up-study-mini/tree/main/sungbin/mini회고드디어 스터디클럽의 여정이 끝났다. 정말 힘들고 출사표때 전달했던 많이 부딧혔고 깨졌다. 그러면서 나는 점점 성장을 해 나간것 같다. 비록 1달이라는 짧다면 짧은 여정이였지만 내 학습의 여정은 아직 끝이 안 났기에 계속 달려볼려고 한다. 이 클럽을 수료하더라도 혹은 1기로 다시 재 신청을 하더라도 내 본연의 학습여정은 계속 될 것이며 그 여정동안 많이 깨지고 부딪히면서 점점 성장하는 개발자 양성빈이 되어야겠다. 화이팅 🔥🔥🔥🔥 📚 참조http://www.jjal.today/bbs/board.php?bo_table=gallery&wr_id=94&sfl=wr_subject%7C%7Cwr_content%7C%7Cwr_4&stx=웃짤&sop=and&page=7

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

윤대

[인프런 워밍업 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 들은 자바부터 상세히 파보기 시작했습니다. 그러면서 여러 자료를 찾다가 다른 러너분들과 공유되고 싶다는 글을 찾게 되었습니다. 이렇게 첫 시작으로 지금 마음가짐으로 끝까지 완주해서 우수러너까지 노려봤으면 좋겠습니다. 다들 응원 부탁드립니다. 🥳 📚 참고자료오라클 블로그 

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

Groot

[워밍업 클럽 0기 - 백엔드] 참여 후기

"인프런 워밍업 클럽 0기 - 백엔드" 참가 후기입니다.참가한 계기참가하게 된 계기는 기본기를 기르고자하는 마음 60%, 호기심 40%? 때문입니다.백엔드 프로그래머를 지망하는 만큼 자바로 실무를 하게 될 가능성이 높은데 이때까지는 이상하게 FastAPI, NestJS, ASP.NET과 같은 프레임워크를 많이 사용했었습니다.그러면서도 자바는 상당히 익숙했습니다.학교 수업을 들으며 자바로는 오히려 Client 프로그래밍을 훨씬 많이 한 것 같습니다. 학우들은 자바를 다 알아서 눈에 보이는 작업물을 보여주기에는 자바 Swing만한 것이 없었던 것 같습니다...아무튼 학교를 빠져나와서 백엔드 지망하는 분들이랑 얘기나 스터디를 하다보면 항상 예제로 나오는 것이 스프링이었습니다. DI, 컨테이너와 같은 컨셉도 소개될 때마다 예시로 등장하는 것이 스프링이고 스터디를 하다가도 다른 분이 예시로 보여주는 것도 스프링일만큼 매일 등장해서 아.. 계층 나누고 주입해주는 것은 비슷한데 자바가 훨씬 자료가 많고 추상화가 더 잘되어있구나 싶었습니다. 안 해봐서 잘은 모르지만 무엇보다 완성도가 높은 느낌이 들었습니다... 몇 년 전에 Node 진영에 ORM을 공부할 때는 자료가 너무 없어서 JPA 코드를 보면서 공부했기도 했습니다.프로젝트 끝날 때마다 Spring Boot는 해보려고 마음 먹고는 했는데 이번 기회에 입문했습니다. 기본적인 개념도 익히고 적절하게 매일 1시간 정도 과제를 고민하고 해결하며 직접 코드를 짜볼 수 있어서 좋았습니다. 다른 스터디 참여자분의 과제 코드도 볼 수 있어서 공부가 많이 된 것 같습니다.다른 프레임워크에서 넘어온만큼 보이는 것도, 예전에 제가 실수한 것도 떠올라서 강사님의 라이브 영상이나 Q&A 내용을 보면서도 많은 팁을 얻었습니다.참가하며 남긴 기록들학교가 개강하고 다른 취준 일정이랑 겹치면서 점점 시간에 쫓기고 빨리해야 했어서 마지막에 몰아서 해버렸지만 후... 아무튼 완강까지 성공했습니다.후기강의는 백엔드 개발에 입문하는 사람이 대상입니다. 그렇다 보니, Git, HTTP나 서버-클라이언트 등의 개념도 입문자를 위한 설명을 포함됩니다. 다른 MVC 프레임워크 경험한 분은 30~50% 정도는 익숙한 내용일 수도 있을 것 같습니다. 개념에 맞춰서 잘 쪼개어 있고 강의 설명이 정말 깔끔합니당.. 이런 분이 강의를 하시구나 했습니다. 저는 아무래도 시간에 쫓기며 웹 개발을 많이 했기에 여러 내용들은 점검해가는 과정이라 생각하고 열심히 수강했습니다.그래도 난이도가 낮은 경우 빠르게 보고 덮은 다음, 스스로 찾아보면서 코드를 구현해보았습니다. 매일 라디오 듣듯이 따라가며, 다른 유사 MVC 프레임워크에서 추구했던 것도 비교하며 학습했습니다. 다른 분들의 질답도 많은 공부가 되었습니다. 고민의 깊이가 다르시더라고요!!배운 것최종적으로는 자바의 역사와 주변 지식, JDSL, Lombok, 코드를 보는 관점 등 여러 키워드를 얻을 수 있어서 유익했습니다.이전에는 VSCode를 많이 썼는데 이 참에 인텔리제이의 단축키도 익숙해질 수도 있었습니다.자바에서 자주 쓰는 단축키 CheatSheet을 만들어보는 것은 어떤가요?다른 사람의 과제랑 코드를 볼 수 있는 것도 좋은 경험이었습니다. 스터디를 매우 열심히 참여하시던 분들이 많아 놀랐습니다. 그러다보니 저도 기운을 받아서 더 공부하고 찾아봤던 것 같습니다ㅎㅎ.앞으로도 파이팅해서 만들고 싶은 프로젝트를 하나 만들어보려고 합니다. To Inflearn, 최태현 지식 제공자님. 재밌고 유익한 스터디 만들어주셔서 감사합니다!

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

crispin

[인프런 워밍업 스터디 클럽 0기_BE] 후기

3주간의 스터디가 끝이났다. 정말 많은 걸 배울 수 있었고, 새로운걸 경험해 볼 수 있었다. 1. 코치님0기 백엔트 코치님은 최태현 코치님이다. 이전 찍으셨던 몇개의 강의(사실 전부..ㅎㅎ) 를 통해 내적 친밀감이 쌓여있었는데스터디 코치님 이라는 이야기를 듣고 꼭 스터디에 참여하고 싶다는 생각이 들었다. 강의를 통해서도 정말 많은걸 알려주시려 하고, 질문도 매우 친절하게 답변해주셨었는데 스터디를 진행하면서도 코치님은 질문 뿐만 아니라특강을 통해서 정말 많은걸 알려주시려고 하는 모습이 정말 대단하다고 느껴졌다.👍 현업에서 일을 하시면서 늦은 시간까지 이어지는 질문에도 친절하게 답변해주시는 모습을 보면서 나도 저런 좋은 영향력을 주위에 끼칠수 있는 개발자가 되고 싶다는 생각이 들었다. 2. 코드리뷰스터디를 처음 시작할때 커리큘럼을 보고 미니 프로젝트를 진행할때 다른 분들과 함께 코드리뷰를 하며 진행하면 어떨까?라는 생각이 들었었다. 할까말까 고민을 정말 많이 했었는데 개인적으로도 코드 리뷰를 해보고 싶었고, 좀 더 이 스터디기간동안 많은걸 배우고 싶어 용기를 내어 함께 진행해 볼 인원을 구했었다. 정말 다행히도 5분의 스터디원 분들이 함께해보자고 하셨고, 결과적으로 너무 만족스러운 시간을 보낼 수 있었다. 내 코드를 다른 사람에게 보여준다는건 지금도 쑥쓰럽긴 하지만 그렇기 때문에 코드 한줄을 작성할때도 정말 많은 생각을 하면서 작성할 수 있었다. 그리고 그 고민했던 내용을 함께 나누면서 정말 많은걸 배울 수 있었다. 다시 한번 함께 미니프로젝트를 진행했던 백엔드 스터디원분들 에게 감사하다는 말을 전해고 싶다.🙇‍♂ 3. 인프런보통의 회사에서 하기 어려운 이런 좋은 영향력을 전파하고 만드는걸 할 수 있다는게 늘 놀랍다. 해커톤이나 개발자 컨퍼런스 후원 기업에 매우 자주 인프런이 있는걸 볼 수 있고, 직접 인프콘 이라는(한번도 가보지 못했다. 나는왜 운이 없는것인가..💧) 큰 컨퍼런스를 열기도 하고. 전반적으로 개발자 생태계에 정말 좋은영향력을 널리 퍼트리고 있는 모습을 보면 정말 대단한 기업이라는 생각이 든다. 생각은 누구나 할 수 있지만 이걸 실천할수 있다는게 참 놀랍다. 이번 스터디도 그렇고 앞으로 더 많은 좋은 영향력을 널리 퍼트리려고 다양한 시도를하고 있는 모습을 보고 있으면 언제가는 인프런에서 일을 꼭 해보고 싶다 라는 생각이 듣다.(자바 개발자 안필요 하신가요💧) 4. 0기앞으로 이 워밍업 클럽이 0기에서 멈추는것이 아니라 1기 2기 쭉쭉 더 많은 사람들에게 좋은 경험이 될 수 있도록커져갔으면 좋겠다. 다음에 더 좋은 스터디가 열린다면 주변에 적극 추천해주고, 나도 다시 한번 참여해봐야겠다. 5. 마무리좋은 기회를 만들어주신 인프런 관계자 분들에게 정말 감사드린다. 특히 스터디 운영에 대해 이런저런 공지와 궁금증을해결해주신 셰리 에게 감사의 인사를 전하고 싶다. 더 많은걸 알려주시려 했던 최태현코치님 그리고 함께 했던 모든 백엔드 스터디 원 분들에게도 다시 한번 감사하다는 인사를 전하고 싶다. 주저리주저리 말이 많아졌는데, 혹시라도 미래에 있을 1기 신청을 고민하며 이 글을 보고 있다면 당장 신청 하라고 적극 권해 주고 싶다.

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

[인프런 워밍업 클럽 BE] 참여 후기

이 후기글은 인프런 워밍업 클럽 0기 BE의 전체 소감문입니다.https://inf.run/Hywa 사실 후기글을 써본 경험이 거의 전무하기에, 어떻게 작성해야 하는지조차 감이 잘 잡히지 않는다. 그래서 느낀 감정들을 두서없이 그저 솔직하게 작성해볼까 한다. 참가 신청과 첫 주이번 최태현 코치님 강의는 이전에 절반만 들어놓고 반년 간 시간을 허비하며 지낸 그런 부끄러운 과거 속의 강의 중 하나였다 . 그래도 운이 좋게도 메일로 이번 워밍업 클럽 홍보 글이 날아온 것을 보았고 바로 신청했다. 나는 스스로의 실력에 확신이 차지 않으면 도전하지 못하는, 어찌보면 많이 소극적인 성격인 편이다. 그랬기에 그동안 인프런에서 팀 프로젝트를 시도해보려고 해도 개발자로서의 지식이 너무 얕은 것 같아 차마 도전해보기가 어려웠다. 이번 스터디 신청 자체가 나 나름대로의 하나의 도전이었던 셈이었다. 결과적으로는 잘한 선택이었다고 현재는 마음 깊이 생각하고 있다.디스코드에 처음 접속해 서로 인사말을 남기는데 사실 조금, 아니 상당히 당황스러웠다. 나처럼 초보자 분들이 많이 오는 것을 예상했으나 이미 현업 종사자분들도 다수 참가하시는 것을 보았다. 그때부터 발등에 불이 떨어진 기분이었다. 다른 분들의 질문의 깊이나 발자국, 과제 내용 등을 보고 압도되는 기분이었다. 😂😂그리고 대망의 첫 중간 점검 라이브 때, 일정 관리 실패로 나는 바보같이 라이브를 놓쳐버렸다. 완주 러너의 조건에 중간 점검 라이브를 모두 참가해야 한다는 조항이 있었기 때문에 일주일 만에 열심히 하겠다는 나름의 다짐이 완전히 망가졌다. 그 이후, 그리고 스터디 끝첫 라이브를 놓치고 하루는 기분이 약간 쳐지긴 했었다. 딱히 우수 러너를 노리거나 포인트를 꼭 받아야겠다! 는 아니었지만, 그래도 무언가를 완주했다는 그 성과 자체가 없어지는 것이니 뼈아픈 일이었다. (그리고 수료증을 못 받는 것도 아쉬웠고... ) 하지만 제 1의 목표는 어디까지나 학습이니 그 이후도 내 나름대로 착실히 수행했다. 완주 러너가 되지는 못하겠지만 과제도 꾸준히 제출했고 2차 점검 라이브도 모두 참석했다.이전 후기에서도 작성했듯이 미니 프로젝트 코드 리뷰도 진행해보았고 지금은 좋은 분들과 함께 사이드 프로젝트를 진행 중이다. 아직 본격적인 개발 단계 전이지만... 😊그리고 인프런과 코치님들의 배려로 완주 조건이 완화되어 무사히 완주 러너가 될 수 있었다!당연히 완주 러너가 못 될 것을 예상해 오프라인 수료식 참가 신청을 하지 않은 것은 후회하긴 했지만... 완주 러너가 된 것 만으로도 만족했다. 그리고 대망의 수료식 날, 정말 예상하지 못하게 우수 러너로 선정이 되었다. 선정해주신 코치님에게 몇 번이고 감사 인사를 드리고 싶었다. 우수 러너 혜택으로 인프콘 티켓과 코치님과의 1:1 멘토링을 얻게 되었으니 정말 너무 과분하고 감사한 보상이다. 🙇‍♀️🙇‍♀ 반성할 점과 이모저모람다 함수와 스트림 학습을 위해 새 블로그 글을 만든 것이 있는데 첫 날 이후 정말 하나도 갱신하지 못했다.😭 3/9에 SQLD 시험이 있었기에 강의, 과제, 자격증 시험 공부를 병행하면서 저 글까지 갱신하는 것은 아무래도 과한 목표였을지도 모른다. 사실 저 SQLD 시험도 진작에 공부했으면 됐겠지만... 이미 지나버렸으니 어쩔 수가 없다. 요즘은 학습한 내용을 모두 개인 노션 페이지에 정리하고 있기에 아마 추가 학습을 해도 저 블로그 글은 갱신이 안 되지 않을까 싶다.요즘은 영한님의 로드맵을 따라 쭉 강의를 듣고 있다. 사이드 프로젝트에 폐를 끼치지 않기 위해 본격적인 시작 전 최대한 기반 지식을 더 다지고 있다. 우수 러너 혜택으로 1:1 멘토링도 남아있어서 더 많은 것을 얻기 위해서라도 많은 지식을 쌓아놓아야 할 듯 하다.

백엔드스터디후기

망고123

인프런 워밍업 클럽 스터디(BE) 0기 / 3주차 발자국

일주일 간의 학습 내용에 대한 간단한 회고📕어느덧 마지막 3주차 회고 작성입니다. 인프런 워밍업 클럽 스터디 0기(BE)에 참여하면서, 어설프게 할 생각이었다면 시작조차 안 하는 것이 백배 낫다고 생각하며 진정성 있는 참여를 하기 위해 미션과 미니 프로젝트를 수행하기 위해 최선을 다하려고 끊임없는 도전과 열정을 코드로 보이기 위해 노력했습니다. 3주의 시간이 정말 짧게 느껴질 정도로, 수많은 고민과 반복적인 공부를 통해 스스로의 객관화를 가질 수 있는 매우 유익한 시간이라고 확신합니다.🔥미니 프로젝트 4단계까지 구현을 성공했습니다. 비록💡한 걸음 더! 는 마감 기간 내에 구현하지 못했지만, 처음 시작할 때의 열정과 목표를 잊지 않으면서 강의를 통해 멘토님께서 전달하고자 하는 지혜와 경험을 바탕으로 3주 동안 스스로 많이 성장한 모습에 놀라며 끊임없는 도전으로 구현 능력의 한계를 극복할 수 있게 된 특별한 경험이었습니다. 이 경험으로 취업에 성공하여 회사에 의미를 더해줄 훌륭한 개발자로 성장을 목표로 오늘도 어김없이 정진하겠습니다.일주일 동안 스스로 칭찬하고 싶은 점 미니 프로젝트를 진행하면서 정말 많은 고민을 통해 구현한 코드는 매우 의미가 있습니다. 4단계까지 무사히 마칠 수 있는 스스로에게, 한계를 넘어 벽에 부딪히는 것에 두려워하지 말고 한계를 도달하고 뛰어 넘을 수 있다는 가능성을 눈 앞에서 증명했다는 것에 감사하다고 전하고 싶습니다.아쉬웠던 점 회사 이력서를 작성하면서 4단계 💡한 걸음 더! 배포를 기간 내에 마치지 못한 것에 많은 아쉬움을 갖고 있지만, 이 또한 나의 부족함과 나태함의 결과로 승복하였습니다. 하루라도 더, 몇 시간이라도 더, 조금만 더 잠을 안자고 했더라면 충분히 할 수 있었음에도 불구하고 못한 자신에게 아쉬움을 표합니다.보완하고 싶은 점 아무리 코드를 따라 치고, 강의를 보더라도 스스로 고민을 하고 아무것도 없는 상황에서 해결하지 못한다면 의미가 없다는 것을 크게 느꼈습니다. 단기 기억 공간과 다른 모든 인지 기억에 마치 잘 알고 있다는 착각에 빠지지 않고, 철을 관철하는 정신으로 공부한 모든 내용을 체득할 것입니다.다음주에는 어떤 식으로 학습하겠다는 스스로의 목표 강의 PDF, PPT 를 통해 처음부터 끝까지 다시 복습을 진행하며, 백엔드 개발 관련 책을 구매하여 함께 병행할 예정입니다. 소중한 경험을 소홀히 하지 않고 앞으로의 부족함을 채우는 행보에 지치고 힘들며 한계에 부딪히는 순간들을 맞이하고 뛰어 넘기를 목표합니다. 학습 내용 정리미니 프로젝트 1단계API 에 따라 정상적인 동작을 수행하는지 과정을 검증하는 공간입니다. 직원과 팀 엔티티 확인하기 #4 직원과 팀의 컨트롤러, 서비스, 리포지토리 확인하기 #6커밋 메세지와 코드 확인은 위의 이슈로 이동하면 확인할 수 있습니다.0. 데이터 API팀 등록하기POST / /api/v1/team[{  "name" : "여신관리팀"}] 모든 팀 조회하기GET / /api/v1/team[  {      "name": "여신관리팀",      "manager": "김민수",      "memberCount": 2  },  {      "name": "영업1팀",      "manager": "박현준",      "memberCount": 2  },  {      "name": "전산팀",      "manager": "박준우",      "memberCount": 1  }] 직원 등록하기POST / /api/v1/members[{  "name" : "김사원",  "role" : "MEMBER",  "birthday" : "1999-11-02",  "workStartDate" : "2023-11-01",}] [{  "name" : "김사원",  "teamName" : "여신관리팀", // 직원 등록 시점에 팀 등록을 할 수 있습니다.  "role" : "MEMBER",  "birthday" : "1999-11-02",  "workStartDate" : "2023-11-01",}] 모든 직원 조회하기GET / /api/v1/members[  {      "name": "김민수",      "teamName": "여신관리팀",      "role": "MANAGER",      "birthday": "1970-11-11",      "workStartDate": "2010-02-23"  },  {      "name": "이민혁",      "teamName": "여신관리팀",      "role": "MEMBER",      "birthday": "1984-06-12",      "workStartDate": "2017-08-11"  },  {      "name": "박현준",      "teamName": "영업1팀",      "role": "MANAGER",      "birthday": "1980-09-29",      "workStartDate": "2012-02-13"  },  {      "name": "이현민",      "teamName": "영업1팀",      "role": "MEMBER",      "birthday": "1990-11-15",      "workStartDate": "2017-08-26"  },  {      "name": "박준우",      "teamName": "전산팀",      "role": "MANAGER",      "birthday": "1994-03-15",      "workStartDate": "2019-02-05"  },  {      "name": "서포터",      "teamName": null, // 팀에 포함되지 않은 직원은 null 표시합니다.      "role": "MEMBER",      "birthday": "2001-11-12",      "workStartDate": "2024-03-04"  }] 1. 테이블 생성직원과 팀 테이블은 다음과 같은 쿼리를 통해 자동으로 생성됩니다.팀 테이블직원 테이블2. 데이터 추가팀 테이블에 여신관리팀, 영업1팀, 전산팀 을 등록합니다. 직원 테이블에 다수의 직원 데이터를 등록합니다.2.1 팀 등록하기팀 등록(POST / /api/v1/team)을 통해 여신관리팀, 영업1팀, 전산팀 데이터를 팀 테이블에 저장합니다.팀 테이블여신관리팀, 영업1팀, 전산팀 데이터를 성공적으로 저장한 것을 확인합니다.2.2 직원 등록하기직원 등록(POST / /api/v1/members)을 통해 다수의 직원들을 등록합니다.직원 테이블team_id 를 통해 해당 팀에 알맞게 데이터를 성공적으로 저장합니다. 팀에 속하지 못한 경우에는 null 으로 표시됩니다.팀 테이블회원 등록과 함께 직원이 팀에 포함되면 member_count 를 증가합니다. manager 는 회원 등록 role의 MANAGER 만 가능합니다. 이미 MANAGER 가 존재하는 팀에다가 MANAGER 인 직원을 포함하려고 하면 예외가 발생합니다.3. 데이터 조회3.1 직원 조회모든 직원 조회(GET / /api/v1/members)으로 정보를 출력합니다. 팀에 속하지 못한 직원은 teamName 이 null 으로 출력됩니다.3.2 팀 조회모든 팀을 조회합니다.미니 프로젝트 2단계0. 데이터 API출근 / POST / http://localhost:8080/api/v1/commute/start?memberId=1퇴근 / POST / http://localhost:8080/api/v1/commute/end?memberId=1특정 직원의 날짜별 근무시간 조회 / GET / http://localhost:8080/api/v1/commute?id=1&date=2024-03{  "detail": [      {          "date": "2024-03-05",          "workingMinutes": 12      },      {          "date": "2024-03-04",          "workingMinutes": 600      },      {          "date": "2024-03-03",          "workingMinutes": 583      },      {          "date": "2024-03-02",          "workingMinutes": 987      }  ],  "sum": 2182} 1.테이블 미리보기출퇴근 동작 확인을 위해 미리 팀과 직원 데이터를 작성합니다.팀 테이블직원 테이블출퇴근 테이블2. 출근하기-http://localhost:8080/api/v1/commute/start?memberId=1 으로 id 가 1인 회원이 출근 처리됩니다.그림 첨부는 없지만, 회원 id 가 2, 3, 4 또한 출근으로 등록한 상태라는 점을 참고합니다.출퇴근 테이블출근에 성공한 직원은 출퇴근 테이블에 다음 그림과 같이 저장됩니다.3. 퇴근하기-http://localhost:8080/api/v1/commute/end?memberId=1 으로 id 가 1인 회원이 퇴근 처리됩니다.그림 첨부는 없지만, 회원 id 가 2, 3, 4 또한 퇴근으로 등록한 상태라는 점을 참고합니다.출퇴근 테이블퇴근에 성공한 직원은 출퇴근 테이블에 다음 그림과 같이 저장됩니다.4. id=1 직원에게 2024-03 근무한 데이터 추가하기id 가 1, 2, 3, 4 인 직원은 모두 2024-03에 한 번은 출퇴근한 상태입니다.비교를 위해 id 가 1 인 직원에게 2024-03 에 해당하는 날짜에 출퇴근 기록 데이터를 INSERT 쿼리를 수행하여 출력 데이터를 준비합니다.출퇴근 테이블5. id=1 직원의 2024-03 출퇴근 조회하기GET / http://localhost:8080/api/v1/commute?id=1&date=2024-03정상적으로 특정 직원의 날짜별 근무 시간을 조회하는 기능 을 확인할 수 있습니다.미니 프로젝트 3단계0. 데이터 API휴가 등록 / POST / /api/v1/vacation{  "id" : 2,  "date" : "2024-03-23"} 남은 휴가 개수/ GET/ /api/v1/vacation?id=114 팀마다 며칠 뒤에 휴가 등록이 가능한 일 입력 / POST / /api/v1/vacation/role{"name" : "여신관리팀","minDay" : 3 // 오늘 기준으로 몇 일 뒤에 휴가 등록 가능한 지 숫자 입력} 특정 직원의 날짜별 근무시간 조회 / GET / /api/v1/commute?id=2&date=2024-03{"detail": [{"date": "2024-03-01","workingMinutes": 670,"usingDayOff": false},{"date": "2024-03-02","workingMinutes": 720,"usingDayOff": false},... // 03 ~ 08 일 생략{"date": "2024-03-09","workingMinutes": 0,"usingDayOff": true},... // 10 ~ 30 일 생략{"date": "2024-03-31","workingMinutes": 0,"usingDayOff": false}],"sum": 1390} 1.테이블 미리보기휴가 동작 확인을 위해 데이터를 작성합니다. 참고하기 바랍니다.직원 테이블팀 테이블출퇴근 테이블휴가 테이블휴가룰 테이블1. 휴가 정책팀마다 며칠 뒤에 휴가 등록이 가능한 일 입력 / POST / /api/v1/vacation/role팀에 휴가 정책을 지정합니다. 휴가룰 테이블에서 확인이 가능합니다.휴가룰 테이블2. 휴가 등록id 가 1 인 직원에게 2024-03-10 에 휴가를 등록합니다.minDay의 3 휴가 정책에 따라서 만약 2024-03-08 이하로 입력하거나 중복된 휴가는 등록되지 않습니다.휴가 테이블3. 남은 휴가 개수남은 휴가 개수/ GET/ /api/v1/vacation?id=1남은 휴가 갯수를 숫자로 반환합니다.직원 번호 1 은 휴가 13 개를 정상적으로 반환하고 휴가를 모두 사용한 직원 번호 2는 휴가 0 개를 반환합니다.4. 특정 직원의 날짜별 근무시간 조회특정 직원의 날짜별 근무시간 조회 / GET / /api/v1/commute?id=2&date=2024-03해당 월의 모든 날짜를 휴가 유무와 출퇴근에 따라서 정상적으로 출력합니다.미니 프로젝트 4단계0. 데이터 API초과 근무 계산 / GET / /api/v1/overtime[{"id": 1,"name": "연**1","overtimeMinutes": 11100},{"id": 2,"name": "연**2","overtimeMinutes": 0}] 1.테이블 미리보기초과 근무 계산 동작 확인을 위해 데이터를 작성합니다. 참고하기 바랍니다.직원 테이블팀 테이블출퇴근 테이블2. 초과 근무 계산공공 데이터 포털로 대한민국의 공휴일과 대통령령의 대체 공휴일을 가져옵니다.요청한 년-월의 토요일, 일요일 과 공휴일 그리고 휴가일 따라서 전체 근무 일자가 몇 일인지 구합니다.요구 사항에 따라 하루 기준 8시간으로 한 달동안 법적 근로 시간을 산출하여 초과 근무 시간을 계산하여 출력합니다.그림과 같이 포스트맨의 "id" = 1의 경우 다음과 같습니다.2024-03 의 공휴일과 주말은 총 11 일입니다. 따라서 88시간 = 5280분2024-03 의 전체 평일은 총 20 일입니다. 따라서 20일 * 8시간 = 160시간 = 9600분2024-03 의 직원 (id = 1) 의 전체 근무 시간은 345 시간입니다. 따라서 345시간 * 60분 = 20,700분포스트맨의 결과 동일하게 초과 근무 시간 = 20,700 - 9600 = 11000 으로 정상적으로 출력되는 것을 확인할 수 있습니다.2024-03 의 직원 (id = 2) 는 초과 근무 시간 이 없으므로 0 으로 출력됩니다.이름은 첫 글자와 마지막 글자를 제외한 모든 글자는 * 으로 표시합니다.직원 테이블로부터 이름을 조회한 것으로 데이터베이스 안에는 정확한 이름이 존재합니다. * 는 서비스 로직에서 처리되어 연**1 처럼 응답합니다.

백엔드SpringJava

양성빈

[인프런 워밍업 스터디 클럽] 미니프로젝트 - step1

미니 프로젝트 - 개발 일지드디어 미니프로젝트 시작이다.프로젝트 세팅언어: JDK21프레임워크: Spring Boot 3.2.3, Spring Data JPA라이브러리: 롬복DB: mysql테스트: junit5요구사항환경 설정1. profile 분리먼저 나는 프로필을 분리하기로 하였다. 개발환경 profile과 운영환경 profile 그리고 공통적인 부분을 묶어두었다. 또한 DB의 정보는 민감한 정보이므로 환경변수에 등록해두었다.application.ymlspring: profiles: group: dev: "dev, common" prod: "prod, common" active: dev --- spring: config: activate: on-profile: dev datasource: url: "jdbc:mysql://${DB_DEV_HOST}/${DB_DEV_SCHEMA}" username: ${DB_DEV_USERNAME} password: ${DB_DEV_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update properties: hibernate: show_sql: true format_sql: true open-in-view: false logging: level: sql: trace --- spring: config: activate: on-profile: prod --- spring: config: activate: on-profile: common2. Auditing 기능 개발다음으로 나는 spring data jpa에서 제공해주는 Auditing 기능을 먼저 이용하려고 한다. 기본 엔티티를 만들기 전에 추상클래스로 공통적인 속성들을 묶어서 만들기로 하였다.BaseDateTimeEntity.java@Getter @MappedSuperclass @EntityListeners(value = AuditingEntityListener.class) public abstract class BaseDateTimeEntity { @CreatedDate @Comment("생성 날짜") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @Column(nullable = false, updatable = false) private LocalDate createdAt; @LastModifiedDate @Comment("최종 수정 날짜") @Column(nullable = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate updatedAt; }BaseEntity.java@Getter @MappedSuperclass @EntityListeners(value = AuditingEntityListener.class) public class BaseEntity extends BaseDateTimeEntity { @CreatedBy @Comment("생성한 직원") @Column(nullable = false, updatable = false) private String createdBy; @LastModifiedBy @Comment("최종 수정한 직원") @Column(nullable = false) private String updatedBy; }먼저 위의 BaseDateTimeEntity와 같이 생성 날짜와 최종 수정 날짜를 정의하였고, BaseEntity에서는 추가적으로 생성한 직원 최종 수정한 직원 부분까지 더했다. 그 이유는 요구사항에는 BaseEntity 부분이 필요가 없겠지만 나중에 추후 확장성을 위해 사용하기로 하였다. 그리고 이를 위해 Auditing 설정 파일을 작성해주었다.AuditConfig.java@Configuration @EnableJpaAuditing public class AuditConfig { @Bean public AuditorAware<String> auditorAwareProvider() { return new AuditorAwareImpl(); } }AuditorAwareImpl.javapublic class AuditorAwareImpl implements AuditorAware<String> { @Override public Optional<String> getCurrentAuditor() { return Optional.of("tester"); } }공통 예외 부분우리가 예외를 처리하다보면 커스텀하게 예외를 던져야 할 경우가 생긴다. 그리고 예외가 던져졌을 때 에러 로그가 아니라 그에 대한 커스텀 응답을 받고 싶은 경우도 있을 것이다. 이에 따라 일련의 과정을 정리해본다.먼저 예외에 마다 특정 예외에 코드가 있다고 생각을 하였다. 그에 따른 인터페이스를 이와 같이 정의하였다.public interface ExceptionCode { HttpStatus getHttpStatus(); String getCode(); String getMessage(); }그리고 해당 인터페이스를 구현한 GlobalExceptionCode enum 클래스를 개발한다.@Getter @RequiredArgsConstructor public enum GlobalExceptionCode implements ExceptionCode { INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "G-001", "Invalid Input Value"), METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "G-002", "Invalid Http Request Method"), ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "G-003", "Resource Not Found"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "F-001", "Server Error!"); private final HttpStatus httpStatus; private final String code; private final String message; }이제 커스텀 예외 응답 클래스를 개발하자. 이번엔 조금 디자인 패턴 중 정적 팩터리 메서드 패턴을 적용해보았다.@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ExceptionResponse { private String message; private HttpStatus status; private String code; private List<ValidationException> errors; private LocalDateTime timestamp; private ExceptionResponse(final ExceptionCode exceptionCode) { this.message = exceptionCode.getMessage(); this.status = exceptionCode.getHttpStatus(); this.code = exceptionCode.getCode(); this.timestamp = LocalDateTime.now(); this.errors = new ArrayList<>(); } private ExceptionResponse(final ExceptionCode errorCode, final String message) { this.message = message; this.status = errorCode.getHttpStatus(); this.code = errorCode.getCode(); this.timestamp = LocalDateTime.now(); this.errors = new ArrayList<>(); } private ExceptionResponse(final ExceptionCode errorCode, final List<ValidationException> errors) { this.message = errorCode.getMessage(); this.status = errorCode.getHttpStatus(); this.code = errorCode.getCode(); this.timestamp = LocalDateTime.now(); this.errors = errors; } public static ExceptionResponse of(final ExceptionCode errorCode) { return new ExceptionResponse(errorCode); } public static ExceptionResponse of(final ExceptionCode errorCode, final String message) { return new ExceptionResponse(errorCode, message); } public static ExceptionResponse of(final ExceptionCode code, final BindingResult bindingResult) { return new ExceptionResponse(code, ValidationException.of(bindingResult)); } public static ExceptionResponse of(final ExceptionCode errorCode, final List<ValidationException> errors) { return new ExceptionResponse(errorCode, errors); } }다음으로 우리가 정의하지 않는 validation Exception부분도 처리해줄 필요가 있었다. 그래서 아래와 같이 커스텀하게 구성을 해보았다.@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ValidationException { private String field; private String value; private String reason; private ValidationException(String field, String value, String reason) { this.field = field; this.value = value; this.reason = reason; } public static List<ValidationException> of(final String field, final String value, final String reason) { List<ValidationException> validationExceptions = new ArrayList<>(); validationExceptions.add(new ValidationException(field, value, reason)); return validationExceptions; } public static List<ValidationException> of(final BindingResult bindingResult) { final List<FieldError> validationExceptions = bindingResult.getFieldErrors(); return validationExceptions.stream() .map(error -> new ValidationException( error.getField(), error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(), error.getDefaultMessage())) .collect(Collectors.toList()); } }마지막을 ExcpetionHandler를 통해 예외처리를 해두었다. 여기서 이제 커스텀 예외가 생길때 예외 클래스를 생성 후 RuntimeException을 상속받은 후에 해당 핸들러 클래스에 적용해두면 된다.@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * Java Bean Validation 예외 핸들링 */ @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity<ExceptionResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.error("handle MethodArgumentNotValidException"); return new ResponseEntity<>(ExceptionResponse.of(INVALID_INPUT_VALUE, e.getBindingResult()), INVALID_INPUT_VALUE.getHttpStatus()); } /** * EntityNotFound 예외 핸들링 */ @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<ExceptionResponse> handleEntityNotFoundException(EntityNotFoundException e) { log.error("handle EntityNotFoundException"); return new ResponseEntity<>( ExceptionResponse.of(ENTITY_NOT_FOUND, e.getMessage()), ENTITY_NOT_FOUND.getHttpStatus()); } /** * 유효하지 않은 클라이언트의 요청 값 예외 처리 */ @ExceptionHandler(IllegalArgumentException.class) protected ResponseEntity<ExceptionResponse> handleIllegalArgumentException(IllegalArgumentException e) { log.error("handle IllegalArgumentException"); return new ResponseEntity<>( ExceptionResponse.of(INVALID_INPUT_VALUE, e.getMessage()), INVALID_INPUT_VALUE.getHttpStatus() ); } @ExceptionHandler(TeamAlreadyExistsException.class) protected ResponseEntity<ExceptionResponse> handleTeamAlreadyExistsException(TeamAlreadyExistsException e) { log.error("handle TeamAlreadyExistsException"); return new ResponseEntity<>( ExceptionResponse.of(INVALID_INPUT_VALUE, e.getMessage()), INVALID_INPUT_VALUE.getHttpStatus() ); } /** * 잘못된 HTTP Method 요청 예외 처리 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) protected ResponseEntity<ExceptionResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.error("handle HttpRequestMethodNotSupportedException"); return new ResponseEntity<>( ExceptionResponse.of(METHOD_NOT_ALLOWED), METHOD_NOT_ALLOWED.getHttpStatus() ); } /** * 잘못된 타입 변환 예외 처리 */ @ExceptionHandler(BindException.class) protected ResponseEntity<ExceptionResponse> handleBindException(BindException e) { log.error("handle BindException"); return new ResponseEntity<>( ExceptionResponse.of(INVALID_INPUT_VALUE, e.getBindingResult()), INVALID_INPUT_VALUE.getHttpStatus() ); } /** * 모든 예외를 처리 * 웬만해서 여기까지 오면 안됨 */ @ExceptionHandler(Exception.class) protected ResponseEntity<ExceptionResponse> handleException(Exception e) { log.error("handle Exception", e); return new ResponseEntity<>( ExceptionResponse.of(INTERNAL_SERVER_ERROR), INTERNAL_SERVER_ERROR.getHttpStatus() ); } }주요기능팀 등록 기능🤔 고려해볼 점1. 팀을 등록할 수 있어야 한다.2. 팀 이름이 null이거나 공란으로 요청이 갈 경우 예외처리3. 만약 이미 존재하는 팀이라면 예외를 던진다.요청 DTOspring boot starter validation을 통하여 요청 필드에 대하여 validation 처리DTO를 엔티티화 하는 로직부분을 해당 DTO안에 구현public record RegisterTeamRequestDto( @NotBlank(message = "이름은 공란일 수 없습니다.") @NotNull(message = "이름은 null일 수 없습니다.") String name ) { public Team toEntity() { return Team.builder() .name(name) .build(); } }서비스 레이어별 다른 것은 없고 insert 쿼리가 날려주는 작업으로 메서드에 트랜잭션 어노테이션을 붙여주었다.private 메서드로 해당 팀이 이미 존재하는지 확인하는 validation을 추가하였다. @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class TeamService { private final TeamRepository teamRepository; @Transactional public void registerTeam(RegistrationTeamRequestDto requestDto) { validateTeam(requestDto); Team team = requestDto.toEntity(); this.teamRepository.save(team); } /** * 팀 유효성 검사 * @param requestDto */ private void validateTeam(RegistrationTeamRequestDto requestDto) { if (this.teamRepository.existsByName(requestDto.name())) { throw new TeamAlreadyExistsException("이미 존재하는 팀 이름입니다."); } } }요청으로 온 DTO의 이름으로 유효한 팀인지 검사 후, 해당 DTO를 엔티티로 변환하고 저장시킨다.컨트롤러 레이어package me.sungbin.domain.team.controller; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import me.sungbin.domain.team.model.request.RegistrationTeamRequestDto; import me.sungbin.domain.team.model.response.TeamInfoResponseDto; import me.sungbin.domain.team.service.TeamService; import org.springframework.web.bind.annotation.*; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.domain.team.controller * @fileName : TeamController * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/team") @RequiredArgsConstructor public class TeamController { private final TeamService teamService; @PostMapping("/register") public void registerTeam(@RequestBody @Valid RegistrationTeamRequestDto requestDto) { this.teamService.registerTeam(requestDto); } }  테스트 결과성공(포스트맨)실패(이미 중복된 팀 이름)실패 (팀 이름이 공란이거나 null)테스트 코드package me.sungbin.domain.team.controller; import me.sungbin.domain.team.entity.Team; import me.sungbin.domain.team.model.request.RegistrationTeamRequestDto; import me.sungbin.domain.team.repository.TeamRepository; import me.sungbin.global.common.controller.BaseControllerTest; import me.sungbin.global.exception.GlobalExceptionCode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; 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.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author : rovert * @packageName : me.sungbin.domain.team.controller * @fileName : TeamControllerTest * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ class TeamControllerTest extends BaseControllerTest { @Autowired private TeamRepository teamRepository; @BeforeEach void setup() { Team team = new Team("개발팀"); this.teamRepository.save(team); } @Test @DisplayName("팀 등록 테스트 - 실패 (팀 이름이 공란)") void register_team_test_fail_caused_by_team_name_is_empty() throws Exception { RegistrationTeamRequestDto requestDto = new RegistrationTeamRequestDto(""); this.mockMvc.perform(post("/api/team/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isNotEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("팀 등록 테스트 - 실패 (이미 존재하는 팀)") void register_team_test_fail_caused_by_already_exists_team() throws Exception { RegistrationTeamRequestDto requestDto = new RegistrationTeamRequestDto("개발팀"); this.mockMvc.perform(post("/api/team/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("팀 등록 테스트 - 성공") void register_team_test_success() throws Exception { RegistrationTeamRequestDto requestDto = new RegistrationTeamRequestDto("디자인팀"); this.mockMvc.perform(post("/api/team/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("팀 정보 조회 테스트 - 성공") void find_team_info_test_success() throws Exception { this.mockMvc.perform(get("/api/team") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); } }기본으로 사용하는 어노테이션들을 아래의 어노테이션으로 묶음package me.sungbin.global.common.annotation; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author : rovert * @packageName : me.sungbin.global.annotation * @fileName : IntegrationTest * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Transactional @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface IntegrationTest { }이 어노테이션을 BaseControllerTest라는 클래스에 선언@Disabled @IntegrationTest public class BaseControllerTest { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; }추가적으로 test 디렉터리에도 resources 디렉터리 생성 후 해당 경로에 application.yml을 생성후 테스트 profile active시켜두었다.spring: profiles: active: test --- spring: config: activate: on-profile: test datasource: url: "jdbc:h2:mem:commutedb" username: sa password: driver-class-name: org.h2.Driver jpa: properties: hibernate: show_sql: true format_sql: true open-in-view: false threads: virtual: enabled: true직원 등록 기능🤔 고려점1. 직원을 먼저 생성한다. (필수 값들은 공란일 수 없음)2. 해당 직원을 팀에 등록 시킨다. (단, 등록할 직원이 매니저인 경우 해당 팀의 매니저가 없어야 한다.)3. 등록하려는 팀이 존재해야 한다.주요 코드를 보자. 먼저 연관관계 매핑을 해야한다.package me.sungbin.domain.employee.entity; import jakarta.persistence.*; import lombok.*; import me.sungbin.domain.employee.type.Role; import me.sungbin.domain.team.entity.Team; import me.sungbin.global.common.entity.BaseDateTimeEntity; import org.hibernate.annotations.Comment; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.domain.member.entity * @fileName : Member * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Entity @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AttributeOverrides({ @AttributeOverride(name = "createdAt", column = @Column(name = "work_start_date", nullable = false, updatable = false)), @AttributeOverride(name = "updatedAt", column = @Column(name = "updated_at", nullable = false)) }) public class Employee extends BaseDateTimeEntity { @Id @Comment("직원 테이블 PK") @Column(name = "employee_id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Comment("직원 이름") @Column(name = "employee_name", nullable = false) private String name; @Comment("팀의 매니저인지 아닌지 여부") @Column(nullable = false) private boolean isManager; @Column(nullable = false) private LocalDate birthday; @Builder public Employee(String name, boolean isManager, LocalDate birthday) { this.name = name; this.isManager = isManager; this.birthday = birthday; } @ManyToOne(fetch = FetchType.LAZY) private Team team; public void updateTeam(Team team) { this.team = team; } public String getTeamName() { return this.team.getName(); } public String getRole() { return isManager ? Role.MANAGER.name() : Role.MEMBER.name(); } } package me.sungbin.domain.team.entity; import jakarta.persistence.*; import lombok.*; import me.sungbin.domain.employee.entity.Employee; import me.sungbin.global.common.entity.BaseDateTimeEntity; import org.hibernate.annotations.Comment; import java.util.ArrayList; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.domain.team.entity * @fileName : Team * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Entity @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AttributeOverrides({ @AttributeOverride(name = "createdAt", column = @Column(name = "created_at", nullable = false, updatable = false)), @AttributeOverride(name = "updatedAt", column = @Column(name = "updated_at", nullable = false)) }) public class Team extends BaseDateTimeEntity { @Id @Comment("팀 테이블 PK") @Column(name = "team_id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Comment("팀 이름") @Column(name = "team_name", nullable = false, unique = true) private String name; @OneToMany(mappedBy = "team") private List<Employee> employees = new ArrayList<>(); @Builder public Team(String name) { this.name = name; } public void addEmployee(Employee employee) { this.employees.add(employee); employee.updateTeam(this); } public String getManagerName() { return employees.stream() .filter(Employee::isManager) .map(Employee::getName) .findFirst() .orElse(null); } public boolean hasManager() { return this.employees.stream().anyMatch(Employee::isManager); } public int getEmployeeCount() { return employees != null ? employees.size() : 0; } }위와 같이 연관관계 매핑을 해준다. 여기서 Employee의 getRole부분의 메서드의 Role은 enum타입으로 아래와 같이 되어 있다.package me.sungbin.domain.employee.type; import lombok.Getter; import lombok.RequiredArgsConstructor; import me.sungbin.global.common.type.EnumType; /** * @author : rovert * @packageName : me.sungbin.domain.member.type * @fileName : Role * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Getter @RequiredArgsConstructor public enum Role implements EnumType { MEMBER("MEMBER", "팀원"), MANAGER("MANAGER", "매니저"); private final String name; private final String description; }위의 코드를 보면 EnumType이라는 인터페이스가 있는데 그 안에는 아래와 같다.package me.sungbin.global.common.type; /** * @author : rovert * @packageName : me.sungbin.global.common.type * @fileName : EnumType * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ public interface EnumType { String name(); String getDescription(); }이렇게 한 이유는 나중의 확장성 때문에 구현을 해둔 것이다.@Transactional public void registerEmployee(RegistrationEmployeeRequestDto requestDto) { Employee employee = requestDto.toEntity(); Team team = this.teamRepository.findByName(requestDto.teamName()).orElseThrow(TeamNotFoundException::new); // 매니저가 이미 존재하는 경우 예외 발생 if (employee.isManager() && team.hasManager()) { throw new AlreadyExistsManagerException("이미 매니저가 해당 팀에 존재합니다."); } this.employeeRepository.save(employee); team.addEmployee(employee); this.teamRepository.save(team); }그리고 위와 같이 서비스 로직을 작성해준다. 해당 로직은 dto로부터 엔티티화 시키고 요청한 팀의 이름으로 팀이 존재하는지 찾는다.만약 없으면 예외를, 있다면 해당 팀에 매니저가 존재하는지 유무도 추가해두었다. 이미 있다면 예외를 없다면 해당 직원을 저장시킨다. 컨트롤러 레이어package me.sungbin.domain.employee.controller; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import me.sungbin.domain.employee.model.request.EmployeesInfoResponseDto; import me.sungbin.domain.employee.model.request.RegistrationEmployeeRequestDto; import me.sungbin.domain.employee.service.EmployeeService; import org.springframework.web.bind.annotation.*; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.domain.member.controller * @fileName : EmployeeController * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @RestController @RequiredArgsConstructor @RequestMapping("/api/employee") public class EmployeeController { private final EmployeeService employeeService; @PostMapping("/register") public void registerEmployee(@RequestBody @Valid RegistrationEmployeeRequestDto requestDto) { this.employeeService.registerEmployee(requestDto); } }  테스트성공실패 (존재하는 팀이 없음)실패(이미 그 팀에 매니저가 있음)테스트코드class EmployeeControllerTest extends BaseControllerTest { @Autowired private TeamRepository teamRepository; @Autowired private EmployeeRepository employeeRepository; @BeforeEach void setup() { Team team = new Team("개발팀"); this.teamRepository.save(team); } @Test @DisplayName("직원 등록 테스트 - 실패 (잘못된 입력 값)") void register_employee_test_fail_caused_by_wrong_input() throws Exception { RegistrationEmployeeRequestDto requestDto = new RegistrationEmployeeRequestDto("", "", false, LocalDate.of(1996, 5, 22)); this.mockMvc.perform(post("/api/employee/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isNotEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("직원 등록 테스트 - 실패 (존재하지 않는 팀에 등록)") void register_employee_test_fail_caused_by_register_not_exists_team() throws Exception { RegistrationEmployeeRequestDto requestDto = new RegistrationEmployeeRequestDto("장그래", "영업팀", false, LocalDate.of(1992, 2, 22)); this.mockMvc.perform(post("/api/employee/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("직원 등록 테스트 - 성공") void register_employee_test_success() throws Exception { RegistrationEmployeeRequestDto requestDto = new RegistrationEmployeeRequestDto("양성빈", "개발팀", false, LocalDate.of(1996, 5, 22)); this.mockMvc.perform(post("/api/employee/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } }팀 조회 기능서비스 레이어public List<TeamInfoResponseDto> findTeamInfo() { List<Team> teams = this.teamRepository.findAll(); return teams.stream().map(TeamInfoResponseDto::new).toList(); }해당 팀들을 findAll로 select한 이후로 응답 DTO로 매핑해준다.아래는 포스트맨 테스트 결과다.이제 테스트 코드를 살펴보자.@Test @DisplayName("팀 정보 조회 테스트 - 성공") void find_team_info_test_success() throws Exception { this.mockMvc.perform(get("/api/team") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); }직원 조회 기능서비스 레이어public List<EmployeesInfoResponseDto> findEmployeesInfo() { List<Employee> employees = this.employeeRepository.findAll(); return employees.stream().map(EmployeesInfoResponseDto::new).toList(); }전체 직원을 select하여 stream 객체를 이용하여 응답 DTO와 매핑해주었다. 아래는 테스트 결과다.아래는 테스트 코드다.@Test @DisplayName("직원 정보 조회 테스트 - 성공") void find_employees_info_test_success() throws Exception { this.mockMvc.perform(get("/api/employee") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); }회고1단계는 이제까지 우리가 배운 개념들로 충분히 개발 할 수 있는 것들이였다. 하지만 나는 여기서 더 나아가서 좀 더 예외상황을 생각해보고 더 발전시키도록 노력했다. 그리고 또한 다른 러너분들과 코드리뷰를 통해 내 코드를 리팩토링 해가면서 뭔가 실력이 점점 쌓여만 가는 것 같았다. 

백엔드인프런워밍업스터디클럽미니프로젝트

양성빈

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

발자국어느덧 인프런 워밍업 스터디 클럽을 시작한지도 2주째가 시작된다. 그리고 이번주 1주에 대한 회고를 시작해보려고 한다.이번주도 여러가지를 배우고 많은 경험이 된 한 주였다. 그럼 회고를 시작하겠다. 완주 및 우수러너를 위해 오늘도 달려본다.강의 요약Day 6. 스프링 컨테이너의 의미와 사용방법📖 UserController와 스프링 컨테이너상식적으로 static이 아닌 코드를 사용하려면 객체화(인스턴스화)가 필수적이다. 하지만 이전 학습의 UserController부분을 확인해보면 의아한 부분이 존재한다.private final UserService userService; public UserController(JdbcTemplate jdbcTemplate) { this.userService = new UserService(jdbcTemplate); }이렇게 UserService는 UserController 생성자 부분에서 인스턴스화를 하였지만, 정작 UserController부분은 인스턴스화를 해주지 않았지만 잘 작동하는 것을 알 수 있었다. 이로 인해 아래의 의문점이 남아진다.🙋🏻 그럼 누가 UserController부분을 인스턴스화 시켜준다는건데 누가 그런 걸 해주나요?또한 위의 코드에서 또 하나의 의문점이 남는다.🙋🏻 그리고 나는 JdbcTemplate 클래스를 따로 만져준 적이 없는데 UserController 클래스는 어떻게 이 클래스를 가져올 수 있을까요?바로 @RestController라는 어노테이션때문이다. 우리는 앞전에 @RestController라는 어노테이션이 API 진입점이라고 배웠다. 하지만 이 @RestController는 진입점의 역할과 더불어 UserController 클래스를 스프링 빈으로 등록을 시켜준다.🙋🏻 그럼 스프링 빈이 뭐에요? 빈은 영어니까 번역하면 콩인것 같은데 그럼 스프링 콩인가요?위의 질문이 나는 자연스럽게 떠올랐다. 그럼 정확히 스프링 빈이 무엇인지 알아보자.🫛 스프링 빈우리가 스프링 부트로 만든 프로젝트를 동작시키면, 우리가 만든 서버가 동작을 하는 것이다. 그러면 이 서버 내부에 거대한 컨테이너를 만들어준다. 그리고 컨테이너 안에는 빈으로 등록시킨 클래스 정보(이름, 타입)가 들어간다. 그리고 이 클래스를 인스턴스화 시켜준다. 이 때, 들어간 클래스를 스프링 빈이라고 부른다.🙋🏻 그런데 여기서 위의 코드를 보면 UserController를 인스턴스화할려면 JdbcTemplate가 필요하지 않나요?요놈은 어디서 가져오는 거에요?사실, JdbcTemplate 클래스도 빈으로 등록된 클래스이다.🙋🏻 그럼, 누가 JdbcTempalte을 인스턴스화 시켜줬어요?바로 build.gradle에 dependencies에 등록한 spring boot starter data jpa라는 것이 JdbcTemplate을 등록시켜줬다.dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' }그래서 인텔리제이로 UserController 생성자 부분을 보면 책모양으로 아이콘이 있는데 이것이 빈으로 등록되었다는 의미이다.즉, 결론을 내보면 우리가 가져온 Dependency가 JdbcTemplate을 빈으로 등록시켜준다는 의미이다.그러면 여기서 또 하나 결론이 나온다. 스프링 컨테이너 안에 우리가 작성한 스프링 빈으로 등록한 클래스는 이 컨테이너 안에 들어가게 된다. 또한 필요한 의존성이 자동 설정된다.그럼 여기서 의문점이 든다. 우리가 이전에 작성한 UserRepository 클래스와 UserService 클래스도 JdbcTemplate의 의존성이 필요하고 이 JdbcTemplate을 가져오려면 이 두개의 클래스도 빈으로 등록이 되어있어야 한다. 하지만 이 2개의 클래스는 빈으로 등록되지 않았다. 인텔리제이 화면만 봐도 책 모양 아이콘이 존재하지 않는다. 그럼 2개의 클래스를 빈으로 등록시키자! 🫛 Repository와 Service 빈 등록시키기 & Controller 클래스 변경두개의 클래스를 빈으로 등록시키는 방법은 정말 간단하다. Repository 클래스는 @Repository 어노테이션을 클래스 위에 붙여주고, Service 클래스는 @Service 어노테이션을 클래스 위에 붙여주면 빈으로 등록이 된다. 그리고 Controller부분을 수정해본다. 그럼 아래와 같이 변경될 것이다. 코드는 일부만 표기하겠다.UserController.java@RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } ... UserService.java@Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } ... UserRepository.java@Repository public class UserRepository { private final JdbcTemplate jdbcTemplate; public UserRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } ... 📚 정리그러면 한번 정리해보자. 스프링 서버가 시작이 되면 의존성에 의해 빈으로 등록된 JdbcTemplate이 스프링 컨테이너로 들어간다. 그리고 이 JdbcTemplate의 의존성을 가진 UserRepository가 빈으로 등록된다. 그러면 UserRepository를 의존하는 UserService가 빈으로 등록된다. 그리고 UserService를 의존하는 UserController가 빈으로 등록된다.그런데 아래의 의문점이 든다.🤔 아니 뭐가 좋아진거야? 그냥 new 연산자로 객체생성하면 안되는건가? 스프링 컨테이너 왜 쓰는데?이 의문점은 다음 강의에서 해소가 되었다.📖 스프링 컨테이너를 왜 사용할까?만약 아래의 요구사항이 있다고 하자.책 이름을 저장하는 API를 구현하라. 단, 이름 저장은 메모리에 저장시킨다.우리는 그럼 열심히 비즈니스 로직을 만들 것이다. 먼저 Book 객체부터 만들고, BookController, BookService, BookMemoryRepository를 만들 것이다. 그리고 BookMemoryRepository를 BookService는 아래와 같이 객체를 생성할 것이다.그런데 이렇게 열심히 만들고나니 추가 요구사항이 생겼다.public class BookService { private final BookMemoryRepository repository = new BookMemoryRepository(); } 생각해보니, 메모리가 아닌 MySQL과 같은 RDB에 저장시켜야해! 그리고 JdbcTemplate은 Repository가 바로 설정할 수 있다 하자.그러면 BookMySQLRepository를 만들고 BookService에 BookMemoryRepository가 아닌 BookMySQLRepository를 인스턴스화 해줘야 한다.public class BookService { private final BookMySQLRepository repository = new BookMySQLRepository(); }이런 과정을 하면서 우리는 불편함을 느꼈을 것이다. 우리는 repository의 기능적인 역할만 변경하였는데 서비스 코드까지 변경해야하는 경우가 생긴 것이다. 지금은 몇개 안되지만, 이 repository를 쓰는 서비스 코드가 수백개 클래스에 있다면 바로 오늘 야근을 해야하고 야근 신청서를 올려야 한다.🥲그러면 이런 야근을 피하기 위해서 repository를 변경하더라도 서비스 클래스는 변경을 안하는 방법은 없을까? 그래서 생각을 한 것이 java의 interface를 이용하는 방법이다. BookRepository라는 인터페이스를 만들고 BookMemoryRepository와 BookMySQLRepository를 구현하면 되는 것이다. 그러면 서비스 코드는 이런 식으로 변경 될 것이다.public class BookService { private final BookRepository repository = new BookMySQLRepository(); }하지만 그래도 서비스 코드는 repository 역할 변경에 다라 수정이 되긴 해야한다. 바로 new 연산자의 부분을 전부 변경해야 하기 때문이다. 또 야근 당첨이다 🥲 그러면 이걸 또 해결할 수 있는 방법은 없을까? 바로 스프링 컨테이너가 그래서 등장하였다.스프링 컨테이너가 BookService 대신 repository를 인스턴스화 해주고 그때 그때 알아서 어떤 repository 클래스를 쓸지 결정을 해줄 수 있다. 이런 방식을 제어의 역전(IoC, Inversion of Control)이라고 한다. 그리고 컨테이너가 repository 클래스를 선택해서 서브스 레이어에 넣어주는 과정의 의존성 주입(DI, Dependency Injection)라고 한다.그러면 어떤 Repository를 주입시켜줄까? 그것은 우리가 @Primary 어노테이션을 활용해 조절할 수 있다.@Primary: 우선권을 결정하는 어노테이션📖 스프링 컨테이너를 다루는 방법@Configuration: 클래스에 붙여주는 어노테이션, @Bean 어노테이션과 같이 사용@Bean: 보통은 메서드 위에 붙으며, 해당 메서드에서 반환되는 객체를 스프링 빈으로 등록시켜준다.그리고 아래의 의문사항이 든다. 그러면 우리가 이전에 @Service, @Repository 어노테이션을 붙여줬는데 이 어노테이션은 언제 사용해야할까? 위의 @Configuration + @Bean 어노테이션을 쓰면 안될까?요약하자면 다음과 같다.@Service나 @Repository 어노테이션은 개발자가 직접 만든 클래스를 빈으로 등록시키고 싶을 때 사용하며,@Configuration + @Bean 어노테이션은 외부 라이브러리나 프레임워크에서 만든 클래스를 등록시킬때 사용한다.다음으로 살펴 볼 어노테이션은 @Component 어노테이션이다.@Component: 주어진 클래스를 컴포넌트로 간주하며, 이 클래스들은 스프링 서버가 시작할 때 자동감지한다.@Component 어노테이션 덕분에 우리가 사용했던 어노테이션들이 감지가 된것이다.Service.javapackage org.springframework.stereotype; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Service { @AliasFor( annotation = Component.class ) String value() default ""; }Repository.javapackage org.springframework.stereotype; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Repository { @AliasFor( annotation = Component.class ) String value() default ""; }이렇게 각 어노테이션들의 내부구조를 보면 이렇게 @Component 어노테이션이 들어가져 있다.그럼 @Component 어노테이션은 언제 사용할까?컨트롤러, 서비스, 리포지토리가 모두 아니고 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용한다.🫛 빈 주입 받는 방법빈을 주입받는 방법은 3가지가 존재한다.생성자를 이용한 주입방법 (권장)setter와 @Autowired -> 누군가 setter를 사용하면서 오작동 가능성이 존재private final JdbcTemplate jdbcTemplate; @Autowired public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; }필드에 직접 @Autowired 사용 -> 테스트 어려움@Autowired private JdbcTemplate jdbcTemplate;마지막으로 @Qualifier 어노테이션을 알아보자. @Primary와 유사하다.스프링 빈을 사용하는 쪽, 스프링 빈을 등록하는 쪽 모두 @Qualifier를 사용할 수 있다!스프링 빈을 사용하는 쪽에서만 쓰면, 빈의 이름을 적어주어야 한다. 양쪽 모두 사용하면, @Qualifier 끼리 연결된다!@Service @Qualifier("main") public class BananaService implements FruitService { }@RestController public class UserController { private final UserService userService; private final FruitService fruitService; public UserController(UserService userService, @Qualifier("main") FruitService fruitService) { this.userService = userService; this.fruitService = fruitService; }그러면 @Qualifier와 @Primary 어노테이션중에 누가 우선순위가 높을까?사용하는 쪽에서 직접 적어준 @Qualifier가 이긴다!📚섹션3 정리클린코드가 무엇이고, 우리의 코드를 레이어 아키텍쳐로 분리도 해보며, 스프링 컨테이너가 무엇이고 스프링 빈이 무엇인지 이해를 하며 어떤 어노테이션을 통해 주입을 받고 빈으로 등록할 수 있는지 알아보았다. Day7. Spring Data JPA를 사용한 데이터베이스 조작📖 문자열 SQL을 직접 사용하는 것이 너무 어렵다!!우리는 현재 레이어드 아키텍쳐로 코드를 작성하였고, 해당 빈들을 스프링 컨테이너가 관리를 하였고 포스트맨을 통하여 API를 호출하였다. 또한 repository 레이어로 mysql과 통신을 하였다. 그런데 repository 레이어에서는 DB 쿼리를 문자열로 작성하였다. 하지만 이렇게 문자열로 작성하면 아래와 같은 문제가 있을 수 있다.문자열로 작성 시, 오타가 날 수 있는 실수가 있다. 하지만 이 실수는 컴파일 타임에 발견되지 않고 런타임에 발견되는 안 좋은 점이 있다. 그래서 어플리케이션 운영 시점에 해당 API를 사용 시, 에러를 확인할 수 있기에 엄청 치명적이다.특정 DB에 종속적이다. 만약 우리가 MySQL을 쓰다가 어느 이유로 DB를 변경하게 된다면 해당 쿼리들을 변경하는 DB 쿼리 문법에 맞게 수정해줘야한다. 마이그레이션도 일이지만 해당 쿼리를 다 고쳐야한다면 야근 당첨일 것이다. 🥲반복 작업이 많아진다. 보통 테이블당 기본적으로 CRUD 쿼리를 작성해줘야 하는데, 단순 반복작업들이 이어질 수 있다.데이터베이스 테이블과 객체의 패러다임이 다르다. 쉽게 생각해서 연관관계 매칭을 할 때 양방향 매핑을 할 때 연관관계 갖는 테이블 A는 B를 가리키고 B또한 A클래스를 가리킬 수 있지만 실제 테이블은 한쪽만 가리키게 된다. 또한 상속개념은 자바는 존재하지만 DB는 상속개념을 구현하기 매우 힘들다. 그래서 JPA라는 것이 등장하였다. JPA는 ORM의 일종인데 이 두 용어를 살펴보면 아래와 같다.JPA(Java Persistence API) : 자바 영속성 API그럼 영속성은 무엇일까? 우리는 이전에 메모리에 회원 정보를 저장하는 코드를 작성했지만 이런 코드는 서버를 재부팅하면 데이터는 날라간다. 그 이유는 RAM에 데이터가 저장되기 때문이다. 그런데 영속성은 서버가 재부팅되어도 데이터는 영구적으로 저장되는 속성을 의미한다.그리고 API는 일종의 규칙이다. 그래서 이것을 풀어써보면 아래와 같다.JPA란, 데이터를 영구적으로 보관하기 위해 자바 진영에서 정해진 규칙을 뜻한다.그러면 ORM은 무엇일까? 자바코드와 DB의 테이블을 짝 지어준다는 의미이다.📚 요약 (JPA란?)객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 자바 진영의 규칙을 뜻한다.그런데 JPA를 검색해보면 연관검색으로 Hibernate가 나온다. 이 Hibernate란 무엇일까?JPA는 쉽게 규칙이라고 하였다. 이 규칙을 구현한 구현체가 Hibernate이다. 또한 Hibernate은 내부적으로 JDBC를 사용한다. 📖 유저 테이블에 대응되는 Entity Class 만들기이제 실제로 유저 테이블과 유저 클래스를 매핑시켜보자. 이를 위해선 어노테이션 @Entity를 붙여줘야 한다.🙋🏻 Entity란?저장되고 관리되어야 하는 데이터를 의미한다.유저 테이블은 위와 같이 구성되어 있다. 먼저, id를 primary key로 설정되어 있고 auto_increment가 적용되어 있다. 이것을 자바 코드에 적용하려면 @Id와 @GeneratedValue(strategy=GenerationType.IDENTY)를 설정해줘야 한다. 그렇게 적용한 코드는 아래와 같다.@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; .... } DB의 종류마다 자동 생성 전략이 다르다!우리는 MySQL의 auto_increment를 사용했고, 이는 IDENTITY 전략과 매칭된다. JPA를 사용하기 위해 기본 생성자가 반드시 필요하다.다음으로 name 부분을 짝 지어줘야 한다.이를 위해서 @Column 어노테이션을 통해 매핑해줘야 한다.@Column(nullable = false, length = 20, name = "name") private String name;여기서 nullable = false는 이 속성은 null이 불가능하다는 의미이며, length = 20은 DB로 보면 varchar(20)을 의미한다.또한 name = "name"은 이 속성은 테이블의 name 필드와 매핑시키겠다는 의미이다.⚠ 참고참고로 name은 필드이름과 동일할 경우 생략이 가능하다.그리고 이런 nullable, length등 이런 속성을 기본으로 쓸 때 @Column 어노테이션 자체를 생략이 가능하다.이제 application.yml로 JPA 설정을 해줘야 한다. jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true dialect: org.hibernate.dialect.MySQLDialectddl-auto: 스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지create : 기존 테이블이 있다면 삭제 후 다시 생성create-drop : 스프링이 종료될 때 테이블을 모두 제거update : 객체와 테이블이 다른 부분만 변경validate : 객체와 테이블이 동일한지 확인none : 별다른 조치를 하지 않는다.show_sql: JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄 것인가format_sql: SQL을 보여줄 때 예쁘게 포맷팅 할 것인가, 여기서 예쁘게는 뭔가를 꾸미는게 아니라 우리가 쉽게 볼 수 있게 포맷팅을 해준다는 것이다.dialect: 방언(사투리), 이 옵션으로 DB를 특정하면 조금씩 다른 SQL을 수정해준다.⚠ 주의강좌에서는 방언 설정을 할 때 org.hibernate.dialect.MySQLDialect를 org.hibernate.dialect.MySQL8Dialect로 하셨다. 하지만 최근에 org.hibernate.dialect.MySQL8Dialect가 deprecated가 되었다는 warning이 발생한다. 그리고 org.hibernate.dialect.MySQLDialect로 변경하라고 써져있다.📖 Spring Data JPA를 이용해 자동으로 쿼리 날리기우리는 이제 직접 sql을 작성해주지 않고 JPA를 이용하여 유저의 생성/조회/업데이트 기능을 리팩토링할 것이다.먼저 아래와 같이 Repository 인터페이스를 만들어준다.public interface UserRepository extends JpaRepository<User, Long> { }그리고 서비스 코드에서 해당 UserRepository로 의존성 주입을 한다.다음으로 생성 부분 메서드를 만들어보자.public void saveUser(UserCreateRequest request) { this.userRepository.save(new User(request.getName(), request.getAge())); }여기서 save 메서드는 JpaRepository를 상속받은 Repository에 정의되어 있지 않지만 사용이 가능하다. 그 이유는 Spring Data JPA에서 기본으로 제공해주는 저장 로직이 담긴 로직이다. 해당 메서드를 실행하면 insert 쿼리가 날라간다.다음으로 조회 부분 메서드를 보자.public List<UserResponse> getUsers() { return this.userRepository.findAll() .stream().map(UserResponse::new) .collect(Collectors.toList()); }여기서 findAll 메서드도 기본으로 제공한다. 이 메서드의 반환은 List형태이다. 이 메서드를 실행하면 select * ~ 쿼리가 날라간다.다음으로 업데이트 기능으로 보자. 업데이트는 유저가 존재하는지 확인하고 있다면 update쿼리를 아니면 예외를 날린다.public void updateUser(UserUpdateRequest request) { User user = this.userRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new); user.updateName(request.getName()); this.userRepository.save(user); }먼저 findById라는 메서드를 호출한다. 이 메서드는 기본으로 제공해주는 메서드로 해당 메서드는 select * from user where id = ?의 쿼리를 날려준다. 이 메서드의 반환타입은 1개의 데이터를 가져오기 때문에 객체 단일 타입으로 반환된다. 여기선 User가 반환된다. 그리고 updateName이라는 메서드를 엔티티에 만들어준다. 이 메서드는 단순 setter의 역할이다. 마지막으로 setter로 속성 변경을 한 후 save로 저장을 시킨다.그럼 여기서 이렇게 메서드를 통해 쿼리 작성없이 쿼리가 날라갈 수 있는 이유는 JPA가 아닌 Spring Data JPA 때문이다.Spring Data JPA: 복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리즉, 전체적인 구조를 보면 Spring Data JPA가 JPA라는 규칙을 사용하는데 이 규칙은 Hibernate가 이 규칙을 구현했고 Hibernate는 구현할때 JDBC를 사용한다고 볼 수 있다. 📖 Spring Data JPA를 이용해 다양한 쿼리 작성하기이제 삭제 기능을 Spring Data JPA로 변경해보자. 먼저 삭제는 요청으로 들어온 유저의 이름이 존재하는지 확인하고 있다면 삭제쿼리를 날리고 아니면 예외를 날린다.public void deleteUser(String name) { User user = this.userRepository.findByName(name).orElseThrow(IllegalArgumentException::new); this.userRepository.delete(user); }여기서 나온게 findByName과 delete 메서드이다. findByName은 기본으로 제공해준 메서드가 아니고 우리가 인터페이스에 정의를 해야한다.public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByName(String name); }이런식으로 정의를 하면 이 메서드를 사용할때 select * from user where name = ? 쿼리가 나간다.다음으로 delete 메서드는 기본으로 제공해주는 메서드이다. 이 메서드를 사용하면 delete SQL이 나간다.이제 구체적으로 findByName처럼 우리가 일정 규칙에 맞게 인터페이스에 정의를 하면 쿼리들을 제공해주는데 그 규칙들을 살펴보자.find : 1건을 가져온다. 반환 타입은 객체가 될 수도 있고, Optional<타입>이 될 수도 있다.findAll : 쿼리의 결과물이 N개인 경우 사용. List<타입> 반환.exists : 쿼리 결과가 존재하는지 확인. 반환 타입은boolean count : SQL의 결과 개수를 센다. 반환 타입은 long이다.이제 By뒤에 규칙을 알아볼 텐데 By뒤에는 where 조건을 적어주는 것처럼 적어주면 된다. 조건이 여러개일 경우 And 혹은 or 조건을 통해 규칙을 정해준다.🏛 예시List<User> findAllByNameAndAge() : select * from user where name = ? and age = ?그외에 아래와 같이 다양한 조건들을 붙일 수 있다.📚 By 뒤에 조건GreaterThan : 초과GreaterThanEqual : 이상LessThan : 미만LessThanEqual : 이하Between : 사이에StartsWith : ~로 시작하는EndsWith : ~로 끝나는 Day8. 트랜잭션과 영속성 컨텍스트📖 트랜잭션 이론편트랜잭션이란 무엇일까? 트랜잭션 말만 들어봤지 이게 정확히 무슨 의미인지 알지 못했다. 트랜잭션은 아래와 같이 말한다.트랜잭션: 쪼갤 수 없는 업무의 최소 단위 = 모두 성공시키거나, 모두 실패시킨다.상황을 살펴보자.쇼핑몰이 있다고 하자. 어떤 회원이 주문을 하는 상황을 생각해보자. 주문을 하면 주문내역이 저장되고 포인트가 저장되고 결제기록이 저장될 것이다. 이 비즈니스 로직은 하나의 메서드로 묶여 있다. 그러다가 어떠한 이유로 결제기록의 비즈니스 로직에서 에러가 발생했다고 하자. 그러면 주문내역과 포인트는 있는데 결제되었다는 사실이 없을 것이다. 이런 경우 특정 비즈니스 로직에 에러가 발생할 경우 모든 SQL을 실패시켜야 할 것이다. 물론 모두 성공할 경우 성공시켜야 할 것이다. 이것을 트랜잭션이 해결해준다.DB 쿼리로 트랜잭션 시작을 알리는 쿼리는 아래와 같다.start transaction;트랜잭션 정상 종료는 아래와 같다.commit;트랜잭션 실패 처리는 아래와 같다.rollback;이 실습을 통해 알게 된 점은 트랜잭션 안에 저장/업데이트/삭제 쿼리가 발생해도 commit 전까지 반영이 안 된다는 점이다. 📖트랜잭션 적용과 영속성 컨텍스트Spring Data JPA에서 트랜잭션 적용은 @Transactional 어노테이션으로 해결할 수 있다. 이 어노테이션은 서비스 레이어의 저장/업데이트/삭제 로직에 붙일 수 있다. 조회로직에는 @Transactional(readOnly = true)로 쓸 수 있다.그리고 강좌에서 아래와 같이 말씀하셨다.⚠ 주의CheckedException은 롤백이 일어나지 않는다.하지만 이 점이 궁금해서 알아본 결과 아래와 같다.RuntimeException이든 CheckedException이든 rollback을 할지 말지는 우리가 결정할 수 있다. 바로 @Transactional의 rollbackFor이라는 옵션을 통해서다. 다만, 기본적으로는 CheckedExcpetion은 rollback을 하지 않고 RuntimeExcpetion은 rollback을 해준다. 이점을 명심하자.영속성 컨텍스트는 테이블과 매핑된 Entity 객체를 관리/보관하는 역할을 한다. 즉, 쉽게 말해서 스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.영속성 컨텍스트에는 마치 초능력자처럼 능력을 몇가지 가지고 있다.변경감지(Dirty Check): 영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다. 그래서 이전에 업데이트 로직에서 마지막에 save로직으로 저장을 했는데 @Transactional 어노테이션이 붙으면 아래와 같이 작성이 가능하다.@Transactional public void updateUser(UserUpdateRequest request) { User user = this.userRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new); user.updateName(request.getName()); }쓰기 지연: DB의 INSERT / UPDATE / DELETE SQL을 바로 날리는 것이 아니라, 트랜잭션이 commit될 때 모아서 한 번만 날린다. 이런 기능이 없다면 save 메서드가 3개가 있을 때 insert 쿼리를 일일이 3번 날리는게 아니라 일단 영속성 컨텍스트가 기억하고 한번에 날려준다.1차 캐싱: 똑같은 객체를 조회하는 로직이 있을 때 조회하는 만큼 일일이 조회쿼리를 날려주는게 아니라 처음에 영속성 컨텍스트가 해당 객체를 캐싱하고 다음 같은 객체 조회를 할때 이를 기억하고 한번의 쿼리만 날라간다.Day9. 조금 더 복잡한 기능을 API로 구성하기이번에는 실전으로 책을 생성하고 대출하고 반납하는 기능을 만들었다. 여기서 이제까지 배운 개념들을 적용했다. 물론 코치님께서 알려주시긴 하지만 나는 강좌를 멈추고 내 스스로 코드를 작성해본 다음에 코치님 설명과 비교를 했다. 여기서 대출기능을 할 때 나는 연관관계를 매핑해서 처리를 할려고 했지만 코치님께서는 일단은 대출관련 테이블을 만든 뒤에 그에 대한 엔티티, repository, service를 만드셨다. 그래서 나는 여기서 조금 깨달은 부분이 있었다. 무조건 연관관계를 짓는게 아니라 만약 실무에서 연관관계를 짓는게 불가하다면 이런 경우로 풀수도 있다는 사실을 깨달았다.미션 해결과정Day6이번 미션은 과제4에서 만들었던 Fruit관련 API를 3단분리하고, FruitRepository를 인터페이스로 만들고 해당 인터페이스를 구현한 FruitMemoryRepository와 FruitMysqlRepository를 만들어 @Primary 어노테이션을 통해 repository의 역할을 바꿔가며 해보는 과제였다.나는 먼저 기존 컨트롤러에 모여있는 비즈니스 로직을 저장, 수정, 조회기능은 repository레이어에 그리고 예외처리관련은 서비스 레이어에 분리하였다. 그리고 컨트롤러는 순수 HTTP 통신 관련만 구현해두었다. 그런 다음에 DB로직 관련 repository 클래스를 FruitMysqlRepository로 변경하고 FruitRepository 인터페이스를 생성 후 구현하고 나머지 FruitMemoryRepository를 생성하여 메모리 관련 로직을 작성해두었다. 다음 각각 클래스에 @Primary 어노테이션을 붙이고 각각 메서드에 Logback을 이용해 로그를 찍으면서 확인을 했다. 이를 통해 학습의 효과를 느낄 수 있었다. 학(강의 시청)으로 개념을 배우고 습(실습을 통한 체득)으로 체득을 함으로 좀 더 익숙하게 쓸 수 있게 되는 계기가 된 것 같다. 자세한 것은 아래 블로그를 통해 보시면 자세한 과정을 알 수 있다.https://inf.run/3EWwN피드백피드백 전까지 테스트코드도 나름 잘 작성하고 validation부분까지 잘 작성해서 나름 이번은 성공적이라고 느꼈다. 하지만 코치님께서 피드백을 주셨다. 서비스의 비즈니스 로직이 복잡할 때는 다른 내부 서비스 로직을 호출하기도 하지만 DTO와 도메인에 계산로직과 비즈니스 로직을 나눠서 넣기도 한다고 하였다. 내 코드를 보니 뭔가 DTO에도 처리할만한 부분이 있지 않았을 까 반성하게 되는 계기 된 것같다. Day7이번 미션은 과제6에서 만든 기능들을 JPA로 변경하는 부분이 있었다. 또한 다양한 쿼리메서드를 연습해볼 기회로 문제를 몇개 주셨다. 먼저 문제1에서 Spring Data JPA로 바꾸는 것은 그리 어려운 작업은 아니었던 것 같았다. 단순히 repository 인터페이스를 JpaRepository에 상속받고 엔티티를 연습했던것처럼 바꿔주면 되기 때문이다. 하지만 나는 여기서 더 나아갔다. 집계함수 부분을 Spring Data JPA로 변경할 때 좀 고민이 있었다. 집계함수를 제공해주는 쿼리메서드는 없었던 것 같았다. 그래서 집계함수를 이용하지 않고 select 쿼리를 이용해서 List<엔티티> 타입으로 반환해야하나 생각을 하던 결과 문듯 아이디어가 떠올랐다. 바로 @Query와 jpql이다. 그래서 나는 여기서 @Query 어노테이션을 이용하여 JPQL로 쿼리를 작성해보았다. 그리고 반환을 엔티티타입이 아닌 DTO로 반환해보았다. 그러니 서비스 레이어도 간단해졌다.그렇게 쉽게 바꿔서 문제1은 가볍게 해결했다. 그리고 문제2를 풀면서 다양한 쿼리 메서드를 테스트할 수 있었다. 먼저 count~로 시작하는 메서드를 만들어 count 쿼리를 작성할 수 있었다.마지막 문제3도 GreaterThanEqual, LessThanEqual의 조건을 이용하는 쿼리메서드를 작성하는 거였다.이번 미션도 테스트를 작성해보고 이번엔 진짜 잘했다고 느꼈다. JPQL을 통해 DTO로 직접 반환하는 부분까지 완벽했다고 자만했다. 하지만 피드백을 듣고 아직 많이 부족하다는 것을 느꼈다.피드백마지막 문제의 parameter GTE, LTE 부분을 enum 클래스로 관리할 수 있다고 하셨다. 이 말을 본 순간 "앗~"이라는 말이 절로 나왔다. enum을 아예 몰랐던것도 아니고 조금 반성하게 된 계기였다. 금방 과제가 끝났다고 끝까지 고민을 못해본 결과였다. Day8 ~Day8부터 미니프로젝트 과제이다. 아직은 미니프로젝트 미완성이므로, 해당 프로젝트가 단계별 완성시, 새로운 포스트로 남기겠다.회고오늘까지 나는 학습을 하면서 많은 것을 깨달았다. 물론 지식도 지식이지만 하나의 문제를 풀 때 수학처럼 다양한 방식으로 푸는 방법에 대해 깨달음을 얼었다. 무조건 좋은 방법으로 풀 수 없는 경우 우회를 해서 푸는 방식으로도 할 수 있다는 것을 알고 나 자신 스스로 반성하는 부분을 가졌다. 마음속으로 "이렇게 해서 우수러너가 될 수 있으며 원하는 기업으로 이직을 할 수 있을까?"라는 반성의 시간을 가지고 다른 열심히 하시는 러너분들을 생각해 더욱 자극을 받아서 우수러너가 되기까지 노력해보기로 생각을 하였다. 

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