[TIL] 백엔드 부트캠프 9주차 (2024/09/12 목) 레거시 코드의 테스트 코드 기반 리팩토링 -2

2024. 9. 12. 00:38·TIL 🔖/TIL

💻Today's Schedule

09:00 ~ 10:00 CODEKATA
10:00 ~ 11:00 개인 학습
11:00 ~ 12:00 팀미팅/코드리뷰
12:00 ~ 13:00 점심 식사
13:00 ~ 14:00 개인 학습
14:00 ~ 15:30 스탠다드 이론반 수강
15:30 ~ 18:00 개인학습
18:00 ~ 19:00 저녁 식사
19:00 ~ 21:00 개인 학습

✍ Today I Learned

CODEKATA

  • 알고리즘 (63번 숫자 짝꿍)

개인과제  Level 2-1. 테스트 연습

문제
이런!😱테스트 코드를 잘못 작성했어요!

테스트 패키지 package org.example.expert.config; 의 PassEncoderTest 클래스에 있는 matches_메서드가_정상적으로_동작한다() 테스트가 의도대로 성공할 수 있게 수정해 주세요.
    @Test
    void matches_메서드가_정상적으로_동작한다() {
        // given
        String rawPassword = "testPassword";
        String encodedPassword = passwordEncoder.encode(rawPassword);

        // when
        boolean matches = passwordEncoder.matches(encodedPassword, rawPassword);

        // then
        assertTrue(matches);
    }
수정한 코드
  • passwordEncoder.matches() 메서드의 matches() 메서드는 첫 번째 인자로 원본 비밀번호(rawPassword), 두 번째 인자로 암호화된 비밀번호(encodedPassword)를 받아야 한다. 원본 코드에서는 순서가 반대로 되어 있다. 이 순서를 올바르게 수정하여 테스트가 의도대로 성공할 수 있도록 했다.
 @Test
    void matches_메서드가_정상적으로_동작한다() {
        // given
        String rawPassword = "testPassword";
        String encodedPassword = passwordEncoder.encode(rawPassword);

        // when
        boolean matches = passwordEncoder.matches(rawPassword,encodedPassword);

        // then
        assertTrue(matches);
    }

 

개인과제  Level 2-2. 유닛 테스트-1

문제 
이런! 😱 테스트 코드를 잘못 작성했어요!
테스트 패키지 package org.example.expert.domain.manager.service; 의 ManagerServiceTest의 클래스에 있는 manager_목록_조회_시_Todo가_없다면_NPE_에러를_던진다() 테스트가 성공하고 컨텍스트와 일치하도록 테스트 코드와 테스트 코드 메서드 명을 수정해 주세요.
Hint ! 던지는 에러가 NullPointerException이 아니므로 메서드명 또한 수정되어야 해요!
    @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());
    }
수정한 코드
  • 메서드 이름에 NullPointerException (NPE)이 언급되어 있지만, 실제로 테스트에서 발생하는 예외는 InvalidRequestException이기 때문에 테스트 메서드 명과 테스트 코드의 내용이 일치하지 않다. 
  • 예외로 NullPointerException이 아닌 InvalidRequestException을 던지기 때문에 메서드 이름에서 NPE를 삭제하고, 올바른 예외 타입을 반영하도록 수정했다. 또, Todo가 없을 때 발생하는 예외이기 때문에 예외 메세지를 Manager not foun에서 Todo not found로 수정했다. 
@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());
}

 

개인과제  Level 2-3. 유닛 테스트 - 2

문제
이런! 😱 테스트 코드를 잘못 작성했어요!
테스트 패키지 `org.example.expert.domain.comment.service;` 의 `CommentServiceTest` 의 클래스에 있는 `comment_등록_중_할일을_찾지_못해_에러가_발생한다()` 테스트가 성공할 수 있도록 **테스트 코드**를 수정해 주세요.
@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());
    }
