스프링 AOP에 대해 설명하고, 실제 사용사례를 들어 그 적용 방식을 구체적으로 설명해 보세요.
스프링 AOP는 공통된 로직을 비즈니스 로직과 분리하고 싶을 때 사용한다.
주로 로깅, 보안, 트래잭션 관리 같은 관심사를 처리할 때 활용하게 된다.
포커파인더 어드민에서는 API 호출시 @JwtRequired라는 어노테이션을 만들어서 AOP로 JWT 인증로직을 분리하여 사용했다.
AOP는 프록시 패턴으로 동작한다. 프록시 객체가 비즈니스 로직 전후로 부가 작업을 처리한다.
항상 사용하는 @Transactional이 AOP의 대표적인 예이다.
프록시는 Spring Boot에서는 CGLib를 사용한다. 호출 클래스를 로그 찍어보면 $$CGLib이런식으로 표시되는 것을 통해 확인할 수 있다.
타겟에 대한 호출이 들어오면 AOP 프록시가 이를 가로챈다.
AOP 프록시에서 Transaction Advisor가 commit / rollback 등의 트랜잭션 처리를 한다.
트랜잭션 처리 외에 다른 부가 기능이 있을 경우 해당 커스텀 Advisor에서 그 처리를 한다.
각 Advisor에서 부가 기능 처리를 마치면 타겟 method를 수행한다.
interceptor chain을 따라 원래 호출한 곳에 결과를 다시 전달한다.
이러한 프록시 방식 때문에 주의해야 하는 점이 있다.
- private은 트랜잭션 처리를 할 수 없다.
- 프록시 객체는 타겟을 상속받아서 구현한다고 했다. private 제어자 상태이면 자식인 프록시 객체에서 호출할 수가 없다. 따라서 @Transactional이 붙는 메서드, 클래스는 프록시 객체에서 접근 가능한 접근 레벨로 설정해야 한다.
- @Transactional에는 전파 옵션이 있다.
- REQUIRED
- 기본 설정이다.
- 부모 트랜잭션이 있는 경우, 새로 트랜잭션을 생성하지 않고 부모트랜잭션에 합쳐진다.
- 부모 트랜잭션이 없는 경우에만 자신의 트랜잭션을 새로 생성한다.
- REQUIRES_NEW
- 보무 트랜잭션 여부와 상관없이 무조건 자신만의 트랜잭션을 새로 생성한다.
- 기존에 실행중이던 부모 트랜잭션은 자식 트랜잭션이 종료될 때까지 대기한다.
- 자식 트랜잭션이 롤백되더라도 부모 트랜잭션이 롤백되지 않는다.
- 자식 트랜잭션이 정상적으로 종료되고 부모 트랜잭션이 롤백되는 경우에도 자식 트랜잭션은 롤백되지 않는다.
- 따라서 이 옵션에서는 각 트랜잭션이 상호 독립적이다.
- NESTED
- 부모 트랜잭션의 여부와 상관없이 무조건 자신의 트랜잭션을 생선한다.
- 기존 실행중이던 부모 트랜잭션은 자식 트랜잭션이 종료될 때까지 대기한다.
- 자식 트랜잭션은 부모 트랜잭션과 함께 커밋된다.
- 자식 트랜잭션이 롤백되는 경우, 부모 트랜잭션도 롤백된다.
- 자식 트랜잭션이 정상적으로 종료되고 부모 트랜잭션이 롤백되는 경우, 자식 트랜잭션도 함께 롤백된다.
- REQUIRES_NEW와 다른 점은, 부모와 자식 트랜잭션의 롤백이 상호 종속적이라는 것이다.
- SUPPORTS
- 부모 트랜잭션이 있는 경우, 부모 트랜잭션에 합쳐진다.
- 부모 트랜잭션이 없는 경우 트랜잭션을 적용하지 않는다.
- NOT_SUPPORTED
- 부모 트랜잭션이 있는 경우, 부모 트랜잭션을 일시중지 시킨다.
- 이후 트랜잭션을 적용하지 않은 채로 메서드를 수행하고, 메서드가 종료된 후 부모 트랜잭션을 재개한다.
- 부모 트랜잭션이 없는 경우, 트랜잭션을 적용하지 않는다.
- MANDATORY
- 부모 트랜잭션이 있는 경우, 새로 트랜잭션을 생성하지 않고 부모 트랜잭션에 합쳐진다.
- 부모 트랜잭션이 없는 경우, 예외가 발생한다.
- NEVER
- 부모 트랜잭션이 있는 경우, 예외가 발생한다.
- 부모 트랜잭션이 없는 경우, 트랜잭션을 적용하지 않는다.
- 따라서 트랜잭션을 절대 사용하지 않는 경우에 붙인다.
- 같은 클래스 내부에 있는 @Transactional 메서드는 원하는 대로 동작하지 않는다.
- 예를 들어 class A 안에 Transactional이 붙는 메서드 a(), b()가 있다고 하자.
- a 안에서 b를 호출하는 경우 원하는 대로 동작하지 않는다.
- 왜냐하면 AOP에서 class A의 프록시를 만들어서 그 클래스 전후로 트랜잭션 열고 닫는 코드를 추가해주기 때문에 하나의 트랜잭션만 열리게 되기 때문이다.\
- 따라서 Transactional 안에서 Transactional이 붙는 메서드를 실행시키고 싶으면 클래스 자체를 분리해서 실행시킨다고 생각하는 게 편하다.
JWT의 내부 구조와 동작 방식을 설명해 보세요.
JWT는 Header, Payload, Signature 세 부분으로 나뉜다.
Header에는 토큰 유형과 서명 알고리즘 정보가 포함된다.
Payload에는 Claim이 담겨있다. (원하는 정보를 넣는 것임.)
Signature는 HMAC, RSA 등의 알고리즘으로 서명되어 토큰의 무결성을 보장한다.
JWT는 서버가 상태를 저장하지 않고도 클라이언트의 인증 상태를 관리할 수 있어서 stateless 애플리케이션을 만들 수 있게 해준다.
서명 검증은 클라이언트가 Header에 Bearer {token} 형태로 전송한 토큰값을 서버에서 동일한 비밀키나 공개키로 검증하는 방식으로 이루어진다.
따라서 키를 탈취당하는 경우에는 보안 이슈가 터지기 때문에 키 관리를 신경써야 한다.
키를 주기적으로 변경하거나 JWT 만료 시간을 짧게 설정하여 리스크를 줄일 수 있다.