블로그

이양구

[인프런 워밍업 클럽 FE 0기] 미션7 - 예산 계산기 앱

💸 Budget Calculator APP GitHub 💸 Budget Calculator APP DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 일곱 번째 미션인 '예산 계산기 앱' 입니다. 따라하며 배우는 리액트 섹션 0~3(To-Do 앱) 목표의존성 배열(Dependency Array) 을 이용해 함수 실행하기state 를 전역 변수처럼(?) 사용해보기 구현구조|-- App | |-- Form | |-- Lists | | |-- List  의존성 배열(Dependency Array) 을 이용해 함수 실행하기// App.jsx const [budgetList, setBudgetList] = useState( JSON.parse(localStorage.getItem("budgetList")) || [] ); useEffect(() => { localStorage.setItem("budgetList", JSON.stringify(budgetList)); }, [budgetList]); const totalCost = useCallback(() => { return budgetList.reduce((acc, cur) => acc + cur.cost, 0); }, [budgetList]); 최상위 컴포넌트인 <App> 컴포넌트에서 만든 'budgetList'이라는 state를 useEffect와 useCallback의 의존성 배열에 추가했다.useEffect에서는 해당 state가 변경되면 로컬 스토리지의 budgetList를 최근의 리스트로 변경한다.이렇게 하면 일일이 setBudgetList가 호출되는 곳마다 함수를 사용하지 않아도 된다.다음은 예산의 총 금액을 반환하는 함수가 리스트가 변경될 때마다 실행되도록 useCallback으로 감싸고 의존성 배열에 state를 추가했다.// Form.jsx const budgetNameRef = useRef(); const [budgetName, setBudgetName] = useState(""); const [budgetCost, setBudgetCost] = useState(0); useEffect(() => { if (isEdit) { setBudgetName(budget.name); setBudgetCost(budget.cost); budgetNameRef.current.focus(); } }, [isEdit]); <Form> 컴포넌트에서는 useEffect에 'isEdit'이라는 state를 의존성 배열에 추가했다.사용자가 예산을 수정하기 위해 list의 Edit 버튼을 클릭하면 해당 budget의 name과 cost를 최근 state로 불러오고, useRef를 이용해 name을 입력하는 <input> 요소에 focus 상태가 되도록 했다.state 를 전역 변수처럼(?) 사용해보기// App.jsx const [currentBudget, setCurrentBudget] = useState({ isEdit: false, budget: {}, }); // List.jsx const handleEdit = () => { setCurrentBudget({ isEdit: true, budget: list, }); setHandleStatus({ type: "edit", message: "Editing..." }); }; // Form.jsx const handleBudgetSubmit = (e) => { const newBudget = { id: Date.now(), name: budgetName, cost: budgetCost, }; // isEdit의 값에 따라 새로 추가할지 수정할지 결정 if (isEdit) { setBudgetList((prevBudgetList) => { const newBudgetLists = [...prevBudgetList]; const index = newBudgetLists.findIndex(({ id }) => id === budget.id); newBudgetLists[index] = newBudget; return newBudgetLists; }); setCurrentBudget({ isEdit: false, budget: {} }); setHandleStatus({ type: "submit", message: "Edit Success!" }); } else { setBudgetList((prevBudgetLists) => [...prevBudgetLists, newBudget]); setHandleStatus({ type: "submit", message: "Submit Success!" }); } // submit 종료 시 input의 데이터를 초깃값으로 설정 setBudgetName(""); setBudgetCost(0); }; 배웠던 To Do 앱은 List의 Edit 버튼을 클릭했을 때 해당 List의 요소를 input 요소로 변경시키고 수정을 했다.하지만 과제는 클릭을 했을 때 List의 요소를 변경시키는 게 아니라 Form의 input에 해당 예산의 데이터를 전달해야 했다.그래서 마치 전역 변수처럼 사용할 'currentBudget'이라는 state를 생성하고 'isEdit'이라는 boolean 값과 수정할 예산의 데이터를 담을 'budget'이라는 값을 설정했다.'isEdit'의 상태 값이 true일 때 수정하기와 삭제하기 <button> 요소를 disabled로 변경한다.또한 submit 함수는 새로운 입력 값을 budgetList에 추가하지 않고 해당 예산의 index를 찾아 수정하고 리스트를 변경한다.이렇게 하니 onSubmit과 onEdit 처럼 비슷한 기능을 하는 함수를 여러 개 만들지 않아도 되었다. ⚠ setTimeout 렌더링const { type, message } = handleStatus; const handleStyle = useCallback(() => { if (type === "edit") { return "text-gray-500 block"; } else if (type === "none") { return "hidden"; } else { // 2초 뒤에 실행 --> App - Form - Status 1번 더 렌더링 setTimeout(() => { setHandleStatus({ type: "none", message: "" }); }, 2000); if (type === "submit") { return "text-green-400 block"; } else { return "text-red-400 block"; } } }, [type]); 추가, 삭제, 수정의 완료 및 진행 중 상태를 보여주는 <Status> 컴포넌트를 만들었다.App에서 만든 'handleStatus'라는 state를 전달하고 메세지가 나타난 뒤에 사라지게 만들고 싶어서 setTimeout() 메서드를 이용해 2초 뒤에 상태를 초기화했다.하지만 이 상태가 App과 Form 컴포넌트에서 참고하다 보니 나타나고 사라질 때마다 렌더링이 발생했다.CSS의 opacity로 처리하기엔 state의 값을 변경해야 했기에 알맞는 방법은 아니라 생각했다.뭔가 <Status> 컴포넌트 내부에서만 렌더링이 일어나게 하고 싶었는데 아직 다른 방법을 찾지 못했다.😢😢😢 회고다른 컴포넌트의 클릭 이벤트로 변경된 state를 이용하는 부분이 생각보다 오래 걸렸다.처음엔 콜백 함수처럼 App 컴포넌트에서 함수 만들고 prop으로 넘겨봤지만 List와 Form은 종속적인 관계가 아니라 힘들었다. 😢그래서 생각해낸 게 state를 이용해서 상태의 변경을 이벤트처럼 사용하는 것이었다.pub-sub 혹은 observer 패턴 같다는 생각도 했지만, 이렇게 최상위에서 선언한 state가 이곳저곳 돌아다니는 게 좋은 방법은 아닐 것 같다는 생각이 들었다.규모가 커지면 렌더링 관리도 힘들고 props를 쫓아다녀야 하기 때문이다.이래서 상태 관리 라이브러리가 나왔나 보다. 🤔 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

konakyeon3

인프런워밍업스터디_BE_0기 2주차 발자국

2주차 학습 내용 요약 6일차 스프링컨테이너의 의미와 사용방법6일차 스프링 컨테이너의 개념 미션 수행강의에서는 스프링 빈과 컨테이너 개념에 대해 알아보았다.@Component @Bean 어노테이션을 넣으면 해당 클래스가 런타임 시에 스프링컨테이너에 등록을 하게 된다는 점을 배웠다.인스턴스화를 자동화 해준다는 느낌이다.미션으로는 기존의 코드를 Controller - Service - Repository 로 코드를 분리시켜서 리펙토링하는 것이 미션이였다.리펙토링하는 코드를 작성 할 때 Controller 클래스가 기존에 작성한 코드에 종속되어 있어서 api를 호출 할 때마다 오류가 났었다.그래서 아예 새로운 Controller 클래스를 만들었다.7일차 JPA를 왜 사용하는가_어떻게 JPA 사용하는가7일차 JPA를 왜 사용하는가_어떻게 JPA 사용하는가JPA에 대해 학습하고, 기존 코드를 JPA로 리펙토링하는 것을 미션으로 수행했다.아직은 왜 정확히 sql 쿼리문에 JPA를 사용하는지 납득이 되지 않는다.JPA를 사용할 때 제일 큰 장점은 유지보수성을 높여준다는 것이라고 한다.객체지향 구조 사용해 이미 만들어진 객체를 활용할 수 있다. sql쿼리문으로 db 테이블마다 crud sql문들을 다 작성해야 하지만 jpa 코드는 기존에 만들어진 메서드와 엔티티 객체를 사용해서 중복을 줄이고 유지보수 하기도 편해진다고 한다. 🤔 몇 만줄 되는 복잡한 코드를 유지보수 해본 경험이 없으니 JPA의 이점에 대해 납득하지 못하는 것 같다.직접 프로젝트를 진행하면서 JPA를 사용하는 것과 사용 안하는 프로젝트를 비교를 해볼 수 밖에 없겠다. 그리고 직접 코드를 작성 했을 때는 JPA 코드가 sql 쿼리문에 비해 직관적이지 않았다.JPA repository 인터페이스의 메서드들을 직접 코드를 사용하기 전까지는 무슨 뜻인지 알 수 없었다. 🤔이 부분은 아직 JPA코드를 많이 작성 해보지 않아서 생기는 문제인 것 같다.최대한 다양한 JPA 코드를 작성해보자. 8일차 트랜잭션이 무엇이고_왜 사용해야 하는가🔥 데이터베이스는 원자성 속성을 보장하지 않습니다. 여러 데이터베이스 작업이 "전부 아니면 전무" 기준에 따라 하나의 원자 단위로 실행된다는 보장은 없습니다. 일련의 작업에서 한 번 작업이 실패하면 데이터베이스는 계속해서 다음 작업을 실행합니다https://www.java4coding.com/contents/oracle/oracle-transaction)트랜잭션을 사용해야 데이터베이스의 원자성을 회복시킬 수 있다. 예를 들자면 이런 식이다.유저를 등록하고 해당 유저에 대한 반납 기록 저장하는 sql로직을 둘을 동시에 해야 되는데, 트랜잭션을 사용하지 않으면 유저는 등록했는데 반납 기록이 저장되지 않는다거나 반납 기록만 저장되서 DB 저장에 문제가 생길 수 있다.@Transaction을 사용한 코드는 한쪽 sql 로직이 이루어지지 않으면 다른 로직도 롤백시키는 일을 해준다. 🤔 생각해보면 트랜잭션을 사용하지 않으면 있어야 될 내용이 DB에 없거나, 없어야 될 내용이 있는 등 끔찍한 일이 벌어질 게 뻔하다.정말정말 트랜잭션을 중요한 내용인 것 같다. 9일차 조금 더 복잡한 기능을 API로 구성하기libraryapp에 반납기능과 책 대출 기능을 만드는 것을 실습했었다.show tables 는 sql문은 해당 데이터베이스 안에 어떤 테이블이 있는지 알려준다. 🤔 show tables 를 정말 자주 사용했다.테이블 삭제, 생성, 구조 변경 코드도 많이 사용했던 것 같다. 왜 일까?쿼리문 여러개를 동시에 실행하다보니 비슷한 이름의 테이블이 중복되서 만들어지는 일이 많았다. 그래서 데이터베이스에 어떤 테이블이 있는지 확인하는 코드가 필요했다. 그래서 use tables또 다른 DB 관련 문제로는 같은 이름을 지닌 책이 중복해서 등록이 되거나 같은 이름을 가진 유저가 중복되서 등록되는 일이 벌어지기도 했었다. 이 문제는 나중에 이름 부분을 unique key로 등록해서 중복 등록을 방지하는 것으로 해결했다. 간단한 ERD를 먼저 그려보고 코딩을 하거나, DB 설계 내용을 그림에 먼저 그려 보고 나서 코딩을 했다면 이렇게 복잡해지지 않았을 것 같다. 정리뭔가 미션을 수행하면 시간이 없어서 허겁지겁 끝내는 느낌이다.시간이 없다면 최대한 미션에서 명세한 기능만이라도 만들어서 코드를 제출하던가 해야되는데 뭔가 쓸떼 없는 일에 집착하다가 늦어지는 것 같다.무작정 코드를 짜게 되면서 문제가 생기는 경우도 많은 것 같다.erd를 그려본다거나 간단한 코드라도 종이에 먼저 어떻게 코드를 작성할지 써보고 코드를 작성하는 습관을 만드는 것이 좋을 것 같다.이런 나의 생각과 느낌을 최대한 넣어서 작성하는 회고도 좋지만, 정량적인 지표들(코드 작성 시간, 미션 수행에 들어간 시간 측정, 미션 수행 난이도, 등)을 이번 주에 대해 최대한 객관적으로 평가해볼 필요가 있을 것 같다.내가 아닌 제3자의 피드백도 필요하다. 

이동원

[인프런 워밍업 스터디 클럽 0기_BE] 2주차 회고록 정리

짧은 다리의 두 번째 발걸음 6일차 역할의 분리와 스프링 컨테이너 Spring Bean: 스프링 컨테이너 안으로 들어간 클래스를 스프링 빈이라 함@Service와 @Repository를 통해서 Service와 Repository도 스프링 빈으로 등록가능함 @Primary: 어노테이션의 일좀으로 우선권을 정해줄 수 있다 @Configuration: @Bean과 함께 사용 @Bean: 메소드 객체를 스프링에 등록할 때 사용 @Component: @Controller, @Service, @Repository 모두 아니면서, 직접 작성한 클래스를 스프링 빈으로 등록한다 7일차 Spring Data JPA를 사용한 DB 조작 SQL 문 직접 작성의 문제-> 문자열 작성이어서 오타 발생시 알아채기 어려움, 컴파일 시점에 발견안되고 런타임 시에 발견된다-> 특정한 DB에 종속된다-> 테이블 마다 CRUD반복 작업이 필요하다-> 객체와 달리, 양방향이 아닌 단방향이다 JPA 를 통해 문제해결: 객체와 관계형 DB 테이블을 짝지어 데이터를 영구적 저장하도록 해주는 JAVA 진영 규칙 spring: 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 dialect: org.hibernate.dialect.MySQL8Dialect 8일차 트랜잭션과 영속성 컨텍스트를 이용한 요구사항 구현 트랜잭션: DB의 논리적 연산 단위. 1개의 트랜잭션에는 N개(≥1)의 SQL문 포함 @Transcational붙어있는 메서드가 시작할 때 트랜잭션을 시작예외 없이 잘 끝났다면 commit오류라면 rollback 영속성 컨텍스트: 테이블과 매핑된 Entity 객체를 관리/보관하는 역할, 스프링에서는 트랜잭션 사용 시 영속성 컨텍스트가 생성, 트랜잭션 종료 시 영속성 컨텍스트가 종료 특징변경감지쓰기 지연1차 캐싱회고: 과제가 끝나면서 조금 마음이 가벼워진 느낌이 있었다. 실제 프로젝트를 하려하니 생각이 많아지고 어떻게 하면 좋을지 고민하는 시간이 많았다. 코치님의 각 주차에 따라 내용을 따라는 가는것 같은데 막상 내것이 되었나 생각하며 코드를 치려고 하니 시간이 배로 걸리고 오류가 자주났다. 진짜 하,,,,,,,, 라고 한숨만 하루에 100번도 넘게 쉰적이 많았다. 특히나 db연결하는데 있어서 인텔리제이랑 mysql연동해놓고 하다가 컴퓨터 껐다가 켜서 시작 프로그램이 좀 지저분해 몇개 지우니 db연결이 끊기고 다시 연결안되는 문제로 한나절 고생한것을 생각하니 진짜 암담했다. jpa에 대해서 말만 듣고 실제 해보니 마법과도 같은 편리함과 놀라움이어서 재밌었다. 문법을 좀 알고 공부해야 할것 같은데. 이렇게 나마 써보고 배울수 있어서 유익한 시간이었다. 각 주차에 따라, 따라가는 것이 좀 버겁지만 걸어가든 기어가든 완주를 하면서 얻는것이 많다고 생각하기에 끝날때까지 복습하면서 완주해낼 생각이다. 남은 시간도 스스로에게 부끄럽지 않게 공부할수 있길 바란다. 

망고123

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

