본문 바로가기
공부/프로그래밍

[springboot, aop] 반복적인 작업은 이제 그만, AOP로 해결하기

by demonic_ 2020. 6. 17.
반응형

특정 정보의 코드나 key를 통해 value값을 로드해야 할때, 매번 코드마다 똑같은 코드를 복사하는 것도 꽤나 힘든 작업이다. 스프링을 사용하면 AOP를 이용해 한번에 해결이 가능하다.

실제로 최근 WMS 관련 프로젝트를 했는데, 상품바코드 조회하는 곳이 많았다. 이 바코드가 유효한지 여부를 수시로 검토해줘야 했었다. 그래서 메인로직으로 들어가기 전에 해당 바코드가 유효한지, 유효하지 않다면 비슷한 유형의 바코드가 있는건 아닌지 점검하는 로직을 넣어주었다. 덕분에 서비스마다 호출해야 했던 것을 어노테이션만 붙여서 해결 가능했다.

이번 예제는 barcode를 통해 상품정보를 자동으로 조회하도록 했다.

AOP를 사용하지 않는다면 매 서비스마다 유효성을 체크하는 메서드를 호출하는 방식으로 개발할 것이다.

우선 상품정보가 있는 클래스를 먼저 생성

public class ItemManager {
    public static Map<String, String> items = Map.ofEntries(
            Map.entry("E01010101", "새우깡"),
            Map.entry("E01010102", "감자칩"),
            Map.entry("E01010201", "아메리카노"),
            Map.entry("E01010202", "카페라떼"),
            Map.entry("E01010203", "카페모카")
    );

    public enum ItemCodes {
        E01010101, E01010102, E01010201, E01010202, E01010203
    }
}

그리고 비지니스 로직이 있는 서비스에 바코드를 받아 상품조회하는 것을 추가한다

@Service
public class BarcodeManagerService {
    /**
     * 비지니스 로직
     */
    public String itemInfoByBarcode(String barcode) {
        // 바코드가 유효한지 체크해야 함
        String itemName = CheckBarcodeUtil.checkAndGetByBarcode(barcode);
        System.out.println("상품명: " + itemName);

        return itemName;
    }
}

바코드의 유효성을 체크하는 클래스

public class CheckBarcodeUtil {
    /**
     * 바코드 유효성 체크 및 상품이름 리턴
     */
    public static String checkAndGetByBarcode(String barcode) {
        if(ItemManager.items.containsKey(barcode) == false) {
            throw new IllegalArgumentException("잘못된 바코드 입니다");
        }
        return ItemManager.items.get(barcode);
    }
}

 

바코드 유효성 및 상품정보를 조회하기 위해선 매 서비스마다 다음의 한줄을 추가해야 한다.

CheckBarcodeUtil.checkAndGetByBarcode

 

하지만 AOP를 사용한다면 이 부분을 생략할 수 있다. 또한 코드에 일관성을 유지할 수 있어 좋다.

 

그럼 AOP를 인터페이스와 연결, 등록하는 부분을 알아보자.

 

 

# AOP 작성하기

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckBarcode {
}

 

CheckBarcode 라는 인터페이스를 생성했다. 해당 인터페이스가 등록되어 있는 메서드는 모두 바코드를 체크하게 한다.

Target을 Method로 지정했기 때문에 메서드실행시 적용된다. 그 외 클래스가 생성될때(Constructor)나 파라미터 등에도 적용할 수 있다.

 

다음은 이 인터페이스를 구현할 클래스를 작성한다.

@Component
@Aspect
public class CheckBarcodeAOP {

    @Before("@annotation(CheckBarcode)")
    public void checkBarcode(JoinPoint joinPoint) {
        CheckBarcodeDto checkBarcodeDto = Arrays.stream(joinPoint.getArgs())
                .filter(CheckBarcodeDto.class::isInstance)
                .map(CheckBarcodeDto.class::cast)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("잘못된 파라미터 입니다(CheckBarcodeDto 가 없습니다)"));


        invalidBarcode(checkBarcodeDto);
    }

    // 바코드 체크 & 이름 리턴
    private void invalidBarcode(CheckBarcodeDto checkBarcodeDto) {
        if(ItemManager.items.containsKey(checkBarcodeDto.getBarcode()) == false) {
            throw new IllegalArgumentException("잘못된 바코드 입니다");
        }

        checkBarcodeDto.setItemName(ItemManager.items.get(checkBarcodeDto.getBarcode()));
    }
}

