카테고리 없음

TIL Lv.5 ‘내’가 정의한 문제와 해결 과정

creator7087 2025. 4. 20. 10:45

1. 수상한 점: 제멋대로인 패키지 구조

패키지 구조가 기능과 무관하게 뒤섞여 있었고, 명확한 기준 없이 흩어진 클래스들이 존재. 예상 문제는 다음과 같음:

  • 유지보수가 어려움
  • 기능 추가 및 분석 시 클래스 위치 파악이 비효율적임
  • 공통 로직 재사용성이 떨어짐

1-2. 문제 해결 과정: 단계별 리팩토링

1단계: 공통 로직 분리

  • domain 하위에 섞여 있던 공통 클래스들을 common 패키지로 통합

2단계: 예외 처리 구조 통합

  • 여러 도메인에 흩어진 예외 클래스들을 common.exception.custom으로 이동하여 관리 일관성 확보

3단계: 설정 및 기타 클래스 정리

패키지들을 아래와 같이 역할 중심으로 재구성

패키지 명내용 설명
aop AOP 관련 Aspect 클래스
auth @Auth 애너테이션, 관련 DTO 및 Resolver
interceptor 인증/인가 요청 흐름을 제어하는 인터셉터 클래스들
config Spring 설정 관련 Bean, Config 파일
entity BaseEntity 등 공통 상속용 엔티티 클래스
enums 프로젝트 전반에서 사용하는 Enum 모음
exception 전역/커스텀 예외 정의 및 핸들러
jwt JWT 관련 Util, Filter 구성

1-3. 결론: 역할 기반 패키지 구조로 리팩토링 완료

리팩토링 결과, 각 기능별로 명확하게 책임이 분리되어 다음과 같은 장점을 확보할 수 있었습니다:

  • 새로운 기능 추가 시 관련 위치를 쉽게 파악할 수 있음
  • 공통 예외 처리 및 인증 로직을 손쉽게 적용 가능
  • 도메인 간 의존성을 최소화하여 테스트 작성 및 협업이 용이해짐

2. 수상한 점: PersistenceConfig의 모호한 책임

  • JPA의 감사 기능(Auditing) 설정을 위해 별도의 PersistenceConfig 클래스가 존재했으나, 설정이 중복되거나 불필요하게 분산되어 관리 책임이 모호함

2-2. 문제 해결 과정: 리팩토링

  • PersistenceConfig 클래스 삭제
  • ExpertApplication 클래스에 @EnableJpaAuditing 직접 추가
더보기

.해당 코드 

@SpringBootApplication
@EnableJpaAuditing
public class ExpertApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExpertApplication.class, args);
    }

}

 

2-3. 결론: 설정 구조 측면

  • JPA 감사 기능의 책임이 BaseEntity와 ExpertApplication으로 명확하게 구분
    • BaseEntity: @CreatedDate, @LastModifiedDate 관리
    • ExpertApplication: @EnableJpaAuditing으로 기능 활성화
  • 불필요한 설정 클래스를 제거하여 구조 간결화 달성

3 . 수정한 점 : JwtFilter 등록 순서 설정 (setOrder) 적용

기존의 FilterConfig에서는 JwtFilter를 등록만 하고 필터 실행 순서를 지정하지 않아 다음과 같은 문제가 발생할 수 있었습니다:

  • Spring Security 필터보다 먼저 또는 늦게 실행되어 인증 흐름이 꼬이거나,
    요청이 제대로 처리되지 않는 현상 발생 가능
  • 다른 필터들과의 우선순위가 명확하지 않아 디버깅 시 처리 순서 추적이 어려움
  • 필터 충돌 또는 중복 실행 문제가 생길 여지가 있음

3-2. 문제 해결 방법

FilterRegistrationBean에 setOrder(int order)를 명시적으로 추가하여 필터의 실행 순서를 제어함.

더보기
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilter() {
    FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new JwtFilter(jwtUtil));
    registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다.
    registrationBean.setOrder(1);
    return registrationBean;
}

3-3. 결론: JwtFilter의 실행 순서를 명시

JwtFilter의 실행 순서를 명시함으로써 필터 간 충돌 가능성을 줄이고, 인증 로직의 안정성을 높일 수 있게 되었습니다.

  • 인증 필터의 의도된 흐름 보장
  • 필터 순서 기반의 명확한 요청 처리 구조 확보
  • 다양한 필터 간 우선순위 설정 관리가 쉬워짐

