블로그

도롱이

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

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

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

wisehero

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

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

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

잇택잇

[인프런 워밍업 클럽 스터디 BE] 세번째 발자국

 원본 : https://itaekit.tistory.com/15 들어가기 전에드디어 커리큘럼의 마지막 3주차를 지났다.과정이 시작할 쯔음에 이번 스터디를 통해 많이 배우고 성장하고 싶어했다.그리고 그걸 이룬 것 같아 뿌듯하다. 3주차에는 아래의 내용을 학습했다.JPA 연관관계AWS EC2를 활용한 배포Spring Boot 설정코드리뷰전부 처음 배우는 개념들이었지만 역시 쉽게 배울 수 있었다. 그동안 배운 내용을 실제 서비스의 형태로 배포하기까지의 과정을 배우며서버 개발자로 필요한 전반적인 지식들에 대한 기초와 자신감을 얻기 충분한 시간이었다. Day 10: 객체지향과 JPA 연관관계연관관계엔터티 간 관계를 정의하는 방법으테이블(엔터티) 간 객체지향적 모델링이 가능해진다. 1:1 (@OneToOne)한 엔터티가 하나의 엔터티만 연관된 경우에 해당한다. 1:N (@OneToMany)한 엔터티가 여러개의 엔터티와 연관된 경우에 해당한다. ex) 하나의 템 엔터티는 여러개의 멤버 엔터티를 가진다 N:M (@ManyToMany)여러 엔터티가 여러 엔터티와 연관된 경우에 해당한다. ex) 여러 학생은 여러 과목 엔터티를 가진다 연관관계는 항상 사용하는 것이 좋을까?연관관계를 통해 객체지향 모델링이 가능해지고,서비스 로직이 도메인 계층에 작성되어 Service코드가 간결해진다는 장점이 있다. 그러나,지나친 연관관계로 모든 엔터티 간 결합이 발생하면시스템을 파악하기 어려워지고코드를 수정할 때 다른 코드에 영향을 끼치게 되어 유지보수에 어려움을 줄 수 있다. 짧은 회고SQLD를 공부할 때 배웠던엔터티 간 관계를 적용할 수 있는 시간이었다.배웠던 개념이 실제 어떻게 구현되는지 확인할 수 있어 유익했다. 좋은 코드가 작성되기 위해 적절한 DB모델링이 수반되어야함을 깨달았다. Day 11: 기본적인 배포를 위한 준비배포 (Deployment)서비스를 만든 로컬의 모든 환경 및 소스코드를 서버 컴퓨터에 옮겨 실행하는 작업 Profile local, dev로 나누어 DB 적용  똑같은 코드를 실행시켜도 Profile 설정에 따라 실행시 설정을 다르게 할 수 있다. Git, GitHubGit은 버전 관리 소프트웨어로로컬에서 프로젝트의 형상을 관리한다. GitHub는 단순 원격 저장소로프로젝트 공유, 협업, 배포 등에 활용된다. 익숙하게 써온 툴인만큼,가볍게 점검할 수 있었다. AWS의 EC2AWS는 Amazon Web Service의 약자로,클라우드 기반의 다양한 웹 개발 서비스를 제공한다. EC2는 Elastic Compute Cloud의 약자로,탄력적으로 사용할 수 있는 클라우드 컴퓨터를 제공한다. '탄력적'이라는 뜻은,원격에서 언제든 생성하고 제거할 수 있어 붙은 뜻이다. EC2에서 컴퓨터를 할당받는 것을 인스턴스를 생성한다고 하는데,원하는 옵션에 맞춰 인스턴스를 생성한다. 실제 현업에서처럼,OS의 경우 Linux로 맞추는 것을 권장한다. 짧은 회고회고라고 할 것도 없이 날로 먹은 기분이다. Git, GitHub야 너무 익숙했고,AWS도 결국 플랫폼일뿐이라는 것을 알게되니 그동안 왜 무서워했던걸까싶었다. 결국 낯설어서 어려워하는 것 뿐,익숙해지면 별 거 아니다라는 사실을 오늘도 깨닫는다! Day 12: AWS EC2 배포Linux 환경에서 필요한 프로그램 설치서버 컴퓨터는 대부분 리눅스로 사용하는 만큼,리눅스에 대해 익숙해져야한다. 다행히(?) 리눅스마스터 자격증을 딸 때 공부해둔만큼,반가운 마음으로 리눅스와 재회할 수 있었다. Linux OS에서 Git, Java, MySQL을 설치해야하며아래의 블로그 글에 미리 정리해두었다. https://itaekit.tistory.com/14 AWS Linux 2023에서 Git, Java, MySQL 설치하기프로젝트 배포를 위해 AWS EC2를 사용하게 되었고,인스턴스 OS는 AWS Linux 2023으로 선택했다. 간단한 스프링 프로젝트 배포 및 실행을 위해 필요한 프로그램은 아래와 같다.GitJava 17MySQL 8.0각각의 설itaekit.tistory.com Linux 환경에서 build, 실행로컬에서는 Intellij를 통해,자동으로 빌드 및 실행할 수 있었다. 그러나,리눅스 환경에서는 개발자가 리눅스명령을 통해 빌드 및 실행을 해야한다. Build on Linuxchmod +x ./gradlew ./gradlew build -x test ./gradlew clean 빌드를 위한 gradlew를 실행하기 위해 가장 먼저 권한 설정을 해야한다.빌드 결과물을 지우기 위해서는 ./gradlew clean을 실행한다. Run on Linuxjava -jar {Path}/{파일명}.jar --spring.profiles.active={profile명} 빌드 결과물인 Artifact는build/libs안에 ~.jar의 형태로 생성되어있다. profile 지정까지!  java 명령어로 해당 jar를 실행하는 것으로 서버에서 애플리케이션을 동작시킬 수 있다. 짧은 회고리눅스 마스터 따두길 잘했다...리눅스가 이렇게 반갑고 재밌을 줄이야! "현업에서 서버 컴퓨터는 리눅스로 배포하니..."예전에 강의를 들을 때 이런 말을 들은 적이 몇 번 있었다. 그땐 뭐가 뭔지도 모르고 리눅스를 배웠었는데...이젠 잘 알겠다! Day 13: Spring Boot 설정이전에는 그냥 따라만했던 설정에 대해 조금 더 알아보는 시간이었다. build.gradlegradle(build tool)을 이용해 프로젝트 빌드 및 의존성 관리를 위해 작성하는 Build Script이다.빌드 스크립트에 작성하는 요소에 대해 하나씩 소개한다. plugins프로젝트에서 사용하는 플러그인 작성 group, version, sourceCompatibility프로젝트 그룹명, 버전, JDK 버전에 대한 작성version의 경우 빌드 결과물(.jar)의 확장명에 사용된다 repositories외부 라이브러리, 프레임워크를 가져오는 실제 원격 저장소를 작성한다. dependencies프로젝트에서 사용하는 외부 라이브러리, 프레임워크에 대해 작성하는 부분이다. Spring과 Spring Boot의 차이Spring에서 Spring Boot로 넘어오며 아래의 이점들을 갖게된다. 간편한 설정 : 복잡한 xml 지옥에서 벗어남쉬워진 의존성 관리강력한 확작성 : 다양한 starter를 지원, Tomcat(WAS)가 내장됨 application.yml (application.properties)스프링 설정 파일에 해당각각의 확장명에 따라 작성 포맷이 다름 짧은 회고모른채 따라하기 바빴던 설정하는 법도알게되니 별 거 아니였다! 사실 아무것도 모를 때 설정부터 배웠다면 감도 못잡았을텐데,어느정도 사용법에 익숙해지고나서 배우니 "아 그게 이거였구나~" 하면서 깨닫게된다. 무조건 모르면 머리 싸매고 공부하는 자세보다,일단 써보고 차차 알아가는 흐름으로 공부하는 자세를 좀 더 배워야할 것 같다. Day 14: 마무리강의 마지막날에는 조언과 꿀팁 소개 등 가벼운(?) 구성으로 진행됐다. 학습 방향성첫번째,스프링의 원리, 핵심 가치에 대해 학습이 필요하나,그 전에 OOP 및 디자인 패턴에 대한 학습이 필요하다. 스프링에서 제공하는 다양한 모듈들 학습하기 전에,CS 지식도 필요하다. 두번째,클린 코드, OOP, 테스트 코드 관련 학습이 필요하다.IT 대기업의 기술 블로그에 소개된 파일럿 프로젝트나 실습형 강의를 수강하는 것도 크게 도움이 된다. 세번째,배포에 대한 학습이 필요하다.배포 자동화에 대한 학습도 요구된다. 정리하자면,스프링에 대한 기본 사용법을 배우게 되었으니 계속 심화 학습을 할 수 있지만,본격적인 심화 학습에 앞서 필요한 기본 지식들을 빠르게 다져야할 것 같다. 짧은 회고독학으로 준비하고 있는 내게가장 어려운 것은 공부 그 자체보다 방향성이다. 그동안 너무나 많은 시행착오를 겪어왔기때문에,방향을 설정하는 것이 얼마나 중요한지 정말 잘 알고있다. 강사님의 조언덕분에,당분간의 스케쥴을 정할 수 있었다. 스프링 심화에 앞서 필요한 재료들을 빠르게 학습하고,본격적인 스프링 학습으로 뛰어들어야겠다. 네번째 Online Session: 프로젝트 코드리뷰마지막 온라인 세션이 진행됐다.이번 세션에서는 기다리고 기다리던 코드리뷰가 진행되었고나 포함 신청자 3명의 코드를 공개적으로 리뷰해주셨다. 칭찬받은 부분package 설계를 잘했다고 칭찬받았다.사실 설계를 의도했다기보다 강사님 어깨너머 봐왔던 코드를 최대한 따라하다보니 자연스레 나온 것 같다.역시 고수의 코드를 배우고 체득하는 것은 큰 공부가 된다. 지도받은 부분무척 감사하게도,대부분의 코드에 대해 지도를 해주셨다. @RequestMapping()에 직접 URL을 작성하기보다 web/ApiUrlConstants 클래스를 활용할 것 권장Spring에 내장된 Tomcat은 멀티쓰레드로 동작하므로 동시성 문제에 대해 고려 필요DTO 클래스를 만들 때 Record 클래스를 활용해 볼 것Query 요청수는 성능에 크리티컬하므로 N+1문제에 대해 해결 필요Entity 작성시 확장성을 위해 boolean(tinyint)로 설계하기보다 enum class를 활용해 볼 것후...단순히 기능만 구현해냈다고해서 만족했던 과거의 나 반성해라...!! 이제 정말 '좋은 코드'를 고민하고, 배워가야할 것 같다. 짧은 회고코드리뷰를 신청하기전만해도,기능 구현은 다 해놨기 때문에 자신있었다. 애초에 그렇게 어렵지 않은 기능들이었고,내가 작성한 코드보다 좋은 코드가 나와봤자 얼마나 나올까? 하는 궁금증도 있었다. 뚜껑을 까보니,사실상 모든 코드를 피드백받았다. 그리고 처음으로,'좋은 코드'를 생각하는 사람에게 내 코드를 보여주면서 부끄러움을 느끼기도 했다. 기능 구현은 당연히 해내야하는 것이었고,좋은 코드를 위해 필요한 CS지식, 코드 작성 방법 등에 대한 공부가 정말 많이 필요함을 느낄 수 있었다. 이번주 가장 유익한 시간이었다. 3주차 후기스프링 사용, API 작성으로 바빴던 1,2주차와 다르게3주차는 그동안 배운 내용 중 모르고 지나왔던 부분에 대한 보충,작성한 코드를 배포하기 위한 툴을 배웠다.일일 과제도 없어 학습 부담도 없었다. 역설적이게도,가장 유익했던 한 주였다. 그동안 내가 극복하지 못했던 학습 스타일을 고치게되는 계기가 되었고실제 서비스의 A-Z까지 맛보며 좋은 백엔드 개발자가 되기 위해 필요한 역량, 자세를 배울 수 있었다. 모든 강의와 라이브 세션이 끝났고,이제 최종 프로젝트만이 남았다. 수료의 기준은 모두 통과했지만코드리뷰받은 부분까지 다시 되새김질하며 프로젝트를 마무리할 계획이다. 3주간 정말 많이 배우고 성장했다!수고했다 나!!!

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