수정한 코드
  • 오류 메세지에 따르면 예상된 예외는 ServerException이지만, 실제로 발생한 예외는 InvalidRequestException이다.
  • 따라서, 테스트에서 발생하는 예외를 ServerException이 아니라 InvalidRequestException으로 수정했다.
    @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
        InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
            commentService.saveComment(authUser, todoId, request);
        });

        // then
        assertEquals("Todo not found", exception.getMessage());
    }

개인과제  Level 2-4. 유닛 테스트 - 3

이런! 😱 팀원이 로직을 수정했는데, 기존에 성공하던 테스트 코드가 실패하고 있어요!
테스트 패키지 org.example.expert.domain.manager.service의 ManagerServiceTest 클래스에 있는 todo의_user가_null인_경우_예외가_발생한다() 테스트가 성공할 수 있도록 서비스 로직을 수정해 주세요.
@Transactional
    public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
        // 일정을 만든 유저
        User user = User.fromAuthUser(authUser);
        Todo todo = todoRepository.findById(todoId)
                .orElseThrow(() -> new InvalidRequestException("Todo not found"));

        if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
            throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
        }

        User managerUser = userRepository.findById(managerSaveRequest.getManagerUserId())
                .orElseThrow(() -> new InvalidRequestException("등록하려고 하는 담당자 유저가 존재하지 않습니다."));

        if (ObjectUtils.nullSafeEquals(user.getId(), managerUser.getId())) {
            throw new InvalidRequestException("일정 작성자는 본인을 담당자로 등록할 수 없습니다.");
        }

        Manager newManagerUser = new Manager(managerUser, todo);
        Manager savedManagerUser = managerRepository.save(newManagerUser);

        return new ManagerSaveResponse(
                savedManagerUser.getId(),
                new UserResponse(managerUser.getId(), managerUser.getEmail())
        );
    }
@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());
    }
수정한 코드
  • Todo.getUser()가 null인 상황에서 예외 처리가 없어서 NullPointerException이 발생했기 때문에, todo.getUser()가 null이면 InvalidRequestException을 던지도록 서비스 로직을 수정했다.
@Transactional
    public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
        // 일정을 만든 유저
        User user = User.fromAuthUser(authUser);
        Todo todo = todoRepository.findById(todoId)
                .orElseThrow(() -> new InvalidRequestException("Todo not found"));

        if (todo.getUser() == null) {
            throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
        }

        if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
            throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
        }

        ...
        
    }

개인과제 Level 2-5. AOP

- 어드민 사용자만 접근할 수 있는 특정 API에는 접근할 때마다 접근 로그를 기록해야 합니다.
- 어드민 사용자만 접근할 수 있는 컨트롤러 메서드는 다음 두 가지예요.
   - 패키지 `org.example.expert.domain.comment.controller;` 의 `CommentAdminController` 클래스에 있는    `deleteComment()`
   - 패키지 package org.example.expert.domain.user.controller; 의 UserAdminController 클래스에 있는 changeUserRole()
- Spring AOP를 사용하여 해당 API들에 대한 접근 로그를 기록하는 기능을 구현하세요.
주의사항! -
로그 기록에는 다음 정보가 포함되어야 합니다.
요청한 사용자의 ID  /  API 요청 시각 / API 요청 URL
작성한 코드
  • AOP의 개념을 다시 한번 학습했지만, 코드로 작성하는 과정이 어렵게 느껴졌다. 여러 방법을 고민하다 정적주입 방식으로 코드를 작성했는데, 최종 제출로는 동적주입으로 변경했다. 

초기 코드 

@Slf4j
@Aspect
@Component
public class AdminAccessAspect {

    private final HttpServletRequest request;

    public AdminAccessAspect(HttpServletRequest request) {
        this.request = request;
    }

    // deleteComment Pointcut
    @Pointcut("execution(* org.example.expert.domain.comment.controller.CommentAdminController.deleteComment(..))")
    public void deleteCommentMethod() {}

