[Spring] Interceptor, ArgumentResolver를 활용한 깔끔한 인증 처리

2023. 5. 8. 13:57· Spring
목차
  1. 💋 리팩터링 전 코드
  2. 💋 Interceptor를 사용해 리팩터링하기
  3. 💋 ArgumentResolver를 사용해 리팩터링하기
  4. 💋 조금 더 욕심 내서 Extractor로 분리해보기
반응형

지난 포스팅에서 Interceptor와 Argument Resolver에 대해서 공부했었다! 

만약 WebConfig 설정 파일을 통해서 MVC를 커스터마이징하는 것이 낯설다면 이 포스팅을 참고하면 될 것 같다! 

 

오늘 포스팅에서는 지난 포스팅에서 소개했던 Interceptor와 Argument Resolver를 코드에 적용해볼 것이다.

Interceptor를 만들어서 공통적인 인증과 관련된 코드를 빼내보자!

 

 

 

💋 리팩터링 전 코드

 

먼저 컨트롤러와 서비스의 코드를 보자! (지저분함 주의)

 

 

[컨트롤러] 

 

/cart로 오는 모든 요청을 잡고 있는 컨트롤러이다. 

 

@RestController
@RequestMapping("/carts")
public class CartController {

    private final CartService cartService;

    public CartController(final CartService cartService) {
        this.cartService = cartService;
    }

    @GetMapping("/all")
    public ResponseEntity<List<Product>> findItems(@RequestHeader("Authorization") String authorization) {
        if (authorization.isBlank()) { // 반복적인 코드
            return ResponseEntity.badRequest().build();
        }
        final List<Product> products = cartService.findCartItems(authorization);
        return ResponseEntity.ok().body(products);
    }

    @PostMapping("/{productId}")
    public ResponseEntity<CartItem> addItem(@PathVariable("productId") long productId, @RequestHeader("Authorization") String authorization) {
        if (authorization.isBlank()) { // 반복이 거슬림
            return ResponseEntity.badRequest().build();
        }
        final CartItem addedItem = cartService.addItem(authorization, productId);
        return ResponseEntity.ok().body(addedItem);
    }

    @DeleteMapping("/{productId}")
    public ResponseEntity<Void> deleteItem(@PathVariable("productId") long productId, @RequestHeader("Authorization") String authorization) {
        if (authorization.isBlank()) { // 또또 반복 어차피 다 해야하는거잖아!
            return ResponseEntity.badRequest().build();
        }
        cartService.deleteItem(authorization, productId);
        return ResponseEntity.noContent().build();
    }
}

 

[서비스]

 

/carts URL 하위의 모든 URL에 대한 처리를 하는 메서드들이다.

이 코드들에서는 모두 다 Basic Authentication이 필요하다. 

현재 코드에서는 validateAuth()라는 메서드를 반복하고 있고, 이 부분이 굉장히 반복적이고 거슬린다. 

 

@Service
public class CartService {
    public static final String BASIC_TYPE_PREFIX = "Basic";
    private static final String DELIMITER = ":";

    private final CartItemDao cartItemDao;
    private final MemberDao memberDao;
    private final ProductDao productDao;

    public CartService(final CartItemDao cartItemDao, final MemberDao memberDao, final ProductDao productDao) {
        this.cartItemDao = cartItemDao;
        this.memberDao = memberDao;
        this.productDao = productDao;
    }

    public CartItem addItem(final String authorization, final Long productId) {
        final Member authenticatedMember = validateAuth(authorization); // 거슬림
        if (authenticatedMember == null) {
            return null;
        }
        final CartItem cartItem = new CartItem(authenticatedMember.getId(), productId);
        return cartItemDao.save(cartItem);
    }

    public List<Product> findCartItems(final String authorization) {
        final Member authenticatedMember = validateAuth(authorization); // 거슬림
        final List<CartItem> cartItems = cartItemDao.findByMemberId(authenticatedMember.getId());
        return cartItems.stream()
                .map(CartItem::getProductId)
                .map(productDao::findById)
                .collect(Collectors.toList());
    }

