게시글
질문&답변
[58. 인덱스와 정렬]을 듣다가 질문입니다.
자답:바로 다음 단원에서 나옵니다.
- 0
- 2
- 26
질문&답변
해시 인덱스를 사용하는 경우 데이터 저장 평균 시간 복잡도에 대해 질문
자답강의 해시 알고리즘6 - 해시 충돌 구현 15:02 부분에서 이미 설명한 내용. 데이터를 추가하는데 O(n)의 시간 복잡도가 소요.그러나, 해시 충돌이 발생하지 않는 상황은 결국 각 bucket의 데이터가 0개나 1개 들어있는 거기때문에 contains(..)가 O(1)의 시간 복잡도로 소요된다. 따라서, 해시 인덱스를 사용하는 경우 데이터 저장을 할 때 평균 시간 복잡도가 O(1)이라 할 수 있다.괜한 알람가게 해서 죄송합니다.
- 0
- 2
- 49
질문&답변
private 인데 static 을 붙인이유
안녕하세요. 비공식 서포터즈 임형준입니다.NetworkMessage가 클래스라고 생각이 들어 조금 헷갈리실 수 있었을 거 같습니다. 이걸 메서드라고 생각해볼까요?보통 메서드를 작성할 때, 인스턴스 변수를 사용할 때 static으로 선언하지 않고, 반대로 인스턴스 변수를 사용하지 않을 때 static으로 선언합니다. 즉, 상태를 가지고 있지 않기 때문에 모든 객체들이 똑같이 행동할 수 있어 static으로 효율을 높입니다. 마찬가지로 이 Network 클래스에서의 인스턴스 변수가 없고, NetworkMessage에서 사용하지도 않기 때문에 static으로 선언했다고 볼 수 있습니다.
- 0
- 2
- 290
질문&답변
클래스 접근제어자
안녕하세요. 비공식 서포터 임형준입니다.hxhxhx님께서 말씀하신 클래스 접근제어자는 public, default가 맞습니다. package레벨에서 한 번 생각해볼까요? 그러면 두가지의 경우만 필요하다는 것을 알 수 있어요. (package 안에서만 쓸 수 있는 경우, package안팎에서 쓸 수 있는 경우) 이에 따라 default, public으로 나눈겁니다.그렇다면 이 nested class라는 것을 class(정확히는 top-level class)안에 마치 변수처럼 사용을 할 때는 구분을 어떻게 할 수 있을까요? 모든 범위, 같은 package 안, 같은 class 안 이렇게 3가지 경우로 나눌 수 있습니다. 이에 따라 모든 범위 -> public같은 package -> default같은 class -> private 로 나눌 수 있게 됩니다.접근 제어자가 어떤 역할을 하는건지에 따라 나뉘어진다는게 키포인트인 거 같습니다.
- 0
- 2
- 101
질문&답변
섹션 5 - 3(타입 안전 열거형 패턴) if 문대신 개선된 switch 문으로 변경
안녕하세요. 비공식 서포터즈 임형준입니다. 당나귀님처럼 코드변경해보는거 저도 좋아합니다. 아마 Constant expression required 과 같은 컴파일 에러가 발견되었겠죠? 이렇게 뚜렷하게 키워드들이 나오면 저는 공식문서나 스펙으로 한 번 확인해봐요. (멋있잖아요. ㅎㅎ)https://docs.oracle.com/javase/specs/jls/se23/html/jls-14.html#jls-SwitchLabel들어가보면 바로 다음 문구를 확인하실 수 있습니다.> Every case constant must be either a constant expression (§15.29), or the name of an enum constant (§8.9.1), otherwise a compile-time error occurs.발번역 해본다면, '모든 case constant 는 constant expression 이거나 enum 상수의 이름이어야 하고 다른 것들은 컴파일 에러가 발생한다.' 라고 합니다. 정리해본다면, case문 안에 상수는 1. constant expression: 보통 final static 으로 선언된 상수를 말합니다.2. enum 객체 자체(아마 Java 17부터 가능)이 두가지 경우가 선언될 수 있습니다. 하지만, ClassGrade.BASIC 과 같은 경우는 final static으로 선언된 상수(primitive type 혹은 String)가 아니라 객체이기 때문에 허용이 안됩니다.
- 0
- 2
- 393
질문&답변
오버라이딩 논리를 다시 설명해주세요.
이 상황은 Parent를 상속받은 Child라는 객체를 생성하여, 현재 Parent,Child 두 객체가 생성된 상황이며, poly를 부모 변수로 선언하였습니다. 부모는 자식에 대해 알 수 없다라는 논리에 따라 Child라는 객체가 있어도 부모 Parent는 Child에 선언된 메서드와 필드를 모른다는 것이 영한님의 설명인 것 같습니다.이 부분 먼저 조금 수정해봐도 괜찮을까요?Parent poly = new Child();1. new 연산자에 의해 Child 인스턴스가 딱 하나가 힙에 생성(힙 메모리에 적재)됩니다. 이 Child 인스턴스는 Parent를 상속받았으므로 Parent, Child클래스의 구조를 포함하고 있지요.2. Parent 타입의 참조 변수 poly가 main stack frame에 생성(메모리에 적재)되고 이 인스턴스에 할당됩니다.poly.value변수는 김도협님의 질문 의도와 같이 Parent 타입을 기준으로 동작합니다.poly.method()메서드는 Java에서 오버라이딩 된 메서드에 우선권을 부여합니다. 이를 좀 더 구체적으로 말씀드리면 Java에서는 런타임에 객체의 실제 타입(여기서는 new Child()로 인한 Child타입)을 기준으로 동작합니다. 그렇다면 Java 에서 왜 이렇게 만들었을까요? 이럴 때 저는 자주 사용하는 방법이 반대로 생각하기입니다. 만약, poly.method() 가 변수와 똑같이 적용된다고 해보겠습니다.```javaParent poly = new Parent();poly.method(); // "Parent method" Parent poly = new Child();poly.method(); // "Parent method"```이렇게 된다면 결국 다형성을 사용하지 못하게 됩니다. 이 단원의 맨처음을 생각해보면 이렇게 하는 이유는 다 다형성을 활용하게 하기 위해서 였습니다. 저렇게 메서드를 오버라이딩 된 메서드에 우선권을 강제로 부여해서 말이죠.참고) 이렇게 객체의 실제 타입으로 메서드를 호출시키는 것을 컴퓨터 과학에서는 Dynamic dispatch 라고 합니다.하지만, 초반에 이런 이론에 너무 빠지는 거보다 Java에서 그냥 이렇게 해주는구나. 라고 받아들이고 코딩을 왕창하시는게 훨씬 훨씬 빠르게 성장하실겁니다. 열공하세요.
- 0
- 2
- 265
질문&답변
다운캐스팅에 대한 질
따라서 다운캐스팅을 해도 자식클래스 메서드를 호출하지 못한다 -> 라고 생각하면 되는 건가요? 에 대한 답변:애초에 new Parent로 생성된 인스턴스에는 Child 클래스에 대한 정보가 없으므로 다운 캐스팅을 할 수 없습니다.그렇지만 다시 생각해보면 메서드는 메서드 영역 내에서 가져다가 호출하는 방식인데, 그렇게 된다면 자식 클래스의 메서드 또한 메서드 영역내에서 불러오면 되니까 호출이 가능한 게 아닐까요..? 에 대한 답변:위에서 다운 캐스팅이 성립이 안되기 때문에 런타임 에러(ClassCastException)가 발생되어 메서드 호출하기 전에 프로그램이 종료됩니다.제공해주신 콘솔을 확인해보면 마지막에 at poly.basic.CastingMain1.main(CastingMain1.java:7)이라고 나와있습니다. 7번 라인에서 예외가 발생한 것임을 알 수 있습니다. (다음에 코드 스크린샷을 제공할 때, 라인 수도 같이 찍어주시면 더 좋을거에요!) 아마 child.childMethod()를 지우시고 실행하셔도 똑같은 에러가 발생할겁니다.
- 0
- 3
- 433
질문&답변
HTTP API 설계 예시 - 컨트롤러, 컨트롤 URI 부분 동사를 직접 사용에 대한 질문입니다.
어익후. 갑자기 깨달았습니다.GET, POST만 지원되는 경우에 삭제하라같은 명령을 내릴 수 없으니까 그럴때는 동사처리하는 거군요. 신중하지 못했던 질문에 대해서 반성합니다.
- 0
- 2
- 173
질문&답변
view 분리에 대해 질문이 있습니다!
MemberFormControllerV1을 예로 들어보겠습니다.process(..)는 다음과 같이 두가지 부분으로 나뉘어 질 수 있습니다.(사진)왜 그럴까요? 위의 빨간 상자는 계속 변할 수 있기 때문입니다. 반면 아래 상자는 변함이 없는 로직입니다. 따라서, 이 두 박스를 분리해줍니다.(사진)다른 Controller들을 비교해보면 이 process의 2줄 로직이 모두 동일하다는 걸 알 수 있습니다. 그렇다면 이걸 그냥 효율적으로 MyView라는 객체를 만들어서 한군데에서 처리해주겠다는 걸 적용하려고 하니까 고쳐야할 점이 보입니다.이 getRequestDispatcher(..)가 viewPath를 사용한다는 점입니다. 따라서, 이 viewPath를 MyView에 넘겨줘야합니다. 어떤 값을 넘겨줄려고 할 때 스프링 기본편에서 배웠던 Dependency Injcection 을 사용하면 MyView생성자가 viewPath를 외부에서 주입받는 방향으로 코딩을 하게 됩니다. 결국, MyView는 viewPath의 구체적인 값을 몰라도 되게 됩니다.그렇다면 질문주신 "String 타입의 파라미터를 받는 메서드로 만들면 안 되나요?" 에 대한 답변은 만들 수는 있다입니다. 하지만 파라미터의 갯수를 늘리는 방식으로 리팩터링을 하면 단점이 있습니다. 해당 메서드를 호출하는 쪽에서도 알맞는 값을 넣어줘야 한다는 단점이 있습니다.반복되는 행위를 하나의 클래스를 만들어서 그 클래스가 반복되는 행위를 하게끔 하는 것이 객체지향적으로도 효율면에서도 더 좋기 때문에 객체로 따로 뺀것입니다.
- 0
- 2
- 195
질문&답변
static Factory method로 빈 생성
질문에 오류가 있어 다시 질문드립니다.헷갈리게 해서 죄송합니다. https://www.inflearn.com/questions/1280576/static-factory-method%EB%A1%9C-%EB%B9%88-%EC%83%9D%EC%84%B1
- 0
- 2
- 202
블로그
전체 72024. 03. 09.
0
[인프런 워밍업 스터디 클럽] 0기 - 세번째 발자국
세번째 발자국이 글은 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]를 수강하고 인프런 워밍업 클럽에 참여하여 쓰는 두번째 회고록입니다.3주차 내용Day 11 객체지향과 JPA 연관관계(강의 33~36강)33강전체적으로 api를 개발할 때 어떤 순서로 짜야하는지 정리할 수 있었다. 그리고 처음부터 강의를 듣지 않고 내가 먼저 구현을 해보고 최태현님은 어떻게 구현을 하시는지 비교해봄으로써 좀 더 집중해서 들을 수 있는 강의였다.api 개발 순서요구사항 정확하게 분석API spec 정의머릿속으로 전체적인 이미지 구상테이블 설계 (4,5번 할 때 스펠링 틀리는 거 각별히 신경)엔티티(객체) 구현 & Repository 구현(둘이는 셋트라고 생각)API 스펙에 맞는 dto 만들기controller, service 도 마저 만들기책 생성 API최태현님과 나의 비교is_return 테이블을 설계할 때 tinyint(1)을 쓰셨다미리 낱개의 실험 데이터를 가정하고 그려보았다요구사항을 제대로 파악하고 주석을 쓴다생성자에 뭔가 고정값이 담긴다거나 파라미터가 너무 많아 보이면 리팩토링 함으로써 깔끔하게 해준다'그'의 요구사항 내용책 정보를 가져온다.대출 기록 정보를 확인해서 대출 중인지 확인한다.만약에 확인했는데 대출 중이라면 예외를 발생시킵니다.유저 정보를 가져온다.책과 유저 정보를 바탕으로 유저 대출 정보에 등록해준다.'나'의 요구사항 내용유저를 찾는다.책정보를 찾는다.만약 내가 빌려가려는 책이 이미 다른 사람이 빌렸다면 예외 처리요청한 책을 내 이름으로 등록한다34강JPA를 쓰는 이유 중에 패러다임을 객체지향적으로 하기 위해서 쓰는 것도 있었다. 그러나, 이전까지의 코드를 살펴보면 절차지향에 가까웠다. 객체들끼리 서로 협력을 하는 것이 아니라 각자의 객체가 스스로 처리했다. BookService에서 반납 기능은 User에서 정보를 가져오고 UserLoanHistroy에서 대출 정보를 가져와 반납으로 처리한다.이러한 패러다임을 객체지향적으로 바꾸려면 객체 간의 관계가 필요하다. 그 관계를 JPA에서 연관 관계로 표현할 수 있다.1:1 관계 - @OneToOnePerson이랑 Address 연관 관계 실험두 개의 테이블에 각각의 연관된 id를 만들어서 실험[결과]address가 주체일 때 address테이블에만 person_id가 저장person이 주체일 때 person테이블에만 address_id가 저장두 개의 엔티티에 전부 mappedBy를 입력해줬을 때다음과 같이 AnnotationException이 발생한다.@Transactional이 끝나기 전에 객체를 가져왔을 때 null이 안나오게 하는 해결책 setter로 한 번에 둘로 이어주게끔 하면 된다.N : 1 관계 - @ManyToOne@JoinColumn연관관계 주인(owner Entity)에게 쓰는 거@Column과 쓰임이 비슷, 필드의 이름을 정할 수 있다.N : M 관계 - @ManyToMany사용 Xcascade 옵션폭포수관계들이 쭉 폭포수처럼 이어져 있게 하는 개념orphanRemoval 옵션orpthan: 고아removal: 제거연결되어 있던 관계에서 한 엔티티가 없어지면 연결되있던 엔티티의 데이터도 사라지는 개념Day 12 기본적인 배포를 위한 준비api구현을 마무리하고 배포를 어떻게 하는지 배웠다. 새롭게 알게 된 내용만 간단하게 추려보았다.37강. 배포란 무엇인가배포란 쉽게 말해서 전용 컴퓨터로 코드(우리의 서버)를 옮긴다! 라고 한다. 최종 사용자에게 SW를 전달하는 과정이란걸 알았다.38강. profile과 H2 DB내 컴퓨터 로컬에 설치한 mysql을 쓰게 해줬던 것처럼 전용 컴퓨터로 내 코드를 옮겼을때도 똑같은 환경을 만들어 줘야 한다는 것을 알 수 있었다.그리고 데이터베이스는 데이터가 영구적이기 때문에 테스트로 활용하기엔 불편한 점이 있는데 이를 해결하기 위해 H2데이터베이스를 쓴다는 걸 알 수 있었다.39~40강. git github개발 처음 시작할 때 바로 깃과 깃허브부터 접했어서 생소한 개념이 있지는 않았다. 새로 배웠다거나 깨달은 건 두가지였다.git push --set-upstream origin main 여태까지 이걸 한번만 하면 다음부터는 git push만 쓰면된다는 걸 모르고 있었다..gitignore언제 생성시켜야 하는지 타이밍을 제대로 알 수 있었다.Day 13 AWS와 EC2 배포배포를 통해 localhost에서만 돌아가는 서버를 aws ec2에 배포해 보았다. 이 과정을 혼자 막연하게 부딪히면서 했다면 하루를 온전히 다 투자해야 됐을 것 같다.네트워크에 대해서 조금 찍먹해 볼 수 있는 경험을 가졌다. ssh, DNS, 인바운드 규칙, IP 같은 키워드를 궁금하게 하는 실습이었다.Day14, 15 Spring Boot 설정, 버전업 이해하기, 마무리 및 추가 꿀팁 영상gradle, .yml, 버전업 등을 통해 스프링 프로젝트를 구성하는 요소를 좀 더 이해할 수 있었다. 게다가 마무리에 추가 영상도 있었다. 질문자들에 대한 답을 영상으로 따로 만든 것이었다.최태현님은 항상 느끼는 거지만 질문에 대한 답을 너무 성실하게 해주신다. 볼 때마다 감탄한다.미니 프로젝트프로젝트 레파지토리: https://github.com/hyungjunn/employee-commute결국 머리에 제일 땀이 많이 나는 건 고민하면서 진행하는 프로젝트인 거 같다. 가장 의미있는 시간이었다. 진행하면서 그리고 다른 사람 코드리뷰를 보면서 느낀 점은 다음과 같다.서비스로직에서의 코드 분리 조금만 복잡한 비지니스 로직을 구현하려고 하면 서비스가 점점 뚱뚱해져서 스스로 느낄 때도 메서드가 여러 일을 하고 있어 알아보기가 힘들어 졌었다. 결국 기본으로 돌아가 객체 지향적인 개념이 너무 중요한 거 같다.연관 관계에 대한 것 JPA를 이 강의를 통해 처음 배웠었다. 또 결국 비판적 사고를 하지 않고 JPA는 연관관계를 무조건 써야 좋은지 알고 프로젝트 하면서 최대한 쓸려고 했었다. 피드백을 듣고 연관 관계가 단점이 많다는걸 이제서야 깨달았다. 연관관계 뿐만 아니라 어느걸 쓸 때 최대한 합당한 이유로 써야하는 습관을 길러야 겠다.일급 컬렉션, 그리고 상황에 따른 변수 선언 서비스 로직에서 코드를 분리할 때 어떤식으로 하는지 보여주셨는데 그게 일급 컬렉션이란 걸 말씀해주셨다. 그리고 도메인 로직에서는 어떠한 값을 담을 것이 필요하면 바로 변수를 새로 선언하는 걸 보여주셨다. 코드를 짤 때 유연해야 된다는걸 너무 크게 느꼈다.이 외에도 너무너무 많다. 결국 계속해서 고민하면서 코드를 만들고 고치는거 밖에 방법이 없다는걸 느꼈다.인프런 워밍업 클럽을 참여하지 않았다면 큰일날 뻔 했다. 마침 너무 이론만 배우고 있던 시기라 사이드 프로젝트를 하고 싶었는데 이 스터디를 통해 사이드 프로젝트를 혼자 만들 수 있을 거 같다는 자신감을 조금 높일 수 있어서 좋았다.
백엔드
・
백엔드
・
인프런워밍업스터디
2024. 03. 03.
0
[인프런 워밍업 스터디 클럽] 0기 - 두번째 발자국
두번째 발자국이 글은 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]를 수강하고 인프런 워밍업 클럽에 참여하여 쓰는 두번째 회고록입니다.2주차 내용2주차에서는 스프링 컨테이너에 대해서 배우고 JPA를 통해 API를 만드는 방법을 배웠다.간단하게 요번주 수업 내용을 요약해보면 다음과 같다.Day 7 스프링 컨테이너의 의미와 사용 방법(19강 ~ 22강)여태까지의 개선들이 다 스프링 컨테이너와 빈을 설명하기 위한 것이란 걸 알았다.먼저, 의존한다라는 것의 정의를 알 수 있었다. 그리고, 왜 인스턴스를 생성하지 않았는데 Controller의 생성자에서 JdbcTemplate를 사용할 수 있는 이유를 알게 되었다.그 이유를 실제로 다른 곳(Repository)에 적용해나가면서 프로젝트를 또 개선해나가면서 스프링의 빈이 무엇인지 명확하게 알 수 있었다.Day 8 Spring Data JPA를 사용한 데이터베이스 조작(23강 ~ 26강)JPA를 왜 사용하는게 좋은지 그 이유에 대해서 알고 배워나가기 시작했다. 기본적인 Entity 정의하는 것과 일반적인 쿼리를 JPA에서는 어떻게 바뀌는 건지 알게 되었다.이 단원에서는 두가지 부분에서 얻어 가는게 있었다.최태현 선생님께서 JPA 로직 작성하실 때 setter를 안쓰고 updateName으로 메서드를 따로 만들었다. 최태현님의 유튜브 에서 setter를 사용하면 안되는 이유를 보게 되었다. 여태까지 와닿지가 않았었는데 안쓰는게 좋은 이유를 총망라해주셔서 너무 도움이 되었다. 특히 비지니스 로직과 테크니컬 로직에서의 setter 지양, 지향을 설명해주실 때 깨달음을 한 번 얻게 되었다. JPA를 사용하게 되면서 모던 자바 문법을 밥 먹듯이 사용하게 된다는 걸 알 수 있었다. 절대 그냥 넘어가면 안된다는 경각심을 다시 한번 깨우치게 됐다. Day 9 트랜잭션과 영속성 컨텍스트(27강 ~ 29강)트랜잭션에 대해서는 평소에 알고 있어서 무난했다.그런데 영속성 컨텍스트는 처음들었다. 굉장히 신기했었다.실습하면서 맨 처음에 다른 패키지의 @Transactional을 썼었었다. springframework에 있는 Transactional을 붙이자.Day 10 조금 더 복잡한 기능을 API로 구성하기(30강 ~ 32강)아예 처음부터 기능을 JPA를 사용해서 만들어보면서 체화하는 과정을 거쳤다. 인강을 먼저 보지 않고 내가 구현을 먼저 해봄으로써 마치 과제를 수행하듯이 하고 인강을 들으니 확실히 더 좋았다.그리고 조금 복잡한 로직이 있으면 최태현님의 영상에서 처럼 잘게 쪼개고 주석으로 적고 기능 구현 한다는 점에 초점을 맞춰서 보았다.과제과제 6여섯번째 과제: https://www.inflearn.com/blogs/6865인강에서 처럼 Controller-Service-Repository 로 나누어 보는 과제였다.과제 7일곱번째 과제: https://www.inflearn.com/blogs/6901공통 피드백을 듣고 단순히 문자열로 option 조건을 쓴 것을 enum class를 써서 바꿨다.public List getSpecificOptionPriceFruits(String option, Long price) { if (isNotSpecificPriceOption(option)) { throw new IllegalArgumentException(); } List fruits = new ArrayList(); if (GTE.equals(option)) { fruits = fruitRepository.findByPriceGreaterThanEqualAndIsSold(price, false); } if (LTE.equals(option)) { fruits = fruitRepository.findByPriceLessThanEqualAndIsSold(price, false); } return convertToFruitsSpecificOptionPriceRespond(fruits); }하지만, 딱 봐도 한 눈에 코드들이 읽히지가 않는다. 계속해서 한눈에 읽혀지는 코드를 지향해야 되겠다.
백엔드
・
인프런워밍업클럽
・
백엔드
2024. 02. 27.
0
[인프런 워밍업 스터디 클럽] 0기 - 백엔드 과제 #7
JPA 이용해서 API 만들기이 글은 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]를 수강하고 인프런 워밍업 클럽에 참여하여 쓰는 첫번째 회고록입니다.문제 1과제 #6의 Fruit 기능들을 JPA를 이용하게끔 변경해보자.application.yml에 jpa 설정 추가 jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true dialect: org.hibernate.dialect.MySQL8Dialectdomain에 있던 Fruit을 @Entity annotation 붙혀주기@Entity public class Fruit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(nullable = false) private String name; private LocalDate warehousing_date; private long price; public boolean is_sold; protected Fruit() {} // protected로 기본생성자 생성 public Fruit(long id, String name, LocalDate warehousing_date, long price) { this.id = id; this.name = name; this.warehousing_date = warehousing_date; this.price = price; this.is_sold = false; } public long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousing_date() { return warehousing_date; } public long getPrice() { return price; } public boolean is_sold() { return is_sold; } }FruitJpaRepository interface 만들기public interface FruitJpaRepository extends JpaRepository {}FruitService 새로 만들기saveFruit(FruitCreateRequest request)@Override public void saveFruit(FruitCreateRequest request) { String sql = "INSERT INTO fruit(name, warehousing_date, price) VALUES (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getDate(), request.getPrice()); }public void saveFruit(FruitCreateRequest request) { fruitRepository.save(new Fruit(request.getName(), request.getDate(), request.getPrice())); }FruitReadSalesAmountRespond getSalesFruitAmount(String name)내가 서칭을 해본 결과 현재 이해하는 방법이 두가지가 있었다.직접 쿼리 사용(Native Query)Java Stream 사용JPA는 데이터베이스 종속성을 피하고, 좀 더 객체지향적인 코드(확장성, 유지보수성을 고려하는 코드)를 짜기 위한 방법이므로 Java Stream을 써서 해결했다.그리고 마침 어제 메모리에 저장하는 코드를 작성할 때 Stream으로 만들어 놨어서 작성하기 수월했다.@Override public FruitReadSalesAmountRespond getSalesFruitAmount(String name) { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold"; List salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name); Long salesAmount = salesAmounts.get(0); Long notSalesAmount = salesAmounts.get(1); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } 먼저, FruitJpaRepository에 List타입의 findByName을 선언해준다.public interface FruitJpaRepository extends JpaRepository { List findByName(String name); } 그리고, FruitServieV2에 다음 코드를 작성한다.public FruitReadSalesAmountRespond getSalesFruitAmount(String name) { List fruits = fruitRepository.findByName(name); Long salesAmount = fruits.stream() .filter(Fruit::isSold) .mapToLong(Fruit::getPrice) .sum(); Long notSalesAmount = fruits.stream() .filter(fruit -> !fruit.isSold()) .mapToLong(Fruit::getPrice) .sum(); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); }계산하는 로직을 공통으로 묶어 리팩토링 하면 다음과 같다.public FruitReadSalesAmountRespond getSalesFruitAmount(String name) { List fruits = fruitRepository.findByName(name); Long salesAmount = calculateAmount(fruits, true); Long notSalesAmount = calculateAmount(fruits, false); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } private Long calculateAmount(List fruits, boolean isSold) { return fruits.stream() .filter(fruit -> fruit.isSold() == isSold) .mapToLong(Fruit::getPrice) .sum(); }updateSoldFruitInformation(Long id)public void updateSoldFruitInformation(Long id) { if (fruitRepository.isNotExistFruit(id)) { throw new IllegalArgumentException(); } fruitRepository.updateFruit(id); }update메서드가 따로 없기 때문에 Fruit 도메인에 updateSoldInformation 메서드를 하나 만든다.public void updateSoldInformation() { // Fruit class에 추가 this.isSold = true; }public void updateSoldFruitInformation(Long id) { Fruit fruit = fruitRepository.findById(id) .orElseThrow(IllegalAccessError::new); fruit.updateSoldInformation(); fruitRepository.save(fruit); }최종코드는 다음과 같다.@Service public class FruitServiceV2 { private final FruitJpaRepository fruitRepository; public FruitServiceV2(FruitJpaRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruit(FruitCreateRequest request) { fruitRepository.save(new Fruit(request.getName(), request.getDate(), request.getPrice())); } public FruitReadSalesAmountRespond getSalesFruitAmount(String name) { List fruits = fruitRepository.findByName(name); Long salesAmount = calculateAmount(fruits, true); Long notSalesAmount = calculateAmount(fruits, false); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } public void updateSoldFruitInformation(Long id) { Fruit fruit = fruitRepository.findById(id) .orElseThrow(IllegalAccessError::new); fruit.updateSoldInformation(); fruitRepository.save(fruit); } private Long calculateAmount(List fruits, boolean isSold) { return fruits.stream() .filter(fruit -> fruit.isSold() == isSold) .mapToLong(Fruit::getPrice) .sum(); } }테스트id가 2인 사과의 is_sold가 0에서 1로 바뀐 것을 확인할 수 있다.문제2특정 과일을 기준으로 판매된 과일 갯수를 세는 api 구현HTTP specHTTP method : GETHTTP path : /api/v1/fruit/countHTTP queryname : 과일 이름예시 GET /api/v1/fruit/count?name=사과HTTP 응답 Body{ "count": long }FruitControllerspec에 명시된 내용을 그대로 코드에 옮기면 된다.@GetMapping("/fruit/count") // method, path public Long getSoldFruitCount(String name) { // 요청 query, 반환타입(Long) return fruitServiceV2.getSoldFruitCount(name); // 반환값 }FruitServiceV2문제를 한국어 문장으로 풀어보면 결국 다음과 같다.요청한 쿼리의 name과 repository에 있는 name이 일치하는 과일 중에팔린(isSold가 true인) 것들의 갯수를 반환반환 : 타입(Long), 값 return요청한 쿼리의 name과 repository에 있는 name이 일치하는 과일 중: fruitRepository.findByName(name).stream()팔린(isSold가 true인) 것들:.filter(fruit -> fruit.isSold == true)의 갯수:.count()public Long getSoldFruitCount(String name) { return fruitRepository.findByName(name).stream() .filter(fruit -> fruit.isSold == true) .count(); }테스트팔린 '낑깡'은 1개이다.실제로 요청을 하면 1이 반환되는 것을 알 수 있다.하.지.만 실제 문제를 보면{ "count": long }이렇게 json형태로 응답을 요구했다. 따라서, respond객체를 만들고 반환 타입을 이 객체 타입으로 바꾸면 된다.(수정)FruitController@GetMapping("/fruit/count") public FruitSoldCountRespond getSoldFruitCount(String name) { return fruitServiceV2.getSoldFruitCount(name); }(수정)FruitServiceV2public FruitSoldCountRespond getSoldFruitCount(String name) { Long count = fruitRepository.findByName(name).stream() .filter(fruit -> fruit.isSold == true) .count(); return new FruitSoldCountRespond(count); }새로 만든 respond 객체public class FruitSoldCountRespond { private final Long count; public FruitSoldCountRespond(Long count) { this.count = count; } public Long getCount() { return count; } } 테스트문제객체를 새로 담아서 응답해야 하는데 그냥 무지성으로 Fruit Entity에다가 count variable을 만들어줬었다. 그 때 에러는 다음과 같다.500에러는 내부 서버문제니 ide에서 확인이 가능하다.SQLSyntaxErrorException 에러가 났다.f1_0.count가 Unknown column이라고 한다.application.yml에서 설정했던걸 활용했다. 찍힌 값을 확인해보니 다음과 같았다.즉, Fruit Entity는 실제 테이블과 매칭이 되는 건데 실제 테이블에는 count라는 column이 없어서 난 에러였다. 생각을 안해서 생긴 에러지만, 한 에러부분을 발견할 수 있었다. 문제(2)응답객체를 만들어서 반환해줬는데 위와 같이 406에러코드가 반환되었다.Spring에서 Content-Type에 선언된 형식으로 변환이 불가능할 경우 406에러가 발생하며, 어떤 요청을 받았는지 또는 어떤 응답을 보내야하는지에 따라 사용하는 HttpMessageConverter가 달라진다.JSON 요청이고 JSON 본문이 들어올 경우(Content-type이 JSON) JsonMessageConverter가 사용되서 요청 JSON 메세지를 User 객체로 변경하며 이 때 자바 빈 규약에 따른 프로퍼티 바인딩 (Getter/Setter)가 발생하며 Getter를 생략하였기 때문에 406에러가 발생하는 것이라고 한다.[참고]@ResponseBody 사용할 때 객체에 getter가 없을 경우https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/converter/HttpMessageConverter.html HttpMessageConverter는 어디에서 발견되는것인지 궁금하여 찾아보았다. @RequestBody의 javadoc에 친절하게 설명되어 있었다.Annotation indicating a method parameter should be bound to the body of the web request. The body of the request is passed through an HttpMessageConverter to resolve the method argument depending on the content type of the request. Optionally, automatic validation can be applied by annotating the argument with @Valid.메서드 파라미터를 가리키는 어노테이션은 웹 요청의 바디에 바운드되어야 한다. 요청의 바디는 요청의 content type에 의존하는 메서드 인자를 찾기위해 HttpMessageConverter를 지나간다.문제 3판매되지 않은 특정 금액 이상 혹은 특정 금액 이하의 과일 목록 받는 GET api 구현HTTP specHTTP method : GETHTTP path : /api/v1/fruit/listHTTP queryoption : "GTE" 혹은 "LTE" 라는 문자열이 들어온다.GET : greather than equalLTE : less than equalprice : 기준이 되는 금액이 들어온다.예시 1 - GET /api/v1/fruit/list?option=GTE&price=3000판매되지 않은 3000원 이상의 과일 목록을 반환해야 한다.예시 2 - GET /api/v1/fruit/list?option=LTE&price=5000판매되지 않은 5000원 이하의 과일 목록을 반환해야 한다.HTTP 응답 Body[{ "name": String, "price": long, "warehousingDate": LocalDate }, ...]FruitController@GetMapping("/fruit/list") // method, path public List getSpecificOptionPriceFruits(@RequestParam String option, @RequestParam Long price) { // query, 반환 타입 // name, price, warehousingDate return fruitServiceV2.getSpecificOptionPriceFruits(option, price); // 반환값 }FruitServiceV2public List getSpecificOptionPriceFruits(String option, Long price) { // GTE : select * from fruit where price >= ? and is_sold = false // LTE : select * from fruit where price refactoringif-else 구문에서 "LTE"의 조건이 명확하게 표시가 안되서 조금 헷갈리게 할 수도 있다. else를 제거해보자.public List getSpecificOptionPriceFruits(String option, Long price) { if (isNotSpecificPriceOption(option)) { throw new IllegalArgumentException(); } if ("GTE".equals(option)) { return fruitRepository.findByPriceGreaterThanEqualAndIsSold(price, false) .stream() .map(FruitsSpecificOptionPriceRespond::new) .collect(Collectors.toList()); } if ("LTE".equals(option)) { return fruitRepository.findByPriceLessThanEqualAndIsSold(price, false) .stream() .map(FruitsSpecificOptionPriceRespond::new) .collect(Collectors.toList()); } }각각의 조건문도 메서드로 따로 빼자.public List getSpecificOptionPriceFruits(String option, Long price) { if (isNotSpecificPriceOption(option)) { throw new IllegalArgumentException(); } if (isGTE(option)) { return fruitRepository.findByPriceGreaterThanEqualAndIsSold(price, false) .stream() .map(FruitsSpecificOptionPriceRespond::new) .collect(Collectors.toList()); } if (isLTE(option)) { return fruitRepository.findByPriceLessThanEqualAndIsSold(price, false) .stream() .map(FruitsSpecificOptionPriceRespond::new) .collect(Collectors.toList()); } } private boolean isNotSpecificPriceOption(String option) { return !("GTE".equals(option) || "LTE".equals(option)); } private boolean isGTE(String option) { return "GTE".equals(option); } private boolean isLTE(String option) { return "LTE".equals(option); }그러나, getSpecificOptionPriceFruits메서드의 분기처리를 제대로 하지 않아서 컴파일 에러가 난다. 그래서 아래와 같이 List fruits 를 따로 선언해준다. 그리고 if문 return값이 공통인 부분을 따로 메서드로 빼서 만들어주자.public List getSpecificOptionPriceFruits(String option, Long price) { if (isNotSpecificPriceOption(option)) { throw new IllegalArgumentException(); } List fruits = new ArrayList(); if (isGTE(option)) { fruits = fruitRepository.findByPriceGreaterThanEqualAndIsSold(price, false); } if (isLTE(option)) { fruits = fruitRepository.findByPriceLessThanEqualAndIsSold(price, false); } return convertToFruitsSpecificOptionPriceRespond(fruits); }
백엔드
・
인프런워밍업클럽
・
백엔드
2024. 02. 26.
0
[인프런 워밍업 스터디 클럽] 0기 - 백엔드 과제 #6
우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍문제 1과제 #4 에서 만들었던 API를 강의 내용 처럼 Controller - Service - Repository로 분리해보세요!과제 #4에서 만들었던 API@RequestMapping("/api/v1") @RestController public class FruitController { private final JdbcTemplate jdbcTemplate; public FruitController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @PostMapping("/fruit") public void createFruit(@RequestBody FruitCreateRequest request) { String sql = "INSERT INTO fruit(name, warehousing_date, price) VALUES (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getDate(), request.getPrice()); } @PutMapping("/fruit") public void updateSoldFruitInformation(@RequestBody FruitUpdateRequest request) { String readSql = "SELECT * FROM fruit WHERE id = ?"; boolean isEmpty = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty(); if (isEmpty) { throw new IllegalArgumentException(); } String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, request.getId()); } @GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold"; List salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name); Long salesAmount = salesAmounts.get(0); Long notSalesAmount = salesAmounts.get(1); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } }첫번째Controller에서 한 메서드가 가진 역할이 많다.API 관련해 HTTP Body를 객체로 변환하는 역할 -> FruitController과일이 있는지 없는지 확인 후 예외처리 하는 역할 -> FruitServicesql문을 사용해 데이터베이스에 접근하는 역할 -> FruitRepository@RequestMapping("/api/v1") @RestController public class FruitController { private final FruitService fruitService = new FruitService(); private final JdbcTemplate jdbcTemplate; public FruitController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @PostMapping("/fruit") public void saveFruit(@RequestBody FruitCreateRequest request) { fruitService.saveFruit(jdbcTemplate, request); } @GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { return fruitService.readSalesFruitAmount(jdbcTemplate, name); } @PutMapping("/fruit") public void updateSoldFruitInformation(@RequestBody FruitUpdateRequest request) { fruitService.updateSoldFruitInformation(jdbcTemplate, request.getId()); } }public class FruitService { private final FruitRepository fruitRepository = new FruitRepository(); public void saveFruit(JdbcTemplate jdbcTemplate, FruitCreateRequest request) { fruitRepository.saveFruit(jdbcTemplate, request); } public FruitReadSalesAmountRespond readSalesFruitAmount(JdbcTemplate jdbcTemplate, String name) { return fruitRepository.getSalesFruitAmount(jdbcTemplate, name); } public void updateSoldFruitInformation(JdbcTemplate jdbcTemplate, long id) { if (fruitRepository.isNotExistFruit(jdbcTemplate, id)) { throw new IllegalArgumentException(); } fruitRepository.updateFruit(jdbcTemplate, id); } }public class FruitRepository { public void saveFruit(JdbcTemplate jdbcTemplate, FruitCreateRequest request) { String sql = "INSERT INTO fruit(name, warehousing_date, price) VALUES (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getDate(), request.getPrice()); } public FruitReadSalesAmountRespond getSalesFruitAmount(JdbcTemplate jdbcTemplate, String name) { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold"; List salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name); Long salesAmount = salesAmounts.get(0); Long notSalesAmount = salesAmounts.get(1); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } public boolean isNotExistFruit(JdbcTemplate jdbcTemplate, long id) { String readSql = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); } public void updateFruit(JdbcTemplate jdbcTemplate, long id) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } }두번째Repository 가 데이터베이스 관련 로직이 들어가 있어야 되는거라 JdbcTemplate 객체가 있는건 괜찮다. 하지만 Service와 Controller에도 있으니 없애준다.@RequestMapping("/api/v1") @RestController public class FruitController { private final FruitService fruitService; public FruitController(JdbcTemplate jdbcTemplate) { this.fruitService = new FruitService(jdbcTemplate); } @PostMapping("/fruit") public void saveFruit(@RequestBody FruitCreateRequest request) { fruitService.saveFruit(request); } @GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { return fruitService.readSalesFruitAmount(name); } @PutMapping("/fruit") public void updateSoldFruitInformation(@RequestBody FruitUpdateRequest request) { fruitService.updateSoldFruitInformation(request.getId()); } }public class FruitService { private final FruitRepository fruitRepository; public FruitService(JdbcTemplate jdbcTemplate) { this.fruitRepository = new FruitRepository(jdbcTemplate); } public void saveFruit(FruitCreateRequest request) { fruitRepository.saveFruit(request); } public FruitReadSalesAmountRespond readSalesFruitAmount(String name) { return fruitRepository.getSalesFruitAmount(name); } public void updateSoldFruitInformation(long id) { if (fruitRepository.isNotExistFruit(id)) { throw new IllegalArgumentException(); } fruitRepository.updateFruit(id); } }public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruit(FruitCreateRequest request) { String sql = "INSERT INTO fruit(name, warehousing_date, price) VALUES (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getDate(), request.getPrice()); } public FruitReadSalesAmountRespond getSalesFruitAmount(String name) { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold"; List salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name); Long salesAmount = salesAmounts.get(0); Long notSalesAmount = salesAmounts.get(1); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } public boolean isNotExistFruit(long id) { String readSql = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); } public void updateFruit(long id) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } }문제 2문제 1에서 코드가 분리되면 FruitController/FruitService/FruitRepository 가 생겼을 것입니다.기존에 작성했던 FruitRepository를 FruitMemoryRepository와 FruitMySqlRepository 로 나누고 @Primary 어노테이션을 활용해 두 Repostory를 바꿔가며 동작시킬 수 있도록 코드를 변경해보세요!😊📌 @Qualifier 어노테이션을 사용해도 좋습니다! 🙂FruitController@RequestMapping("/api/v1") @RestController public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping("/fruit") public void saveFruit(@RequestBody FruitCreateRequest request) { fruitService.saveFruit(request); } @GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { return fruitService.readSalesFruitAmount(name); } @PutMapping("/fruit") public void updateSoldFruitInformation(@RequestBody FruitUpdateRequest request) { fruitService.updateSoldFruitInformation(request.getId()); } }FruitService@Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruit(FruitCreateRequest request) { fruitRepository.saveFruit(request); } public FruitReadSalesAmountRespond readSalesFruitAmount(String name) { return fruitRepository.getSalesFruitAmount(name); } public void updateSoldFruitInformation(long id) { if (fruitRepository.isNotExistFruit(id)) { throw new IllegalArgumentException(); } fruitRepository.updateFruit(id); } }FruitRepository@Repository public interface FruitRepository { void saveFruit(FruitCreateRequest request); FruitReadSalesAmountRespond getSalesFruitAmount(String name); boolean isNotExistFruit(long id); void updateFruit(long id); }FruitMemoryRepository@Primary @Repository public class FruitMemoryRepository implements FruitRepository { private final List fruits = new ArrayList(); private static Long id = 1L; @Override public void saveFruit(FruitCreateRequest request) { fruits.add(new Fruit(id++, request.getName(), request.getDate(), request.getPrice())); } @Override public FruitReadSalesAmountRespond getSalesFruitAmount(String name) { long salesAmount = calculateSalesAmount(name); long notSalesAmount = calculateNotSalesAmount(name); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } @Override public boolean isNotExistFruit(long id) { return fruits.stream().noneMatch(fruit -> fruit.getId() == id); } @Override public void updateFruit(long id) { fruits.stream() .filter(fruit -> fruit.getId() == id) .forEach(fruit -> fruit.is_sold = true); } private long calculateSalesAmount(String name) { return fruits.stream() .filter(fruit -> fruit.getName().equals(name) && fruit.is_sold()) .mapToLong(Fruit::getPrice) .sum(); } private long calculateNotSalesAmount(String name) { return fruits.stream() .filter(fruit -> fruit.getName().equals(name) && !fruit.is_sold()) .mapToLong(Fruit::getPrice) .sum(); } }FruitMySqlRepository@Repository public class FruitMySqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitMySqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void saveFruit(FruitCreateRequest request) { String sql = "INSERT INTO fruit(name, warehousing_date, price) VALUES (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getDate(), request.getPrice()); } @Override public FruitReadSalesAmountRespond getSalesFruitAmount(String name) { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold"; List salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name); Long salesAmount = salesAmounts.get(0); Long notSalesAmount = salesAmounts.get(1); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } @Override public boolean isNotExistFruit(long id) { String readSql = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); } @Override public void updateFruit(long id) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } }
백엔드
・
인프런워밍업클럽
・
백엔드
2024. 02. 25.
0
[인프런 워밍업 스터디 클럽] 0기 - 첫번째 발자국
첫번째 발자국이 글은 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]를 수강하고 인프런 워밍업 클럽에 참여하여 쓰는 첫번째 회고록입니다.1주차 내용1주차에서는 layered architecture로 구성한 api를 만드는 과정을 배웠다. 이 때, 그냥 완성된 코드를 떡하니 보여주고 알려주는 흔한 방식의 강의가 아니었다. 최태현 멘토님께서는 흔히 이야기하는 가장 간단한 "Hello World"수준의 코드에서 시작하여 문제 ->해결의 과정으로 api를 점진적으로 발전시켜가면서 설명해주셨다.간단하게 요번주 수업 내용을 요약해보면 다음과 같다.Day 1OT를 하였다. 인프런 워밍업 클럽 진행 방식에 대해서 설명해주셨다.Java의 역사도 핵심적인 부분을 설명해주시면서 모던 자바에 대한 중요성을 알려주셨다.Day 2서버, 네트워크와 HTTP 기초적인 부분을 "이세계" 비유법으로 설명해주셔서 추상적인 개념을 좀 더 와닿게 깨달을 수 있었다. 그리고 왜 HTTP를 사용하는지도 예시를 들어 설명하셔서 바로 수긍이 갈 수 있었다.서버는 어떠한 기능을 수행하는 것 -> 그 기능을 수행하길 원하면 요청을 해야함. -> 요청은 인터넷, 네트워크에서 이루어짐 -> 서로 다른 컴퓨터가 연결하려면 ip, port가 필요 -> 이 ip는 외우기 어려움 -> domain name(host) 로 해결, 이러한 시스템을 DNS라 함.(최태현님의 강의 방식은 이렇게 문제, 해결 순서로 기술을 설명하시고 + 비유로 추상적인 개념을 좀 더 빠르게 알 수 있게 해준다는 걸 알았고 어떻게 초점을 맞추어서 공부해야 할 지 알 수 있었다.)데이터를 주고 받을 때 표준이 필요함 -> HTTP -> HTTP 역시 규칙이 있다. -> HTTP Method(GET, POST, PUT, DELETE), Path -> 요청을 보낼 때는 GET, DELETE는 쿼리, POST, PUT은 바디 / 응답은 상태코드어떠한 요청을 보낼 때 이러한 부분들이 정해져 있어야 한다. -> API -> 우리는 이 API개발을 하는 것@RestController // 입구 public class CalculatorController { @GetMapping("/minus") // HTTP method, path public int addTwoNumber(@RequestParam int number1, @RequestParam int number2) { // 쿼리 return number1 - number2; // 반환값 } }위의 예제같이 api를 작성할 때, 두가지 규칙을 생각하며 작성하면 되었다. 1) api 설계 2) 확장성api 설계HTTP methodpath쿼리반환값확장성위의 코드를 다음과 같이 변경이 가능하다.@RestController // 입구 public class CalculatorController { @GetMapping("/minus") // HTTP method, path public int minusTwoNumber(CalculatorRequest request) { // 하나의 dto객체로 return request.getNumber1() - request.getNumber2(); // 반환값 } }public class CalculatorRequest { private final int number1; private final int number2; public CalculatorRequest(int number1, int number2) { this.number1 = number1; this.number2 = number2; } public int getNumber1() { return number1; } public int getNumber2() { return number2; } }파라미터가 많아지게 되면 신경써야 될게 많아지기 때문이다. 즉, 미래에 어떻게 변할지를 염두하며 코드를 짜야된다.Day 3Post API를 만들어 봤다. 다시 혼자 만들면서 에러를 만났다. 다음과 같았다.@RestController // 입구 public class CalculatorController { @PostMapping("/divide") public int divideTwoNumber(CalculatorRequest request) { return request.getNumber1() / request.getNumber2(); } }httpie를 이용하여 테스트를 하였더니 다음과 같은 응답메시지를 받았다.$ http -v POST localhost:8080/divide number1=10 number2=3"status"가 400으로 떴다. 뭐가 문제였을까? 400 status code는 클라이언트 에러를 나타낸다. RFC 9110참고.클라이언트 에러는 형식에 맞지 않는 요청 문법, 유효하지 않은 요청 메시지 형태이다. Post 요청은 GET과 다르게 body를 넘겨줘야 한다. 파라미터 부분을 @RequestBody 을 명시해주지 않아서 발생한 에러였다.@RestController // 입구 public class CalculatorController { @PostMapping("/divide") public int divideTwoNumber(@RequestBody CalculatorRequest request) { return request.getNumber1() / request.getNumber2(); } }그렇다면 0으로 나누는 에러를 일부러 발생시켜보니 다음과 같은 응답을 받을 수 있었다.$ http -v POST localhost:8080/divide number1=10 number2=0500 status code는 서버 에러를 나타낸다. RFC 9110참고. 서버 에러가 나면 다음과 같이 스택트레이스에 표시가 났다.그리고 본격적인 어플리케이션 api를 개발하였다. 이 때, 어떤 데이터를 저장하는데 domain의 개념을 사용해서 저장할 객체를 새로 만들었다.Day 4기본적인 데이터베이스 사용법을 배웠다. 이 때, Day 3에서의 문제점(데이터 휘발성)을 알게 되었고 데이터베이스를 써야 하는 이유에 대해 제대로 알게 되었다.Day 5데이터베이스를 이용해 기존의 메모리에 저장하는 방식의 코드를 디스크에 저장하는 방식의 코드를 바꿔 api를 만들었다. Day 6클린 코드가 왜 필요한지에 대해 배웠다. 그냥 코드를 딱 보여주시는게 아니라 왜 클린코드를 지향해야 되고 리팩토링하는 과정을 세심하게 다뤄주셨다.기존에 외우는 식으로 그냥 계층형 구조를 만들어서 코드를 짰다면, 이 강의를 지금까지 들으면서 왜 그렇게 해야하는지 와닿으며 코드를 짜니까 응용력이 확실히 전보다 생기는거 같다.과제백문이 불여일타란 말은 프로그래밍 분야에서의 유행어이다. 직접 쳐보는게 중요하다는 것이다. 하지만, 더 좋은 방법이 있다. 불여일시다. 그날 그날 바로 시험을 보는 것처럼 과제를 수행하니 코딩을 강제적으로라도 하게 되었다.그리고 어떠한 개념에 대한 과제를 내주실 때도 최태현님께서 강조하시는 왜? 언제? 장단점을 고민해보게 했다.과제 1첫번째 과제: 어노테이션어노테이션에 대한 궁금증이 사실 별로 있지 않았다. 궁금증이 없다는게 아니라 사실은 대충 넘어 갔었다는게 더 올바른 표현인거 같다.하지만, 요번에 사용하는 이유를 조사해보고 나만의 어노테이션을 만들어 봄으로써 어노테이션을 구성하는 메타 어노테이션에 대해서 제대로 알게 되었다.그 과정에서 spring의 대부분 어노테이션이 @Retention 이 RetentionPolicy.RUNTIME이란걸 알았다. spring은 Java의 Reflection api를 활용해 런타임에 동적으로 컴포넌트 스캔이 가능해야 되기 때문에 유지 정책을 런타임까지로 정한 것이다.과제 2두번째 과제: api 개발여러 요구사항 제시해주셔서 응용력을 기를 수 있었다.첫번째 문제를 발전시킨 과정은 다음과 같다.@RestController public class CalculatorController { @GetMapping("/api/v1/calc") public Calculator calculateTwoNumber(CalculatorResponse response) { int add = response.getNum1() + response.getNum2(); int minus = response.getNum1() - response.getNum2(); int multiply = response.getNum1() * response.getNum2(); return new Calculator(add, minus, multiply); } }컨트롤러에서 덧셈, 뺄셈, 곱셈의 로직을 처리하게 했다. 과제를 마감하고 최태현님께서 공통 피드백을 주셨다. 공통 피드백을 듣고 난 후 고쳐본 코드는 다음과 같다.@RequestMapping("/api/v1") @RestController public class CalculatorController { @GetMapping("/calc") public CalculatorResponse calculateTwoNumber(CalculatorRequest request) { return new CalculatorResponse(request); } }멘토님의 피드백을 통해 Controller라는 클래스는 요청에 대한 응답을 처리해주는 역할만 하는 쪽으로 코드를 짜는게 좋을거 같다는 생각을 했다.두번째 문제에서는 LocalDate타입의 객체를 @GetMapping을 달아준 메서드의 파라미터로 코드를 짰더니 바인딩이 안되는 문제가 있었다. 같이 활동을 하시는 분께서 버전문제일 거 같다고 알려주셨고 덕분에 해결할 수 있었다. 참고과제 3세번째 과제: 람다식에 대해 조사하기람다식, 더 나아가 모던자바라고 불리우는 Java8에 관한 내용은 이제는 매우 필수적인 항목이다. 그러나, 계속 어려워 뒤로 미뤘었던 주제이다.조사를 하고 예제를 매우 간단하게만 만들어봤다. 아직까지 익숙하게 체화가 되지 않은 상태인 거 같다. 코드를 직접 여러번 짜면서 모던자바 8에 대한 내용을 체화시키는 것을 요번 워밍업클럽 활동 중에 해야할 나만의 미션으로 정한 계기가 되었다.과제 4네번째 과제: api를 데이터베이스를 이용해서 구현하기최종 코드는 다음과 같았다.@GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold"; List salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name); Long salesAmount = salesAmounts.get(0); Long notSalesAmount = salesAmounts.get(1); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); }혹시나 더 좋은 코드는 뭘까 생각 중이다.과제 5다섯번째 과제: 주어진 코드를 클린하게 리팩토링해보기과제를 하면서 내가 짠 코드가 점점 산으로 간다는게 느껴졌다. 이유는 두가지였던 거 같다.게임이란 것의 주체를 정확하게 파악을 안하고 무조건 class로 나눌려고만 함.main 메서드에 대한 정확한 정의그래서, 다시 처음 부터 다시 만들었고 코드는 다음과 같다.public class DiceGame { private static final int DICE_FACE = 6; private final int[] resultCounts = new int[DICE_FACE]; public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); DiceGame game = new DiceGame(); game.startGame(); } private void startGame() { determineRollCounts(User.inputNumber()); printResult(); } private void determineRollCounts(int a) { for (int i = 0; i public class User { protected static int inputNumber() { Scanner scanner = new Scanner(System.in); return scanner.nextInt(); } }DiceGame이란 class를 먼저 정의하고 기능, 역할을 나누니까 너무 간단하게 리팩토링이 되었다. 그리고 User 는 그냥 숫자만 넣어주는 행위밖에 안하기 때문에 다음과 같이 나눠봤다.그리고 화룡점정으로 스트림을 이용하면,public class DiceGame { private static final int DICE_FACE = 6; private final int[] resultCounts = new int[DICE_FACE]; public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); DiceGame game = new DiceGame(); game.startGame(); } private void startGame() { determineRollCounts(User.inputNumber()); printResult(); } private void determineRollCounts(int a) { IntStream.range(0, a) .map(i -> (int) (Math.random() * DICE_FACE)) .forEach(randomDiceFace -> resultCounts[randomDiceFace]++); } private void printResult() { IntStream.range(0, resultCounts.length).forEach(i -> System.out.printf("%d번 눈금이 %d번 나왔습니다.\n", i + 1, resultCounts[i])); } }다음과 같이 리팩토링 할 수 있었다.
백엔드
・
인프런워밍업클럽
2024. 02. 23.
0
[인프런 워밍업 스터디 클럽] 0기 - 백엔드 과제 #5
본 내용은 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]를 듣고 작성한 글입니다. 5일차에는 클린코드에 대해서 배웠다. 클린한 코드가 왜 중요할까?개발자들은 항상 요구사항을 구현할 뿐만 아니라 여러 명이서 협업을 하기 때문에 코드를 많이 읽는다. 이러한 과정에서 깔끔한 코드는 다른 사람을 위해서 혹은 나중에 다시 코드를 보고 판단해야 할 나를 위해서 필수적이다. [과제에서 사용한 tool]운영체제 : MacBook Air M1, 2020Java : 17.0.9-amznIDE : IntelliJ Ultimate과제목표주어진 코드를 클린하게 개선해보면서 클린코드에 대한 감을 익히기 [제시된 코드]public class Main { public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); 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 = 0 && b = 1 && b = 2 && b = 3 && b = 4 && b = 5 && b [반복되는 로직 처리하기]반복이 세 군데에서 일어나고 있다.변수 r1~r6for문출력문먼저, 변수 r1 ~ r6를 배열에 담아보자. 그러면서 동시에 변수명을 각각의 주사위 면이 나온 횟수 를 뜻하는 diceFaceCounts로 바꿔보자.int[] diceFaceCounts = new int[6]; for (int diceFaceCount : diceFaceCounts) { diceFaceCount = 0; }그 다음에 for문 안에 특정하게 반복되는 숫자들의 패턴이 보인다. 인덱싱을 해줘서 for문을 만들어보자.for (int i = 0; i = j && b 이어서 출력문도 1~6까지의 숫자들이 같은 패턴으로 나열되어 있다. 인덱싱을 해 간단하게 나타내주자.for (int i = 0; i 1차적으로 리팩토링한 코드는 다음과 같다.public class Main1 { public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); Scanner scanner = new Scanner(System.in); int a = scanner.nextInt(); int[] diceFaceCounts = new int[6]; for (int diceFaceCount : diceFaceCounts) { diceFaceCount = 0; } for (int i = 0; i = j && b [메서드로 기능 분리하기]다음은 이 코드들이 무엇을 하는 건지 명확하게 표현이 되어 있지 않다. 이러한 점을 보완하기 위해 메서드로 코드들을 나눠보자.Scanner scanner = new Scanner(System.in); int a = scanner.nextInt(); 숫자를 입력받는 코드이다.메서드명 : inputNumber()반환타입 : int파라미터 : Xpublic static int inputNumber() { Scanner scanner = new Scanner(System.in); return scanner.nextInt(); }그 다음으로 주사위에 면을 생성하는 로직이다.int[] diceFaceCounts = new int[6]; for (int diceFaceCount : diceFaceCounts) { diceFaceCount = 0; }메서드명 : createDiceFaceCounts()반환타입 : int[]파라미터 : int faceNumberspublic static int[] createDiceFaceCounts(int faceNumbers) { int[] diceFaceCounts = new int[faceNumbers]; for (int diceFaceCount : diceFaceCounts) { diceFaceCount = 0; } return diceFaceCounts; }그 다음엔 주사위면이 각각 몇 회씩 나왔는지 알려주는 로직이다. 2중 for문이라 한 번에 읽히지가 않는다. 기능 단위로 덩어리 지어보자.for (int i = 0; i = j && b b >= j && b 메서드명 : isDiceNumber반환타입 : boolean파라미터 : int diceNumber, int ipublic static boolean isDiceNumber(int diceNumber, int i) { return diceNumber >= i && diceNumber 두번째로는 randomDiceNumber와 알맞는 주사위면의 횟수를 올려주는 로직이다.메서드명 : increaseRollCounts반환타입 : void파라미터 : int randomDiceNumber, int[] diceFaceCountspublic static void increaseRollCounts(int randomDiceNumber, int[] diceFaceCounts) { for (int j = 0; j 마지막으로 printRollCounts 메서드를 만들어보자.public static void printRollCounts(int[] diceFaceCounts) { for (int i = 0; i 총 정리해보면 다음과 같다.public class Main { public static void main(String[] args) { System.out.print("숫자를 입력하세요 : "); int number = inputNumber(); int[] diceFaceCounts = createDiceFaceCounts(6); for (int i = 0; i = i && diceNumber 하지만, 여전히 문제가 있다. 누가 이 메서들을 동작시키는 건지 명시가 안되어 있다. 이 문제를 class화 하여 해결할 수 있다. 이 때, 클래스는 의인화를 하여 마치 메서드를 하는 행위라고 생각하면 클래스 나누기가 수월하다.OutputView classprintNumberInput()printRollCounts()InputView classinputNumber()Dice classisDiceNumber()Player classcreateDiceFaceCounts()Game classincreaceRollCounts()incrementFaceCount()public class InputView { public static int inputNumber() { Scanner scanner = new Scanner(System.in); return scanner.nextInt(); } }public class OutputView { public static void printNumberInput() { System.out.print("숫자를 입력하세요 : "); } public static void printRollCounts(int[] diceFaceCounts) { for (int i = 0; i public class Dice { public static boolean isDiceNumber(int diceNumber, int i) { return diceNumber >= i && diceNumber public class Player { public static int[] createDiceFaceCounts(int faceNumbers) { int[] diceFaceCounts = new int[faceNumbers]; for (int diceFaceCount : diceFaceCounts) { diceFaceCount = 0; } return diceFaceCounts; } }public class Game { public static void increaseRollCounts(int randomDiceNumber, int[] diceFaceCounts) { for (int j = 0; j public class Main { public static void main(String[] args) { OutputView.printNumberInput(); int number = InputView.inputNumber(); int[] diceFaceCounts = Player.createDiceFaceCounts(6); Game.incrementFaceCount(number, diceFaceCounts); OutputView.printRollCounts(diceFaceCounts); } } 하.지.만 객체는 상태(멤버 변수)와 행동(메서드)를 가지고, 객체 지향 프로그래밍은 이러한 객체들이 서로 협력하는 프로그래밍을 말한다. 따라서, 상태들을 추가해주고 인스턴스화한 객체가 할 수 있는 행동에 대해서는 static을 제거해준다.
백엔드
・
인프런워밍업클럽
2024. 02. 22.
0
[인프런 워밍업 스터디 클럽] 0기 - 백엔드 과제 #4
본 내용은 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지] 를 수강하고 쓴 글입니다. 2일차에 GET API와 POST API에 대해서 배웠습니다. 하지만, 문제가 생겼습니다. 바로 서버를 종료하면 메모리에 저장되었던 데이터들이 사라지게 된다는 점입니다. 3일차에서는 이를 해결하기 위해 데이터베이스를 사용하게 됩니다. 메모리에 저장했던 방식을 데이터베이스의 디스크에 저장하는 방식으로 바꾸면 데이터들은 서버를 종료해도 계속 남아있게 됩니다. 4일차에서는 더 나아가 UPDATE API와 DELETE API에 대해서 배웠습니다. 하지만 또 문제가 발생했습니다. 기존에 존재하지 않은 데이터를 삭제하거나 수정하면 HTTP status code가 '200 ok'로 나온다는 점입니다. 이를 방지하기 위해 예외처리를 하였습니다. [과제에서 사용한 tool]운영체제 : MacBook Air M1, 2020Java : 17.0.9-amznspringboot : 3.2.2MySql : mysql ver. 8.0.25IDE : IntelliJ UltimateAPI test : HTTPie 과제목표주어진 요구사항들을 구현하면서 API개발에 익숙해지기 문제 1우리는 작은 과일 가게를 운영하고 있습니다. 과일 가게에 입고된 "과일 정보"를 저장하는 API를 만들어 봅시다.[스펙]HTTP method : POSTHTTP path : /api/v1/fruitHTTP 요청 body{ "name": String, "warehousingDate": LocalDate, "price": long }응답 : 성공시 HTTP status code 200[설계]HTTP method 가 POST : @RestController -> @PostMappingHTTP path 가 /api/v1/fruit : @RequestMapping("/api/v1") -> @PostMapping("/fruit")반환타입 : 단지 상태코드만 반환되면 된다 -> void메서드명 : 과일 정보를 저장 -> createFruit()설계를 바탕으로 api를 구현하면 아래와 같다. @RequestMapping("/api/v1") @RestController public class FruitController { private final JdbcTemplate jdbcTemplate; public FruitController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @PostMapping("/fruit") public void createFruit(@RequestBody FruitCreaeteRequest request) { String sql = "INSERT INTO fruit(name, warehousing_date, price) VALUES (?, ?, ?)"; jdbcTemplate.update(sql, request.getName(), request.getDate(), request.getPrice()); } }public class FruitCreateRequest { private long id; private String name; private LocalDate date; private long price; public long getId() { return id; } public String getName() { return name; } public LocalDate getDate() { return date; } public long getPrice() { return price; } }터미널에 다음과 같이 입력한 후 결과$ http POST localhost:8080/api/v1/fruit name=딸기 date=2024-02-02 price=5000 문제 2과일이 팔리게 되면, 우리 시스템에 팔린 과일 정보를 기록해야 합니다.[스펙]HTTP method : PUTHTTP path : /api/v1/fruitHTTP 요청 body{ "id" : long }[설계]HTTP method 가 PUT : @RestController -> @PutMappingHTTP path 가 /api/v1/fruit : @RequestMapping("/api/v1") -> @PutMapping("/fruit")반환타입 : 단지 상태코드만 반환되면 된다 -> void메서드명 : 과일 정보를 업데이트 -> updateFruitInformation()팔린 과일 정보 : 팔렸는지 안팔렸는지 상태를 확인해줄 수 있는 데이터 컬럼 추가 -> is_sold is_sold 컬럼이 추가 -> FruitUpdateRequest를 생성하고 isSold 멤버변수 추가ALTER TABLE fruit ADD is_sold boolean DEFAULT FALSE; -- 참고 https://dev.mysql.com/doc/refman/8.0/en/alter-table-generated-columns.htmlpublic class FruitUpdateRequest { long id; boolean isSold; public long getId() { return id; } public boolean isSold() { return isSold; } }@PutMapping("/fruit") public void updateFruitInformation(@RequestBody FruitUpdateRequest request) { String sql = "UPDATE fruit SET is_sold = ? WHERE id = ?"; jdbcTemplate.update(sql, request.isSold(), request.getId()); }위에서 입력했던 터미널 명령어와 비슷하게 요번에 PUT으로 입력하면,$ http -v PUT localhost:8080/api/v1/fruit id=3 isSold=TRUE이어서 데이터베이스에서 확인을 해봤더니 다음과 같았다.생성자를 안만들어줬더니 status code는 200이 뜨지만 실제로는 바뀌지 않았다는 걸 알 수 있었다. 생성자를 만들고 다시 위와 같은 실행 과정을 반복해보자.public class FruitUpdateRequest { long id; boolean isSold; // 추가 public FruitUpdateRequest(long id, boolean isSold) { this.id = id; this.isSold = isSold; } public long getId() { return id; } public boolean isSold() { return isSold; } }$ http -v PUT localhost:8080/api/v1/fruit id=3 isSold=TRUE아까는 0이었던 게 1로 바뀌었다. 그런데 왜 true false가 아니고 0, 1로 표시될까?mysql 메뉴얼TINYINT(1)와 동의어라고 한다. 하지만, 아직 남아있는게 많다. 주어진 요구사항은 "id"만 JSON형태로 반환하는 것이다. 그리고 예외처리도 해주어야 한다.메서드명 : updateSoldFruitInformation() 으로 변경sql 쿼리 : UPDATE fruit SET is_sold = 1 WHERE id = ? 으로 변경예외 처리 로직 작성@PutMapping("/fruit") public void updateSoldFruitInformation(@RequestBody FruitUpdateRequest request) { String readSql = "SELECT * FROM fruit WHERE id = ?"; boolean isEmpty = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty(); if (isEmpty) { throw new IllegalArgumentException(); } String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, request.getId()); }$ http -v PUT localhost:8080/api/v1/fruit id=4문제 3특정 과일을 기준을 팔린 금액, 팔리지 않은 금액을 조회하고 싶습니다.예를 들어(1, 사과, 3000원, 판매 O) (2, 사과, 4000원, 판매 X) (3, 사과, 3000원, 판매 O)와 같은 세 데이터가 있다면 우리의 API는 판매된 금액 : 6000원, 판매되지 않은 금액 4000원 이라고 응답해야 합니다. [스펙]HTTP method : GETHTTP path : /api/v1/fruit/statHTTP query name : 과일 이름예시 : GET /api/v1/fruit/stat?name=사과HTTP 응답 Body 예시{ "salesAmount": 6000, "notSalesAmount": 4000 } [설계]HTTP method 가 GET : @RestController -> @GetMappingHTTP path 가 /api/v1/fruit/stat : @RequestMapping("/api/v1") -> @GetMapping("/fruit/stat")반환타입 : JSON -> FruitReadSalesAmountRespond메서드명 : 팔리거나 안필린 금액 합계 -> readSalesFruitAmount() public class FruitReadSalesAmountRespond { private long salesAmount; private long notSalesAmount; public FruitReadSalesAmountRespond(long salesAmount, long notSalesAmount) { this.salesAmount = salesAmount; this.notSalesAmount = notSalesAmount; } public long getSalesAmount() { return salesAmount; } public long getNotSalesAmount() { return notSalesAmount; } }private long salesAmount; private long notSalesAmount; @GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { String sql = "SELECT * FROM fruit WHERE name = ?"; List query = jdbcTemplate.query(sql, (rs, rowNum) -> { long price = rs.getLong("price"); boolean isSold = rs.getBoolean("is_sold"); if (isSold) { salesAmount += price; } if (!isSold) { notSalesAmount += price; } return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); }, name); return new FruitReadSalesAmountRespond(query.get(query.size() - 1).getSalesAmount(), query.get(query.size() - 1).getNotSalesAmount()); }$ http -v GET ":8080/api/v1/fruit/stat?name=사과"1차적으로는 구현을 했다. 하지만, 이 메서드에서만 쓰이는 salesAmount와 notSalesAmount를 FruitController의 멤버변수에 선언하는 것은 컨트롤러의 규모가 늘어날 때마다 신경을 써줘야 하는 부분이 있다. 가독성 부분에서도 뜬금없이 튀어나와서 좋지 못하다. WHERE절에 AND를 써서 리팩터링 해보자.@GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { long salesAmount = 0; long notSalesAmount = 0; String salesAmountSql = "SELECT * FROM fruit WHERE name = ? AND is_sold = 1"; List salesPrices = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong("price"), name); String notSalesAmountSql = "SELECT * FROM fruit WHERE name = ? AND is_sold = 0"; List notSalesPrices = jdbcTemplate.query(notSalesAmountSql, (rs, rowNum) -> rs.getLong("price"), name); for (Long salesPrice : salesPrices) { salesAmount += salesPrice; } for (Long notSalesPrice : notSalesPrices) { notSalesAmount += notSalesPrice; } return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); } 위와 같이 지역변수로 선언할 수 있게 되었다. 하지만, 더해지는 과정을 코드로 계속 더하는거 보다 좋은 방법이 없을까? sql의 집계함수를 이용하면 된다.다음과 같은 쿼리를 날려보니 아래와 같은 결과가 나온다.즉, 첫번째가 판매된 사과들의 가격의 합, 두번째가 판매되지 않은 사과들의 가격의 합이다. 이걸 코드에 적용해보면 다음과 같다.@GetMapping("/fruit/stat") public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) { String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold"; List salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name); Long salesAmount = salesAmounts.get(0); Long notSalesAmount = salesAmounts.get(1); return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount); }
백엔드
・
인프런워밍업클럽