    // changeUserRole Pointcut
    @Pointcut("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
    public void changeUserRoleMethod() {}

    // Pointcut 결합, 관리자 접근 로깅
    @Before("deleteCommentMethod() || changeUserRoleMethod()")
    public void logAdminAccess(JoinPoint joinPoint) {
        Long userId = getUserId();
        String url = request.getRequestURI();
        LocalDateTime requestTime = LocalDateTime.now();
        log.info("요청한 사용자의 ID: {}, API 요청 URL: {}, API 요청 시각: {}", userId, url, requestTime);
    }

    private Long getUserId() {
        return (Long) request.getAttribute("userId");
    }
}
  • AOP는 횡단 관심사를 모듈화 할 수 있는 방법으로 즉, 여러 클래스에 걸쳐 공통적으로 발생하는 작업(여기서는 로그 기록)을 따로 분리하여 관리할 수 있도록 해준다. 과제에서는 관리자(Admin)만 사용할 수 있는 API에 접근할 때마다, 해당 접근을 로깅하는 기능을 AOP를 사용하여 구현했다.
  • Aspect 클래스 정의
    • @Aspect와 @Component를 사용하여 Spring에서 AOP 기능을 활성화한 클래스로 AdminAccessAspec 클래스는 Admin 사용자만 접근할 수 있는 특정 API의 접근을 감지하고, 접근 로그를 기록하는 역할을 한다. 
  • Pointcut 설정
    • @Pointcut은 AOP에서 어떤 메서드나 클래스에 대해 관심사(관심지점)를 정의하는 데 사용된다. 여기서 두 가지 Pointcut을 정의했다. 두 Pointcut은 각각의 API 메서드 호출 전에 로그를 기록하기 위한 것이다.
      • deleteCommentMethod: CommentAdminController 클래스의 deleteComment() 메서드를 타깃으로 한다.
      • changeUserRoleMethod: UserAdminController 클래스의 changeUserRole() 메서드를 타겟으로 한다.
  • Before Advice 사용
    • @Before 어노테이션을 사용하여 특정 메서드가 실행되기 전에 로깅 작업이 수행되도록 했다.
      • @Around와 @Before 중에 어떤 것을 사용할지 고민이었는데, @Around 어드바이스는 메서드의 실행 전후를 모두 감싸며, 메서드 실행 결과에 따라 후처리 로직을 추가할 수 있지만 이 경우에는 메서드의 결과에 상관없이 메서드가 실행되기 직전에 호출한 사용자와 요청 시각을 기록할 수 있기 때문에 @Before가 적절하다고 생각했다.  
    • 여기서는 deleteCommentMethod()와 changeUserRoleMethod()에 대한 Pointcut을 결합하여, 두 메서드가 호출되기 전에 logAdminAccess() 메서드가 실행되도록 설정했다.
  • HttpServletRequest 의존성 주입
    • AdminAccessAspect 클래스는 생성자에서 HttpServletRequest 객체를 주입받아 현재 HTTP 요청과 관련된 정보를 쉽게 추출할 수 있도록 했다.
    • 이 주입된 HttpServletRequest 객체는 JwtFilter에서 사용자 인증이 완료된 후 사용자 정보가 설정된 상태로 전달된다. 필터를 통해 userId, email, userRole 등의 정보가 요청에 저장되며, AOP에서는 이 정보를 로깅이나 추가 작업에 사용할 수 있다.
    • AdminAccessAspect에서는 사용자 ID와 API 요청 URL을 추출하여 로깅하는 데 사용되며, AOP에서 필터를 통해 전달된 데이터를 활용할 수 있는 구조를 갖췄다.
  • getUserId() 메서드
    • getUserId() 메서드는 HttpServletRequest 객체의 getAttribute() 메서드를 통해 사용자 ID를 추출한다.
    • 이때 userId는 JWT 토큰이 유효할 때 JwtFilter에서 request.setAttribute("userId", Long.parseLong(claims.getSubject()))로 설정된 값이다.
    • 이 값은 JWT 토큰의 subject 필드에서 가져온 사용자 고유 ID로, JWT 토큰 검증 과정에서 토큰이 올바르게 검증되었을 때만 설정되므로, AOP에서는 이를 신뢰할 수 있는 인증된 사용자 정보로 간주하고 사용할 수 있다.
    • 필터를 통해 미리 설정된 이 값은 이후 컨트롤러나 AOP에서 손쉽게 접근할 수 있어, 추가적인 인증 과정 없이 효율적인 인증 정보 활용을 가능하게 한다.
  • 로그 기록 구현
    • logAdminAccess() 메서드는 실제로 로그를 기록하는 기능을 한다.
      • 사용자의 ID: HttpServletRequest 객체에서 userId를 가져온다. 이는 사용자 인증이 완료된 상태에서 request attribute로 저장된 사용자 ID이다.
      • API 요청 URL: request.getRequestURI()를 사용하여 현재 요청된 URL을 가져온다.
      • API 요청 시각: LocalDateTime.now()를 사용하여 요청이 발생한 시각을 기록한다.
      • 이 정보를 log.info() 메서드를 통해 로그로 남긴다.

개선한 코드

@Slf4j
@Aspect
@Component
public class AdminAccessAspect {

