๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Spring

Spring AOP, ํŠธ๋žœ์žญ์…˜, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

์Šคํ”„๋ง AOP, ํŠธ๋žœ์žญ์…˜, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

์ŠคํŒŒ๋ฅดํƒ€ ์ฝ”๋”ฉ ํด๋Ÿฝ์˜ Spring ์‹ฌํ™”๋ฐ˜ 5์ฃผ ์ฐจ ๋‚ด์šฉ์ธ ์Šคํ”„๋ง AOP, ํŠธ๋žœ์žญ์…˜, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ •๋ฆฌํ•œ๋‹ค.

ํ•ต์‹ฌ ๋‚ด์šฉ

  • AOP์˜ ๊ฐœ๋… ์ดํ•ด์™€ ์‚ฌ์šฉ๋ฐฉ๋ฒ•
  • DB ํŠธ๋žœ์žญ์…˜ ์ดํ•ด
  • ์Šคํ”„๋ง ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฐฉ๋ฒ•

AOP์˜ ๊ฐœ๋… ์ดํ•ด์™€ ์‚ฌ์šฉ๋ฐฉ๋ฒ•

AOP (Aspect Oriented Programming)

AOP : ๊ด€์  ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ

AOP์˜ ์‚ฌ์šฉ ์ด์œ 

ํ•ต์‹ฌ ๊ธฐ๋Šฅ๊ณผ ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ•œ๋‹ค๊ณ  ํ•˜์ž.

ํ•ต์‹ฌ ๊ธฐ๋Šฅ์€ ๋ง ๊ทธ๋Œ€๋กœ API๊ฐ€ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ํ•ต์‹ฌ์ ์ธ ๊ธฐ๋Šฅ์ด๊ณ , ๋ถ€๊ฐ€๊ธฐ๋Šฅ์€ ๋ถ€๊ฐ€์ ์œผ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ๋  ๋กœ๊น…, ์ธ์ฆ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์ด๋‹ค.

๋ชจ๋“  ํ•ต์‹ฌ ๊ธฐ๋Šฅ์— ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด์•ผ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•  ๋•Œ, ํ•ต์‹ฌ ๊ธฐ๋Šฅ์˜ ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ๊ทธ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์— ํ•˜๋‚˜ํ•˜๋‚˜ ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ๋„ฃ์–ด์•ผ ํ•˜๊ณ , ์ด๋•Œ ์‹ค์ˆ˜๋ฅผ ํ•  ๊ฐ€๋Šฅ์„ฑ๊ณผ ์ถ”ํ›„์— ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ์œ ์ง€ ๋ณด์ˆ˜ํ•จ์— ์žˆ์–ด์„œ ์–ด๋ ค์›€์ด ์ƒ๊ธด๋‹ค.

์ด๋•Œ AOP๋Š” ๋‹ค๋ฅธ ๊ด€์ (Aspect)์„ ๊ฐ€์ง„ ๋ถ€๊ฐ€๊ธฐ๋Šฅ๊ณผ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ถ€๊ฐ€๊ธฐ๋Šฅ ์ค‘์‹ฌ์œผ๋กœ ์„ค๊ณ„, ๊ตฌํ˜„ํ•˜๋„๋ก ๋„์™€์ค€๋‹ค.

OOP(๊ฐ์ฒด์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ) VS AOP

OOP๋Š” ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ๋ชจ๋“ˆํ™” ํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ
AOP๋Š” ๋ถ€๊ฐ€๊ธฐ๋Šฅ(๋กœ๊น…, ํŠธ๋žœ์žญ์…˜ ...)์„ ๋ชจ๋“ˆํ™”ํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ
๋”ฐ๋ผ์„œ AOP๋Š” OOP๋ฅผ ๋Œ€์ฒดํ•ด ์ฃผ๋Š” ๊ฒƒ์ด ์•„๋‹Œ ๋ณด์™„ ํ•ด์ฃผ๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ด๋‹ค.

์–ด๋“œ๋ฐ”์ด์Šค, ํฌ์ธํŠธ ์ปท

  • ์–ด๋“œ๋ฐ”์ด์Šค : ๋ถ€๊ฐ€๊ธฐ๋Šฅ
  • ํฌ์ธํŠธ ์ปท : ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ์ ์šฉํ•  ์œ„์น˜

