[Spring] AOP๋กœ ๊ถŒํ•œ ์ฒ˜๋ฆฌํ•˜๊ธฐ
Back-end/Spring

[Spring] AOP๋กœ ๊ถŒํ•œ ์ฒ˜๋ฆฌํ•˜๊ธฐ

728x90

๐Ÿ“‘ ๊ฐœ์š”

์ด๋ฒˆ์— ์ง„ํ–‰ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์˜ ๊ฒฝ์šฐ,

๋งŽ์€ API์—์„œ ์š”์ฒญ์œผ๋กœ ๋“ค์–ด์˜จ travel id์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€, ํ•ด๋‹น travel id์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€์— ๋Œ€ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

์˜ˆ์™ธ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ์• ๋…ธํ…Œ์ด์…˜์„ ๋งŒ๋“ค์–ด ์ฒ˜๋ฆฌํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

 

๐Ÿ“‘ Custom Annotation ์ƒ์„ฑ

๋จผ์ €, ๊ถŒํ•œ ๊ฒ€์‚ฌ๋ฅผ ์ ์šฉํ•  ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉํ•  ์ปค์Šคํ…€ ์• ๋…ธํ…Œ์ด์…˜์„ ๋งŒ๋“ ๋‹ค.

package withbeetravel.security;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Travel์— ๋Œ€ํ•œ ๊ถŒํ•œ ๊ฒ€์‚ฌ๋ฅผ ์ ์šฉํ•  ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉ
 */

@Target(ElementType.METHOD) // ๋ฉ”์†Œ๋“œ๋‹จ์—์„œ ์‚ฌ์šฉ
@Retention(RetentionPolicy.RUNTIME) // ๋Ÿฐํƒ€์ž„ ์‹œ์ ์— ์œ ์ง€
public @interface CheckTravelAccess {

    // ํ•ด๋‹น ์• ๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ ๋ฉ”์†Œ๋“œ์—์„œ ๊ฐ€์ ธ์˜ฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„
    String travelIdParam() default "travelId";
}

 

๐Ÿ“‘ AOP Aspect ํด๋ž˜์Šค ์ƒ์„ฑ

AOP๋ฅผ ํ†ตํ•ด ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „์— ๊ถŒํ•œ์„ ๊ฒ€์ฆํ•˜๋Š” Aspect ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

package withbeetravel.security;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import withbeetravel.domain.Travel;
import withbeetravel.exception.CustomException;
import withbeetravel.exception.error.TravelErrorCode;
import withbeetravel.repository.TravelMemberRepository;
import withbeetravel.repository.TravelRepository;

/**
 * ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „ Travel์— ๋Œ€ํ•œ ๊ถŒํ•œ ๊ฒ€์ฆ
 */

@Aspect
@Component
@RequiredArgsConstructor
public class TravelAccessAspect {

    private final TravelRepository travelRepository;
    private final TravelMemberRepository travelMemberRepository;

    @Before("@annotation(checkTravelAccess)") // @CheckTravelAccess๊ฐ€ ๋ถ™์€ ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „์— ์‹คํ–‰
    public void checkAccess(JoinPoint joinPoint, CheckTravelAccess checkTravelAccess) {

        // ๋กœ๊ทธ์ธ๋œ ํšŒ์› id
        Long userId = 3L;

        // @CheckTravelAccess๋ฅผ ๋ถ™์ธ ๋ฉ”์†Œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ travelId ์ถ”์ถœ
        Long travelId = getTravelIdFromArgs(joinPoint, checkTravelAccess.travelIdParam());

        // travelId์— ํ•ด๋‹นํ•˜๋Š” travel์ด ์žˆ๋Š”์ง€ ํ™•์ธ(์—†๋‹ค๋ฉด ์˜ˆ์™ธ ๋˜์ง€๊ธฐ)
        travelRepository.findById(travelId)
                .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND));

        // ๊ถŒํ•œ ๊ฒ€์‚ฌ
        travelMemberRepository.findByTravel_IdAndUser_Id(travelId, userId)
                .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_ACCESS_FORBIDDEN));
    }

    private Long getTravelIdFromArgs(JoinPoint joinPoint, String travelIdParam) {

        // ๋ฉ”์†Œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์ด๋ฆ„์„ ๋งคํ•‘ํ•˜์—ฌ travelId๋ฅผ ์ฐพ์•„ ๋ฐ˜ํ™˜
        var paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
        var args = joinPoint.getArgs();

        for (int i = 0; i < paramNames.length; i++) {
            if(paramNames[i].equals(travelIdParam) && args[i] instanceof Long) {
                return (Long) args[i];
            }
        }

        // @CheckTravelAccess๋ฅผ ๋ถ™์ธ ๋ฉ”์„œ๋“œ์—์„œ "travelId"๋ผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ
        throw new IllegalArgumentException("travelId ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
    }
}

์•„์ง, ์ธ์ฆ/์ธ๊ฐ€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•„์„œ

30๋ฒˆ ๋ผ์ธ์˜ userId๋ฅผ ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋กœ ๋„ฃ์–ด๋†จ๋‹ค.

 

์ธ์ฆ/์ธ๊ฐ€ ๊ตฌํ˜„์ด ์™„๋ฃŒ๋˜์–ด ์žˆ๋Š” ์ƒํƒœ๋ผ๋ฉด,

userId์— ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž์˜ userId๋กœ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

 

๐Ÿ“‘ Annotation ์ ์šฉ

์ด์ œ ๊ถŒํ•œ ๊ฒ€์‚ฌ๊ฐ€ ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ์— @CheckTravelAccess ์• ๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค.

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/travels/{travelId}/payments")
public class SharedPaymentController implements SharedPaymentControllerDocs {

    @Override
    @CheckTravelAccess
    @PatchMapping(value = "/{sharedPaymentId}/records", consumes = "multipart/form-data")
    public ResponseEntity<String> addAndUpdatePaymentRecord(
            @PathVariable Long travelId,
            @PathVariable Long sharedPaymentId,
            @RequestPart(value = "paymentImage") MultipartFile paymentImage,
            @RequestParam(value = "paymentComment", required = false) String paymentComment,
            @RequestParam(value = "isMainImage", defaultValue = "false") boolean isMainImage
    ) {

        ...
    }
}

 

@CheckTravelAccess๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”์„œ๋“œ์—์„œ travel id๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ช…์ด travelId๊ฐ€ ์•„๋‹ˆ๋ผ ๋‹ค๋ฅธ ์ด๋ฆ„์ด๋ผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

public class SharedPaymentController implements SharedPaymentControllerDocs {

    ...
    @CheckTravelAccess(travelIdParam = "id") // id๋ผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ travelIdParam์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค.
    public ResponseEntity<String> addAndUpdatePaymentRecord(
            @PathVariable Long id, // travel id๋กœ ์‚ฌ์šฉํ•  ํŒŒ๋ผ๋ฏธํ„ฐ ๋ช…์ด id์ž„
			...
    ) {

        ...
    }
}
728x90

'Back-end > Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Spring] Swagger ์„ค์ • ๋ฐ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•  (4) 2024.11.11
[Spring] Custome Exception ๋งŒ๋“ค๊ธฐ  (0) 2024.11.07