[Spring] 횡단 로직 - AOP를 활용한 로그인 체크 기능 구현하기

2024. 10. 28. 12:41·Spring

횡단 로직 ?

 

횡단 로직은 애플리케이션의 여러 부분에 공통적으로 적용되는 기능을 말한다.

로깅, 보안, 트랜잭션 관리 등이 이에 해당한다.

횡단 로직을 효과적으로 관리하기 위해 AOP(Aspect-Oriented Programming)를 사용한다.

AOP를 통해 코드 중복을 줄이고 관심사를 분리할 수 있다.

 


이번 글에서는 AOP를 이용하여 로그인 체크 로직을 구현하는 방법을 정리해보려고 한다.

아래 코드는 지금 현재 진행중인 팀 프로젝트 코드의 일부이다.


 

기본 구성

@LoginCheck

package com.coma.app.view.annotation;

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

@Target({ElementType.METHOD, ElementType.TYPE}) // 어노테이션이 메서드와 클래스에 적용될 수 있도록 설정
@Retention(RetentionPolicy.RUNTIME) // 런타임 시에 어노테이션 정보가 유지되도록 설정
public @interface LoginCheck {
    // @interface는 자바에서 새로운 어노테이션을 정의하기 위해 사용하는 키워드
}

 

 

@Target({ElementType.METHOD, ElementType.TYPE}): 이 어노테이션은 메서드와 클래스에 적용될 수 있다.

@Retention(RetentionPolicy.RUNTIME): 이 어노테이션은 런타임까지 유지된다. 따라서 런타임에 리플렉션을 통해 어노테이션을 읽을 수 있다.

 

이해 +)

 

https://www.youtube.com/watch?v=AfC5DK04INA&t=183s

 

 

LoginCheckImpl 클래스

package com.coma.app.view.annotation;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;

@Component
public class LoginCheckImpl {

    private static final String MEMBER_ID = "MEMBER_ID"; // 회원 ID를 나타내는 상수
    private static final String CREW_CHECK = "CREW_CHECK"; // 크루 체크를 나타내는 상수