๋™์ž‘ ์›๋ฆฌ

์Šคํ”„๋ง(๋””์ŠคํŒจ์ณ ์„œ๋ธ”๋ฆฟ)์ด ํ•ต์‹ฌ ๊ธฐ๋Šฅ์— ์ ‘๊ทผํ•˜๊ธฐ ์ „์—, ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ์ค‘๊ฐ„์— ์‚ฝ์ž…ํ•˜๋Š” ์›๋ฆฌ๋กœ AOP๊ฐ€ ๋™์ž‘ํ•œ๋‹ค.

์†Œ์Šค ์˜ˆ์‹œ


@Component // ์Šคํ”„๋ง IoC ์— ๋นˆ์œผ๋กœ ๋“ฑ๋ก
@Aspect
public class UserTimeAop {
    private final UserTimeRepository userTimeRepository;

    public UserTimeAop(UserTimeRepository userTimeRepository) {
        this.userTimeRepository = userTimeRepository;
    }

    @Around("execution(public * com.sparta.springcore.controller..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        // ์ธก์ • ์‹œ์ž‘ ์‹œ๊ฐ„
        long startTime = System.currentTimeMillis();

        try {
            // ํ•ต์‹ฌ๊ธฐ๋Šฅ ์ˆ˜ํ–‰
            Object output = joinPoint.proceed();
            return output;
        } finally {
            // ์ธก์ • ์ข…๋ฃŒ ์‹œ๊ฐ„
            long endTime = System.currentTimeMillis();
            // ์ˆ˜ํ–‰์‹œ๊ฐ„ = ์ข…๋ฃŒ ์‹œ๊ฐ„ - ์‹œ์ž‘ ์‹œ๊ฐ„
            long runTime = endTime - startTime;
            // ๋กœ๊ทธ์ธ ํšŒ์›์ด ์—†๋Š” ๊ฒฝ์šฐ, ์ˆ˜ํ–‰์‹œ๊ฐ„ ๊ธฐ๋กํ•˜์ง€ ์•Š์Œ
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
                // ๋กœ๊ทธ์ธ ํšŒ์› -> loginUser ๋ณ€์ˆ˜
                UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
                User loginUser = userDetails.getUser();

                // ์ˆ˜ํ–‰์‹œ๊ฐ„ ๋ฐ DB ์— ๊ธฐ๋ก
                UserTime userTime = userTimeRepository.findByUser(loginUser);
                if (userTime != null) {
                    // ๋กœ๊ทธ์ธ ํšŒ์›์˜ ๊ธฐ๋ก์ด ์žˆ์œผ๋ฉด
                    long totalTime = userTime.getTotalTime();
                    totalTime = totalTime + runTime;
                    userTime.updateTotalTime(totalTime);
                } else {
                    // ๋กœ๊ทธ์ธ ํšŒ์›์˜ ๊ธฐ๋ก์ด ์—†์œผ๋ฉด
                    userTime = new UserTime(loginUser, runTime);
                }

                System.out.println("[User Time] User: " + userTime.getUser().getUsername() + ", Total Time: " + userTime.getTotalTime() + " ms");
                userTimeRepository.save(userTime);
            }
        }
    }
}

@Around ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด AOP๋ฅผ ์ ์šฉํ•  ๋ฒ”์œ„๋ฅผ ์ •ํ•ด์ค€๋‹ค.

joinPoint.proceed(); ๋กœ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

DB ํŠธ๋žœ์žญ์…˜์˜ ์ดํ•ด

ํŠธ๋žœ์žญ์…˜

ํŠธ๋žœ์žญ์…˜: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ํ•˜๋‚˜์˜ ๋…ผ๋ฆฌ์  ์‹คํ–‰๋‹จ๊ณ„

ACID (์›์ž์„ฑ, ์ผ๊ด€์„ฑ, ๊ณ ๋ฆฝ์„ฑ, ์ง€์†์„ฑ)๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ˆ˜ํ–‰๋œ๋‹ค๋Š” ๊ฒƒ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•œ ์„ฑ์งˆ์„ ๊ฐ€๋ฆฌํ‚ค๋Š” ์•ฝ์–ด