잇택잇

[인프런 워밍업 클럽 스터디 BE] 두번째 발자국

출처: https://itaekit.tistory.com/13 들어가기 전에2주차를 마무리하였다. 나름대로 1주차를 충실히 보낸건지,2주차는 조금 더 수월하게 따라갈 수 있었다. 2주차에는 아래의 내용을 학습했다.스프링 컨테이너스프링 빈Spring Data JPA트랜잭션1주차의 내용에 깊이를 더했고,조금 더 프레임워크스러운 내용을 배웠다. Day 06 : 스프링 컨테이너의 의미와 사용 방법여섯째 날,드디어 스프링의 동작 원리에 대해 기본적인 내용을 학습할 수 있었다. 코드를 프로그래머가 직접 제어하는 라이브러리와 달리프레임워크는 프로그래머의 코드가 사용되는 입장이다보니,그 안에서 돌아가는 방식이 정말 궁금했다. Spring Container스프링 컨테이너는 스프링 프레임워크의 핵심 중 하나로 다음의 역할을 수행한다.스프링 빈 관리의존성 주입AOP트랜잭션 관리DI, IoCDI(Dependency Injection)은 '의존성 주입'으로,의존성이 발생하는 클래스에 대해 스프링 빈으로 등록되어있는 경우 스프링에 의해 자동 주입된다. IoC(Inversion Of Control)은 '제어 역전'으로,사용되는 대상을 프로그래머가 아닌 프레임워크가 결정하는 개념이다.Spring Bean스프링 컨테이너에 의해 생성되고 관리되는 객체를 의미한다. Spring Bean 등록 방법스프링 빈으로 등록하기 위해서는 아래의 방법 중 하나를 선택한다. @RestController, @Service, @Repository이전까지 사용하던 방법으로 사용자가 정의한 클래스에 직접 붙여 스프링 빈으로 등록한다. @Configuration, @Bean @Configuration과 @Bean은 세트다.  주로 외부 라이브러리, 프레임워크에서 사용하는 방법이다. @Component 스프링 빈이 되기 위해서는 반.드.시. 가져야하는 어노테이션 특정 클래스를 컴포넌트(스프링 빈)로 취급하기 위한 어노테이션으로,컴포넌트는 스프링 컨테이너의 감지 대상이 된다. @RestController, @Service, @Repository, @Configuration 모두 내부적으로@Component를 가지고 있다. 컨트롤러, 서비스, 레포지토리 이외의 클래스를 추가적으로 스프링 빈으로 등록하기 위해 사용하는 방법이다. Spring Bean 주입 방법스프링 빈에 대한 의존성을 갖는 경우,스프링 컨테이너에 의해 자동으로 DI된다. DI를 처리하기 위해서는 다음과 같은 방법 중 하나를 선택할 수 있다. 생성자를 이용한 주입 (권장) 근본 가장 권장되는 방법으로,특정 스프링 빈을 필드로 갖게 하고 생성자를 작성한다. 참고로 스프링 3이전에는 생성자 위에 @Autowired를 명시해야 했다. setter + @Autowried를 이용한 주입 벌써 불편 @Autowired는 스프링 빈을 찾아 연결해야 함을 전달한다. setter를 이용하는 경우,누군가가 setter를 사용할 수 있어 나도 모르는 사이에 문제가 발생할 수 있는 코드다. 필드에 @Autowired를 이용한 주입 작성하기 편해도 나중에 ㅅ 필드에 직접 주입하는 방법으로테스트가 어려워져 권장하지 않는다. @Qualifier @Qualifier(Spring Bean 이름) 스프링 빈을 매핑하여 사용해야하는 경우 사용하는 어노테이션정상적인 IoC를 위해 사용된다. 과제 리뷰첫번째는,지난 과제에 대해 Layered Architecture에 맞게 리팩토링하는 과제였다. Layerd Architecture로 구조를 잡으니,확실히 코드의 역할이 분명해졌고 유연하게 확장할 수 있게됐다. 두번째는,하나의 인터페이스를 구현하고 있는 여러개의 레포지토리 클래스 중 특정 레포지토리를 사용하기 위한 방법을 묻는 과제였다. 오늘 배운 @Qualifire로 매핑할 수도 있고,@Primary로 지정할 수도 있다. 짧은 회고조금씩 로우레벨로 들어가다보니 확실히 재밌다.그동안 궁금했던 내용에 대해 조금씩 알게 되니 속이 다 시원! 그러나 알아야 할 것들이 너무나 많다는 사실이 조금 섭섭하다. 아직 스프링 빈도 몇 개 안되고,각 레이어에서도 클래스가 한 개 씩 있다보니 아키텍쳐 관점에서의 힘을 충분하게 체감하지 못한 것 같다. Day 07 : Spring Data JPA를 이용한 DB 조작이전까지는 레포지토리 스프링 빈으로 DB와 통신했다.직접 SQL문을 작성했다. 과연 이것이 좋은 코드일까? Java는 객체지향 언어로 절차지향의 코드에서 벗어나야한다.그러나 SQL문을 그대로 작성하는 것 역시 절차지향 관점에 더 가깝다고 볼 수 있다. 직접 SQL을 사용할 때 단점쿼리를 문자열로 작성하게 되어 런타임에서만 오류를 알 수 있음특정 DBMS에 종속적인 코드를 짜게 됨DB의 Table과 코드의 Object는 서로 다른 패러다임으로 이루어져 있어 객체지향 활용이 어려움 JPA (Java Persistence API)Java 진영의 ORM(Object-Relational Mapping)으로 Hibernate를 구현체로 한다. 참고로,Hibernate의 내부는 JDBC로 동작한다. Spring Data JPA는 복잡한 JPA를 한번 더 Wrapping한 라이브러리다. Entity DB의 User tableUser Entity JPA의 객체로 간주되는 클래스를 일컫는다.DB Table과 완벽 호환되어 사용된다. Spring Data JPA를 이용한 쿼리 날리기JPA를 도입한 이유는,코드 레벨에서 DB와 스프링을 객체지향 관점으로 작성하기 위함이다. JPA를 통해 더이상 SQL문을 직접 작성하지 않고도,동일한 기능을 수행할 수 있게 된다. JpaRepository를 상속하는 Repository 생성 @Repository를 붙이지 않아도 스프링 빈으로 등록되는 마법 해당 레포지토리 인터페이스는 스프링에 의해 빈으로 등록되어 객체화된다.프로그래머는 추상화된 인터페이스를 갖고 활용만하면 된다. 아아... Spring Data JPA 너란 녀석은...(1) 기본적으로 다양한 SQL문에 대응하는 여러 연산을 지원한다.  아아... Spring Data JPA 너란 녀석은...(2) 필요에 따라 인터페이스 안에 추상메서드로 선언하는 것으로 기능을 구현할 수 있다. 과제 리뷰역시 바로 이전 과제의 연장선이다. MySQL로 직접 연결하여 사용한 코드를JPA로 리팩토링하는 문제였다. 사실 SQL에도 자신이 있어 크게 불편하지는 않았지만JPA가 손에 익으면 더 편해질 것 같았다. 특히 레포지토리 인터페이스에 새로운 메서드를 작성하는 것으로 손쉽게 SQL문을 그대로 구현할 수 있었다.조금 더 객체지향 코드에 가까워졌다. 그러나,과연 JPA로 복잡한 SQL문까지 완벽히 대체하는걸까? 라는 의문이 들기도 했다. 나중에 알게 되겠지. 짧은 회고처음엔 분명 겁을 먹었다. JPA...?ORM...?Hibernate...? 익숙하지 않아서,들어본 적 없어서 지레 겁을 먹고 배우기를 미뤘던 내용들이었다. 결국에는 일맥상통하는 개념이었다. 아마도,새로운 것을 배워나감에 있어 이런 기분은 계속 이어질 것 같다. 무엇을 배우더라도열린 마음으로 적극적으로 배워나가다보면,결국 익숙해지고다 알게 되는 것 같다.   Day 08 : 트랜잭션과 영속성 컨텍스트SQLD를 취득한 내게 트랜잭션은 꽤 익숙한 개념이다.트랜잭션에 대한 개념을 다시금 확인하고,스프링에서는 트랜잭션을 어떻게 구현하는지 알아보자. Transaction트랜잭션은 쪼갤 수 없는 업무의 최소 단위를 의미한다. 예를 들어,A 계좌에서 B 계좌로 송금이 되었다.그러나 전산 오류로 B 계좌는 돈이 들어오지 않았다. 어쨌거나 돈은 보냈으니,이는 정상적으로 동작했다고 볼 수 있는가? 당연히 아니다.본래 송금이라는 것은 상대방 계좌에 돈이 안전하게 들어가는 것 까지 모든 동작이 이뤄져야 한다. 이렇게 쪼갤 수 없는 업무의 단위를 트랜잭션이라고 한다. @Transactional 점점 프레임워크의 매력에 빠져간다...이렇게 쉽게 해주다니... 스프링은 트랜잭션을 허무할만큼 간단하게 구현할 수 있다.트랜잭션으로 처리할 메서드에 @Transactional을 붙이는 것으로 구현한다. 메서드의 모든 로직이 성공적으로 수행되면 commit 처리되며,동작 중 예외가 발생하는 경우 rollback처리된다. 영속성 컨텍스트 (Persistence Context)트랜잭션 수행 중 Entity 객체를 관리, 보관하는 역할을 수행한다.트랜잭션이 수행될 때 자동으로 생성되며,트랜잭션이 종료되면 함께 종료된다. 영속성 컨텍스트의 특징Dirty Check : Entity 변경 사항을 자동으로 감지하여 저장 (별도의 save가 필요없음)쓰기 지연 : 모든 SQL 요청을 한번에 묶어서 전송하여 DB 통신으로 발생하는 오버헤드를 줄임1차 캐싱 : id를 기준으로 DB로부터 읽어들인 Entity 객체를 캐싱하여 효율적인 입출력 처리 짧은 회고실제 서비스를 위한 재료를 많이 얻어간 날이다.어렵지 않으나 유익한 시간이기도했다. 단순 API 작성에서,JPA를 사용한 DB 처리와 트랜잭션을 고려한 API 작성이 가능하게 됐다. 프레임워크의 강력함에 대해 나날이 느끼고 있다.프레임워크의 내부 동작에 대해서는 여전히 많은 공부가 필요한 것도 사실이다. 사용법만 아는 코더가 되기보다,원리와 해결법 모두 이해한 엔지니어가 되도록 노력하자! Day 09 : 조금 더 복잡한 기능을 API로 구성하기새롭게 배운 내용 없어 정리는 생략! (개꿀) 짧은 회고아홉번째 날은 이제껏 배운 모든 내용을 활용하여 API를 작성하는 연습을 가졌다. 역시 프로그래밍은 내가 직접 만들어갈 때가 제일 재밌다. 세번째 Online Session : Test Code, Refactoring세번째 온라인 미팅은 정기 미팅이 아닌 깜짝 미팅이었다.참여자는 적었지만 배운 내용은 그 어느 때 보다 많았다. 테스트 코드 작성하는 방법많은 채용공고 우대사항 중 "TDD를 사랑하는 사람"이라는 내용을 많이 봤는데아직 내겐 먼 내용이라 생각해 배울 생각도 하지 못한 개념에 대해 맛 볼 수 있었다. 너무나 당연하지만,테스트 코드란 실제 작성한 코드의 구현을 테스트 하는 코드이다. 테스트 코드를 작성함으로써 얻을 수 있는 이점은,나중에 실제 코드를 변경하게 되더라도 동일한 결과를 내는 동일한 로직인지를 쉽게 알아낼 수 있다는 점이다. 작성하는 방법은,필요한 의존성을 설치하고 @Test를 붙이는 것으로 끝이다...public class CalculatorTest { @Test public void addTest() { // given : 데이터 준비 Calculator calculator = new Calculator(); // when : 테스트 메서드 호출 int result = calculator.operate(1, 5, '+'); // then : 값 검증 (예외 테스트 경우 when과 then 통합) assertThat(result).isEqualTo(8); } } 물론 좋은 테스트 코드를 작성하는 것은 지금 수준에서는 벽처럼 느껴졌으나그렇게 어렵지만은 않은 내용이겠구나라고 느낌을 가질 수 있던 것 만으로도 큰 수확이었다. 리팩토링리팩토링이라 하면 이전의 코드와 동일한 기능을 수행하되코드의 가독성을 개선하는 작업이다. 좋은 리팩토링을 하기 위해서는좋은 테스트 코드를 작성함으로써 준비할 수 있다고 한다. (아직 와닿지 않음) 2주차 후기스프링 부트에 대해 익었다고 표현할 수 있을 것 같다. 처음부터 프로젝트 설정에 대해 반복 연습을 하다보니,설정 관련 문제로 막히지 않아 낯설게만 느껴진 프레임워크가 점점 익숙해져간다. API 작성은 계속 연습하고 있고,그동안 배웠지만 흩어졌던 지식들이 하나로 합쳐지면서,"배운 건 어떻게든 돌아오는구나" 라고 느꼈다. 과제도 미리 다 끝내 놓으니 부담도 없다!우수 러너에 선정될지는 모르겠지만... 이미 너무나 많은 것을 배워가고 있단 생각에 이 과정에 참여한 것이 정말 잘한 선택이라고 생각한다.  멘토님 말씀으로,오늘까지 기본적인 재료들은 다 배웠고,앞으로는 조금 더 객체지향스러운 코드를 짜는 법을 배운다고 하신다. '좋은 코드'에 대한 고민을 게을리하지말자.지금부터 확실하게 연습해두자!

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

