๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BE/๐Ÿƒ Spring

[Spring Boot] AOP๋ฅผ ํ™œ์šฉํ•œ ์ปค์Šคํ…€ ์• ๋…ธํ…Œ์ด์…˜ ๋งŒ๋“ค๊ธฐ

by ํ‹ด๋”” 2024. 11. 19.
๋ฐ˜์‘ํ˜•

2024.11.17 - [BE/๐Ÿƒ Spring] - [Spring Boot] AOP ๊ฐœ๋… ์ •๋ฆฌ

2024.11.18 - [BE/๐Ÿƒ Spring] - [Spring Boot] AOP ์‚ฌ์šฉ ๋ฐฉ๋ฒ•๊ณผ ์˜ˆ์‹œ์ฝ”๋“œ

 

ํ˜„์žฌ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ์˜ ์ผ๋ถ€ API์—์„œ ์š”์ฒญ/์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ End-to-End ์•”ํ˜ธํ™” ํ•ด์„œ ํ†ต์‹ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‹ค ๋ณด๋‹ˆ Controller ํ˜น์€ ์•”๋ณตํ˜ธํ™”์™€ ๊ด€๋ จ ์—†๋Š” ๋กœ์ง์— ์•”ํ˜ธํ™” ๊ด€๋ จ ์ฝ”๋“œ๊ฐ€ ๋“ค์–ด๊ฐ€์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. 

์•”๋ณตํ˜ธํ™”๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” static object๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉ์„ ํ•ด์™”์—ˆ๋Š”๋ฐ, AOP๋ฅผ ์ด์šฉํ•ด์„œ ์ด๋ฅผ ๊ฐœ์„ ํ•ด ๋ณด๋ฉด ์–ด๋–จ๊นŒ ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. ์š”์ฒญ ๊ฐ์ฒด ์ž์ฒด๋ฅผ ๊ฐ€๋กœ์ฑ„ ๋ณตํ˜ธํ™” ํ•ด์„œ ํ‰๋ฌธ๊ฐ’์„ ๋ฐ”๋กœ Controller์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ณ , ์‘๋‹ต์„ ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌํ•˜๊ธฐ ์ „์— ์‘๋‹ต ๊ฐ’์„ ๊ฐ€๋กœ์ฑ„ ์•”ํ˜ธํ™” ํ•˜๊ณ  ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ Controller์—์„œ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ์ธ ์•”๋ณตํ˜ธํ™” aspect๋ฅผ ๋ถ„๋ฆฌํ•ด ๋‚ผ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. 

 

์ผ๋ถ€ API์—์„œ ์•”๋ณตํ˜ธํ™”๊ฐ€ ์ง„ํ–‰๋ฌ๊ธฐ ๋•Œ๋ฌธ์— Controller ํด๋ž˜์Šค๋‚˜ ํŒจํ‚ค์ง€ ๋‹จ์œ„๊ฐ€ ์•„๋‹Œ ์ปค์Šคํ…€ ์• ๋„ˆํ…Œ์ด์…˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์š”์ฒญ/์‘๋‹ต ์•”๋ณตํ˜ธํ™”๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋กœ ์ธํ•ด ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ์–ด๋–ค ๋™์ž‘์„ ํ•  ๊ฒƒ์ธ์ง€ ํ•จ๊ป˜ ์ผํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ช…์‹œ์ ์œผ๋กœ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ฒฐ๋ก ์ ์œผ๋กœ ํ•ด๋‹น ๋ฐฉ๋ฒ•์„ ์‹ค์ œ ์ฝ”๋“œ์— ์ ์šฉํ•˜์ง„ ๋ชปํ–ˆ์ง€๋งŒ AOP์™€ ์• ๋„ˆํ…Œ์ด์…˜ ์กฐํ•ฉ์„ ๊ณต์œ ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„ ๊ธ€์„ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค


๊ธฐ๋ณธ์ฝ”๋“œ

    @PostMapping("/regist")
    public ResponseEntity<BaseResponse> regist(
            @RequestBody RegistAccountRequest request
    ) {
        RegistAccountResponse registAccountResponse = bankService.registAccount(request);
        BaseResponse response = BaseResponse.builder()
                .code("00000")
                .msg("success")
                .data(registAccountResponse)
                .build();
        return ResponseEntity.ok(response);
    }

 