    // 현재 요청과 응답, 세션 객체를 이용하여 로그인 정보를 검사하는 메서드.
    public void checkLogin(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
        String[] loginInfo = getLoginInformation(request, session); // 로그인 정보를 가져옴
        synchronizeLoginInformation(loginInfo, session); // 세션과 쿠키 간의 로그인 정보를 동기화

        if (loginInfo[0] == null) { // 로그인 정보가 없으면
            String redirectUrl = redirectLogin(); // 로그인 페이지로 리다이렉트 URL 설정
            if (redirectUrl != null) { // 리다이렉트 URL이 null이 아니면
                try {
                    response.sendRedirect(redirectUrl); // 로그인 페이지로 리다이렉트
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 로그아웃 시 세션과 쿠키를 무효화하는 메서드
    public static void logout(HttpServletRequest request, HttpServletResponse response) {
        invalidateSession(request); // 세션 무효화
        clearCookies(request, response); // 쿠키 무효화
    }

    // 세션을 무효화하는 메서드
    private static void invalidateSession(HttpServletRequest request) {
        HttpSession session = request.getSession(false); // 세션이 존재하면 반환하고, 그렇지 않으면 null 반환
        if (session != null) { // 세션이 null이 아니면
            session.invalidate(); // 세션 무효화
        }
    }

    // 쿠키를 무효화하는 메서드
    private static void clearCookies(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies(); // 요청에서 쿠키 배열을 가져옴
        if (cookies != null) { // 쿠키가 null이 아니면
            for (Cookie cookie : cookies) { 
                if (MEMBER_ID.equals(cookie.getName()) || CREW_CHECK.equals(cookie.getName())) { 
                    cookie.setMaxAge(0); // 쿠키를 무효화 (만료 시간 0 설정)
                    cookie.setPath("/"); // 전체 경로에 적용
                    response.addCookie(cookie); // 응답에 추가
                }
            }
        }
    }

    // 요청과 세션 객체에서 로그인 정보를 가져오는 메서드
    private String[] getLoginInformation(HttpServletRequest request, HttpSession session) {
        String[] loginInfo = new String[2]; // 로그인 정보 배열 생성
        fillLoginInfoFromCookies(request, loginInfo); // 쿠키에서 로그인 정보를 가져와 배열에 저장
        fillLoginInfoFromSession(session, loginInfo); // 세션에서 로그인 정보를 가져와 배열에 저장
        return loginInfo; // 로그인 정보 배열 반환
    }

    private void fillLoginInfoFromCookies(HttpServletRequest request, String[] loginInfo) {
        Cookie[] cookies = request.getCookies(); // 요청에서 쿠키 배열을 가져옴
        if (cookies != null) { // 쿠키가 null이 아니면
            for (Cookie cookie : cookies) { 
                if (MEMBER_ID.equals(cookie.getName())) { // 쿠키 이름이 MEMBER_ID인 경우
                    loginInfo[0] = cookie.getValue(); // 배열의 첫 번째 요소에 쿠키 값을 저장
                } else if (CREW_CHECK.equals(cookie.getName())) { // 쿠키 이름이 CREW_CHECK인 경우
                    loginInfo[1] = cookie.getValue(); // 배열의 두 번째 요소에 쿠키 값을 저장
                }
            }
        }
    }

    private void fillLoginInfoFromSession(HttpSession session, String[] loginInfo) {
        if (loginInfo[0] == null) { // 배열의 첫 번째 요소가 null인 경우
            loginInfo[0] = (String) session.getAttribute(MEMBER_ID); // 세션에서 MEMBER_ID를 가져와 저장
        }
        if (loginInfo[1] == null) { // 배열의 두 번째 요소가 null인 경우
            loginInfo[1] = (String) session.getAttribute(CREW_CHECK); // 세션에서 CREW_CHECK를 가져와 저장
        }
    }

    private void synchronizeLoginInformation(String[] loginInfo, HttpSession session) {
        if (session.getAttribute(MEMBER_ID) == null && loginInfo[0] != null) { // 세션의 MEMBER_ID가 null이고 배열의 첫 번째 요소가 null이 아닌 경우
            session.setAttribute(MEMBER_ID, loginInfo[0]); // 세션의 MEMBER_ID에 배열의 첫 번째 요소를 저장
        }
        if (session.getAttribute(CREW_CHECK) == null && loginInfo[1] != null) { // 세션의 CREW_CHECK가 null이고 배열의 두 번째 요소가 null이 아닌 경우
            session.setAttribute(CREW_CHECK, loginInfo[1]); // 세션의 CREW_CHECK에 배열의 두 번째 요소를 저장
        }
    }

    private String redirectLogin() {
        return "redirect:login.do"; // 로그인 페이지 URL 반환
    }
}

 

LoginCheckImpl 클래스는 로그인 상태를 검사하고 유지하는 데 필요한 로직을 포함한다. 

 

이 클래스는 HTTP Request, Response, 그리고 HTTP Session의 정보를 바탕으로 로그인 상태를 확인한다.


checkLogin 메서드는 로그인 정보가 없으면 로그인 페이지로 보낸다.


logout 메서드는 세션과 쿠키를 무효화하여 로그인 상태를 초기화한다.


getLoginInformation, fillLoginInfoFromCookies, fillLoginInfoFromSession 메서드는 로그인 정보를 쿠키와 세션에서 가져오는 역할을 한다.


synchronizeLoginInformation 메서드는 쿠키와 세션 간의 로그인 정보를 동기화한다.

 

 

 

LoginAspect 클래스

이 클래스는 특정 메서드 호출 전에 로그인 여부를 확인하는 횡단 관심사를 구현한다.

package com.coma.app.view.member;

import com.coma.app.view.annotation.LoginCheckImpl;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@Aspect
@Component
public class LoginAspect {

    private LoginCheckImpl loginCheckImpl;

    @Autowired
    public LoginAspect(LoginCheckImpl loginCheckImpl) {
        this.loginCheckImpl = loginCheckImpl;
    }


    @Around("@annotation(com.coma.app.view.annotation.LoginCheck)")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        // 현재 요청과 응답 객체를 가져옴
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
        // 세션 객체를 가져옴
        HttpSession session = request.getSession();

        // 로그인 체크 로직 실행
        loginCheckImpl.checkLogin(request, response, session);

        // 로그인된 경우 원래 메서드를 실행
        return joinPoint.proceed();
    }
}

 

@Aspect 어노테이션은 이 클래스가 하나 이상의 Advice를 포함하는 Aspect임을 나타낸다.
이 어노테이션을 통해 Spring은 이 클래스를 AOP 지원 클래스로 인식한다.

 

로그인 체크는 대상 메서드가 실행되기 전에 반드시 수행되어야 한다. 로그인이 되어 있지 않다면, 메서드를 실행하지 않고 로그인 페이지로 리다이렉트해야 한다. 이러한 요구사항을 만족시키기 위해 @Around 어드바이스를 사용하는 것이다.

 

로그인 된 경우: joinPoint.proceed()를 호출하여, 대상 메서드를 계속 실행
로그인 되지 않은 경우: 로그인 페이지로 리다이렉트

 

의존성 주입

→ LoginCheckImpl 빈을 주입받는다. 이를 통해 로그인 검사 로직을 분리하여 재사용한다.
주입되는 LoginCheckImpl 클래스는 로그인 여부를 검사하는 실제 로직을 포함한다.

<!-- LoginCheckImpl 빈 등록  -->
   <bean id="loginCheckImpl" class="com.coma.app.view.annotation.LoginCheckImpl"/>

   <!-- LoginAspect 빈 등록  -->
   <bean id="loginAspect" class="com.coma.app.view.member.LoginAspect">
      <constructor-arg ref="loginCheckImpl"/>
   </bean>

 

 

LoginAspect 클래스는 AOP를 활용하여 로그인 체크 로직을 특정 메서드 실행 전후에 적용한다.

 

@Aspect 어노테이션을 통해 해당 클래스가 AOP 기능을 수행하는 것을 명시한다.


@Around 어노테이션은 LoginCheck 어노테이션이 달린 메서드를 감싸서 실행 전후에 횡단 로직을 적용할 수 있게 해준다.


checkLogin 메서드는 로그인 체크 로직을 수행한 후 기존 메서드를 실행한다.


이 클래스는 로그인 체크 로직을 비즈니스 로직에서 분리하여 코드의 가독성과 유지보수성을 높이는 역할을 한다.


AOP와 Spring 통합

위 예시에서 LoginAspect 클래스는 LoginCheckImpl 클래스를 통해 로그인 체크 로직을 실행한다.

이때 @Aspect와 @Around 어노테이션을 사용하여 특정 어노테이션이 달린 메서드 호출 전후에 로그인 체크를 수행한다.

 

 

 

이러한 접근 방식은 다음과 같은 장점을 제공한다.
→ 비침투적인 설계: 비즈니스 로직이 로그인 체크와 같은 횡단 로직으로 인해 복잡해지지 않는다.
→  유지보수 용이: 횡단 로직을 별도의 클래스로 분리하여 관리할 수 있다.
→  재사용성: LoginCheck 로직을 여러 군데에서 사용할 수 있다.

 


AOP는 횡단 로직을 다루는 데 매우 유용한 도구이다.

위 예시에서 로그인 체크를 AOP로 구현함으로써 코드의 가독성과 유지보수성을 향상시킬 수 있었다.

이와 같은 방식으로 다른 횡단 로직으로도 (예 : 트랜잭션 관리) AOP를 적용할 수 있다.

 

 

'Spring' 카테고리의 다른 글

[Spring] AOP  (0) 2024.11.10
[Spring] 롬복(Lombok) 라이브러리  (0) 2024.10.30
[Spring] 쿠키 활용하기 - 자동 로그인  (0) 2024.10.25
[Spring] Multipart & 파일 업로드  (1) 2024.10.23
[Spring] 커스텀 어노테이션  (1) 2024.10.23
'Spring' 카테고리의 다른 글
  • [Spring] AOP
  • [Spring] 롬복(Lombok) 라이브러리
  • [Spring] 쿠키 활용하기 - 자동 로그인
  • [Spring] Multipart & 파일 업로드
yn98
yn98
좌우명 : 여전할 것 인가, 역전할 것 인가? 백엔드 개발자가 되고싶은 역전하고 있는 개발자 꿈나무의 블로그입니다. 개발을 하면서 공부한 것들을 기록합니다. 24.06 ~
  • yn98
    개발 꿈나무
    yn98
  • 전체
    오늘
    어제
    • 분류 전체보기 (131)
      • Python (3)
      • 공부 (7)
      • DB (7)
      • JAVA (24)
      • JSP (9)
      • jQuery (2)
      • HTML (3)
      • Spring (20)
      • 웹 (4)
      • C (1)
      • Git (2)
      • 에러일기 (19)
      • 프로젝트 (6)
      • 책 (21)
        • 멘토씨리즈 자바 (14)
        • 2024 수제비 정보처리기사 (7)
      • 기타 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
    • Notion
  • 공지사항

  • 인기 글

  • 태그

    정처기 실기
    티스토리챌린지
    MVC
    aop
    어노테이션
    스프링 프레임워크
    정처기
    codeup 4891 : 행복
    @repository
    Di
    수제비
    recoverabledataaccessexception
    @Component
    Spring
    ViewResolver
    멘토씨리즈 자바
    java
    2-layered 아키텍처
    정보처리기사 실기
    생성자
    정보처리기사
    오블완
    @service
    객체지향
    DispatcherServlet
    이벤트 스케줄러
    jsp
    오버로딩
    html
    상속
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
yn98
[Spring] 횡단 로직 - AOP를 활용한 로그인 체크 기능 구현하기
상단으로

티스토리툴바