    public void deleteItem(final String authorization, final long productId) {
        final Member member = validateAuth(authorization); // 거슬림
        cartItemDao.delete(member.getId(), productId);
    }
    
    private Member validateAuth(final String authorization) {
    	// 매번 반복해야 하는 코드 (한줄한줄 이해할 필요 X)
        final boolean isBasicAuthentication = authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase());

        if (!isBasicAuthentication) {
            return null;
        }

        String authHeaderValue = authorization.substring(BASIC_TYPE_PREFIX.length()).trim();
        byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
        String decodedString = new String(decodedBytes);

        String[] credentials = decodedString.split(DELIMITER);
        String email = credentials[0];
        String password = credentials[1];

        return memberDao.findByEmailAndPassword(email, password).get();
    }
}

 

특징을 보자면 /carts 로 들어오는 요청들에 대해서 빠짐없이 인증을 거쳐야 한다. 

이 부분을 Interceptor로 빼보자!

 

 

 

 

💋 Interceptor를 사용해 리팩터링하기

 

먼저, interceptor 패키지 아래에 BasicAuthInterceptor를 만들었다.

 

@Component
public class BasicAuthInterceptor implements HandlerInterceptor {

    private static final String BASIC_TYPE_PREFIX = "Basic";
    private static final String DELIMITER = ":";

    private final MemberDao memberDao;

    public BasicAuthInterceptor(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
        final String authorization = request.getHeader("Authorization"); // Basic Ym94c3RlckB3b290Z...
        final boolean isBasicAuthentication = authorization != null && authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase());

        if (!isBasicAuthentication) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        String authHeaderValue = authorization.substring(BASIC_TYPE_PREFIX.length()).trim();
        byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
        String decodedString = new String(decodedBytes);

        String[] credentials = decodedString.split(DELIMITER);
        String email = credentials[0];
        String password = credentials[1];

        final Member authenticatedMember = memberDao.findByEmailAndPassword(email, password).orElse(null);

        if (authenticatedMember == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

		// 컨트롤러에서 사용하기 위한 request 파라미터 추가
        request.setAttribute("authenticatedMember", authenticatedMember); 
        return true;
    }
}

 

 

 

이제 변화한 Service 코드를 보자!

 

 

@Service
public class CartService {
    private final CartItemDao cartItemDao;
    private final ProductDao productDao;

    public CartService(final CartItemDao cartItemDao, final ProductDao productDao) {
        this.cartItemDao = cartItemDao;
        this.productDao = productDao;
    }

    public CartItem addItem(final long memberId, final long productId) {
        final CartItem cartItem = new CartItem(memberId, productId);
        return cartItemDao.save(cartItem);
    }

    public List<Product> findCartItems(final long memberId) {
        final List<CartItem> cartItems = cartItemDao.findByMemberId(memberId);
        return cartItems.stream()
                .map(CartItem::getProductId)
                .map(productDao::findById)
                .collect(Collectors.toList());
    }

    public void deleteItem(final long memberId, final long productId) {
        cartItemDao.delete(memberId, productId);
    }
}

 

 

 

이제 컨트롤러를 리팩터링해보자!

 

 

@RestController
@RequestMapping("/carts")
public class CartController {

    private final CartService cartService;

    public CartController(final CartService cartService) {
        this.cartService = cartService;
    }

    @GetMapping("/all")
    public ResponseEntity<List<Product>> findItems(HttpServletRequest request) {
        final Member authenticatedMember = (Member) request.getAttribute("authenticatedMember");
        final List<Product> products = cartService.findCartItems(authenticatedMember.getId());
        return ResponseEntity.ok().body(products);
    }

    @PostMapping("/{productId}")
    public ResponseEntity<CartItem> addItem(@PathVariable("productId") long productId, HttpServletRequest request) {
        final Member authenticatedMember = (Member) request.getAttribute("authenticatedMember");
        final CartItem addedItem = cartService.addItem(authenticatedMember.getId(), productId);
        return ResponseEntity.ok().body(addedItem);
    }

