Spring Boot Interface - 골격 구현 클래스 - 클래스 구조 변경(Feat. Composition)

최민길(Gale)·2023년 6월 25일
2

Spring Boot 적용기

목록 보기
29/46

안녕하세요 오늘은 Spring Boot의 클래스들을 Interface - 골격 구현 클래스 - 클래스 구조로 변경하는 작업을 진행해보겠습니다.

이펙티브 자바의 18~25번 아이템을 프로젝트에 적용시켜보았습니다. 기존 클래스의 경우 Spring Boot에서 기본적으로 제공하는, 혹은 별도의 dependency를 추가하여 제공받는 클래스를 필요한 클래스에 직접 상속하는 방식으로 진행하였습니다. 하지만 이 경우 상속받는 클래스에서 상위 클래스의 메소드를 오버라이딩하는 과정에서 상위 메소드를 중복 실행하는 등 로직 측면에서 잘못 구현될 수 있기 때문에 클래스의 안정성이 떨어집니다. 또한 성격이 유사한 클래스의 경우 같은 패키지에 넣어놓고 어노테이션 등으로 구별하였습니다. 이 경우는 코드의 중복이 발생하여 유지 보수 및 가독성의 문제가 있습니다.

따라서 저는 성격이 유사한 클래스들의 공통 부분을 묶은 후 인터페이스에서 메소드를 정의하고, 인터페이스를 구현한 추상 클래스인 골격 구현 클래스에서 기본적인 골격을 구현하며, 하위 클래스에서 인터페이스와 골격 구현 클래스를 상속받아 메소드를 오버라이딩하는 방식으로 아키텍쳐를 변경했습니다. 여기서 만약 기존 클래스가 다른 클래스와 상속 관계에 있는 경우 상속하는 클래스를 골격 구현 클래스에서 상속받은 후 필요한 메소드만 오버라이딩하여 제공하는 방식으로 구성하였습니다.

이런 방식으로 구현할 때 몇 가지 주의점이 있습니다.

먼저 골격 구현 클래스는 추상 클래스이기 때문에 인스턴스화할 수 없어 빈 주입이 되지 않기 때문에 @Component 주입 시 에러가 반환됩니다. 사실 추상 클래스는 인스턴스화 할 수 없기 때문에 싱글톤 패턴을 적용하는 것이 의미가 없기 때문에 어노테이션 없이 진행해주시면 됩니다.

다음으로 골격 구현 클래스의 생성자에 다른 매개 변수가 있을 경우, 하위 클래스에서 @RequiredArgsConstructor 어노테이션을 사용해서 생성자를 생성할 수 없습니다. @RequiredArgsConstructor 자체적으로 super()를 호출할 수 없기 때문에 부모 클래스의 생성자를 호출할 수 없어서 발생하는 문제입니다. 따라서 자식 클래스에 생성자 매개 변수가 많을 경우 코드가 지저분해질 우려가 있기 때문에 골격 구현 클래스에서 사용하고자 하는 매개 변수를 상위 인터페이스에서 getter로 정의한 후 골격 구현 클래스에서 getter 메소드를 사용한 후 하위 클래스에서 오버라이딩한다면 정상적으로 사용하실 수 있습니다.

아래 인터페이스 - 골격 구현 클래스 - 클래스 구조를 예시를 들어 설명해보겠습니다. jwt 토큰 필터를 다음의 3단계로 나누어 BaseFilter에서는 필터 클래스에서 공통적으로 사용하는 메소드를 정의하고, AbstractFilter에서는 OncePerRequestFilter를 상속받고 BaseFilter를 구현하는 골격 구현 클래스를 정의했습니다. 여기서 하위 클래스에서 모두 공통적으로 사용하는 OncePerRequestFilter의 doFilterInternal을 오버라이딩하여 공통 내용을 작성한 후, 하위 클래스에서 작성할 로직과 필터 ExceptionHandler를 인터페이스에서 정의한 메소드로 사용하였습니다. 마지막으로 JwtFilter에서 골격 구현 클래스에서 사용한 인터페이스 메소드를 오버라이딩한다면 공통 부분이 깔끔하게 정리되며 필터를 추가할 경우 확장하기 편하고 코드의 중복이 줄어들어 유지 보수의 편리성이 증가하였습니다.

public interface BaseFilter {
    /**
     * 실제 필터 로직 수행하는 메소드
     * @param request
     * @param response
     */
    void doFilterLogic(HttpServletRequest request, HttpServletResponse response);

    /**
     * FilterExceptionHandler getter
     * @return
     */
    FilterExceptionHandler getFilterExceptionHandler();
}
public abstract class AbstractFilter extends OncePerRequestFilter implements BaseFilter {
    /**
     * 실제 필터 로직 수행하는 메소드 구현
     * @param request
     * @param response
     * @param filterChain
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws IOException {
        try{
            doFilterLogic(request,response);
            filterChain.doFilter(request,response);
        }
        catch (ValidationException e){
            logger.error(e.getMessage());
            getFilterExceptionHandler().setErrorResponse(e.getCode(),e.getMessage(),response);
        }
        catch (Exception e){
            logger.error(e.getMessage());
            getFilterExceptionHandler().sendErrorToSlack(request,response,e);
        }
    }
}
@Component
@RequiredArgsConstructor
public final class JwtFilter extends AbstractFilter implements BaseFilter {
    private final FilterExceptionHandler filterExceptionHandler;
    private final UriProvider uriProvider;
    private final TokenProvider tokenProvider;

    @Override
    public void doFilterLogic(HttpServletRequest request, HttpServletResponse response) {
        // 2. 헤더의 토큰이 존재하는지 체크
        logger.info("2. 토큰 유효성 검사");

        // 토큰이 필요 없는 API는 패스
        String uri = uriProvider.getURI(request);
        if(!uriProvider.isValidationPass(uri)){
            String jwt = tokenProvider.resolveToken(request, TokenProvider.HEADER_NAME);

            // 토큰 유효성 검증 후 SecurityContext에 저장
            Claims claims = tokenProvider.getAuthenticationClaims(jwt);
            Authentication authentication = tokenProvider.getAuthentication(jwt, claims);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
    }

    @Override
    public FilterExceptionHandler getFilterExceptionHandler() {
        return filterExceptionHandler;
    }
}
profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글