Skip to content
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

Open
wants to merge 8 commits into
base: sojeong0202
Choose a base branch
from

Conversation

sojeong0202
Copy link

@sojeong0202 sojeong0202 commented Jul 17, 2024

✍️구현 내용

  • 시간 CRUD API 구현
  • 시간 테이블에 저장된 값으로만 예약 추가 구현
  • 레이어드 아키텍처 패턴 적용

😱어려웠던 점

  • 9단계에서 많이 헤맸습니다.
    계속 @RequestBody DTO에 null이 들어가는 문제가 발생했습니다.
    역직렬화에서 문제가 발생했고(참고: https://heejjeoyi.tistory.com/175),
    json 형식으로는 "time"이라고 들어오는데 DTO 필드명을 timeId로 설정한 것이 원인이었습니다.
    자바스크립트 파일까지 확인한 다음에야 원인을 알아서,
    프론트와 의사소통이 중요하겠구나 생각하게 되었습니다...

🧐 궁금한 점

  • 400번 대 오류가 발생하면 프론트에서는 어떻게 오류를 확인하나요?
    포스트맨 돌리나요?
    서버에서는 4xx 오류가 발생하면 테스트 코드 아니면 오류 메세지가 안 떠서,
    디버깅을 통해 원인을 알게 된 후 문득 궁금해져 질문합니당
image
  • 여기에서 말하는 비즈니스 규칙 책임은 유효성 검증을 말하는 걸까요?

Copy link

@YehyeokBang YehyeokBang left a 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)로 설정해요.
image
  • 이렇게 소정이 설정한 상태코드나 본문(body) 등을 프론트도 똑같이 볼 수 있어요! 그러면 프론트엔드 개발자도 후속 조치를 취하거나 예외 처리를 진행할 수 있을거에요.
  • 추가로 개발자 모드의 네트워크 탭을 이용하면 HTTP 요청 메시지나 응답 메시지를 찾아볼 수 있을 거에요.

여기에서 말하는 비즈니스 규칙 책임은 유효성 검증을 말하는 걸까요?

  • 저도 비즈니스 규칙 책임 내부에는 유효성 검증이 있다고 생각해요! (소정과 같은 생각) 예를 들어, Product의 price 필드가 있다면 0 미만의 수는 불가능하도록 Product 스스로가 지킬 수 있도록 관련 코드를 작성해둘 것 같아요.
// 아주 간단하게 만든 예시
public class Product {
    String name;
    Long price;
    
    public updatePrice(Long price) {
        if (price < 0) { 불가능 }
        this.price = price;
    }
}

Comment on lines 20 to 29
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);
}

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 객체로 반환해준다면 좀 더 깔끔하게 작성할 수 있을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(제대로 한 건지는 확실하지 않지만) 수정했습니다
좋은 방법 알려주셔서 감사합니다.

한 가지 질문이 있습니다.

Time 객체를 반환하는 과정에서 TimeDAOfindById() 메소드를 사용했는데 이게 좋은 방법인지 확신이 없습니다!

차라리 String sql = "SELECT * FROM time WHERE id = ?"; 이 sql을 상수로 두어 insert문에 사용할까도 생각해봤지만 findById()메소드 내부 코드를 두 번 쓰는 것 같아 이것도 좋은 방법이 아닌 것 같았습니다.

아니면 두 개 다 버리고 다른 방법을 사용하는 게 좋을까요?

Comment on lines 31 to 36
public List<TimeResponseDto> readAllTimes() {
return timeDAO.findAllTimes()
.stream()
.map(TimeResponseDto::new)
.collect(Collectors.toList());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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를 반환하기 때문에 자주 사용해요!

Copy link
Author

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로 값을 꺼내도 데이터 수정 불가능합니다.
  • 다만, UnmodifiableImmutable은 다릅니다. Unmodifiable수정을 막는 것이 핵심 기능이므로 원본 객체와 주소가 공유되어 있고, 원본 객체가 변경되면 동일하게 영향을 받습니다.

참고 사이트


추가로 공부해야 하는 키워드

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.collect(Collectors.toList()) -> .toList()로 코드도 수정했습니다!

Comment on lines +23 to +35
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);
}

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이 더 좋은 코드인가요? 소정이 학습하고 알려주세요!

Copy link
Author

@sojeong0202 sojeong0202 Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤓지역변수 타입에 대한 고찰 (Longlong 중에 어떤 것을 사용하는 것이 좋은가?)

원시 타입과 참조 타입 정의

  • 원시 타입: 정수, 실수, 문자 등의 실제 데이터 값을 저장하는 타입
  • 참조 타입: 객체의 메모리 번지를 참조하는 타입, 원시 타입을 제외한 타입들이 참조 타입에 해당
    (부연 내용 참고)

