일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- replaceAll()
- string
- 동일성과 동등성
- 래퍼타입
- 스프링환경설정
- stringbuilder의 reverse()
- 최대공약수와 최소공배수
- string과 stringbuilder
- 자바 유클리드
- 스프링뼈대
- 스프링
- 최대공약수
- 모던자바
- isuppercase()
- git 컨벤션
- sql 데이터형 변환
- toLowerCase()
- islowercase()
- 자바 스트링
- 유클리드호제법
- addDoc
- 자바 최소공배수
- 프로그래머스 레벨1
- StringBuilder
- ineer join
- 자바 최대공약수
- while과 two-pointer
- 베주계수
- Git사용법
- 최소공배수
- Today
- Total
주노 님의 블로그
20240911 본캠프 43일차 TIL 본문
본캠프 43일차 내용 간단요약
- 09:00 ~ 10:00 : 코드카타
- 10:00 ~ 11:00 : 개인과제
- 11:00 ~ 12:00 : 면접 특강
- 12:00 ~ 13:00 : 점심시간
- 13:00 ~ 18:00 : 개인과제
- 18:00 ~ 19:00 : 저녁시간
- 19:00 ~ 21:00 : 개인과제
오늘 해야할 일 ✔️ 🔺 ❌
✔️과제 필수구현 완료
🔺도전과제 10 SERVICE 테스트 코드 작성
면접 특강
- 채용공고를 보며 어떤 트렌드인지 파악하라
- 면접의 타입은 크게 세가지다
가. 사전과제 > 과제를 위주로 면접 진행
나. 라이브 코딩
다. 과제 없는 실시간 대화 - 회사는 어떤역량을 확인하고자 하는가?
가. 기술적 역량 : 이 사람이 정말 우리팀에 기여 할 수 있을까?
>> ~~를 만드세요 / ~와 ~의 차이는 뭘까요? / ~상황을 가정했을때 개발자는 어떻게 처리해야할까요?
나. 문화 적합성 : 이 사람이 잘 어우러질수 있는가?
>> 팀원들과 갈등을 해결한 경험 / ~한 문제가 있다면 어떻게 해결할 수 있을까요?
다. 성장 가능성 : 이 사람이 성장가능성이 있는가?
>> 최근에 학습한 기술은 무엇일까요? - STAR기법으로 답변을 해봐라
Situation (상황) : ~ 를 구현해야 했습니다
Task (과제) : ~부분을 담당하였습니다
Action (행동) : ~를 사용하여 ~를 개발했으며, ~를 사용하였습니다
Result (결과) : 이 덕분에 ~를 하게되었고 ~를 런칭할수 있었습니다 - 해야할것과 하지말아야할것
가. 이미 알고 있는 내용에 대한 질문
O : 두괄식, 열거형, 시선처리 명확
X : 주절주절 금지
나. 모르는 내용에 대한 질문
O : 질문을 하여 대화를 주도하거나, 힌트를 받기. (다시 말해달라, 좀더 명확히 얘기해달라)
X : 몰라용
다. 미리 준비한 내용에 대한 질문
O : 면접관이 원하는 내용만 간단하게 답변
X : 대본을 준비한 티를 내면 안됨. - 평소에 어떻게 준비해야하나?
가. 기본지식
나. TIL 작성으로 성실성, 태도, 성장가능성
다. 팀프로젝트를 통한 회고
라. 모의면접
과제
레벨 1-1
@Transactional
public SignupResponse signup(SignupRequest signupRequest) {
String encodedPassword = passwordEncoder.encode(signupRequest.getPassword());
UserRole userRole = UserRole.of(signupRequest.getUserRole());
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}
User newUser = new User(
signupRequest.getEmail(),
encodedPassword,
userRole
);
User savedUser = userRepository.save(newUser);
String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole);
return new SignupResponse(bearerToken);
}
위 코드에서 encode 작업이 시작되기전 이메일 검증로직을 수행하라.
@Transactional
public SignupResponse signup(SignupRequest signupRequest) {
//과제 1-1 이메일 중복 검사 로직을 먼저 수행한다
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}
String encodedPassword = passwordEncoder.encode(signupRequest.getPassword());
UserRole userRole = UserRole.of(signupRequest.getUserRole());
User newUser = new User(
signupRequest.getEmail(),
encodedPassword,
userRole
);
User savedUser = userRepository.save(newUser);
String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole);
return new SignupResponse(bearerToken);
}
왜 먼저 수행을 하는가?
encode로직은 회원가입 로직을 수행할때만 적용하면된다.
이메일이 이미 있다면 회원가입은 무용지물 따라서 encode로직도 필요없는게 된다.
위처럼 이메일 중복 검사 로직을 먼저 수행하면 불필요한 encode로직이 수행되지 않는다.
레벨 1-2 if-else 제거
WeatherDto[] weatherArray = responseEntity.getBody();
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
} else {
if (weatherArray == null || weatherArray.length == 0) {
throw new ServerException("날씨 데이터가 없습니다.");
}
}
if else문이 복잡하게있다면 가독성을 떨어트릴뿐더러, 유지보수에 어려움이 있다. 분리하자
//과제 1-2 불필요한 if-else 로직을 제거
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
}
if (weatherArray == null || weatherArray.length == 0) {
throw new ServerException("날씨 데이터가 없습니다.");
}
왜 분리를 하는가?
불필요한 중첩을 만들면, 코드를 이해하기어렵다
또한 if else가 복잡하게 얽허였으면 유지보수시 실수를 할 가능성이 높다.
1-3 메서드 분리
@Transactional
public void changePassword(long userId, UserChangePasswordRequest userChangePasswordRequest) {
if (userChangePasswordRequest.getNewPassword().length() < 8 ||
!userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
!userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
}
User user = userRepository.findById(userId)
.orElseThrow(() -> new InvalidRequestException("User not found"));
if (passwordEncoder.matches(userChangePasswordRequest.getNewPassword(), user.getPassword())) {
throw new InvalidRequestException("새 비밀번호는 기존 비밀번호와 같을 수 없습니다.");
}
if (!passwordEncoder.matches(userChangePasswordRequest.getOldPassword(), user.getPassword())) {
throw new InvalidRequestException("잘못된 비밀번호입니다.");
}
user.changePassword(passwordEncoder.encode(userChangePasswordRequest.getNewPassword()));
}
위 코드의 비밀번호 체크 로직을 메서드로 분리한다
@Transactional
public void changePassword(long userId, UserChangePasswordRequest userChangePasswordRequest) {
checkPasswordValidation(userChangePasswordRequest);
User user = userRepository.findById(userId)
.orElseThrow(() -> new InvalidRequestException("User not found"));
if (passwordEncoder.matches(userChangePasswordRequest.getNewPassword(), user.getPassword())) {
throw new InvalidRequestException("새 비밀번호는 기존 비밀번호와 같을 수 없습니다.");
}
if (!passwordEncoder.matches(userChangePasswordRequest.getOldPassword(), user.getPassword())) {
throw new InvalidRequestException("잘못된 비밀번호입니다.");
}
user.changePassword(passwordEncoder.encode(userChangePasswordRequest.getNewPassword()));
}
//과제 1-3 메서드 분리
private static void checkPasswordValidation(UserChangePasswordRequest userChangePasswordRequest) {
if (userChangePasswordRequest.getNewPassword().length() < 8 ||
!userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
!userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
}
}
적절한 메서드명을 작성한다
왜 메서드 분리를 해야할까?
- 단일 책임 원칙
하나의 클래스는 단 하나의 기능만을 담당함 - 가독성 및 유지보수성 향상
충분히 복잡한 메서드라서 가독성을 위해 빼는게 좋다.,
과제 1-4 controller jwt 유효성 검사 로직 수정
@DeleteMapping("/todos/{todoId}/managers/{managerId}")
public void deleteManager(
@RequestHeader("Authorization") String bearerToken,
@PathVariable long todoId,
@PathVariable long managerId
) {
Claims claims = jwtUtil.extractClaims(bearerToken.substring(7));
long userId = Long.parseLong(claims.getSubject());
managerService.deleteManager(userId, todoId, managerId);
}
위 코드에서 jwt 토큰을 받아, 그 토큰을 해석하는 역할까지 가져가고 있다.
jwt 처리 로직과 비즈니스 로직을 분리해보자.
위 saveManager에서 authUser을 사용하고 있으니 auth를 사용했다
@DeleteMapping("/todos/{todoId}/managers/{managerId}")
public void deleteManager(
@Auth AuthUser authUser,
@PathVariable long todoId,
@PathVariable long managerId
) {
long userId = authUser.getId();
managerService.deleteManager(userId, todoId, managerId);
}
auth user에서 id정보를 받아와서 사용한다.
왜 수정해야할까?
- 단일 책임 원칙 준수
- 가독성과 유지보수성을 위해
과제 2-5
@Test
void matches_메서드가_정상적으로_동작한다() {
// given
String rawPassword = "testPassword";
String encodedPassword = passwordEncoder.encode(rawPassword);
// when
boolean matches = passwordEncoder.matches(encodedPassword, rawPassword);
// then
assertTrue(matches);
}
matches 메서드가 정상 동작하지 않는다.
Expected :true
Actual :false
예상하기론 true를 예상했는데 실제로는 false를 찍어줬다.
matches 메서드를 봤을때 매개변수의 위치가 다른것을 볼 수 있다.
그래서 boolean이 틀릴 수 밖에 없다
@Test
void matches_메서드가_정상적으로_동작한다() {
// given
String rawPassword = "testPassword";
String encodedPassword = passwordEncoder.encode(rawPassword);
// when
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
// then
assertTrue(matches);
}
메서드가 정상적으로 동작한다.
과제 2-6 유닛 테스트 1
@Test
public void manager_목록_조회_시_Todo가_없다면_NPE_에러를_던진다() {
// given
long todoId = 1L;
given(todoRepository.findById(todoId)).willReturn(Optional.empty());
// when & then
InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> managerService.getManagers(todoId));
assertEquals("Manager not found", exception.getMessage());
}
목록 조회시 TODO가 없다면 NULLPOINTEREXEPTION을 바라고 있다.
하지만 실행결과는
Expected :Manager not found
Actual :Todo not found
였다
실제 결과인 Todo not found로 바꿔주고,
NULL POINTER EXCEPTION이 아닌
InvalidRequestExcption인것을 볼 수 있다.
메서드명과 예상값을 변경한 코드는 아래와 같다.
@Test
public void manager_목록_조회_시_Todo가_없다면_InvalidRequestException_에러를_던진다() {
// given
long todoId = 1L;
given(todoRepository.findById(todoId)).willReturn(Optional.empty());
// when & then
InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> managerService.getManagers(todoId));
assertEquals("Todo not found", exception.getMessage());
}
과제 2-7 유닛 테스트2
@Test
public void comment_등록_중_할일을_찾지_못해_에러가_발생한다() {
// given
long todoId = 1;
CommentSaveRequest request = new CommentSaveRequest("contents");
AuthUser authUser = new AuthUser(1L, "email", UserRole.USER);
given(todoRepository.findById(anyLong())).willReturn(Optional.empty());
// when
ServerException exception = assertThrows(ServerException.class, () -> {
commentService.saveComment(authUser, todoId, request);
});
// then
assertEquals("Todo not found", exception.getMessage());
}
위 테스트코드가 정상 작동하지 않는다.
Expected :class org.example.expert.domain.common.exception.ServerException
Actual :class org.example.expert.domain.common.exception.InvalidRequestException
serverException을 요구하는데 실제로는 invalidRequestException인것.
InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
commentService.saveComment(authUser, todoId, request);
});
성공 할 수 있게 assert의 예외를 InvalidRequstExcption으로 변경해준다.
성공
과제 2-8 유닛 테스트3
@Test
void todo의_user가_null인_경우_예외가_발생한다() {
// given
AuthUser authUser = new AuthUser(1L, "a@a.com", UserRole.USER);
long todoId = 1L;
long managerUserId = 2L;
Todo todo = new Todo();
ReflectionTestUtils.setField(todo, "user", null);
ManagerSaveRequest managerSaveRequest = new ManagerSaveRequest(managerUserId);
given(todoRepository.findById(todoId)).willReturn(Optional.of(todo));
// when & then
InvalidRequestException exception = assertThrows(InvalidRequestException.class, () ->
managerService.saveManager(authUser, todoId, managerSaveRequest)
);
assertEquals("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.", exception.getMessage());
}
위 코드를 확인하면 invalidRequestException을 원하고 있다.
아래 테스트 결과를 확인해보면 NullPointerException을 반환하는것으로 보인다
Unexpected exception type thrown, expected: <org.example.expert.domain.common.exception.InvalidRequestException> but was: <java.lang.NullPointerException>
Expected :class org.example.expert.domain.common.exception.InvalidRequestException
Actual :class java.lang.NullPointerException
if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}
위 코드에서 에러가 생긴걸 확인할 수 있는데
InvalidRequestException을 던지는것을 볼 수 있다.
메서드의 명과 같이 todo에서 user를 찾을때 null인경우
에러가 발생하는것 같다.
User user = User.fromAuthUser(authUser);
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
if (todo.getUser() == null) {
throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}
예외를 만들어주되, 테스트코드의 예외 메세지와 맞춰준다.
과제 2-9 AOP 적용
aop를 적용하여
- 요청한 사용자의 ID
- 요청 시각
- API URL 경로
이 세가지를 출력한다.
패키지 org.example.expert.domain.comment.controller; 의 CommentAdminController 클래스에 있는 deleteComment()
패키지 package org.example.expert.domain.user.controller; 의 UserAdminController 클래스에 있는 changeUserRole()
이 두가지의 경우에서 AOP를 적용해야한다
@Aspect
@Component
@Slf4j
public class AdminAccessLogger
{
Aspect를 부착하고
@Pointcut("execution(* org.example.expert.domain.comment.controller.CommentAdminController.deleteComment(..))")
private void commentAdminControllerPointcut() {}
@Pointcut("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
private void userAdminController() {}
pointcut으로 컨트롤러의 메서드를 적용해준다.
@Around("commentAdminControllerPointcut() || userAdminController()")
public Object logAdminAccess(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Long userId = (Long) request.getAttribute("userId");
log.info("admin id : {}", userId);
log.info("current time : {}", LocalDateTime.now());
log.info("url : {}", request.getRequestURL());
return joinPoint.proceed();
}
그리고 로그를 찍게한다.
httpServlet의 getAttribute를 하여, 정보를 가져온다.
오늘의 회고 & 12시간 몰입했는가?
과제를 하고 찾아본다고 이번달 가장 몰입한것 같다!
'TIL' 카테고리의 다른 글
20240919 본캠프 46일차 TIL (0) | 2024.09.19 |
---|---|
20240912 본캠프 44일차 TIL (0) | 2024.09.12 |
20240910 본캠프 42일차 TIL (0) | 2024.09.11 |
20240909 본캠프 41일차 TIL (0) | 2024.09.09 |
20240906 본캠프 40일차 TIL (0) | 2024.09.06 |