-
Notifications
You must be signed in to change notification settings - Fork 178
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 장바구니 - 1단계] 연로그(권시연) 미션 제출합니다. #31
Conversation
- 비밀번호 조합 조건 수정 - 비밀번호 조합 테스트 케이스 추가
- 하드 코딩 제거 - 메서드 분리
- 불필요한 import문 삭제
- account로 조회하는 경우 Optional 반환 - TokenResponse 위치 변경 - 레거시 코드 username -> account 명칭 변경
- 잘못된 예외 문구 수정
- 하드 코딩 제거 - 메서드 이름 변경 - 메서드 분리 - given, when, then 분리 - 줄바꿈 추가/제거
- 메서드 이름 변경 - 클래스 이름 변경 - 줄바꿈 추가/제거
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단계 미션 구현 잘 해주셨습니다!
몇 가지 코멘트를 남겼으니 확인 부탁드려요~ 질문은 편하게 DM 주세요!
@Override | ||
public void addInterceptors(InterceptorRegistry registry) { | ||
registry.addInterceptor(authenticationInterceptor()) | ||
.addPathPatterns("/customers/**"); | ||
} | ||
|
||
@Bean | ||
public AuthenticationInterceptor authenticationInterceptor() { | ||
return new AuthenticationInterceptor(jwtTokenProvider); | ||
} | ||
|
||
@Bean | ||
public AuthenticationPrincipalArgumentResolver createAuthenticationPrincipalArgumentResolver() { | ||
return new AuthenticationPrincipalArgumentResolver(authService); | ||
return new AuthenticationPrincipalArgumentResolver(jwtTokenProvider); | ||
} |
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.
Interceptor와 ArgumentResolver를 사용해서 적절하게 책임을 나누어주셨군요 👍
이 두 객체가 꼭 스프링이 관리하는 Bean이여야하는지 한 번 고민해보시면 좋을 것 같습니다.
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.
큰 의미를 갖고 짠 코드가 아닌데 빈 등록의 의미가..... 없는 것 같아요😅
애초에 addInterceptors/addArgumentResolvers 를 서버가 구동될 때 단 한번만 호출할거라는 생각이 드네요
싱글톤으로 관리할 필요도 없고 의존 관계를 주입할 일도 없고 필요 없다고 판단했습니다!
리팩터링 때 제거하겠습니다 :D
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.
맞습니다. 현재 스프링의 빈 등록 방식은 크게 2가지가 많이 사용되는데요, 하나는 @Component
를 이용한 방식이고 다른 하나는 @Configuration
과 @Bean
을 활용한 방식입니다. 후자의 가장 큰 장점은 명시적으로 원하는 객체만을 빈으로 등록할 수 있다는 점이에요. 전자는 의존 관계를 갖는 모든 클래스들을 빈으로 등록해줘야 하죠.
이 글을 한 번 읽어보시면 도움이 될 것 같습니다 :)
public static String extract(HttpServletRequest request) { | ||
Enumeration<String> headers = request.getHeaders(AUTHORIZATION); | ||
while (headers.hasMoreElements()) { | ||
String value = headers.nextElement(); | ||
if ((value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase()))) { | ||
String authHeaderValue = value.substring(BEARER_TYPE.length()).trim(); | ||
request.setAttribute(ACCESS_TOKEN_TYPE, value.substring(0, BEARER_TYPE.length()).trim()); | ||
int commaIndex = authHeaderValue.indexOf(','); | ||
if (commaIndex > 0) { | ||
authHeaderValue = authHeaderValue.substring(0, commaIndex); | ||
} | ||
return authHeaderValue; | ||
public static String extract(String value) { | ||
if ((value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase()))) { | ||
String authHeaderValue = value.substring(BEARER_TYPE.length()).trim(); | ||
int commaIndex = authHeaderValue.indexOf(','); | ||
if (commaIndex > 0) { | ||
authHeaderValue = authHeaderValue.substring(0, commaIndex); |
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.
기존 코드를 적절하게 리팩토링해주셨군요 💯
다만, 이 메서드가 기존과 동일한 동작을 하는지 검증이 필요하다는 생각이 드네요 😄
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.
완전히 동일한 동작을 하진 않았지만 코드를 개선하면서 필요에 맞게 개선되었습니다!
NativeWebRequest를 HttpServletRequest로 변환하는 것보다 NativeWebRequest에서 특정 값을 꺼내고 해당 값을 보내주는게 더 편하다고 생각했어요
검색하다보니 아래 코드를 통해서 편하게 변환되기는 하네요🤔
webRequest.getNativeRequest(HttpServletRequest.class);
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.
조금 더 생각하다보니 Authorization이 아닌 다른 위치에서 값을 꺼내서 보낼 수도 있다는 생각이 들어요
HttpServletRequest를 보내는 방향으로 리팩터링 했습니다!
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.
리팩토링 잘 해주셨어요 💯
다만, 코멘트의 의도는 테스트 코드를 작성하면 좋을 것 같다는 의도였어요.
기존 코드를 리팩토링할 때, 테스트 코드 작성 -> 리팩토링의 순서대로 하면 안정적으로 리팩토링이 가능합니다!
return Jwts.parserBuilder() | ||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody() | ||
.getSubject(); | ||
} | ||
|
||
public boolean validateToken(String token) { | ||
public void validateToken(String token) { | ||
try { | ||
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); | ||
|
||
return !claims.getBody().getExpiration().before(new Date()); | ||
} catch (JwtException | IllegalArgumentException e) { | ||
return false; | ||
Jwts.parserBuilder() | ||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) | ||
.build().parseClaimsJws(token); |
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.
중복되는 로직들은 메서드 분리를 할 수 있지 않을까요?
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) | ||
@SuppressWarnings("NonAsciiCharacters") | ||
@DisplayName("JWT 기능 관련") | ||
class JwtTokenProviderTest { |
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 void encrypt(PasswordEncoder passwordEncoder) { | ||
value = passwordEncoder.encrypt(value); | ||
} |
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.
- 암호화 된/되지 않은 텍스트 구분을 위한 정적 팩터리 메서드 생성
- Password.encrypt()와 Password.plain() 딱 봤을때 뭐가뭔지 혼동이 옴 (경험담)- Password는 인터페이스화 시키고 이를 구현한 EncodePassword, RawPassword 따로 생성
- 과한 설계처럼 느껴짐- 암호화 작업을 Password 밖에서 수행
- 글자수 체크하는 등의 validate 작업도 Password 밖에서 수행해야함- 암호화 관련 클래스를 의존 관계로 주입하고 encrypt() 메서드 따로 수행 필요 (현재 선택한 방법)
- 현재는 Password 값 객체가 가변이기 때문에 암호화된 값과 평문이 모두 들어갈 수 있는 구조에요. Password 객체를 사용하는 입장에서 객체 내부에 평문이 들어가있을지, 암호화된 값이 들어가있을지 예측할 수 있을까요?
- Password를 단방향으로 암호화하는 이유는 평문을 보면 안 되기 때문이겠죠. 그래서 Password 객체가 갖고 있어야하는 값은 암호화된 값이라고 생각합니다. 평문은 최대한 빠르게 로직에서 제거하면 좋을 것 같아요.
- 이렇게 규약을 놓고 보면 정적팩토리 메서드도 나쁘지 않다고 생각합니다. 항상 암호화된 값을 갖는다고 하면 생성자로는 암호화된 값을 받고, 팩토리 메서드 plain으로부터는 평문을 받아서 복호화할 수 있다고 생각해요.
@@ -2,7 +2,6 @@ | |||
|
|||
import org.junit.jupiter.api.DisplayName; | |||
import org.junit.jupiter.api.Test; | |||
|
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.
AuthAcceptanceTest는 구현되어야하지 않나요?
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.
Auth 관련 컨트롤러를 삭제하면서 같이 삭제되어야하던 클래스인데 깜빡한 것 같아요💦
이번에 리팩터링하며 AuthContoller가 다시 생겨 관련 부분 테스트 코드 추가했습니다
public ResponseEntity handleInvalidAccess(final RuntimeException e) { | ||
return ResponseEntity.badRequest().body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler | ||
@ResponseStatus(HttpStatus.BAD_REQUEST) | ||
public ErrorResponse handleBadRequest(BadRequestException e) { | ||
return new ErrorResponse(e.getMessage()); | ||
} |
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.
새로운 커스텀 예외 클래스가 추가될 때마다 ControllerAdvice가 변경될 것 같은데 해결할 수 있는 방법이 있을까요?
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.
Bad Request를 내려주기 위한 예외, Not Found를 내려주기 위한 예외 등 크게 나누었기 때문에 이 외의 다른 상태 코드가 발생할 경우가 생기면 그때 커스텀 예외 클래스가 추가됩니다.
새로운 커스텀 예외 클래스가 추가될 일이 많이 없을거라 생각했어요
다만 범블비가 말한 문제에 대해서 생각해보다가 어떤 상태 코드를 반환할지 서비스에서 결정한다는 생각이 들었어요
어떤 예외가 발생한건지 바로 파악하기도 힘든 것 같고요
그래서 패스워드 불일치로 인한 예외 발생 시 아래 같은 클래스를 반환하고
public class PasswordMismatchException extends UnauthorizedException {
//...
}
핸들러 익셉션에서는 UnauthorizedException에 대해서 처리하도록 리팩터링 하였습니다!
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으로 처리한다던지 하면 일반적으로 처리가 가능할 것 같네요.
private CustomerEntity findCustomerById(Long customerId) { | ||
return customerDao.findById(customerId) | ||
.orElseThrow(() -> new NotFoundException(NO_CUSTOMER_ERROR)); | ||
} |
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.
전 주로 조회 로직은 find vs get
을 따라서 이름을 지어요.
원칙은 아니지만 이렇게 컨벤션을 잡고 네이밍을 하면 나중에 보기 편하더라구요 :)
CustomerEntity customerEntity = customerDao.findByAccount(account) | ||
.orElseThrow(() -> new NotFoundException(NO_CUSTOMER_ERROR)); | ||
|
||
if (!passwordEncoder.match(signinRequest.getPassword(), customerEntity.getPassword())) { | ||
throw new UnauthorizedException(LOGIN_FAILED_ERROR); | ||
} |
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.
getter가 보이면 로직을 도메인으로 넣을 수 있을지 고민해보시면 좋을 것 같아요.
CustomerEntity 대신에 Customer를 사용한다면 password를 객체 내부에서 비교하고 검증할 수 있지 않을까요?
- Customer의 불필요한 생성자 제거 - 테스트 코드 정적 팩토리 메서드 사용
안녕하세요 범블비! 1차 피드백 반영하고 다시 코드리뷰 요청 보냅니다!
|
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 void create(CustomerRequest customerRequest) { | ||
if (customerDao.existsByAccount(customerRequest.getAccount())) { | ||
throw new DuplicatedCustomerException(); | ||
} | ||
Customer customer = customerRequest.toCustomer(); | ||
customerDao.save(CustomerEntity.from(customer)); | ||
} |
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 <-> Domain, Entity <-> Domain의 변환은 누구의 책임일까요?
- Domain이 넣으면 DTO가 많아질수록 Domain이 커져 DTO에 두었습니다
- Domain의 로직이 필요 없는 경우에는 DTO -> Entity의 변환도 상관 없을까요? 현재는 로직 변환의 책임을 주기 애매해서 DTO <-> Domain <-> Entity 식으로 Domain을 거쳐야 변환이 가능하게 구현했습니다
- DTO <-> Domain의 경우 서비스 레이어가 dto를 파라미터로 받고 응답 타입으로 사용할 때는 서비스 레이어가 해주는 게 적당하다고 생각해요. 다만, 중복 코드가 발생하고 서비스 코드가 너무 길어진다면 DTO에서 변환해줘도 괜찮다고 생각합니다.
- Domain <-> Entity 같은 경우에는 조금 원론적으로는 변환해주는 계층이 하나 더 있어야한다고 생각합니다. Service와 Dao 사이에 Repository 정도가 있으면 적당하겠네요(그렇다고 Repository가 변환만을 위한 계층이라는 건 아니에요. 오해하기 쉬운 부분이라 따로 남깁니다 😄 ). 하지만 지금 코드의 경우 domain과 entity가 단순하게 결합되어 있기 때문에 굳이 그럴 필요는 없다고 생각해요. 처음부터 CusomterEntity와 Customer가 분리될 필요도 없다고 생각합니다. 여튼 지금 정도로 두고, 나중에 다른 코드를 구현할 때 이 점을 염두해서 구현해보시면 좋을 것 같습니다 :)
@RestController | ||
public class CustomerController { | ||
|
||
private final CustomerService customerService; | ||
|
||
public CustomerController(CustomerService customerService) { | ||
this.customerService = customerService; | ||
} | ||
|
||
@PostMapping("/signup") |
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.
API URL 앞에
/api/...
를 붙이는건 컨벤션일까요?
- 다른 크루들과 이야기를 나누다보니 URL에 /api/customers/ ... 식으로 작성하신 분들이 많더라고요 이유를 물어보니 CORS 에러에서 /**와 같이 모든 요청을 허용하는게 보안적으로 문제가 있을 수 있다고 하셨어요 이 사유에 대해서는 납득되었는데 왜 하필
api
키워드를 앞에 붙였는지는 잘 모르겠더라고요😂 URL에도 앞에 뭘 붙여야한다, 어떤 구성으로 만들어야 한다 같은 컨벤션이 있나요??
글쎄요... 백엔드 api를 사용하는 클라이언트에는 웹 뿐만 아니라 ios/android 등 다양한 클라이언트 들이 있는데요, 제가 알기로는 CORS는 웹에만 적용하면 됩니다. 그래서 웹 전용 api에만 붙이면 돼서 굳이 api를 붙일 필요가 있나 싶어요. 연로그가 불필요하다고 생각하신다면 넘어가도 될 부분 같네요 😄
안녕하세요 범블비! 연로그입니다😄
이번 미션에는 레거시 코드들이 존재하는데요
1단계는 요구 사항에 따라 회원 관련 API만 구현 하였으며 그 외 product, order 등의 레거시 코드들은 2단계 때 리팩터링할 예정입니다!
auth 폴더 및 shoppingcart 내부의 customer 위주로 리뷰를 주시면 감사합니다!
그리고 이번 미션에 대해 가장 많이 고민했던건 Password의 암호화 부분인데요
처음에는 Password 생성자에서 encrypt 처리를 해줬다가 DB에서 불러온 비밀번호를 가져올 때 곤란을 겪었습니다
DB에는 암호화된 비밀번호를 저장합니다.
이를 Password로 변환할 때
new Password(DB에서 가져온 비밀번호)
를 거치는 바람에 암호화가 2번 되어버립니다.따라서 여러가지 방안을 생각해보았는데요
범블비라면 어떤 방법을 선택하실지, 이 외에도 방법이 있을지 궁금합니다!