// request
{
    "owner_name" : "user1",
    "password" : "2017",
    "amount" : 1000000
}

// response
{
    "code": "00000",
    "msg": "success",
    "data": {
        "accountNumber": "084256-19-1458"
    }
}

 

๊ธฐ๋ณธ ์ฝ”๋“œ๋Š” ํ‰๋ฌธ request/response๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

 

์•”๋ณตํ˜ธํ™” 

    @PostMapping("/regist")
    public ResponseEntity<BaseResponse> regist(
            @RequestBody BaseRequest request
    ) throws Exception {
        String plaintText = AESUtil.decrypt(request.getData()); // v
        RegistAccountRequest registAccountRequest = objectMapper.readValue(plaintText, RegistAccountRequest.class); // v
        RegistAccountResponse registAccountResponse = bankService.registAccount(registAccountRequest);
        String dataJson = objectMapper.writeValueAsString(registAccountResponse); // v

        BaseResponse response = BaseResponse.builder()
                .code("00000")
                .msg("success")
                .data(AESUtil.encrypt(dataJson)) // v
                .build();

        return ResponseEntity.ok(response);
    }

 

์—„๊ฒฉํ•œ ๋ณด์•ˆ์„ ์œ„ํ•ด End-to-End ์•”ํ˜ธํ™”๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค๊ณ  ํ•ด๋ด…์‹œ๋‹ค. ์˜ˆ์ œ ์ฝ”๋“œ์—์„œ๋Š” Controller์— request๋ฅผ ๋ณตํ˜ธํ™” ํ•˜๊ณ  response๋ฅผ ์•”ํ˜ธํ™” ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ„๋‹จํžˆ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ์ค‘์—์„œ reqeust๋ฅผ ํ‰๋ฌธ์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ๊ณผ, ์‘๋‹ต์˜ body์— ๋“ค์–ด๊ฐ€๋Š” ๊ฐ์ฒด๋ฅผ ์•”ํ˜ธํ™” ํ•˜๋Š” ๊ฒƒ์„ ์ปค์Šคํ…€ ์• ๋„ˆํ…Œ์ด์…˜๊ณผ aop๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ž‘์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

// request
{   
    "data" : "/PJ/p778x9sf8G0YXUUxGwn5NRhrPu4gWqwxKxDdQMxwq+wxJAxOucVvZSmzEnjNu/Z41THSQKzaQn8IVEZxfg=="
}

// response
{
    "code": "00000",
    "msg": "success",
    "data": "dMDQDB5ePZTBfxDJtkSREbZEHxguTT5RI+4MoBjtcVNWimRi/Ypg1NlXrHr9Rn+O"
}

 

์•”๋ณตํ˜ธํ™”์‹œ request/response ์˜ˆ์‹œ๋Š” ์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์˜ˆ์‹œ๋ฅผ ์œ„ํ•ด ๊ฐ„๋‹จํ•œ ์•”ํ˜ธํ™”๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

AESUtil

public class AESUtil {
    private static final String SECRET_KEY = "youabledevsecret";

    public static String encrypt(String data) throws Exception {
        SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes()));
    }

    public static String decrypt(String data) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        return new String(cipher.doFinal(Base64.getDecoder().decode(data)));
    }
}

 

์˜ˆ์‹œ ์ฝ”๋“œ๋ฅผ ์œ„ํ•ด ๋ณด์•ˆ์— ์ทจ์•ฝํ•œ ์•”๋ณตํ˜ธํ™” Util์„ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

 