wisehero

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

이번 3주차에는 3단계 까지 수행한 내용을 적어보고 프로젝트를 수행하면서 든 생각 등을 적어보겠습니다.전체 코드는 아래의 깃허브 주소에서 확인하실 수 있습니다.https://github.com/wisehero/warming-up_mini-project/tree/master/src/main/java/com/example/warmingup_miniproject  문제 1. 팀 등록 기능 / 직원 등록 기능 / 팀 조회 기능 문제 1번은 그다지 어렵지 않았습니다. 아래 팀과 직원이라는 1:N 관계를 모델링해주고 기능을 작성하면 됩니다. 팀 등록 기능의 경우엔 동일한 팀이름이 존재할 수는 없으므로 서비스 로직에 다음과 같은 로직만 추가해주었습니다.직원 등록 기능의 경우엔 한 가지 예외만 처리해주었습니다. 보통 팀(혹은 부서)에는 Manager의 역할을 하는 팀장이 1명입니다. 물론 예외도 있습니다만 여기서는 팀에 팀장은 한 명만 존재한다고 가정하고 다음과 같이 로직을 완성했습니다.우선 등록하려는 팀이 있는 팀인지 확인한다. 존재하지 않으면 관련 예외를 던진다.팀이 존재한다면, 역할이 Manager로 정해지지 않았는지 확인한다. 이미 해당 팀에 매니저가 있는 경우, 이미 팀장이 존재한다는 예외를 던진다.위 두 가지 케이스를 통과했다면 팀을 할당하고 저장팀 정보 조회는 아래와 같이 작성했습니다. 그저 조회 메소드이니 특별한 것은 없습니다.다만, 응답 DTO에 필드가 늘어난다면 map()에 파라미터로 전달되는 부분을 축소할 필요가 있어보이는데 이 부분은 리팩토링 해야할 부분으로 남겨두었습니다. 아래는 각 기능을 수행했을 때 정상적으로 작동하는 것을 보여주는 스크린샷입니다. 문제 2. 출근 기능 / 퇴근 기능 / 특정 직원의 날짜별 근무 기간을 조회하는 기능 이 기능들은 다음과 같은 엣지 케이스가 있을 수 있습니다.등록되지 않은 직원이 출근하려는 경우출근한 직원이 또 다시 출근하려는 경우퇴근하려는 직원이 출근하지 않았던 경우그 날, 출근했다 퇴근한 직원이 다시 출근하려는 경우저는 이 출근, 퇴근이라는 기능에서 날짜를 정하는 부분을 내부적으로 LocalDate.now()로 잡았는데요. 두 번째, 세 번째 케이스의 경우 오늘 날짜에 출근을 이미했거나 퇴근을 시도할 때 '오늘' 출근한 기록이 없으면 퇴근이 기록될 수 없게 했습니다. 물론 출근 기능에 문제가 생길 수 있으나, 논리적으로 출근을 하지 않았으니 당연히 퇴근 기록이 남는 것도 말이 안된다고 봤습니다. 또한 저는 오늘 근무한 시간을 퇴근을 기록하면서 출근시간과 퇴근시간의 차이를 구해 기록하는 식으로 남겼기 때문에 퇴근 날짜만 남겼을 경우 엉뚱한 값이 기록될 수 있어 이렇게 정했습니다. 가장 고민이 되었던 것은 4번째였는데요. 저는 이미 당일날 출근을 했으면 다시 출근이 기록될 수 없게 했기 때문에 만약 하루에 출-퇴근을 2번 찍었다면 2개의 행이 들어갈 수가 없게되었습니다. 그래서 우선은 임시방편으로 퇴근시간을 null로 바꾸었는데요. 이렇게 되면 하루 총 근무 시간이 초과 근무 시간을 넘게되는 일이 발생할 수 있는데 이 문제를 해결할 다른 정책?을 고민해봐야겠습니다. 문제 2번의 전체 코드는 아래와 같습니다.@Service @RequiredArgsConstructor @Transactional(readOnly = true) public class AttendanceService { private final AttendanceRepository attendanceRepository; private final EmployeeRepository employeeRepository; private final DayOffRepository dayOffRepository; @Transactional public void recordGoToWorkTime(Long employeeId) { // 등록되지 않은 직원이 출근할 수 없다. Employee employee = employeeRepository.findById(employeeId).orElseThrow(EmployeeDoesNotExistException::new); LocalDate today = LocalDate.now(); Optional<Attendance> attendedEmployee = attendanceRepository.findAttendedEmployee(employee.getId(), today); // 연차를 이미 사용한 날짜엔 출근을 기록할 수 없다. if (dayOffRepository.existsByEmployeeIdAndDayOffDate(employee.getId(), today)) { throw new AttendanceTodayIsDayOffException(); } if (attendedEmployee.isPresent()) { Attendance attendance = attendedEmployee.get(); // 당일 출근과 퇴근이 모두 기록되었다면 퇴근 시간을 null로 업데이트 if (attendance.getGetOffWorkTime() != null) { attendance.recordGetOffWorkTime(null); return; } // 이미 당일날 출근을 등록한 경우 예외 발생 throw new AttendanceAlreadyArrivedException(); } attendanceRepository.save(new Attendance(employee)); } @Transactional public void recordGetOffWorkTime(Long employeeId) { // 퇴근하려는 직원이 당일 출근하지 않았을 경우엔 ERR Attendance attendance = attendanceRepository.findAttendedEmployee( employeeId, LocalDate.now()).orElseThrow(AttendanceGetOffNotAvailableException::new); attendance.recordGetOffWorkTime(LocalDateTime.now()); attendance.recordWorkingMinutes(); } public ResponseAttendanceInfoByEmployee getAttendanceInfoByEmployee(Long employeeId, YearMonth date) { Employee findEmployee = employeeRepository.findById(employeeId).orElseThrow(EmployeeDoesNotExistException::new); LocalDate startOfMonth = date.atDay(1); LocalDate endOfMonth = date.atEndOfMonth(); List<Attendance> attendanceInfo = attendanceRepository.findAttendanceByEmployeeId(findEmployee.getId(), startOfMonth, endOfMonth); List<DayOff> dayOffTaken = dayOffRepository.findDayOffTakenByEmployeeAndMonth( findEmployee.getId(), startOfMonth, endOfMonth); List<AttendanceDetail> details = attendanceInfo.stream() .map(attendance -> new AttendanceDetail(attendance.getGetOffWorkTime().toLocalDate(), attendance.getWorkingMinutes(), false)) .collect(Collectors.toList()); // 연차 정보도 AttendanceDetail 형태로 변환하여 details 리스트에 추가 List<AttendanceDetail> dayOffDetails = dayOffTaken.stream() .map(dayOff -> new AttendanceDetail(dayOff.getDayOffDate(), 0L, true)) .toList(); details.addAll(dayOffDetails); details.sort(Comparator.comparing(AttendanceDetail::date)); Long sum = details.stream().mapToLong(AttendanceDetail::workingMinutes) .sum(); return new ResponseAttendanceInfoByEmployee(details, sum); } }  문제 3. 연차 신청 / 연차 조회 / 특정 직원의 날짜별 근무 시간 조회 여기서는 연차와 관련된 정보를 따로 관리하는 엔티티를 만들었는데요. 지금 생각해보면 조금 아쉬웠던? 결정이었습니다. 왜냐하면 근태관리 시스템에선 보통 연차/반차/조퇴/초과근무/정상출퇴근을 함께 관리하고 있는데 Attedance에 이를 구분하는 코드를 삽입하는 속성을 추가해서 함께 관리했으면 연차를 위한 엔티티를 따로 만들 필요가 없었던 것입니다. 근태라는것의 유형을 분류해서 로직을 구성했으면 오히려 좀 더 응집도가 있었을 수 있었는데 저는 그렇게 하지 못했습니다. 그래서 위에 보시면 직원의 근태정보를 가져오는 부분에서 레포지토리를 세개나 주입을 받고 있습니다. 이 부분은 추후에 한 번 개선을 해볼만한 지점입니다. 연차를 신청할 때는 다음의 예외를 다뤄줘야 했는데요. 연차 사용 횟수가 없는 경우엔 연차를 신청할 수가 없다.연차 사용일 - 연차 신청일이 팀에서 정한 기한보다 작은 경우엔 연차를 신청할 수 없다.이미 연차를 신청한 날짜에 또 다시 연차를 신청할 수 없다.연차를 사용한 날에는 출근이 기록될 수 없도록 출근 기능에서도 예외 핸들링을 해줘야 한다.따라서 연차 신청은 다음과 같은 코드로 수행했습니다.여러 케이스를 다루다보니, 코드가 좀 길어졌는데 따로 연차 신청이 유효한지 판단하는 private 메서드를 만들어서 빼주도록 리팩토링하는 시간을 가져봐야겠습니다. 남은 연차를 조회하는 경우에는 이 Employee에 dayOffRemains라는 정수 필드를 추가했습니다. 그래서 연차 사용이 정상적으로 마무리 되었으면 subtractDayOffRemain()이라는 함수를 호출해서 연차 사용 횟수를 1회 깎아주는 방식을 채택했습니다.  연차 정보까지 포함한 근태정보를 가져오는 로직에서는 고민을 좀 많이 했는데요. 로직을 보시면보시면 로직이 굉장히 깁니다. 정상 출퇴근 정보를 갖고 있는 Attedance 테이블에서 조회를 해오고, 연차정보를 조회해오고, 이를 다시 날짜순으로 정렬을 해주고, 근무 시간의 총합을 구하는 스트림을 돌고 있습니다. 연산을 굉장히 많이하게 되는데요. 사실 더 좋은 방법이 없을까..? 쿼리로 한 번에 가져올 수는 없을까? 생각을 했는데 처음에 든 생각은,  '음... 겨우 한 달짜리만 가져오는 거잖아?" 네 사실 조회하는 데이터나... 연산의 대상이되는 데이터의 크기가 크지 않습니다. 근데 또... 기간이 6개월이라면? 1년이라면...? 이라는 생각이 들기는 하는데 그래도 많은 양인가? 엄청난 통계성 데이터일만큼? 이라는 생각이 들어서 로직을 분리하는 것외에는 더이상의 리팩토링을 할 필요가 있을까 싶기도 하고... 또 로직이 단순해지려면 위에서 말했던 것처럼 그냥 연차를 Attendance에서 한번에 관리하는 것이 더 좋겠다는 생각이 들었습니다. 시간 관계상 프로젝트는 3단계까지 했지만 4단계까지 쭉 해볼 생각입니다. 이번 프로젝트를 하면서 변경에 유연한 설계를 할 수 있는지에 대한 시험대에 오른 것 같다는 생각이 들었습니다. 계속해서 리팩토링 해보고 더 많은 기능을 추가해보면서 실력을 갈고 닦아야겠습니다. 이런 좋은 과제를 준비해주신 태현 코치님에게 감사드립니다. 

