주노 님의 블로그

20240911 본캠프 43일차 TIL 본문

TIL

20240911 본캠프 43일차 TIL

juno0432 2024. 9. 11. 22:52

본캠프 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 테스트 코드 작성


 

면접 특강

더보기
  1. 채용공고를 보며 어떤 트렌드인지 파악하라
  2. 면접의 타입은 크게 세가지다
    가. 사전과제 > 과제를 위주로 면접 진행
    나. 라이브 코딩
    다. 과제 없는 실시간 대화
  3. 회사는 어떤역량을 확인하고자 하는가?
    가. 기술적 역량 : 이 사람이 정말 우리팀에 기여 할 수 있을까?
    >> ~~를 만드세요 / ~와 ~의 차이는 뭘까요? / ~상황을 가정했을때 개발자는 어떻게 처리해야할까요?
    나. 문화 적합성 : 이 사람이 잘 어우러질수 있는가?
    >> 팀원들과 갈등을 해결한 경험 / ~한 문제가 있다면 어떻게 해결할 수 있을까요?
    다. 성장 가능성 : 이 사람이 성장가능성이 있는가?
    >> 최근에 학습한 기술은 무엇일까요?
  4. STAR기법으로 답변을 해봐라
    Situation (상황) : ~ 를 구현해야 했습니다
    Task (과제) : ~부분을 담당하였습니다
    Action (행동) : ~를 사용하여 ~를 개발했으며, ~를 사용하였습니다
    Result (결과) : 이 덕분에 ~를 하게되었고 ~를 런칭할수 있었습니다
  5. 해야할것과 하지말아야할것
    가. 이미 알고 있는 내용에 대한 질문
    O : 두괄식, 열거형, 시선처리 명확
    X : 주절주절 금지

    나. 모르는 내용에 대한 질문
    O : 질문을 하여 대화를 주도하거나, 힌트를 받기. (다시 말해달라, 좀더 명확히 얘기해달라)
    X : 몰라용

    다. 미리 준비한 내용에 대한 질문
    O : 면접관이 원하는 내용만 간단하게 답변
    X : 대본을 준비한 티를 내면 안됨.
  6. 평소에 어떻게 준비해야하나?
    가. 기본지식
    나. 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. 단일 책임 원칙
    하나의 클래스는 단 하나의 기능만을 담당함
  2. 가독성 및 유지보수성 향상

충분히 복잡한 메서드라서 가독성을 위해 빼는게 좋다.,

 

과제 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정보를 받아와서 사용한다.

 

왜 수정해야할까?

  1. 단일 책임 원칙 준수
  2. 가독성과 유지보수성을 위해

과제 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를 적용하여 

  1. 요청한 사용자의 ID
  2. 요청 시각
  3. 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