Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

유비무환

로그인 검증 로직에 AOP를 적용해서 중복을 제거해보자 본문

IT/개인 프로젝트

로그인 검증 로직에 AOP를 적용해서 중복을 제거해보자

키다리병장 2020. 6. 10. 13:42

개인 프로젝트를 진행하면서 서비스 이용을 위해서 로그인 기능을 추가하게 되었습니다. 주로 비즈니스 로직에 로그인 기능을 사용하게 되는데 예를 들면 '물건을 장바구니에 넣는다'는 행위가 있겠네요. 물론 비회원으로 서비스를 제공하는 사이트도 있겠지만 여기서는 논외로 하겠습니다.

 

로그인 기능은 고객뿐만 아니라 관리자도 자주 그리고 반복해서 사용하게 되는 기능입니다. 그렇다면 이렇게 중복해서 사용되는 기능을 어떻게 제거할 수 있을까요? 지금부터 단계별로 알아보도록 하겠습니다.


기본 로직

>> CartController.java(리펙토링 전)

@PostMapping
public HttpStatus create(@RequestBody CartItem cartItem, HttpSession session) {
    String loginUserEmail = HttpSessionUtil.getLoginUserEmail(session);
    
    // 중복!!
    if (loginUserEmail == null) {
        throw new IllegalStateException("로그인(유저)이 필요합니다.");
    }

    cartService.registerItem(cartItem, loginUserEmail);

    return HttpStatus.CREATED;
}

@GetMapping
public List<Object> list(HttpSession session) {
    String loginUserEmail = HttpSessionUtil.getLoginUserEmail(session);

    // 중복!!
    if (loginUserEmail == null) {
        throw new IllegalStateException("로그인(유저)이 필요합니다.");
    }
    
    return cartService.getList(loginUserEmail);
}

위에 코드는 장바구니 기능을 만들 때 사용한 CartController.java입니다. 장바구니에 상품을 넣는 메서드와 장바구니 목록을 조회하는 메서드가 존재합니다. 장바구니 기능을 사용하기 위해선 회원정보가 필요하기 때문에 두 메서드에는 세션을 통해 로그인 이메일을 가져와서 검증하는 로직이 동일하게 들어가 있습니다.

 

if (loginUserEmail == null) {
    throw new IllegalStateException("로그인(유저)이 필요합니다.");
}

다음과 같이 로그인 검증 로직이 중복해서 나타나는데 어떻게 분리하면 좋을까요? 아마 많은 분들이 메서드를 만들어서 분리한다는 생각을 떠올리실 텐데요. 그럼 메서드를 통해 분리해봅시다.


메서드를 만들어서 로그인 검증 로직을 분리하자

>> CartController.java(1차 리펙토링)

@PostMapping
public HttpStatus create(@RequestBody CartItem cartItem, HttpSession session) {
    String loginUserEmail = HttpSessionUtil.getLoginUserEmail(session);
    
    // 중복!!
    loginVerify(loginUserEmail);

    cartService.registerItem(cartItem, loginUserEmail);

    return HttpStatus.CREATED;
}

@GetMapping
public List<Object> list(HttpSession session) {
    String loginUserEmail = HttpSessionUtil.getLoginUserEmail(session);

    // 중복!!
    loginVerify(loginUserEmail);
    
    return cartService.getList(loginUserEmail);
}

private void loginVerify(String email) {
    if(email == null) {
        throw new IllegalStateException("로그인(유저)이 필요합니다.");
    }
}

자 기존에 있던 로그인 검증 로직을 loginVerify()라는 메서드로 분리해봤습니다. 어떤가요 코드가 좀 깔끔해진 것 같나요? 핵심 로직은 한결 보기 좋아진 것 같습니다. 만약 더욱 복잡한 로직이었다면 변화가 더 뚜렷했을 것 같네요.

 

하지만 아쉬운 점이 보이는데요. 이 글의 제목이 분명 '중복을 제거해보자' 였는데 지금 한 건 '제거'가 아니라 '분리'에 가깝습니다. 이것은 메서드로 검증 로직을 분리해냈더라도 핵심 로직 안에는 검증 메서드를 호출하는 부가 기능이 남아있기 때문입니다. 이렇게 부가 기능이 남아있으면 핵심 기능에 집중하기 어렵고 실수가 발생할 확률이 높아질 수 있습니다.

 