    @DeleteMapping("/{productId}")
    public ResponseEntity<Void> deleteItem(@PathVariable("productId") long productId, HttpServletRequest request) {
        final Member authenticatedMember = (Member) request.getAttribute("authenticatedMember");
        cartService.deleteItem(authenticatedMember.getId(), productId);
        return ResponseEntity.noContent().build();
    }
}

 

✔ 아쉬운 점

 

내 코드는 현재 Interceptor를 통해서 

 

1. 가입된 사용자가 맞는지

2. 가입된 사용자를 확인해 Member로 변환

 

두 가지를 모두 하고 있다.

 

하지만 Interceptor의 preHandle() 메서드는 컨트롤러로 넘어갈 지 여부를 결정하는 boolean만을 반환하기 때문에, 인증이 끝난 후 찾아낸 authenticatedMember를 Controller에 전달하기 위해서 request에 addAttribute을 사용했다.

 

 

 

하지만 리뷰를 통해 받은 내용처럼, HTTP 요청을 임의로 조작하는 것은 부자연스럽다! 

 

추천받은 HandlerMethodArgumentResolver를 사용해서 리팩터링 해보자!!!

 

 

 

 

 

💋 ArgumentResolver를 사용해 리팩터링하기

 

어려웠던 점에 대해서 작성했는데, 궁금한 사람은 접힌 글을 열어서 확인하면 좋을 것 같다! 

 

더보기
  • Argument Resolver를 통해서 request 헤더의 Authentication 내에 들어있는 인증과 관련된 부분을 처리했다.
    • 로그인이 불가능한 경우에, 401 상태코드를 곧바로 반환하고 싶어서, 아래와 같이 코드를 썼었다. (주석만 슬쩍 보면 됨)

 

