-
Notifications
You must be signed in to change notification settings - Fork 81
[그리디] 강동현 Spring JPA (1차) 4, 5, 6 단계 미션 제출합니다. #201
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
base: main
Are you sure you want to change the base?
Conversation
|
안녕하세요, 동현님🙂 많이 고민하시고 코드를 작성하신 게 눈에 잘 보여서 인상 깊게 봤습니다 :) 잘 해주셨지만 코드 잘 읽어보면서 몇 가지 사소한 개선 포인트가 보여서 <피드백>1. 사용하지 않는 메서드 및 import문 존재현재 사용되지 않는 메서드나 import 문은 정리해주시면 2. 반복되는 getter 메서드반복되는 getter 메서드는 Lombok의 @Getter를 활용해보셔도 좋을 것 같습니다. 3. Dto 관리 포인트DTO를 사용하신다면, 관련 DTO들을 한 위치에 모아두거나 4. record 사용 권장단순한 응답/전달용 DTO의 경우, record를 활용해보면 작은 제안들이라 꼭 반영하지 않으셔도 되고, <질문>
|
|
|
||
| public interface MemberRepository extends JpaRepository<Member, Long> { | ||
| Optional<Member> findByEmailAndPassword(String email, String password); | ||
| Optional<Member> findByName(String name); |
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 Long getReservationId() { | ||
| return reservationId; | ||
| } | ||
|
|
||
| public String getTheme() { | ||
| return theme; | ||
| } | ||
|
|
||
| public String getDate() { | ||
| return date; | ||
| } | ||
|
|
||
| public String getTime() { | ||
| return time; | ||
| } | ||
|
|
||
| public String getStatus() { | ||
| return status; | ||
| } | ||
|
|
||
| @JsonIgnore | ||
| public Long getId() { | ||
| return reservationId; | ||
| } | ||
| } |
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.
혹시 Lombok의 @Getter 어노테이션을 사용해보셨나요?
이를 활용하면 각 필드에 대한 get 메서드를 일일이 작성하지 않아도 되어 코드가 훨씬 간결해집니다.
지금처럼 단순 값 반환인 경우, 어노테이션 사용을 추천드립니다 :)
| @Override | ||
| public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { | ||
| String token = JwtUtil.extractTokenFromCookies(request.getCookies()); | ||
| if (token.isEmpty()) { | ||
| response.setStatus(401); | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| Claims claims = JwtUtil.parseClaims(token, secretKey); | ||
| String role = claims.get("role", String.class); | ||
| if (!"ADMIN".equals(role)) { | ||
| response.setStatus(401); | ||
| return false; | ||
| } | ||
| return true; | ||
| } catch (Exception e) { | ||
| response.setStatus(401); | ||
| return false; | ||
| } | ||
| } | ||
| } |
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.
오류를 잘 걸러주셨어요 :)
다만 살짝 아쉬운 점은 문제상황에 같은 오류코드를 반환하고 있네요.
현재 토큰이 없는 경우와 권한이 없는 경우의 모든 실패 케이스에서 401을 반환하고 있는 데
이럴 때는 오류가 생겼을 때 오류의 원인이 모호할 수 있어요.
상황에 따라 상태코드를 분리해보는 건 어떨까요?
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.
또 PageController를 보아하니 동현님께서 응집화에 많이 신경쓰고 계신 거 같아 하나 더 알려 드리자면,
개인적으로는 에러 또한 메시지를 한 곳에서 함께 관리하는 편이 유지보수에도 더 좋고, 확장할 때도 안정적이라고 느껴졌어요.
그래서 error 관련 내용을 enum으로 통일해서 관리해보는 건 어떨까 조심스럽게 제안드립니다 😊
ex)
@Getter
@AllArgsConstructor
public enum FailMessage {
//400
BAD_REQUEST(HttpStatus.BAD_REQUEST, 40000, "잘못된 요청입니다."),
BAD_REQUEST_REQUEST_BODY_VALID(HttpStatus.BAD_REQUEST, 40001, "잘못된 요청본문입니다."),
BAD_REQUEST_MISSING_PARAM(HttpStatus.BAD_REQUEST, 40002, "필수 파라미터가 없습니다."),
BAD_REQUEST_METHOD_ARGUMENT_TYPE(HttpStatus.BAD_REQUEST, 40003, "메서드 인자타입이 잘못되었습니다."),
BAD_REQUEST_NOT_READABLE(HttpStatus.BAD_REQUEST, 40004, "Json 오류 혹은 요청본문 필드 오류 입니다. ");
private final HttpStatus httpStatus;
private final int code;
private final String message;
}| package roomescape.auth; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import jakarta.servlet.http.Cookie; |
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.
안 쓰는 import문도 지워주는 것이 좋아요!
| public class LoginMember { | ||
| private Long id; | ||
| private String name; | ||
| private String email; | ||
| private String role; |
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.
처음에는 이 객체가 어떤 역할을 하는 지 바로 와닿지 않았는 데, 쓰임을 보아하니 DTO 성격의 객체인 거 같습니다!
보통 DTO의 경우 LoginRequestDto처럼 클래스명에 Dto를 명시해 주는 경우가 많은데,
이렇게 하면 해당 객체가 엔티티인지, 전달용 객체인지를 빠르게 구분하기 쉽기때문이죠.
지금은 외부 api를 사용하는 프로젝트가 아니라서 필수는 아니지만,
그래도 이런 관례가 있다는 걸 알아두면 좋을 거 같아 남겨둡니다!
| public String createToken(Member member) { | ||
| return Jwts.builder() | ||
| .setSubject(member.getId().toString()) | ||
| .claim("name", member.getName()) | ||
| .claim("role", member.getRole()) | ||
| .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
| .compact(); | ||
| } | ||
|
|
||
|
|
||
| public String createTokenFromEmailAndPassword(String email, String password) { | ||
| Member member = memberService.login(email, password); | ||
| return createToken(member); | ||
| } | ||
|
|
||
|
|
||
| private String extractTokenFromCookie(Cookie[] cookies) { | ||
| if (cookies == null || cookies.length == 0) { | ||
| return ""; | ||
| } | ||
| for (Cookie cookie : cookies) { | ||
| if ("token".equals(cookie.getName())) { | ||
| return cookie.getValue(); | ||
| } | ||
| } | ||
| return ""; | ||
| } |
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.
(1) 동현님은 컨트롤러의 역할/책임을 어디까지로 보고 계신가요?
(2) 지금 컨트롤러에 토큰 생성/파싱/쿠키 추출 로직이 같이 들어가 있는데, 혹시 JwtUtil로 분리하지 않고 컨트롤러에 둔 이유가 있을까요?
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.
(3) 토큰 생성에 토큰 만료시간을 따로 설정 하지 않은 거 같은데요.
이 경우 토큰이 사실상 무기한 유효해질 수 있습니다. 이는 운영 관점에서 어떤 문제가 생길 수 있을까요?
| public class MyReservationResponse { | ||
| private Long reservationId; | ||
| private String theme; | ||
| private String date; | ||
| private String time; | ||
| private String status; |
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.
record를 사용해보신 적이 있을까요?
DTO의 경우 많은 분들이 record를 사용하곤 합니다.
getter와 생성자 같은 보일러플레이트 코드를 줄일 수 있고, 불변 데이터라는 의도가 코드로 명확히 드러난다는 장점이 있기 때문인데요.
물론 현재처럼 클래스로 사용해도 전혀 문제는 없지만,
한 번 직접 사용해보면 어떤 점이 더 편한지 체감하기 쉬워서
시간이 되신다면 record로 한 번 바꿔보는 것도 권장드려요 🙂
| @Service | ||
| public class MemberService { | ||
| private MemberDao memberDao; | ||
| private MemberRepository memberRepository; |
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.
Service 계층은 여러 요청이 동시에 접근할 수 있어서 가변 상태를 줄이는 것도 중요해요.
생성자 주입을 사용하고 있으니, 의존성 필드에 final을 붙여 불변으로 두는 건 어떨까요?
| public ReservationResponse save(ReservationRequest reservationRequest, Long loginMemberId) { | ||
| Time time = timeRepository.findById(reservationRequest.getTime()).orElseThrow(); | ||
| Theme theme = themeRepository.findById(reservationRequest.getTheme()).orElseThrow(); | ||
| Reservation reservation = new Reservation(reservationRequest.getName(), reservationRequest.getDate(), time, theme); | ||
| if (loginMemberId != null) { | ||
| Member memberRef = memberRepository.getReferenceById(loginMemberId); | ||
| reservation.setMember(memberRef); | ||
| } | ||
| reservation = reservationRepository.save(reservation); | ||
|
|
||
| return new ReservationResponse(reservation.getId(), reservation.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); |
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.
여러 레포지토리를 사용하는 서비스 메서드라 @transactional로 경계를 잡아주면 더 안전할 것 같아요.!
안녕하세요 신혜빈 리뷰어님!
새해 복 많이 받으세요!
이번에도 좋은 티키타카가 오갔으면 좋겠습니다!
변경한 파일
MyReservationResponse
초기 구현에서는 주어진 테스트 코드에 코드를 맞추기 위해 getId() 메서드를 두었는데,
Jackson 직렬화 과정에서 해당 getter가 자동으로 감지되어 의도하지 않게 id 필드가 JSON 응답에 함께 포함되는 문제가 발생했습니다.
따라서 자바 객체 내부에서는 getId()를 유지하면서JSON 응답에서는 id를 노출하지 않도록 @JsonIgnore를 적용해 직렬화 대상에서만 제외했습니다.
이번 미션에서는 구현 양이 방대해, 중간부터는 흐름을 놓치고 당장의 기능구현에 급급했던 것 같습니다..!
리뷰해주시면서 어색하거나 구조나 흐름 측면에서 보완이 필요해 보이는 부분이 있다면 의견 주시면 충분히 고민한 뒤 반영하겠습니다!!
감사합니다!