이제 핵심 로직에서 부가 기능을 완전히 제거해봅시다.


핵심 로직에서 로그인 검증 로직을 제거하자

AOP: 애스펙트 지향 프로그래밍
애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법 (토비의 스프링 3.1)

인용구의 설명처럼 AOP를 적용해서 핵심 기능에서 부가 기능을 제거해볼 겁니다. 여기서 핵심 기능은 '장바구니 기능'이고 부가 기능은 '로그인 검증 기능'이 되겠네요.

 

implementation 'org.springframework.boot:spring-boot-starter-aop'

먼저 AOP를 사용하기 위해서 의존성을 추가해줍니다. 참고로 저는 gradle에서 작업을 했고 만약 maven에서 작업하신다면 pom.xml 파일에서 의존성을 추가해줄 수 있습니다.

 

@EnableAspectJAutoProxy	// AOP 사용!!
@SpringBootApplication
public class FoodRocketApplication {

	public static void main(String[] args) {
		SpringApplication.run(FoodRocketApplication.class, args);
	}

}

의존성을 추가하고 최상위 클래스에 AOP 사용을 명시하는 @EnableAspectJAutoProxy 어노테이션을 추가해줍니다. 자 이제 AOP를 사용할 수 있는 환경이 갖춰졌으니 AOP 로직을 작성해보도록 합시다.

 

>> LoginCheckAspect.java

@Aspect
@Component
public class LoginCheckAspect {

    @Before("@annotation(com.hoon.foodrocket.aop.LoginType) && @annotation(loginType)")
    public void loginCheck(LoginType loginType) {
        HttpSession session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();

        switch (loginType.level()) {
            case USER:
                verifyUserSession(session);
                break;
            case OWNER:
                verifyOwnerSession(session);
                break;
        }
    }

    private void verifyUserSession(HttpSession session) {
        String loginUserEmail = HttpSessionUtil.getLoginUserEmail(session);

        if (loginUserEmail == null) {
            throw new IllegalStateException("로그인(유저)이 필요합니다.");
        }
    }

    private void verifyOwnerSession(HttpSession session) {
        // ...
    }

}

 

>> LoginType

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginType {
    AccountPermissionLevel level();  // USER, OWNER
}

LoginCheckAspect에서 HttpSession 값을 꺼내고 loginType에 따라 각각의 로그인 검증 로직을 실행시킵니다. 만약 세션에 값이 없다면 IllegalStateException 예외를 던져줍니다.

 

>> AOP를 적용한 CartController.java(최종 리펙토링)

@LoginType(level = USER)
@PostMapping
public HttpStatus create(@RequestBody CartItem cartItem, HttpSession session) {
    String loginUserEmail = HttpSessionUtil.getLoginUserEmail(session);

    cartService.registerItem(cartItem, loginUserEmail);

    return HttpStatus.CREATED;
}

@LoginType(level = USER)
@GetMapping
public List<Object> list(HttpSession session) {
    String loginUserEmail = HttpSessionUtil.getLoginUserEmail(session);

    return cartService.getList(loginUserEmail);
}

AOP를 적용하고 나서 기존에 있던 로그인 검증 로직이 깔끔하게 제거된 것을 확인할 수 있습니다. 


마무리

지금까지 중복된 코드를 어떻게 제거했는지 예시를 통해 알아봤습니다. 해당 글에서는 간단한 로직과 적은 예시를 가지고 진행했기에 이게 무슨 소용이냐고 생각하는 분들도 계실 거라 생각합니다. 하지만 프로젝트가 커질수록 분명 중복되는 로직이 생기게 될 겁니다. 처음에는 한두 번 정도겠지만 추후에 100번, 1000번 반복된다면 AOP의 효과가 극대화될 겁니다. 만약 중복되는 로직이 있다면 AOP를 적용해보시기 바랍니다.

 

참고

토비의 스프링 3.1

Comments