@Component
public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumentResolver {

    private static final String BASIC_TYPE_PREFIX = "Basic";
    private static final String DELIMITER = ":";

    private final MemberDao memberDao;

    public AuthenticatedMemberArgumentResolver(final MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    @Override
    public boolean supportsParameter(final MethodParameter parameter) {
        return parameter.hasParameterAnnotation(AuthenticatedMember.class)
                && Member.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(
            final MethodParameter parameter,
            final ModelAndViewContainer mavContainer,
            final NativeWebRequest webRequest,
            final WebDataBinderFactory binderFactory) throws Exception {
        final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

        final String authorization = request.getHeader("Authorization");
        final boolean isBasicAuthentication = authorization != null && authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase());

        if (!isBasicAuthentication) {
            return new ResponseEntity<>("Unauthorized", HttpStatus.UNAUTHORIZED); // 곧바로 401 상태코드를 포함한 채로 반환
        }

        final Member authenticatedMember = findMemberFromAuthentication(authorization);

        if (authenticatedMember == null) {
            return new ResponseEntity<>("Unauthorized", HttpStatus.UNAUTHORIZED); // 곧바로 401 상태코드를 포함한 채로 반환
        }

        return authenticatedMember;
    }

    private Member findMemberFromAuthentication(final String authorization) {
        String authHeaderValue = authorization.substring(BASIC_TYPE_PREFIX.length()).trim();
        byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
        String decodedString = new String(decodedBytes);

        String[] credentials = decodedString.split(DELIMITER);
        String email = credentials[0];
        String password = credentials[1];

        return memberDao.findByEmailAndPassword(email, password).orElse(null);
    }
}

 

그리고 곧바로 접근 권한이 없는데 물건을 지우겠다거나 한다면 401 상태코드를 반환한다는 테스트 코드를 짰다. 

 

    @Test
    public void deleteItemWhenNotLoggedInTest() {
        RestAssured.given()
                .log().all()

                .when()
                .delete("/carts/1")

                .then()
                .log().all()
                .statusCode(HttpStatus.UNAUTHORIZED.value());
    }

 

근데 이게 에러가 뜨는 것...!

 

 

  • 이유는... ArgumentResolver에서 반환한 값이 그대로 파라미터로 @AuthenticatedMember Member 를 만나서 바인딩이 되어버리는데, ResponseEntity 형태는 Member 객체로 바인딩할 수 없어서 나타나는 (ClassCastException 비슷한) 것이었다...! 
  • 해결 방법은... ArgumentResolver에서 예외를 던지고, 그것을 ControllerAdvice에서 받아서 처리하는 식으로 해결했다... 

 

@Component
public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumentResolver {

	// ...
    
    private final MemberDao memberDao;

    public AuthenticatedMemberArgumentResolver(final MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    @Override
    public boolean supportsParameter(final MethodParameter parameter) {
        return parameter.hasParameterAnnotation(AuthenticatedMember.class)
                && Member.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(
            final MethodParameter parameter,
            final ModelAndViewContainer mavContainer,
            final NativeWebRequest webRequest,
            final WebDataBinderFactory binderFactory) throws Exception {
        final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

        final String authorization = request.getHeader("Authorization");
        final boolean isBasicAuthentication = authorization != null && authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase());

        if (!isBasicAuthentication) {
            throw new UnAuthenticatedMemberException("로그인 해 주세요!"); // 커스텀 예외 만들어서 던짐
        }

        final Member authenticatedMember = findMemberFromAuthentication(authorization);

        if (authenticatedMember == null) {
            throw new UnAuthenticatedMemberException("로그인 해 주세요!"); // 커스텀 예외 만들어서 던짐
        }

        return authenticatedMember;
    }

    private Member findMemberFromAuthentication(final String authorization) {
        String authHeaderValue = authorization.substring(BASIC_TYPE_PREFIX.length()).trim();
        byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
        String decodedString = new String(decodedBytes);

        String[] credentials = decodedString.split(DELIMITER);
        String email = credentials[0];
        String password = credentials[1];

        return memberDao.findByEmailAndPassword(email, password).orElse(null);
    }
}

 

그리고 이 커스텀 예외를 처리하는 코드

 

    @ExceptionHandler(UnAuthenticatedMemberException.class)
    public ResponseEntity<String> handleUnAuthenticatedMemberException(UnAuthenticatedMemberException e, HttpServletResponse response) {
        return ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body("Unauthorized");
    }

 

 

 

아래와 같이 ArgumentResolver를 작성하자.

 

@Component
public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumentResolver {

    private static final String BASIC_TYPE_PREFIX = "Basic";
    private static final String DELIMITER = ":";

    private final MemberDao memberDao;

    public AuthenticatedMemberArgumentResolver(final MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    @Override
    public boolean supportsParameter(final MethodParameter parameter) {
        return parameter.hasParameterAnnotation(AuthenticatedMember.class)
                && Member.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(
            final MethodParameter parameter,
            final ModelAndViewContainer mavContainer,
            final NativeWebRequest webRequest,
            final WebDataBinderFactory binderFactory) throws Exception {
        final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

        final String authorization = request.getHeader("Authorization");
        final boolean isBasicAuthentication = authorization != null && authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase());

        if (!isBasicAuthentication) {
            throw new UnAuthenticatedMemberException("로그인 해 주세요!");
        }

        final Member authenticatedMember = findMemberFromAuthentication(authorization);

        if (authenticatedMember == null) {
            throw new UnAuthenticatedMemberException("로그인 해 주세요!");
        }

        return authenticatedMember;
    }

    private Member findMemberFromAuthentication(final String authorization) {
        String authHeaderValue = authorization.substring(BASIC_TYPE_PREFIX.length()).trim();
        byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
        String decodedString = new String(decodedBytes);

        String[] credentials = decodedString.split(DELIMITER);
        String email = credentials[0];
        String password = credentials[1];

        return memberDao.findByEmailAndPassword(email, password).orElse(null);
    }
}

 

Controller에서 바인딩된 객체를 받을 수 있도록 인터페이스를 설정하고

 

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticatedMember {
}

 

컨트롤러에서 파라미터를 통해서 받아낸다.

 

@RestController
@RequestMapping("/carts")
public class CartController {

    private final CartItemAddService addService;
    private final CartItemFindService findService;
    private final CartItemDeleteService deleteService;

    public CartController(final CartItemAddService addService, final CartItemFindService findService, final CartItemDeleteService deleteService) {
        this.addService = addService;
        this.findService = findService;
        this.deleteService = deleteService;
    }

    @GetMapping("/all")
    public ResponseEntity<List<ProductResponse>> findItems(@AuthenticatedMember Member authenticatedMember) {
        final List<ProductResponse> products = findService.findCartItems(authenticatedMember.getId()).stream()
                .map(ProductResponse::from)
                .collect(Collectors.toList());
        return ResponseEntity.ok().body(products);
    }

    @PostMapping("/{productId}")
    public ResponseEntity<CartItemResponse> addItem(@PathVariable("productId") long productId, @AuthenticatedMember Member authenticatedMember) {
        final CartItem addedItem = addService.addItem(authenticatedMember.getId(), productId);
        final CartItemResponse cartItem = CartItemResponse.from(addedItem);
        return ResponseEntity.ok().body(cartItem);
    }

    @DeleteMapping("/{productId}")
    public ResponseEntity<Void> deleteItem(@PathVariable("productId") long productId, @AuthenticatedMember Member authenticatedMember) {
        deleteService.deleteItem(authenticatedMember.getId(), productId);
        return ResponseEntity.noContent().build();
    }
}

 

ArgumentResolver를 통해 반환되는 값은 설정한 형태(Member)로 바인딩되기 때문에, 

접근할 수 없는 사용자에 대해 401 Unauthorized를 보내주려면, ResponseEntity를 반환하는 대신 ArgumentResolver에서 예외를 일으킨 후에 ControllerAdvice에서 예외를 처리해 주어야 한다. 

 

package cart.entity.member;

public class Member {
    private final Long id;
    private final String email;
    private final String password;

    public Member(final Long id, final String email, final String password) {
        this.id = id;
        this.email = email;
        this.password = password;
    }

    public Member(final String email, final String password) {
        this(null, email, password);
    }

    public long getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    public String getPassword() {
        return password;
    }
}

 

 

마지막으로, ArgumentResolver를 설정 파일에 등록한다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final HandlerMethodArgumentResolver argumentResolver;

    public WebConfig(final HandlerMethodArgumentResolver argumentResolver) {
        this.argumentResolver = argumentResolver;
    }

    @Override
    public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(argumentResolver);
    }
}

 

 

💋 조금 더 욕심 내서 Extractor로 분리해보기

 

Request의 Authorization 내용을 분리했다. 

 

@Component
public class BasicAuthenticationExtractor {

    private static final String BASIC_TYPE_PREFIX = "Basic";
    private static final String DELIMITER = ":";

    public MemberAuthenticationDto extract(final String authentication) {
        validateAuthentication(authentication);

        String authHeaderValue = authentication.substring(BASIC_TYPE_PREFIX.length()).trim();
        byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
        String decodedString = new String(decodedBytes);

        String[] credentials = decodedString.split(DELIMITER);
        String email = credentials[0];
        String password = credentials[1];

        return new MemberAuthenticationDto(email, password);
    }

    private static void validateAuthentication(final String authorization) {
        if (authorization == null) {
            throw new UnAuthenticatedMemberException("로그인 해 주세요!");
        }
        if (!authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase())) {
            throw new UnAuthenticatedMemberException("로그인 해 주세요!");
        }
    }
}

 

그리고 쪼오오끔 더 깔끔해진 Argument Resolver로 남겨두면 진짜 끗!

 

@Component
public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumentResolver {
    private final MemberDao memberDao;
    private final BasicAuthenticationExtractor extractor;

    public AuthenticatedMemberArgumentResolver(final MemberDao memberDao, final BasicAuthenticationExtractor extractor) {
        this.memberDao = memberDao;
        this.extractor = extractor;
    }

    @Override
    public boolean supportsParameter(final MethodParameter parameter) {
        return parameter.hasParameterAnnotation(AuthenticatedMember.class)
                && Member.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) throws Exception {
        final String authorization = webRequest.getHeader("Authorization");

        final Member authenticatedMember = findMemberFromAuthentication(authorization);

        if (authenticatedMember == null) {
            throw new UnAuthenticatedMemberException("로그인 해 주세요!");
        }

        return authenticatedMember;
    }

    private Member findMemberFromAuthentication(final String authorization) {
        final MemberAuthenticationDto memberAuthenticationDto = extractor.extract(authorization);
        final String email = memberAuthenticationDto.getEmail();
        final String password = memberAuthenticationDto.getPassword();
        return memberDao.findByEmailAndPassword(email, password).orElse(null);
    }
}

 

 

편-안

 

끗

 

 

 

 