일주일 간의 학습 내용에 대한 간단한 회고📕지난 1주차 회고에서는 10000자 되는 내용으로 깊게 정리했습니다.😅 2주차는 1주차처럼 모든 내용을 담은 정리하지 않고, '핵심 포인트' 위주로 필요하다고 생각되는 부분의 요약과 함께 내용을 정리하는 2주차 회고를 작성합니다😀 (10000자 -> 4000자) 🔥무엇보다 학습 내용 범위 벗어난 스펙과 기술을 사용하지 않는 것을 원칙으로 학습하고 있습니다. 처음 시작할 때의 열정과 목표를 잊지 않으면서, 강의를 통해 멘토님께서 전달하고자 하는 지혜와 경험을 쌓아가며 <미니 프로젝트> 의 모든 단계를 성공적으로 마무리할 수 있도록 성장하기를 나, 스스로에게 응원합니다. 일주일 동안 스스로 칭찬하고 싶은 점 하루도 빠지지 않고 열심히 학습하고 달려왔고, 어떻게 해야 좀 더 효과적인 학습을 할 수 있을까에 대해 고민하고 행동으로 실천하여 효율적인 학습을 할 수 있었습니다.아쉬웠던 점 감기에 걸려서 제대로 학습하지 못한 날이 꽤 있었습니다. 아파서 학습하지 못한 이 공백을 채우기 위해 더욱 집중력 있게 학습할 수 있도록 노력해야 겠다고 생각했습니다.보완하고 싶은 점 2 주차는 깊게 정리하지 않고 유연하게 정리하면서 멘토님의 PPT와 PDF 를 중심으로 학습하면서 훨씬 효율적인 복습을 했습니다. 복습하면서 아직은 완벽하게 체득하지 못한 부분들이 꽤 있다는 것을 <미니 프로젝트>를 진행하면서 느끼게 됐습니다.다음주에는 어떤 식으로 학습하겠다는 스스로의 목표 개인적으로 진행하고 있는 프로젝트와 학습하는 것의 비중을 낮추고, <미니 프로젝트> 를 중심으로 부족한 부분들에 대한 이론과 예제를 개인적으로 정리하면서 진행하려고 합니다. 기회가 된다면 <미니 프로젝트> HTML이라도 구현하는 것을 목표로 하고 있습니다.   학습 내용 정리19강. UserController와 스프링 컨테이너@RestController는 컨트롤러 클래스를 API 진입 지점으로 만들어주고 스프링 빈으로 등록시킵니다. 스프링 빈은 스프링 컨테이너에 들어간 클래스를 의미합니다. 스프링 빈에 등록된 클래스들을 식별하기 위해 이름 및 타입과 함께 다양한 정보가 저장되며 인스턴스화를 수행합니다. 그리고 JdbcTemplate 역시 스프링 빈으로 등록되어 있습니다. build.gradle 안의 spring-boot-starter-data-jpa 의존성에 의해JdbcTemplate 을 스프링 빈으로 미리 등록됩니다.따라서 스프링 컨테이너는 UserController 를 인스턴스화할 때, UserController에서 필요한 JdbcTemplate을 스프링 컨테이너 내부에서 찾아 인스턴스화를 진행하게 됩니다. 따라서 JdbcTemplate을 스프링 빈으로 등록하는 의존성인 spring-boot-starter-data-jpa 이 없으면 에러가 발생한다는 점을 참고합니다.스프링 부트 서버를 실행하면 다음과 같은 일이 순차적으로 내부에서 실행됩니다.스프링 컨테이너가 시작합니다.스프링 컨테이너에 기본적으로 많은 스프링 빈이 등록됩니다.( JdbcTemplate이 등록됩니다.)개발자가 작성한 스프링 빈이 등록됩니다.( UserController 가 등록됩니다.)필요한 의존성이 자동으로 설정됩니다.( UserController 를 만들 때 JdbcTemplate 을 알아서 넣어줍니다.)이제 지금까지 UserRepository 가 JdbcTemplate 을 바로 가져오지 못하는 이유를 알 수 있습니다. UserController 는 @RestController 에 의해 스프링 빈에 등록하고 동일한 스프링 빈인 JdbcTemplate 을 가져올 수 있지만, UserRepository 는 스프링 빈이 아니기 때문에 가져올 수 없습니다. 따라서 서비스와 리포지토리를 스프링 컨테이너에 등록하기 위해 @Service, @Repository어노테이션을 사용해야 합니다.정리하자면 UserController - UserService - UserRepository 클래스는 서버가 시작할 때 다음과 같이 수행됩니다.JdbcTemplate 을 이용해 UserRepository 가 스프링 빈으로 등록됩니다. (인스턴스화를 수행합니다.)UserRepository 를 의존하는 UserService 가 스프링 빈으로 등록됩니다.UserService 를 의존하는 UserController 가 스프링 빈으로 등록됩니다.이렇게 3개의 클래스 모두 스프링 빈으로 등록됩니다!20강. 스프링 컨테이너를 왜 사용할까?!MySQL 을 사용하여 데이터를 저장하는 방식으로 변경하면 다음과 같은 일이 발생됩니다.BookMemoryRepository 대신하는 BookMySqlRepository 를 생성합니다. JdbcTemplate 을 생성자로 받을 수도 있지만, BookMySqlRepository 가 직접 설정해 준다고 가정합니다.BookService 도 변경됩니다. BookMemoryRepository()대신 BookMySqlRepository() 를 사용합니다.서비스까지 변경되는 것이 바로 가장 큰 문제입니다. 데이터를 메모리에 저장할지 MySQL에 저장할지에 대해서만 변경하고 싶지만, BookService까지 필연적으로 변경이 일어나게 됩니다. 이 고민에 대한 해결책이 바로 스프링 컨테이너입니다.스프링 컨테이너를 사용한다고 가정합니다.스프링 컨테이너는 BookMemoryRepository 혹은 BookMySqlRepository 중 하나를 선택한 후, BookService 를 생성합니다. 이런 방식을 어려운 말로 제어의 역전 (IoC, Inversion of Control)이라 부릅니다.또한 컨테이너가 BookService 를 생성할 때 BookMemoryRepository 와 BookMySqlRepository 중 하나를 선택해서 넣어주는 과정을 의존성 주입(Dependency Injection)이라고 합니다.@Service public class BookService { private final BookRepository bookRepository; public BookService(BookRepository bookRepository) { this.bookRepository = bookRepository; } }public interface BookRepository { public void save(String bookName); }@Repository public class BookMemoryRepository implements BookRepository { @Override public void save(String bookName) { println("Memory Repository " + bookName); } }@Repository @Primary // 우선권을 부여하는 어노테이션!! public class BookMySqlRepository implements BookRepository { @Override public void save(String bookName) { println("MySQL Repository " + bookName); } }스프링 컨테이너에 BookMemoryRepository 혹은 BookMySqlRepository 둘 중 어느 것을 등록할 지에 대해서는@Primary 어노테이션을 이용해 우선권을 제어할 수 있습니다.21강. 스프링 컨테이너를 다루는 방법서비스와 리포지토리 클래스를 @Service, @Repository 어노테이션으로 스프링 빈으로 등록했습니다. 이 방식 뿐만 아니라 다른 어노테이션으로도 스프링 빈에 등록할 수 있습니다.@Configuration : 클래스에 붙이는 어노테이션. @Bean을 사용할 때 함께 사용해 주어야 합니다.@Bean : 메서드에 붙이는 어노테이션. 메서드에서 반환되는 객체를 스프링 빈에 등록합니다.다음 예제는 UserRepository 를 @Configuration 과 @Bean 을 활용한 예제입니다.@Configuration public class UserConfiguration { @Bean public UserRepository userRepository(JdbcTemplate jdbcTemplate) { return new UserRepository(jdbcTemplate); }        @Bean public UserService userService(UserRepository userRepository) { return new UserService(userRepository); } }그렇다면 언제 @Service, @Repository 를 사용해야 하고, 언제 @Configuration + @Bean 을 사용해야 할까요? 정답은 없습니다!일반적으로 개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때에는 @Service, @Repository 를 사용합니다.외부 라이브러리, 프레임워크의 생성된 클래스를 스프링 빈으로 등록할 때 @Configuration + @Bean 조합을 많이 사용하게 됩니다.@Component 어노테이션은 @RestController, @Service, @Repository, @Configuration 모두 가지고 있습니다. @Component 어노테이션을 붙이면 주어진 클래스를 ‘컴포넌트'로 간주하고, 컴포넌트들은 스프링 스프링 서버가 뜰 때 자동으로 감지됩니다. @Component 덕분에 지금까지 우리가 사용했던 어노테이션들이 모두 자동으로 감지된 것입니다.@Component어노테이션은 컨트롤러, 서비스, 리포지토리가 아니라 추가적인 클래스를 스프링 빈으로 등록할 때 종종 사용됩니다.스프링 빈으로 등록하는 방법을 살펴보았으니, 스프링 빈을 주입받는 방법은 다음과 같습니다. 가장 간단하고 권장되는 방법은 생성자를 이용해 주입받는 방법입니다. 지금까지 우리가 계속 사용한 방법입니다.@Repository public class UserRepository { private final JdbcTemplate jdbcTemplate;     // 생성자에 JdbcTemplate이 있으므로 스프링 컨테이너가 넣어준다. public UserRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }두 번째 방법은 setter 주입 방식입니다. final 키워드를 제거하고 setter 메서드에 @Autowired 어노테이션을 작성해야 합니다.@Repository public class UserRepository { private JdbcTemplate jdbcTemplate; // 1. final 제거        @Autowired // 2. @Autowired 추가 public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }@Autowired 어노테이션이 있어야 스프링 컨테이너에 있는 스프링 빈을 찾아 setter 메서드에 넣어 주게 됩니다.세 번째 방법은 필드에 직접적으로 주입하는 방법입니다. 필드 위에 @Autowired 어노테이션을 작성합니다.@Repository public class UserRepository { @Autowired // 필드에 @Autowired 추가 private JdbcTemplate jdbcTemplate; }setter 주입 방식과 필드에 바로 주입하는 방법은 기본적으로 권장되지 않습니다.setter를 사용하게 되면 혹시 누군가가 setter를 사용해 다른 인스턴스로 교체해 동작에 문제 가 생길 수도 있고,필드에 바로 주입하게 되면 테스트가 어렵기 때문입니다.@Qualifier어노테이션은 @Primary 어노테이션이 없는 경우에 주입받는 쪽에서 특정 스프링 빈을 선택할 수 있습니다.public interface FruitService {} // 과일 인터페이스@Service public class AppleService {} // 사과 클래스@Service public class BananaService {} // 바나나 클래스@Service public class OrangeService {} // 오렌지 클래스@RestController public class UserController { private final UserService userService; private final FruitService fruitService;     public UserController( UserService userService, @Qualifier("appleService") FruitService fruitService) { this.userService = userService; this.fruitService = fruitService;       } }@Qualifier("appleService") FruitService fruitService)에 의해 FruitService 에는 AppleService 가 들어오게 됩니다.@Qualifier 어노테이션은 스프링 빈을 사용하는 쪽과 스프링 빈을 등록하는 쪽 모두 사용할 수 있습니다. 이 경우에는 @Qualifier 어노테이션에 적어준 값이 같은 것끼리 연결됩니다.@Service @Qualifier("main") public class BananaService {}@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; } }만약 @Primary 와 @Qualifier 를 둘 다 사용하고 있으면 @Qualifier 의 우선 순위가 높습니다. 왜냐하면 스프링 빈을 사용하는 쪽에서 특정 빈을 지정해 준 것이 더욱 우선 순위를 높게 간주합니다.22강. Section3 정리좋은 코드가 왜 중요한지 이해하고, 원래 있던 Controller 코드를 보다 좋은 코드로 리팩 토링한다.스프링 컨테이너와 스프링 빈이 무엇인지 이해한다.스프링 컨테이너가 왜 필요한지, 좋은 코드와 어떻게 연관이 있는지 이해한다.스프링 빈을 다루는 여러 방법을 이해한다.23강. 문자열 SQL을 직접 사용하는 것이 너무 어렵다!!SQL을 직접 작성해 개발하게 되면서 '컴파일 타임 에러 체크 불가능', '특정 데이터베이스에 종속', '수많은 반복 작업', '데이터베이스 테이블과 객체의 패러다임' 등 이러한 어려움이 있었습니다. 그래서 사람들은 JPA를 만들게 되었습니다. JPA란 Java Persistence API의 약자로 자바 진영의 ORM(Object-Relational Mapping) 기술 표준을 의미합니다.영속성(Persistence)은 데이터를 생성한 프로그램이 종료되더라도, 그 데이터는 영구적인 속성을 갖는 것을 의미합니다.API는 우리가 만든 HTTP API에서도 쓰였지만, ‘정해진 규칙’을 의미합니다.그럼 여기까지 정리해 보면, JPA는 데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙입니다. 이제 ORM(Object-Relational Mapping)에 대해 이해합니다.Object 단어는 우리가 Java에서 사용하는 ‘객체’와 동일합니다.Relational 의미는 관계형 데이터베이스의 ‘테이블’을 의미합니다.Mapping이라는 의미는 말 그대로 둘을 짝지어 준다는 의미입니다.여기까지 정리하면 JPA란 다음과 같이 이해할 수 있습니다. 🔥 객체와 관계형 데이터베이스의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙JPA(ORM)는 규칙(Interface)이기 때문에 구현체가 필요합니다. 따라서 JPA 를 실제 코드로 작성한 가장 유명한 프레임워크가 바로 Hibernate 가 있습니다. Hibernate은 내부적 으로 JDBC를 사용하고 있습니다. 그림으로 나타내면 다음과 같습니다.24강. 유저 테이블에 대응되는 Entity Class 만들기User 객체에 @Entity 어노테이션을 작성합니다. Entity는 ‘저장되고, 관리되어야 하는 데이터’를 의미합니다. 어노테이션은 마법 같은 일을 해준다고 했습니다. @Entity 를 붙이게 되면, 스프링이 @Entity 인식하여 서버가 동작하면 User 객체와 user 테이블을 같은 것으로 간주합니다.user 테이블에만 존재하는 id를 User 객체에 추가합니다.id는 테이블에서 primary key 를 의미합니다.@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null;     // 생략... }@Id : 이 필드를 primary key로 간주한다.@GeneratedValue : primary key는 DB에서 자동 생성해 주기 때문에 이 어노테이션을 붙여주어야 합니다. DB의 종류마다 자동 생성 전략이 다른데, MySQL의 경우 auto_increment 를 사용합니다. 이 전략은 IDENTITY 전략과 매칭됩니다.JPA에 의해 테이블과 매핑된 객체는 파라미터를 가지지 않은 기본 생성자가 꼭 필요합니다. 현재는 User(String name, Integer age) 파라미터를 2개 가진 생성자만 있기 때문에 에러가 발생합니다. 기본 생성자도 추가할 때 protected 해도 괜찮습니다.@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null;        @Column(nullable = false, length = 20, name = "name") private String name;     private Integer age;     protected User() { } // 생략... }Column에 대해 @Column 어노테이션으로 다양한 옵션을 설정할 수 있습니다. 주로 필드에 null이 들어갈 수 있는지의 여부, 길이 제한, DB에서의 column 이름을 설정합니다. 지금은 User 객체와 user 테이블의 필드 이름이 같지만, 다를 경우 @Column 어노테이션을 통해 설정해 주면 됩니다.@Column 어노테이션이 존재하지 않는 필드이더라도 JPA는 해당 필드가 Table에도 있을 거라 생각합니다. 예를 들어 private Integer age 라는 필드는 자동으로 user 테이블의 age 와 매핑하게 됩니다.이제 최초 JPA를 적용할 때 설정해 주는 옵션을 추가합니다. application.yml 파일을 찾은 다음과 같이 입력합니다.spring: 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 dialect: org.hibernate.dialect.MySQL8Dialectspring.jpa.hibernate.ddl-auto스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지에 대한 옵션.create : 기존 테이블이 있다면 삭제 후 다시 생성.create-drop : 스프링이 종료될 때 테이블을 삭제.update : 객체와 테이블이 다른 부분만 변경.validate : 객체와 테이블이 동일한지 확인.none : 별다른 조치를 하지 않음.현재 우리는 DB에 테이블이 잘 만들어져 있고, 미리 넣어둔 데이터도 있으므로 none 이라 설정합니다.spring.jpa.properties.hibernate.show_sql : JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄지 결정. (true)spring.jpa.properties.hibernate.format_sql : JPA를 사용해 DB에 SQL을 날릴 때 SQL을 예쁘게 포맷팅할지 결정. (true)spring.jpa.properties.hibernate.dialect dialect : 한국어로 방언, 사투리라는 의미. 이 옵션을 통해 JPA가 알아서 Database끼리 다른 SQL을 조금씩 수정합니다. 우리는 MySQL 8버전을 사용하고 있으므로org.hibernate.dialect.MySQL8Dialect 로 설정하면 됩니다.엔티티 생성과 application.yml 설정으로 객체와 테이블 간의 매핑을 모두 마쳤습니다. 다음 시간에 SQL 을 직접 작성하지 않고 DB에 쿼리를 수행합니다.25강. Spring Data JPA를 이용해 자동으로 쿼리 날리기SQL을 작성하지 않고 유저 테이블에 쿼리를 수행합니다. 유저 생성 / 조회 / 업데이트 기능을 리팩토링합니다.User 도메인 객체와 같은 위치에 UserRepository 라는 인터페이스를 생성.public interface UserRepository {}JpaRepository 를 상속. ( 매핑 객체인 User 와 유저 테이블의 id인 Long 타입을 작성)public interface UserRepository extends JpaRepository<User, Long> {}UserService 에서 직접 SQL 작성을 작성한 UserRepository 대신 새로운 UserRepository 를 사용합니다. 가장 먼저 UserService 의 저장 기능부터 변경합니다.// JDBC 구현 public void saveUser(UserCreateRequest request) {    userJdbcRepository.saveUser(request.getName(), request.getAge()); } ​ // Spring Data JPA 구현 public void saveUser(UserCreateRequest request) { userRepository.save(new User(request.getName(), request.getAge())); } ​ // Spring Data JPA 구현 - id 출력하기 public void saveUser(UserCreateRequest request) { User user = userRepository.save(new User(request.getName(), request.getAge())); System.out.println(user.getId()); }조회 기능도 변경을 변경합니다.// JDBC 구현 public List<UserResponse> getUsers() { return userJdbcRepository.getUserResponses(); } // Spring Data JPA 구현 public List<UserResponse> getUsers() { return userRepository.findAll().stream() .map(user -> new UserResponse(user.getId(), user.getName(), user.getAge())) .collect(Collectors.toList()); }findAll 메서드는 모든 유저 데이터를 조회하는 SQL이 수행되며 그 결과는 List 로 반환됩니다. List 를 UserResponse 으로 전달합니다. 만약 UserResponse 에서 User 를 받는 생성자를 작성하면 코드를 더욱 깔끔하게 변경할 수 있습니다.public List<UserResponse> getUsers() { return userRepository.findAll().stream() .map(UserResponse::new) .collect(Collectors.toList()); }다음으로는 업데이트 기능을 변경합니다.업데이트에서는 2번의 쿼리를 사용합니다.id를 통해 User를 가져와 User가 있는지 없는지 확인하고,User가 있다면 update 쿼리를 날려 데이터를 수정.// JDBC 구현 public void updateUser(UserUpdateRequest request) { if (userJdbcRepository.isUserNotExist(request.getId())) { throw new IllegalArgumentException(); } userJdbcRepository.updateUserName(request.getName(), request.getId()); }// Spring Data JPA 구현 public void updateUser(UserUpdateRequest request) { User user = userRepository.findById(request.getId()) .orElseThrow(IllegalArgumentException::new); user.updateName(request.getName()); userRepository.save(user); }findById는 id에 해당하는 1개의 데이터를 가져올 수 있습니다. 이때 Java 라이브러리의 Optional이 반환되는데, orElseThrow 를 사용하면 User가 비어있는 경우 에러를 던집니다. 반환된 User 객체의 이름을 업데이트해주고, 위에서 사용했던 save 기능을 호출하면 됩니다.setter 대신 updateName 으로 명시적인 이름을 붙여준 이유는 다음 링크 영상에서 참고할 수 있습니다.https://www.youtube.com/watch?v=5P7OZceQ69Q지금까지 사용한 기능은 다음과 같습니다.save : 주어지는 객체를 저장하거나 업데이트.findAll : 주어지는 객체가 매핑된 테이블의 모든 데이터를 가져옴.findById : id를 기준으로 특정한 1개의 데이터를 가져옴.그런데 한 가지 궁금한 점이 있습니다. 어떻게 SQL을 작성하지 않아도 쿼리가 나갈 수 있을까요? 객체와 테이블을 자동으로 매핑해 준 JPA가 처리해 준 것일까요? 정답은, 비슷하지만 조금 다릅니다. JPA를 이용하는 Spring Data JPA 가 자동으로 처리해준 것입니다. 23강에서 확인했던 JPA, Hibernate, JDBC 관계에 Spring Data JPA를 추가해 보면 다음과 같습니다.사용한 save, findAll 같은 메소드는 SimpleJpaRepository 에서 찾아볼 수 있습니다. 스프링을 시작하면 여러가지 설정을 해준다고 했는데, 스프링은 JpaRepository 를 구현 받는 리포지토리에 대해 자동으로 SimpleJpaRepository 기능을 사용할 수 있도록 합니다. SimpleJpaRepository 코드를 열어보면, 조금 복잡한 코드들을 확인할 수 있는데, 이게 바로 JPA 코드입니다. Spring Data JPA 를 사용하는 덕분에 복잡한 JPA 코드를 직접 사용하는 게 아니라, 추상화된 기능으로써 사용할 수 있습니다.이를 그림으로 표현해 보면 다음과 같습니다.26강. Spring Data JPA를 이용해 다양한 쿼리 작성하기유저 삭제 기능을 구현해 보고, Spring Data JPA를 이용한 다양한 조회 쿼리 작성 방법을 학습합니다.// JDBC 구현 public void deleteUser(String name) { if (userJdbcRepository.isUserNotExist(name)) { throw new IllegalArgumentException(); } userJdbcRepository.deleteUserByName(name); } 이름을 통해 유저 여부를 확인하고 delete 쿼리를 수행합니다. UserRepository 인터페이스에서 다음과 같은 메소드 시그니처를 작성합니다.public interface UserRepository extends JpaRepository<User, Long> { User findByName(String name); }User : 이름을 기준으로 유저 데이터를 조회해 유저 객체를 반환(유저 정보가 없다면, null 반환)findByName함수 이름으로 알아서 SQL 조립find는 1개의 데이터를 가져옴.By 뒤에 붙는 필드 이름으로 SELECT 쿼리의 WHERE 문이 작성됨.예를 들어, findByName 은 select * from user where name = ? 과 동일.findByName(String name) 을 통해 이름을 기준으로 User 정보를 가져올 수 있습니다. UserRepository에서 기본으로 제공되는 delete 메소드를 사용합니다.public void deleteUser(String name) { User user = userRepository.findByName(name); if (user == null) { throw new IllegalArgumentException(); } userRepository.delete(user); }UserController에서 UserServiceV2으로 변경하고 테스트를 수행합니다. UserService 인터페이스를 생성하여 다형성을 이용할 수도 있지만, 간단한 작업이므로 객체 타입 전체를 변경합니다.@RestController public class UserController { // UserServiceV2를 사용하도록 변경 private final UserServiceV2 userServiceV2;     public UserController(UserServiceV2 userServiceV2) { this.userServiceV2 = userServiceV2; } }생성 / 조회 / 업데이트 / 삭제 기능까지 모두 JDBC 대신 Spring Data JPA를 사용해 잘 동작하는 것을 확인할 수 있습니다. Spring Data JPA의 추가적인 쿼리 작성법에 대해 학습합니다.By 앞에는 다음과 같은 구절이 들어갈 수 있습니다.find : 반환 타입은 객체가 될 수도 있고, Optional<타입> 이 될 수도 있음.findAll : 쿼리의 결과물이 N개인 경우 사용. 반환 타입은 List<타입>.exists : 쿼리 결과가 존재하는지를 확인. 반환 타입은 boolean.count : SQL의 결과 개수 반환. 반환 타입은 long.By 뒤에는 필드 이름이 들어갑니다. 또한 이 필드들은 And 나 Or 로 조합될 수 있습니다.findAllByNameAndAge 작성하게 되면, select * from user name = ? and age = ? 쿼리가 수행됩니다.findAllByNameOrAge 작성하게 되면, select * from user name = ? or age = ? 쿼리가 수행됩니다.동등 조건 ( = ) 외에 다양한 조건을 활용할 수도 있습니다. 크다 작다를 사용할 수도 있고, 사이에 있는지 확인할 수도 있습니다. 또한 특정 문자열로 시작하는지 끝나는지 확인할 수도 있습니다.GreaterThan : 초과GreaterThanEqual : 이상LessThan : 미만LessThanEqual : 이하Between : 사이StartsWith : ~로 시작하는EndsWith : ~로 끝나는예를 들어 특정 나이 사이의 유저를 검색하고 싶다면, 다음과 같은 함수를 만들 수 있습니다.public interface UserRepository extends JpaRepository<User, Long> { List<User> findAllByAgeBetween(int startAge, int endAge); }JPA와 Spring Data JPA를 활용하여 SQL을 직접 사용해야 하는 아쉬움을 해결 했습니다. 하지만 아직 Service 계층의 역할이 남아 있습니다. 서비스 계층의 중요한 역할은 바로 ‘트랜잭션’ 관리이다. 다음 시간에는 트랜잭션이 무엇인지 그리고 왜 필요한지 알아보도록 하자.27강. 트랜잭션 이론편트랜잭션이란 여러 SQL을 사용해야 할 때 한 번에 성공시키거나, 하나라도 실패하면 모두 실패시키는 기능입니다. 그래서 트랜잭션을 ‘쪼갤 수 없는 업무의 최소 단위’라고 표현합니다. 트랜잭션을 시작하고 사용한 SQL을 모두 취소하고 싶다면, commit 대신 rollback 이라는 명령어를 사용하면 됩니다.다음 시간에는 트랜잭션을 어떻게 적용할 수 있을지 알아보도록 합니다.28강. 트랜잭션 적용과 영속성 컨텍스트트랜잭션을 UserService 에 적용하고 JPA에 등장하는 영속성 컨텍스트라는 개념에 대해 학습합니다.지난 시간에 살펴보았던 것처럼, 우리가 원하는 것은서비스 메소드가 시작할 때 트랜잭션이 시작되어,서비스 메소드 로직이 모두 정상적으로 성공하면 commit 되고,서비스 메소드 로직 실행 도중 문제가 생기면 rollback 되는 것 입니다.트랜잭션을 적용하는 방법은 매우 간단합니다! 대상 메소드에 @Transactional 어노테이션을 붙여주기만 하면 됩니다. 주의할 점으로는 org.springframework.transaction.annotation.Transactional 을 붙여야 합니다. 다른 패키지의 @Transactional 을 붙이면 정상 동작하지 않을 수 있습니다.@Transactional public void saveUser(UserCreateRequest request) { userRepository.save(new User(request.getName(), request.getAge())); }@Transactional public void updateUser(UserUpdateRequest request) { User user = userRepository.findById(request.getId()) .orElseThrow(IllegalArgumentException::new); user.updateName(request.getName());    userRepository.save(user); }public void deleteUser(String name) { User user = userRepository.findByName(name); if (user == null) { throw new IllegalArgumentException(); } userRepository.delete(user); }@Transactional(readOnly = true) public List<UserResponse> getUsers() { return userRepository.findAll().stream() .map(UserResponse::new) .collect(Collectors.toList()); }데이터의 변경이 없고, 조회 기능만 있을 때는 readOnly 옵션을 줄 수 있습니다.@Transactional(readOnly = true)트랜잭션 적용이 성공적으로 모두 잘 됐는지 테스트를 수행합니다.@Transactional public void saveUser(UserCreateRequest request) { userRepository.save(new User(request.getName(), request.getAge())); throw new IllegalArgumentException(); }@Transactional 어노테이션에 대해 한 가지 알아두어야 할 점은, Unchecked Exception에 대해서만 롤백이 일어난다는 점입니다. IOException과 같은 Checked Exception에서는 롤백 이 일어나지 않습니다.영속성 컨텍스트란, 테이블과 매핑된 Entity 객체를 관리/보관하는 역할을 수행합니다. 스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨 나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료됩니다. 또한, 영속성 컨텍스트는 특별한 능력을 4가지 가지고 있습니다.변경 감지 (Dirty Check) 영속성 컨텍스트에 등록된 Entity는 명시적으로 save 를 해주지 않더라도 알아서 변경을 감지하여 저장쓰기 지연 영속성 컨텍스트에 의해 트랜잭션이 commit 되는 시점에 SQL을 모아서 한 번만 쿼리를 수행( update, delete 동일)1차 캐싱 ID를 기준으로 Entity를 기억하는 기능으로 영속성 컨텍스트가 보관하고 있는 데이터를 활용29강. Section 4 정리. 다음으로!문자열 SQL로 구성했던 우리의 데이터 접근 기술을 객체 지 향 프로그래밍이 가능하도록 JPA를 활용해 완전히 변경했습니다. 이 과정에서 아래의 내용들을 익힐 수 있었습니다.문자열 SQL을 직접 사용하는 것의 한계를 이해하고, 해결책인 JPA, Hibernate, Spring Data JPA가 무엇인지 이해한다.Spring Data JPA를 이용해 데이터를 생성, 조회, 수정, 삭제할 수 있다.트랜잭션이 왜 필요한지 이해하고, 스프링에서 트랜잭션을 제어하는 방법을 익힌다.영속성 컨텍스트와 트랜잭션의 관계를 이해하고, 영속성 컨텍스트의 특징을 알아본다.30강. 책 생성 API 개발하기먼저 요구사항을 살펴봅니다.도서관에 책을 등록할 수 있다.다음으로 API 스펙을 확인합니다.HTTP Method : POSTHTTP Path : /bookHTTP Body (JSON){ "name": String // 책 이름 }결과 반환 X (HTTP 상태 200 OK이면 충분합니다.)book 테이블을 설계하고, Book 객체를 만들고, Repository, Service, Controller, DTO를 만들어 주면 됩니다. 꼭 이 순서로 진행해야 하는 것은 아닙니다. 작업하다 보면 익숙한 순서가 생기게 됩니다.테이블create table book( id bigint auto_increment, name varchar(255), primary key (id) );엔티티@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null;     @Column(nullable = false) private String name; }리포지토리public interface BookRepository extends JpaRepository<Book, Long> {}RookCreateRequestpublic class BookCreateRequest { private String name; public String getName() { return name; } }BookController@RestController public class BookController {     private final BookService bookService;     public BookController(BookService bookService) { this.bookService = bookService; }     @PostMapping("/book") public void saveBook(@RequestBody BookCreateRequest request) { bookService.saveBook(request); } } ​@Service public class BookService {        private final BookRepository bookRepository;     public BookService(BookRepository bookRepository) { this.bookRepository = bookRepository; } ​    @Transactional public void saveBook(BookCreateRequest request) { bookRepository.save(new Book(request.getName())); } }@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null;     @Column(nullable = false) private String name;        // Book.java 안에 추가된 로직 protected Book() { } ​    public Book(String name) { if (name == null || name.isBlank()) { throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다", name)); } this.name = name; } }이 과정에서 필요한 Book의 생성자가 자연스럽게 생성됩니다. 다음 시간에는 이어서 대출 기능을 구현합니다.31강. 대출 기능 개발하기요구사항사용자가 책을 빌릴 수 있다.다른 사람이 그 책을 진작 빌렸다면 빌릴 수 없다.API 스펙HTTP Method : POSTHTTP Path : /book/loanHTTP Body (JSON){ "userName": String "bookName": String }결과 반환 X (HTTP 상태 200 OK이면 충분합니다.)테이블create table user_loan_history ( id bigint auto_increment, user_id bigint, book_name varchar(255), is_return tinyint(1), primary key (id) )엔티티@Entity public class UserLoanHistory { @Id @GeneratedValue(strategy = IDENTITY) private Long id; private long userId; private String bookName; private boolean isReturn; }is_return 필드는 tinyint입니다. 이를 boolean에 메핑하게 되면 true인 경우 1, false인 경우 0이 저장됩니다.리포지토리public interface UserLoanHistoryRepository extends JpaRepository<UserLoanHistory, Long> {}BookLoanRequest DTO// DTO public class BookLoanRequest { private String userName; private String bookName;     public String getUserName() { return userName; }     public String getBookName() {        return bookName; } }컨트롤러 - loanBook 메서드 추가// Controller (BookController.java) @PostMapping("/book/loan") public void loanBook(@RequestBody BookLoanRequest request) { bookService.loanBook(request); }서비스@Transactional public void loanBook(BookLoanRequest request) {}우선은 책 객체를 이름을 가져옵니다. 만약 책이 없는 경우에는 예외를 던져주어야 합니다. 이름을 기준으로 책을 가져오려면, BookRepository 에 메소드 시그니처 작성도 필요합니다.// Repository public interface BookRepository extends JpaRepository<Book, Long> { Optional<Book> findByName(String bookName); } ​ // Service @Transactional public void loanBook(BookLoanRequest request) { Book book = bookRepository.findByName(request.getBookName()) .orElseThrow(IllegalArgumentException::new); }Book 객체를 가져왔다면, DB에 존재하는 책입니다. 그리고 Book 객체의 책이 누군가 대출 중인지 확인합니다. 이번에는 UserLoanHistoryRepository 에 메소드 시그니처 작성이 필요합니다.public interface UserLoanHistoryRepository extends JpaRepository<UserLoanHistory, Long> {    boolean existsByBookNameAndIsReturn(String bookName, boolean isReturn); }existsByBookNameAndIsReturn의 매개변수로 책 이름과 false 를 넣은 값이 true가 나왔다는 의미는 현재 반납되지 않은 대출 기록이 있다는 의미이니 누군가 대출했다는 의미입니다. 따라서 Service는 다음과 같이 변경됩니다.@Service public class BookService {     private final BookRepository bookRepository;     // UserLoanHistoryRepository에 접근해야 하니 의존성을 추가해주었다! private final UserLoanHistoryRepository userLoanHistoryRepository;     // 생성자에서 스프링 컨테이너를 통해 주입받도록 하였다. public BookService(BookRepository bookRepository, UserLoanHistoryRepository userLoanHistoryRepository) { this.bookRepository = bookRepository; this.userLoanHistoryRepository = userLoanHistoryRepository; }     // 저장 로직 생략 @Transactional public void loanBook(BookLoanRequest request) { Book book = bookRepository.findByName(request.getBookName()) .orElseThrow(IllegalArgumentException::new);         // 추가된 로직, user_loan_history를 확인해 예외를 던져준다. if (userLoanHistoryRepository.existsByBookNameAndIsReturn(book.getName(), false)) { throw new IllegalArgumentException("진작 대출되어 있는 책입니다"); } } }if 문이 실행되지 않으면 대출되지 않은 책이라는 뜻입니다. 따라서 대출 기록을 쌓아주면 됩니다. 이때 userId 가 필요하기 때문에 유저 객체를 가져온 후 UserLoanHistory 를 저장합니다. UserRepository 에 대한 의존성도 새로 필요하고, UserRepository 의 로직도 변경이 필요하며, UserLoanHistory 에 새로운 생성자도 필요합니다. 최종적인 Service 코드는 다음과 같습니다.@Service public class BookService {        private final BookRepository bookRepository; ​    private final UserLoanHistoryRepository userLoanHistoryRepository; ​    private final UserRepository userRepository; ​    public BookService(        BookRepository bookRepository,        UserLoanHistoryRepository userLoanHistoryRepository,        UserRepository userRepository ) {        this.bookRepository = bookRepository;        this.userLoanHistoryRepository = userLoanHistoryRepository;        this.userRepository = userRepository;   } ​    // 저장 로직 생략 @Transactional public void loanBook(BookLoanRequest request) { Book book = bookRepository.findByName(request.getBookName()) .orElseThrow(IllegalArgumentException::new);         if (userLoanHistoryRepository.existsByBookNameAndIsReturn(book.getName(), false)) { throw new IllegalArgumentException("진작 대출되어 있는 책입니다"); }         User user = userRepository.findByName(request.getUserName()) .orElseThrow(IllegalArgumentException::new);         userLoanHistoryRepository.save(new UserLoanHistory(user.getId(), book.getName())); } }다음 시간에는 마지막 요구사항인 반납 기능을 개발합니다.32강. 반납 기능 개발하기요구사항사용자가 책을 반납할 수 있다.API 스펙HTTP Method : PUTHTTP Path : /book/returnHTTP Body (JSON){ "userName": String "bookName": String }결과 반환 X (HTTP 상태 200 OK이면 충분합니다.)BookReturnRequest DTOpublic class BookReturnRequest { private String userName; private String bookName;    public String getUserName() { return userName; } ​    public String getBookName() { return bookName; } }컨트롤러 - returnBook 메서드 추가@PutMapping("/book/return") public void returnBook(@RequestBody BookReturnRequest request) { bookService.returnBook(request); }서비스 - returnBook 메서드 추가@Transactional public void returnBook(BookReturnRequest request) { User user = userRepository.findByName(request.getUserName()) .orElseThrow(IllegalArgumentException::new);     UserLoanHistory history = userLoanHistoryRepository.findByUserIdAndBookName(user.getId(), request.getBookName()) .orElseThrow(IllegalArgumentException::new);     history.doReturn(); }User 와 UserLoanHistory 가 직접 협업할 수 있게 처리하도록 변경할 수 있지 않는지에 대해 다음 시간에 그 방법을 학습합니다.2주차 미션강의에서 학습한 범위 내에서 미션을 풀어내는 것을 목표로 진행했습니다. 학습 효과를 높이기 위해 어떠한 자료 혹은 검색 없이 스스로 문제 해결을 하려고 노력하고 한 줄마다 의미를 명확하게 이해하고 적용했습니다.여섯 번째 과제! (진도표 6일차)Memory 방식을 제외하고 MySQL 로 동작하도록 구현한다.FruitMySqlRepositoryEx06 리포지토리에 우선 순위를 부여하기 위해 @Primary 를 작성한다.서비스에서 예외 처리를 수행. 리포지토리 새로 추가된isSalesFruitNotExist 메서드로 데이터가 있는지 확인한다.나머지 코드들은 분리한 형태로 코드 분리된 결과입니다. 다음 링크에서 과제 코드를 확인할 수 있습니다.깃허브 링크로 이동하기일곱 번째 과제! (진도표 7일차)과제 #7 제출 스레드 에서 각 코드에 대해 자세히 살펴볼 수 있습니다. 아래 링크는 각 커밋 메세지와 함께 구현한 코드입니다.과제 7 문제 Controller 구현문제 7 문제 Repository 구현문제 7 문제 Service 구현문제 7 문제 Request, Response 구현

백엔드SpringJava

또니

[인프런 워밍업 클럽 0기 BE] 2주차 발자국

[2주차 학습 내용]벌써 스터디를 시작한지 2주차가 되어간다.2주차에는 컨테이너가 무엇인지, 어떻게 동작하는지 JPA 사용해보기, 트랜잭션그리고 조금 더 객체지향 적으로 코드 리팩토링 하는 방법을 배워 보았다.[2주차 과제]이번 2주차에는 기존에 Controller에서 그대로 SQL을 작성했던 것을 Controller - Service - Repository로 분리해보았고,Repository에 SQL을 사용하는 것이 아닌 JPA를 사용하여 DB와 통신할 수 있도록 해보았다.6일차 과제 보기 - https://ddonydev.tistory.com/807일차 과제 보기 - https://ddonydev.tistory.com/81[느낀점]다른 스프링 강의를 들었을 땐 완벽하게 이해가 되지 않고 넘어 갔었고, 정확하게 어떤 일을 하는 지 어떻게 동작 하는지 와닿지 않았었다.하지만 이번 강의를 들으면서 정말 완벽하진 않더라도 어느정도 이해가 되었고, 미션과 병행하며 코드를 쳐보니 조금 더 이해가 수월 했던 것 같다. 또, 그동안은 내가 A라고 알고 있던 것이 전혀 다르게 동작할때 또는 에러가 날때 등 그냥 넘어가거나 에러만 잡기 급급했는데왜 에러가 났는지, 또 왜 전혀 다르게 동작 했는지 이 메서드는 어떤 역할을 하는지 등등 어떻게 궁금해하고 어떻게 찾아가야하는지생각하는 능력이 성장한 것 같아 너무 기뻤다. 이번에 처음 써본 JPA는 너무 어려웠지만 조금 더 공부해본 뒤 미니프로젝트를 진행해 볼 예정이다.이번 주는 웹 개발의 전반적인 흐름과 생각하는 능력을 성장시킬 수 있어서 뜻깊은 한 주였다. 강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

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

도롱이

[인프런 워밍업 클럽_0기] 2주차, 두 번째 발자국 #2

2주차, 두 번째 발자국2주차는 계속 되는 야근으로 인해 시간에 쫓기듯이 공부하고, 과제했던 주였다. 속도가 급하면 체하기 마련! 그래서 오타 등의 실수도 많이해서 삽질로 많은 시간을 보내기도 했다.이전 발자국에서도 남겼지만 강의 별 요약본은 notion에 있다.https://abalone-copper-ebe.notion.site/d2e9b3e27b3348abbde60994cf627ebd?pvs=4 Day 7. 스프링 컨테이너의 의미와 사용 방법스프링 컨테이너의 역할을 정확히알게 되었다.스프링 컨테이너 : 서로 필요한 관계(의존 관계)에 있는 스프링 빈을 연결해주는 역할스프링 컨테이너의 동작 과정을 알게 되었다.스프링이 서버를 시작한다 > 스프링 컨테이너가 시작된다 > 기본적으로 설정된 스프링 빈들이 등록된다 > 개발자가 설정한 스프링 빈이 등록된다 > 필요한 의존성이 자동으로 설정된다.스프링에서 등록한 스프링 빈을 어떤식으로 땡겨와야하는지 알게 되었다.등록된 스프링 빈을 사용하려면 해당 클래스 파일을 스프링 빈에 등록해야 땡겨올 수 있다!Interface를 이용하여 코드 수정 시 수정하는 부분을 최소화 할 수 있다.다른 클래스에서 Interface를 상속받게하고 생성자에서 교체하고 싶은 구현체를 변경만해주면 되기 때문!하지만 위의 방법도 어찌저찌 됐든 생성자를 수정해야하기 때문에 수정은 불가피하다! 이런 걸 해결할 수 있는 어노테이션이 @Primary 와 @Qualifier를 사용하면 구현체 주입 우선순위를 설정해줄 수 있다.참고로 Primary와 Qualifier 중 우선순위가 더 높은 건 Qualifier이다.  과제https://devhan.tistory.com/323기존에 작성했던 코드를 Controller, Service, Repository 3단계로 분리하는 과제였다. 다른 건 다 괜찮았는데 Repository가 Interface로 변경되고 MemoryRepository와 MySQLRepository로 구현체를 나뉘어서 개발하는 부분에 시간을 너무 많이 쏟았던 기억이 난다.그래도 개발하면서 기존에 작성했던 코드를 리팩토링 하는 과정도 거칠 수 있어서 더 재밌게 코딩할 수 있었던 거 같다. Day 8. Spring Data JPA를 사용한 데이터베이스 조작8일차에는 String으로 작성했던 쿼리 방식의 단점을 알아보고 변경하는 시간이었다.Java의 객체와 Database의 테이블의 패러다임이 다르다는걸 정확하게 알게되었다.또한 Java의 상속을 Database로 표현하기 어려운 부분이 존재한다는걸 알게되었다.JPA는 단순한 규칙이라는걸 알게되었다.JPA의 전반적인 설정 방법과 사용 방법들을 알게되었다.람다식이 우르르 나와서 해당 관련 공부를 또 뼈저리게 느꼈다. 과제https://devhan.tistory.com/324이번 과제도 기존에 작성했던 코드 중 문자열로 작성된 쿼리를 JPA로 변경하는 과제였다.warehousingDate 필드가 제대로 맵핑되지 않는 문제때문에 골머리를 앓았다. 찾아보니 스프링에서는 카멜케이스로 작성된 필드는 DB 컬럼에서는 '_'로 단어를 구분한다고 한다. ex) warehousingDate -> warehousing_date이 에러는 ddl-auto를 create로 만들어주고 처음부터 다시 매핑해서 해결되었다.그 외에는 딱히 어려운 점은 없었다! Day 9. 트랜잭션과 영속성 컨텍스트트랜잭션의 이론에 대해서 알게 되었다. 트랜잭션이 뭘 하는지는 대략적으로 알았는데 딱 정의할 수는 없었다. 트랜잭션 -> 쪼갤 수 없는 업무의 최소 단위@Transactional의 사용 범위와 readOnly 옵션에 대해 알게 되었다.Select 쿼리에서는 readOnly 옵션을 사용하는게 성능상 더 좋다영속성 컨텍스트에 대해 알게 되었다.테이블과 매핑된 엔티티 객체를 관리하고 보관스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨남트랜잭션을 종료하면 영속성 컨텍스트가 종료됨영속성 컨텍스트의 4가지 장점변경 감지(Dirty Check) : 영속성 컨텍스트 안에서 불러와진 객체는 명시적으로 save 하지 않더라도, 기존 정보와의 변경을 감지해 자동으로 저장된다.쓰기 지연 : DB의 INSERT, UPDATE, DELETE SQL을 바로바로 날리는게 아니라 트랜잭션이 commit될 때 한번에 모아서 한 번만 날린다.1차 캐싱 : 아이디를 기준으로 객체를 기억하고, 해당 객체가 DB에서 다시 불려와질 때 변경된 정보가 없다면 굳이 DB를 통하지 않고 영속성 컨텍스트에서 해당 정보를 돌려준다.캐싱된 객체는 완전히 동일하다 -> 객체의 인스턴스 주소까지 완전 동일 과제이 날부터 미니 프로젝트가 시작되었다.첫 날이라 유의미한 코딩을 한 건 아니었고 프로젝트 생성부터 설정까지 완료하였다.그리고 앞으로 어떤식으로 개발할지 전체적인 틀을 그렸던 것 같다! Day 10. 조금 더 복잡한 기능을 API로 구성하기책 생성, 대출, 반납 기능을 개발하면서 전체적인 개념을 다시 잡기도하고 좀 더 객체지향적으로 코딩하는 방법을 알았던 날이다.되게 좋았던 건 얼마 전에 객체지향의 사실과 오해라는 책을 읽었었는데 사실 책에서는 예시 코드가 나오지 않아 약간 실무에서는 어떤식으로 접근해야할까에 대한 의문이 좀 남아있었다. 책을 읽어도 약간 반만 알겠는 느낌..? 근데 이 강의에서 딱 객체지향적으로 코딩을 해볼 수 있어서 그래도 느낌을 좀 알 것 같았다. 평소 회사에서 전혀 객체지향적으로 개발하지 않아 domain에 비즈니스 로직을 직접 넣어도 되는지 조차 몰랐었는데 이번 강의에서 그 부분에 대한 갈증을 조금 해소할 수 있어서 아주 마음에 들었다. 과제https://devhan.tistory.com/326어느정도 개발이 끝났던 시기였던 거 같다. 과제를 하면서 Team과 Employee의 연관관계를 담은 테이블을 강의처럼 따로 만들어야할지 말지 고민이 많았었고 또, Team의 ID를 Long 타입의 id로 하느냐, String 타입의 name으로 하느냐의 고민도 많았다..! Long 타입의 id로 하면 나중에 Employee를 조회할 때마다 Team의 객체를 가져와서 name을 또 Response 객체에 저장해줘야할 것 같은 번거로움 때문에 그냥 String name을 primary key로 지정했다. 코드에 대한 많은 생각을 할 수 있어서 좋은 시간이었던 거 같다.   

백엔드워밍업클럽백엔드스터티

이양구

[인프런 워밍업 클럽 FE 0기] 미션4-2 - GitHubFinder 앱

🔍 github-finder-app GitHub 🔍 github-finder-app 개요인프런 워밍업 클럽 FE 0기의 네 번째 미션인 'GitHubFinder 앱' 입니다.따라하며 배우는 자바스크립트 섹션 5(OOP), 섹션 6(비동기) 목표Fetch API 를 이용해 깃허브 유저 목록 불러오기Closure 를 이용해 Debounce Function 만들기 구현Fetch API 를 이용해 깃허브 유저 목록 불러오기async function loadUser(input) { prevInputValue = input; try { // const response = await fetch('./src/javascript/user.json'); const response = await fetch(`${url}/${input}`); if (!response.ok) { throw new Error('Failed to fetch user json'); } const json = await response.json(); setUserAvatar(json); setUserInfo(json); await loadUserRepos(json); } catch (error) { console.error(error); } }fetch() 메서드의 응답은 HTTP 응답 전체를 나타내는 'response' 객체를 반환한다.response의 ok 속성은 응답의 성공 여부를 불리언 값으로 가지고 있다.따라서 응답이 성공이 아닐 경우 오류 객체(new Error())를 반환하고 catch 문으로 Promise의 오류를 처리한다.응답에 성공한 response 객체를 JSON으로 사용하기 위해선 json() 메서드를 이용해 파싱해야 한다. Closure 를 이용해 Debounce Function 만들기// debounce debounceInput.addEventListener('input', debounce(loadUser, 1000)); // debounceInput.addEventListener('input', e => callback(e)); function debounce(callback, delay = 0) { // timer는 부모 함수에서 선언된 지역 변수 let timer = null; return (arg) => { // 여기서 arg는 input event if (timer) { // 이미 타이머가 있는데 또 실행되면 타이머 삭제 clearTimeout(timer); } // 변수 timer는 부모 함수에서 선언되었지만 내부 함수에서 사용(클로저) timer = setTimeout(() => { callback(arg.target.value); }, delay); }; }<input> 요소의 'input' 이벤트는 요소의 value가 변경될 때마다 발생한다.만약 사용자가 입력할 때마다 서버에 데이터를 요청한다면 서버의 부하가 커지기 때문에 좋은 방법은 아니다.이럴 때 사용자의 입력이 끝난 뒤 마지막 value를 이용해 서버로 요청하는 게 효율적인 방법이라 할 수 있다.함수의 실행 요청이 반복될 때 마지막 요청만으로 실행하는 걸 '디바운싱(debouncing)'이라고 부른다.debounce 함수는 인자로 실행할 함수를 받고 자식 함수를 반환한다.부모 함수인 debounce 함수에서 선언한 변수(timer)를 자식 함수에서 사용할 수 있는 클로저(Closure)를 이용해 자식 함수의 setTimeout() 메서드의 반환 값인 'timeoutID'를 할당한다.변수 'timer'에 할당한 timeoutID를 이용해 setTimeout() 메서드의 지연 시간(delay)이 종료되기 전에 요청이 들어왔다면 이전에 생성한 타이머를 clearTimeout() 메서드를 이용해 종료하고 다시 타이머를 할당한다. 이렇게 delay로 설정한 시간 이내에 사용자의 입력이 없을 경우 API 요청 함수를 실행하게 된다. 반복적인 함수의 실행을 다루는 방법으로 디바운싱(debouncing)와 쓰로틀링(throttling)이 있다.여러 변수를 고려해 'lodash' 라이브러리의 debounce를 많이 사용한다. 회고이번 미션은 debounce가 반환하는 자식 함수의 인자(argument)가 어떤 타입인지 알기 때문에 callback 함수에 전달하는 인자를 수정해서 미숙한 debounce 함수라고 볼 수 있다.늘 라이브러리를 통해 사용하던 함수를 만들려고 하니 모르는 것도 많고, 고려해야 할 부분이 많다는 걸 알게 됐다.자바스크립트의 기초를 잘 알아야 이런 라이브러리 메서드의 원리를 이해하기도 쉽고, 커스텀하기에 수월한 것 같다.(외의로 GitHub의 API 요청이 API key 없이도 되어서 신기했고, 그 덕에 조금은 수월했다. 아주 조금... 😵) DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

[인프런워밍업스터디_BE_0기] 일곱 번째 과제!

 문제 1여섯번째 과제에서 만들었던 Fruit 기능들을 JPA를 이용하도록 변경해보세요! 오늘의 과제는 어제까지 선생님께 배운 다형성을 활용할 기회다! MemoryRepository를 사용하더라도, MySqlRepository를 사용하더라도 @Primary만 바꿔주면 되도록 변경해두었고, 주말에 미리 예습해두었던 JPA에서 사용할 수 있도록 Repository의 메서드도 맞춰둔 상황! JpaRepository만 만들고 바꿔주면 되겠지?! [JPA를 적용한 Repository]public interface FruitJpaRepository extends JpaRepository<Fruit, Long> { List<Fruit> findAllByName(String name); @Query("select new com.day4.fruitapp.dto.fruit.model.FruitStat(f.soldYn, sum(f.price)) from Fruit f where f.name = :name group by f.soldYn") List<FruitStat> getStats(@Param("name") String name); }하지만 역시 놓친 부분이 있었으니..[기존 서비스 코드]@Service public class FruitServiceV1 implements FruitService{ // 중략 ... @Override public void sellFruit(FruitSellReqeust reqeust) { Fruit fruit = fruitRepository.findById(reqeust.getId()) .orElseThrow(() -> new FruitNotFoundException("과일 정보를 찾을 수 없습니다.")); if (fruit.isSold()) { throw new FruitNotFoundException("이미 팔린 과일입니다."); } fruitRepository.sell(fruit.getId()); // getStats처럼 만들어야하나...? } // 후략 ... }팔린 과일에 대한 정보를 업데이트하는 부분인데.. FruitJpaRepository의 getStats 처럼 @Query 를 통해 구현할까도 싶었지만! sell이라는 팔렸다는 표현은 Fruit 도메인에 더 잘어울리는 것 같으니 다음과 같이 변경하자! @Service public class FruitServiceV2 implements FruitService { // 중략 ... @Override public void sellFruit(FruitSellReqeust reqeust) { Fruit fruit = fruitRepository.findById(reqeust.getId()) .orElseThrow(() -> new FruitNotFoundException("과일 정보를 찾을 수 없습니다.")); if (fruit.isSold()) { throw new FruitNotFoundException("이미 팔린 과일입니다."); } fruit.sell(); fruitRepository.save(fruit); } // 후략 ... }각 api의 요청 결과도 그대로 잘 나온다! 문제 2특정 과일을 기준으로 우리 가게를 거쳐갔던 과일 개수를 세고 싶다.예를 들어1. (1, 사과, 3000원, 판매 O)2. (2, 바나나, 4000원, 판매X)3. (3, 사과, 3000원, 판매 O)와 같은 세 데이터가 있을 때 사과를 기준으로 개수를 센다면 2를 반환한다. API의 스펙은 다음과 같다.1. HTTP method: GET2. HTTP path: /api/v1/fruit/count3. HTTP query: name=과일이름4. 예시 GET /api/v1/fruit/count?name=사과5. HTTP 응답 Body{"count" : long}6. HTTP 응답 예시{"count" : 2} 이번 요구사항은 간단하다. 바로 구현해보자![컨트롤러 클래스]@RestController public class FruitController { // 중략 ... @GetMapping("/api/v1/fruit/count") public FruitCountResponse getCount(@RequestParam String name) { return fruitService.getCount(name); } } [서비스 클래스]@Service public class FruitServiceImpl implements FruitService { // 중략 ... @Override public FruitCountResponse getCount(String name) { return new FruitCountResponse(fruitRepository.countByName(name)); } } [리포지토리 클래스]public interface FruitJpaRepository extends JpaRepository<Fruit, Long> { List<Fruit> findAllByName(String name); @Query("select new com.day4.fruitapp.dto.fruit.model.FruitStat(f.soldYn, sum(f.price)) from Fruit f where f.name = :name group by f.soldYn") List<FruitStat> getStats(@Param("name") String name); long countByName(String name); }DB에 없는 과일의 개수를 요청할 때 예외처리를 할 것인지 고민되었지만 거쳐갔던 과일의 개수를 반환하는 것이 요구사항이니 거쳐갔던 적이 없는 과일은 0을 반환하는 것이 맞는 것 같아 예외처리하지 않았다.이제 API 요청 결과를 보자! 문제 3아직 판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록을 받고 싶다.API 스펙은 다음과 같다.1. HTTP method: GET2. HTTP path: /api/v1/fruit/list3. HTTP query: option=GTE또는option=LTE, price=기준 금액- GTE : greater than equal의 의미- LTE : less than equal의 의미4. 예시 GET /api/v1/fruit/count?option=GTE&price=3000- 판매되지 않은 3000원 이상의 과일 목록을 반환한다.5. HTTP 응답 Body[{"name" : String,"price" : long,"warehousingDate" : LocalDate}, ...]6. HTTP 응답 예시[{"name" : "사과","price" : 4000,"warehousingDate" : "2024-01-05"},{"name" : "바나나","price" : 6000,"warehousingDate : "2024-01-8"}] 이번 요구사항은 조금 복잡하지만 그래도 어렵진 않다! LTE, GTE라는 파라미터를 문자열로 받기 보다는 Enum으로 관리하면 좋을 것 같다.. 바로 구현해보자! [요청 클래스와 Enum 클래스]public class NotSoldFruitRequest { private PriceComparison option; private long price; public NotSoldFruitRequest(PriceComparison option, long price) { this.option = option; this.price = price; } public PriceComparison getOption() { return option; } public long getPrice() { return price; } } public enum PriceComparison { GTE, LTE }  [컨트롤러 클래스]@RestController public class FruitController { // 중략 ... @GetMapping("/api/v1/fruit/list") public List<NotSoldFruitResoponse> getNotSoldFruits(NotSoldFruitRequest request) { return fruitService.getNotSoldFruits(request); } } [서비스 클래스]@Service public class FruitServiceImpl implements FruitService { // 중략 ... @Override public List<NotSoldFruitResoponse> getNotSoldFruits(NotSoldFruitRequest request) { List<Fruit> fruits = switch (request.getOption()) { case GTE -> fruitRepository.findAllBySoldYnAndPriceGreaterThanEqualOrderByName(request.getPrice()); case LTE -> fruitRepository.findAllBySoldYnAndPriceLessThanEqualOrderByName(request.getPrice()); }; return fruits.stream().map(NotSoldFruitResoponse::new).collect(Collectors.toList()); } } [리포지터리 클래스]public interface FruitJpaRepository extends JpaRepository<Fruit, Long> { // 중략 ... List<Fruit> findAllBySoldYnAndPriceGreaterThanEqualOrderByName(String soldYn, long price); List<Fruit> findAllBySoldYnAndPriceLessThanEqualOrderByName(String soldYn, long price); }"N"을 직접 파라미터로 넘겨주는게 마음에 들지 않지만 시간이 부족하니 오늘은 여기까지..ㅠ 이제 요청해보자

백엔드

jikwan12

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

2024 새로운 도전을 하기 위해 여러 사이트를 돌아다니던 중 인프런 워밍업 클럽을 발견하게 되었습니다. 마침 저는 취업 준비를 앞두고 있고 자바스크립트 기초 지식을 단단하게 다져 취업을 준비해야겠다는 생각이 들었고 이러한 좋은 기회를 놓칠 수 없어 바로 신청하게 되었습니다.강의 요약01. 자바스크립트 기초[ console 객체 ]JavaScript에서 제공하는 내장 객체 중 하나이다.이 객체를 사용하면 개발자는 브라우저의 콘솔이나 터미널에 다양한 정보를 출력하고 디버깅할 수 있다.다양한 메서드를 제공하여 개발자가 원하는 정보를 출력하거나 디버깅할 때 유용하다.console 객체 일부 메서드log(): 콘솔에 일반적인 로그 메시지를 출력error(): 콘솔에 에러 메시지를 출력warn(): 콘솔에 경고 메시지를 출력 table(): 테이블 형태로 데이터를 출력time()과 timeEnd(): 특정 코드 블록의 실행 시간을 측정  [ let, var, const]let:블록 범위(block-scoped) 변수를 선언변수를 선언한 블록 내에서만 접근 가능변수의 값은 재할당이 가능같은 이름의 변수를 중복해서 선언할 수 없음var:함수 범위(function-scoped) 변수를 선언변수를 선언한 함수 내에서만 접근 가능변수의 값은 재할당이 가능같은 이름의 변수를 중복해서 선언할 수 있음호이스팅(hoisting)이 발생하여 변수 선언이 해당 스코프의 최상단으로 끌어올려짐const:블록 범위(block-scoped) 상수를 선언변수를 선언하고 초기값을 할당한 후, 재할당이 불가능변수의 값이 변하지 않아야 할 때 사용같은 이름의 변수를 중복해서 선언할 수 없음 [ 호이스팅 ]JavaScript에서 변수 선언과 함수 선언을 해당 스코프의 최상단으로 끌어올리는 동작변수 호이스팅의 예시console.log(x); // undefined var x = 5; console.log(x); // 5위 코드에서 var x = 5; 문장 이전에 console.log(x);를 호출하고 있다. 하지만 결과는 undefined가 출력된다. 이는 변수 x의 선언이 호이스팅에 의해 해당 스코프의 최상단으로 끌어올려진 후, 초기값이 할당되기 전까지 undefined로 초기화되기 때문이다. 함수 호이스팅의 예시sayHello(); // "Hello!" function sayHello() { console.log("Hello!"); }위 코드에서 sayHello() 함수를 호출하기 전에 함수를 선언하고 있다. 이는 함수 선언이 호이스팅에 의해 해당 스코프의 최상단으로 끌어올려진 후, 호출 위치와 상관없이 함수를 사용할 수 있게 된다. [ 타입, 타입 변환 ]자바스크립트의 타입은 다음과 같음원시 타입: Boolean, String, Number, null, undefined, Symbol참조 타입: Object, Array  원시타입은 고정된 크기로 Call Stack 메모리에 저장되며 실제 데이터가 변수에 할당된다.참조타입은 데이터 크기가 정해지지 않고 Call Stack 메모리에 저장되며 데이터의 값이 Heap에 저장되며 메모리의 주소 값이 할당된다. 타입 변환 유형명시적 데이터 변환: 직접 함수를 통한 변환자동 데이터 변환: 자바스크립트 자체에 의해 자동으로 변환 [ 연산 및 Math Object ]Math object에서 제공하는 다양한 메서드들을 이용해 다양한 연산을 할 수 있다.Math 일부 메서드Math.abs(x):주어진 숫자의 절댓값을 반환합니다.예: Math.abs(-5)는 5를 반환합니다.Math.ceil(x):주어진 숫자보다 크거나 같은 정수 중 가장 작은 수를 반환합니다.예: Math.ceil(4.2)는 5를 반환합니다.Math.floor(x):주어진 숫자보다 작거나 같은 정수 중 가장 큰 수를 반환합니다.예: Math.floor(4.8)는 4를 반환합니다.[ Template Literals ]Template literal은 JavaScript에서 문자열을 작성하는 방법으로, 백틱(``)을 사용한다. 이를 통해 변수나 표현식을 쉽게 삽입하고 멀티라인 문자열을 작성할 수 있으며, 특수 문자 처리에 용이하다. ${}를 활용하여 문자열에 변수를 할당할 수 있다.[ Loops ]for: 코드 블록을 여러 번 반복for/in: 객체 속성을 따라 반복while: 지정된 조건이 true 인 동안 코드 블록을 반복do/while: 조건이 true인지 검사 전에, 코드 블록을 한 번 실행시킨다. 그 이후 while 반복문 실행02. Window 객체 및 DOM[ window 객체 ]window 객체는 웹 브라우저 창을 나타내는 전역 객체입니다.JavaScript에서 모든 전역 변수와 함수는 window 객체의 프로퍼티 또는 메서드로 접근할 수 있습니다. 예를 들어, window.alert()와 window.innerWidth와 같이 사용할 수 있습니다.브라우저 창의 크기, URL 정보, 타이머, 팝업 창 등 다양한 기능을 제공합니다.window 객체는 생략 가능하므로, alert()와 innerWidth와 같이 사용하는 것도 가능합니다.[ DOM ]DOM은 HTML, XML 또는 XHTML 문서의 구조를 표현하는 객체 지향적인 표현 방식입니다.웹 페이지의 요소(element)들을 트리 구조로 표현하며, 각 요소는 노드(node)라고 불리는 객체로 표현됩니다.DOM을 통해 JavaScript는 문서의 구조, 스타일, 내용 등을 동적으로 조작할 수 있습니다.DOM은 HTML 요소에 접근하고 조작하기 위한 다양한 메서드와 속성을 제공합니다. 예를 들어, getElementById(), querySelector(), innerHTML 등을 사용하여 요소를 선택하거나 내용을 변경할 수 있습니다.DOM은 웹 페이지의 상호작용과 동적인 변경을 가능하게 해주는 핵심적인 요소로, JavaScript에서 웹 페이지와의 상호작용을 구현하는 데 사용됩니다.03. Event[ Event ]JavaScript에서 이벤트는 웹 페이지나 애플리케이션에서 발생하는 사용자의 동작이나 시스템 상태 변화를 감지하고, 이에 대한 응답을 처리하는 메커니즘입니다. 이벤트는 HTML 요소나 JavaScript 객체에서 발생할 수 있으며, 특정 동작이나 상태 변화에 대응하여 코드를 실행할 수 있도록 도와줍니다.[ Event Bubbling과 Event Capturing ]이벤트 버블링(Event Bubbling)과 이벤트 캡처링(Event Capturing)은 HTML 요소 간에 중첩된 구조에서 이벤트가 전파되는 방식을 설명하는 개념입니다.이벤트 버블링 (Event Bubbling):이벤트 버블링은 이벤트가 발생한 요소에서 시작하여 상위 요소로 전파되는 현상을 의미합니다.예를 들어, <div> 요소 안에 <p> 요소가 있고, <p> 요소를 클릭할 때 해당 이벤트가 발생하면, 이벤트는 <p> 요소에서 시작하여 <div> 요소, 그리고 상위 요소들로 전파됩니다.이벤트 버블링은 부모 요소로 이벤트가 전파되므로, 상위 요소에서도 동일한 이벤트를 처리할 수 있습니다.이벤트 리스너를 상위 요소에 등록하면 하위 요소에서 발생한 이벤트도 감지할 수 있습니다.이벤트 캡처링 (Event Capturing):이벤트 캡처링은 이벤트가 발생한 요소에서 시작하여 상위 요소로 전파되는 현상과 반대로, 상위 요소에서 시작하여 이벤트가 발생한 요소까지 전파되는 현상을 의미합니다.이벤트 캡처링은 이벤트가 발생한 요소의 최상위 부모 요소부터 시작하여 하위 요소로 전파됩니다.이벤트 캡처링은 이벤트 리스너를 상위 요소에 등록하고, 하위 요소에서 발생한 이벤트를 상위 요소까지 전파하여 처리할 수 있습니다. [ Event Delegation ]이벤트 위임(Event Delegation)은 상위 요소에 이벤트 리스너를 등록하여 하위 요소들의 이벤트를 처리하는 방식 04. 자바스크립트 중급[ this ]Method의 this: 해당 객체를 가리킨다.함수에서 this: window 객체를 가리킨다.constructor의 this: 빈 객체를 가리킨다. [ bind, call, apply]call():함수를 호출하는 함수.첫 번째 인자 값으로 어떠한 것을 전달해 주면 호출되는 함수의 this가 인자 값으로 지정apply(): call()과 유사하나 인수 부분에 배열을 넣어줘야함.bind(): 해당 함수가 지정한 인자 값을 가리키도록 하지만 call(), apply()와 다르게 직접 함수 실행 X [ 삼항 연산자 ]? 앞의 조건을 기준으로 조건이 참이면 : 앞의 있는 부분을 반환하고 거짓이면 뒷부분 반환 [ Event Loop]이벤트 루프(Event Loop)는 JavaScript에서 비동기 작업을 처리하는 동작 메커니즘입니다. 이벤트 루프는 단일 스레드 환경에서 비동기 작업을 효율적으로 처리하기 위해 사용됩니다.[ Closure ]다른 함수 내부에 정의된 함수가 있는 경우, 외부 함수가 실행을 완료하고 해당 변수가 해당 함수의 외부에서 더 이상 엑세스할 수 없는 경우에도, 해당 내부 함수는 외부 함수의 변수 및 액세스가 가능하다. 이 기능을 Closure라고 부른다.[ 구조 분해 할당 ]배열이나 객체의 속성을 분해하여 사용할 수 있는 형태로 만드는 것[ Map, Filter, Reduce ]Map, Filter, Reduce은 배열 메서드의 대표적인 예시이다.map(): 배열 내의 모든 요소 각각에 대하여 동작을 수행하고 나온 결과를 새로운 배열로 만들어 반환한다.filter(): 기존 배열의 필터를 통과하는 요소만을 새로운 배열로 반환한다.reduce(): 배열의 각 요소에 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결괏값을 반환한다.[ 얕은 복사 VS 깊은 복사 ]깊은 복사(Deep Copy): 원본 객체의 모든 내용을 복사하여 새로운 객체를 생성합니다. 얕은 복사(Shallow Copy): 원본 객체의 참조만을 복사하여 동일한 객체를 가리키는 새로운 객체를 생성합니다.[ 함수 표현식, 함수 선언문 ]함수 선언문: 함수 선언은 함수를 만들고 이름을 지정하는 것이다.일반적인 함수 선언 방식으로 function 키워드와 식별자를 표기하여 사용한다.함수 표현식은 함수를 만들고; 변수에 할당하는 것이다.익명 함수(function 키워드는 사용했으나 식별자 X), 화살표 함수 사용05. OOP객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그래밍 패러다임 중 하나로, 현실 세계의 개념과 개체들을 프로그램의 구조로 모델링하는 방식입니다. OOP는 코드의 재사용성, 유지보수성, 확장성 등을 향상시킬 수 있는 다양한 개념과 기법을 제공합니다.OOP의 주요 개념은 다음과 같습니다:클래스(Class):클래스는 객체를 생성하기 위한 설계도 또는 템플릿으로, 속성(멤버 변수)과 동작(메서드)을 포함합니다.예를 들어, '사람' 클래스는 '이름'과 '나이'라는 속성을 가지고 '인사하기'라는 동작을 수행할 수 있습니다.객체(Object):클래스의 인스턴스로, 실제로 메모리에 할당된 개체입니다.객체는 클래스에서 정의한 속성과 동작을 가지며, 각각의 객체는 독립적인 상태를 유지합니다.'사람' 클래스의 객체로는 '철수'나 '영희' 등이 있을 수 있습니다.상속(Inheritance):상속은 클래스 간의 계층 구조를 구성하여 코드의 재사용성을 높이는 개념입니다.부모 클래스(상위 클래스)의 속성과 동작을 자식 클래스(하위 클래스)가 물려받아 사용할 수 있습니다.상속을 통해 일반적인 기능을 가진 부모 클래스를 정의하고, 이를 기반으로 특화된 자식 클래스를 만들 수 있습니다.다형성(Polymorphism):다형성은 동일한 메서드를 다양한 방식으로 호출할 수 있는 개념입니다.하나의 메서드가 다른 클래스의 객체에 따라 다른 동작을 수행할 수 있습니다.다형성은 상속과 함께 사용되어 코드의 유연성과 확장성을 높여줍니다.캡슐화(Encapsulation):캡슐화는 관련된 속성과 동작을 하나의 단위로 묶는 개념입니다.클래스는 내부의 속성과 동작을 외부로부터 숨기고, 외부에서는 제공된 인터페이스를 통해 클래스와 상호작용합니다.캡슐화는 데이터의 은닉과 코드의 모듈화를 가능하게 합니다.06. 비동기[ 동기 비동기 ]동기(Synchronous)는 작업을 순차적으로 실행하고, 이전 작업이 완료될 때까지 다음 작업을 실행하지 않는 방식입니다. 작업이 순서대로 진행되기 때문에 실행 순서가 보장되며, 결과를 예측하기 쉽습니다. 하지만 작업이 완료될 때까지 대기해야 하므로 다른 작업을 처리하는 동안 시간이 지체될 수 있습니다.비동기(Asynchronous)는 작업을 동시에 실행하고, 이전 작업의 완료 여부와 상관없이 다음 작업을 실행하는 방식입니다. 작업이 백그라운드에서 동시에 처리되기 때문에 실행 순서가 보장되지 않을 수 있습니다. 비동기 방식은 작업이 완료되기를 기다리지 않고 다른 작업을 처리할 수 있으므로 시간을 효율적으로 활용할 수 있습니다.07. Iterator, Generator[ Symbol ]Symbol(심볼)은 ECMAScript 6에서 추가된 원시 데이터 타입입니다. 심볼은 고유하고 변경 불가능한 값으로, 주로 객체의 속성 키로 사용됩니다. 심볼은 Symbol() 함수를 통해 생성되며, 생성된 심볼은 다른 심볼과 절대로 동일하지 않습니다.심볼은 유일성과 불변성을 보장하기 때문에 객체의 속성 키로 사용하면 충돌을 방지하고 예상치 못한 동작을 방지할 수 있습니다. 또한, 심볼은 접근할 수 없는 외부에서 속성을 덮어쓸 위험을 줄여줍니다. 예를 들어, 두 개의 객체가 동일한 심볼을 속성 키로 사용하더라도 실제로는 서로 다른 속성을 가지게 됩니다.[ Iterator ]Iterator(이터레이터)는 JavaScript의 내장 프로토콜 중 하나로, 반복 가능한 객체(Iterable)에서 요소를 차례대로 가져오는 방법을 제공하는 인터페이스입니다. 이터레이터는 next() 메서드를 사용하여 다음 요소를 반환하고, { value, done } 형태의 객체로 값을 전달합니다. value에는 다음 요소의 값이 들어있고, done은 이터레이터가 더 이상 요소를 가지고 있지 않을 때 true가 됩니다. Symbol.iterator 메서드를 사용하여 이터레이터를 반환하는 것이 일반적입니다.[ Generator ]Generator(제너레이터)는 이터레이터를 생성하는 함수입니다. 제너레이터 함수는 function* 키워드로 정의되며, 함수 내부에서 yield 키워드를 사용하여 값을 반환하고 일시적으로 실행을 중지하며, next() 메서드를 호출함으로써 다시 시작할 수 있습니다. 제너레이터 함수를 호출하면 제너레이터 객체가 반환되며, 이 객체는 이터레이터 프로토콜을 따르기 때문에 next() 메서드를 사용하여 값을 가져올 수 있습니다. 08. Design Pattern디자인 패턴의 장점재사용성과 유지보수성 향상코드 가독성과 이해도 향상소프트웨어의 확장성과 유연성설계의 일관성과 표준화문제 해결 과정의 가속화Singleton Pattern: 특정 클래스의 인스턴스를 전역에서 하나만 생성하고 접근할 수 있도록 하는 디자인 패턴입니다.Factory Pattern: 객체를 생성하는 인터페이스를 정의하고, 이를 서브 클래스에서 구현하여 객체 생성을 캡슐화하는 디자인 패턴입니다.Mediator Pattern: 객체들 간의 상호작용을 중재하는 중재자 객체를 도입하여 결합도를 낮추고, 객체 간의 직접적인 통신을 제어하는 디자인 패턴입니다.Observer Pattern: 한 객체의 상태 변화에 따라 다른 객체들에게 자동으로 변화를 알리고, 그에 따른 처리를 할 수 있도록 하는 디자인 패턴입니다.Module Pattern: 모듈화된 코드를 구성하여 캡슐화하고, 필요한 부분만 노출시키며 코드의 재사용성과 유지보수성을 향상시키는 디자인 패턴입니다. 느낀점 및 각오자바스크립트의 중요성에 대하여 다시 한 번 크게 느낄 수 있었던 1주차였습니다. 자바스크립트 지식을 탄탄히 쌓고 배운 것들을 항상 이해하고 프로젝트에 적용까지 하며 기초를 상시 이해하며 면접준비까지 탄탄하게 해야겠다는 생각이 들었습니다. 남은 기간 동안에도 과제 수행을 열심히하며 저의 성장을 위해 최선을 다해보겠습니다! 감사합니다.

웹 개발FE워밍업클럽

이양구

[인프런 워밍업 클럽 FE 0기] 미션1 - 음식 메뉴 앱

🍝 food-recipe-app API from TheMealDBGitHub food-recipe-app 개요인프런 워밍업 클럽 FE 0기의 첫 번째 미션인 '음식 메뉴 앱' 만들기입니다.따라하며 배우는 자바스크립트의 섹션 1~3(자바스크립트 기초, Window 객체 및 DOM, Event)를 보고 자바스크립트의 DOM 요소를 조작하는 데 중점을 두었습니다.음식 데이터는 TheMealDB의 API를 이용했습니다. 사용한 API가 '음식 레시피'라서 이름을 변경했습니다. 목표문서 객체 모델(The Document Object Model, 이하 DOM)의 메소드(methods)를 이용해 요소(element)에 접근하고 생성하고 교체하기이벤트 리스너(Event Listener) 메소드를 이용해 요소에 이벤트를 등록하고 이벤트 객체 이용하기메뉴 데이터를 Fetch API를 사용해 불러오기 구현이벤트 위임(Event Delegation)을 이용한 이벤트 생성/* <nav id="food-navigation"> <div class="food-navigation-item"> <button id="Beef"> <figure> <img src="https://www.themealdb.com/images/category/beef.png"> <figcaption> Beef </figcaption> </figure> </button> </div> // ... </nav> */ // Not Event Delegation foodNavigation.querySelectorAll('button').forEach((button) => { button.addEventListener('click', async () => { const targetId = button.id; await setFoodList(targetId); }); }); // Event Delegation foodNavigation.addEventListener('click', async (event) => { const targetElement = event.target; // closest() 메서드는 주어진 CSS 선택자와 일치하는 요소를 찾을 때까지, // 자기 자신을 포함해 위쪽(부모 방향, 문서 루트까지)으로 문서 트리를 순회합니다. const targetDiv = targetElement.closest('.food-navigation-item'); if (!targetDiv) { return; } const targetButton = targetDiv.querySelector('button'); const targetId = targetButton.id; await setFoodList(targetId); });이벤트 위임이란 '상위 요소에서 하위 요소의 이벤트를 제어하는 것'을 의미합니다.이벤트를 위임하는 이유이벤트를 하나의 핸들러로 처리함으로써 메모리 사용량을 줄이고 성능을 향상시킬 수 있다.새로운 요소가 추가되거나 제거되는 경우 이벤트 리스너는 상위 요소에 연결되어 있어 재연결의 필요성이 줄어든다.저는 nav 태그에 이벤트를 등록하고 closest 메서드를 이용해 버튼의 id를 찾는 방법을 사용했습니다. 하위 요소를 제거하고 생성한 요소를 추가하기/* <div id="food-list"> <div class="food-list-item"> <figure> <img src="img src" /> </figure> <div class="food-list-item-desc"> <p>food name</p> <hr /> <div> food recipe </div> </div> </div> </div> */ const foodList = await getFoodList(strCategory); const foodListElement = document.getElementById('food-list'); const foodListItem = document.querySelectorAll('.food-list-item'); foodListItem.forEach((item) => item.remove()); // foodListElement.innerHTML = ''; foodList.map(async (food) => { // ... const foodElement = getFoodElement( idMeal, strMeal, strMealThumb, strInstructions ); foodListElement.appendChild(foodElement); });배웠던 removeChild()와 replaceChild() 메서드를 이용하고자 했으나...'만약 해당 카테고리의 음식 리스트의 개수가 다르다면 어떻게 하지?'라는 생각에 한번에 제거하기로 결정했습니다.처음엔 innerHTML을 이용해 하위 코드를 공백으로 만들었지만, 뭔가 이건 너무 이상하다는 생각(요소의 참조나 연결 같은 게 깨지진 않을까)이 들어 찾아보았습니다.stack overflow의 Remove child nodes (or elements) or set innerHTML=""?라는 글에서는 innerHTML은 하위 요소의 이벤트 핸들러가 완전히 제거되지 않을 수도 있다고 한다.또한 Why InnerHTML Is a Bad Idea and How to Avoid It?에서는 innerHTML이 보안상 좋지 않다는 점을 말하고 있다. Stack Overflow의 글을 자세히 읽어 보니 다음과 같은 글이 있었다.What is the best way to empty a node in JavaScript그리고 MDN 문서에도 이렇게 소개하고 있다.replaceChildren() provides a very convenient mechanism for emptying a node of all its children. You call it on the parent node without any argument specified:즉 replaceChildren()메서드를 빈 인자로 실행하면 하위 자식 노드를 모두 지워준다는 것...!😅 회고빈 폴더를 놓고 코드를 작성해본 게 너무 오랜만인 것 같다.자료를 찾기 귀찮다는 마음과 첫 미션이니까 API를 써볼까 하며 자만했던 순간도 있었다.미션의 목적보다 어느새 다른 부분을 신경 쓰느라 배보다 배꼽이 점점 커지는 것 같았다.딸랑 script 태그 한 줄 작성하고 js 파일을 제대로 못 불러와서 몇 시간을 해결 방법을 찾아서 해매기도 했다.😭이벤트 위임 코드를 작성할 때 이은재 님의 시나브로 자바스크립트에서 배웠던 부분을 참고했다.음식 레시피를 불러올 때 요소를 지우고 불러와서 그런지 해당 부분이 사라지고 나타나서 페이지가 늘었다 줄었다 하는 게 눈에 띈다.이래서 가상 돔을 쓰는걸까? 아니면 태그의 속성을 하나하나 수정하면 되는걸까?일단 진도를 따라잡고 배워서 발전시켜야겠다. DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽FE프론트프론트엔드과제미션발자국

xicodey

[인프런 워밍업 클럽 0기] BE 3일차 과제

자바의 람다식은 왜 등장했을까? 자바는 람다식 함수형 프로그램밍이 사용한 이유는 불필요한 코드를 줄이고, 가독성을 높이기 위해서다 .그리고 함수 만드는 과정없이 한번에 처리 할 수 있기 때문에 생산성이 높아진다.병령 프로그램밍에 용이하다. 람다식과 익명 클래스는 어떤 관계가 있을까? 함수형 프로그래밍이란 함수를 정의하고 이 함수를 데이터 처리부로 보내 데이터를 처리하는 기법이다.데이터 처리부는 데이터만 가지고 있을 뿐, 처리 방법이 정해져 있지 않아 외부에서 제공져 있지 않아 외부에서 제공된 함수에 의존한다.데이터 처리부는 제공된 함수의 입력값으로 데이터를 넣어 함수에 정의된 처리 내용을 실행하고, 동일한 데이터라도 함수 A를 제공한 결과 값과 함수 B를 제공하여 처리된 결과 값은 다를 수 있다 이것이 함수형 프로그램의 특징인 데이터 처이의 다형성이다.람다식은 함수를 하나의 식으로 표현하여 익명 함수를 반환한다.익명 클래스는 선언된 클래스 내에서만 한 번만사용될 경우 별도로 변수에 담을 필요가 없다.람다식을 쓰면 함수형 인터페이스로 인스턴스를 만들 수 있으며 코드를 줄 일 수 있다.메서드 매개변수와 리턴 타입, ㅌ변수로 만들어 사용도 가능하다. 람다식의 문법은 어떻게 될까? 데이터 처리부에 제공되는 함수 역활 하는 매개변수를 가진 중괄호 블록이다.{매개변수, ...} -> {처리 내용};  인터페이스가 단 하나의 추상 메소드를 가질 경우 이를 함수형 인터페이스라고 한다.public interface Runnable { void run(); }람다식() -> { ...}; @FunctionalInterface public interface Calculable { void calculate(int x, int y); }람다식(x, y ) -> { ...}@FunctionalInterface 어노테이션을 사용한 이유는 인터페이스가 함수형 인터페이스를 보장하기 위해서다.붙이는 것은 선택사항이지만, 컴파일 과정에서 추상 메소드가 하나인 검사하기 때문에 정확한 함수형 인터페이스를 작성하게 도와주는 역활을 해준다.매 매개변수가 없는 람다식실행문이 하나일 경우 중괄호를 생략 가능하고 두개 이상일 경우는 생략할 수 없다.() -> 실행문; () -> { 실행문; 실행문; } 매개변수가 있는 람다식매개변수를 선언할 때 타입은 생략할 수 있고, 구체적인 타입 대신에 var를 사용할 수 있다.(타입, 매변수, ... ) -> { 실행문; 실행문; }(타입, 매변수, ... ) -> 실행문;(var 매개변수, ...) -> { 실행문; 실행문; }(var 매개변수, ...) -> 실행문;(매개변수, ...) -> { 실행문; 실행문; }(매개변수, ...) ->실행문;매개변수 -> { 실행문; 실행문 };매개변수 -> 실행문; 리턴값이 있는 람다식return 문 하나만 있는 경우에는 중괄호와 함께 return 키워드를 생략가능하다.(매개변수, ...) -> { 실행문; return 값 )(매개변수, ...) -> 값 메소드 참조메소드를 참조하여 매개변수의 정보 및 리턴 타입을 알아내 람다식에서 불필요한 매개변수를 제거한다. (left, right) -> Math.max(left, right);Math.max() 메소드의 매개값은 전달하는 역활만 하기 때문에 다음과 같이 생략이 가능하다.Math :: max;정적 메소드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 메소드 이름을 기술한다.클래스 :: 메서드참조변수 :: 메소드 매개변수의 메소드 참조(a, b) -> {a.instaceMethod(b);}메소드 참조는 a 클래스 이름 뒤에 :: 기호를 붙이고 매소드 이름을 기술한다.작성 방법은 메소드 참조와 동일하지만, a의 인스턴스 메소드가 사용된다는 점이 다르다.클래스 :: instaceMethod Reference이것은 자바다(책)

워밍업클럽과제

xicodey

[인프런 워밍업 클럽 0기] BE 1일차 과제

어노테이션 사용하는 이유어노테이션은 사용 용도로 3가지가 있습니다.1. 컴파일 시 사용하는 정보 전달2. 빌드 툴이 코드를 자동으로 생성할 때 사용하는 정보 전달3.실행 시 특정 기능을 처리할 때 사용하는 정보 전달컴파일 시 사용하는 정보 전달의 대표적인 예는 @Override 어노테이션입니다.컴파일러가 메소드 재정의 검사를하도록 설정합니다. 재정의되지 않았다면 컴파일러는 에러를 발생시킵니다.웹 개발에 많이 사용하는 Spring Framework 또는 Spring Boot는 다양한 종류의 어노테이션을 사용해서 웹 애플리케이션을 설정하는데 사용합니다.나만의 어노테이션은 어떻게 만들 수 있을까? 어노테이션을 정의하는 방법은 인터페이스를 정의하는것과 유사합니다.@interface 뒤에 사용할 어노테이션을 이름을 정의합니다.오노테이션은 속성을 가질 수 있으며, 속성은 타입과 이름으로 구성됩니다. 속성은 기본값 default 키워드로 지정할 수 있습니다.어떤 대상에 설정 정보를 적용할 것인지, 적용대상을 정의 해야 합니다.클래스 명위에 @Target 어노테이션을 붙어 정의 합니다.적용할 수 있는 대상의 종류는 ElememtType 열거 상수로 정의되어 있습니다.TYPE : 클래스, 인터페이스 열거타입ANOTATION_TYPE: 어노테이션FIELD: 필드CONSTERUCTOR: 생성자METHOD: 메서드LOCAL_VARIABLE: 로컬 변수PACKAGE: 패키지@Target의 기본 속성인 value는 배열을 값을 가질 수 있습니다.어노테이션을 정의할 때 한 가지 더 추가해야 할 내용은 @AnnotationNamed 언제까지 유지 할 것인지 지정하는 우지 정책을 정해야합니다.RetentionPolicy 열거 상수

워밍업클럽과제

유원준

[인프런 워밍업 클럽(백엔드, 0기)] - 개별 정리 : 자바 어노테이션

스프링으로 프로젝트를 하면서 자바 어노테이션을 항상 사용해 왔지만 아직 잘 모르는 부분이 많은 것 같아서 1일 차에 대한 과제 제출 기간은 지났지만, 추가적으로 인프런 블로그를 통해 다시 한 번 정리해 보고자 한다. (1) https://twojun-space.tistory.com/178본인 블로그에 내용을 별도로 정리했지만 한 번 더 리마인드해 보고자 한다.   1. 자바 어노테이션(Java Annotation)1-1. 정의(1) 어노테이션은 사전적 정의로는 "주석"이라는 의미를 가지고 있지만 자바에서의 어노테이션은 소스 코드에 추가할 수 있는 일종의 메타 데이터라고 볼 수 있다.(2) 애플리케이션 레벨에서 처리되어야 하는 대상이 아닌, 컴파일, 런타임 시점에 코드를 어떻게 처리해야 할지 알려주기 위한 정보로 볼 수 있겠다.  1-2. 장점(1) 코드 가독성, 컴파일 시점에서의 오류 체크코드 레벨에서 동일하게 작성되기 때문에 코드의 가독성이 좋고, 일부 어노테이션의 경우 컴파일 시점에 문법 에러(아래에서 설명할 @Override, @FunctionalInterface)를 잡아주기도 한다. (2) 중복 코드 제거중복되는 어노테이션의 경우 공통화시킬 수 있고 재사용이 가능하기 때문에 코드의 중복을 줄이고 효율적인 코드 작성이 가능하다. (3) 커스텀 어노테이션 (사용자 정의 어노테이션) 사용 가능직접 용도에 맞게 커스텀 어노테이션을 작성할 수 있다. 프로젝트를 진행함에 따라 각각 필요한 제약사항들을 별도로 정리해서 커스텀 어노테이션 구성이 가능하다.  1-3. 단점(1) 런타임 시 발생할 수 있는 오버헤드만약 런타임 시점에 자바의 리플렉션(Reflection) 등을 사용해서 처리되는 어노테이션이라면 이 부분을 처리하기 위한 별도의 오버헤드가 발생할 수 있다 (성능 상 문제)    2. 어노테이션의 종류살펴볼 어노테이션의 종류로 총 2가지가 있다.(1) 표준 어노테이션(빌트 인 어노테이션)(2) 메타 어노테이션    3. 표준 어노테이션(1) 표준 어노테이션의 경우 자바에서 기본적으로 제공하고 있는 어노테이션이다. 대표적으로 아래와 같이 3가지가 있다. 3-1. @Override(1) 현재 메서드가 부모 타입 클래스 또는 인터페이스의 메서드를 오버라이딩했음을 컴파일러에게 명시하는 역할을 수행한다. 만약 형식에 맞지 않게 오버라이딩되었다면, 컴파일러가 이를 인지하고 오류를 발생시킨다.  3-2. @Deprecated(1) 현재 메서드를 사용하지 않도록 유도한다. 만약 해당 어노테이션이 붙은 메서드를 사용하면 컴파일러가 오류를 발생시킨다.  3-3. @FunctionalInterface(1) 해당 인터페이스가 함수형 인터페이스임을 명시한다. 함수형 인터페이스의 경우 추상 메서드가 반드시 1개 존재해야 한다. 추상 메서드가 존재하지 않거나 2개 이상이라면 컴파일러가 오류를 발생시킨다.    4. 메타 어노테이션(Meta Annotation)(1) 메타 어노테이션이란 다른 어노테이션에서 사용될 수 있는 어노테이션을 의미하며 아래에서 작성할 커스텀 어노테이션(사용자 정의 어노테이션)을 생성할 때 주로 사용되는 어노테이션이다.   4-1. @Target(1) 어노테이션을 적용할 위치를 알려주는 어노테이션이다. (2) 예를 들어 @Target(ElementType.TYPE)의 경우 해당 어노테이션을 다른 어노테이션의 대상으로 사용할 수 있다는 의미이다. (3) 메타 어노테이션을 선언해 줄 때 사용되는 일반적인 방법 중 하나다.@Target({ ElementType.PACKAGE, // 패키지 선언 ElementType.TYPE, // 타입 선언 ElementType.CONSTRUCTOR, // 생성자 선언 ElementType.FIELD, // 멤버 변수 선언 ElementType.METHOD, // 메소드 선언 ElementType.ANNOTATION_TYPE, // 어노테이션 타입 선언 ElementType.LOCAL_VARIABLE, // 지역 변수 선언 ElementType.PARAMETER, // 매개 변수 선언 ElementType.TYPE_PARAMETER, // 매개 변수 타입 선언 ElementType.TYPE_USE // 타입 사용 })  4-2. @Retention(1) @Retention의 경우 어노테이션이 적용되고 유지되는 범위를 설정하기 위해 사용되는 메타 어노테이션이다.@Retention(RetentionPolicy.RUNTIME) // 컴파일 이후에도 JVM에 의해서 참조가 가능하다. @Retention(RetentionPolicy.CLASS) // 컴파일러가 클래스를 참조할 때까지 유효하다. @Retention(RetentionPolicy.SOURCE) // 어노테이션 정보는 컴파일 이후 없어진다.  4-3. @Inherited(1) 해당 어노테이션이 적용된 경우 자식 클래스가 해당 어노테이션을 상속받을 수 있게 된다.(2) 따라서 부모 클래스에 선언된 @Inherited 어노테이션은 하위 클래스에서 자동으로 상속받는다.  4-4. @Repeatable(1) 반복 가능한 어노테이션을 정의할 때 사용될 수 있는 어노테이션이다.    5. 커스텀 어노테이션(Custom Annotation, 사용자 정의 어노테이션) 5-1. 정의, 사용 방법public @interface SimpleAnnotation { }(1) 자바에서는 위와 같이 @interface 키워드를 통해 커스텀 어노테이션을 정의할 수 있다.   5-2. 실제로 커스텀 어노테이션 적용해 보기@Retention(RetentionPolicy.RUNTIME) @Inherited @Target(ElementType.TYPE) @RestController @RequestMapping("/new") public @interface CustomMyAnnotation { String name() default "MemberController"; String value(); } @CustomMyAnnotation(name = "MemberController", value = "MemberController") @RequiredArgsConstructor public class MemberController { private final MemberService memberService; @GetMapping("/list") public List<MemberListResponseDto> getAllMemberList() { List<Member> allMemberList = memberService.findAllMembersList(); return allMemberList.stream() .map(member -> new MemberListResponseDto(member)) .collect(Collectors.toList()); } }  5-3. 자바의 리플렉션(Reflection)(1) 현재 어노테이션을 사용해서 코드의 가독성이 좋아짐은 물론 어노테이션 자체가 되게 많은 일을 대신 해주고 있는 것을 확인해 볼 수 있다. 이 부분은 자바의 리플렉션 기술들이 해결해 주고 있는데 추후에 리플렉션에 관한 내용을 블로그에 다시 한 번 정리해 볼 예정이다.  마지막으로 부족하지만 글을 읽어주신 분들께 감사드립니다!!  

백엔드백엔드인프런워밍업자바어노테이션스프링

konakyeon3

인프런워밍업클럽BE-스터디0기 1주차 회고

인프런워밍업클럽 0기 링크인프런워밍업클럽백엔드 0기를 진행하면서 느낀점과 배운점을 위주로 회고를 작성한다.인프런워밍업클럽BE 공부 범위 이번 인프런0기백엔드 코스는 자바와 스프링 부트로 생애 최초 서버 만들기 강의를 들으면서 스프링부트 웹프레임워크와 MySql, JPA 같은 DBMS 관련 기술과 Amazone EC2 클라우드를 배포까지 공부하는 웹 백엔드 전반을 공부하는 스터디이다.매일 강의와 강의 내용과 관련한 내용으로 미션을 수행하며 매일 그 미션을 수행하고 인증을 해야지 스터디를 완주할 수 있다.미션 내용은 처음 스프링부트를 배우는 사람에게는 조금 어렵지만 미션을 하면서 백엔드와 스프링부트에 대한 이해도를 높일 수 있고, 이미 스프링부트를 다루고 있는 사람 입장에서도 스프링부트의 기본적인 내용을 공부할 수 있도록 구성되어 있다고 한다. (글쓴이는 스프링부트 초심자라서 미션이 조~금 어려웠다!) 자바와 스프링 부트로 생애 최초 서버 만들기-아래는 이번 주에 공부한 내용에 대한 링크와 간단한 정리이다.이번 주 공부한 내용 간단 정리 0일차: OT, 자바의 역사와 자바의 버전별 업데이트 사항 라이브 강의OT를 간단하게 진행하고 최태현 멘토님이 간단하게 자바의 역사에 대해 간단하게 정리하는 강의를 배웠었다.자바의 버전별 업데이트 내역에서 자바7에서 자바8로 업데이트 될 때 람다식,Stream, Optional, 등 이 추가되는 대격변이 일어나서 이 부분을 주목해서 봐야 된다.개인적으로도 람다식이나 Stream은 이번 스터디에서 처음 배우고 유용하게 쓰고 있다.람다식이 탄생한 배경이나 공부하고 싶은 사람이라면 코틀린에서 람다를 다루는 방법 강의를 들어보는 것이 좋을 것 같다.람다식을 왜 쓰는지 람다식이 도입되기 이전의 문법과 차이점, 람다식의 장점에 대해 쉽게 설명하고 있다.1일차: 자바 어노테이션, 어노테이션을 쓰는 이유자바 어노테이션자바와 HTTP 개념 공부주석이 개발자(사람)에게 이 프로그램이 무슨 뜻인지에 대한 메타정보를 준다면,자바의 어노테이션은 자바 컴파일러, 빌더, 런타임에 메타정보를 주는 역할을 수행한다. 스프링 코드를 보다 보면 어노테이션을 통해 각종 설정을 하거나 @setter를 통해 코드를 자동생성하는 등 유용한 용도로 쓰인다. 2일차: GET, POSTGET, POST API 만들기스프링부트로 직접 GET, POST API를 만드는 것을 실습했다. 3일차: 자바 람다식_ 스트림자바 람다식자바 람다식에 대해 배우고 왜 람다식이 탄생했는지 기술의 탄생 배경과 람다식을 어떻게 쓰는지에 대해 학습을 했다. 4일차: PUT, POST API 만들기PUT POST API 5일차: 클린코드클린코드아직은 클린코드가 뭔지 정확히 모르겠다. 도서관에서 클린코드 책을 빌려다가 읽으면서 공부중이다. 느낀 점 & 배운 점 솔직히 이번 스터디 참여하기 전에 개발공부에 대한 재미도 떨어지고 게을러진 상태였다.그러던 중에 이렇게 살다가는 안되겠다는 마음에 신청한 이번 인프런워밍업BE0기 스터디를 신청하고 매일 미션을 수행하니 하루 종일 컴퓨터랑 싸우면서 7~8씩 개발 공부를 하게 되었다.참 신기하다 그렇게 게으르다가 스터디를 신청하니까 이렇게나 사람이 달라지다니 어떻게 다음 주는 공부 할 것인가? 동기식 공부의 문제점현재 나의 공부 방식은일단 강의 듣기 (강의 코드 따라치기)미션 풀기미션 내용 정리하기관련 도서 읽기이런 식으로 진행이 된다.현재 공부 방식에 조금 문제가 있는데 공부가 동기식으로 진행이 된다는 점이다.강의를 공부하다가 막혀서 그 문제를 해결 하기 위해서 시간을 4,5 시간 정도를 쓴다거나그리 중요하지 않은 개념에 꽂혀서 거의 하루 종일(4시간 이상) 쓰는 일이 발생하면지나치게 딜레이가 걸려서 다른 일을 진행하지 못하는 일이 발생했다.1일차 어노테이션을 공부 할 때도 리플랙션 관련 부분이 제대로 이해가 안가서 블로그글을 10개 이상 보고 집에 있는 자바책들(이것이 자바다, 코어 자바9)등 책을 보고 해결이 안되서 다음날에 겨우 리플랙션을 적용해서 어노테이션을 만들어봤다.(근데 친구가 그래서 리플랙션이 뭔데? 라고 말하면 뭐라고 설명을 못하겠다. 아이고)그런데 멘토님(최태현 개발자님)은 신입분은 아직 리플렉션에 대해 몰라도 상관은 없다고 했다.그 외에도 vscode에 스프링 실행 환경 만든다고 몇시간을 쓴다거나, 옵시디언(메모 프로그램) 설정을 하는데만 집중하는 일이 벌어졌다. 조금 중요하지만 그리 중요하지 않은 일에 몰두하다가 진짜 중요한 일(스터디 미션 수행, 개인 프로젝트)를 하는 일에 시간을 거의 쓰지 못하는 일이 자주 벌어졌다. 이런 공부 방식을 고치기 위해서 이 방식을 수행하려고 한다. 1. 먼저 일단 오늘 할 일(공부 외의 일들도 적어야 됨)을 종이(메모앱X)에 적는다.2. 각 할 일 대해 중요도(개발 공부, 긴급한 일이 중요도가 높다) 우선 순위를 정한다.3. 가장 우선순위가 높은 일을 수행한다.4. 잠깐 멈춘다.5. 일 하나를 끝내면 다시 종이 할 일을 적거나 수정한다. 다시 2로 이동한다.  여기서 일이 지나치게 지연이 된다.(과제 마감이 2시간 남았을 떄)다른 우선순위가 높은 일을 수행하면 된다.(빨리 과제를 끝내는데 집중한다  앞으로는 아래처럼 일을 수행하면 된다.  과제 수행이 가장 우선순위가 높은 일이라서 이렇게 작성했다.1. 과제를 제일 먼저 확인하고 일단 강의 안보고 조금만!( 몇 줄 코드 작성하고 초고 작성 하는 정도) 과제를 진행한다.2. 강의를 듣고 다시 과제를 진행하는 식으로 하자.3. 내용 정리는 시간이 남으면 하자.

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

김형준

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

저는 리액트 관련 프로젝트를 하고 있어서 리액트 위주로 강의를 먼저 선행 해서 들었습니다.투두 리스트를 다시 만들어 보면서 리액트의 기본 동작원리를 파악하는데 중점을 두어서 수업을 들었고 다음주중에는 리덕스를 이용한 상태관리 방식을 배워보고자 합니다. 이번 미션에서는 투두리스트를 했기 때문에 크게 어려운점은 없었지만 넷플릭스 관련 프로젝트는 생각보다 어려울것 같아보여서 도움이 될만한 클론 코딩 영상들을 찾아보면서 같이 공부해볼 계획입니다. 이번주가 리액트 공부를 하면서 아주 많은 내용들이 내 머릿속을 휙휙 지나간 느낌입니다. 앞으로 주간 회고를 하기로 했기 때문에 나는 개인적으로 때마다 어떤것을 했는지 메모를 해두어야겠다는 생각을 주기적으로 해서 놓치는 내용들 없이 회고 할 수 있도록 노력해야겠습니다. 강의와 연관된 프로젝트 내용이 제가 해보고 싶었던 것들이 많아서 프로젝트에 애정을 가지고 열심히 참여할 수 있을것 같고 능동적이고 긍정적인 자세로 참여할 것입니다. 정말 나만 잘하면 될것 같다. 앞으로 기본 프로젝트보다 생각한 기능을 더 추가해서 프로젝트 성공률을 150퍼센트 까지 끌어올릴 수 있도록 최선을 다해서 프로젝트에 참여 할 것입니다.

도롱이

[인프런 워밍업 클럽_0기] 1주차, 첫 번째 발자국 #1

1주차, 첫 번째 발자국 1주차는 어려운 내용은 딱히 없었다! 어느정도 기반기가 있었다면 다들 어렵지 않게 해냈었을 것 같다.강의 요약은 강의를 들으면서 노션에 하나하나 요약했기 때문에 노션 링크를 남긴다.https://abalone-copper-ebe.notion.site/d2e9b3e27b3348abbde60994cf627ebd?pvs=4 그래도 너무 노션 링크만 띡 남기면 정 없으니 한 번 쭉 훑어보며 하루하루 대략적으로 어떤 것을 공부했고, 어떤 것들을 알게 되었나 작성해보자. Day2 02/19 서버 개발을 위한 환경 설정 및 네트워크 기초(1~5강 + 52강)첫 날은 프로젝트 소스를 다운받고, 프로젝트의 spring boot 버전을 2.7점대에서 3.0.x로 업데이트를 진행했다.Java, IntelliJ, PostMan, MySQL, Git은 이미 설치가 되어 있어서 따로 영상을 챙겨보진 않았다.52강을 들으면서 느낀 건 안 그래도 저번에 2점대 버전에서 3점대 버전으로 마이그레이션 하려는 시도를 했었었는데, 그때는 spring이라는 프레임워크를 잘 몰랐었던 때고, 3점대가 나온지 얼마 안돼서 정보도 그렇게 많지 않아 장렬히 실패했었던 기억이 있었다. 이번에도 에러가 엄청 날까봐 걱정을 많이 했는데 강의가 잘 정리되어 있어 어렵지 않게 마이그레이션 할 수 있었다. MySQL이 원래 깔려 있어서 비밀번호 입력하는 부분만 빼면 말이다! (MySQL 오류) 본격적으로 강의를 들어가기 전에 Java를 공부하기 전에 알아두면 좋을 것들!이라는 유튜브를 두 개 시청했다. 사실 Java를 공부한지는 꽤 됐는데 JVM의 이점 부분만 대략적으로 알았지 JRE나 JDK은 스킵하고 넘어갔었다. JVM이 제일 중요하다고 알려져있으니까. 이번 강의에서 본격적인 내용을 시작하기 전에 한 번 짚어주는 유튜브가 있어서 별 거 아닌데도 갑자기 많은 생각이 들기 시작했다.나는 왜 Java를 공부하면서 이런 것들도 몰랐을까?나는 Java를 잘 안다고 할 수 있을까?대충 공부함으로써 내가 얻을 수 있는 것들이 뭐였을까?라는 생각들이 스쳐지나 갔다...! 앞으로는 조금 더 꼼꼼한 사람이 돼야 겠다는 목표도 생겼다...! 본격적인 강의 시작에서는 Spring Boot 프로젝트를 실행하는 법과 네트워크, HTTP, API, GET API를 공부했다. 강사님이 최대한 이해하기 쉽게 이것 저것 비유해가면서 얘기해주셔서 이해가 잘 됐었던 거 같다.제일 기억에 남는 것은 함수 파라미터를 변수에서 객체로 변경한 이유가 기억에 남았다. 초보 입장에서는 이런 부분을 놓치는경우가 많고, 생각조차 안 나는 경우가 많은데 이렇게 사소한 것 까지도 짚어주시면서 강의를 진행해주시니 더 꼼꼼하게 코드를 작성할 수 있던 거 같다. 미션https://devhan.tistory.com/318어노테이션에 관한 미션이었다.어노테이션을 단순히 쓰라해서 사용하기만 했는데 어노테이션의 역할이 한 개만 있는 것이 아니라 목적에 따른 다양한 종류의 어노테이션이 있다는 걸 알게되었다!강사님의 코멘트어노테이션이 '마법' 같은 일을 해주기 위해서는 리플렉션이라는 기술이 사용된다.리플렉션은 라이브러리나 프레임워크를 개발할 때 간혹 사용되는 기술로, 코드를 직접적으로 호출하지 않고 코드를 제어하는 기술이다.   Day3 02/20 첫 HTTP API 개발 (6~10강) Day3에서는 GET API 이외에 POST API 개발, User 생성 API 개발, User 조회 API 개발, MySQL 사용에 대해서 공부했다.이번에도 기초적인 부분을 다루었기 때문에 딱히 어려운 것은 없었다. 강의를 따라가면서 느낀 건 API 스펙을 정하는 부분이 아주 좋았다! 다른 강의에서는 API 스펙을 정하는 부분이 없이 그냥 말로만 진행하는 강의도 다수 있었는데 이 강의에서는 미리 API 스펙을 알려주니 스펙을 보고 먼저 개발해본 다음에 강의를 들으면서 고치거나 할 수 있어서 좋았다. 미션https://devhan.tistory.com/319여태 했던 미션 중에 제일 오래 걸린 미션이 아닌가 싶다.. 왜냐면 미션 하는 중에 에러가 발생했기 때문!에러 내용은 @RequestBody 사용 시 해당 DTO 생성자에 파라미터가 한 개만 존재하는 생성자가 있고, 기본 생성자가 없어서 발생하는 에러였다.해결 방법은 @JsonCreator를 기존 생성자 메서드에 붙여주거나, 기본 생성자를 만들어주면 된다.강사님의 코멘트1번 - 본인이라면 DTO 쪽에 사칙 연산 기능을 넣었을 것이다. Service 계층의 코드를 깔끔하게 만들기 위해서는 일부 계산 로직을 DTO 쪽으로 넣는 방법을 사용할 수 있다.2번 - LocalDate를 사용! query parameter가 1개라서 바로 LocalDate를 사용해서 요청을 받을 것 같다.3번 - List를 받아보도록 연습! POST API + List 필드가 있는 DTO를 사용하면 쉽게 해결할 수 있다.  Day4 02/21 기본적인 데이터베이스 사용법 (11~13강)이번 강의에서는 MySQL에서 DDL, DML을 이용해 테이블을 생성 및 삭제, 데이터의 CRUD, Spring Boot에서 MySQL 연동을 해봤다. 이번 강의에서는 에러가 발생했다! MySQL 설정 시 발생하는 에러였는데 간단한 구글링을 통해 빠르게 해결할 수 있었다. (MySQL 연동 오류) 기본적인 SQL 문법을 간단하게 훑어 넘어가는 식으로 강의가 진행됐다. 기초가 없었으면 약간 따라가기 힘들었을 것 같기도 하다!그리고 User 테이블을 생성하고 Java 코드를 메모리에 저장하는 방식에서 데이터베이스(MySQL)에 저장하는 방식으로 변경하도록 코딩했다. 이번 강의에서 람다가 처음으로 나왔는데 람다에 대해서 따로 공부해본 적이 없어서 생소하게만 다가왔다. 이번에 람다를 보면서 OT때 강사님이 얘기했던 모던자바 인 액션 책을 꼭 공부해봐야겠다고 생각했다..! 미션https://devhan.tistory.com/320 익명 클래스와 람다에 대해 알아보는 시간이었다.이번 미션을 하면서 하루라도 빨리 모던자바 인 액션을 읽어야겠다고 생각해 책을 얼른 구매했다. Day5 02/22 데이터베이스를 사용해 만드는 API (14~16강)이번 강의에서는 JdbcTemplate을 사용한 API 개발을 구현하기 위해 기존에 있던 코드들을 변경하는 강의 내용이었다.User 업데이트, 삭제 부분을 코딩하는거였는데 14강에서는 단순히 변경만 했고 15강에서는 예외 상황을 대비해 예외 코드를 추가했다! 이 코드가 제일 신기했는데, 결과가 하나라도 있으면 0을 반환하게하는 코드이다. 그리고 최종적으로 0은 List로 반환된다.결과가 0건이면 빈 List가 반환된다! 미션https://devhan.tistory.com/321이번엔 Fruit 테이블을 생성하고, 요구사항에 맞는 API들을 개발하는 미션이었다.제일 고민이었던 건 판매 여부의 컬럼명과 데이터를 0과 1로 할지 아니면 Enum을 사용해서 String으로 저장할지 고민했는데 상태값이 두 개밖에 없어서 그냥 0과 1을 사용했다.강사님 코멘트select * from table을 사용하고 덧셈을 하는 경우는 데이터베이스에서 서버로 네트워크를 타고 모든 데이터가 넘어온 이후에 서버에서 직접 덧셈 -> 네트워크 대역폭도 많이 잡아 먹고 서버의 연산 비용도 들어감.반면 sum()을 사용하면 합산 결과만 네트워크를 타고 이동하며, 서버는 그 결과를 DTO로 감싸 전송만 하면 되기에 네트워크 및 연산 비용이 훨씬 저렴하다.이런 다양한 방법을 비교할 수 있으려면 1) 일차적으로는 방법들을 알아야하고 (지식의 넓이) 2) 다음으로는 각 방법의 매커니즘을 이해해야 함(지식의 깊이)Day6 02/23 클린코드의 개념과 첫 리팩토링 (17~18강)이번 강의에서는 좋은 코드(Clean Code)의 개념과 기존에 작성했던 코드를 Layered Architecture로 변경하는 작업을 했다.클린 코드는 아직 읽어보지 않았지만 워낙 유명한 책이라 강의에서 만난게 마치 오래전에 알던 친구를 만난 것처럼 재밌었다! 이 기회에 또 읽어봐야할 책이 하나 더 늘었다..!클린 코드에서 가장 기억에 남았던 건 유명 회사 앱이 클린 코드로 코드를 작성하지 않아 점차 망해가는 얘기였다. 그런 얘기가 떠돌아다닐 정도로 코드의 깔끔함은 앞으로의 유지보수에 있어 많은 부분에서 좋은 효과를 줄 수 있다는 걸 배웠다!클린 코드 얘기는 너무 많이 들었지만 어떻게 해야 깔끔하고 좋은 코드인지 가늠하기는 어려웠다. 나는 솔루션 회사에 재직해서 spring boot를 실무에서 쓸 일이 없어서 더욱 가늠이 안 갔던 거 같다. 이번 강의를 통해 조금이나마 클린 코드로 가는 틀을 잡을 수 있어서 좋았다!그리고 또 Layered Architecture란 이름을 알게되었다. Controller, Service, Repository로 구성된 애플리케이션은 여태 수도 없이 보았던 거 같은데 이런 명칭이 있는지는 처음 알았다. 대부분 그냥 MVC 패턴이라하며 갑자기 뭉뚱그려 넘어가서 몰랐었던 거 같다.  미션https://devhan.tistory.com/322작성된 주사위 놀이 코드에 클린 코드를 적용해 리팩토링해보는 미션이었다.제일 고민됐던 것은 Dice를 클래스로 따로 뺄지 말지였다.뭔가 빼면 과하게 빼는 거 같기도 하고,,? Main 메서드에 너무 아무것도 없는 거 같아 뭔가 심심해보이기도 했다.그리고 그 다음으로 고민했던 건 한 걸음 더! 내용이었다.주사위의 범위가 달라지더라도 코드를 적게 수정할 수 있도록 하는 거였는데 사용자에게 주사위 면체의 정보를 입력받을까 하다가 그런 얘기는 나와있지 않아서 그냥 Dice 클래스에 면체와 관련된 필드와 생성자를 추가해주었다..!마지막 1주차 느낀점 정리나는 되게 무언가를 대충 아는 정도였던 거 같다.하루빨리 자바8과 관련된 책을 읽고 지식을 습득해야 할 것 같다. (람다 관련 응용이 아예 안 되는 중이다.)클린 코드의 책도 읽고 클린 코드의 감을 잡아보도록 해야겠다.직장인이라 시간적 여유가 매우 부족해서 아쉬웠다. 저번주 주말에 미리미리 진도를 안빼놨었으면 진작에 수료 기준에 벗어날 뻔했다..! 직장인이니 남들보다 더 미리미리 진도를 나가야겠다. 이번주에만 글쎄 야근을 3일이나 해서 죽는 줄 알았다...생각보다 내가 강의를 잘 따라가고 있는 거 같다. 뭐 실력적으로 잘은 모르겠지만 그래도 꾸준히 놓치지 않고 하려는 모습이 약간은 기특해보일정도! 앞으로도 놓치지말고 꾸준히해서 이번 스터디를 완주했으면 좋겠다!   

백엔드인프런워밍업클럽스터디최태현자바와스프링부트로생애최초서버만들기SpringBootbackend

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - API 레이어 분리 테스트 (Day6)

과제진도표 6일차와 연결됩니다우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍문제 1과제4에서 만들었던 API를 Controller - Service - Repository로 분리하라고 하셨다.하지만 이전에 과제4를 진행하면서 나는 이미 레이어를 분리했지만 강의에 대한 복습 겸, 다시 진행해보기로 했다.step0. DB 생성 및 테이블 생성먼저 데이터베이스부터 다시 만들기로 하였다. 아래와 같이 쿼리를 작성하여 데이터베이스를 생성한다.create database fruit;다음으로 내가 생성한 fruit 데이터베이스에 접속한다.use fruit;그리고 테이블 목록을 조회해본다. 당연히 비어 있을 것이다.show tables;그러면 아래와 같이 테이블 목록들이 비어있는 것을 확인할 수 있을 것이다.그러면 이제 아래와 같이 쿼리를 작성해서 테이블을 만들어보자. 테이블 컬럼들은 기존과 동일하게 적용한다.CREATE TABLE fruit ( id bigint auto_increment, name varchar(20) not null, warehousingDate date not null, price bigint not null, is_sold boolean not null default false, primary key (id) );그리고 테이블이 잘 생성 되었는지 조회를 해서 확인해본다.show tables;step1. DB 설정 정보 적용이제 DB 연결정보를 Spring Boot 프로젝트와 연결해보자.프로젝트의 src/main/resources의 경로에 있는 application.properties를 application.yml로 변경하고 설정정보를 아래와 같이 작성한다.spring: datasource: url: "jdbc:mysql://localhost/fruit" username: "root" password: "" driver-class-name: com.mysql.cj.jdbc.Driver⚠ 주의username과 password는 본인에 따라 달리 작성한다.또한 굳이 application.yml 로 확장자 변경을 안하고 properties 확장자로 이용해도 무관하다.step2. 기존 컨트롤러 클래스 파일 가져오기나는 이미 과제4에서 레이어를 분리해두었다. 하지만 이번 과제의 취지에 맞게 기존에 파일들을 가져오기는 하지만 비즈니스 로직들을 컨트롤럴 클래스에 포함된 파일들로 가져오기로 하였다.Fruit.javapackage me.sungbin.entity.fruit; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : Fruit * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class Fruit { private long id; private String name; private LocalDate warehousingDate; private long price; private boolean isSold; public Fruit() { } public Fruit(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public Fruit(String name, LocalDate warehousingDate, long price, boolean isSold) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public boolean isSold() { return isSold; } }SaveFruitInfoRequestInfo.javapackage me.sungbin.dto.fruit.request; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import me.sungbin.entity.fruit.Fruit; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.request * @fileName : SaveFruitInfoRequestDto * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class SaveFruitInfoRequestDto { @NotBlank(message = "과일 이름이 공란일 수 없습니다.") @NotNull(message = "과일 이름이 null일 수는 없습니다.") private String name; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate warehousingDate; @Min(value = 0, message = "가격이 음수가 될 수는 없습니다.") private long price; public SaveFruitInfoRequestDto(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public Fruit toEntity() { return new Fruit(name, warehousingDate, price); } }UpdateFruitRequestDto.javapackage me.sungbin.dto.fruit.request; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.request * @fileName : UpdateFruitRequestDto * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class UpdateFruitRequestDto { private long id; public UpdateFruitRequestDto() { } public UpdateFruitRequestDto(long id) { this.id = id; } public long getId() { return id; } }GetFruitResponseDto.javapackage me.sungbin.dto.fruit.response; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.response * @fileName : GetFruitResponseDto * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class GetFruitResponseDto { private long salesAmount; private long notSalesAmount; public GetFruitResponseDto(long salesAmount, long notSalesAmount) { this.salesAmount = salesAmount; this.notSalesAmount = notSalesAmount; } public long getSalesAmount() { return salesAmount; } public long getNotSalesAmount() { return notSalesAmount; } }FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final JdbcTemplate jdbcTemplate; public FruitController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @PostMapping public void saveFruitInfo(@RequestBody @Valid Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } @PutMapping public void updateFruitInfo(@RequestBody Fruit fruit) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; boolean isNotExistsFruitInfo = jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, fruit.getId()).isEmpty(); if (isNotExistsFruitInfo) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { long salesAmount = 0; long notSalesAmount = 0; String sql = "SELECT SUM(price) as total, is_sold FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); if (aggregatedData.containsKey(true)) { salesAmount = aggregatedData.get(true); } if (aggregatedData.containsKey(false)) { notSalesAmount = aggregatedData.get(false); } if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } return new GetFruitResponseDto(salesAmount, notSalesAmount); } }step3. 레이어 분리이제 레이어를 분리해보겠다. 일단 현재 컨트롤러에는 HTTP 통신하는 부분과 DB처리 관련 로직, 예외로직이 엄청 많다. 이것은 클린코드의 단일책임원칙에 위배가 되므로 서비스 레이어를 만들어 분리해보도록 하자.FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import java.util.HashMap; import java.util.Map; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class FruitService { private final JdbcTemplate jdbcTemplate; public FruitService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(Fruit fruit) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; boolean isNotExistsFruitInfo = jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, fruit.getId()).isEmpty(); if (isNotExistsFruitInfo) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } public GetFruitResponseDto getFruitInfo(String name) { long salesAmount = 0; long notSalesAmount = 0; String sql = "SELECT SUM(price) as total, is_sold FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); if (aggregatedData.containsKey(true)) { salesAmount = aggregatedData.get(true); } if (aggregatedData.containsKey(false)) { notSalesAmount = aggregatedData.get(false); } if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } return new GetFruitResponseDto(salesAmount, notSalesAmount); } }FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.service.fruit.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(JdbcTemplate jdbcTemplate) { this.fruitService = new FruitService(jdbcTemplate); } @PostMapping public void saveFruitInfo(@RequestBody @Valid Fruit fruit) { this.fruitService.saveFruitInfo(fruit); } @PutMapping public void updateFruitInfo(@RequestBody Fruit fruit) { this.fruitService.updateFruitInfo(fruit); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } }좀 더 컨트롤러 클래스가 깔끔해진 것을 볼 수 있다. 하지만 서비스 클래스에 DB 관련 처리과 더해 예외로직들이 있는 것은 클린코드에 위배되는 것 같다. 따라서 FruitService 코드도 레파지토리 레이어를 만들어서 분리해보도록 하자. 그리고 각각 리팩토링 작업도 거쳤다. 아래의 코드를 보자. FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.jdbc.core.JdbcTemplate; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class FruitService { private final FruitRepository fruitRepository; public FruitService(JdbcTemplate jdbcTemplate) { this.fruitRepository = new FruitRepository(jdbcTemplate); } public void saveFruitInfo(Fruit fruit) { this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(Fruit fruit) { validate(fruit.getId()); this.fruitRepository.updateFruitInfo(fruit); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } }FruitRepository.javapackage me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(Fruit fruit) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }하지만 뭔가 이상한 점을 발견할 수 있다. 현재 DB를 이용하는 것은 레파지토리 레이어이다. 즉, JdbcTemplate을 이용하는 것은 레파지토리 레이어뿐인 것이다. 하지만 코드를 보면 알 수 있듯이 컨트롤러, 서비스 레이어에도 전부 JdbcTemplate을 매개변수로 넣고 있다. 이런 것을 어떻게 해결할까? 바로 서비스와 레파지토리 레이어에 빈을 주입할 수 있는 어노테이션을 붙여준다. 이 부분은 오늘 강의시간에도 다뤘으니 적용해보자. FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.service.fruit.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping public void saveFruitInfo(@RequestBody @Valid Fruit fruit) { this.fruitService.saveFruitInfo(fruit); } @PutMapping public void updateFruitInfo(@RequestBody Fruit fruit) { this.fruitService.updateFruitInfo(fruit); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } }FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruitInfo(Fruit fruit) { this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(Fruit fruit) { validate(fruit.getId()); this.fruitRepository.updateFruitInfo(fruit); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } } FruitRepository.javapackage me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(Fruit fruit) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }각각 레파지토리 레이어와 서비스 레이어에 @Repository, @Service 어노테이션을 붙여주었고 이 어노테이션들은 @Component 어노테이션들을 붙어 있어서 빈을 주입 받을 수 있다. 그래서 각각 생성자 주입 방식으로 주입을 받았다.step4. 엔티티 대신에 DTO로!검색을 해보면 request나 response로 받아주는 것을 DTO로 받는게 좋다고 했다. 그이유는 아래와 같다. 📚 엔티티 대신에 DTO를 사용하는 이유?DTO(Data Transfer Object)를 엔티티 대신 사용하는 이유는 여러 가지가 있다. 첫째, DTO를 사용하면 애플리케이션의 프레젠테이션 계층과 데이터 접근 계층 사이의 의존성을 줄일 수 있어, 애플리케이션의 확장성과 유지보수성이 향상된다. 각 계층이 서로에 대해 덜 알고 있기 때문에, 변경 사항이 한 계층에만 국한되어 다른 계층에는 영향을 주지 않는 경우가 많다.둘째, DTO를 사용하면 클라이언트에 전송되는 데이터의 양과 형식을 조정할 수 있어, 네트워크를 통한 데이터 전송량을 최적화하고, 클라이언트가 필요로 하는 데이터 형식을 맞춤 제공할 수 있다. 이는 특히 모바일 애플리케이션 개발이나 대역폭이 제한된 환경에서 중요하다.셋째, DTO를 사용하면 엔티티의 모든 정보를 클라이언트에 노출하지 않아도 된다. 이는 보안 측면에서 매우 중요한데, 예를 들어 사용자 엔티티에는 비밀번호와 같은 민감한 정보가 포함될 수 있으나, 이를 DTO를 통해 필터링하고 클라이언트에 필요한 정보만 전달할 수 있다.넷째, 엔티티의 경우 JPA와 같은 ORM 기술을 사용할 때 지연 로딩(Lazy Loading) 등의 문제로 인해 직렬화에 어려움이 있을 수 있습니다. DTO를 사용하면 이러한 문제를 피하고, 데이터 전송을 위해 최적화된 객체를 생성할 수 있습니다.이러한 이유로 한번 DTO로 변경해보자. 현재 DTO는 과제4에서 사용했던 DTO를 이용할 것이다. 그리고 이 DTO의 코드내용은 step2에서 보여줬으므로 이것을 이용해보자.FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.service.fruit.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping public void saveFruitInfo(@RequestBody @Valid SaveFruitInfoRequestDto requestDto) { this.fruitService.saveFruitInfo(requestDto); } @PutMapping public void updateFruitInfo(@RequestBody UpdateFruitRequestDto requestDto) { this.fruitService.updateFruitInfo(requestDto); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } }FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) { Fruit fruit = requestDto.toEntity(); this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(UpdateFruitRequestDto requestDto) { validate(requestDto.getId()); this.fruitRepository.updateFruitInfo(requestDto.getId()); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } }FruitRepository.javapackage me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(long id) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }step5. postman 테스트이제 이렇게 리팩토링한 것을 postman을 이용해서 테스트해보자.현재 fruit 테이블은 아래와 같이 비어있다.과일 생성수정위의 생성 테스트가 잘 되었으니, 몇개의 데이터를 아래와 같이 만들었다.이제 2000원짜리 오렌지가 팔린 테스트를 해보겠다.조회 테스트이제 조회 테스트를 해보자. 오렌지가 팔린 금액과 안 팔린 금액을 조회해보자. step6. 테스트 코드이제 테스트 코드로 다시 한번 검증해보자. FruitControllerTest.javapackage me.sungbin.controller.fruit; import com.fasterxml.jackson.databind.ObjectMapper; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitControllerTest * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @SpringBootTest @AutoConfigureMockMvc class FruitControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test @DisplayName("문제 1번 통합 테스트 - 실패 (가격이 음수거나 과일 이름이 공란)") void question1_test_fail_caused_by_price_is_minus_or_fruit_name_is_empty() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("", LocalDate.of(2024, 1, 1), -1000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("문제 1번 통합 테스트 - 성공") void question1_test_success() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("파인애플", LocalDate.of(2024, 2, 2), 20000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 2번 통합 테스트 - 성공") void question2_test_success() throws Exception { UpdateFruitRequestDto requestDto = new UpdateFruitRequestDto(1); this.mockMvc.perform(put("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 3번 통합 테스트 - 성공") void question3_test_success() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/stat") .param("name", "사과")) .andDo(print()) .andExpect(status().isOk()); } }문제2문제2는 FruitRepository를 FruitMemoryRepository 와 FruitMysqlRepository로 나누고 @Primary 어노테이션을 이용하여 두 Repository를 번갈아가며 동작시키는 것을 구현하시라고 하셨다. step1. FruitRepository 코드를 FruitMysqlRepository로 변경package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitMysqlRepository { private final JdbcTemplate jdbcTemplate; public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(long id) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }step2. FruitRepository 인터페이스 생성package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public interface FruitRepository { void saveFruitInfo(Fruit fruit); // 과일 생성 void updateFruitInfo(long id); // 과일 정보 업데이트 GetFruitResponseDto getFruitInfo(String name); // 과일 조회 boolean isNotExistsFruitInfo(long id); } step3. FruitMemoryRepository 생성 및 로직 개발step3-1. Fruit 클래스에 다중 생성자 추가(메모리 용 때문에)package me.sungbin.entity.fruit; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : Fruit * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class Fruit { private long id; private String name; private LocalDate warehousingDate; private long price; private boolean isSold; public Fruit() { } public Fruit(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public Fruit(String name, LocalDate warehousingDate, long price, boolean isSold) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public Fruit(long id, String name, LocalDate warehousingDate, long price, boolean isSold) { this.id = id; this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public boolean isSold() { return isSold; } }step3-2. FruitMemoryRepository 생성 및 로직 추가package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Repository; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitMemoryRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Primary @Repository public class FruitMemoryRepository implements FruitRepository { private final List<Fruit> fruits = new ArrayList<>(); private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); @Override public void saveFruitInfo(Fruit fruit) { log.info("[FruitMemoryRepository] - saveFruitInfo"); fruits.add(fruit); System.out.println(fruits); } @Override public void updateFruitInfo(long id) { log.info("[FruitMemoryRepository] - updateFruitInfo"); for (int i = 0; i < fruits.size(); i++) { Fruit fruit = fruits.get(i); if (fruit.getId() == id) { // Assuming Fruit class has an appropriate constructor to handle this case Fruit updatedFruit = new Fruit(fruit.getId(), fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice(), true); fruits.set(i, updatedFruit); break; } } System.out.println(fruits); } @Override public GetFruitResponseDto getFruitInfo(String name) { log.info("[FruitMemoryRepository] - getFruitInfo"); List<Fruit> filteredFruits = fruits.stream() .filter(fruit -> fruit.getName().equals(name)) .toList(); long salesAmount = filteredFruits.stream() .filter(Fruit::isSold) .mapToLong(Fruit::getPrice) .sum(); long notSalesAmount = filteredFruits.stream() .filter(fruit -> !fruit.isSold()) .mapToLong(Fruit::getPrice) .sum(); System.out.println(fruits); return new GetFruitResponseDto(salesAmount, notSalesAmount); } @Override public boolean isNotExistsFruitInfo(long id) { return fruits.stream().noneMatch(fruit -> fruit.getId() == id); } }step4. FruitMysqlRepository 수정package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitMysqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void saveFruitInfo(Fruit fruit) { log.info("[FruitMysqlRepository] - saveFruitInfo"); String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } @Override public void updateFruitInfo(long id) { log.info("[FruitMysqlRepository] - updateFruitInfo"); String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } @Override public GetFruitResponseDto getFruitInfo(String name) { log.info("[FruitMysqlRepository] - getFruitInfo"); String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } @Override public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }step5. FruitService 수정package me.sungbin.service.fruit; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitMysqlRepository; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.stereotype.Service; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) { Fruit fruit = requestDto.toEntity(); this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(UpdateFruitRequestDto requestDto) { validate(requestDto.getId()); this.fruitRepository.updateFruitInfo(requestDto.getId()); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } }step6. postman 테스트 현재 @Primary 어노테이션을 FruitMemoryRepository로 붙여두고 테스트를 해보았다.생성 (메모리) 수정 (메모리)조회 (메모리)이제 FruitMysqlRepository로 이용해보자! FruitMemoryRepository의 @Primary 어노테이션을 지워주고 FruitMysqlRepository에 붙여주자!package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Primary @Repository public class FruitMysqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void saveFruitInfo(Fruit fruit) { log.info("[FruitMysqlRepository] - saveFruitInfo"); String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } @Override public void updateFruitInfo(long id) { log.info("[FruitMysqlRepository] - updateFruitInfo"); String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } @Override public GetFruitResponseDto getFruitInfo(String name) { log.info("[FruitMysqlRepository] - getFruitInfo"); String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } @Override public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }  생성 (Mysql)수정 (MySQL)조회 (Mysql)회고오늘의 강의 핵심은 의존성 주입과 제어의 역전이었다. 나는 기존에 이런 개념들이 뭔지는 대강 알고는 있었지만 확실히 강의와 이렇게 실습함으로 뭔가 체득이 되었다. 아직 많이 부족한 부분이 있을테니 나 따로 더 연습을 해봐야겠다. 

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

망고123

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

인프런 워밍업 클럽 스터디(BE) 0기 / 1주차 발자국📕일주일 간의 학습 내용에 대한 간단한 회고커리큘럼에 따라 매일마다 최소 20분에서 1시간 사이의 짧은 강의를 수강했지만, 매일 최소 3시간 복습하며 각 강의마다 전달하고자 하는 지식을 체득하기 위해 노력하는 중입니다. 최태현 멘토님의 열정적인 강의와 적극적인 피드백에 많은 동기부여가 됐습니다. 남은 커리큘럼도 최선을 다하고 진정성 있게 참여하겠습니다. 🐜일주일 동안 스스로 칭찬하고 싶은 점 : 신입 채용 서류 광탈에도 최선을 다하는 나에게 칭찬 ^^아쉬웠던 점 : 순간 놓치는 1초 2초가 어쩌면 평생 놓친 시간이 될 수도 있음에도 불구하고, 병든 닭마냥 꾸벅꾸벅 졸고 있음보완하고 싶은 점 : 깃허브 프로젝트 칸반과 위키에 학습 내용 정리 방식을 좀 더 직관적으로 보완할 필요성이 있음다음주에는 어떤 식으로 학습하겠다는 스스로의 목표 : 에빙 하우스의 망각 곡선에 따라 효율적인 학습을 실천1. 소개지식공유자 최태현님이 운영하는 인프런 워밍업 클럽 스터디의 학습 내용 정리하는 공간입니다.인프런 워밍업 스터디 클럽 0기🍃 는[자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! 서버 개발 올인원 패키지] 에서 자세한 학습 내용과 인프런 워밍업 클럽 스터디 진행에 대한 전반적인 소개는 인프런에서 참고하기 바랍니다. Wiki 에서는 매일마다 학습해야 되는 내용들을 정리하고 데일리 과제를 정리하는 공간으로 활용하여 나중에 시작하게 될 프로젝트 참고 자료로 활용합니다.모든 학습 내용은 깃허브 리포지토리에서 다음과 같이 확인할 수 있습니다. 🍃깃허브로 이동하기위키 : 미션 및 미니 프로젝트와 정리하는 공간깃허브 프로젝트 : 각 장마다 학습한 내용을 정리하는 공간깃허브 전략feature 브랜치와 main 브랜치로 구성하여 강의의 각 장마다 이슈 발행깃허브 프로젝트의 칸반과 이슈 번호를 연동하고 커밋에 학습 내용을 요약 정리이슈에서 각 장에 대한 요약 정리 확인이 가능2. 커리큘럼1주차 학습 내용 정리는 커밋과 깃허브 프로젝트에서 자세한 내용을 확인할 수 있습니다. 🍃깃허브로 이동하기Day 2 | 2.19(월) | 서버 개발을 위한 환경 설정 및 네트워크 기초강의 (1~5강) | 미션 ODay 3 | 2.20(화) | 첫 HTTP API 개발강의 (6~9강) | 미션 ODay 4 | 2.21(수) | 기본적인 데이터베이스 사용법강의 (10~13강) | 미션 ODay 5 | 2.22(목) | 데이터베이스를 사용해 만드는 API강의 (14~16강) | 미션 ODay 6 | 2.23(금) | 클린코드의 개념과 첫 리팩토링강의 (17~18강) | 미션 O3. 학습 내용 요약학습 내용 요약은 해당 강의에서 핵심 내용을 최대한 간단하게 정리하고 핵심 키워드를 작성합니다. 하지만 개인적으로 특정 장에서 자세한 설명이 필요하다고 느껴진다면, 제목에 🌈 표시와 함께 자세한 설명과 함께 해당 장을 정리합니다. 각 장마다 자세한 학습 내용 요약은 🍃깃허브로 이동하기 에서 확인할 수 있습니다.3.1 서버 개발을 위한 환경 설정 및 네트워크 기초강의 (1~5강)1강 스프링부트 프로젝트 생성 및 최태현 멘토님의 강의 첨부 자료에 대해 소개https://start.spring.io/, 강의 자료2강 어노테이션, 서버, 요청에 대한 이해와 스프링부트 초기화를 담당하는 @SpringBootApplication 학습어노테이션, 서버, 요청, @SpringBootApplication3강 이세계와 실세계의 사례로 네트워크의 전반적인 흐름에 대한 이해IP, Domain, DNS, Port, HTTP Header & Body, OSI 7 Layer, TCP/IP Layer, 3 Way Handshake🌈 4강 HTTP와 API 구성과 동작 원리에 대한 이해4.1 HTTP 메서드클라이언트와 서버 사이에 이뤄지는 요청과 응답 데이터를 전송하는 방식쉽게 말하면, 서버에 주어진 리소스에 수행하길 원하는 행동서버가 수행해야 할 동작을 지정하는 요청을 보내는 방법HTTP 메소드의 종류는 총 9가지가 있다. 이 중 주로 쓰이는 메소드는 5가지로 보면 된다.4.2 HTTP 주요 메소드GET : 리소스 조회(데이터 요청, 쿼리)POST: 요청 데이터 처리, 주로 등록에 사용(데이터 저장, 바디)PUT : 리소스를 대체(덮어쓰기), 해당 리소스가 없으면 생성(데이터 수정, 바디)PATCH : 리소스 부분 변경 (PUT이 전체 변경, PATCH는 일부 변경)DELETE : 리소스 삭제(데이터 삭제, 쿼리)4.3 쿼리와 바디는 정보를 보내는 2가지 방법GET 에서는 쿼리를 사용POST 에서는 바디를 사용4.3.1 GET 예제GET/portion?color=red&color=2 Host:spring.com:3000GET : HTTP MethodHost:spring.com:3000 : HTTP 요청을 받는 컴퓨터와 프로그램 정보를 의미/portion : HTTP 요청을 받는 컴퓨터에게 원하는 자원(Path)? : 구분 기호color=red : 자원의 세부 조건(색 : 빨강)& : 구분 기호(다른 세부 조건과 구분하기 위한 기호)color=2 : 자원의 세부 조건(개수 2개)color=red&color=2 : 쿼리에 해당4.3.2 POST 예제POST /oak/leather Host: spring.com:3000POST : 요청을 받는 컴퓨터에게 저장Host: spring.com:3000 : HTTP 요청을 받는 컴퓨터와 프로그램 정보를 의미/oak/leather : HTTP 요청을 받는 컴퓨터에게 원하는 자원(Path)자원정보 : 다음 시간에 설명(바디에 해당)POST /oak/leather의 의미인 행위와 자원은 HTTP 요청을 보내기 전에 약속해야 합니다.4.4 APIAPI(Application Programming Interface)는 정해진 약속을 하여, 특정 기능을 수행하는 것헤더와 바디 사이에 한 칸을 띄우고 작성합니다. 4.5 URLURL(Uniform Resource Locator)는 흔히 브라우저 주소 칸에 작성하는 주소를 의미합니다.http : 사용하고 있는 프로토콜:// : 구분 기호spring.com:3000 : 도메인이름:포트(도메인 이름은 IP로 대체 가능)/portion : 자원의 경로(Path)? : 구분 기호color=red&count=2 : 쿼리(추가 정보)요약 정리(웹을 통한) 컴퓨터 간의 통신은 HTTP 라는 표준화된 방식이 존재HTTP의 요청은 HTTP Method (GET, POST)와 Path(/portion)가 핵심이다.요청에서 데이터를 전달하기 위한 2가지 방법은 쿼리와 바디이다.HTTP 응답은 상태 코드가 핵심이다.클라이언트와 서버는 HTTP를 주고 받으며 기능을 동작하는데 이때 정해진 규칙을 API라고 한다.🌈 5강 GET API 개발하고 테스트하기5.1 덧셈 API이번 시간에는 덧셈 API 를 직접 생성합니다. 두 수의 합 결과를 반환합니다.@RestController : 주어진 Class를 Controller로 등록한다. Controller는 API 입구를 의미한다.@GetMapping("/add) : 아래 함수를 HTTP Method가 GET 이고 HTTP path가 /add인 API로 지정한다.@RequestParam : 주어진 쿼리 함수 파라미터에 넣는다.5.2 @RequestParamHTTP 요청에서 파라미터를 추출하는 데 사용되는 어노테이션.url의 number1=100&number2=200에 요청 파라미터에 해당요청 파라미터가 많은 경우에는@RequestParam을 사용 하기 보다는 예제처럼 CalculatorAddRequest 객체를 생성하고 getter 메서드로 요청 파라미터를 핸들링. 이를 dto 라고 한다. (DTO(Data Transfer Object, 데이터 전송 객체)란 프로세스 간에 데이터를 전달하는 객체를 의미)5.3 @RestController@RestController가 작성된 해당 클래스의 모든 메서드는 HTTP 요청에 응답하는 컨트롤러의 역할을 수행@Controller에 @ResponseBody가 추가@RestController는 JSON 형태로 객체 데이터를 반환스프링 MVC에서 컨트롤러 메서드는 일반적으로 뷰에 데이터를 전달하고 해당 뷰를 렌더링하는 데 사용됩니다. 그러나 @ResponseBody를 사용하면 컨트롤러 메서드가 반환하는 객체가 HTTP 응답 본문에 직접 포함되어 클라이언트로 전송됩니다. 이것은 주로 RESTful 웹 서비스를 구축할 때 JSON이나 XML과 같은 데이터 형식으로 데이터를 반환할 때 사용됩니다.5.4 포스트맨 결과 화면컨트롤러package com.group.libarayapp.controller.calculator; ​ import com.group.libarayapp.dto.calculator.request.CalculatorAddRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; ​ @RestController public class CalculatorController { ​    /**     * http://localhost:8080/add?number1=100&number2=200     * @apiNote @RequestParam 이란, HTTP 요청에서 파라미터를 추출하는 데 사용되는 어노테이션     * @param number1 요청 파라미터     * @param number2 요청 파라미터     */    @GetMapping("/add") // GET /add    public int addTwoNumbers(            @RequestParam int number1,            @RequestParam int number2             ) {        return number1 + number2;   } ​    /**     * http://localhost:8080/add-request?number1=100&number2=200     * @param request @RequestParam 을 제거하고 객체를 요청 파라미터로 보낼 수 있다.     * @return 객체의 getter 메서드로 접근하여 두 필드의 값을 더한다.     */    @GetMapping("/add-request") // GET /add    public int addTwoNumbersRequest(CalculatorAddRequest request) {        return request.getNumber1() + request.getNumber2();   } }요청 dtopackage com.group.libarayapp.dto.calculator.request; ​ public class CalculatorAddRequest { ​    private final int number1;    private final int number2; ​    public CalculatorAddRequest(int number1, int number2) {        this.number1 = number1;        this.number2 = number2;   } ​    public int getNumber1() {        return number1;   } ​    public int getNumber2() {        return number2;   } }3.2 첫 HTTP API 개발강의 (6~9강)🌈 6강 POST API 개발하고 테스트하기6.1 Q. POST에서는 데이터를 어떻게 받을까? A. HTTP Body 를 이용합니다. 이때 사용되는 문법이 있는데, JSON 이라고 합니다. (JavaScript Object Notation, JSON)6.2 JSON객체 표기법,즉 무언가를 표현하기 위한 형식이다!중괄호 안에 작성한다 {}키(key)와 값(value)으로 구성한다.Java로 비유하자면, Map<Object, Object> 와 비슷한 느낌에 해당한다. Object 타입의 경우에도 다양한 타입이 가능한 것 처럼, JSON 또한 다양한 타입을 지원하기 때문이다. JSON 안에 JSON 또한 가능하다.{    "name" : "최태현",    "age" : 99    “house”: {   “address”: “대한민국 서울”,   “hasDoor”: true   } }6.3 실습 코드@PostMapping("/multiply") // POST /multiply public int multiplyTwoNumbers(@RequestBody CalculatorMultiplyRequest request) { return request.getNumber1() * request.getNumber2(); }@RequestBody 가 적용된 위의 코드를 포스트맨에서 실행하는 과정은 다음과 같다.포스트맨에서 스프링부트로 요청한다.스프링부트에서 곱셈을 수행하고 결과를 포스트맨으로 응답한다.@RequestBody는 HTTP Body 에 있는 데이터를 자바로 매핑을 도와주는 어노테이션이다.7강. 유저 생성 API 개발유저 도메인을 생성하고 컨트롤러를 구현한다.유저 컨트롤러 구현하기유저 도메인 구현하기유저 dto 구현하기8강. 유저 조회 API 개발과 테스트유저 조회 API 스펙은 다음과 같다.HTTP Method : GET HTTP Path : /user 쿼리 : 없음 반환 결과[{ "id" : Long, "name" : String (null 불가능), "age" : Integer }, ... ]결과 반환이 리스트 안의 객체들이 저장된 JSON 형식이다. 그 이유는 Controller에서 getter 메서드가 있는 객체를 반환하면JSON이 된다. 새롭게 id 가 추가되는데, id는 유저별로 겹치지 않는 식별 번호다. List에 있는 유저의 순서를 id로 해주자!9강. Section1 정리. 다음으로!지금까지 구현한 문제점은 종료했다가 다시 재시작하면 모든 정보가 메모리에서만 유지되기 때문에 재시작하는 경우 모든 정보가 서버 종료와 함께 초기화되고 사라지게 된다. 이 문제를 해결하기 위해 데이터베이스 채택을 고려하자.3.3 기본적인 데이터베이스 사용법강의 (10~13강)10강. Database와 MySQL인텔리제이에서 mySQL 접속하기🌈 11강. MySQL에서 테이블 만들기MySQL 에 [ root / mysql ] 으로 로그인하여 library데이터베이스(스키마) 에서 테이블을 생성하여 학습을 진행한다.11.1 MySQL 타입 살펴보기 - 정수타입tinyint : 1바이트int : 4바이트bigint 8바이트11.2 MySQL 타입 살펴보기 - 실수타입double : 8바이트decimal(A, B) : 소수점을 B개 가지고 있는 전체 A 자릿수 실수ex) decimal(4, 2) : 12.2311.3 MySQL 타입 살펴보기 - 문자열타입char(A) : A 글자가 들어갈 수 있는 문자열 (고정된 크기)varchar(A) : 최대 A 글자가 들어갈 수 있는 문자열 (가변 크기)11.4 MySQL 타입 살펴보기 - 날짜, 시간타입date : 날짜, yyyy-MM-ddtime : 시간, HH:mm:ssdatetime : 날짜와 시간을 합친 타입, yyyy-MM-dd HH:mm:ss11.5 실습 코드create database library default character set utf8; ​ use library; ​ show databases; ​ create table fruit (   id           bigint auto_increment,   name         varchar(20),   price        int,   stocked_date date,    primary key (id) );오늘 학습한 내용은 모두 DDL(Data Definition Langauge) 라고 한다. DDL은 데이터베이스 스키마를 정의하는 일련의 SQL 명령으로 생성, 수정, 삭제 등을 수행하고 CREATE, ALTER, DROP, TRUNCATE 가 있다.🌈 12강. 테이블의 데이터를 조작하기fruit 테이블에 CRUD 수행을 한다.C(Create)R(Read)U(Update)D(Delete) 의 약자를 의미한다.12.1 데이터 추가하는 쿼리문insert into [테이블이름](필드1이름, 필드2이름) values(값1, 값2, ...)12.2 fruit 테이블에 데이터 추가id 의 경우는 auto_increment 로 설정했기 때문에 데이터를 추가하지 않아도 자동적으로 추가됩니다.insert into fruit (name, price, stocked_date) values ('사과', 1000, '2023-01-01');12.3 fruit 모두 조회하기select * from fruit12.4 fruit의 이름(name)과 가격(price) 조회하기select name, price from fruit12.5 데이터 조회에 조건을 설정하기select * from [테이블 이름] where [조건];12.6 fruit에서 이름이 사과이거나 가격이 1000인 모든 값을 조회하기select * from fruit where name= '사과' or price = 1000;12.7 fruit에서 이름이 사과 또는 수박인 과일의 모든 값을 조회하기select * from fruit where name in ('사과', '수박');12.8 fruit에서 이름이 사과가 아닌 과일 조회하기select * from fruit where name not in ('사과');12.9 데이터 업데이트하기update [테이블 이름] set [필드1이름=값; 필드2이름=값, ...] where [조건];12.10 fruit 에서 이름이 사과이면 가격을 1500으로 수정하기update fruit set price = 1500 where name = '사과';주의 사항은 만약 [조건]을 붙이지 않으면 모든 데이터가 수정된다는 것을 기억하세요.12.11 데이터 삭제하기delete from [테이블 이름] where [조건]12.12 fruit 테이블에서 이름이 사과이면 삭제하기select from fruit where name = '사과';🌈13강. Spring에서 Database 사용하기오늘의 핵심은 JdbcTemplate 을 사용하여 데이터베이스와 연동을 수행했다.JdbcTemplate 으로 유저 정보 저장하기데이터베이스에 저장된 정보 조회하기3.4 데이터베이스를 사용해 만드는 API강의 (14~16강)14강. 유저 업데이트 API, 삭제 API 개발과 테스트JdbcTemplate 를 활용하여 유저를 등록하고 삭제할 수 있도록 유저 컨트롤러를 수정한다.유저 등록 구현하기유저 삭제 구현하기15강. 유저 업데이트 API, 삭제 API 예외 처리 하기API 에서 예외를 던지면 어떻게 되는지 간단한 API 를 생성해서 테스트를 수행한다.포스트맨에서 500 Internal Server Error 발생 확인하기API에서 데이터 존재 여부를 확인하여 예외를 던지기16강. Section2 정리. 다음으로!한 개의 컨트롤러에서 많은 수행이 발생되는 문제를 해결하기 위해 고민해야 한다. 다음 장에서는 고민을 해결하는 시간을 갖는다.3.5 클린코드의 개념과 첫 리팩토링강의 (17~18강)🌈17강. 좋은 코드(Clean Code)는 왜 중요한가?!이번 섹션 [17강 ~22강] 에서 학습 내용의 목표는 다음과 같다.좋은 코드가 왜 중요한지 이해하고,원래 있던 Controller 코드를 보다 좋은 코드로 리팩토링한다.스프링 컨테이너와 스프링 빈이 무엇인지 이해한다.스프링 컨테이너가 왜 필요한지,좋은 코드와 어떻게 연관이 있는지 이해한다.스프링 빈을 다루는 여러 방법을 이해한다.코드를 읽는 것은 필수적이고 피할 수 없다. 대부분 회사에서 코드를 작성하는 시간보다 코드를 보고 이해하는 시간이 상당히 많은 것은 사실이다. 따라서 동료 혹은 내가 개발한 코드에 대해 이해를 돕기 위해 클린 코드에 대해 학습할 필요가 있다.17.1 Controller에서 모든 기능을 구현하면 안되는 이유함수는 최대한 작게 만들고 한 가지 일만 수행하는 것이 좋다.클래스는 작아야 하며 하나의 책임 만을 가져야 한다.17.2 우리가 작성한 Controller 함수 1개가 3000줄을 넘으면?!그 함수를 동시에 여러 명이 수정할 수 없다.그 함수를 읽고, 이해하는 것이 너무 어렵다.그 함수의 어느 부분을 수정하더라도 함수 전체에 영향을 미칠수 있기 때문에 함부로 건들 수 없게 된다.너무 큰 기능이기 때문에 테스트도 힘들다.종합적으로 유지 보수성이 매우 떨어진다.17.3 지금까지 구현한 controller 코드의 문제점17.4 [1] API의 진입 지점으로써 HTTP Body를 객체로 변환한다.17.5 [2] 현재 유저가 있는지 없는지 확인하고 예외 처리를 한다.17.6 [3] SQL을 사용해 실제 Database와의 통신을 담당한다.다음 강의에서 Controller 에 구현한 세 가지 기능을 분리할 것이다.[1] API의 진입 지점으로써 HTTP Body를 객체로 변환한다.[2] 현재 유저가 있는지 없는지 확인하고 예외 처리를 한다.[3] SQL을 사용해 실제 Database와의 통신을 담당한다.🌈 18강. Controller를 3단 분리하기 - Service와 RepositoryController의 함수 1개가 하고 있던 역할API의 진입지점으로써 HTTP Body를 객체로 변환하고 있다. - controller 역할현재 유저가 있는지, 없는지 등을 확인하고 예외 처리를 해준다. - service 역할SQL을 사용해 실제 DB 와의 통신을 담당한다. - repository 역할한가지 궁금한 점을 남기면서 다음 강의 주제에 대해 미리 고민해보자.Controller에서 JdbcTemplate은 어떻게 가져온 것일까?4. 과제 내용 요약깃허브 Wiki 에서 모든 과제 정리를 확인할 수 있습니다. 📚깃허브 Wiki로 이동하기4.1 첫 번째 과제! (진도표 1일차)어떤 관점에서 접근했는지 : 책과 강의 중심으로 기본 내용 위주로 정리문제를 해결하는 과정은 무엇이었는지 : 아래에 정리왜 그런 식으로 해결했는지 : 기본을 모르고 사용하면 무의미라고 생각Q. 어노테이션을 사용하는 이유 (효과) 는 무엇일까?어노테이션(Annotation)은 자바 프로그래밍 언어에서 메타데이터를 표현하는 방법 중 하나입니다. 즉, 프로그램에 대한 데이터를 프로그램 자체에 포함시키는 것입니다. 어노테이션은 컴파일러에게 정보를 제공하거나 코드를 분석하고 처리하는 데 사용됩니다.어노테이션은 @ 기호를 사용하여 선언되며, 클래스, 메서드, 필드 및 다른 프로그램 요소에 적용될 수 있습니다. 어노테이션은 일종의 주석으로도 볼 수 있지만, 주석과는 달리 프로그램에 대한 추가 정보를 제공하고 프로그램의 행동을 변경할 수 있습니다.어노테이션은 다양한 용도로 사용될 수 있습니다. 주요 용도는 다음과 같습니다.컴파일러 지시자: 어노테이션을 사용하여 컴파일러에게 특정 경고를 무시하도록 지시하거나, 코드를 자동 생성하도록 지시할 수 있습니다.런타임 처리: 어노테이션을 사용하여 런타임에 특정 기능을 활성화하거나 비활성화하거나, 특정 조건을 검사할 수 있습니다.문서화: 어노테이션을 사용하여 코드를 문서화하거나, 코드의 목적이나 사용법을 설명할 수 있습니다.코드 분석 및 검증: 어노테이션을 사용하여 코드를 분석하고 검증하는데 활용할 수 있습니다.예를 들어, 스프링 프레임워크에서는 @Controller, @Autowired, @RequestMapping 등의 어노테이션을 사용하여 컴포넌트를 식별하고 의존성을 주입하며, 요청 매핑을 정의합니다. 이러한 어노테이션을 사용하여 스프링이 애플리케이션을 자동으로 구성하고 관리할 수 있습니다.Q. 나만의 어노테이션은 어떻게 만들 수 있을까?새로운 어노테이션을 정의하는 방법은 @ 기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일하다.@interface 어노테이션이름{    타입 요소이름(); // 어노테이션의 요소를 선언한다.   . . . }어노테이션의 요소어노테이션의 요소(element)는 어노테이션 내에 선언된 메서드를 말한다.어노테이션에도 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 없다.어노테이션의 요소는 반환 값이 있고, 매개 변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다.단, 어노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해야 한다. 요소의 이름도 같이 작성하기 때문에 순서는 상관없다.다음 예제는 어노테이션의 선언과 사용 예제입니다:@interface TestInfo {    int count();    String testedBy();    String[] testTools();    TestType testType(); // enum TestType { FIRST, FINAL }    DateTime testDate(); // 자신이 아닌 다른 어노테이션(@DateTime)을 포함할 수 있다. } ​ @interface DateTime {    String yymmdd();    String hhmmss(); }// 애너테이션 사용. 요소의 값을 타입에 맞게 전부 적어주어야 한다. @TestInfo(    count=3, testedBy="Kim",    testTools={"JUnit", "AutoTester"},    testType=TestType.FIRST,    testDate=@DateTime(yymmdd="160101", hhmmss="235959") ) public class NewClass { ... }어노테이션 요소의 기본 값어노테이션의 각 요소는 기본값을 가질 수 있다.기본값이 있는 요소는 어노테이션을 적용할 때 지정하지 않으면 기본값이 사용된다.기본값으로 null을 제외한 모든 리터럴이 가능하다.어노테이션의 요소가 하나이고 이름이 value인 경우, 어노테이션을 적용할 때 요소의 이름을 생략 하고 값만 적어도 된다.요소의 타입이 배열인 경우, 중괄호 {}를 사용해서 여러 개의 값을 지정할 수 있다.기본값을 지정할 때도 중괄호{}를 사용할 수 있다.@interface TestInfo {    int count() default 1;  // 기본값을 1로 지정 } ​ @TestInfo  // @TestInfo(count = 1)과 동일 public class NewClass { ... }// 어노테이션의 요소가 오직 1개에 이름이 value인 경우, 어노테이션 요소의 이름을 생략하고 값만 작성해도 된다. @interface TestInfo {    String value(); } ​ @TestInfo("passed") // @TestInfo(value="passed")와 동일 class NewClass { ... }// 요소의 타입이 배열인 경우, 괄호 {}를 사용해서 여러 개의 값을 지정할 수 있다. @interface TestInfo{ String[] testTools(); } ​ @TestInfo(testTools={"Junit", "AutoTester"}) // 값이 여러 개인 경우 @TestInfo(testTools="Junit") // 값이 하나일 때는 괄호 {} 생략 가능 @TestInfo(testTools={})  // 값이 없을 때는 괄호{}가 반드시 필요// 기본값을 지정할 때도 마찬가지로 괄호 {}를 사용할 수 있다. @interface TestInfo {    String[] info() default {"aaa","bbb"};    String[] info2() default "ccc"; // 기본값이 하나인 경우 괄호 생략가능. } ​ @TestInfo // @TestInfo(info={"aaa","bbb"}, info2="ccc") 와 동일 @TestInfo(info2={}) // @TestInfo(info={"aaa","bbb"}, info2={}) 와 동일 class NewClass { ... }// 요소의 타입이 배열일 때도 요소의 이름이 value이면, 요소의 이름을 생략할 수 있다. @interface SuppressWarnings { String[] value(); } ​ // @SuppressWarnings(value = {"deprecation", "unchecked"}) @SuppressWarnings({"deprecation", "unchecked"}) class NewClass { ... }java.lang.annotation.Annotation모든 어노테이션의 조상은 Annotation 이다. 그러나 어노테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotation을 조상으로 지정할 수 없다.@interface TestInfo extends Annotation { // 에러. 허용되지 않는 표현    int count();    String testedBy();   ... }그리고 아래의 코드에서 볼 수 있듯이 Annotation은 어노테이션이 아니라 일반적인 인터페이스로 정의되어 있다.package java.lang.annotation; ​ public interface Annotation { // Annotation 자신은 인터페이스이다.    boolean equals(Object obj);    int hashCode();    String toString(); ​    Class<? extends Annotation> annotationType(); // 어노테이션 타입 반환Annotation 인터페이스 안의 메소드들은 추상 메소드이지만, 컴파일러가 자동으로 내용을 구현해주기 때문에 메소드 사용 가능. 모든 어노테이션의 조상이므로 모든 어노테이션 객체에 대해 equals(), hashCode(), toString() 과 같은 메서드를 호출하는 것이 가능하다.마커 어노테이션 - Marker Annotation 값을 지정할 필요가 없는 경우, 어노테이션의 요소를 하나도 정의하지 않을 수 있다. Serializable 이나 Cloneable 인터페이스처럼, 요소가 하나도 정의되지 않은 어노테이션을 마커 어노테이션이라고 한다.@Target(ElementType.METHOD) @Retension(RetentionPolicy.SOURCE) public @interface Override {} // 마커 어노테이션. 요소가 0개 ​ @Target(ElementType.METHOD) @Retension(RetentionPolicy.SOURCE) public @interface Test {} // 마커 어노테이션. 요소가 0개어노테이션 요소의 규칙 어노테이션의 요소를 선언할 때 아래의 규칙을 반드시 지켜야 한다.요소의 타입은 기본형, String, enum, 어노테이션, Class 만 허용된다.괄호 안에 매개변수를 선언할 수 없다.예외를 선언할 수 없다.요소를 타입 매개변수로 정의할 수 없다.아래의 코드에서 오른쪽 주석을 통해 잘못된 부분을 알아본다.@interface AnnoTest {    int id = 100; // OK. 상수 들어갈 수 있다. static final int id = 100;    String major(int i, int j); // 에러. 매개변수 들어갈 수 없다.    String minor() throws Exception; // 에러. 예외 처리 불가능    ArrayList<T> list(); // 에러. 요소의 타입에 타입 매개변수 정의 불가능 }위의 방법을 통해서 간단하게 어노테이션을 구현하면 다음과 같습니다:import java.lang.annotation.*; ​ @Retention(RetentionPolicy.RUNTIME) // 어노테이션 유지 정책 설정 @Target(ElementType.METHOD) // 어노테이션이 메서드에 적용되도록 지정 public @interface MyAnnotation {    String value(); // 속성 정의 }@interface 키워드 사용: 어노테이션을 정의하기 위해 @interface 키워드를 사용합니다. 이 키워드를 사용하여 새로운 어노테이션을 선언하고 그 내부에 속성을 정의할 수 있습니다.속성 정의: 어노테이션 내에서 사용할 속성을 정의합니다. 속성은 메서드처럼 선언되며, 반환 유형과 속성 이름이 있습니다. 이러한 속성을 사용하여 어노테이션에 추가 정보를 제공할 수 있습니다.Retention 정책 설정 (optional): 어노테이션을 정의할 때 Retention 정책을 설정할 수 있습니다. Retention 정책은 어노테이션이 유지될 시점을 결정합니다. RetentionPolicy 열거형에는 SOURCE, CLASS, RUNTIME 세 가지 정책이 있으며, 각각 컴파일 시, 클래스 로딩 시, 런타임 시에 어노테이션이 유지됩니다. 기본적으로는 RUNTIME 정책이 적용됩니다.위의 코드에서 @interface 키워드를 사용하여 MyAnnotation이라는 새로운 어노테이션을 정의하였습니다. 이 어노테이션은 메서드에 적용될 수 있도록 @Target(ElementType.METHOD)를 설정하였고, 런타임 시에 어노테이션이 유지되도록 @Retention(RetentionPolicy.RUNTIME)을 설정하였습니다. value()라는 속성을 정의하였습니다.이제 위에서 정의한 어노테이션을 사용할 수 있습니다:public class MyClass { ​    @MyAnnotation(value = "Hello")    public void myMethod() {        // 메서드 내용   } }위의 코드에서 myMethod() 메서드에 @MyAnnotation 어노테이션을 적용하고, 속성 값으로 "Hello"를 전달하였습니다. 이렇게 정의된 어노테이션은 런타임 시에 메서드에서 해당 어노테이션 정보를 읽어올 수 있습니다.4.2 두 번째 과제! (진도표 2일차)어떤 관점에서 접근했는지 : 강의 중심으로 정리문제를 해결하는 과정은 무엇이었는지 : 아래에 정리왜 그런 식으로 해결했는지 : 강의에서 벗어난 방식으로 구현하고 싶지 않았음문제 1Ex01Controllerpackage com.group.libarayapp.controller.task; ​ import com.group.libarayapp.dto.task.request.Ex01Request; import com.group.libarayapp.dto.task.response.Ex01Response; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; ​ @RestController public class Ex01Controller { ​    @GetMapping("/api/v1/calc")    public Ex01Response firstEx(Ex01Request request) {        return new Ex01Response(request);   } }Ex01Requestpackage com.group.libarayapp.dto.task.request; ​ public class Ex01Request {    private final int num1;    private final int num2;    public Ex01Request(int num1, int num2) {        this.num1 = num1;        this.num2 = num2;   } ​    public int getNum1() {        return num1;   } ​    public int getNum2() {        return num2;   } }Ex01Responsepackage com.group.libarayapp.dto.task.response; ​ import com.group.libarayapp.dto.task.request.Ex01Request; ​ public class Ex01Response { ​    private final int add;    private final int minus;    private final int multiply; ​ ​    public Ex01Response(Ex01Request ex01Request) {        this.add = ex01Request.getNum1() + ex01Request.getNum2();        this.minus = ex01Request.getNum1() - ex01Request.getNum2();        this.multiply = ex01Request.getNum1() * ex01Request.getNum2();   } ​    public int getAdd() {        return add;   } ​    public int getMinus() {        return minus;   } ​    public int getMultiply() {        return multiply;   } }Ex01Controller에서 쿼리 파라미터를 입력 받을 수 있도록 public Ex01Response firstEx(Ex01Request request) 으로 메서드명 선언한다.구체적으로, firstEx 메서드의 매개변수인 Ex01Request 클래스에 두 개의 입력 받은 쿼리 파라미터인 num1, num2 가 저장되고, Ex01Response 클래스의 생성자의 매개변수로 Ex01Request 를 작성하여 요구사항에 따라 두 수의 합, 뺄셈, 곱을 구한다.계속 해메고 있는 문제가 JSON 형식으로 다시 출력할 때, 키의 이름은 getter 메서드명에 의해 정해지는 것을 계속 해메고 나서 알게 됐다.문제 2Ex02Controllerpackage com.group.libarayapp.controller.task; ​ import com.group.libarayapp.dto.task.request.Ex02Request; import com.group.libarayapp.dto.task.response.Ex02Response; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; ​ @RestController public class Ex02Controller { ​    @GetMapping("/api/v1/day-of-the-week")    public Ex02Response secondEx(Ex02Request request) {        return new Ex02Response(request);   } }Ex02Requestpackage com.group.libarayapp.dto.task.request; ​ import java.time.LocalDate; ​ public class Ex02Request { ​    private final LocalDate date; ​    public Ex02Request(LocalDate date) {        this.date = date;   } ​    public LocalDate getDate() {        return date;   } }Ex02Responsepackage com.group.libarayapp.dto.task.response; ​ import com.group.libarayapp.dto.task.request.Ex02Request; ​ import java.time.format.TextStyle; import java.util.Locale; ​ public class Ex02Response { ​    private final String dayOfTheWeek; ​    public Ex02Response(Ex02Request request) {        this.dayOfTheWeek = request.getDate().getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase();   } ​    public String getDayOfTheWeek() {        return dayOfTheWeek;   } }한 개의 쿼리 파라미터를 받는 문제이기 때문에 @RequestParam 사용을 고려했지만, 별도의 dto 를 생성하는 경우가 더욱 많을 것으로 생각되기 때문에 연습할 겸 Ex02Request 클래스에 쿼리 파라미터를 받을 수 있도록 생성했다.LocalDate 는 날짜 정보만 제공하는데 사용되는 클래스이다. 참고한 자료는 https://www.tcpschool.com/java/java_time_localDateTime 이다.문제 1번과 동일한 방식과 동일하다. Ex02Response 의 생성자의 매개변수를 쿼리 파라미터가 저장된 Ex02Request 으로 작성하고, 쿼리 파라미터인 날짜 정보를 알맞은 포메팅 형식으로 변환한다. 그리고 getter 메서드 이름에 유의하여 키 이름과 동일한 dayOfTheWeek 으로 하자.문제 3Ex03Controllerpackage com.group.libarayapp.controller.task; ​ ​ import com.group.libarayapp.dto.task.request.Ex03Request; import com.group.libarayapp.dto.task.response.Ex03Response; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; ​ @RestController public class Ex03Controller { ​    @PostMapping("/api/v1/numbers-sum")    public int thirdEx(@RequestBody Ex03Request request) {        Ex03Response result = new Ex03Response(request.getNumbers());        return result.getSum();   } }Ex03Requestpackage com.group.libarayapp.dto.task.request; ​ import java.util.ArrayList; import java.util.List; ​ public class Ex03Request { ​    private final List<Integer> numbers = new ArrayList<>(); ​    public List<Integer> getNumbers() {        return numbers;   } ​ ​ }Ex03Responsepackage com.group.libarayapp.dto.task.response; ​ import java.util.List; ​ public class Ex03Response {    private int sum; ​    public Ex03Response(List<Integer> numbers) {        for (int number : numbers) {            sum += number;       }   } ​    public int getSum() {        return sum;   } }문제 1, 문제 2 와 방식이 다르다. 지금까지 인풋으로 쿼리 파라미터를 보냈지만, 문제 3은 JSON을 인풋으로 넣고 반환 타입은 정수이다.참고로 JSON기반의 메시지를 사용하는 요청의 경우에는 @RequestBody 십중팔구 사용한다는 것을 기억하자. @RequestBody를 사용하면, HTTP Body 안의 데이터를 자바 객체로 변환하여 매핑된 메서드 파라미터로 전달한다.그래서 Ex03Request 안에 여러 숫자를 저장하기 위한 List가 있다. 그리고 지난 문제들과 동일한 방식으로 모든 처리는 response 에서 수행한다. Ex03Response 에서 입력된 모든 수를 더한다. 이때, 어떠한 숫자도 입력이 안된 상황이거나 null인 경우에 대한 예외는 작성하지 않고 넘어갔다. 배우지 않은 다른 것들을 생각하기 보다는 학습한 내용을 중점적으로 복습할 것이다.4.3 세 번째 과제! (진도표 3일차)어떤 관점에서 접근했는지 : 책과 강의 중심으로 정리문제를 해결하는 과정은 무엇이었는지 : 아래에 정리왜 그런 식으로 해결했는지 : 기본을 모르고 사용하면 무의미라고 생각1. 자바의 람다식은 왜 등장했을까?자바의 람다식(lambda expression)은 자바 8에서 도입되었으며, 그 등장 배경에는 여러 이유가 있습니다. 람다식은 간결한 코드 작성을 가능하게 하여 자바의 표현력을 크게 향상시켰고, 함수형 프로그래밍 패러다임을 자바에 도입하여 더 유연하고 효율적인 프로그래밍 방식을 제공합니다. 람다식이 등장한 주요 이유는 다음과 같습니다:코드의 간결성: 람다식을 사용하면 익명 클래스를 사용할 때보다 훨씬 더 간결한 코드로 이벤트 리스너나 콜백과 같은 함수형 인터페이스의 인스턴스를 생성할 수 있습니다. 이는 코드의 가독성을 크게 향상시키고, 개발자가 더 중요한 로직에 집중할 수 있게 해줍니다.함수형 프로그래밍의 도입: 자바 8 이전 버전은 주로 객체 지향 프로그래밍 패러다임을 따랐습니다. 람다식의 도입으로 자바는 함수형 프로그래밍 개념을 효과적으로 통합하여, 개발자가 상태 변경이나 가변 데이터를 피하는 순수 함수형 프로그래밍 스타일을 채택할 수 있게 되었습니다. 이는 병렬 처리와 이벤트 처리 코드를 더 쉽고 효율적으로 작성할 수 있게 해줍니다.병렬 처리의 용이성: 자바 8에서는 스트림 API도 함께 도입되었습니다. 람다식과 스트림 API의 조합은 컬렉션 처리를 위한 선언적인 접근 방식을 제공하여, 병렬 처리를 쉽게 구현할 수 있게 해줍니다. 이는 멀티코어 프로세서의 이점을 활용하여 성능을 향상시킬 수 있는 중요한 기능입니다.API의 일관성과 유연성 향상: 람다식을 통해 자바의 기존 API들은 더 유연하고 강력한 방식으로 확장될 수 있게 되었습니다. 예를 들어, java.util.Collection 인터페이스에 새롭게 추가된 forEach, removeIf 같은 메소드들은 람다식을 이용하여 보다 쉽게 사용할 수 있게 되었습니다.람다식의 도입은 자바를 더 현대적이고, 표현력이 풍부하며, 다양한 프로그래밍 스타일을 지원하는 언어로 변모시켰습니다. 이러한 변화는 자바가 계속해서 발전하고 현대적인 프로그래밍 요구사항에 부응할 수 있게 하는 데 중요한 역할을 했습니다.2. 람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?2.1 람다식과 익명 클래스의 관계구현 방식의 차이람다식은 익명 클래스를 사용하는 것보다 훨씬 간결합니다.람다식은 컴파일러에 의해 익명 클래스로 변환될 수 있지만, 람다식이 제공하는 간결성과 명확성은 익명 클래스보다 뛰어납니다.사용 범위의 차이람다식은 오직 함수형 인터페이스에만 사용될 수 있습니다.익명 클래스는 함수형 인터페이스 뿐만 아니라, 추상 클래스나 구체 클래스의 하위 클래스를 만드는 데에도 사용될 수 있습니다.this 키워드의 차이람다식 내부에서 this 키워드는 람다식을 감싸는 클래스를 가리킵니다.익명 클래스 내부에서 this는 익명 클래스 자신을 가리킵니다.직렬화익명 클래스 인스턴스는 직렬화할 수 있지만(해당 클래스가 직렬화를 지원한다면)람다식은 직렬화에 대해 명시적으로 설계되지 않았습니다. 람다식의 직렬화는 권장되지 않으며, 사용 시 주의가 필요합니다.람다식과 익명 클래스 사이의 이러한 차이점들은 자바에서 특정 상황에 맞는 가장 적절한 도구를 선택하는 데 도움을 줍니다. 람다식은 간결성과 명확성 때문에 많은 경우 익명 클래스를 대체하게 되었지만, 익명 클래스가 여전히 유용한 상황이 있습니다.2.2 this 키워드, 사용 범위의 차이this 키워드의 차이this 키워드는 익명 클래스와 람다식에서 다르게 작동합니다.익명 클래스 예제public class AnonymousClassExample {    private String message = "익명 클래스 메시지"; ​    public void doWork() {        Runnable r = new Runnable() { // 익명 클래스 생성            private String message = "Runnable 메시지"; ​            @Override            public void run() {                System.out.println(this.message); // this.message는 익명 클래스 내부의 message를 의미           }       };        r.run();   } ​    public static void main(String[] args) {        new AnonymousClassExample().doWork();   } }위 익명 클래스 예제에서 this.message는 익명 클래스 내부의 message 변수를 가리킵니다. 따라서 출력은 "Runnable 메시지"가 됩니다.람다식 예제public class LambdaExample {    private String message = "람다 메시지"; ​    public void doWork() {        Runnable r = () -> System.out.println(this.message); // 람다식 사용        r.run();   } ​    public static void main(String[] args) {        new LambdaExample().doWork();   } }위 람다식 예제에서 this.message는 람다식을 감싸는 LambdaExample 클래스의 인스턴스 변수 message를 가리킵니다. 따라서 출력은 "람다 메시지"가 됩니다.사용 범위의 차이함수형 인터페이스와 익명 클래스 모두를 사용할 수 있는 상황과 익명 클래스만 사용할 수 있는 상황의 예를 들어보겠습니다.함수형 인터페이스 사용 예제함수형 인터페이스인 Runnable에 대한 람다식과 익명 클래스의 사용 예:람다식Runnable rLambda = () -> System.out.println("람다식 실행"); rLambda.run(); 익명 클래스Runnable rAnonymous = new Runnable() {    @Override    public void run() {        System.out.println("익명 클래스 실행");   } }; rAnonymous.run(); 위 예제에서는 람다식과 익명 클래스 모두 Runnable을 구현할 수 있습니다.익명 클래스만 사용할 수 있는 상황 예제익명 클래스를 사용하여 추상 클래스의 추상 메소드를 구현하는 경우:abstract class MyAbstractClass {    abstract void myMethod(); } ​ public class Test {    public static void main(String[] args) {        MyAbstractClass myObj = new MyAbstractClass() {            @Override            void myMethod() {                System.out.println("추상 클래스의 메소드 구현");           }       };        myObj.myMethod();   } }이 경우, MyAbstractClass는 함수형 인터페이스가 아니므로 람다식을 사용할 수 없습니다. 따라서 이런 상황에서는 익명 클래스를 사용해야 합니다.이 예제들을 통해 this 키워드의 차이와 사용 범위의 차이를 이해할 수 있습니다. 익명 클래스는 더 일반적인 사용 사례에 적합할 수 있지만, 람다식은 코드를 더 간결하고 명확하게 만드는 데 유리합니다.2.3 인터페이스의 익명 클래스와 람다 표현식// 익명 클래스로 작성하기 Operate operate = new Operate() {    public int perate(int a, int b) {        return a + b;   } };// 위의 코드를 람다식으로 표현하기 Operate operate = (a, b) -> {    return a + b; };람다식으로 표현하면 코드를 작성해야 하는 글자 수가 줄어들고, 간결하게 표현이 가능합니다. 해당 코드를 함수형 프로그래밍에서는 람다 또는 익명 함수라고도 부릅니다.public class Main {    public static void main(String[] args) {        Calculator calculator = new Calculator(20, 10);        int result = calculator.result((a, b) -> {            return a + b;       });        System.out.println(result); // 30                int result2 = calculator.result((a, b) -> {            return a - b;       });        System.out.println(result2); // 10   } }이전의 코드보다 람다를 이용한 코드 작성이 코드 작성 수도 적어지고 보기도 편해질 수 있습니다.Operate operate = (a, b) -> {    return a + b; };이전 코드보다 람다는 더 간단히 작성이 가능합니다.Operate operate = (a, b) -> a + b;이렇게 줄일 수 있는 경우는 반환값이 있는 람다이어야 하고, 람다 내부 구문이 코드로 한 줄 작성이 가능해야 하며 return을 명시하지 않아도 인지할 수 있도록 연산 과정이 한 줄로 작성되어야 합니다.// 이러한 코드는 더 간단히 작성될 수 없다. Operate operate = (a, b) -> {    System.out.println("Operate");    return a + b; } 이러한 람다 구문은 더 간단히 작성될 수 없습니다. 쉽게 얘기하자면 세미콜론; 이 두 번 찍히는 내부 구문은 더 줄일 수 없다고 보시면 됩니다.해당 그림은 익명 클래스 인스턴스와 람다와의 연관 관계를 작성한 그림입니다. 변환 과정을 좀 더 한 눈에 볼 수 있습니다.2.4 람다 표현식의 제한참고로 람다 표현식을 사용하기 위해서는 다음의 두 가지 제약 조건을 모두 만족해야 합니다.인터페이스이어야 한다.인터페이스에는 하나의 추상 메서드만 선언되어야 한다.public interface Operate {    // 오버라이드 해야 할 메서드가 두 개인 경우에는 람다 표현식이 불가능하다.    int operate(int a, int b);    void print(); }Operate operate = new Operate() {    public int operate(int a, int b) {        return a + b;   }    public void print() {        System.out.println("출력");   } };오버라이드 해야 할 추상 메서드가 두 개 이상인 경우는 람다 표현식을 사용할 수 없습니다. 람다는 하나의 행위만을 사용한다고 가정하기 때문입니다.public interface Operate {    // 추상 메서드가 하나이다    int operate(int a, int b); ​    // default 메서드는 추상 메서드에 포함되지 않는다    default void print() {        System.out.println("출력");   } }Operate operate = (a, b) -> {    print();    return a + b; };그러나 추상 메서드가 아닌 default 메서드가 포함된 경우는 람다 표현식이 가능합니다. 결론적으로 오버라이드 해야 하는 추상 메서드는 하나이기 때문입니다.2.5 람다의 컨셉우리는 항상 매개 변수로 값을 전달한다는 개념으로 배웠습니다. 물론, 상수 값이나 인스턴스의 참조 값을 전달하는 것은 맞습니다. 그러나 생각을 확장할 필요가 있습니다. 람다를 무엇을 해야 한다는 행위로 본다면, 값이 아니라 행위를 전달한다고 볼 수 있습니다.익숙하지 않은 경우 개념 자체가 어려울 수 있습니다. 그래도 괜찮습니다. 최대한 익숙하게 사용하려고 하면 됩니다.public class Main {    public static void main(String[] args) {        Calculator calculator = new Calculator(20, 10); ​        int result1 = calculator.result((a, b) -> {            return a + b; // a + b를 하라는 행위 전달       });                int result2 = calculator.result((a, b) -> {            return a - b; // a - b를 하라는 행위 전달       }); ​        int result3 = calculator.result((a, b) -> {            return a / b; // a / b를 하라는 행위 전달       }); ​        int result4 = calculator.result((a, b) -> {            return a * b; // a * b를 하라는 행위 전달       });   } }값을 전달한다는 개념보다는 '이렇게 해라' 라는 행위를 전달하는 개념은 함수형 프로그래밍으로 가기 위한 기초입니다. 그러나 우리는 아직 함수형 프로그래밍이라는 것에 대해 뭔지도 잘 알지 못할 뿐더러 익숙하지 않고 너무 많은 내용을 배우고 가기 때문에 머리속에 정리하기 힘든 과정이라는 점을 인지해야 합니다.2.6 람다식의 문법은 어떻게 될까?람다식 (Lambda expression)자바가 1996년에 처음 등장한 이후로 두 번의 큰 변화가 있었는데, 한번은 JDK1.5부터 추가된 지네릭스(generics)의 등장이고, 또 한번은 JDK1.8부터 추가된 람다식(lambda expression)의 등장이다. 람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다.람다식이란?람다식(Lambda expression)은 메서드를 하나의 식(expression)으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다. 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 익명 함수(anonymous function)이라고도 한다.int[] arr = new int[5]; Arrays.setAll(arr, i -> (int)(Math.random() * 5) + 1); //arr = [1, 5, 2, 1, 1] ​ //i -> (int)(Math.random() * 5) + 1 int method(int i) { return (int)(Math.random() * 5) + 1; }모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 이 메서드를 호출할 수 있다. 그러나 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있다.람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다. 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.메서드와 함수의 차이객체지향개념에서는 함수(function) 대신 객체의 행위나 동작을 의미하는 메서드(method)라는 용어를 사용한다. 메서드는 함수와 같은 의미이지만, 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미의 다른 용어를 선택해서 사용해 왔다. 그러나 이제 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 함수라는 용어를 사용하게 되었다.람다식 작성하기람다식은 익명 함수이므로 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 -> 를 추가한다.반환타입 메서드이름(매개변수 선언) { 문장들 } ​ /* 반환타입 메서드이름 */ (매개변수 선언) -> { 문장들 } ​ /* Example : 두 값 중에서 큰 값을 반환하는 메서드 max */ int max(int a, int b) { retrun a > b ? a : b; } ​ /* int max */ (int a, int b) -> { return a > b ? a : b; } ​ /* 반환값이 있는 메서드의 경우, return문 대신 식(expression)으로 대신할 수 있다. 식의 연산결과가 자동적으로 반환값이 된다.    이때는 문장(statement)이 아닌 식이므로 끝에 ;을 붙이지 않는다. */ ​ (int a, int b) -> { return a > b ? a : b; } (int a, int b) -> a > b ? a : b ​ /* 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있는데,    대부분의 경우에 생략 가능하다. (int a, b) -> a > b ? a : b    와 같이 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않는다. */     (int a, int b) -> a > b ? a : b (a, b) -> a > b ? a : b ​ /* 선언된 매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있다.    단, 매개변수의 타입이 있으면 괄호()를 생략할 수 없다. */     (a) -> a * a (int a) -> a * a a -> a * a //OK int a -> a * a //에러 ​ /* 괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있다.    이 때 문장의 끝에 ;을 붙이지 않는다. */     (String name, int i) -> { System.out.println(name + " = " + i); } (String name, int i) -> System.out.println(name + " = " + i)     /* 괄호{} 안의 문장이 return문일 경우 괄호{}를 생략할 수 없다. */     (int a, int b) -> { return a > b ? a : b; } //OK (int a, int b) -> return a > b ? a : b //에러3.메서드를 람다식으로 변환한 예제//메서드1 int max(int a, int b) { return a > b ? a : b; } ​ //람다식1 (int a, int b) -> { retrun a > b ? a : b; } (int a, int b) -> a > b ? a : b (a, b) -> a > b ? a : b ​ ​ //메서드2 void printVal(String name, int i) { System.out.println(name + " = " + i); } ​ //람다식2 (String name, int i) -> { System.out.println(name + " = " + i); } (name, i) -> { System.out.println(name + " = " + i); } (name, i) -> System.out.println(name + " = " + i) ​ ​ //메서드3 int square(int x) { return x * x; } ​ //람다식3 (int x) -> x * x (x) -> x * x x -> x * x ​ //메서드4 int roll() { return (int)(Math.random() * 6); } ​ //람다식4 () -> { return (int)(Math.random() * 6); } () -> (int)(Math.random() * 6) ​ ​ //메서드5 int sumArr(int[] arr) { int sum = 0;    for (int i : arr)   sum += i;    return sum; } ​ //람다식5 (int[] arr) -> { int sum = 0;    for (int i : arr)   sum += i;    return sum; }4.함수형 인터페이스(Functional Interface)람다식은 익명 클래스의 객체와 동등하다.(int a, int b) -> a > b ? a : b ​ new Object() { int max(int a, int b) {   return a > b ? a : b;   } }함수형 인터페이스(functional interface) - 람다식을 다루기 위한 인터페이스@FunctionalInterface interface MyFunction { //함수형 인터페이스 MyFunction을 정의 public abstract int max(int a, int b); }함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다. 반면에 static메서드와 default메서드의 개수에는 제약이 없다.@FunctionalInterface를 붙이면, 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해준다.//인터페이스의 메서드 구현 List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa"); ​ Colldections.sort(list, new Comparator<String>() { public int compare(String s1, String s2) {   return s2.compareTo(s1);   } }); ​ //람다식 List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa"); Collections.sort(list, (s1, s2) -> s2.compareTo(s1));함수형 인터페이스 타입의 매개변수와 반환타입//함수형 인터페이스 MyFunction 정의 @FunctionalInterface interface MyFunction { void myMethod(); //추상 메서드 }//메서드의 매개변수가 MyFunction타입이면, //이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다는 뜻이다. void aMethod(MyFunction f) { //매개변수의 타입이 함수형 인터페이스 f.myMethod(); //MyFunction에 정의된 메서드 호출 } ​ MyFunction f = () -> System.out.println("myMethod()"); aMethod(f); ​ //또는 참조변수 직접 람다식을 매개변수로 지정하는 것도 가능하다. aMethod(() -> System.out.println("myMethod()")); //람다식을 매개변수로 지정 ​ //메서드의 반환타입이 함수형 인터페이스타입이라면, //이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 //람다식을 직접 반환할 수 있다. MyFunction myMethod() { MyFunction f = () -> {};    return f; //return () -> {}; }람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능해진 것이다.5.람다식의 타입과 형변환함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. 정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없다. 그래서 대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요하다.//interface MyFunction{void method();} MyFunction f = (MyFunction)(() -> {}); //양변의 타입이 다르므로 형변환 필요 람다식은 이름이 없을 뿐 객체인데도 분명하고 Object타입으로 형변환 할 수 없다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.Object obj = (Object)(() -> {}); //에러. 함수형 인터페이스로만 형변환 가능 ​ //Object타입으로 형변환하려면, 먼저 함수형 인터페이스로 변환해야 한다. Object obj = (Object)(MyFunction)(() -> {}); String str = ((Object)(MyFunction)(() -> {})).toString();일반적인 익명 객체라면, 컴파일러가 객체의 타입을 '외부클래스이름$번호'와 같은 형식으로 만들어내지만, 람다식의 타입은 '외부클래스이름$$Lambda$번호'와 같은 형식으로 만들어낸다.외부 변수를 참조하는 람다식람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 익명 클래스와 동일하다.6 java.util.function패키지java.util.function패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다. 매번 새로운 함수형 인터페이스를 정의하지 말고, 가능하면 이 패키지의 인터페이스를 활용하는 것이 좋다.java.util.function패키지의 주요 함수형 인터페이스매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의되어 있고, Function의 변형으로 Predicate가 있는데, 반환값이 boolean이라는 것만 제외하면 Function과 동일하다. Predicate는 조건식을 함수로 표현하는데 사용된다.타입 문자 T는 Type을, R은 Return Type을 의미한다.조건식의 표현에 사용되는 PredicatePredicate는 Function의 변형으로, 반환타입이 boolean이라는 것만 다르다. Predicate는 조건식을 람다식으로 표현하는데 사용된다.Predicate<String> isEmptySTr = s -> s.length() == 0; String s = ""; ​ if (isEmptyStr.test(s)) //if(s.length() == 0) System.out.println("This is an empty String.");매개변수가 두 개인 함수형 인터페이스매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 Bi가 붙는다.매개변수의 타입으로 보통 T를 사용하므로, 알파벳에서 T의 다음 문자인 U, V, W를 매개변수의 타입으로 사용하는 것일 뿐 별다른 의미는 없다. 두 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어 써야 한다.//3개의 매개변수를 갖는 함수형 인터페이스 선언 @FunctionalInterface interface TriFunction<T, U, V, R> { R apply(T t, U u, V v); }UnaryOperator와 BinaryOperatorFunction의 또 다른 변형으로, 매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.UnaryOperator와 BinaryOperator의 조상은 각각 Function과 BiFunction이다.컬렉션 프레임웍과 함수형 인터페이스컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그 중의 일부는 함수형 인터페이스를 사용한다.인터페이스메서드설명Collectionboolean removeIf(Predicate fliter)조건에 맞는 요소를 삭제Listvoid replaceAll(UnaryOperator operator)모든 요소를 변환하여 대체Iterablevoid forEach(Consumer action)모든 요소에 작업 action을 수행MapV compute(K key, BiFunction<K, V, V> f)지정된 키의 값에 작업 f를 수행V computeIfAbsent(K key, Function<K, V> f)키가 없으면, 작업 f 수행 후 추가V computeIfPresent(K key, BiFunction<K, V, V> f)지정된 키가 있을 때, 작업 f 수행V merget(K key, V value, BiFunction<V, V, V> f)모든 요소에 병합작업 f를 수행void forEach(BiConsumer<K, V> action)모든 요소에 작업 action을 수행void replaceAll(BiFunction<K, V, V> f)모든 요소에 치환작업 f를 수행기본형을 사용하는 함수형 인터페이스7.Function의 합성과 Predicate의 결합java.util.function패키지의 함수형 인터페이스에는 추상메서드 외에도 디폴트 메서드와 static메서드가 정의되어 있다.//Function default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) default <V> Function<V, R> compose(Function<? super V, ? extends T> before) static <T> Function<T, T> identity() ​ //Predicate default Predicate<T> and(Predicate<? super T> other) default Predicate<T> or(Predicate<? super T> other) default Predicate<T> negate() static<T> Predicate<T> isEqual(Object targetRef)* 원래 Function인터페이스는 반드시 두개의 타입을 지정해 줘야하기 때문에, 두 타입이 같아도 Function 라고 쓸 수 없다. Function<T, T>라고 써야한다.Function의 합성두 람다식을 합성해서 새로운 람다식을 만들 수 있다./* 문자열을 숫자로 변환하는 함수 f와 숫자를 2진 문자열로 변환하는 함수 g를    andThen()으로 합성하여 새로운 함수 h 만들기 */ Function<String, Integer> f = (s) -> Integer.parseInt(s, 16); //s를 16진수로 인식 Function<Integer, String> g = (i) -> Integer.toBinaryString(i); //함수 h의 지네릭 타입이 <String, String>이므로 String을 입력받아서 String을 결과로 반환한다. Function<String, String> h = f.andThen(g); ​ /* compose()를 이용해 두 함수를 반대의 순서로 합성 */ Function<Integer, String> g = (i) -> Integer.toBinaryString(i); //i를 2진 문자열로 반환 Function<String, Integer> f = (s) -> Integer.parseInt(s, 16); //s를 16진수로 인식해서 변환 //함수 h의 지네릭 타입이 <Integer, Integer>이다. Function<Integer, Integer> h = f.compose(g); ​ /* identity()는 함수를 적용하기 이전과 이후가 동일한 항등 함수가 필요할 때 사용한다.    이 함수를 람다식으로 표현하면 x -> x 이다. */ Function<String, String> f = x -> x; //Function<String, String> f = Function.identity(); //위의 문장과 동일 ​ System.out.println(f.apply("AAA")); //AAA가 그대로 출력됨* 항등 함수는 함수에 x를 대입하면 결과가 x인 함수를 말한다. f(x) = xPredicate의 결합여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.Predicate<Integer> p = i -> i < 100; Predicate<Integer> q = i -> i < 200; Predicate<Integer> r = i -> i % 2 == 0; Predicate<Integer> notP = p.negate(); //i >= 100 ​ Predicate<Integer> all = notP.and(q).or(r); //100 <= i && i < 200 || i % 2 == 0 System.out.println(all.test(150); //true ​ //람다식을 직접 넣기 Predicate<Integer> all = notP.and(i -> i < 200).or(i -> i % 2 == 0);* Predicate의 끝에 negate()를 붙이면 조건식 전체가 부정이 된다.static메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다.먼저, isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정한다.Predicate<String> p = Predicate.isEqual(str1); boolean result = p.test(str2); //str1과 str2가 같은지 비교하여 결과를 반환 ​ //위 두 문장 합친 하나의 문장 boolean result = Predicate.isEqual(str1).test(str2); //str1과 str2가 같은지 비교8.메서드 참조람다식이 하나의 메서드만 호출하는 경우에는 메서드 참조(method reference) 라는 방법으로 람다식을 간략히 할 수 있다.//문자열을 정수로 변환하는 람다식 Function<String, Integer> f = (String s) -> Integer.parseInt(s); ​ //메서드 Integer wrapper(String s) { //이 메서드의 이름은 의미없다. return Integer.parsetInt(s); } ​ //메서드를 빼고 Integer.parseInt()를 직접 호출 //람다식의 일부가 생략되었지만, //컴파일러는 생략된 부분을 우변의 parseInt메서드의 선언부로부터, //또는 좌변의 Function인터페이스에 지정된 지네릭 타입으로부터 쉽게 알아낼 수 있다. Function<String, Integer> f = Integer::parseInt; //메서드 참조 ​ //another Example BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2); ​ //참조변수 f의 타입을 봤을 때 람다식이 두 개의 String타입의 매개변수를 받는다는 것을 알 수 있다. //그러므로, 람다식의 매개변수들은 없어도 된다. //매개변수 s1과 s2를 생략하면 equals만 남는데, //두 개의 String을 받아서 Boolean을 반환하는 equals라는 이름의 메서드는 다른 클래스에도 //존재할 수 있기 때문에 equals앞에 클래스 이름은 반드시 필요하다. BiFunction<String, String, Boolean> f = String::equals; //메서드 참조 ​ //이미 생성된 객체의 메서드를 람다식에서 사용한 경우에는 //클래스 이름 대신 그 객체의 참조변수를 적어줘야 한다. MyClass obj = new MyClass(); Function<String, Boolean> f = (x) -> obj.equals(x); //람다식 Function<String, Boolean> f2 = obj::equals; //메서드 참조람다식을 메서드 참조로 변환하는 방법static메서드 참조 | (x) -> ClassName.method(x) | ClassName::method인스턴스메서드 참조 | (obj, x) -> obj.method(x) | ClassName::method특정 객체 인스턴스메서드 참조 | (x) -> obj.method(x) | obj::method하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름' 으로 바꿀 수 있다.생성자의 메서드 참조생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.Supplier<MyClass> s = () -> new MyClass(); //람다식 Supplier<MyClass> s = MyClass::new; //메서드 참조매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.Function<Integer, MyClass> f = (i) -> new MyClass(i); //람다식 Function<Integer, MyClass> f2 = MyClass::new; //메서드 참조 ​ BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s); //람다식 BiFunction<Integer, String, MyClass> bf2 = MyClass::new; //메서드 참조 ​ //배열 생성 Function<Integer, int[]> f = x -> new int[x]; //람다식 Function<Integer, int[]> f2 = int[]::new; //메서드 참조 메서드 참조는 람다식을 마치 static변수처럼 다룰 수 있게 해준다.4.4 네 번째 과제! (진도표 4일차)어떤 관점에서 접근했는지 : 강의 중심으로 정리문제를 해결하는 과정은 무엇이었는지 : 아래에 정리왜 그런 식으로 해결했는지 : 강의에서 벗어난 방식으로 구현하고 싶지 않았음진도표 4일차와 연결됩니다 우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API 들을 만들어 보며 API 개발에 익숙해져 봅시다!문제는 깃허브 프로젝트에서 확인 가능합니다. 코드는 이슈 #31에서도 확인이 가능합니다.한 걸음 더! (문제 3번을 모두 푸셨다면) SQL의 sum, group by 키워드를 검색해 적용해보세요! :)아래 컨트롤러는 sum, group by 키워드만 적용하여 구현한 코드를 포함하고 있습니다.(학습 범위를 벗어난 기능은 사용하지 않습니다.)지금까지 학습한 내용을 바탕으로 sum, group by 미포함된 문제3의 코드도 아래에 있으니 참고 바랍니다.Task04ExControllerpackage com.group.libarayapp.controller.task04; ​ import com.group.libarayapp.dto.task04.request.Task04CreateRequest; import com.group.libarayapp.dto.task04.request.Task04ExRequest; import com.group.libarayapp.dto.task04.response.Task04ExResponse; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; ​ import java.util.ArrayList; import java.util.List; ​ @RestController public class Task04ExController { ​    private final JdbcTemplate jdbcTemplate; ​    public Task04ExController(JdbcTemplate jdbcTemplate) {        this.jdbcTemplate = jdbcTemplate;   } ​    // 문제 1    @PostMapping("/api/v1/fruit")    public void saveFruit(@RequestBody Task04ExRequest request) {        String sql = "insert into fruit(name, warehousing, price) values(?, ?, ?)";        jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());   } ​ ​    // 문제 2    @PutMapping("/api/v1/fruit")    public void salesQuantityFruit(@RequestBody Task04ExRequest request) {        String readSql = "select * from fruit where id=?";        boolean isSalesFruit = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty();        if (isSalesFruit) {            throw new IllegalArgumentException();       }        String sql = "update fruit set salesQuantity=? where id=?";        jdbcTemplate.update(sql, request.getSalesQuantity() + 1, request.getId());   } ​    // 문제 3 - sum, group by 적용    @GetMapping("/api/v1/fruit/stat")    public Task04ExResponse SalesAmountFruit(@RequestParam String name) {        String readNotSalesSql = """                select sum(price) as notSalesAmount                from fruit                where name=? and salesQuantity = 0                group by salesQuantity;                """;        List<Integer> readNotSalesList = jdbcTemplate.query(readNotSalesSql, (rs, rowNum) -> rs.getInt("notSalesAmount"), name); ​        String readSalesSql = """                select sum(price) as salesAmount                from fruit                where name=? and salesQuantity = 1                group by salesQuantity;                """;        List<Integer> readSalesList = jdbcTemplate.query(readSalesSql, (rs, rowNum) -> rs.getInt("salesAmount"), name); ​        return new Task04ExResponse(readSalesList, readNotSalesList);   } }Task04ExController - 문제3 - sum(), group by() 미적용으로 작성하기package com.group.libarayapp.controller.task04; ... ​ @RestController public class Task04ExController {   ... ​    private final List<Task04CreateRequest> Task04CreateRequestList = new ArrayList<>();    long salesSum = 0;    long notSalesSum = 0; ​    // 문제 3 - sum, group by 미적용    @GetMapping("/api/v1/fruit/stat")    public Task04ExResponse SalesAmountFruit(@RequestParam String name) { ​        // 모든 값을 List 에 저장        String readSql = "select * from fruit";        jdbcTemplate.query(readSql, (rs, rowNum) -> {            Task04CreateRequest createRequest = new Task04CreateRequest(                    rs.getLong("id"),                    rs.getString("name"),                    rs.getDate("warehousing").toLocalDate(),                    rs.getLong("price"),                    rs.getInt("salesQuantity"));            Task04CreateRequestList.add(createRequest); ​            return createRequest;       }); ​        // 판매된 상품과 판매되지 않은 상품의 가격을 반환        for (Task04CreateRequest task04CreateRequest : Task04CreateRequestList) {            if (task04CreateRequest.getName().equals(name)) {                if (task04CreateRequest.getSalesQuantity() > 0) {                    salesSum += task04CreateRequest.getSalesQuantity() * task04CreateRequest.getPrice();               } else {                    notSalesSum += task04CreateRequest.getPrice();               }           }       }        return new Task04ExResponse(salesSum, notSalesSum);   } }Task04ExResponse - 문제3 - sum(), group by() 미적용으로 작성하기package com.group.libarayapp.dto.task04.response; ​ public class Task04ExResponse {    private long salesAmount;    private long notSalesAmount; ​    public Task04ExResponse(long salesAmount, long notSalesAmount) {        this.salesAmount = salesAmount;        this.notSalesAmount = notSalesAmount;   } ​    public long getSalesAmount() {        return salesAmount;   } ​    public long getNotSalesAmount() {        return notSalesAmount;   } }Task04CreateRequestpackage com.group.libarayapp.dto.task04.request; ​ import java.time.LocalDate; ​ public class Task04CreateRequest { ​    Long id;    String name;    LocalDate warehousing;    Long price;    int salesQuantity; ​    public Task04CreateRequest(Long id, String name, LocalDate warehousing, Long price, int salesQuantity) {        this.id = id;        this.name = name;        this.warehousing = warehousing;        this.price = price;        this.salesQuantity = salesQuantity;   } ​    public String getName() {        return name;   } ​    public Long getId() {        return id;   } ​    public LocalDate getWarehousing() {        return warehousing;   } ​    public Long getPrice() {        return price;   } ​    public int getSalesQuantity() {        return salesQuantity;   } }Task04ExRequestpackage com.group.libarayapp.dto.task04.request; ​ import java.time.LocalDate; ​ public class Task04ExRequest { ​    private Long id;    private String name;    private LocalDate warehousingDate; ​    private int salesQuantity;    private Long price; ​    public Task04ExRequest(String name, LocalDate warehousingDate, Long price) {        this.name = name;        this.warehousingDate = warehousingDate;        this.price = price;   } ​    public String getName() {        return name;   } ​    public LocalDate getWarehousingDate() {        return warehousingDate;   } ​    public Long getPrice() {        return price;   } ​    public Long getId() {        return id;   } ​    public int getSalesQuantity() {        return salesQuantity;   } }Task04ExResponsepackage com.group.libarayapp.dto.task04.response; ​ import java.util.List; ​ public class Task04ExResponse {    private final long salesAmount;    private final long notSalesAmount; ​    public Task04ExResponse(List<Integer> readSalesList, List<Integer> readNotSalesList) {        this.salesAmount = readSalesList.get(0);        this.notSalesAmount = readNotSalesList.get(0);   } ​    public long getSalesAmount() {        return salesAmount;   } ​    public long getNotSalesAmount() {        return notSalesAmount;   } }문제1POST 방식으로 JSON 데이터인 이름, 날짜, 가격 을 MySQL에 저장하기 위해 update 쿼리문을 생성한다.포스트맨으로부터 보낸 JSON 데이터는 @RequestBody 에 의해 이름, 날짜, 가격 을 가져오고 Task04ExRequest dto 에 매핑한다.jdbcTemplate.update() 메서드의 매개변수는 update 쿼리문 그리고 인 파라미터를 넣어준다. 인 파라미터는 매핑된 dto 클래스로부터 이름, 날짜, 가격을 getter 메서드로 불러오면 된다.한 걸음 더! 자바에서 정수를 다루는 가장 대표적인 두 가지 방법은 int와 long입니다. 이 두 가지 방법 중 위 API 에서 long을 사용한 이유는 무엇일까요?int 보다 long이 더 많은 정수를 표현할 수 있기 때문입니다. 다음은 각 타입의 표현 범위입니다.int 4바이트 : -2,147,483,648 ~ 2,147,483,647long 8바이트 : -263 ~ (263 - 1), -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807문제 2과일이 판매되면 salesQuantity속성의 값이 1씩 증가하도록 변경했다. (판매된 갯수만큼 증가시키면 좋겠지만 판매 여부가 관건인 문제이다.) 판매되면 1, 판매가 안된 상황이면 0이다. 따라서salesQuantity 속성의 변경은 PUT 방식으로 작성하고 JSON 형식으로 id 를 요청하고 JSON 형식으로 id 를 응답 받는다 .문제 3 - sum(), group by() 미적용 상태데이터베이스에 저장된 모든 데이터를 리스트에 저장하고, 쿼리 파라미터와 동일한 과일의 판매 여부를 비교하여 판매된 과일 금액, 팔리지 않은 금액을 JSON 형식으로 응답한다.문제 3 sum(), group by() 로 변경하기주어진 문제 구하는 방식을 쿼리를 중심으로 구하도록 변경했다. sum(), group by() 쿼리문을 사용하여 요구사항 이외의 다른 문법을 배제하고 코드가 더욱 간결하고 적절하게 쿼리가 수행됐다.4.5 다섯 번째 과제! (진도표 5일차)어떤 관점에서 접근했는지 : 주사위 굴리는 코드를 클린 코드를 지향하고 최적화를 목표문제를 해결하는 과정은 무엇이었는지 : 아래에 정리왜 그런 식으로 해결했는지 : 클린 코드와 최적화가 주 목적이기 때문에 오히려 불필요한 클래스 분할은 무의미하다고 생각하여 다음과 같이 구현을 진행. 제품 및 서비스 관점에서 구현이 주 목표가 아닌 요구 사항에 따라 코드를 클린 코드를 지향하며 최적화에 중점적으로 구현하기 위해 실천.전체 코드package com.group.libarayapp.exercise.day05; ​ import java.util.Scanner; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.IntStream; ​ public class DiceRollStatistics {    private static final int NUMBER_OF_SIDES = 6;    private static final int MAX_THREADS = Runtime.getRuntime().availableProcessors(); ​    public static void main(String[] args) {        try (Scanner scanner = new Scanner(System.in)) {            // 사용자로부터 주사위를 던질 횟수를 입력받습니다.            System.out.print("주사위를 던질 횟수를 입력하세요: ");            int numberOfThrows = scanner.nextInt(); ​            // 주사위를 던져 각 눈의 개수를 계산합니다.            int[] diceCounts = getDiceCounts(numberOfThrows); ​            // 통계를 출력합니다.            printStatistics(diceCounts);       }   } ​    private static int[] getDiceCounts(int numberOfThrows) {        // 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다.        AtomicInteger[] counts = new AtomicInteger[NUMBER_OF_SIDES]; ​        // AtomicInteger 배열을 초기화합니다.        IntStream.range(0, NUMBER_OF_SIDES)               .forEach(i -> counts[i] = new AtomicInteger()); ​        // 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다.        IntStream.range(0, numberOfThrows)               .parallel()               .limit(MAX_THREADS) // 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다.               .forEach(i -> {                    // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다.                    int result = ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES);                    counts[result].incrementAndGet();               }); ​        // 각 눈의 개수를 배열로 변환하여 반환합니다.        return IntStream.range(0, NUMBER_OF_SIDES)               .map(i -> counts[i].get())               .toArray();   } ​    private static void printStatistics(int[] diceCounts) {        // 각 눈의 개수를 출력합니다.        IntStream.range(0, NUMBER_OF_SIDES)               .forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i]));   } }코드 동작 요약java.util.Random 대신 java.util.concurrent.ThreadLocalRandom을 사용하여 더 효율적인 랜덤 생성을 수행합니다.try-with-resources 구문으로 Scanner를 자동으로 닫습니다.스레드 안전한 AtomicInteger 배열을 사용하여 각 주사위 눈의 개수를 계산합니다.병렬 스트림에서 최대 스레드 수를 제한하여 너무 많은 스레드가 생성되는 것을 방지합니다.다음은 코드 라인을 기준으로 각 코드마다 작성한 이유에 대한 설명입니다.private static final int NUMBER_OF_SIDES = 6; private static final int MAX_THREADS = Runtime.getRuntime().availableProcessors();NUMBER_OF_SIDES : 전체 주사위 크기를 6 으로 고정합니다.MAX_THREADS : 병렬 스트림이 사용할 수 있는 최대 스레드 수를 제한합니다.Runtime 클래스는 Java 어플리케이션과 JVM(Java Virtual Machine) 사이의 인터페이스를 제공하는 클래스입니다. 이 클래스의 인스턴스는 getRuntime() 메서드를 통해 얻을 수 있으며, JVM의 실행 환경에 대한 정보를 제공하고 시스템 리소스에 대한 액세스를 제어합니다.멀티스레드 환경에서 병렬 작업을 수행을 위해 현재 내 컴퓨터의 실행 환경 정보 제공을 위해 availableProcessors() 메서드를 용하여 현재 시스템에서 사용 가능한 프로세서 수를 확인할 수 있습니다. availableProcessors() 메서드 뿐만 아니라 메모리 관리(freeMemory(), totalMemory()), 외부 프로세스 실행(exec()), 가비지 컬렉션(gc()), 시스템 종료(exit()) 등 다양한 기능을 지원하는 Runtime 클래스입니다.현재 코드 내에서 Runtime 클래스 사용 목적은 병렬 스트림에서 사용할 최대 스레드 수를 최적화합니다.public static void main(String[] args) { try (Scanner scanner = new Scanner(System.in)) { // 사용자로부터 주사위를 던질 횟수를 입력받습니다. System.out.print("주사위를 던질 횟수를 입력하세요: "); int numberOfThrows = scanner.nextInt(); ​ // 주사위를 던져 각 눈의 개수를 계산합니다. int[] diceCounts = getDiceCounts(numberOfThrows); ​ // 통계를 출력합니다. printStatistics(diceCounts); }numberOfThrows : 콘솔창에서 사용자로부터 주사위 던지는 횟수를 입력받습니다.diceCounts : numberOfThrows만큼 주사위를 굴립니다.printStatistics : 주사위 던진 결과를 출력합니다.Main 메서드 안에는 입력과 로직 처리를 함수로 구현하여 간결하게 작성했습니다.병렬로 구현한 주사위private static int[] getDiceCounts(int numberOfThrows) { // 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다. AtomicInteger[] counts = new AtomicInteger[NUMBER_OF_SIDES]; ​ // AtomicInteger 배열을 초기화합니다.    IntStream.range(0, NUMBER_OF_SIDES) .forEach(i -> counts[i] = new AtomicInteger()); ​ // 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다. IntStream.range(0, numberOfThrows) .parallel() .limit(MAX_THREADS) // 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다. .forEach(i -> { // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다. int result = ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES); counts[result].incrementAndGet(); }); ​ // 각 눈의 개수를 배열로 변환하여 반환합니다. return IntStream.range(0, NUMBER_OF_SIDES) .map(i -> counts[i].get()) .toArray(); }병렬 처리 시 스레드 안전성을 보장하기 위해 AtomicInteger를 활용합니다.// 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다. AtomicInteger[] counts = new AtomicInteger[NUMBER_OF_SIDES];AtomicInteger는 Java에서 제공하는 스레드 안전한 int 자료형을 갖고 있는 Wrapping 클래스입니다. 멀티스레드 환경에서 동시성을 보장하는 AtomicInteger 클래스는 원자적으로 연산을 수행하여 스레드 간의 경쟁 조건(Race Condition)을 방지합니다.경쟁 조건(Race Condition)이란 여러 프로세스 / 스레드가 동시에 같은 데이터를 조작할 때 타이밍이나 접근 순서에 따라 결과가 달라질 수 있는 상황을 의미합니다.자바 동시성 그리고 AtomicInteger 클래스선수 지식이 필요한 내용입니다. 자바 기본 문법 책 혹은 강의를 통해 학습하면서 스레드에 대해 기본적인 이해를 갖고 있어야 합니다.동시성(Concurrency) 이란 한 CPU에서 동시에 여러 작업을 하는 것처럼 보이게 만드는 것입니다. 한 CPU에서 2개의 프로세스가 있다고 가정합니다. 이 둘은 엄청나게 짧은 시간에 컨텍스트 스위치가 일어나며 번갈아 실행됩니다. 그래서 사람이 볼 때 동시에 동작하는 것처럼 보이는데, 이것이 프로그래밍 세계에서의 동시성입니다.하나의 코어에서 여러 쓰레드가 번갈아가며 실행하는 성질을 의미실제로는 한 코어에서 한 쓰레드만 실행하고 있고 번갈아가면서 실행하고 있기에 동시에 실행되는 것처럼 보이는 것쓰레드 개수가 코어 수보다 많으면 쓰레드 스케줄링을 통해 쓰레드를 어떤 순서로 동시성으로 실행할지 결정하는 게 필요자바는 우선순위 방식, 순환할당 방식으로 쓰레드 스케줄링을 구현가시성(visibility)이란 값을 사용한 다음 블록을 빠져나가고 나면 다른 쓰레드가 변경된 값을 즉시 사용할 수 있게 해야 한다는 뜻입니다. 적절하게 동기화시키지 않으면 다른 쓰레드에서 최신 값이 아닌 예전 값을 읽게 됩니다. 따라서 가시성은 쓰레드가 항상 최신 값을 받아볼 수 있게 하는 성질입니다.원자성(Atomicity)이란 어떤 것이 원자성을 갖고 있다면 원자적이라고 합니다. 어떤 작업이 실행될 때 언제나 완전하게 진행돼 종료되거나 그럴 수 없는 경우 실행을 하지 않는 경우를 말합니다. 원자성을 갖는 작업은 실행되어 진행되다가 종료하지 않고 중간에서 멈추는 경우는 있을 수 없습니다. 따라서 원자성은 어떤 작업이 프로그램(소스코드) 안에서 가장 작은 단위라서 더 이상 다른 작업으로 나눌 수 없는 성질입니다.동시성 문제란?스레드는 cpu 작업의 한단위입니다. 여기서 멀티스레드 방식은 멀티태스킹을 하는 방식 중, 한 코어에서 여러 스레드를 이용해서 번갈아 작업을 처리하는 방식입니다. 멀티 스레드를 이용하면 공유하는 영역이 많아 프로세스방식보다 context switcing(작업전환) 오버헤드가 작아, 메모리 리소스가 상대적으로 적다는 장점이 있습니다. 하지만 자원을 공유해서 단점도 존재합니다. 그것이 바로, 동시성(concurrency) 문제입니다. 여러 스레드가 동시에 하나의 자원을 공유하고 있기 때문에 같은 자원을 두고 경쟁 상태(Race Condition) 같은 문제가 발생하는 것입니다.동시성 문제 해결의 세 가지 방법자바에서 동시성 문제를 해결하는데 3가지 방법이 있습니다.volatile 은 Thread1에서 쓰고, Thread2에서 읽는 경우만 동시성을 보장합니다. 두 개의 쓰레드에서 쓴다면 문제가 될 수 있습니다. synchronized를 쓰면 안전하게 동시성을 보장할 수 있습니다. 하지만 비용이 가장 큽니다. Atomic 클래스는 CAS(compare-and-swap)를 이용하여 동시성을 보장합니다. 여러 쓰레드에서 데이터를 write해도 문제가 없습니다. AtomicInteger는 synchronized 보다 적은 비용으로 동시성을 보장할 수 있습니다.AtomicInteger 객체 생성AtomicInteger 클래스는 동기화를 사용하거나 성능에 큰 영향을 주지 않고 동시에 다른 스레드에서 액세스하는 정수 카운터에 대한 스레드로부터 안전한 작업을 제공합니다. AtomicInteger 클래스에는 모두 스레드로부터 안전한 많은 유틸리티 메서드가 있습니다. [오라클 자바 8 공식 홈페이지](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicInteger.html)를 통해 자바 8에서 추가된 메서드도 함께 확인할 수 있습니다.다음은 AtomicInteger 객체 생성의 간단한 예제입니다.public void atomicInteger1() {    AtomicInteger atomic = new AtomicInteger();    System.out.println("value : " + atomic.get()); ​    AtomicInteger atomic2 = new AtomicInteger(10);    System.out.println("value : " + atomic2.get()); }// 결과 value : 0 value : 10 초기값은 0이며, 초기값을 변경하고 싶으면 인자로 int 변수를 전달하면 됩니다. AtomicInteger 메서드에 대한 사용 방법은 https://codechacha.com/ko/java-atomic-integer/ 블로그에서도 확인할 수 있습니다.AtomicInteger 배열을 생성하고 초기화하여 멀티 스레드 환경에서 안전하게 사용할 수 있도록 준비하는 역할을 합니다.// AtomicInteger 배열을 초기화합니다. IntStream.range(0, NUMBER_OF_SIDES) .forEach(i -> counts[i] = new AtomicInteger()); IntStream.range(0, NUMBER_OF_SIDES)은 0부터 NUMBER_OF_SIDES - 1까지의 정수 스트림을 생성합니다. 즉, 주사위의 면의 수(NUMBER_OF_SIDES)에 해당하는 횟수만큼 반복됩니다.forEach(i -> counts[i] = new AtomicInteger())는 각 인덱스(i)에 대해 AtomicInteger 객체를 생성하여 AtomicInteger 배열(counts)에 할당합니다. 이렇게 함으로써 AtomicInteger 배열이 초기화되고, 각 원소는 스레드 안전한 정수형 변수로 사용될 수 있습니다.병렬로 주사위를 던지고, 각 주사위 눈의 개수를 안전하게 카운트하는 작업을 수행합니다.// 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다. IntStream.range(0, numberOfThrows) .parallel() .limit(MAX_THREADS) // 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다. .forEach(i -> { // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다. int result = ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES); counts[result].incrementAndGet(); }); 주어진 횟수만큼 주사위를 병렬로 던지고, 각 주사위 눈의 개수를 계산하는 부분입니다.IntStream.range(0, numberOfThrows)은 0부터 numberOfThrows - 1까지의 정수 스트림을 생성합니다. 이는 주어진 횟수(numberOfThrows)만큼 반복됩니다.parallel() 메서드는 병렬 스트림을 생성합니다. 이를 통해 주어진 범위의 요소들이 병렬로 처리됩니다.limit(MAX_THREADS)는 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다. 이렇게 함으로써 너무 많은 스레드가 생성되는 것을 방지하고, 시스템 자원을 효율적으로 사용할 수 있습니다.forEach() 메서드는 각 요소에 대해 주어진 작업을 수행합니다. 여기서는 람다식으로 주사위를 던지고, 각 주사위 눈의 개수를 증가시키는 작업을 수행합니다.ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES)는 현재 스레드의 지역 랜덤 인스턴스에서 주어진 범위(NUMBER_OF_SIDES) 내에서 랜덤한 정수를 생성합니다. 즉, 0부터 NUMBER_OF_SIDES - 1까지의 랜덤한 숫자를 얻어옵니다.counts[result].incrementAndGet()는 주사위의 결과에 해당하는 인덱스(result)에 해당하는 AtomicInteger의 값을 1 증가시킵니다. 이를 통해 각 주사위 눈의 개수를 카운트할 수 있습니다.AtomicInteger 배열에 저장된 값을 가져와서 일반 정수 배열로 변환하여 반환하는 작업을 수행합니다.// 각 눈의 개수를 배열로 변환하여 반환합니다. return IntStream.range(0, NUMBER_OF_SIDES) .map(i -> counts[i].get()) .toArray();IntStream.range(0, NUMBER_OF_SIDES)는 0부터 NUMBER_OF_SIDES - 1까지의 정수 스트림을 생성합니다. 이는 주사위의 눈의 개수(NUMBER_OF_SIDES)만큼 반복됩니다.map(i -> counts[i].get())는 각 인덱스(i)에 해당하는 AtomicInteger의 값을 가져와서 매핑합니다. 즉, AtomicInteger 배열에 저장된 값들을 가져와서 일반 정수 값으로 변환합니다.toArray()는 스트림에 있는 요소들을 배열로 변환하여 반환합니다. 이렇게 함으로써 AtomicInteger 배열에 저장된 각 주사위 눈의 개수를 포함한 일반 정수 배열을 생성합니다.이렇게 생성된 배열은 주사위 눈의 개수를 나타내며, 이를 반환하여 주사위 눈의 통계를 완성합니다.주사위 통계 출력주어진 주사위 눈의 개수 배열을 받아서 각 눈의 개수를 형식에 맞게 출력하는 작업을 수행합니다.private static void printStatistics(int[] diceCounts) { // 각 눈의 개수를 출력합니다. IntStream.range(0, NUMBER_OF_SIDES) .forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i])); }IntStream.range(0, NUMBER_OF_SIDES)는 0부터 NUMBER_OF_SIDES - 1까지의 정수 스트림을 생성합니다. 즉, 주사위의 눈의 개수(NUMBER_OF_SIDES)만큼 반복됩니다.forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i]))는 각 인덱스(i)에 해당하는 주사위 눈의 개수를 가져와서 출력합니다. printf 메서드를 사용하여 주사위 눈의 번호와 해당하는 개수를 형식에 맞게 출력합니다. 여기서 i + 1은 눈의 번호를 1부터 시작하도록 보정하는 역할을 합니다.이렇게 각 눈의 개수를 출력하고, 마지막에는 개행 문자(\n)를 추가하여 줄을 바꿔줍니다.한 걸음 더!현재 코드는 주사위가 1부터6까지만 있다는 가정으로 작성되어 있습니다. 따라서 주사위가 1부터12까지 있거나 1부터20까지 있다면 코드를 많이 수정해야 합니다.위의 코드를 클린하게 개선해 보았다면, 주사위의 숫자 범위가 달라지더라도 코드를 적게 수정할 수 있도록 고민해 봅시다!import java.util.Scanner; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.IntStream; ​ public class DiceRollStatistics {    public static void main(String[] args) {        try (Scanner scanner = new Scanner(System.in)) {            System.out.print("주사위의 최소 숫자를 입력하세요: ");            int minNumber = scanner.nextInt(); ​            System.out.print("주사위의 최대 숫자를 입력하세요: ");            int maxNumber = scanner.nextInt(); ​            int numberOfSides = maxNumber - minNumber + 1; ​            // 사용자로부터 주사위를 던질 횟수를 입력받습니다.            System.out.print("주사위를 던질 횟수를 입력하세요: ");            int numberOfThrows = scanner.nextInt(); ​            // 주사위를 던져 각 눈의 개수를 계산합니다.            int[] diceCounts = getDiceCounts(numberOfThrows, numberOfSides); ​            // 통계를 출력합니다.            printStatistics(diceCounts);       }   } ​    private static int[] getDiceCounts(int numberOfThrows, int numberOfSides) {        // 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다.        AtomicInteger[] counts = new AtomicInteger[numberOfSides]; ​        // AtomicInteger 배열을 초기화합니다.        IntStream.range(0, numberOfSides)               .forEach(i -> counts[i] = new AtomicInteger()); ​        // 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다.        IntStream.range(0, numberOfThrows)               .parallel()               .forEach(i -> {                    // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다.                    int result = ThreadLocalRandom.current().nextInt(numberOfSides);                    counts[result].incrementAndGet();               }); ​        // 각 눈의 개수를 배열로 변환하여 반환합니다.        return IntStream.range(0, numberOfSides)               .map(i -> counts[i].get())               .toArray();   } ​    private static void printStatistics(int[] diceCounts) {        // 각 눈의 개수를 출력합니다.        IntStream.range(0, diceCounts.length)               .forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i]));   } }

백엔드JavaSpring

wisehero

<인프런 워밍업 스터디 클럽 0기> - BE 발자국 1주차

사실 강의 수강은 워밍업 클럽이 열리기 전부터 듣고 있었어요. 이미 JPA로 전환하는 부분까지 다 넘어갔었습니다. 사실 이것때문에 30% 할인 쿠폰을 미리 받지 못한 것에 대해서는 조금 아쉽긴 했습니다 ㅋㅋㅋ. 강의를 수강하게 된 이유는 저는 국비교육을 수료한 이후로 다른 정량적 스펙이나 코딩테스트, 자기소개서에 시간을 너무 많이 쓰게 되었고 중간에 인턴이나 현재 재직중인 직장에서는 스프링 부트나 스프링 5이상 버전, JDK 11~21 버전을 사용하지 않는 환경에 있었어요. 그래서 최근 개발 트렌드나 버전에 따른 변화에 뒤쳐지지 않기 위해 강의를 수강하기 시작했습니다. 1주차 동안은 주로 강의를 다시 듣는 것은 아니었고 이미 들었던 강의, 강의를 들으면서 작성했던 코드들을 다시 한번 보게 되었어요. 이 과정에서 JPA를 주로 사용하는 바람에 JdbcTemplate을 잘 사용해본 적이 없어서 이에 다시 적응하는 시간이 되어서 좋았습니다. 그리고 정말 레이어드 아키텍처에 따른 모든 코드들을 오랜만에 작성하면서 예전에 국비 교육때 열심히 했던 시간들을 다시 한번 되새길 수 있었고, 취업 준비를 하면서 많이 꺾였던 마음을 다시 세울 이유와 동기를 얻는 과정이어서 좋았습니다. 아쉬웠던 점은 시간이 그렇게 많지 않아, 과제 수행 중에 발견한 문제점이나 궁금했던 점에 대해 따로 깊이 파볼 시간이 조금 부족했던 것이 있습니다. 아무래도 2월부터 회사에 다니게 되었고 이 루틴이 익숙치 않아 오는 문제점인것 같은데 어떻게든 해결책을 마련해서 극복해야겠습니다. 워밍업 클럽을 진행하면서, 혹은 강의를 수강하면서 스스로에게 그나마 좋았다고 말해줄 수 있는 점은 질문을 적극적으로 하는 자세였습니다. 아마도 태현님 강의에서 질문을 제일 많이 한 것 같아요. 그리고 그 질문들중 좋은 질문이라고 반응해주셔서 행복했고, 태현님이 제 질문에 답변을 주신 것을 다른 분들의 질문에 대한 답변으로 대신하시는 것을 보고 '내가 의미있는 질문을 했구나'하는 생각을 했습니다. 세상에 멍청한 질문은 없다지만, 그 질문들 가운데서도 핵심을 짚는, 가치가 높은 질문들은 있다고 생각을 하는데 그런 질문을 하는 사람이 되어간다는 느낌을 받았습니다. 앞으로도 그런 질문을 계속 던질 수 있는 개발자가 되어야겠다고 다짐했던 좋은 경험이었습니다. 미션 수행과 관련해서...미션 수행은 저 말고도 다른 분들도 크게 어렵지 않게 해결하셨을거 같아요. 다만 저의 경우엔 몇 가지 아쉬운점이 있었어요. 어노테이션 관련해서 딥다이브를 하는 과정에서 과연 '딥'하게 들어갔는지에 대해는 의문이었어요. 다른 분들은 어노테이션이 '마법'을 일으키는 과정을 따라들어가보면 '리플렉션'이라는 개념이 등장하는데 제가 이 리플렉션 코드를 직접 짜보거나 하지는 않았거든요. 반성해야할 지점이었습니다. 단순히 개념적인 것, 글만 읽고 끝내는 공부를 또 반복하게 된듯한 느낌이었거든요.  나머지 미션들이 크게.. 특별히 어려웠던 점은 없었는데 코치님께서 남겨주신 4일차 과제 피드백을 듣고 다음에 비슷한 동작을 수행하는 코드를 작성할 때 더 좋은 코드를 작성할 수 있는 법을 배웠어요. 코치님이 남기신 피드백 내용은 다음과 같습니다.제가 이 피드백에서 교훈을 얻은 이유는 코치님이 언급하신, 데이터베이스에서 데이터를 전달해주고, 서버에서 연산 작업을 처리하게 되어 네트워크 대역폭 증가와 서버 자원 사용량의 증가라는 효과를 불러일으키는 방식으로 코드를 작성했기 때문이에요. 그렇다면 왜 그런 코드를 작성했을까요?사실 그냥 이 과제가 SQL 문제로 주어졌다면, 저는 아무런 고민없이 데이터베이스에서 바로 연산을 하는 SQL문을 바로 짤 수 있었을 거에요. 하지만 서버 애플리케이션 프로그래밍을 배우면서 이런 얘기를 들었어요. '데이터베이스에서 비즈니스 로직을 처리하게 되면 DB 종속적으로 프로그래밍을 하게 되어 서버 애플리케이션의 존재 의미가 흐릿해진다.'실제로도 현재 근무하고 있는 회사에서는 비즈니스 로직이 오라클 데이터베이스의 프로시져에 몽땅 때려박혀있는 구조이고 저는 이것을 지금 개선하고 있기 때문에 DB라는 것에서 어떤 처리를 최소한으로 하려고 하는 습관이 생겼어요. 그리고 여기에 더해 강의를 통해 자바 스트림을 적극적으로 사용하는 것을 보고, 스트림 처리를 적극적으로 사용하는 것이 간결하고 명확하며 멋져보여서 이를 적극적으로 사용하는 것이 머리 속을 지배했습니다.하지만 저는 과제의 요구사항이 '통계성 데이터를 반환하는 것'이라는 것을 잊고 있었고 이에 따른 트레이드 오프를 고민하는 자세를 갖지 못하고 바로 서버에서 스트림으로 연산을 처리해야겠다는 사고에 지배를 당해 아래와 같이 코드를 작성했습니다.@Transactional(readOnly = true) public List<Long> fruitStat(String name) { fruitRepository.findFruitsByName(name); List<Fruit> findFruits = fruitRepository.findFruitsByName(name); if (findFruits.size() == 0) { throw new IllegalArgumentException("해당 이름을 갖고 있는 과일이 없습니다."); } Long salesAmount = findFruits.stream().filter(Fruit::getSold).mapToLong(Fruit::getPrice).sum(); Long notSalesAmount = findFruits.stream().filter(fruit -> !fruit.getSold()).mapToLong(Fruit::getPrice).sum(); return List.of(salesAmount, notSalesAmount); } 이름 하나를 넘겨받고 그 이름과 동일한 이름을 가진 과일을 모두 가져오고, 팔린 물건과 그렇지 않은 물건을 따로따로 계산해주고 있습니다. 하지만 저 코드는 Fruit 테이블에 엄청나게 많은 데이터가 있었다면, 합을 구하는데 오랜 시간이 걸릴 수 있음이 분명했습니다. 통계성 데이터 처리는 그냥 한꺼번에 디비에서 해서 넘겨주는 것이 더 컴퓨팅 자원을 덜 소모할 수 있는 방법이라는 것을 분명 공부했지만 하나의 문제를 해결할 수 있는 방법을 여러개 놓고 그 중에 고른다기보다 저는 기존에 배웠던 것을 새로 배운 것으로 덮어쓰기 해버리는 바람에 트레이드 오프를 고려하는 습관을 유지하지 못한 부끄러움이 있었습니다. 그래서 우선 오늘은 자고 내일 개선해보자라고 생각했으나 마침 6일차 과제에 JPA가 아닌 JdbcTemplate을 사용할 것을 가정하고 나온 과제 내용을 다시 보고 개선할 수 있는 기회를 얻었습니다. 그래서 작성한 코드는 아래와 같습니다.동일한 작업을 모두 SQL로 작성하고 이를 DB에서 처리하게 했습니다. 이렇게 하고 단순히 응답을 맵으로 감싸서 넘기는 방식을 취하고 있죠. 만약 통계 결과를 얻기 위한 데이터가 엄청 많다면 이러한 방식이 더 효율적일 것 같습니다. 다음엔 좀 더 많은 임의의 데이터를 넣고 코드를 시험해봐야겠습니다. 감사합니다.

백엔드워밍업클럽백엔드최태현스프링

bananachacha

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

강의 출처https://inf.run/XKQg 1주차 학습 내용 요약 Day 1: 개발 환경 설정 및 네트워크 기초스프링 프로젝트를 만들고, 웹에서 데이터를 주고 받는 방법에 대해 이해했습니다.API를 만들기 전에 API 설계를 어떻게 하는지 배우고, 그것을 기반으로 스프링의 어노테이션을 사용하여 실제 API를 개발했습니다. Day 2: 첫 HTTP API 개발클라이언트로부터 JSON 형식의 데이터를 받아와서 처리하는지에 대한 기본 코드를 작성하면서, 실제 도서 관리 애플리케이션을 만들어보았습니다. Day 3: 기본적인 데이터베이스 사용법디스크와 메모리의 차이를 이해하고, 간단한 엑셀로 비유를 통해 MySQL이라는 데이터베이스를 어떻게 다루는지 학습했고, MySQL을 사용하여 데이터베이스를 조작하는 기초적인 CRUD 작업을 익혔습니다.또한, 스프링 서버에서 실제로 데이터베이스에 접근하도록 설정 파일을 작성하고 Jdbctemplate을 이용한 데이터베이스 연동을 학습했습니다. Day 4: 데이터베이스를 사용해 만드는 API요청 값의 검증과 예외 처리에 대한 로직을 작성하여 안정적으로 업데이트와 삭제 API를 개발했고, 스프링 서버가 데이터를 저장, 조회, 업데이트, 삭제할 수 있는 모든 기능을 구현했습니다. Day 5: 클린 코드의 개념과 첫 리팩토링클린 코드의 중요성과 코드를 깔끔하게 유지하기 위해 Layered Architecture를 도입하여 Controller, Service, Repository로 3단 분리하여 코드를 리팩토링하는 방법에 대해 배웠습니다.이를 통해 코드의 가독성과 유지보수성을 향상시키는 방법을 알게 되었습니다.  1주차 미션 Day1 어노테이션을 사용하는 이유(효과)는 무엇일까?나만의 어노테이션은 어떻게 만들 수 있을까? 어노테이션에 대해 학습하여 해당 질문에 대한 답변을 스스로 작성해볼 수 있는 미션이었습니다.평소에 스프링에서 어노테이션을 사용할 때 그냥 사용하면 되는구나 정도로만 알고 있었는데, 어노테이션의 기본 문법부터 커스텀 어노테이션까지 학습하면서 어노테이션의 역할과 장점을 이해할 수 있었습니다.보일러 플레이트 코드를 줄여주는 이점과 애플리케이션의 고유한 요구사항에 맞게 커스텀한 어노테이션의 사용이 개발 생산성에 도움을 줄 수 있다는 점도 알게 되었습니다.이제는 개념들을 단순히 사용하는 것이 아니라, 그 뒤에 숨은 이유와 장점을 고려하여 개발에 적용할 수 있게 되었습니다. 블로그https://velog.io/@awesomehill/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EA%B3%BC%EC%A0%9C  Day2 GET API와 POST API 개발하기 문제에 제시된 API Spec을 보고 API를 개발하는 미션이었습니다.먼저, GET API를 개발할 때 쿼리 파라미터의 처리 방법에 대해 고민하였습니다.쿼리 파라미터의 개수가 한 개인 경우에는 변수에 바로 바인딩하는 방식을 선택하였고, 여러 개의 쿼리 파라미터가 필요한 경우에는 Dto 클래스를 활용하여 요청을 매핑하는 방식을 선택했습니다.POST API를 개발할 때에는 Request Body로 전송되는 JSON 데이터를 받아오기 위해 Request Dto를 만들고, 이를 통해 데이터를 매핑하는 코드를 작성했습니다.이때, Java 14에서 추가된 record를 활용하여 Dto를 불변 객체로 만들어 코드의 안정성을 높였습니다.또한, Java 8 이후로 도입된 LocalDate 클래스를 사용하여 Date 클래스와의 차이점과 날짜 데이터를 처리하는 방법을 익힐 수 있었습니다. 블로그https://velog.io/@awesomehill/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%91%90-%EB%B2%88%EC%A7%B8-%EA%B3%BC%EC%A0%9C  Day3 자바의 람다식은 왜 등장했을까?람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까? 람다식이 등장한 이유와 익명 클래스와의 관계에 대해 알아보면서 해당 질문에 대한 답변을 생각할 수 있는 미션이었습니다.기존에는 익명 클래스를 사용하여 함수형 인터페이스를 구현하는데 많은 코드를 필요로 했으며, 이는 작은 작업을 수행하는 경우에도 코드의 양이 불필요하게 많아지는 문제를 야기했습니다.하지만, 람다식의 등장으로 이러한 불편함을 해소하고, 간단한 표현으로 함수형 인터페이스를 구현할 수 있게 되었습니다.람다식이 어떻게 코드를 간결하게 만들어주는지를 실제 코드를 통해 경험하면서 개념을 익힐 수 있었습니다. 블로그https://velog.io/@awesomehill/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%84%B8-%EB%B2%88%EC%A7%B8-%EA%B3%BC%EC%A0%9C  Day4 데이터베이스를 사용해 API 개발하기 JdbcTemplate을 활용하여 데이터베이스와 상호 작용하는 방법에 대해 깊게 학습했습니다.필요한 테이블을 생성하고 JdbcTemplate을 사용하여 SQL 쿼리를 실행하여 데이터를 다루는 방법을 익혔습니다. 예제를 따라가면서 HTTP 요청에 따라 데이터베이스에 새로운 데이터를 추가하거나 업데이트하는 코드를 작성하면서 JdbcTemplate의 실제 활용도를 체감할 수 있었습니다.테이블에 is_sold 칼럼을 추가하여 데이터의 판매 여부를 명시함으로써 데이터베이스의 일관성과 코드의 가독성을 향상할 수 있었습니다.그리고 문제 3에서 SUM이나 GROUP BY 등의 집계 함수에 대한 정확한 이해가 필요하다고 느껴, 틈틈히 SQL 공부를 병행해야 겠다는 생각이 들었습니다. 블로그https://velog.io/@awesomehill/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%84%A4-%EB%B2%88%EC%A7%B8-%EA%B3%BC%EC%A0%9C  Day5 클린 코드 개념을 적용하여 코드 리팩토링하기 클린 코드 작성하는 방법에 대해 찾아보고 과제에 제시된 코드를 리팩토링하는 미션이었습니다.코드를 입력, 실행, 출력 단계로 명확히 분리하여 가독성을 향상하여 Main 클래스에서 코드의 흐름을 쉽게 이해할 수 있도록 설계했습니다.코드를 읽는 개발자가 다른 개발자의 의도를 이해하는 데 도움이 될 수 있도록 메소드명과 변수명을 코드의 목적과 기능을 명확히 드러낼 수 있도록 작성했습니다.코드를 작게 만들어 가독성을 높이고, 재사용성을 증가시키기 위해 클래스와 메소드는 하나의 책임만을 갖도록 설계했습니다.상수를 활용하여 반복되는 문자열을 상수로 정의하여 코드의 일관성을 유지하고 오타를 방지했습니다.주사위를 나타내는 클래스에서 OOP 개념을 도입하여 유연성을 높이고, 나중에 주사위 범위를 변경해도 코드의 수정이 용이하도록 구조화했습니다.이번 미션을 통해 클린 코드 작성의 중요성과 가독성의 영향력을 체감할 수 있었습니다.코드를 작성하는 것뿐만 아니라, 코드를 읽는 것이 개발에 있어서 중요한 부분임을 깨닫게 되었습니다.  블로그https://velog.io/@awesomehill/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%8B%A4%EC%84%AF-%EB%B2%88%EC%A7%B8-%EA%B3%BC%EC%A0%9C  1주차 회고 프로젝트를 진행하면서 스프링 JPA에 대한 학습을 병행했는데, 조금은 힘든 도전이었지만, 그만큼 성취감과 만족도도 크게 느낄 수 있었습니다.또한, 인프런 스터디를 통해 다양한 주제에 대한 학습과 토론을 할 수 있어 지식의 폭을 넓히는 데에 도움이 되었습니다.여러 일을 병행하면서 힘들기도 했지만, 이러한 경험이 성장의 발판이 될 것이라고 생각합니다.앞으로도 끝까지 완주할 수 있도록 노력하겠습니다:)

웹 개발인프런워밍업스터디클럽발자국