-
Notifications
You must be signed in to change notification settings - Fork 132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Spring Core] 박소정 미션 제출합니다. #310
base: sojeong0202
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
400번 대 오류가 발생하면 프론트에서는 어떻게 오류를 확인하나요?
- HTTP 응답 본문에 대해 학습하시면 도움이 될 것 같아요!
- 간단하게 알아보면 우리가 ResponseEntity를 통해 만들어 반환한 객체가 HTTP 응답 메시지로 변환되어 전송됩니다. 예를 들어 소정은
return ResponseEntity.noContent().build();
처럼 204를 반환하기 사용했어요. noContent 메서드를 살펴보면 아래와 같이 status를NO_CONTENT(204)
로 설정해요.
- 이렇게 소정이 설정한 상태코드나 본문(body) 등을 프론트도 똑같이 볼 수 있어요! 그러면 프론트엔드 개발자도 후속 조치를 취하거나 예외 처리를 진행할 수 있을거에요.
- 추가로 개발자 모드의 네트워크 탭을 이용하면 HTTP 요청 메시지나 응답 메시지를 찾아볼 수 있을 거에요.
여기에서 말하는 비즈니스 규칙 책임은 유효성 검증을 말하는 걸까요?
- 저도 비즈니스 규칙 책임 내부에는 유효성 검증이 있다고 생각해요! (소정과 같은 생각) 예를 들어, Product의 price 필드가 있다면 0 미만의 수는 불가능하도록 Product 스스로가 지킬 수 있도록 관련 코드를 작성해둘 것 같아요.
// 아주 간단하게 만든 예시
public class Product {
String name;
Long price;
public updatePrice(Long price) {
if (price < 0) { 불가능 }
this.price = price;
}
}
public TimeResponseDto createTime(TimeSaveRequestDto requestDto) { | ||
if (isTimeArgumentEmpty(requestDto)) { | ||
throw new IllegalArgumentException("잘못된 요청입니다."); | ||
} | ||
|
||
Time time = new Time(requestDto.getTime()); | ||
Long id = timeDAO.insert(time); | ||
|
||
return new TimeResponseDto(id, time); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DAO
에서 저장된 domain 객체 자체를 반환해주면 Time 객체를 새롭게 만드는 과정을 없앨 수 있을 것 같아요!
TimeService
에 있는 createTime()
이라는 메서드는 최대한 예약 시간을 생성하는 것에 집중하도록 작성하면 좋을 것 같아요! DAO 즉, 데이터 엑세스 객체(insert)가 domain 객체로 반환해준다면 좀 더 깔끔하게 작성할 수 있을 것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(제대로 한 건지는 확실하지 않지만) 수정했습니다
좋은 방법 알려주셔서 감사합니다.
한 가지 질문이 있습니다.
Time
객체를 반환하는 과정에서 TimeDAO
의 findById()
메소드를 사용했는데 이게 좋은 방법인지 확신이 없습니다!
차라리 String sql = "SELECT * FROM time WHERE id = ?";
이 sql을 상수로 두어 insert문에 사용할까도 생각해봤지만 findById()
메소드 내부 코드를 두 번 쓰는 것 같아 이것도 좋은 방법이 아닌 것 같았습니다.
아니면 두 개 다 버리고 다른 방법을 사용하는 게 좋을까요?
public List<TimeResponseDto> readAllTimes() { | ||
return timeDAO.findAllTimes() | ||
.stream() | ||
.map(TimeResponseDto::new) | ||
.collect(Collectors.toList()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public List<TimeResponseDto> readAllTimes() { | |
return timeDAO.findAllTimes() | |
.stream() | |
.map(TimeResponseDto::new) | |
.collect(Collectors.toList()); | |
} | |
public List<TimeResponseDto> readAllTimes() { | |
return timeDAO.findAllTimes() | |
.stream() | |
.map(TimeResponseDto::new) | |
.toList(); | |
} |
collect(Collectors.toList())
와toList()
의 차이를 학습하고 알려주세요!- 저는
toList()
가 UnmodifiableList를 반환하기 때문에 자주 사용해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤓 collect(Collectors.toList())
와 toList()
✍️ collect(Collectors.toList())
, collect(Collectors.toUnmodifiableList())
, toList()
표 비교
버전 | java8 |
java10 |
java16 |
---|---|---|---|
List 변환 방법 | .collect(Collectors.toList()) |
.collect(Collectors.toUnmodifiableList()) |
.toList() |
반환 타입 | ArrayList |
UnmodifiableList |
UnmodifiableList |
수정 가능 여부 | O | X | X |
Null 허용 여부 |
O | X | O |
collect(Collectors.toList())
-> toList()
변경의 장점
collect(Collectors.toList())
는 ArrayList
를 반환하고
toList()
는 Collectors.UnmodifiableList
또는 Collectors.UnmodifiableRandomAccessList
를 반환합니다.
두 반환 타입의 차이점은 수정 가능 여부입니다.
ArrayList
는 수정이 가능하고 UnmodifiableList
는 수정이 불가능합니다.
UnmodifiableList
는 수정하려고 하면 UnsupportedOperationException
예외가 발생합니다.
즉, UnmodifiableList
는 읽기 용도로만 사용 가능합니다.
Unmodifiable Collection
- 외부에서 변경 시 예외처리(
UnsupportedOperationException
)되기 때문에 예기치 않은 값 변경 방지합니다. - getter로 값을 꺼내도 데이터 수정 불가능합니다.
- 다만,
Unmodifiable
과Immutable
은 다릅니다.Unmodifiable
은 수정을 막는 것이 핵심 기능이므로 원본 객체와 주소가 공유되어 있고, 원본 객체가 변경되면 동일하게 영향을 받습니다.
참고 사이트
- https://velog.io/@cieroyou/Stream%EC%9D%84-List%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95%EA%B3%BC-%EC%B0%A8%EC%9D%B4Collectors.toList-vs-Stream.toList
- https://binux.tistory.com/146
- https://hello-judy-world.tistory.com/209
- https://tecoble.techcourse.co.kr/post/2021-04-26-defensive-copy-vs-unmodifiable/
추가로 공부해야 하는 키워드
- 방어적 복사, 깊은 복사, 얕은 복사
- 불변 객체
- copy.of
- https://pparksean.tistory.com/122?category=986364
https://ksh-coding.tistory.com/77
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.collect(Collectors.toList())
-> .toList()
로 코드도 수정했습니다!
public ReservationResponseDto createReservation(ReservationSaveRequestDto requestDto) { | ||
if (isReservationArgumentEmpty(requestDto)) { | ||
throw new IllegalArgumentException("잘못된 요청입니다."); | ||
} | ||
|
||
Reservation reservation = new Reservation( | ||
requestDto.getName(), | ||
requestDto.getDate(), | ||
timeDAO.findById(requestDto.getTime()).get()); | ||
Long id = reservationDAO.insert(reservation, reservation.getTime().getId()); | ||
|
||
return new ReservationResponseDto(id, reservation); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Long id = reservationDAO.insert(reservation, reservation.getTime().getId());
이 부분에서 만약 타입을 Long
이 아니라 'long' 으로 변경한다면 메모리 상에서 어떤 차이가 발생하나요? Long이 더 좋은 코드인가요? 소정이 학습하고 알려주세요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤓지역변수 타입에 대한 고찰 (Long
과 long
중에 어떤 것을 사용하는 것이 좋은가?)
원시 타입과 참조 타입 정의
- 원시 타입: 정수, 실수, 문자 등의 실제 데이터 값을 저장하는 타입
- 참조 타입: 객체의 메모리 번지를 참조하는 타입, 원시 타입을 제외한 타입들이 참조 타입에 해당
(부연 내용 참고)
원시 타입과 참조 타입(중 래퍼클래스)의 차이
- 원시 타입은 메모리에 실제 값을 저장
- 참조 타입은 주소를 저장
☑️참조 타입은null
사용이 가능!! - 사용하는 메모리양 비교
참조 타입이 원시 타입보다 압도적으로 사용하는 메모리양이 많음을 알 수 있습니다
원시 타입이 사용하는 메모리 | 참조 타입이 사용하는 메모리 |
---|---|
boolean - 1bit | Boolean – 128 bits |
byte - 8bits | Byte - 128bits |
short, char - 16bits | Short, Charater - 128bits |
int, float - 32bits | Integer, Float - 128bits |
long, double - 64bits | Long, Double - 196bits |
-
Boxing
과Unboxing
Boxing
: 원시 타입 -> 참조 타입 변환(Unboxing
은 역방향)
자바 1.5 이후부터Auto Boxing / Unboxing
기능이 추가되어 명시적으로 원시 타입을 참조 타입으로 감싸주지 않아도 자동으로Boxing/Unboxing
을 해줍니다
☑️Auto Boxing / Unboxing
기능은 메모리 누수의 원인이 될 수 있습니다 -
원시 타입과 래퍼 클래스 비교
결론적으로 래퍼 클래스는 원시 타입을 객체화한 것입니다.
(원시 타입의 객체화 = 래퍼 클래스)
-
Nullability
- 원시 타입은
null
값을 가질 수없으며
, 초기화되지 않으면 기본값은 0 입니다 - 래퍼 클래스는
null
값을 가질 수있습니다
(위에 나왔듯이 참조 타입이므로)
- 원시 타입은
-
Overhead
- 원시 타입은 메모리 및 성능 면에서 추가적인 오버헤드X
- 래퍼 클래스는 객체이므로 추가적인 메모리 및 성능 오버헤드의 가능성 O
-
Boxing and Unboxing
- 래퍼 클래스는
Auto Boxing / Unboxing
으로 런타임에 약간의 성능 저하 가능성 O
- 래퍼 클래스는
-
엔터티 변겅 감지
- JPA와 같은 ORM에서는 엔터티의 변경을 감지하기 위해 필드 값의 변경을 추적하는데,
Integer와 같은 래퍼 타입의 경우, 두 개의 객체가 동일한 값을 가지더라도 다른 객체로 인식될 수 있어서 변경 감지에 영향을 줄 수 있습니다.
- JPA와 같은 ORM에서는 엔터티의 변경을 감지하기 위해 필드 값의 변경을 추적하는데,
공부한 내용을 바탕으로 내린 결론
Entity에서는 DB 컬럼이 null
을 허용하는 경우 때문에 래퍼 클래스를 사용하는 것이 적절합니다.
그러나
Long id = reservationDAO.insert(reservation, reservation.getTime().getId());
해당 코드는 DB에 삽입하고 삽입된 레코드의 id값을 반환합니다. id 값은 not null
이고 자동적으로 생성해서 반환하기 때문에 필연적으로 값이 들어갈 수밖에 없습니다.
즉, null
값이 들어갈 수 없으므로 원시 타입인 long
을 써도 안전합니다.
그렇다면 메모리 측면에서
사용하는 메모리 양이 적은 원시 타입 long
을 사용하는 것이 좋습니다. 또한, long
을 사용하므로써 Auto Boxing / Unboxing
기능을 사용하지 않으므로 메모리 누수도 방지할 수 있습니다.
부연 내용
스택
과힙
영역
- 정적 메모리
스택
영억
기본 타입 변수가 할당되며, 변수의 실제 값 저장
참조 타입의 변수들은 이스택
영역에서힙
영역에 생성된 객체들의 주소 값을 저장 - 동적 메모리
힙
영억
☑️참조 타입들이힙
영역에 생성된 이 객체들의 주소를스택
영역에 저장
참고한 사이트
- https://velog.io/@gillog/%EC%9B%90%EC%8B%9C%ED%83%80%EC%9E%85-%EC%B0%B8%EC%A1%B0%ED%83%80%EC%9E%85Primitive-Type-Reference-Type
- https://annajin.tistory.com/55
- https://wildeveloperetrain.tistory.com/12
- https://warpgate3.tistory.com/entry/JPA-ENTITY-%EC%97%90%EC%84%9C-%EC%9B%90%EC%8B%9C%ED%83%80%EC%9E%85%EA%B3%BC-Wrapper-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%A4%91-%EB%AD%98-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%EB%90%98%EB%82%98
추가로 공부해야 하는 내용
Auto Boxing / Unboxing
기능은 메모리 누수의 원인이 되는 이유
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
코드도 곧 수정하겠습니다!👀
private boolean isStringEmpty(String argument) { | ||
return argument == null || argument.trim().isEmpty(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty가 포함된 메서드 명을 봤을 때는 빈 문자열 또는 공백 문자로만 이루어진 문자열인지 확인하는 메서드로 느껴지는데 null 체크까지 넣은 소정만의 이유가 있나요? (또는 메서드명 작명 이유) 알려주세요!
public Time insert(Time time) { | ||
String sql = "INSERT INTO time (time) VALUES ?"; | ||
KeyHolder keyHolder = new GeneratedKeyHolder(); | ||
|
||
jdbcTemplate.update(connect -> { | ||
PreparedStatement ps = connect.prepareStatement( | ||
sql, | ||
new String[]{"id"}); | ||
ps.setString(1, time.getTime()); | ||
return ps; | ||
}, keyHolder); | ||
|
||
return findById(keyHolder.getKey().longValue()).get(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public Time insert(Time time) { | |
String sql = "INSERT INTO time (time) VALUES ?"; | |
KeyHolder keyHolder = new GeneratedKeyHolder(); | |
jdbcTemplate.update(connect -> { | |
PreparedStatement ps = connect.prepareStatement( | |
sql, | |
new String[]{"id"}); | |
ps.setString(1, time.getTime()); | |
return ps; | |
}, keyHolder); | |
return findById(keyHolder.getKey().longValue()).get(); | |
} | |
public Time insert(Time time) { | |
String sql = "INSERT INTO time (time) VALUES ?"; | |
KeyHolder keyHolder = new GeneratedKeyHolder(); | |
jdbcTemplate.update(connect -> { | |
PreparedStatement ps = connect.prepareStatement( | |
sql, | |
new String[]{"id"}); | |
ps.setString(1, time.getTime()); | |
return ps; | |
}, keyHolder); | |
Long generatedId = keyHolder.getKey().longValue(); | |
return new Time(generatedId, time.getTime()); | |
} |
generatedId
가 정상적으로 반환되었다는 것은 잘 삽입되었다는 것이니 id만 활용해서 바로 객체를 만들고 반환하면findById()
메서드를 사용하지 않아도 될 것 같아요! 소정의 생각은 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sojeong0202 다시 질문하신거 답변했어요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그러네요, 이 메소드 주요 목적인 DB 삽입은 잘 되었으니
굳이 DB에서 찾아 가져올 필요 없이 keyHolder와 삽입에 사용한 Time
객체의 필드 값으로 새로운 객체를 만들어 반환하면 되겠군요!
좋은 방법 알려주셔서 감사합니다!! 수정했습니다
초록스터디에서 spring bean에 대해서 학습했는데, |
@GetMapping("/reservation") | ||
public String goReservationPage() { | ||
return "new-reservation"; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전 viewcontroller
를 만들어서 매핑을 해줘서 뭐가 좋을지 찾아봤는데, 각각의 장단점이 있는 것 같더군요.
ViewController
를 따로 만드는 경우
장점
- 분리된 책임: View 관련 로직과 API 관련 로직을 분리하여 관리하기 쉬워집니다.
- 유지보수 용이: 코드의 책임이 분리되므로 특정 부분의 코드만 변경해도 되며, 변경의 영향 범위가 작아집니다.
- 가독성: 코드가 분리되므로 각 클래스가 더 짧아지고 가독성이 향상됩니다.
단점
- 파일 수 증가: 클래스를 더 많이 만들어야 하므로 파일 수가 증가합니다.
- 간단한 프로젝트에서는 오버헤드
기존 Controller
에 코드를 추가하는 경우
장점
- 간단한 구조: 모든 관련 로직이 하나의 클래스에 있어 단순합니다.
- 빠른 개발: 작은 프로젝트에서는 빠르게 개발할 수 있습니다.
단점
- 책임 과부하: 하나의 클래스가 너무 많은 책임을 가지게 되어 복잡도가 증가할 수 있습니다.
- 유지보수 어려움: 코드가 길어지고 복잡해지면서 유지보수가 어려워질 수 있습니다.
- 가독성 저하: 다양한 로직이 섞이면서 가독성이 떨어질 수 있습니다.
프로젝트의 크기에 따라 viewcontroller
를 만드는 것도 고려해보면 좋을 것 같습니다!
} | ||
|
||
@DeleteMapping("/times/{id}") | ||
public ResponseEntity<Void> deleteTime(@PathVariable Long id) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@PathVariable
사용 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굳~~ 좋아요~~
public class Time { | ||
|
||
private Long id; | ||
private String time; | ||
|
||
public Time() { | ||
} | ||
|
||
public Time(String time) { | ||
this.time = time; | ||
} | ||
|
||
public Time(Long id, String time) { | ||
this.id = id; | ||
this.time = time; | ||
} | ||
|
||
public Long getId() { | ||
return id; | ||
} | ||
|
||
public String getTime() { | ||
return time; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기본 생성자의 접근 지정자가 public이라서 time 필드 없이 생성은 가능한데 time 필드를 채워주는 메서드가 없는 것 같아요! time 필드를 채워주는 메서드(setter)를 만드는 것보단 접근 지정자를 protected나 private으로 정상적으로 사용될 수 없는 객체가 만들어지는 경우는 의도적으로 막을 수 있을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니당~~
public ReservationResponseDto(Reservation reservation) { | ||
this.id = reservation.getId(); | ||
this.name = reservation.getName(); | ||
this.date = reservation.getDate(); | ||
this.time = reservation.getTime(); | ||
this.time = new TimeResponseDto(reservation.getTime()); | ||
} | ||
|
||
public ReservationResponseDto(Long id, Reservation reservation) { | ||
this.id = id; | ||
this.name = reservation.getName(); | ||
this.date = reservation.getDate(); | ||
this.time = reservation.getTime(); | ||
this.time = new TimeResponseDto(reservation.getTime()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
생성자의 역할이 어느정도 비슷하고 중복이 되는데 두 번째 생성자에서
public ReservationResponseDto(Long id, Reservation reservation) {
this(reservation);
this.id = id;
}
이렇게 수정해보는 건 어떨까 싶어요! 소정은 어떻게 생각하시나용?
✍️구현 내용
😱어려웠던 점
계속 @RequestBody DTO에 null이 들어가는 문제가 발생했습니다.
역직렬화에서 문제가 발생했고(참고: https://heejjeoyi.tistory.com/175),
json 형식으로는
"time"
이라고 들어오는데 DTO 필드명을timeId
로 설정한 것이 원인이었습니다.자바스크립트 파일까지 확인한 다음에야 원인을 알아서,
프론트와 의사소통이 중요하겠구나 생각하게 되었습니다...
🧐 궁금한 점
포스트맨 돌리나요?
서버에서는 4xx 오류가 발생하면 테스트 코드 아니면 오류 메세지가 안 떠서,
디버깅을 통해 원인을 알게 된 후 문득 궁금해져 질문합니당