원시 타입과 참조 타입(중 래퍼클래스)의 차이

  • 원시 타입은 메모리에 실제 을 저장
  • 참조 타입은 주소를 저장
    ☑️참조 타입은 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
  • BoxingUnboxing
    Boxing: 원시 타입 -> 참조 타입 변환(Unboxing은 역방향)
    자바 1.5 이후부터 Auto Boxing / Unboxing 기능이 추가되어 명시적으로 원시 타입을 참조 타입으로 감싸주지 않아도 자동으로 Boxing/Unboxing을 해줍니다
    ☑️Auto Boxing / Unboxing 기능은 메모리 누수의 원인이 될 수 있습니다

  • 원시 타입과 래퍼 클래스 비교
    결론적으로 래퍼 클래스는 원시 타입을 객체화한 것입니다.
    (원시 타입의 객체화 = 래퍼 클래스)

  1. Nullability

    • 원시 타입은 null 값을 가질 수 없으며, 초기화되지 않으면 기본값은 0 입니다
    • 래퍼 클래스는 null값을 가질 수 있습니다(위에 나왔듯이 참조 타입이므로)
  2. Overhead

    • 원시 타입은 메모리 및 성능 면에서 추가적인 오버헤드X
    • 래퍼 클래스는 객체이므로 추가적인 메모리 및 성능 오버헤드의 가능성 O
  3. Boxing and Unboxing

    • 래퍼 클래스는 Auto Boxing / Unboxing 으로 런타임에 약간의 성능 저하 가능성 O
  4. 엔터티 변겅 감지

    • JPA와 같은 ORM에서는 엔터티의 변경을 감지하기 위해 필드 값의 변경을 추적하는데,
      Integer와 같은 래퍼 타입의 경우, 두 개의 객체가 동일한 값을 가지더라도 다른 객체로 인식될 수 있어서 변경 감지에 영향을 줄 수 있습니다.

공부한 내용을 바탕으로 내린 결론

Entity에서는 DB 컬럼이 null을 허용하는 경우 때문에 래퍼 클래스를 사용하는 것이 적절합니다.
그러나

Long id = reservationDAO.insert(reservation, reservation.getTime().getId());

해당 코드는 DB에 삽입하고 삽입된 레코드의 id값을 반환합니다. id 값은 not null이고 자동적으로 생성해서 반환하기 때문에 필연적으로 값이 들어갈 수밖에 없습니다.
즉, null 값이 들어갈 수 없으므로 원시 타입인 long을 써도 안전합니다.
그렇다면 메모리 측면에서
사용하는 메모리 양이 적은 원시 타입 long을 사용하는 것이 좋습니다. 또한, long을 사용하므로써 Auto Boxing / Unboxing 기능을 사용하지 않으므로 메모리 누수도 방지할 수 있습니다.

부연 내용

  1. 스택 영역
  • 정적 메모리 스택 영억
    기본 타입 변수가 할당되며, 변수의 실제 값 저장
    참조 타입의 변수들은 이 스택 영역에서 영역에 생성된 객체들의 주소 값을 저장
  • 동적 메모리 영억
    ☑️참조 타입들이 영역에 생성된 이 객체들의 주소를 스택 영역에 저장
  1. 원시 타입과 래퍼 클래스 종류
    image

참고한 사이트

추가로 공부해야 하는 내용

  • Auto Boxing / Unboxing 기능은 메모리 누수의 원인이 되는 이유

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드도 곧 수정하겠습니다!👀

Comment on lines +53 to +55
private boolean isStringEmpty(String argument) {
return argument == null || argument.trim().isEmpty();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty가 포함된 메서드 명을 봤을 때는 빈 문자열 또는 공백 문자로만 이루어진 문자열인지 확인하는 메서드로 느껴지는데 null 체크까지 넣은 소정만의 이유가 있나요? (또는 메서드명 작명 이유) 알려주세요!

Comment on lines 22 to 35
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();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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() 메서드를 사용하지 않아도 될 것 같아요! 소정의 생각은 어떤가요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sojeong0202 다시 질문하신거 답변했어요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러네요, 이 메소드 주요 목적인 DB 삽입은 잘 되었으니
굳이 DB에서 찾아 가져올 필요 없이 keyHolder와 삽입에 사용한 Time 객체의 필드 값으로 새로운 객체를 만들어 반환하면 되겠군요!

좋은 방법 알려주셔서 감사합니다!! 수정했습니다

@ihwag719
Copy link

ihwag719 commented Jul 25, 2024

초록스터디에서 spring bean에 대해서 학습했는데, @Autowired에 대해 학습하고 test 코드 뿐만 아니라 소정이 구현한 코드에도 적용해보는 것도 좋을 것 같습니다!

@GetMapping("/reservation")
public String goReservationPage() {
return "new-reservation";
}

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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PathVariable 사용 👍

Copy link

@YehyeokBang YehyeokBang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굳~~ 좋아요~~

Comment on lines +3 to +27
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;
}
}

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으로 정상적으로 사용될 수 없는 객체가 만들어지는 경우는 의도적으로 막을 수 있을 것 같아요!

shinheekim

This comment was marked as resolved.

Copy link

@shinheekim shinheekim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니당~~

Comment on lines 12 to 24
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());
}
Copy link

@shinheekim shinheekim Jul 26, 2024

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;
}

이렇게 수정해보는 건 어떨까 싶어요! 소정은 어떻게 생각하시나용?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants