블로그

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

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

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

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

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

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

윤대

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

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

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

망고123

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

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

백엔드SpringJava

망고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

망고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

JDK 동적 프록시 예제를 프록시 체이닝으로 구현...?

들어가기 전에김영한님의 스프링 핵심 원리 - 고급편 수업을 듣고 있던 중에 LogTraceBasicHandler에 필터링을 추가한다는 말을 듣고 이전 강의에서 말씀해주셨던 프록시 체이닝이 생각이 나서 "FilterHandler 이후에 LogTraceHandler 로 이어지는 프록시 체이닝을 보여주시려나" 보다 하고 있는데 그 둘을 합친 LogTraceFilterHandler을 생성하셔서 구현하시길래 "어라, JDK 동적 프록시는 프록시 체이닝으로 하기 까다로운가?" 라는 생각이 들었습니다.그렇다면 구현해보면 알 것 같았기 때문에, 한 번 저의 식으로 구현을 해보았습니다. 그리고 아래의 내용은 스프링 AOP와 CGLIB의 진도를 나가기 전에 작성되었습니다.목표강의 예제의 구조는 다음과 같습니다./** * JDK 동적 프록시 사용<br> * - {@link InvocationHandler} JDK 동적 프록시에 로직을 적용하기 위한 Handler<br> * - {@link PatternMatchUtils#simpleMatch}로 WhiteList 기반 URL 패턴 필터링 */ @Slf4j @RequiredArgsConstructor public class LogTraceFilterHandler implements InvocationHandler { private final Object target; private final LogTrace logTrace; private final String[] patterns; @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { // patterns 에 해당 메서드 이름이 없다면, 바로 목표로 이동 if (!PatternMatchUtils.simpleMatch(patterns, method.getName())) { return method.invoke(target, args); } // LogTrace 로직 실행 TraceStatus status = null; try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; status = logTrace.begin(message); Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } } 보면, 필터를 수행하는 로직과 로깅을 수행하는 로직이 하나의 메서드로 합쳐져 있습니다. 물론, 저 두개의 로직을 메서드로 따로 빼면 될 일이긴 합니다만… 저의 SRP 영혼이 슬프게 울고 있더군요.그리고 이전 수업 내용에 프록시의 장점 중 프록시 체이닝이란 것도 있기도 했고, Spring의 Filter도 이런식으로 구현되어 있을거니 나눠봤습니다.제가 구현하고자하는 내용은 아래와 같습니다.흐름만 봐서는 그냥 위의 코드와 동일하지만, 중요한 점은 필터를 담당하는 객체와 로깅을 담당하는 객체가 분리되었다는 것입니다. 그럼 구현을 한 번 해보죠.구현FilterHandler/** * 타겟의 메서드 이름을 필터링하는 Handler<br> * {@link PatternMatchUtils#simpleMatch}를 이용하여 패턴 검증<br> * - 해당 메서드의 이름이 패턴과 일치한다면: {@link #nextHandler}<br> * - 해당 메서드의 이름이 패턴과 일치하지 않는다면: {@link #target} * * @author MinyShrimp * @see Proxy * @see InvocationHandler * @see PatternMatchUtils#simpleMatch(String[], String) * @since 2023-03-02 */ public class FilterHandler implements InvocationHandler { private final Object target; private final Object nextHandler; private final String[] methodPatterns; /** * @param target 최종 목표 구현체 * @param nextHandler 다음 ProxyHandler * @param methodPatterns 필터링을 원하는 패턴 목록 - {@link PatternMatchUtils#simpleMatch} */ public FilterHandler( Object target, Object nextHandler, String[] methodPatterns ) { this.target = target; this.nextHandler = nextHandler; this.methodPatterns = methodPatterns; } @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { Object next = PatternMatchUtils.simpleMatch(methodPatterns, method.getName()) ? nextHandler : target; return method.invoke(next, args); } } 필터를 담당하는 프록시 핸들러입니다. 이 핸들러는 단순히 입력받은 패턴을 조사해 맞으면 다음 핸들러로, 맞지 않으면 바로 목표 구현체로 이동되도록 구현되었습니다.생성자를 보시면 알 수 있겠지만, 최종 목표 구현체(여기서는 OrderControllerV1Impl), 다음 프록시 핸들러(여기서는 LogTraceHandler), 그리고 패턴 패칭을 원하는 문자열 배열을 받습니다.이 예제와는 상관없지만, 개인적으로는 편의를 위해 생성자를 하나 더 만들어서 methodPattern이 배열이 아닌 하나의 문자열만 받을 수 있도록 구현해도 괜찮다고 생각이 듭니다. 하지만, 당장은 사용하지 않기 때문에 제거를 했습니다.그리고 final 맴버변수를 받기 위해 @RequiredArgsConstructor를 사용해도 괜찮지만, 그렇게 하게되면 위와 같이 주석을 남길 수 없기 때문에 사용하지 않았습니다.LogTraceHandler/** * Logging Handler<br> * {@link LogTrace}를 이용하여 로그 출력 * * @author MinyShrimp * @see Proxy * @see InvocationHandler * @see LogTrace * @see ThreadLocalLogTrace * @since 2023-03-02 */ public class LogTraceHandler implements InvocationHandler { private final Object target; private final LogTrace logTrace; /** * @param target 목표 구현체, 다음 ProxyHandler * @param logTrace {@link LogTrace} 구현체 */ public LogTraceHandler( Object target, LogTrace logTrace ) { this.target = target; this.logTrace = logTrace; } @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { TraceStatus status = null; try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; status = logTrace.begin(message); Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } } 로깅을 담당하는 프록시 핸들러입니다. 이 핸들러는 기존 강의에서 사용된 LogTrace를 받아 로그 메시지를 출력하는 역할을 수행합니다. 강의에서 제작한 LogTraceBasicHandler와 동일하기 때문에 설명을 생략합니다.다만, 정말 개인적인 아쉬움이긴 합니다만, 위의 TraceStatus를 받아옴에 있어서 단순히 저 Exception 하나 때문에 status 변수를 try 외부에 null로 선언하고 재할당 해주는 부분이 너무 아쉬웠습니다. 이를 해결하기 위해선 몇가지 방법이 있긴 합니다만, 이전 시간에 배운 ThreadLocal로 한 번 바꿔보겠습니다. ( 단순히 멤버 변수로 할당하면 동시성 문제가 발생합니다. )public class LogTraceHandler implements InvocationHandler { // 동시성 문제를 해결하기 위해 ThreadLocal 사용 private final ThreadLocal<TraceStatus> thStatus = new ThreadLocal<>(); // 중간 부분 생략 @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; TraceStatus status = logTrace.begin(message); // 여기서 생성 thStatus.set(status); // ThreadLocal에 저장 Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { TraceStatus status = thStatus.get(); // ThreadLocal에서 값을 가져옴 logTrace.exception(status, e); throw e; } finally { if (logTrace.isFirstLevel()) { thStatus.remove(); // 사용을 마치면 제거하자. } } } } 재할당하는 부분이 사라졌습니다! 이제 status를 받아오기 위해 ThreadLocal.get()을 사용하면 됩니다.그런데 여기서 주의사항이 있습니다. ThreadLocal의 사용이 끝나면 반드시 remove를 통해 지워줘야 합니다. 그래서 위와 같이 finally를 이용해 TraceId의 Level이 0인지 확인하고 0이면 remove를 하도록 작성해보았습니다. TraceId는 LogTrace가 가지고 있으니 넘겨주면 되겠군요.public class ThreadLocalLogTrace implements LogTrace { // 중간 생략 // LogTrace 인터페이스에도 추가해줍니다. @Override public boolean isFirstLevel() { return traceIdHolder.get().isFirstLevel(); // 그대로 넘겨줍니다. } } 좋습니다. 해결이 된 것 같군요. …과연 그럴까요? 이것 또한 버그가 있습니다.TraceId 는 ThreadLocalLogTrace에서 ThreadLocal로 잡고 있는 값입니다. 이것 또한 우리는 이전 시간에서 remove를 해주었습니다.public class ThreadLocalLogTrace implements LogTrace { // 중간 생략 /** * 이전 TraceID 로 전환<br> * - {@link #complete}에서 호출 */ private void releaseTraceId() { TraceId traceId = traceIdHolder.get(); if (traceId.isFirstLevel()) { traceIdHolder.remove(); // TraceId의 ThreadLocal을 제거한다. } else { traceIdHolder.set(traceId.createPreviousId()); } } } 겉보기에는 문제가 없어보입니다. 맞습니다. 평소에는 문제가 없습니다. 그런데 FirstLevel이 0이 되었을 때 문제가 발생합니다. 찬찬히 살펴보죠.위의 코드에서는 TraceStatus를 제거할 때는 finally에서 진행되고, TraceId를 제거할 때는 releaseTraceId에서 제거됩니다. 그리고 이 releaseTraceId는 end()와 exception()메서드에서 실행됩니다!즉, TraceStatus를 제거하기 위해 isFirstLevel() 메서드에서 traceIdHolder.get()을 사용하면, null을 리턴합니다. 그리고, null.isFirstLevel()은 NPE를 발생시킵니다. 그래서 아래와 같이 수정되어야 합니다.@Override public boolean isFirstLevel() { // releaseTraceId에서 제거되었다면 TraceId의 Level도 0이라는 소리. return traceIdHolder.get() == null; } DynamicProxyConfig/** * JDK 동적 {@link Proxy}를 스프링 빈으로 등록하기 위한 설정 파일 * * @author MinyShrimp * @see Proxy * @see FilterHandler * @see LogTraceHandler * @since 2023-03-02 */ @Configuration public class DynamicProxyConfig { /** * {@link FilterHandler}에서 사용하는 필터링 조건들, Whitelist 방식. */ private static final String[] METHOD_PATTERNS = { "request*", "order*", "save*" }; /** * @param target 최종 목표 구현체, 예) {@link OrderControllerV1Impl} * @param logTrace {@link LogTrace} * @return {@link FilterHandler} -> {@link LogTraceHandler} -> {@link OrderControllerV1Impl} */ private static Object filterLogProxyFactory( Object target, LogTrace logTrace ) { // 타겟이 상속받은 인터페이스들 중 첫 번째를 가져온다. // 해당 예제의 목표 타겟인 app.v1 들의 구현체들은 모두 인터페이스를 하나만 가지고 있기 때문에 가능하다. Class<?> superIntf = target.getClass().getInterfaces()[0]; // LogTraceProxy 생성 Object logTraceProxy = Proxy.newProxyInstance( superIntf.getClassLoader(), new Class[]{superIntf}, new LogTraceHandler(target, logTrace) ); // LogTraceProxy, 목표 타겟을 담은 FilterProxy 생성 return Proxy.newProxyInstance( superIntf.getClassLoader(), new Class[]{superIntf}, new FilterHandler(target, logTraceProxy, METHOD_PATTERNS) ); } /** * @return {@link OrderControllerV1Impl}의 Proxy */ @Bean OrderControllerV1 orderControllerV1(LogTrace logTrace) { OrderControllerV1Impl target = new OrderControllerV1Impl(orderServiceV1(logTrace)); return (OrderControllerV1) filterLogProxyFactory(target, logTrace); } /** * @return {@link OrderServiceV1Impl}의 Proxy */ @Bean OrderServiceV1 orderServiceV1(LogTrace logTrace) { OrderServiceV1 target = new OrderServiceV1Impl(orderRepositoryV1(logTrace)); return (OrderServiceV1) filterLogProxyFactory(target, logTrace); } /** * @return {@link OrderRepositoryV1Impl}의 Proxy */ @Bean OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) { OrderRepositoryV1Impl target = new OrderRepositoryV1Impl(); return (OrderRepositoryV1) filterLogProxyFactory(target, logTrace); } } 위에서 제작한 FilterHandler와 LogTraceHandler를 스프링 빈으로 등록하기 위한 설정 클래스입니다. 기본 베이스는 기존 강의에서 제작한 DynamicProxyFilterConfig와 동일합니다. 차이점이 있다면 각 빈에서 프록시 핸들러를 반환할때 발생하던 중복 코드를 filterLogProxyFactory() 메서드로 합쳐준 것 뿐입니다.그렇기 때문에 가장 중요한 filterLogProxyFactory()에 대해 설명하겠습니다./** * 이 메서드를 사용하기 위해선 FilterHandler와 LogTraceHandler는 * target의 첫 번째 인터페이스를 기반으로 프록시를 생성할 수 있어야 한다. * * @param target 1. 최종 목표 구현체, 예) {@link OrderControllerV1Impl} * @param logTrace {@link LogTrace} * @return {@link FilterHandler} -> {@link LogTraceHandler} -> {@link OrderControllerV1Impl} */ private static Object filterLogProxyFactory( Object target, LogTrace logTrace ) { // 2. 타겟이 상속받은 인터페이스들을 가져온다. Class<?>[] supIntfs = target.getClass().getInterfaces(); // 3. LogTraceProxy 생성 Object logTraceProxy = Proxy.newProxyInstance( supIntfs[0].getClassLoader(), supIntfs, new LogTraceHandler(target, logTrace) ); // 4. LogTraceProxy, 목표 타겟을 담은 FilterProxy 생성 return Proxy.newProxyInstance( supIntfs[0].getClassLoader(), supIntfs, new FilterHandler(target, logTraceProxy, METHOD_PATTERNS) ); } 이 메서드의 과정은 다음과 같습니다.타겟 객체(OrderControllerV1Impl)를 파라미터로 받아온다.타겟이 상속받은 인터페이스들의 정보(supIntfs)를 가져온다.그 정보를 이용해 LogTraceHandler의 프록시(logTraceProxy)를 생성한다.FilterHandler의 프록시를 생성하고 반환한다.기존 코드와 다른 점은 newProxyInstance()를 사용할 때, ClassLoader를 타겟의 첫 번째 인터페이스로 가져오는 것과, 두 번째 인자에 타겟의 모든 인터페이스들을 넣어 주는 부분입니다.이렇게 한 이유는 다름이 아니라, 이 메서드를 사용하는 모든 타겟 객체가 하나의 인터페이스만 상속받으며, 그 인터페이스를 이용해 핸들러들을 프록시로 만들어도 문제가 없기 때문입니다. 만약, 여러 개의 인터페이스를 상속받으며, 첫 번째 인터페이스를 기반으로 프록시를 생성하면 안되는 경우에는 위의 메서드를 사용할 수 없습니다. 그 때는 파라미터를 하나 추가해서 ClassLoader를 받아오면 됩니다.추가로 주의할 점은 FilterHandler 생성자에 LogTraceHandler를 주입한 것이 아닌, LogTraceProxy를 주입했다는 점입니다. LogTraceHandler도 invoke 함수가 제공되지만, 이는 Method의 invoke 함수와 무관하기 때문에 작동되지 않습니다. (예외가 발생합니다.)참고로, MainApplication에 해당 설정 파일을 등록하는 부분은 생략했습니다.완성…?이 구조를 완성했습니다. 구조 자체는 간단합니다만, 이쁘게 만들려다보니 신경써야할 부분이 좀 많았습니다.물론, 처음에 코드를 작성할 때 TraceStatus를 멤버 변수로 등록했다가 동시성 문제도 터지고, 위에서 설명한 finally에서도 NPE가 발생하기도 하고, FilterHandler에 LogTraceProxy를 넣어야하는데 LogTraceHandler를 넣어서 체이닝이 안되기도 했습니다만 구현해 놓고 보니 뿌듯합니다.하지만 아직 미심쩍은 부분과 아쉬운 부분이 보입니다.“ThreadLocal가 좋은건 알겠는데 이렇게 많이 사용해도 괜찮나…?”ThreadLocal의 가장 큰 주의점은 쓰레드 풀 환경에서 remove를 해주지 않는다면 다른 유저가 그 정보를 볼 수 있다는 점입니다. 그리고 ThreadLocalMap은 크기가 커지면 커질 수록 2배의 크기로 할당한다는 점입니다. 지금까지는 별 문제가 없어보이지만, 다른 프로그래머가 코드를 수정하여 remove가 실행이 안되게 되거나, 비즈니스 로직 변경으로 인해 급하게 수정하다가 remove를 놓치게 되면 위의 문제는 꽤 심각하게 발생합니다. 이때는 어떻게 대처를 해야하나요? 아니면 위 문제는 별로 신경쓰지 않아도 되나요?“지금처럼 핏한 상황은 잘 작동하지만, 이 코드를 확장하려면 고칠 부분이 많네…”꽤 만족할만한 코드 퀄리티라고 생각은 합니다만, 위의 코드를 재사용하여 확장해야 하는 상황이 온다면 고쳐야할 부분이 눈에 들어옵니다. 일례로, 목표 타겟의 인터페이스 상속이 늘어나고 순서가 바뀌면 위의 코드의 filterLogProxyFactory는 더이상 사용할 수 없습니다.그리고 Handler가 늘어나면 설정 파일에서 그에 맞게 주입을 먼저 해주어야합니다. 이는 전략 패턴의 단점과도 연결됩니다. 또한, FilterHandler 와 같이 분기점에 따른 Proxy 변경도 많아지게 되면 일반화를 진행해야합니다.(BranchHandlerFactory 와 비슷한 이름으로..)정리사실, 위의 로직들은 JDK 동적 프록시가 아닌 다른 방법으로 구현하는게 맞습니다. 스프링 MVC에서 배운 필터와 인터셉터도 있고, (저는 아직 진도를 안나갔습니다 만은)앞으로 배울 스프링 AOP가 해결 방법이 될 수도 있습니다. 그럼에도 이렇게 시간을 들여 글을 쓰는 이유는 다음과 같습니다.코드를 구현할 때 어떤 방식으로 구현하는지 생각을 정리하고 기록을 남기기 위함이었습니다.혼자서 코드를 작성하는 것은 언제나 자신과의 싸움을 하고 있다는 말과 같습니다. 보통 이런 상황에서 누군가에게 피드백을 받기란 요원한게 사실입니다. 그리고 그 기간이 길어지면 길어질수록 현재 자신의 위치가 어느 정도인지 짐작조차 할 수 없게 되고 다른 사람에게 물어볼때 어떤 방법으로 물어봐야하는지 모를 수 밖에 없습니다.“지금 짜고 있는 코드가 좋은 코드인가?” 과연 어떤 프로그래머가 이 생각을 하지 않겠냐만은, 혼자서 코드를 작성하면 정말 나쁜 코드를 작성하더라도 위에 대한 판단을 내릴 수가 없습니다. 또한, 나쁜 코드를 벗어나서 좋은 코드로 향하는 방법을 알고 싶어도 키워드를 모르니 방법이 없는 거지요.그래서 저의 코드 구현 방식을 공유하고 다른 분들에게 피드백을 받기 위해 이 글을 작성했습니다.현재 구현한 이 방법보다 더 좋은 방법이 무엇인지, 어떤 사이트 이펙트가 발생하는지, 내가 생각하고 있는 개념이 맞는지, JavaDoc 쓰는 방법은 올바른지, 등등 알고 싶은게 많습니다. (그러니까 비-법 소스 주세요!!!)꼭 긴글이 아니더라도 지나가는 말처럼 짧은 키워드만 툭툭 던져주셔도 저같이 공부하시는 분들에게는 많은 도움이 됩니다. 긴 글 읽어주셔서 감사드리며, 저와 같은 다른 취준생 여러분들도 다 같이 화이팅입니다. ^^7

백엔드JavaSpring김영한스프링핵심원리고급편JDK동적프록시프록시체이닝공유