    // deleteComment Pointcut
    @Pointcut("execution(* org.example.expert.domain.comment.controller.CommentAdminController.deleteComment(..))")
    public void deleteCommentMethod() {}

    // changeUserRole Pointcut
    @Pointcut("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
    public void changeUserRoleMethod() {}

    // Pointcut 결합, 관리자 접근 로깅
    @Before("deleteCommentMethod() || changeUserRoleMethod()")
    public void logAdminAccess(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Long userId = (Long) request.getAttribute("userId");
        String url = request.getRequestURI();
        LocalDateTime requestTime = LocalDateTime.now();
        log.info("요청한 사용자의 ID: {}, API 요청 URL: {}, API 요청 시각: {}", userId, url, requestTime);
    }

}
  • 초기에는 아래와 같이 생성자에서 HttpServletRequest를 주입받는 방식으로 코드를 작성했다. 이 방식은 생성자에서 HttpServletRequest 객체를 받아 클래스 내에서 사용할 수 있게 하는 방식으로, 의존성 주입을 통해 현재 HTTP 요청 정보를 쉽게 접근할 수 있지만 특정 요청에 종속적이고 AOP는 여러 요청을 처리하는 특성을 가지고 있기 때문에 이 방식은 한 번 주입된 객체를 사용하기 때문에 현재 요청에 대한 정보와 불일치가 발생할 수 있다고 생각했다.
public AdminAccessAspect(HttpServletRequest request) {
    this.request = request;
}
  • 이 문제를 해결하기 위해 Spring에서 제공하는 RequestContextHolder를 사용하여, 매 요청마다 현재의 HttpServletRequest 객체에 접근할 수 있도록 변경했다.
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Before("deleteCommentMethod() || changeUserRoleMethod()")
public void logAdminAccess(JoinPoint joinPoint) {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    Long userId = (Long) request.getAttribute("userId");
    String url = request.getRequestURI();
    LocalDateTime requestTime = LocalDateTime.now();
    log.info("요청한 사용자의 ID: {}, API 요청 URL: {}, API 요청 시각: {}", userId, url, requestTime);
}
  • RequestContextHolder는 현재 스레드에 바인딩된 요청 정보를 가져올 수 있게 해 준다. 이를 통해 매 요청마다 HttpServletRequest 객체에 접근할 수 있다. ServletRequestAttributes 객체를 통해 현재 요청 정보를 추출하며, AOP가 여러 요청을 처리하더라도 동시에 여러 스레드에서 요청 정보를 정확히 사용할 수 있다.
빈(Bean), 프록시(Proxy) 등에 대한 고민
  • 초기에 작성했던 코드에 대한 확신이 없어서 여러 방법으로 고민을 했는데, 그중 일부를 기록한다.
//빈 주입 방식 예시
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class RequestScopedBean {
    private final HttpServletRequest request;
    public RequestScopedBean(HttpServletRequest request) { this.request = request; }
    public Long getUserId() { return (Long) request.getAttribute("userId"); }
    public String getRequestUrl() { return request.getRequestURI(); }
}
  • 스프링에서 빈 주입을 사용하여 HttpServletRequest를 주입받을 수도 있다. 하지만 이 경우 요청 스코프로 빈을 관리해야 하기 때문에 빈 주입이 매 요청마다 새로 이루어져야 한다.
  • 이를 위해서는 Request Scope로 빈을 선언해야 하고, 이 또한 복잡도를 높일 수 있다고 생각했다. 
  • 즉, @RequestScope로 빈을 주입하여 매번 새로운 요청에 맞는 HttpServletRequest를 생성할 수 있지만, 이 방식 역시 빈의 생명주기와 스코프 설정이 추가로 필요하게 되어 간결하지 않을 수 있다.
//프록시 예시
@Component
@RequestScope
public class RequestProxy {
    private final HttpServletRequest request;
    public RequestProxy(HttpServletRequest request) { this.request = request; }
    public Long getUserId() { return (Long) request.getAttribute("userId"); }
    public String getRequestUrl() { return request.getRequestURI(); }
}
  • 프록시 패턴은 스프링 AOP에서 자주 사용되며, 클래스나 메서드 호출을 감싸서 실행 전후 로직을 추가할 수 있다.
  • HttpServletRequest에 대한 프록시를 만들 수도 있지만, 프록시는 요청이 들어오고 나서 그 객체를 관리하는 데 있어 즉각적인 요청의 변화를 다루기 어려울 수 있어 실제 요청 객체에 대한 동적 접근보다는 정적 방식이 될 가능성이 있다.
  • 프록시를 사용하는 방식은 구현이 복잡해질 수 있고, 매 요청마다 달라지는 정보를 즉시 기록하는 데 적합하지 않을 수 있다고 생각해 이번 과제에 적합하지 않은 방식이라고 생각했다. 

 