4. 수상한 점: @Auth AuthUser를 사용한 인증 방식 미사용

기존 컨트롤러 메서드는 다음과 같은 방식으로 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 파싱 로직이 컨트롤러마다 반복됨 → 코드 중복 증가
  • 인증 책임이 컨트롤러에 직접 드러나서 역할이 모호
  • 인증 처리 방식이 노출되며 테스트와 유지보수 어려움
  • 향후 인증 로직이 바뀔 경우, 모든 컨트롤러를 일일이 수정해야 함

4-2. 문제 해결 방법

@Auth 애너테이션과 AuthUser 객체를 활용해 인증 정보를 처리하는 방식으로 개선

더보기
@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);
}

 

  • 인증 관련 로직은 Resolver에서 캡슐화
  • authUser는 이미 인증된 사용자 정보를 담은 객체로 전달되므로 컨트롤러가 인증에 신경 쓸 필요 없음
  • 전체 컨트롤러 코드가 더 간결하고 직관적으로 변화
더보기

⚠️ 주의: @Auth는 반드시 AuthUser 타입과 함께 사용해야 함

 

@Auth 애너테이션은 인증된 사용자 정보를 파라미터에 주입하기 위한 목적으로 정의된 것으로,
파라미터가 AuthUser 타입이 아니면 다음과 같은 예외 발생:

 

new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다.")

 

즉, @Auth는 AuthUser authUser 와 함께 쌍으로 사용되어야만 정상 동작합니다.

4-3. 결론: 인증 책임 분리 및 컨트롤러 코드 단순화

  • @Auth와 AuthUser의 조합을 통해 인증 관련 책임을 전담 Resolver로 위임
  • 반복되던 JWT 파싱 로직 제거로 가독성 향상, 유지보수 용이성 증가
  • 보안 로직과 비즈니스 로직이 분리되어 아키텍처적으로도 더 깔끔한 구조 확보

5. 수상한 점: HTTP 상태 코드 미반영

  • ⚠ 문제 요약:
    • 반환 타입이 void이며, 클라이언트에게 처리 결과에 대한 상태 코드 응답이 없음
    • RESTful API 명세 측면에서 성공 여부를 명확히 알 수 없음

5-2. 문제 해결 방법

해결 전략:

  • 서비스 계층에서 예외를 throw → @ControllerAdvice를 통해 전역적으로 처리를 함으로 예외 발생은 작성  X
  • 컨트롤러는 처리 성공 시 200 OK, 204 no_content 등의 HTTP 상태 코드만 반환
  • 명시적이면서도 간결한 ResponseEntity 사용으로 REST API 응답 명확화

5-3. 결론: 계층 간 책임 분리 + 간결한 API 응답

  • 컨트롤러는 단순한 역할만 수행 → 응답 상태 코드 반환 (200 OK)
  • 예외 처리 책임은 서비스 → 전역 예외 처리기로 위임 → 계층 간 역할 명확화
  • 클라이언트는 일관된 응답 포맷과 상태 코드로 처리 결과를 쉽게 해석 가능

6. 수상한 점: 관리자 기능 전용 도메인(admin) 분리 및 AOP 적용 구조 

기존 문제 상황:

  • 아래와 같은 메서드들이 comment, user 도메인의 컨트롤러에 포함되어 있었음
더보기

기존 코드

@DeleteMapping("/admin/comments/{commentId}")
public ResponseEntity<Void> deleteComment(@PathVariable long commentId) { ... }

@PatchMapping("/admin/users/{userId}")
public ResponseEntity<Void> changeUserRole(@PathVariable long userId, @RequestBody UserRoleChangeRequest userRoleChangeRequest) { ... }

⚠ 주요 문제점:

  • 관리자(admin) 기능임에도 일반 도메인(controller/user, controller/comment 등)에 섞여 있어 역할 구분이 불분명
  • 이미 서비스 계층은 UserAdminService, CommentAdminService 등으로 분리되어 관리되고 있었음
  • AOP로 관리자 기능만 별도 관리하고 싶었으나, 패키지 기준 포인트컷 적용이 어려움

6-2. 문제 해결 방법

리팩토링 전략:

  • /admin/... 경로의 API를 담당하는 컨트롤러들을 domain.admin.controller 패키지로 이동
  • 서비스도 domain.admin.service로 함께 정리하여 admin 도메인 별도 구성
  • AOP 포인트컷을 다음과 같이 설정하여 admin 도메인 컨트롤러만 감지
