안녕하세요. 카테캠 커리큘럼을 열심히 따라가고 있는 중입니다.
이번 주도 코드 리뷰를 많이 받았는데요. 이에 대해 정리해보려 합니다.
아래는 제가 2주차에 받은 파드백입니다.
https://github.com/kakao-tech-campus-2nd-step2/spring-gift-wishlist/pull/103#event-13375277331
충남대 BE_이은경 2주차 과제(1단계) by pkyung · Pull Request #103 · kakao-tech-campus-2nd-step2/spring-gift-wishlis
구현 과정 ValidProductName 어노테이션을 생성하여 각각의 경우에 따라 다른 에러 메시지를 출력하도록 하였습니다. -> 기본 validate 어노테이션을 활용하도록 리펙토링 controller에서 해당 에러를 Excep
github.com
https://github.com/kakao-tech-campus-2nd-step2/spring-gift-wishlist/pull/212#event-13399923540
충남대 BE_이은경 2주차 과제 (2단계) by pkyung · Pull Request #212 · kakao-tech-campus-2nd-step2/spring-gift-wishli
과제 진행 인증 사용자 도메인 설계 사용자 회원 가입 api 사용자 로그인 - accessToken
github.com
https://github.com/kakao-tech-campus-2nd-step2/spring-gift-wishlist/pull/321#event-13410302454
구현한 기능📄
# spring-gift-product 🎁
## 2주차 기능 목록 📄
### 유효성 검사 및 예외 사항
- [x] 상품 이름은 공백 포함 15자까지 입력
- [x] ( ), [ ], +, -, &, /, _ 외의 특수 문자 불가
- [x] '카카오' 가 포함된 문구는 담당 MD와 협의
### 인증
- [x] 사용자 도메인 설계
- [x] 사용자 회원 가입 api
- [x] 사용자 로그인 - accessToken
### 위시 리스트
- [x] 위시 리스트 도메인 설계
- [x] 위시 리스트 상품 추가
- [x] 위시 리스트 상품 목록 조회
- [x] 위시 리스트 상품 삭제
받은 피드백🎯 - step 1 (유효성 검사 및 예외 사항)
기본 validate 어노테이션 이용하기
상품 이름의 유효성 검사를 진행하는 코드를 validProductName 어노테이션을 생성하여 각 경우에 따라 다른 에러메시지를 출력하도록 처리했습니다. 그런데 멘토님께서 이 모든 작업을 기본 validate 어노테이션으로 해결할 수 있을 것 같다고 하셔서 리펙토링 해보았습니다.
기본 validate 를 사용하지 않은 이유는 정규식이 두 개 필요할 것 같아서였는데 두 개 다 적용해도 된다고 말씀하셔서 아래와 같이 수정했습니다.
public class ProductRequestDto {
private Long id;
@NotEmpty(message = "상품 이름이 옳지 않습니다.")
@NotNull(message = "상품 이름이 옳지 않습니다.")
@Length(min = 1, max =15, message = "상품 이름은 15자 이내 여야 합니다.")
@Pattern(regexp = "[a-zA-Z0-9가-힣\\(\\)\\[\\]\\-+&_\\/\\s]+", message = "상품 이름에는 (), [], -, +, &, _, /, 공백을 제외한 특수 문자를 사용할 수 없습니다.")
@Pattern(regexp = "^(?!.*카카오).*$", message = "상품 이름에 '카카오' 가 포함 되어 있습니다. 담당 MD와 협의가 필요합니다.")
private String name;
private int price;
private String imageUrl;
}
ExceptionHandler의 클래스 분리
기존의 코드는 ExceptionHandler를 controller 내부에서 처리했었습니다. 그러나 그렇게 되면 controller는 외부의 요청을 적절하게 처리하는 역할을 하고 있는데 에러 처리까지 맡게 되기에 한 클래스는 하나의 일만 처리하도록 분리하라는 뜻 같았습니다.
그래서 ProductExceptionHandler를 만들어서 ProductController에서 발생한 에러를 처리했습니다.
@RestControllerAdvice(assignableTypes = ProductController.class)
public class ProductExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<String> handleProductNotFoundException(ProductNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
@ExceptionHandler(ProductAlreadyExistsException.class)
public ResponseEntity<String> handleProductAlreadyExistsException(ProductAlreadyExistsException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<String> handleProductNameException(MethodArgumentNotValidException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getDefaultMessage())
.findFirst()
.orElse("올바르지 않은 입력 방식입니다.");
return ResponseEntity.badRequest().body(errorMessage);
}
}
Valid와 Validated의 차이 알아보기
Valid 어노테이션은 자바 표준 스펙으로 Bean Validator 검증기를 통해 객체의 유효성을 검증합니다
Validated 어노테이션은 Spring에서 제공하는 기능으로 AOP 기반으로 메서드의 요청을 가로채서 유효성을 검증하는 어노테이션입니다.
Valid는 주로 request body를 검증하는데 사용되며 Validated는 request body 뿐만 아니라 url의 PathVariable까지 유효성 검증이 가능합니다. 그리고 유효성 검증 그룹을 지정할 수 있습니다.
이렇게 되면 아래 코드의 예시로 사용자와 관리자에게 다른 조건으로 유효성 검증을 적용할 수 있습니다.
public interface UservalidationGroup {}
public interface AdminvalidationGroup {}
public class ProductRequest {
@NotEmpty(groups = {UserValidationGroup.class, AdminValidationGroup.class})
pricate String name;
@NotEmpty(groups = UserValidationGroup.class)
pirvate String imageUrl;
}
이 차이를 알아보라고 하신 이유는 제가 controller에서 기본 Validate 어노테이션만 사용하고 있으면서 쓸데없이 Validated 를 사용하고 있음의 속뜻으로 생각하고 Validate 어노테이션으로 수정했습니다.
받은 피드백🎯 - step 2 (인증)
REST API 규칙에서는 리소스명을 복수로 사용함
새로운 사실을 알게 되어서 복수형으로 수정했습니다.
@PostMapping("/user")
public ResponseEntity makeUser() {
}
@PostMapping("/users")
public ResponseEntity makeUser() {
}
AuthController 제작하기
기존의 코드에서는 LoginController를 만들었습니다. 그런데 이렇게 되면 차후에 로그아웃을 구현하게 되었을 때, 또 컨트롤러를 만들어야 합니다. 따라서 코드 응집도를 위해 AuthController를 제작하라는 피드백을 받았습니다.
Request 라는 단어가 들어가면 DTO의 의미가 들어있음
기존의 코드에서 DTO를 생성할 때, ProductRequestDto, MemberReqeustDto, MemberReseponseDto 로 DTO를 이름 지었습니다. 그러나 Request 또는, Response 라는 단어가 들어가면 DTO의 의미를 갖고 있기 때문에 수정가능하다고 하셔서 생략했습니다.
JwtUtil의 key는 yml에서 관리하기 - static에 @Value 적용
SecretKey는 보안상 정보이기 때문에 application.properties 또는 yml 파일에 넣어야 한다고 하셨습니다.
사실 저는 이 부분이 중요한 정보임을 알고 있었으나 @Value를 사용했을 때 계속 null이 발생하여 포기했던 부분입니다. 하지만 수정하라고 요구하셨기에 이에 대해 알아보았습니다.
스프링은 static 필드에 @Value 를 지원하지 않습니다. static 필드는 jvm 클래스 로더에 의해 jvm 메모리 영역 중 class area에 런타임에 저장됩니다. 이 시점은 application context가 로드되기 이전이기에 application context에 의존적인 @Valie가 동작하지 않습니다. (@Autowired도 동일함)
setter 메서드를 사용하면 static 변수에 프로퍼티 값을 주입할 수 있습니다.
private static String SECRET_KEY;
@Value("${jwt.secret-key}")
public void setSecretKey(String value) {
SECRET_KEY = value;
}
matchPassword를 도메인에서 작성하는 것이 좋다
기존의 코드에서는 로그인을 할 때, 사용자가 입력한 비밀번호와 데이터베이스에 저장된 비밀번호가 일치하는지 확인하는 코드를 하드코딩했습니다.
처음에는 이 말을 이해하지 못해서 LoginService에 userMatchPassword 메서드를 구현해서 pr을 날렸었는데요. User 객체에 메서드를 만들라는 말이었습니다!
public LoginResponseDto login(LoginRequestDto requestDto) {
User user = userDao.findByEmail(requestDto.getEmail());
if (user == null || !user.getPassword().equals(requestDto.getPassword())) {
throw new UserNotFoundException("해당 유저가 존재하지 않거나 비밀번호가 틀렸습니다.");
}
}
이렇게 도메인에서 처리하는 게 좋다는 말이었습니다.
public class User {
...
public boolean matchPassword(String password) {
if(this.password.equals(password)) {
return true;
}
return false;
}
}
코드를 더 간결하게 작성해도 좋다고 하셨습니다.
public boolean matchPassword(String password) {
return this.password.equals(password)
}
User 객체를 바로 반환하지 말고 Optional을 사용해봐라
Optional 클래스는 자바 8에서 소개된 null을 처리하는 새로운 방법을 제공하는 클래스입니다. Optional은 값이 있을 수도 있고 없을 수도 있는 객체를 감싸는 래퍼 클래스입니다. 이를 통해 명시적으로 해당 값이 null 일 수 있음을 표현할 수 있으며, NullPointerException을 방지할 수 있는 API를 제공합니다.
Optional 클래스의 사용은 코드의 가독성을 높이고 null 체크를 강화함으로써 더 안전한 코드를 작성할 수 있습니다.
이렇게 하나만 조회할 때는 Optional 객체를 반환하도록 만들어 NullPointException에 더 안전한 코드로 수정했습니다.
public Optional<Product> find(Long id) {
String sql = "SELECT id, name, price, imageUrl from products WHERE id = ?";
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, productRowMapper(), id));
} catch (Exception e) {
return Optional.empty();
}
}
받은 피드백🎯 - step 3 (위시리스트)
모든 DTO에 Validate를 습관화하라
에러 처리를 귀찮다고 넘기곤 했는데 생각할 수 있는 모든 부분에서 생길 수 있는 에러에 대해 처리함이 습관화되어야 한다고 조언해주셨습니다.
DTO를 record에 대해 알아보고 적용 해볼 것
자바 21을 쓰면서 과제를 하고 있기 때문에 자바 17이상에서 사용가능한 record에 대해 알아보라고 하셨습니다.
레코드란?
레코드는 키워드 record를 사용하여 정의됩니다. 레코드를 정의할 때는 클래스와 유사하게 필드를 선언할 수 있지만 모든 필드는 final이며, 레코드의 생성자, getter, equals(), hashCode(), toString() 메서드는 컴파일러에 의해 자동으로 생성됩니다.
레코드 장점
레코드를 사용하면 데이터를 담는 객체를 정의할 때 필요한 보일러 플레이트 코드를 대폭 줄일 수 있습니다. 또한, 필드가 final이므로 인스턴스 생성 후에 필드 값을 변경할 수 없습니다. 이는 멀티스레드 환경에서의 안정성을 높이고 버그 발생 가능성을 줄입니다.
레코드가 jpa 엔티티 사용에 불가능한 이유
그러나 레코드는 entity에서 사용할 수은 없고 데이터 전달 객체인 dto에서 사용하는 것이 좋습니다.
hibernate와 같은 jpa는 프록시 생성을 위해 인수 생성자, non-final 필드, stter 및 non-final 클래스가 없는 엔티티에 의존합니다. 즉, 프록시를 생성하기 위해서 entity는 불변이면 안됩니다.
레코드의 사용
이렇게 record의 매개변수 자리에 타입을 적어주면 toStirng, equals, hashCode, 생성자 등이 자동 구현됩니다.
public record ProductRequest(
Long id,
String name,
int price,
String imageUrl
) {}
후기🎓
이번 주는 확실히 피드백의 양이 많았어서 고칠 부분이 많았는데요.
힘들기도 했지만, 혼자 개발할 때는 알지 못했던 좀 더 직관적인 코드를 작성하는 법, record와 같은 새로운 기능, 로직에 대해 헷갈렸던 부분을 피드백 해주셔서 확실히 제 코드가 깔끔해지고 있음을 느끼고 있습니다. 카테캠을 열심히 참여하면 확실히 실력이 느는 것 같습니다.
'🍫카카오 테크 캠퍼스 2기 BE' 카테고리의 다른 글
[카카오 테크 캠퍼스 / BE] 2단계 네 번째 코드 리뷰 (1) | 2024.07.24 |
---|---|
[카카오 테크 캠퍼스 / BE] 2단계 세 번째 코드 리뷰 (1) | 2024.07.14 |
[카카오 테크 캠퍼스 / BE] 2단계 첫 번째 코드 리뷰 후기 (0) | 2024.07.02 |
[카카오 테크 캠퍼스 / BE] 두 번째 미니과제, 자동차 경주🚗 (1) | 2024.06.10 |
[카카오 테크 캠퍼스 / BE] 첫 번째 미니과제, 숫자 야구 게임⚾ (0) | 2024.05.27 |