  • 프록시나 빈 방식은 각각의 구현과 설정이 필요하므로 복잡성을 추가하게 되는데 RequestContextHolder는 성능 부담 없이 매 요청마다 현재 요청 정보를 가져오는 데 유리한 방식일 수 있다.
  • 때문에 이번 요구사항에서는 각 요청마다 다른 정보를 로깅해야 했기 때문에, 매번 새로운 요청 정보를 동적으로 가져올 수 있는 RequestContextHolder 방식이 적합했다. 프록시나 빈을 사용하는 방식에 비해, RequestContextHolder는 설정이 간단하고 유지보수가 쉽고 복잡한 구조를 추가하지 않고도 필요한 기능을 달성할 수 있었다.
AOP 테스트 코드 
  • 어드민 API에 접근했을 때 로그가 정상적으로 기록되는지를 확인하기 위해 HttpServletRequest를 Mockito를 사용하여 모킹 하고, RequestContextHolder를 통해 현재 요청을 설정한 후 로그를 확인한다.
  • 사실 AOP 코드를 완성하고 바로 테스트 코드를 작성했는데, 테스트 코드가 정상적으로 동작함에도 코드에 대한 불신이 있었다^^.. 내 코드를 믿을 수가 없어서 결국 포스트맨으로 실제 로그까지 확인하고 과제를 마무리했다.
@ExtendWith(OutputCaptureExtension.class)
class AdminAccessAspectTest {

    private AdminAccessAspect adminAccessAspect;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        adminAccessAspect = new AdminAccessAspect();
    }