์ถœ์ฒ˜: ์œ„ํ‚ค๋ฐฑ๊ณผ

ํŠธ๋žœ์žญ์…˜์˜ ํŠน์ง•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • ๋” ์ด์ƒ ์ชผ๊ฐค ์ˆ˜ ์—†๋Š” ์ตœ์†Œ๋‹จ์œ„์˜ ์ž‘์—…
  • ๋ชจ๋‘ ์ €์žฅ๋˜๊ฑฐ๋‚˜, ์•„๋ฌด๊ฒƒ๋„ ์ €์žฅ๋˜์ง€ ์•Š๊ฑฐ๋‚˜๋ฅผ ๋ณด์žฅํ•œ๋‹ค.

ํŠธ๋žœ์žญ์…˜๊ณผ AOP

ํŠธ๋žœ์žญ์…˜์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„  ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜์„ ๋งŒ๋“ค์–ด์ค€ ๋’ค, try, catch๋ฌธ์œผ๋กœ ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณตํ•˜๋ฉด commit, ์‹คํŒจํ•˜๋ฉด rollback์„ ์ ์šฉํ•ด ์ค˜์•ผ ํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ์Šคํ”„๋ง์—์„  @Transactional ์ด๋ผ๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ์„ ์–ธํ•ด ์ฃผ๋ฉด, ํŠธ๋žœ์žญ์…˜์— ๋Œ€ํ•œ ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์–ด ๋”ฐ๋กœ ํŠธ๋žœ์žญ์…˜์— ๋Œ€ํ•œ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•„๋„ ํŠธ๋žœ์žญ์…˜์ด ์ ์šฉ๋œ๋‹ค.

ํ˜„์—…์—์„œ์˜ DB ์šด์˜ ๋ฐฉ์‹

  • ์“ฐ๊ธฐ ์ „์šฉ DB(Primary)์™€ ์ฝ๊ธฐ ์ „์šฉ DB(Replica)๋ฅผ ๊ตฌ๋ถ„
  • Primary๋Š” ์“ฐ๊ธฐ ์ „์šฉ์œผ๋กœ์จ @Transactional(readOnly = false)(๊ธฐ๋ณธ๊ฐ’์ด false)
  • Replica๋Š” ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ์จ @Transactional(readOnly = true)
  • Primary์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ, Replica ์ค‘ 1๊ฐœ๊ฐ€ Primary๊ฐ€ ๋˜์–ด DB๊ฐ€ ์ •์ƒ ์šด์˜๋œ๋‹ค.

์Šคํ”„๋ง ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•

ํด๋ž˜์Šค ์ƒ์„ฑ

  • ํŠน์ • ์˜ˆ์™ธ๋ฅผ ์ƒ์†๋ฐ›๋Š” ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“  ๋’ค, ์˜ˆ์™ธ์˜ ๋ฉ”์„ธ์ง€๋ฅผ super()๋กœ ๋ฐ›์•„์ค€๋‹ค.
  • @RestControllerAdvice(๋ ˆ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ์—๋งŒ ์ ์šฉ) ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ธ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด ํŠน์ • ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ „์—ญ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ด ์ค€๋‹ค.
  • ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•  ์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ @ExceptionHandler ์–ด๋…ธํ…Œ์ด์…˜์˜ value์— ๋„ฃ์–ด์ฃผ๊ณ  ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

์˜ˆ์‹œ

public class ApiRequestException extends IllegalArgumentException {
    public ApiRequestException(String message) {
        super(message);
    }

    public ApiRequestException(String message, Throwable cause) {
        super(message, cause);
    }
}

 

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(value = { ApiRequestException.class })
    public ResponseEntity<Object> handleApiRequestException(ApiRequestException ex) {
        ApiException apiException = new ApiException(
                ex.getMessage(),
                // HTTP 400 -> Client Error
                HttpStatus.BAD_REQUEST
        );

        return new ResponseEntity<>(
                apiException,
                // HTTP 400 -> Client Error
                HttpStatus.BAD_REQUEST
        );
    }
}