Custom Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequest {
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptResponse {
}

 

์ปค์Šคํ…€ ์• ๋„ˆํ…Œ์ด์…˜์„ ์ƒ์„ฑํ•ด ์ค๋‹ˆ๋‹ค.

DecryptRequest๋Š” ์•”ํ˜ธํ™”๋œ ์š”์ฒญ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณตํ˜ธํ™” ํ•˜๊ณ , EncryptResponse๋Š” ์‘๋‹ต์„ ์•”ํ˜ธํ™” ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค

 

AOP

@Aspect
@Component
@RequiredArgsConstructor
public class EncryptionAspect {

}

 

Aspect๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค๋‹ˆ๋‹ค.

 

    @Around("@annotation(DecryptRequest)")
    public Object decryptRequest(ProceedingJoinPoint joinPoint, DecryptRequest decryptRequest) throws Throwable {
        Object[] args = joinPoint.getArgs();

        if (args.length > 0 && args[0] instanceof BaseRequest request) {
            String decryptedJson = AESUtil.decrypt(request.getData());
            request.setData(decryptedJson);
        }
        return joinPoint.proceed(args);
    }

 

DecryptRequest ์• ๋„ˆํ…Œ์ด์…˜์ด ๋ถ™์–ด ์žˆ๋Š” ๋ฉ”์„œ๋“œ์˜ ์ฒซ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์ธ BaseRequest์˜ data๋ฅผ ๋ณตํ˜ธํ™” ํ•˜์—ฌ ๋‹ค์‹œ ํ•ด๋‹น ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌํ•ด์ค๋‹ˆ๋‹ค. 

 

    @Around("@annotation(EncryptResponse)")
    public Object encryptResponse(ProceedingJoinPoint joinPoint, EncryptResponse encryptResponse) throws Throwable {
        Object result = joinPoint.proceed();

        if (result instanceof ResponseEntity<?> responseEntity) {
            Object body = responseEntity.getBody();

            if (body instanceof BaseResponse baseResponse) {
                String dataJson = objectMapper.writeValueAsString(baseResponse.getData());
                String encryptedData = AESUtil.encrypt(dataJson);

                BaseResponse encryptedResponse = BaseResponse.builder()
                        .code(baseResponse.getCode())
                        .msg(baseResponse.getMsg())
                        .data(encryptedData)
                        .build();

                return ResponseEntity.ok(encryptedResponse);
            }
        }

        return result;
    }

 

EncryptResponse ์• ๋„ˆํ…Œ์ด์…˜์ด ์ถ”๊ฐ€๋œ ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๊ณ  ๋‚œ ํ›„ ๋ฆฌํ„ด ๊ฐ’์ธ ResponseEntity<BaseResponse>์„ ์ „๋‹ฌ ๋ฐ›์•„ BaseResponse์˜ data ๋ถ€๋ถ„์„ ์•”ํ˜ธํ™” ํ•˜์—ฌ ๋‹ค์‹œ ํ˜ธ์ถœ๋œ ๋ฉ”์„œ๋“œ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

 

Result

    @DecryptRequest
    @EncryptResponse
    @PostMapping("/regist")
    public ResponseEntity<BaseResponse> regist(
            @RequestBody BaseRequest request
    ) throws Exception {
        RegistAccountRequest registAccountRequest = objectMapper.readValue(request.getData(), RegistAccountRequest.class); // v
        RegistAccountResponse registAccountResponse = bankService.registAccount(registAccountRequest);
        BaseResponse response = BaseResponse.builder()
                .code("00000")
                .msg("success")
                .data(registAccountResponse)
                .build();

        return ResponseEntity.ok(response);
    }

 

์š”์ฒญ/์‘๋‹ต ์•”๋ณตํ˜ธํ™”๋ฅผ ์›ํ•˜๋Š” ๋ฉ”์„œ๋“œ์— ์• ๋„ˆํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. Controller ๋กœ์ง์—์„œ ์•”๋ณตํ˜ธํ™” ํ•˜๋Š” ๋กœ์ง์ด Aspect๋กœ ๋ถ„๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

 

// request
{   
    "data" : "/PJ/p778x9sf8G0YXUUxGwn5NRhrPu4gWqwxKxDdQMxwq+wxJAxOucVvZSmzEnjNu/Z41THSQKzaQn8IVEZxfg=="
}

// response
{
    "code": "00000",
    "msg": "success",
    "data": "dMDQDB5ePZTBfxDJtkSREfohsFHbBF6VrddM4ZnyN3hWimRi/Ypg1NlXrHr9Rn+O"
}

 

 

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€