Spring - 사용자 접속 정보 및 요청 기록 남기기

이정수·2025년 11월 25일

Spring Boot

목록 보기
23/25
post-thumbnail

Interceptor을 통해 서버에 접속한 사용자의 IP 정보단말정보 가져와서 DB에 저장하기
서버에 접속한 사용자 정보를 JPA를 통해 DB에 저장
▶ 일일 방문자 수 등을 통계할 수 있음.

  • 인터셉터( Interceptor )
    Controller 기준 앞 또는 뒤에서 전달되는 HTTP 요청인터셉트하는 역할을 수행
    인터셉터를 통해 요청에 포함된 사용자정보 가져오기

서버 접속사용자 정보를 저장할 EntityRepository, Service 정의

  • Entity Class
@Getter
@Entity
public class VisitStat extends BaseEntity {
	private String ip;
	private String userAgent;
	private Long userId;
	private LocalDateTime visitedAt = LocalDateTime.now();
	public VisitStat(String ip, String userAgent, Long userId) {
		this.ip = ip;
		this.userAgent = userAgent;
		this.userId = userId;
	}
}
  • Repository
@Repository
public interface VisitStatRepository extends JpaRepository<VisitStat, Long> {}
  • Service
public interface VisitStatService {
	void create(
		Long userId,
		String ip,
		String userAgent
	);
}
@Service
@Transactional
@RequiredArgsConstructor
public class VisitStatServiceImpl implements VisitStatService {
	private final VisitStatRepository visitStatRepository;
	@Override
	public void create(
		Long userId,
		String ip,
		String userAgent
	) {
		visitStatRepository.save(
			new VisitStat(userId, userAgent, ip)
		);
	}
}
  • EDA를 통한 이벤트 발행에 사용할 POJO 정의
    저수준 클래스에서 인터셉터의존하는 것을 방지하기위해 정의
public record VisitorEvent(
	String ip,
	String userAgent,
	Long userId
) { }

ApplicationEventPublisher에 의해 해당 객체가 생성되어 발행 시 @EventListener(VisitorEvent.class)가 선언된 메서드 호출

  • 인터셉터를 정의하는 HadnlerInterceptor 구현체 클래스Spring Bean으로 등록 및 사용자정보클래스객체에 포함하여 이벤트발행
    HadnlerInterceptor구현HadnlerInterceptor 구현체 작성
    preHandle() : 컨트롤러 전달 전의 HTTP 요청인터셉트
    postHandle() : 컨트롤러 전달 후의 HTTP 요청인터셉트

    저수준 클래스에서 인터셉터의존하는 것을 방지하기위해 ApplicationEventPublisher를 사용하여 DIP 적용
    요청에서 가져온 사용자 IP, 사용자 이름을 해당 POJO이벤트 발행 수행
    @EventListener(VisitorEvent.class)가 선언된 메서드를 통해 다음 Logic을 수행
    Spring- EDA
@Component
@RequiredArgsConstructor
public class VisitStatInterceptor implements HandlerInterceptor {
	private final ApplicationEventPublisher applicationEventPublisher;
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		var principal = Optional.ofNullable(request.getUserPrincipal());
		var name = principal.isPresent() ? Long.parseLong(principal.get().getName()) : null;
		applicationEventPublisher.publishEvent(
			new VisitorEvent(
				request.getRemoteAddr(),
				request.getHeader("User-Agent"),
				name
			)
		);
		return true;
	}
}
  • WebMvcConfigurer구현하여 HadnlerInterceptor 구현체를 등록할 @Configuration Class 작성
    WebMvcConfigureraddInterceptors(InterceptorRegistry registry)registry.addInterceptor(visitStatInterceptor)를 통해 인터셉터를 등록
@Configuration
@RequiredArgsConstructor
public class WebConfiguration implements WebMvcConfigurer {
	// Spring Bean으로 등록한 VisitStatInterceptor을 의존성주입
	private final VisitStatInterceptor visitStatInterceptor;
	// 인터셉터 등록
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(visitStatInterceptor);
	}
}
  • EventListener 클래스 정의
    。해당 POJO Datatype이벤트 발행 시 수신하여 @EventListener(VisitorEvent.class) 메서드 호출

    @EventListener는 기본적으로 동기처리이므로 비동기처리@Async를 추가선언
    Spring에서 비동기처리 사용 시 진입점 클래스@EnableAsync를 추가선언