백엔드백엔드최태현워밍업클럽

wisehero

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

2주차는 좀 많이 아쉬운 주였습니다. 몸이 아파서 학습에 온전히 시간을 쓰지 못했고, 연휴에도 골골대면서 누워있던 시간이 많았습니다. 그래도 중간중간 기운이 좀 나거나 상태가 괜찮아지면 다시 책상에 앉아 코드를 작성했는데요. 미니 프로젝트는 Java 21, 스프링부트 3버전을 사용하고 있습니다. Java 버전을 21로 선택한 이유는 추후 태현님께서 Java 21과 관련된 강의를 출시할 것이라고 예고를 해주셨고 후에 Java 21을 적용할 수 있는 부분을 찾아 연습을 해보기 위해 선택했습니다. 종종 인텔리제이로 코딩을 하면서, Java 최신 기능을 적용할 수 있는 부분은 ToolTip으로 알려주었던 것 같은데, 아직까진 이러한 부분을 찾질 못했습니다. 이번 미니 프로젝트를 진행하면서 새롭게 시도한 부분은 record를 좀 더 적극적으로 사용하는 것이었습니다. 레코드를 사용하게 되면 기존의 DTO 클래스를 작성하는 것보다 좀 더 간결하게 DTO의 목적 자체에 맞는 코드를 작성할 수 있고 롬복과 관련된 애노테이션들을 사용할 필요가 없다는 점이 큰 장점으로 다가왔습니다. 프로젝트는 현재 1단계는 다 완성이 되었고 2단계를 수행하고 있습니다. 한 걸음 더!에 나와있는 edge-case들을 다루는 내용을 해결하는 과정에서 큰 재미를 느끼고 있습니다. 실제 개발에서도 이러한 부분을 사전에 얼마나 잡아내느냐에 따라 제품의 퀄리티와도 직결될 것 같은데, 이런 케이스들을 사전에 고안해내고 핸들링하는 부분에 있어서 경쟁력을 가지고 싶다고 생각이 들었습니다. 현재는 코드리뷰어를 구하지 못해서 GPT에게 코드 리뷰를 대신 부탁해주고 있는 실정인데, 초기에 매우 지저분했던 서비스 레이어가 그래도 나름 깔끔해졌습니다. 물론 여전히 레포지토리에 선언한 쿼리메서드의 메서드명이 좀 긴 것은 고쳐야할 부분입니다. isAttendToday의 경우는 그래도 나름 빨리 적절한 메서드명을 떠올렸는데 퇴근 시간을 기록하는 메서드의 경우는 잘 떠오르지 않아 냅둔 상태네요. 현재 이 부분에서 이미 같은 날의 출근과 퇴근을 모두 마친 직원이 다시 출근을 하려고하는 경우를 핸들링 해야하는데 좋은 방법이 없을 지 생각 중입니다. 미니 프로젝트에서 남은 과제들을 보면, 변경에 유연하게 대처할 수 있는 코드를 작성할 수있고, 설계를 할 수 있는지에 대한 역량을 시험할 수 있는 것 같습니다. 그래서 엔티티에 필드하나를 추가하거나, 코드 한 줄, 한 줄에도 근거를 담아보려고 노력하고 있습니다. 3단계와 4단계가 정말 그 시험대가 될 것 같아요. 부디 다음 주 한주는 최상의 컨디션으로 과제와 강의 모두 잘 병행할 수 있기를 바랍니다.  