    @Test
    void 관리자_접근_로그가_정상적으로_기록된다(CapturedOutput output) {
        // given
        HttpServletRequest mockRequest = mock(HttpServletRequest.class);
        when(mockRequest.getRequestURI()).thenReturn("/admin/comments/1");
        when(mockRequest.getAttribute("userId")).thenReturn(1L);

        RequestAttributes requestAttributes = new ServletRequestAttributes(mockRequest);
        RequestContextHolder.setRequestAttributes(requestAttributes);

        JoinPoint joinPoint = mock(JoinPoint.class);

        // when
        adminAccessAspect.logAdminAccess(joinPoint);

        // then
        assertThat(output.getAll()).contains("요청한 사용자의 ID: 1", "API 요청 URL: /admin/comments/1", "API 요청 시각:");
    }
}
  • HttpServletRequest 모킹
    • Mockito를 사용하여 HttpServletRequest를 모킹 한다.
    • 요청 URI와 userId를 설정하여 실제 요청처럼 동작하도록 한다.
  • RequestContextHolder 설정
    • ServletRequestAttributes를 사용해 현재 요청 정보를 RequestContextHolder에 설정해 AOP가 이 요청 정보를 사용할 수 있게 한다.
  • 로그 확인
    • 로그가 정상적으로 출력되었는지 CapturedOutput을 사용해 검증한다.
    • 로그에는 사용자 ID, API 요청 URL, 요청 시각이 포함되어야 한다.

테스트 코드 확인
댓글 삭제시 로그 확인
Role 변경시 로그 확인

📝 회고

오늘은 하루 종일 AOP코드에 대한 고민을 했던 것 같다.

사실 이번 과제의 핵심은 추가 구현의 테스트 코드라고 생각했는데 AOP에 대한 학습이 부족했었는지 너무 긴 시간을 할애했다. 결국 시간 내에 추가 구현을 하지 못했지만 프록시, 빈 등 개념만 알고 있던 것들을 직접 구현해 보며 어떤 장단점이 있는지 한번 더 학습하고 더 나은 방식을 깊이 생각해 볼 수 있었던 것 같다. 그리고 정말 짧고 단순한 테스트 코드이지만 처음으로 테스트 코드를 작성해 보며 부족했던 부분을 캐치할 수 있었다.

매번 개인과제를 하며 자만을 하고 집중하지 못한다.
시간 내에 목표했던 것들을 계속 해내지 못하는 게 참 못마땅한데 
이제는 내 구현능력과 학습능력에 대한 객관화가 필요한 것 같다.   

🔖 Tomorrow's Goal

  • 테스트 코드 고민해 보기 
'TIL 🔖/TIL' 카테고리의 다른 글
  • [프로그래머스/JAVA] 둘만의 암호
  • [TIL] 백엔드 부트캠프 10주차 (2024/09/19 목) 아웃소싱 프로젝트 - 설계
  • [TIL] 백엔드 부트캠프 9주차 (2024/09/11 수) 레거시 코드의 테스트 코드 기반 리팩토링
  • [TIL] 백엔드 부트캠프 9주차 (2024/09/10 화) AOP / 테스트코드
fargoe
fargoe
    fargoe
    fargoewave
    fargoe
    GitHub
    전체
    오늘
    어제
    • 분류 전체보기 (166)
      • TIL 🔖 (140)
        • TIL (69)
        • 코딩테스트 (71)
      • DEV (14)
        • Java & Spring (7)
        • MySQL (3)
        • Git&Github (4)
      • 개발지식 (10)
        • 알고리즘 (2)
        • 자료구조 (8)
        • CS (0)
      • 3D (1)
        • Unity (1)
      • ETC (0)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
fargoe
[TIL] 백엔드 부트캠프 9주차 (2024/09/12 목) 레거시 코드의 테스트 코드 기반 리팩토링 -2
상단으로

티스토리툴바