더보기
@Around("execution(* org.example.expert.domain.admin.controller..*(..))")
public Object logAdminAccess(ProceedingJoinPoint joinPoint) throws Throwable {
    // 관리자 접근 로그, 감사 처리 등
}

 

구조 예시:

더보기
└── domain
    ├── user
    │   └── UserController.java
    ├── comment
    │   └── CommentController.java
    └── admin
        ├── controller
        │   ├── UserAdminController.java
        │   └── CommentAdminController.java
        └── service
            ├── UserAdminService.java
            └── CommentAdminService.java

6-3. 결론: 도메인 역할 분리 + 관리자 기능 일관 관리

  • 사용자용 기능과 관리자용 기능을 패키지 구조상 명확히 분리
  • admin 도메인은 특수 권한을 요구하는 기능만 포함되므로 보안, 로깅, 접근 제어를 AOP로 집중 처리 가능
  • 서비스 계층 또한 일관되게 분리되어 관리 편의성 및 테스트 용이성 향상

7 수상한 점: 사용되지 DTO 클래스 존재와 댓글 수정/삭제 기능 미비

  • UserSaveResponse는 과거 로그인/회원가입 등의 응답 용도로 사용되었을 가능성이 있으나,
    현재 로직에서 사용되지 않음
  • 더 이상 참조되지 않는 DTO는 유지보수 시 혼란 유발
  • 추후 리팩토링 대상인지 아닌지 판단이 어렵기 때문에 과감히 삭제 결정
  • 댓글 관련 컨트롤러에는 조회/등록 기능만 있고,
    작성자가 자신의 댓글을 수정하거나 삭제할 수 있는 API가 존재하지 않았음
  • 사용자 입장에서는 작성한 댓글을 관리할 수 없다는 기능적 제약이 존재

7-2. 문제 해결 방법

 1. 불필요 클래스 제거

  • UserSaveResponse DTO 완전 삭제
  • 관련 코드 전부 제거하여 클린 코드 유지

 2. 사용자 본인만 수정/삭제 가능한 댓글 기능 추가

컨트롤러에서 @Auth AuthUser를 통해 로그인된 사용자 ID를 받아와,
서비스 내부에서 댓글 작성자 ID와 비교하여 본인 확인 후 수정/삭제 가능하도록 구현

7-3 결론: 사용자 권한 기반의 안전한 수정/삭제 기능 제공

  • 사용자 본인이 작성한 댓글에 대해서만 수정/삭제가 가능하도록 제한하여 보안성 강화
  • API 응답도 HTTP 상태 코드 기반으로 설계 (200 OK, 204 No Content, 403 Forbidden, 404 Not Found)
  • 전역 예외 처리기를 통해 권한 없음 등의 예외를 일관되게 처리 가능

8수상한 점: MethodArgumentNotValidException의 부재 -> 테스트 코드 중 확인

  • @Valid를 사용하여 입력값 검증을 시도했지만,
    유효성 실패 시 발생하는 MethodArgumentNotValidException에 대한 예외 처리 로직이 없음
  • 그 결과, 요청 본문이 잘못되어도 클라이언트는 정확한 에러 메시지를 받지 못함
  • 디버깅 어려움, 클라이언트 입장에서 무엇이 잘못되었는지 알 수 없는 응답(500 등) 수신

8-2. 문제 해결 방법

✅ GlobalExceptionHandler에 예외 처리 메서드 추가

더보기
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();

    ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put("message", error.getDefaultMessage()) // message 키 사용
    );

    return ResponseEntity.badRequest().body(errors);
}

  테스트 코드 확인 가능

더보기
@Test
    void 댓글_저장_실패_null_case() throws Exception {

        // given
        String token = jwtUtil.createToken(1L, "test@example.com", UserRole.USER);
        CommentSaveRequest request = new CommentSaveRequest(null);

        // when & then
        mockMvc.perform(post("/todos/1/comments")
                        .header("Authorization", token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andExpect(status().is4xxClientError())
                .andExpect(jsonPath("$.message").value("댓글은 빈칸이면 안됩니다"));
    }
}

8-3. 결론: API의 유효성 검증 안정성 확보

  • @Valid 사용 시 발생 가능한 예외에 대한 중앙 집중형 처리 도입
  • 클라이언트가 어떤 필드가 잘못됐는지 알 수 있어 UX 및 개발 협업 효율성 향상
  • 모든 유효성 검사는 서비스 로직 진입 전 차단되므로 불필요한 리소스 사용 감소