백엔드최태현백엔드워밍업클럽

wisehero

[워밍업 클럽 BE-0기] 7일차 과제 - JPA로의 전환, 그리고 더 다양한 API 만들

7일차 과제는 기존의 프로젝트를 JPA로 전환하고, 더 다양한 API를 만드는 것입니다.저는 이미 과제를 처음부터 JPA로 진행하고 있었으므로, 해당 부분을 생략하고 문제를 바로 보겠습니다. 첫 번째 문제는 다음의 스펙에 맞추어 API를 만드는 것입니다.HTTP method : GETHTTP path : /api/v1/fruit/countHTTP query : name -> 과일 이름HTTP 응답 Body 예시{ "count" : long } 코드는 다음과 같습니다!FruitControllerFruitServiceFruitRepository이름을 파라미터로 넘겨주면 그 이름과 일치하는 과일 엔티티들의 갯수를 반환하도록 했고요. 아래와 같이 요청을 보냈습니다.그리고 현재 제가 만든 Fruit 테이블의 상태는 아래와 같습니다. 사과가 3개 있으니 3개를 가져와야할 것입니다.정상적으로 3개가 출력되는 것을 확인할 수 있었습니다. 다음 문제는 팔리지 않은 과일들 중에서 특정 금액 이상, 혹은 특정 금액 이하의 과일 목록을 받아오는 것입니다.스펙은 아래와 같습니다HTTP method : GETHTTP path : /api/v1/fruit/listHTTP queryoption : "GTE" 혹은 "LTE"라는 문자열이 들어온다.GTE는 크거나 같다는 의미LTE는 작거나 같다는 의미price : 기준이 되는 금액이 들어온다.응답 Body 예시[{ "name" : String, "price" : long, "warehousingDate" : LocalDate, }, ... ] 저는 아래와 같이 코드를 작성했습니다.FruitControllerFruitServiceFruitRepository그리고 요청을 보내보았습니다.정상 작동을 확인했습니다.다만 아쉬운 점이 서비스레이어에서 엔티티를 DTO로 변환을 하는 과정인데.. 이것을 그냥 레포지토리에서 DTO로 조회를 하는 방식이 더 나을 것 같다는 생각이 들었습니다. 하지만 이렇게 되면 쿼리 애노테이션을 달아주어 SQL을 작성해줘야하는데 레포지토리에 있는 코드들이 길어질 것 같았습니다. 어떤 방식이 더 선호되는 지는 공부를 해보고 개선해봐야겠습니다. 저 변환을 하는 작업이, 필드가 늘면 늘수록 중간 연산의 코드가 점점 더 길어지게 되는 단점이 있다고 생각이 들었거든요. 그럼 오늘도 봐주셔서 감사드리고 모든 워밍업 클러버들의 무운을 빕니다.

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