@Component
@RequiredArgsConstructor
public class InternalEventListener {
	private final VisitStatService visitStatService;
	@Async
	@EventListener(VisitorEvent.class)
	public void onVisitorEvent(VisitorEvent event) {
		visitStatService.create(
			event.userId(),
			event.ip(),
			event.userAgent()
		);
	}
}

요청 발생 시 HadnlerInterceptor 구현체에 의해 VisitorEvent객체가 생성 및 ApplicationEventPublisher에 의해 Event발행
▶ 해당 Listener 클래스에서 이벤트를 수신 및 VisitStatService를 통해 사용자정보Entity매핑DB Table로 저장

Controller를 통과하기전의 요청인터셉트하여 principal 정보를 DB Table로 저장

HandlerIntercepter
WebMvcConfigurer

번외 : 사용자 서비스 요청 기록 남기기
AOP 사용하기.

  • 사용자 요청에 의한 서비스기록을 저장할 EntityRepository, Service 정의

    Entity Class
public enum HistoryType {
	ORDER_CREATE,
	LOGIN,
	LOGOUT,
	PASSWORD_CHANGE;
}
@Entity
@Getter
@NoArgsConstructor
public class History extends BaseEntity {
	@Enumerated(value = EnumType.STRING)
	private HistoryType type;
	private String content;
	private Long userId;
	public History(HistoryType type, String content, Long userId) {
		this.type = type;
		this.content = content;
		this.userId = userId;
	}
}

Repository

@Repository
public interface HistoryRepository extends JpaRepository<History,Long> {}

Service

public interface HistoryService {
	void create(HistoryType type, String content, Long userId);
}
@Service
@Transactional
@RequiredArgsConstructor
public class HistoryServiceImpl implements HistoryService {
	private final HistoryRepository historyRepository;
	@Override
	public void create(HistoryType type, String content, Long userId) {
		historyRepository.save(
			new History(
				type, content, userId
			)
		);
	}
}
  • Logger커스텀 어노테이션 선언
    Service Class메서드에 선언하기위해 메서드레벨에서 선언
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceLogger {
	HistoryType type();
	String content() default "";
}
  • AOP 설정
    。해당 @ServiceLogger이 선언된 메서드 호출AOP에 의해 인터셉트되어 아래 구문을 수행
    UserDetails 객체를 가져와서 principal 정보를 출력하도록 설정
@Aspect
@Component
@RequiredArgsConstructor
public class LoggerAspect {
	private final HistoryService historyService;
	@Around("@annotation(com.kt.shopping.aspect.ServiceLogger) && @annotation(serviceLogger)")
	public Object serviceLogger(ProceedingJoinPoint joinPoint, ServiceLogger serviceLogger) throws Throwable {
		// 로그인 시 UserDetails 객체 가져오기
		CurrentUser principal = (CurrentUser)Arrays.stream(joinPoint.getArgs())
			.filter(arg -> arg instanceof CurrentUser)
			.findFirst().orElse(null);
		Long userId = principal != null ? principal.getId() : null;
		HistoryType type = serviceLogger.type();
		String content = serviceLogger.content();
		historyService.create(type, content, userId);
		return joinPoint.proceed();
	}
}
  • 어노테이션을 선언하여 해당 메서드에 대한 요청DB Table에 기록
	@ServiceLogger(type = HistoryType.LOGIN, content = "사용자 로그인")
	@PostMapping("/login")
	public ApiResult<LoginResponse.Login> login(@Valid @RequestBody LoginRequest.Login request){
		Pair<String,String> tokenPair = authService.login(request.loginId(), request.password());
		// DTO에 AccessToken 과 RefreshToken을 포함
		LoginResponse.Login responseDto = new LoginResponse.Login(tokenPair.getFirst(), tokenPair.getSecond());
		return ApiResult.ok(responseDto);
	}
    @ServiceLogger(type = HistoryType.ORDER_CREATE, content = "주문생성")
	@PostMapping
	public ApiResult<Void> create(
		@AuthenticationPrincipal CurrentUser currentUser, // Spring Security 를 통해 획득
		@RequestBody @Valid OrderRequest.Create request
	){
		orderService.create(
			currentUser.getId(), //
			request.prodId(),
			request.receiverName(),
			request.receiverAddress(),
			request.receiverMobile(),
			request.quantity()
			);
		return ApiResult.ok();
	}


▶ 다음처럼 DB Entity매핑DB Table

profile
공부기록 블로그

0개의 댓글