@Before 를 쓴 이유는 비지니스 메서드에 접근하기 전에 수행해야 한다는 것을 의미한다. 그 밖에 @After와 @Around가 있는데 @After는 메서드 실행 후에, @Around는 메서드 실행 전, 후 모두 실행한다.

 

JoinPoint를 메서드를 받는데, 저기서 파라미터가 무엇이 담겨있는지를 확인할 수 있다.

JoinPoint란?

메서드가 호출하는 시점, 예외가 발생한 시점 등 작업이 실행되는 시점을 의미.

joinPoint 내부를 보면 다음처럼 등록되어 있음을 확인할 수 있다.

 

우리가 검사해야 할 것은 바코드 인데, 문제는 JoinPoint만으론 파라미터명을 찾을 수 없다.

대신 파라미터에 들어있는 클래스 객체 유형은 알 수 있다. 그래서 String으로 넘기는 대신 CheckBarcodDto라는 클래스를 생성해서 넘기도록 한다.

 

바코드가 유효한지 여부를 invalidBarcode에서 수행한다. 그리고 수행이 완료되면 상품이름을 CheckBarcodeDto에 담는다.

 

CheckBarcodeDto 를 만든다.

import lombok.Getter;
import lombok.ToString;

@ToString
@Getter
public class CheckBarcodeDto {
    private String barcode;

    private String itemName;

    public CheckBarcodeDto(String barcode) {
        this.barcode = barcode;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }
}

 

그럼 다시 BarcodeManagerService 로 돌아가보자

이번엔 @CheckBarcode 어노테이션을 이용해 체크해보도록 한다.

@Service
public class BarcodeManagerService {

    /**
     * 비지니스 로직
     */
    public String itemInfoByBarcode(String barcode) {
        String itemName = CheckBarcodeUtil.checkAndGetByBarcode(barcode);
        System.out.println("상품명: " + itemName);

        return itemName;
    }


    /* AOP 적용 */
    @CheckBarcode
    public String itemInfoByBarcode(CheckBarcodeDto checkBarcodeDto) {
        System.out.println("상품명: " + checkBarcodeDto.getItemName());

        return checkBarcodeDto.getItemName();
    }
}

 

 

@CheckBarcode 내에선 CheckBarcodeUtil.checkAndGetByBarcode 가 없다. CheckBarcodeDto 파라미터가 넘어오면서 이미 체크 & 상품이름을 전달하기 때문이다.

 

그럼 테스트를 작성해보겠다.

 

 

# AOP 테스트

테스트는 총 2개로 작성한다.

하나는 바코드를 잘못 넣어을 때, 그리고 하나는 정상을 넣을때다.

@SpringBootTest
class BarcodeManagerServiceTest {

    @Autowired
    BarcodeManagerService barcodeManagerService;


    @Test
    @DisplayName("바코드 조회 중 없는 바코드 테스트")
    void itemInfoErrorByBarcodeTest() {
        // given
        CheckBarcodeDto checkBarcodeDto = new CheckBarcodeDto("TEST");

        // when
        IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> {
            barcodeManagerService.itemInfoByBarcode(checkBarcodeDto);
        });

        // then
        Assertions.assertTrue(ex.getMessage().contains("잘못된 바코드 입니다"));
    }

    @Test
    @DisplayName("바코드를 통해 상품조회")
    void itemInfoByBarcodeTest() {
        // given
        String checkItemName = ItemManager.items.get(ItemManager.ItemCodes.E01010101.name());
        CheckBarcodeDto checkBarcodeDto = new CheckBarcodeDto(ItemManager.ItemCodes.E01010101.name());

        // when
        String itemName = barcodeManagerService.itemInfoByBarcode(checkBarcodeDto);

        // then
        Assertions.assertEquals(checkItemName, itemName);
    }
}

 

바코드가 틀릴경우 IllegalArgumentException 을 발생하는 것을 확인할 수 있다.

 

바코드를 enum으로 한 이유는 코드를 안전하게 관리하기 위함이지 현장에서는 저렇게 쓰이지 않을거다.

무시해도 된다.

 

둘다 테스트 정상 종료.

 

 

끝.

 

 

 

반응형

댓글