wisehero

[워밍업 클럽 BE-0기] 6일차 과제 - 레이어 나누기와 @Primary 사용해보기

오늘 과제는 Controller 코드를 3 계층으로 나누는 것과 FruitMemoryReposiotry와 FruitMySqlRepository라는 두 개의 구현체 중에 하나만을 주입받아 사용하도록 하는 것이었습니다. 하지만 저는 이전의 과제에서부터 계층을 나누어서 코드를 작성했기 때문에 그 부분은 생략하고 Repository쪽의 코드를 확인해보겠습니다. 처음부터 JPA를 사용했으므로, 새로운 코드가 필요했습니다. 따라서 아래와 같이 작성했습니다. FruitRepositorypublic interface FruitRepository { void save(FruitCreateRequest request); void updateStatus(Long id); Map<String, Long> statForFruit(String name); } FruitMySqlRepository@Repository @Primary public class FruitMysqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void save(FruitCreateRequest request) { String sql = "insert into fruit (name, warehousing_date, price) values(?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice()); } @Override public void updateStatus(Long id) { String sql = "update fruit set is_sold = CASE WHEN is_sold = 0 THEN 1 ELSE 0 END WHERE id = ?"; jdbcTemplate.update(sql, id); } @Override public Map<String, Long> statForFruit(String name) { String sql = "SELECT \n" + " SUM(CASE WHEN is_sold = 1 THEN price ELSE 0 END) AS salesAmount,\n" + " SUM(CASE WHEN is_sold = 0 THEN price ELSE 0 END) AS notSalesAmount\n" + "FROM\n" + " fruit\n" + "WHERE \n" + " name = ?;"; Map<String, Object> result = jdbcTemplate.queryForMap(sql, name); Map<String, Long> stats = new HashMap<>(); stats.put("salesAmount", ((Number)result.get("salesAmount")).longValue()); stats.put("notSalesAmount", ((Number)result.get("notSalesAmount")).longValue()); return stats; } } FruitMemoryRepository@Repository public class FruitMemoryRepository implements FruitRepository { private List<Fruit> fruits = new ArrayList<>(); private Long id = 1L; @Override public void save(FruitCreateRequest request) { Fruit fruit = new Fruit(id++, request.getName(), request.getWarehousingDate(), request.getPrice()); fruits.add(fruit); System.out.println(fruits.size()); } @Override public void updateStatus(Long id) { Fruit fruit = fruits.stream().filter(f -> Objects.equals(f.getId(), id)) .findFirst().orElseThrow(IllegalArgumentException::new); fruit.changeStatus(); } @Override public Map<String, Long> statForFruit(String name) { List<Fruit> fruitsByName = fruits.stream() .filter(f -> f.getName().equals(name)).collect(Collectors.toList()); Long salesAmount = fruitsByName.stream().filter(Fruit::getSold) .mapToLong(Fruit::getPrice).sum(); Long notSalesAmount = fruitsByName.stream().filter(f -> !f.getSold()) .mapToLong(Fruit::getPrice).sum(); HashMap<String, Long> result = new HashMap<>(); result.put("salesAmount", salesAmount); result.put("notSalesAmount", notSalesAmount); return result; } }저는 현재 FruitMySqlRepository에 @Primary 어노테이션을 달아주었습니다. 그러면 과연, 스프링 컨테이너가 띄워졌을 때, FruitMySqlRepository가 구현체로 등록되었는 지 확인해보겠습니다. 이를 위해 아래와 같은 코드를 사용했습니다.@SpringBootApplication public class LibraryAppApplication { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(LibraryAppApplication.class, args); FruitRepository bean = applicationContext.getBean(FruitRepository.class); System.out.println("주입된 FruitRepository의 구현체: " + bean.getClass().getSimpleName()); } }스프링 부트 애플리케이션을 실행하고 난 뒤 FruitRepository에 주입되어있는 빈의 이름을 가져오게 했는데요. 실행 결과는 아래와 같습니다.FruitMySqlRepository가 잘 주입된 것을 확인할 수 있습니다. 다시 FruitMemoryRepository에 @Primary 를 달아주면!역시 잘 확인할 수 있었네요. 그런데... 저 뒤에 EnhancerBySpringCGLIB~~ 라는 놈은 무엇일까요?더 깊게 알아보아야할 것을 얻은 것 같습니다. 6일차 과제 이상입니다.

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

wisehero

[워밍업 클럽 BE-0기 백엔드] 과제 5일차 - 코드 리팩토링 과제

오늘은 주사위를 던져서 숫자별로 나온 횟수를 기록하고 결과를 출력하는 절차지향적으로 작성된 코드를 리팩토링 해야합니다. 절차지향적으로 짠 코드가 모두 나쁜 것은 아니지만, 이번 스터디에서 우리는 객체지향 패러다임에 근거해 등장한 자바라는 언어와 그 언어가 가진 패러다임을 극대화한 스프링이라는 프레임워크를 공부하고 있으므로 객체지향적으로 깔끔하게 코드를 정리할 수 있는 역량을 길러야 합니다.  주어진 코드는 아래와 같습니다.public class Assignment { public static void main(String[] args) { // 입력을 받고 받은 입력을 저장하는 역할 역할 System.out.println("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int a = scanner.nextInt(); // 주사위 숫자를 담는 역할 int r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0; // 나온 숫자를 세는 역할 for (int i = 0; i < a; i++) { double b = Math.random() * 6; if (b >= 0 && b < 1) { r1++; } else if (b >= 1 && b < 2) { r2++; } else if (b >= 2 && b < 3) { r3++; } else if (b >= 3 && b < 4) { r4++; } else if (b >= 4 && b < 5) { r5++; } else if (b >= 5 && b < 6) { r6++; } } // 결과를 출력하는 역할 System.out.printf("1은 %d번 나왔습니다.\n", r1); System.out.printf("1은 %d번 나왔습니다.\n", r2); System.out.printf("1은 %d번 나왔습니다.\n", r3); System.out.printf("1은 %d번 나왔습니다.\n", r4); System.out.printf("1은 %d번 나왔습니다.\n", r5); System.out.printf("1은 %d번 나왔습니다.\n", r6); } }객체지향적으로 코드를 리팩토링하기 위해서는 여러 명령문들이 어떠한 역할을 하고있는지 구분하는 것이 중요한 것 같습니다. 역할을 구분해야 역할과 책임에 따른 클래스, 메소드 단위로 구분하기가 쉬워지기 때문입니다. 저는 우선 두 개의 클래스를 사용하려고 합니다. 단순히 주사위를 굴리고 결과를 저장하고, 결과를 출력하는 책임을 갖고 있는 메서드를 가진 클래스그리고 그 클래스를 실행하는 클래스 본디 자바의 main은 프로그램 실행의 진입점으로서 반드시 어딘가에 만들어져 있어야 합니다. 그리고 우리는 스프링 부트를 켜서 기본으로 생성되어있는 클래스를 보시면@SpringBootApplication public class LibraryAppApplication { public static void main(String[] args) { SpringApplication.run(LibraryAppApplication.class, args); } }이렇게 단순히 스프링 애플리케이션을 실행하는 main메서드와 명렁문만이 있습니다. 그래서, 저는 아래처럼 만들었습니다.public class DiceApplication { public static void main(String[] args) { DiceRoller diceRoller = new DiceRoller(); diceRoller.rollDice(); diceRoller.printResult(); } }이 클래스에선 단순히 하나의 일만 합니다. DiceRoller 즉, 주사위를 굴리고 그에 따른 결과를 출력하는 책임을 갖고 있는 클래스를 생성하고, 일을 시키기만 합니다! 저는 예전에 유튜브에서 TDA, Tell, Don't Ask(요청하지말고, 시켜라) 라는 개념을 본 적이 있었는데 그 원칙에 최대한 맞춰보기 위해 위와 같이 작성했습니다. 그리고 DiceRoller라는 클래스는 아래와 같이 작성했습니다.public class DiceRoller { private int[] results = new int[6]; public void rollDice() { System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int rolls = scanner.nextInt(); generateRandomRolls(rolls); } private void generateRandomRolls(int rolls) { for (int i = 0; i < rolls; i++) { int result = (int) (Math.random() * 6); // 0 to 5 results[result]++; } } public void printResults() { for (int i = 0; i < results.length; i++) { System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, results[i]); } } }이 클래스는 초기 주사위 숫자 크기와 결과를 담고있는 배열을 선언하고 있구요. 다음 3개의 메서드를 갖고 있습니다.숫자를 입력받고 입력받은 숫자만큼 주사위를 굴리도록 '시키는' 메서드넘겨받은 숫자만큼 주사위를 굴리고 기록하는 메서드그리고 그 결과를 출력하는 메서드.물론 여기서 핵심 로직은 주사위를 굴리고 결과를 기록하는 것입니다. 더 나누려면 나눌 수 있습니다. 입출력 부분을 함께 수행하는 클래스로 말이죠. 하지만 여기서는 사이즈가 그렇게 크지 않으므로 입출력 및, 주사위 굴리고 결과를 기록하는 메서드를 한 클래스안에 두었습니다. 그리고 Math.random()은 그 결과를 double로 반환하는 까닭에 기존의 코드는 if (b >= 0 && b < 1) { r1++; } else if (b >= 1 && b < 2) { r2++; } else if (b >= 2 && b < 3) { r3++; } else if (b >= 3 && b < 4) { r4++; } else if (b >= 4 && b < 5) { r5++; } else if (b >= 5 && b < 6) { r6++; }이렇게 지저분하게 조건 분기를 해줘야 했죠. 하지만 주사위 굴리기의 결과는 늘 정수이기에 형변환 코드를 넣어줌으로써 저러한 조건 분기를 지울 수 있게 되었습니다. 그리고 입력은 보통 한 줄에서 받는 것이 보기 좋으므로 입력을 받는 부분의 System.out.println()을 System.out.print()로 바꾸어 주었습니다. 그러면 한 걸음 더! 에 있는 문제를 해결해봅시다. 지금 저는 주사위의 숫자가 1부터 6까지 있다고만 가정했습니다. 하지만, 어느날 주사위라는 것 자체가 규격이 바뀌었다고 가정한다면 제가 작성한 코드는 오작동하는 코드입니다. 왜냐면 private int[] results = new int[6]; int result = (int) (Math.random() * 6)이렇게 배열 크기를 주사위가 1부터 6까지 있다는 가정하에 직접 넣어주었기 때문입니다. 그래서 저는 필드에서 선언한 즉시 초기화를 하기보다는, 생성자로 초기화하는 것을 선택했습니다. 그러면 코드는 아래와 같이 바뀔 수 있습니다.public class DiceApplication { public static void main(String[] args) { DiceRoller diceRoller = DiceRoller.makeDice(); diceRoller.rollDice(); diceRoller.printResults(); } } class DiceRoller { private int[] results; private int sides; public DiceRoller(int sides) { this.sides = sides; this.results = new int[sides]; } public static DiceRoller makeDice() { Scanner scanner = new Scanner(System.in); System.out.print("주사위 면의 수를 입력하세요 : "); int sides = scanner.nextInt(); return new DiceRoller(sides); } public void rollDice() { System.out.print("굴릴 횟수를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int rolls = scanner.nextInt(); generateRandomRolls(rolls); } private void generateRandomRolls(int rolls) { for (int i = 0; i < rolls; i++) { int result = (int)(Math.random() * sides); results[result]++; } } public void printResults() { for (int i = 0; i < sides; i++) { System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, results[i]); } } }저는 스태틱 메서드를 사용해서 DiceRoller 객체를 생성하도록 하고 그렇게 생성된 주사위를 굴리고, 결과를 출력하게 끔 했습니다. 실행 결과는 아래와 같습니다. 어떤가요? 많이 클린해졌나요? 누군가는 '작성해야 하는 코드 양이 더 많아졌는데?' 라고 생각하실 순 있겠지만 내부 동작과 어떤 흐름으로 프로그램이 실행되는 지에 대한 '명확성' 부분에서 저는 좀 더 나아졌다고 생각합니다. 저의 리팩토링이 어떠했는지에 대해서 냉철한 평가를 내려주셔도 좋고 조금 더 개선할 부분이 있다면 알려주세요! 감사합니다!

백엔드백엔드최태현자바스프링

wisehero

워밍업 클럽 BE-0기 2일차 과제

오늘은 개념적인 것들을 주로 알아보는 시간이었던 1일차와는 달리 직접 문제에 적인 예제 코드를 작성하는 시간이었습니다.첫 번째 문제는 다음과 같습니다.문제 1 : 두 수를 입력하고, 다음과 같은 결과가 나오는 GET API를 만들어보자!path: /api/v1/calc쿼리 파라미터 : num1, num2{ "add" : 덧셈결과, "minus" : 뺄셈결과, "multiply": 곱셈결과 }쿼리 파라미터로 값을 넘기므로, 최종적으로 요청은 localhost:8080/api/v1/calc?num1=X&num2=Y와 같이나올 것 입니다. 방법은 여러가지가 있습니다. @RequestParam을 사용할 수도 있고 DTO 객체로 파라미터를 받아서 해결할 수 있습니다. 제가 쓴 코드는 다음과 같습니다.code@RestController public class AssignmentTwoProblemOneController { @GetMapping("/api/v1/calc") public CalculateResponse CalculateTwoNumbers(CalculateRequest request) { int add = request.getNumber1() + request.getNumber2(); int minus = request.getNumber1() - request.getNumber2(); int multiply = request.getNumber1() * request.getNumber2(); return new CalculateResponse(add, minus, multiply); } }저는 DTO 객체로 number1과 number2를 받았고 이를 응답하는 CalculateResponse 객체에 담아서 응답했습니다. 그리고 이렇게 요청을 보내면이렇게 결과를 잘 받아올 수 있는 것을 확인할 수 있습니다.하지만 이렇게 DTO 객체로 파라미터를 받지 않고 @RequestParam을 사용해서 받을 수 있습니다. 그리고 저렇게 DTO 객체를 받는 경우엔 객체 앞에 @ModelAttribute를 선언해주는 것이 좋다고 합니다. 선언해주고 선언해주지 않고에 따른 동작의 차이가 큰 것은 아니지만, 코드를 읽는 사람으로 하여금 파라미터를 받는 메서드라는 것을 명확하게 알려주기 때문입니다. 또한 @ModelAttribute를 사용하면 좀 더 세밀하게 데이터 바인딩을 할 수 있습니다. 문제 2 : 날짜를 입력하면, 몇 요일인지 알려주는 GET API를 만들어 보자!path: /api/v1/day-of-the-week?date=2023-01-01{ "dayOfTheWeek" : "MON" }우선 이 문제는 '시간'을 다루지는 않으므로 LocalDateTime이나 LocalTime이 아닌 LocalDate를 사용해야 합니다.이 문제에서는 처음에 해당 요청을 처리할 메서드에 선언한 LocalDate타입의 변수가 Spring의 강력한 ArgumentResolver가 알아서 변환해주지 않을까....? 싶었지만 어림도 없었습니다. LocalDate에 대해서 자세히 알아보라는 코치님의 조언이 있었는데 마침 내일 과제가 자바 8에 추가된 내용들을 공부하는 시간이므로 내일 다뤄보도록 하겠습니다.@RestController public class AssignmentTwoProblemTwoController { @GetMapping("/api/v1/day-of-week") public DayOfWeekResponse letMeKnowDayOfWeek(String date) { LocalDate parsed = LocalDate.parse(date); return new DayOfWeekResponse(String.valueOf(parsed.getDayOfWeek()).substring(0, 3)); } }우선 파라미터는 ISO-8601 형식의 문자열로 넘어옵니다. ISO-8601은 날짜와 시간 정보를 표현하는 국제 표준 형식입니다. 이 표준에서 날짜는 '연-월-일' 순서로 나타내집니다. 자바의 LocalDate 클래스는 이를 따르므로 파싱을 의미하는 parsed 메서드를 사용하면 문자열로 넘어온 '연-월-일'을 LocalDate 객체로 자동으로 바꿔줍니다.(LocalDate 클래스에 들어가보면 ISO-8601을 따른다고 적혀 있습니다.)그리고 그렇게 넘어온 LocalDate 타입의 객체에서 사용할 수 있는 getDayOfWeek() 메서드를 사용하면 해당 날짜의 요일을 Enum 타입으로 반환합니다. 문제 요구사항에서는 요일의 3번째 글자까지 반환할 것을 요구하고 있으므로, 문자열로 변환한 후에 substring 메서드를 사용했습니다.그리고 오늘 날짜로 요청을 보내고 나면!이렇게 결과가 잘 나오는 것을 알 수 있습니다. 문제 3 : 여러 수의 총 합을 반환하는 POST API를 만들어 보자!path : /api/v1/calc{ "numbers" : [1, 2, 3, 4, 5] }이 문제는 좀 반가웠습니다. 사실 그 동안 API를 만들면서 리스트를 입력으로 받아 본 적이 많지는 않아요. 늘 리스트는 레포지토리에서 뽑아서만 쓰곤 했습니다. 그래서 토이 프로젝트를 시작한다면 리스트를 입력으로 받는 기능들을 한번 고민해봐야겠습니다.public class Numbers { private List<Integer> numbers = new ArrayList<>(); public List<Integer> getNumbers() { return numbers; } }리스트를 받을 수 있게 List 타입의 변수를 선언하고 ArrayList를 선언했습니다. 조금 다른 소리이긴한데 항상 저렇게 List 타입의 변수를 선언하고 초기화를 할 때면, 늘 ArrayList를 쓰곤 했는데요. 여러분은 LinkedList나 다른 구현체를 선언해보신적이 있으신가요? 저는 없습니다. 그런데 괜찮다고 합니다. 흔히 자료구조를 공부할 때 배열의 특성을 가진 ArrayList의 경우 중간 삽입, 삭제가 많이 일어날 경우 자료들의 위치를 계속해서 이동 시켜줘야한다는 이유로 비효율적이라고 배웠지만 사실 자바에선 성능상의 큰 차이가 없다고합니다. 자바의 컬렉션 프레임워크를 만드는데 크게 일조한, 이펙티브 자바의 저자로도 유명한 조슈아 블로크도 본인의 트위터에 "내가 만들었긴 했는데 나도 잘 안쓴다." 라고 했다고 합니다. 기계적으로 ArrayList를 쓰는 것에 반성을 크게 하려했지만 이내 안심했습니다. 아무튼 저렇게 선언하고 컨트롤러에서는 다음과 같이 처리했습니다.@RestController public class AssignmentTwoProblemThreeController { @PostMapping("/api/v1/calc") public Integer calculateVariousNumber(@RequestBody Numbers numbers) { List<Integer> numbersList = numbers.getNumbers(); return numbersList.stream().mapToInt(Integer::intValue).sum(); } }다른 문제에서 URL의 쿼리 파라미터로 요청을 받은 것과는 달리, 이 문제는 HTTP Body에 데이터를 담아서 보냅니다. 따라서 이를 핸들링해줄 수 있는 @RequestBody를 사용했습니다. API를 만들어보신 분이라면 자주 사용하고 많이 봤던 어노테이션일 것입니다.이 어노테이션을 사용해 HTTP Body로 들어온 입력값들을 가져온 후, For문으로 합계를 구할 수도 있겠지만 람다식을 이용해서 처리했습니다. 기본적으로 스트림에는 매핑용 중간 연산 메소드로 map을 제공하고 반환하는 형태에 따라 mapToInt, mapToLong, mapToDouble이 있습니다. mapToInt 함수는 IntStream을 반환하기로 정해져있고 IntStream에는 총합을 계산하는 sum() 메서드를 제공합니다. 이는 최종 연산을 수행하는 메서드로서, 스트림은 중간 연산의 결과는 사용할 수 없다는 점을 참고해주세요. for문을 사용하지 않아 줄이 많이 줄어들었고, 한 줄에 짧게 끝낼 수 있었습니다. 그리고 요청을 보내보면, 이렇게 결과를 잘 받을 수 있었습니다. 다른 분들은 과제를 하면서 궁금한 것들을 모두 해결하시던데, 저는 아직 거기까지 파다간 과제를 제때 제출하지 못할 것이 걱정되는 스케줄이기에.. 과제를 수행하면서 더 깊이 파봐야할 것들을 적어놓고 마무리하고자 합니다. @RequestBody의 동작 원리와 Jackson 라이브러리왜 @ModelAttribute나 @RequestParam 같은 것이 없고 그냥 POJO임에도 불구하고 파라미터를 자동으로 잘 매핑해줄까?이상 글 마치겠습니다. 모든 워밍업 클럽에 참여하시는 분들의 성공과 상쾌한 아침을 기원합니다.

백엔드워밍업클럽BE-0기최태현백엔드

채널톡 아이콘