반응형
저작자표시 비영리 변경금지 (새창열림)

'Spring' 카테고리의 다른 글

[Spring] 스프링 빈 등록 어노테이션 기반의 자바 코드로 설정하기: Java based Container Configuration  (0) 2023.05.22
[Spring] Spring MVC에서 WebConfig 파일로 내 입맛에 맞게 MVC 설정하기: View Controllers, Interceptors, Argument Resolvers  (3) 2023.05.08
[Spring/DB] Spring에서 schema.sql과 data.sql 파일은 어떻게 다르고 어떤 순서로 실행될까?  (7) 2023.05.01
[Spring/DB] 데이터베이스에 더미 데이터 추가하는 4가지 방법: data.sql, @PostConstruct, @EventListener(ApplicationReadyEvent.class), @ApplicationRunner  (4) 2023.04.28
[Spring] Spring MVC(5): Configuration (View Controller, View Resolver)  (2) 2023.04.24
  1. 💋 리팩터링 전 코드
  2. 💋 Interceptor를 사용해 리팩터링하기
  3. 💋 ArgumentResolver를 사용해 리팩터링하기
  4. 💋 조금 더 욕심 내서 Extractor로 분리해보기
'Spring' 카테고리의 다른 글
  • [Spring] 스프링 빈 등록 어노테이션 기반의 자바 코드로 설정하기: Java based Container Configuration
  • [Spring] Spring MVC에서 WebConfig 파일로 내 입맛에 맞게 MVC 설정하기: View Controllers, Interceptors, Argument Resolvers
  • [Spring/DB] Spring에서 schema.sql과 data.sql 파일은 어떻게 다르고 어떤 순서로 실행될까?
  • [Spring/DB] 데이터베이스에 더미 데이터 추가하는 4가지 방법: data.sql, @PostConstruct, @EventListener(ApplicationReadyEvent.class), @ApplicationRunner
깃짱
깃짱
연새데학교 컴퓨터과학과 & 우아한테크코스 5기 백엔드 스타라이토 깃짱
반응형
깃짱
깃짱코딩
깃짱
전체
오늘
어제
  • 분류 전체보기
    • About. 깃짱
    • Weekly Momentum
      • 2024
    • PROJECT
      • AIGOYA LABS
      • Stamp Crush
      • Sunny Braille
    • 우아한테크코스5기
    • 회고+후기
    • Computer Science
      • Operating System
      • Computer Architecture
      • Network
      • Data Structure
      • Database
      • Algorithm
      • Automata
      • Data Privacy
      • Graphics
      • ETC
    • WEB
      • HTTP
      • Application
    • C, C++
    • JAVA
    • Spring
      • JPA
      • MVC
    • AI
    • MySQL
    • PostgreSQL
    • DevOps
      • AWS
      • 대규모 시스템 설계
    • frontend
      • HTML+CSS
    • NextJS
    • TEST
    • Industrial Engineering
    • Soft Skill
    • TIL
      • 2023
      • 2024
    • Linux
    • Git
    • IntelliJ
    • ETC
      • 日本語

블로그 메뉴

  • 홈
  • 깃허브

인기 글

최근 글

태그

  • lamda
  • 람다
  • 레벨로그
  • 람다와스트림
  • OOP
  • Composition
  • 우아한테크코스
  • 컴포지션
  • 우아한테크코스5기
  • Java
  • 우테코
  • 우테코5기
  • 상속과조합
  • 예외
  • 함수형프로그래밍
  • Stream
  • 상속
  • 조합
  • TDD
  • 스트림
hELLO · Designed By 정상우.v4.2.0
깃짱
[Spring] Interceptor, ArgumentResolver를 활용한 깔끔한 인증 처리
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.