Web/Spring 2019. 10. 12. 12:49

 

이번에 다루어볼 포스팅은 커스텀 어노테이션과 인터셉터를 이용하여 특정 컨트롤러의 매핑 메서드에 전처리(인증 등)를 하는 예제이다. 즉, 커스텀하게 어노테이션을 정의하고 컨트롤러 혹은 컨트롤러 메서드에 어노테이션을 붙여(마치 @RequestMapping과 같은) 해당 컨트롤러 진입전에 전처리(인증)을 하는 예제이다. 물론, Allow Ip 같은 것은 필터에서 처리하는 것이 더 맞을 수 있지만, 어떠한 API는 인증이 필요하고 어떠한 것은 필요없고 등의 인증,인가 처리는 인터셉터가 더 적당한 것 같다. 이번 포스팅을 이해하기 위해서는 Spring MVC의 구동 방식을 개념정도는 알고 하는 것이 좋을 듯하다. 아래 포스팅을 확인하자.

 

 

spring의 다양한 intercept 방법 (Servlet Filter, HandlerInterceptor, PreHandle, AOP)

1. 다양한 intercept 방법들과 주 사용처 Servlet Filter : 인코딩, 인증, 압축, 변환 등 HandlerInterceptor : 세션, 쿠키, 검증 등 AOP : 비즈니스단 로깅, 트랜잭션, 에러처리 등 2. Servlet Filter 와 Handler..

sjh836.tistory.com

 

개발 방향은 컨트롤러에 인증을 뜻하는 어노테이션을 커스텀하게 붙이고, 인터셉터에서 어노테이션의 유무를 판단하여 인증을 시도한다. 특별히 어려운 코드는 없어 간략한 설명만 하고 넘어가겠다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public enum Auth {
 
    NONE,AUTH
 
}
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsAuth {
 
    Auth isAuth() default Auth.NONE;
 
}
 
@Slf4j
@Component
public class CommonInterceptor implements HandlerInterceptor {
 
    private Map<String,User> userMap = new HashMap<>();
 
    @PostConstruct
    public void setup() {
        User user = new User("","yeoseong_gae",28);
        userMap.put("yeoseong-gae",user);
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("interceptor");
        String userId = request.getParameter("userId");
 
        IsAuth annotation = getAnnotation((HandlerMethod)handler, IsAuth.class);
 
        Auth auth = null;
 
        if(!ObjectUtils.isEmpty(annotation)){
            auth = annotation.isAuth();
            //NONE이면 PASS
            if(auth == Auth.AUTH){
                if(ObjectUtils.isEmpty(userMap.get(userId))){
                    log.info("auth fail");
                    throw new AuthenticationException("유효한 사용자가 아닙니다.");
                }
            }
        }
        return true;
    }
 
    private <extends Annotation> A getAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
        return Optional.ofNullable(handlerMethod.getMethodAnnotation(annotationType))
                .orElse(handlerMethod.getBeanType().getAnnotation(annotationType));
    }
 
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User{
        private String id;
        private String name;
        private int age;
    }
}
 
@Slf4j
@RestControllerAdvice
public class CtrlAdvice {
 
    @ExceptionHandler(value = {Exception.class})
    protected ResponseEntity<String> example(Exception exception,
                                             Object body,
                                             WebRequest request) throws JsonProcessingException {
        log.debug("RestCtrlAdvice");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("message:"+exception.getMessage());
    }
 
}
 
@RestController
public class AuthController {
 
    @IsAuth(isAuth = Auth.AUTH)
    @GetMapping("/auth")
    public boolean acceptUser(@RequestParam String userId){
        return true;
    }
 
    @IsAuth
    @GetMapping("/nonauth")
    public boolean acceptAll(@RequestParam String userId){
        return true;
    }
}
cs

 

마지막으로 인터셉터를 등록하기 위한 Config 클래스이다.

 

1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
 
    private final CommonInterceptor commonInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(commonInterceptor);
    }
}
cs

 

커스텀한 어노테이션을 정의하고, 컨트롤러 매핑 메서드중 인증을 하고 싶은 곳에 어노테이션을 등록해준다. 그리고 인터셉터에서 현재 찾은 핸들러의 핸들러메서드에 우리가 등록한 커스텀한 어노테이션이 있는지 혹은 해당 핸들러메서드의 클래스에 붙어있는지 등을 리플렉션을 이용하여 확인한다. 만약 있다면 유효한 사용자인지 판단한다. 참고로 인터셉터에서는 디스패처 서블릿 더 뒷단에서 구동하기 때문에 현재 매핑된 핸들러 메서드의 접근이 가능하다.

 

마지막으로 인증이 실패한다면 예외를 발생시키고, 컨트롤러 어드바이스를 이용하여 예외를 처리한다. 참고로 인터셉터는 앞에서 말한 것과 같이 디스패처 서블릿 뒷단에서 구동되기때문에 ControllerAdvice 사용이 가능하다.

 

현재 다루고 있는 예제는 하나의 예시일 뿐이다. 사실 스프링 시큐리티를 사용한다면 굳이 필요없는 예제일수 있다. 하지만 인증뿐아니라 여러가지로 사용될수 있기에 응용하여 사용하면 될듯하다. 해당 코드는 절대 실무에선 사용될 수없는 로직을 갖고 있다.(예제를 위해 간단히 작성한 코드이기에) 컨셉을 갖고 더 완벽한 코드로 완성해야 한다. 만약 해당 코드가 필요하다면 밑의 깃헙에 소스를 올려놓았다.

 

 

 

yoonyeoseong/useAnnotationAndInterceptorAuth

Contribute to yoonyeoseong/useAnnotationAndInterceptorAuth development by creating an account on GitHub.

github.com

 

posted by 여성게
:
Web/Spring 2019. 10. 10. 00:22

 

오늘 포스팅할 내용은 간단하게 Springboot Test이다. 아주 자세하게 다루어보진 않지만, 유용하고 자주 사용하는 부분이다. 간단하게 유닛테스트하는 내용은 이전 포스팅을 참고하고, 이번 내용은 Springboot 환경에서의 테스트 내용을 다룰것이다.

 

TDD - 테스트 주도 개발(Test Driven Development)

작성하려는 코드가 있다면 항상 먼저 어떻게 그 코드를 테스트할지 고민해야한다. 코드를 작성한 후에 어떻게 테스트할지 고민하기보다 작성할 코드를 묘사하는 테스트를 설계해야 한다. 이것이 테스트 주도 개발..

coding-start.tistory.com

 

다음 코드는 Springboot에서 빈으로 등록된 객체들을 사용할수 있게하는 테스트이다. 즉, ApplicationContext를 띄우는 것이다.

 

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
32
33
34
35
36
============application.properties============
test.property=test-property
==============================================
@Service
public class CommonService {
    @Autowired
    private CommonBean commonBean;
 
    public String print(){
        String result = commonBean.print();
        System.out.println(result);
        return result;
    }
}
@Component
public class CommonBean {
    @Value("${test.property}")
    private String testProperty;
    public String print(){
        System.out.println(testProperty);
        return testProperty;
    }
}
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestPropertyTest {
    @MockBean
    private CommonBean commonBean;
    @Autowired
    private CommonService commonService;
    @Test
    public void print(){
        commonBean.print();
    }
}
 
cs

 

위 테스트는 성공적으로 통과할 것이다. 하지만 여기서 하나 문제점이 존재한다. 만약 아주 큰 애플리케이션이라 ApplicationContext 로드에 아주 긴 시간이 걸린다면 작은 테스트를 하나 수행할때마다, 긴 시간이 걸릴 것이다. 이러할때, 우리가 테스트에 필요한 빈만 띄울수 있다. 아래 코드를 확인하자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {
        CommonService.class,
        CommonBean.class
},initializers = ConfigFileApplicationContextInitializer.class)
public class TestPropertyTest {
 
    @Autowired
    private CommonService commonService;
 
    @Test
    public void print(){
        commonService.print();
    }
 
}
cs

 

2가지 방법이 존재한다. 첫번째 방법은 @ContextConfiguration 어노테이션을 이용하는 방법이다. 인자로 빈으로 띄울 클래스를 넘긴다. 여기서 중요한것을 관련된 클래스도 모두 넣어줘야한다는 것이다. 위를 예를 들면 CommonService 클래스를 빈으로 등록할 것인데, 해당 클래스의 내용을 보면 CommonBean을 DI받고 있으므로, CommonBean도 빈으로 띄워줘야하는 것이다. 나중에 다루어볼 것이지만 위와같이 빈으로 띄울 클래스를 명시적으로 등록해도되지만, 굳이 테스트에 사용하지 않으면 Mock 빈으로 등록해주어도 된다. 두번째 인자는 initializers이다. 만약 이 인자가 없다면, CommonBean에서 @Value로 DI 받고있는 프로퍼티값을 주입받지 못한다. 즉, application.properties의 내용을 읽지 못하는 것이다. 내부적으로 @Value를 받기위한 빈이 뜨지 못해서 그런 것 같다. 이와 같은 문제를 해결하기 위해서 initializers = ConfigFileApplicationContextInitializer.class를 등록해주므로써 프로퍼티값을 읽어 올 수 있게 하였다. 

 

두번째 방법은 아래와 같다. @SpringBootTest를 이용하여 더 간단한 설정으로 SpringBoot Test를 위한 설정을 할 수 있게된다. 또한 테스트에만 사용할 프로퍼티를 정의할 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {CommonBean.class,CommonService.class}
,properties = {"test.property=dev-property"})
public class TestPropertyTest {
 
    @Autowired
    private CommonService commonService;
 
    @Test
    public void print(){
        commonService.print();
    }
 
}
cs

 

두번째는 Mock빈을 이용하는 방법이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {
        CommonService.class
},initializers = ConfigFileApplicationContextInitializer.class)
public class TestPropertyTest {
 
    @MockBean
    private CommonBean commonBean;
    @Autowired
    private CommonService commonService;
 
    @Test
    public void mockBeanTest(){
        given(commonBean.print()).willReturn("Test Result");
        commonService.print();
    }
 
}
cs

 

CommonService의 비즈니스 로직을 테스트하고 싶은데, 굳이 CommonBean은 띄울 필요가 없을때, 위와 같이 MockBean을 등록해서 임의로 Mock 빈이 리턴할 값을 명시할 수 있다. 위 소스는 CommonService만 테스트를 위한 빈으로 띄우는데, 내부적으로 관련있는 CommonBean은 띄우지 않고 Mock 빈으로 띄운다. 그리고 CommonBean이 리턴하는 값을 임의로 "Test Result"라는 값으로 지정해주었다. 이렇게 테스트할 빈만 집중하여 테스트가 가능해지며 사이드 코드의 양을 줄일 수 있게 된다.

 

다음은 간단하게 Controller를 테스트할 수 있는 코드이다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RunWith(SpringRunner.class)
@WebMvcTest(controllers = CommonTestController.class)
@ContextConfiguration(classes = {
        CommonTestController.class,
        CommonBean.class,
        CommonService.class}
        ,initializers = ConfigFileApplicationContextInitializer.class)
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    public void resultTest() throws Exception {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

굳이 스프링을 실행시키지 않고도 컨트롤러를 테스트할 수 있다. @WebMvcTest 어노테이션을 이용하여 스프링 컨트롤러 테스트가 가능한 설정을 띄워준다. 즉, MVC관련 빈만 띄워 더욱 가볍게 스프링 컨트롤러 테스트가 가능하다. 위와 같은 어노테이션도 가능하며 아래와 같은 어노테이션 설정도 사용가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(classes = {
        CommonTestController.class,
        CommonBean.class,
        CommonService.class
})
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    public void resultTest() throws Exception {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

@AutoConfigureMockMvc 어노테이션을 이용하여 MVC 테스트를 위한 설정을 AutoConfiguration하게 할수 있다. 물론, MVC테스트를 위하여 내부적으로 내/외부 연동코드가 있다면 내/외부연동을 하지 않기 위해 Mock 빈등을 이용하여 테스트 가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(classes = {
        CommonTestController.class
})
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private CommonBean commonBean;
    @MockBean
    private CommonService commonService;
 
    @Test
    public void resultTest() throws Exception {
        given(commonBean.print()).willReturn("test");
        given(commonService.print()).willReturn("test");
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

마지막으로 테스트만을 위한 application.properties를 사용는 방법이다. 우선, application-test.properties를 classpath 밑의 resources 폴더 안에 넣어준다.(기존 application.properties 경로와 동일) 그리고 아래와 같이 @ActiveProfile 어노테이션을 이용하여 프로파일을 이용하여 읽어올 프로퍼티파일을 명시한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(classes = {
        CommonTestController.class,
        CommonBean.class,
        CommonService.class
})
@ActiveProfiles("test")
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    public void resultTest() throws Exception {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

위 코드는 기존 application.properties가 아닌, application-test.properties 파일을 읽어 컨텍스트를 구성할 것이다.

 

아주 간단하게만 springboot test를 다루어봤는데, 추후 포스팅에서 지금까지 다룬 내용과 더 자세한 테스팅관련 기술을 다루어볼 것이다.

posted by 여성게
:
Web/Spring 2019. 9. 11. 23:54

 

이번에 다루어 볼 포스팅은 Spring Cache이다. 스프링 3.1부터 빈의 메서드에 캐시 서비스를 적용할 수 있는 기능을 제공한다. 캐시 서비스는 트랜잭션(@Transaction)과 마찬가지로 AOP를 이용해 메서드 실행 과정에 우리가 모르도록 투명하게 적용된다. 또한 스프링은 장점으로 캐시 서비스 구현 기술에 종속되지 않도록 추상화된 서비스를 제공해주므로 환경이 바뀌거나 구현 기술이 바뀌어도 우리가 적용한 코드의 변경 없이 구현 기술을 바꿔줄 수 있다.

 

그렇다면 여기서 짚고 넘어갈 것이, "애플리케이션 빈의 메서드에 캐시를 적용하는 이유 혹은 목적"은 무엇일까?

 

캐시는 기본적으로 성능의 향상을 위해 사용한다. 여기서 성능 향상은 애플리케이션이 갑자기 막 빨라지고 하는 성능 향상이 아니다. 어떤 요청을 처리하는데 연산이 아주 복잡하거나 데이터베이스와 같은 백엔드 시스템에 많은 부하를 주거나, 외부 API 호출처럼 시간이 오래 걸린다면 캐시의 사용을 고려해볼 만하다. 하지만 결코 앞과 같은 상황에 직면했다고 캐시 기능을 사용하면 안된다.

 

캐시(Cache)는 임시 저장소라는 뜻이다. 복잡한 계산이나 데이터베이스 작업, 외부 API 요청의 처리 결과 등을 임시 저장소인 캐시에 저장해뒀다가 동일한 요청이 들어오면 복잡한 작업을 수행해서 결과를 만드는 대신 이미 캐시에 보관된 결과를 바로 돌려주는 방식이다. 만약 이런 상황이 있다고 생각하자.

 

사용자가 많은 웹사이트 서비스가 있다. 메인 페이지에 접속하면 항상 공지사항이 출력돼야 한다. 공지사항은 어떠한 추가적인 내용이 추가되거나 혹은 내용이 수정되기 전까지는 모든 사용자에게 동일한 내용을 보여준다. 그렇다면 사용자가 만명이 있고 사용자가 들어올때 마다 공지사항을 데이터베이스에 접근하여 보여준다면 총 만번의 데이터베이스 액세스가 이루어질 것이다. 사실 데이터베이스에 자주 접근하는 연산은 비용이 비싸다. 그만큼 데이터베이스에 부하가 간다는 뜻이다. 만약 이럴때 캐시를 적용한다면 첫 사용자가 접근할때만 데이터베이스에서 공지사항 내용을 가져오고 나머지 9999명은 캐시에 저장된 내용을 그대로 돌려준다면 만명의 사용자가 한번의 데이터베이스 액세스로 공지사항을 볼 수 있게 되는 것이다.

 

이렇게 캐시는 여러가지 장점을 가졌지만, 그렇다면 모든 상황에서 캐시를 사용하면 안된다. 

 

"값비싼 비용이 들어가는 요청, 데이터베이스 접근, 외부 요청 등에 캐시를 사용하지만 여기서 전제가 있다. 캐시는 반복적으로 동일한 결과를 주는 작업에만 이용해야 한다. 매번 다른 결과를 돌려줘야 하는 작업에는 캐시를 적용해봐야 오히려 성능이 떨어진다."

 

그렇다면 캐시를 사용하면 아주 주의깊게 체크해야 할 것이 있다.  바로, 캐시에 저장해둔 컨텐츠의 내용이 바뀌는 상황 혹은 시점을 잘 파악하는 것이다. 공지사항이 추가되거나 수정되었다면 캐시는 아직까지 이전 내용의 공지사항을 가지고 있을 것이므로, 이러한 상황에서는 캐시에 저장된 내용을 삭제하고 첫 사용자가 접근하여 캐시의 내용을 최신 내용으로 업데이트 해야한다.

 

이제 직접 코드를 보며 스프링에서 사용할 수 있는 캐시 서비스를 알아보자. 우선 캐시 기능을 사용하기 위해서는 @EnableCaching 애너테이션을 달아주어야 한다.

 

애너테이션을 이용한 캐시 기능 사용(@Cacheable)

스프링의 캐시 서비스 추상화는 AOP를 이용한다. 캐시 기능을 담은 어드바이스는 스프링이 제공한다. 이를 적용할 대상 빈과 메서드를 선정하고 속성을 부여하는 작업은 <aop:config>,<aop:advisor> 같은 기본 AOP 설정 방법을 이용할 수 있다. 하지만 이보다는 @Transaction 애노테이션을 이용하는 것처럼 캐시 서비스도 애너테이션을 이용할 수 있다.

 

캐시 서비스는 보통 메서드 단위로 지정한다. 클래스나 인터페이스 레벨에 캐시를 지정할 수도 있지만 캐시의 특성상 여러 메서드에 일괄적인 설정을 하는 경우는 드물다. 캐시에 저장할 내용과 캐시 설정 정보로 메서드의 리턴 값과 메서드 파라미터를 사용하기에 메서드 레벨로 적용하는 것이 수월하다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Product {
 
    private int productId;
    private ProductType type;
    private String productName;
    private boolean isBest;
 
}
 
@Slf4j
@Service
public class ProductService {
 
    @Cacheable("product")
    public List<Product> bestProductsByType(ProductType productType){
 
        log.info("ProductService.bestProductsByType");
 
        List<Product> bestProducts = new ArrayList<>();
        if(productType.equals(ProductType.TOP)){
            bestProducts.add(new Product(1,ProductType.TOP,"GUCCI SHIRTS",true));
        }else if(productType.equals(ProductType.PANTS)){
            bestProducts.add(new Product(1,ProductType.PANTS,"GUCCI PANTS",true));
        }else{
            bestProducts.add(new Product(1,ProductType.OUTER,"GUCCI OUTER",true));
        }
 
        return bestProducts;
    }
 
    @Cacheable(value = "product", key = "#productType")
    public List<Product> bestProduct(User user, LocalDateTime time, ProductType productType){
 
        log.info("ProductService.bestProduct");
 
        List<Product> bestProducts = new ArrayList<>();
        if(productType.equals(ProductType.TOP)){
            bestProducts.add(new Product(1,ProductType.TOP,"GUCCI SHIRTS",true));
        }else if(productType.equals(ProductType.PANTS)){
            bestProducts.add(new Product(1,ProductType.PANTS,"GUCCI PANTS",true));
        }else{
            bestProducts.add(new Product(1,ProductType.OUTER,"GUCCI OUTER",true));
        }
 
        return bestProducts;
    }
 
    @Cacheable(value = "product", condition = "#user.firstName == 'sora'")
    public List<Product> bestProductForGoldUser(User user){
 
        log.info("ProductService.bestProductForGoldUser");
 
        List<Product> bestProducts = new ArrayList<>();
        bestProducts.add(new Product(1,ProductType.OUTER,"CHANNEL OUTER",true));
        return bestProducts;
    }
 
    @Cacheable("product")
    public List<Product> allBestProduct(){
 
        log.info("ProductService.allBestProduct");
 
        List<Product> bestProducts = new ArrayList<>();
        bestProducts.add(new Product(1,ProductType.TOP,"GUCCI SHIRTS",true));
        bestProducts.add(new Product(1,ProductType.PANTS,"GUCCI PANTS",true));
        bestProducts.add(new Product(1,ProductType.OUTER,"GUCCI OUTER",true));
        return bestProducts;
    }
 
}
cs

 

위 코드는 @Cacheable 애너테이션을 이용하여 캐시 기능을 사용하는 예제 코드이다. 우선 첫번째로 ProductService 클래스의 첫번째 메서드를 살펴보자. 해당 메서드가 처음 호출되는 순간에 캐시에 "product"라는 이름의 공간이 생긴다. 그리고 파리미터로 productType(ProductType.TOP이라는 enum이 들어왔다 생각)이 들어오는데, 해당 값을 이용하여 키를 만든다. 이 말은 "product"라는 캐시 공간에 특정 키값으로 데이터를 캐시한다는 뜻이다. 그리고 첫번째 호출 이후에 동일한 메서드에 동일한 파라미터로 요청이 들어오면 "product"라는 캐시 공간에 ProductType.TOP을 이용한 키값을 가진 데이터가 있는 지 확인하고 만약 데이터가 존재하면 해당 메서드를 호출하지 않고 캐시에 있는 데이터를 돌려준다. 만약에 해당 키값으로 데이터가 없다면 메서드 로직을 수행한 후에 반환 값을 캐시에 적재한다.

 

그렇다면 두번째 메서드처럼 파라미터가 여러개인 메서드라면 어떻게 할까? 답은 바로 코드에 있다. 바로 key라는 설정 값에 SpEL식을 이용해 키값으로 사용할 파라미터 값을 명시할 수 있다. 그렇다면 이제 이 메서드는 명시한 파라미터로만 키값을 생성하게 된다. 만약 파라미터가 특정 객체로 들어온다면 key = "Product.productType" 과 같이 설정할 수 있다. 만약 키값으로 사용할 어떠한 설정 내용도 명시하지 않는 다면 어떻게 처리할까? 바로 여러개의 파라미터의 hashCode()를 조합하여 키값을 생성한다. 그렇지만 해시 코드 값의 조합이 키로서 의미가 있다면 문제가 없지만 대부분은 그렇지 않은 경우가 많기에 key 설정값을 이용하여 키로 사용할 파라미터를 명시해주는 것이 좋다.

 

세번째는 특정 조건에서만 캐싱을 하고 나머지 상황에서는 캐싱을 하고 싶지 않은 경우가 있다. 이럴 경우에는 세번째 메서드처럼 condition이라는 설정 값을 이용한다. 이 설정은 파라미터가 특정 값으로 들어올때만 캐시하고 싶을 때, 사용가능하다. 예제는 위와 같이 사용하면 된다.

 

마지막으로 메서드에 매개변수가 없다면 어떻게 될까? 이럴때는 디폴트 키 값으로 세팅이 되기 때문에 메서드가 처음 호출되면 무조건 데이터가 캐싱이 된다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProductServiceTest {
 
    @Autowired
    ProductService service;
    List<Product> bestProducts;
    User basicUser;
    User goldUser;
 
    @Before
    public void init(){
        bestProducts = new ArrayList<>();
        basicUser = new User(1,"yeoseong","yoon",28, Level.BASIC);
        goldUser = new User(1,"sora","hwang",30, Level.GOLD);
    }
 
    @Test
    public void noArgCacheTest(){
        bestProducts.addAll(service.allBestProduct());
 
        MatcherAssert.assertThat(bestProducts.size(), CoreMatchers.equalTo(3));
 
        service.allBestProduct();
    }
 
    @Test
    public void oneArgCacheTest(){
 
        bestProducts.addAll(service.bestProductsByType(ProductType.TOP));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI SHIRTS"));
 
        service.bestProductsByType(ProductType.TOP);
 
        clearList(bestProducts);
 
        bestProducts.addAll(service.bestProductsByType(ProductType.PANTS));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI PANTS"));
 
        service.bestProductsByType(ProductType.PANTS);
 
        clearList(bestProducts);
 
        bestProducts.addAll(service.bestProductsByType(ProductType.OUTER));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI OUTER"));
 
        service.bestProductsByType(ProductType.OUTER);
 
    }
 
    @Test
    public void manyArgsCacheTest(){
        bestProducts.addAll(service.bestProduct(basicUser, LocalDateTime.now(),ProductType.TOP));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI SHIRTS"));
 
        service.bestProduct(goldUser, LocalDateTime.now(),ProductType.TOP);
    }
 
    @Test
    public void conditionCacheTest(){
        bestProducts.addAll(service.bestProductForGoldUser(basicUser));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("CHANNEL OUTER"));
 
        service.bestProductForGoldUser(goldUser);
        service.bestProductForGoldUser(goldUser);
    }
 
    private void clearList(List<Product> products){
        products.clear();
    }
 
}
cs

 

위의 테스트 코드를 실행시키면 캐시가 적용된 이후에는 더 이상 ProductService 메서드의 로그가 출력되지 않을 것이다. 그 말은 캐시가 적용되어있다면 메서드 자체를 호출하지 않는 것을 알 수 있다.

 

캐시 데이터 삭제(@CacheEvict)

캐시는 적절한 시점에 제거돼야 한다. 캐시는 메서드를 실행했을 때와 동일한 겨로가가 보장되는 동안에만 사용돼야 하고 메서드 실행 결과가 캐시 값과 달리지는 순간 제거돼야 한다. 캐시를 적절히 제거해주지 않으면 사용자에게 잘못된 결과를 리턴하게 된다.

 

캐시를 제거하는 방법은 두 가지가 있다. 하나는 일정한 주기로 캐시를 제거하는 것과 하나는 캐시에 저장한 값이 변경되는 상황이 생겼을 때 캐시를 삭제하는 것이다.

 

캐시의 제거에도 AOP를 이용한다. 간단하게 메서드에 @CacheEvict 애너테이션을 붙여주면 된다. 캐시 삭제도 캐시 적재와 동일하게 키값을 기준해 적용한다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @CacheEvict("product")
    public void clearProductCache(){
        log.info("ProductService.clearProductCache");
    }
 
    @CacheEvict(value = "product", key = "#productType")
    public void clearProductCache(ProductType productType){
        log.info("ProductService.clearProductCache");
    }
 
    @CacheEvict(value = "product", condition = "#user.firstName == 'sora'")
    public void clearProductCache(User user){
        log.info("ProductService.clearProductCache");
    }
    //product의 모든 키값에 해당하는 캐시 데이터 삭제
    @CacheEvict(value = "product", allEntries = true)
    public void clearProductCacheAll(){
        
    }
cs

 

위의 코드를 이용하여 테스트 코드를 만들어 로그가 찍히는 횟수를 확인하자.

 

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
32
33
34
35
36
37
38
39
40
41
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProductServiceTest {
 
    @Autowired
    ProductService service;
    List<Product> bestProducts;
    User basicUser;
    User goldUser;
 
    @Before
    public void init(){
        bestProducts = new ArrayList<>();
        basicUser = new User(1,"yeoseong","yoon",28, Level.BASIC);
        goldUser = new User(1,"sora","hwang",30, Level.GOLD);
    }
 
    @Test
    public void clearCacheTest(){
        service.allBestProduct();
        service.clearProductCache();
        service.allBestProduct();
        System.out.println("===================================");
        service.bestProductsByType(ProductType.TOP);
        service.bestProductsByType(ProductType.PANTS);
        service.clearProductCache(ProductType.TOP);
        service.bestProductsByType(ProductType.TOP);
        service.bestProductsByType(ProductType.PANTS);
        System.out.println("===================================");
        service.bestProductsByType(ProductType.TOP);
        service.clearProductCache();
        service.bestProductsByType(ProductType.TOP);
        System.out.println("===================================");
        service.bestProductForGoldUser(goldUser);
        service.clearProductCache(basicUser);
        service.bestProductForGoldUser(goldUser);
        service.clearProductCache(goldUser);
        service.bestProductForGoldUser(goldUser);
    }
 
}
cs

 

이외에 @CachePut이라는 애너테이션이 존재한다. 이 애너테이션은 캐시에 값을 저장하는 용도로 사용한다. @Cacheable과 비슷하게 메서드 실행 결과를 캐시에 저장하지만 @CachePut은 캐시 데이터를 사용하는 것이 아니고 해당 애너테이션이 붙은 메서드를 호출 할때마다 결과 데이터를 캐시에 적재한다. 보통 한 번에 캐시에 많은 정보를 저장해두는 작업이나, 다른 사용자가 참고할 정보를 생성하는 용도로만 사용되는 메서드에 이용할 수 있다.

 

Cache Manager

스프링의 캐시 서비스는 AOP를 이용해 애플리케이션 코드를 수정하지 않고도 캐시 부가기능을 메서드에 적용할 수 있게 해준다. 동시에 캐시 기술의 종류와 상관없이 후상화된 스프링 캐시 API를 이용할 수 있게 해주는 서비스 추상화를 제공한다. 캐시 기능을 적용하는 AOP 어드바이스는 스프링이 제공해주는 것을 애너테이션을 통해 적용하면 되므로, 우리가 신경 쓸 부분은 적용할 캐시 기술을 선정하고 캐시 관련 설정을 넣어주는 것이다. 

 

캐시 추상화에서는 적용할 캐시 기술을 지원하는 캐시 매너저를 빈으로 등록해줘야 한다. 여기서는 자세한 설정법은 다루지 않고 캐시 매니저에는 무엇이 있는 지만 다루어 볼 것이다.

 

캐시 매니저 설명
ConcurrentMapCacheManager ConcurrentMapCache 클래스를 캐시로 사용하는 캐시 매니저다. ConcurrentHashMap을 이용해 캐시 기능을 구현한 간단한 캐시다. 캐시 정보를 Map 타입으로 메모리에 저장해두기 때문에 빠르고 별다른 설정이 필요없다는 장점이 있지만, 실제 서비스에서 사용하기에는 기능이 빈약하다. 캐시별 용량 제한이나 다양한 저장 방식 지원, 다중 서버 분산과 같이 고급 캐시 프레임워크가 제공하는 기능을 지원하지 않는다. 따라서 저장될 캐시 양이 많지 않고 간단한 기능에 적용할때 혹은 테스트 용도로만 사용해야 한다.
SimpleCacheManager 기본적으로 제공하는 캐시가 없다. 따라서 프로퍼티를 이용해서 사용할 캐시를 직접 등록해줘야 한다. 스프링 Cache 인터페이스를 구현해서 캐시 클래스를 직접 만드는 경우 테스트에서 사용하기 적당하다.
EhCacheCacheManager 자바에서 가장 인기있는 캐시 프레임워크의 하나인 EhCache를 지원하는 캐시 매니저다. 본격적으로 캐시 기능을 적용하려면 사용을 고려할만하다.
CompositeCacheManager 하나 이상의 캐시 매니저를 사용하도록 지원해주는 혼합 캐시 매니저다. 여러 개의 캐시 기술이나 캐시 서비스를 동시에 사용해야 할 때 이용할 수 있다. CompositeCacheManager의 cacheManagers 프로퍼티에 적용할 캐시 매니저 빈을 모두 등록해주면 된다.
NoOpCacheManager 아무런 기능을 갖지 않은 캐시 매니저다. 보통 캐시가 지원되지 않는 환경에서 동작할 때 기존 캐시 관련 설정을 제거하지 않아도 에러가 나지 않게 해주는 기능이다.

 

여기까지 스프링 캐시 추상화에 대해 다루어보았다. 아마 다음 포스팅은 직접 상용에서 사용할 수 있을 만한 캐시 매니저 구현를 이용해보는 포스팅이 될 것같다.

posted by 여성게
:
Web/Spring 2019. 9. 10. 17:06

 

우리는 컨트롤러에서 사용자가 넘겨준 파라미터를 전달 받을 때, @ModelAttribute를 붙여서 혹은 생략한 특정 객체로 파라미터를 받게된다. 우리는 이때 2가지 상황을 고려할 수 있다. 

 

  1. 파라미터 바인딩에 실패(데이터 타입 등이 맞지 않는 경우)
  2. 파라미터 바인딩은 문제없이 됬으나, 들어온 파라미터가 비즈니스 로직에 맞지 않는 혹은 유효하지 않은 파라미터 일경우

이러한 파라미터를 검증하는 방법은 무엇이 있을까? 크게 2가지 방법이 존재한다.

 

  1. 사용자 정의 Validator 구현
  2. JSR-303 애너테이션

간단히 두가지를 다루어본다.

 

사용자 정의 Validator

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Slf4j
@Component
public class UserParamValidator implements Validator {
 
    @Override
    public boolean supports(Class<?> aClass) {
//isAssignableFrom은 하위 타입도 적용되게 하는 메서드이다.
        return (User.class.isAssignableFrom(aClass));
    }
 
    @Override
    public void validate(Object o, Errors errors) {
        log.info("User Param Validator");
        User user = (User)o;
 
        if(StringUtils.isEmpty(user.getFirstName())){
            errors.rejectValue("firstName","param.required");
        }
 
    }
}
 
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @Autowired
    private UserParamValidator validator;
 
    @InitBinder
    public void initBinder(WebDataBinder webDataBinder){
 
        webDataBinder.setValidator(validator);
    }
 
    @GetMapping("/find")
    public Object findById(@ModelAttribute("findUser") @Valid User user, BindingResult bindingResult){
        List<String> resultMessages = new ArrayList<>();
        if(bindingResult.hasErrors()){
            List<FieldError> fieldsErrors = bindingResult.getFieldErrors();
            fieldsErrors.stream().forEach(fieldError->{
                log.info("bind error objectName = {}, fieldName = {} ,error code = {}",fieldError.getObjectName(),fieldError.getField(),fieldError.getCode());
            });
            return resultMessages;
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
 
}
 
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
class User{
 
    private int id;
    private String firstName;
    private String lastName;
    private int age;
    private Level level;
 
}
cs

 

위와 같이 사용자 정의 Validator 클래스를 정의하고 특정 컨트롤러에서 DI 받은 후, @InitBinder 메서드에 해당 Validator를 등록해준다. 그리고 검증이 실패한 값이 들어온다면 컨트롤러 BindingResult에 검증 실패한 정보가 저장된다. 이러한 검증 실패정보를 로그로 찍고 있는 예제이다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @GetMapping("/find")
    public Object findById(@ModelAttribute("findUser") @Valid User user, BindingResult bindingResult, Locale locale){
        List<String> resultMessages = new ArrayList<>();
        if(bindingResult.hasErrors()){
            List<FieldError> fieldsErrors = bindingResult.getFieldErrors();
            fieldsErrors.stream().forEach(fieldError->{
                log.info("bind error objectName = {}, fieldName = {} ,error code = {}",fieldError.getObjectName(),fieldError.getField(),fieldError.getCode());
                resultMessages.add(messageSource.getMessage(fieldError.getField()+"."+fieldError.getCode(),null, locale));
            });
            return resultMessages;
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
 
}
 
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
class User{
 
    private int id;
    @NotNull
    private String firstName;
    private String lastName;
    private int age;
    private Level level;
 
}
cs

 

두번째 방법은 직접 엔티티 클래스에 제약조건 애너테이션을 달아주는 방법이다. 그러면 별도의 Validator DI 없이도 BindingResult에 엔티티 클래스에 있는 애너테이션을 이용한 검증을 한 후 검증에 실패하면 정보를 넣어준다.

 

만약 검증이 아니고 데이터 타입이 잘못넘어와서 바인딩 자체가 실패하면 어떻게 될까?

 

그렇다면 아래와 같은 errorCode가 발생한다.

 

FieldName.typeMismatch

 

좋다 여기까지, 바인딩 오류 혹은 검증이 실패한 결과가 BindResult에 담겼다. 그렇다면 그 이후에는 어떻게 처리하면 좋을까? 보통 이럴때는 MessageSource를 이용해 errorCode에 해당되는 미리 준비된 문자열을 사용자에게 보여준다. 예를 들면 아래와 같은 상황이다.

 

회원가입을 하는 과정에서 사용자가 userId값을 넘기지 않으면 "ID를 입력해주세요."라는 문구가 뿌려지는데, 이럴 경우 미리 준비된 errorCode에 해당되는 메시지를 준비하여 userId 파라미터가 빈값일때 해당 메시지를 전달해줄 수 있다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
 
    @Autowired
    private LocaleChangeInterceptor localeChangeInterceptor;
 
    @Value("${spring.messages.basename}")
    String baseName;
    @Value("${spring.messages.encoding}")
    String defaultEncoding;
    @Value("${cookie.locale.param}")
    String cookieName;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
 
        registry.addInterceptor(localeChangeInterceptor);
 
    }
 
    /*
     * MessageSource Bean register
     */
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
        reloadableResourceBundleMessageSource.setBasename(baseName);
        reloadableResourceBundleMessageSource.setDefaultEncoding(defaultEncoding);
 
        return reloadableResourceBundleMessageSource;
    }
 
    /*
     * Locale Resolver Bean register
     */
    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
        cookieLocaleResolver.setCookieName(cookieName);
        cookieLocaleResolver.setDefaultLocale(Locale.KOREA);
 
        return cookieLocaleResolver;
    }
 
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName(cookieName);
 
        return  localeChangeInterceptor;
    }
}
cs

 

위와 같이 필요한 빈을 등록해준다.

 

 

message.properties 파일들을 생성해준다.

 

1
2
firstName.param.required=이름 입력은 필수입니다.
id.typeMismatch=유효하지 않은 유저 아이디 값입니다.
cs

 

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
32
33
34
35
36
37
38
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @Autowired
    private UserParamValidator validator;
    @Autowired
    private MessageSource messageSource;
 
    @InitBinder
    public void initBinder(WebDataBinder webDataBinder){
        webDataBinder.setValidator(validator);
    }
 
    @GetMapping("/find")
    public Object findById(@ModelAttribute("findUser") @Valid User user, BindingResult bindingResult, Locale locale){
        List<String> resultMessages = new ArrayList<>();
        if(bindingResult.hasErrors()){
            List<FieldError> fieldsErrors = bindingResult.getFieldErrors();
            fieldsErrors.stream().forEach(fieldError->{
                log.info("bind error objectName = {}, fieldName = {} ,error code = {}",fieldError.getObjectName(),fieldError.getField(),fieldError.getCode());
                resultMessages.add(messageSource.getMessage(fieldError.getField()+"."+fieldError.getCode(),null, locale));
            });
            return resultMessages;
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
}
cs

 

바인딩 errorCode에 해당 되는 Message가 생성되는 것을 볼 수 있다.

posted by 여성게
:
Web/Spring 2019. 9. 10. 15:22

 

이번 포스팅은 @RequestParam과 @ModelAttribute의 차이점에 대해 다루어볼 것이다. 이번에 다루어볼 내용은 특정 유저관련 컨트롤러 코드를 작성하여 살펴볼 것이다.

 

작업환경은 MacOS+intelliJ로 구성하였다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @GetMapping("/insert")
    public Object insertUser(@ModelAttribute("findUser") User user, BindingResult bindingResult){
 
        if(bindingResult.hasErrors()){
            log.info("binding error");
            return "invalidParam";
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
 
    @GetMapping("/insert2")
    public Object insertUser(@RequestParam int id, @RequestParam String firstName, @RequestParam String lastName){
        return "RequestParamBinding";
    }
 
}
 
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
class User{
 
    private int id;
    private String firstName;
    private String lastName;
    private int age;
    private Level level;
 
}
cs

 

위와 같은 코드가 있다. 세부적인 로직은 제외하고 파라미터를 바인딩하는 부분을 집중해보자. 우선 @RequestParam과 @ModelAttribute의 파라미터 바인딩에는 큰 차이가 있다. 바로 객체로 받는냐, 혹은 쿼리스트링을 하나하나 바인딩 받느냐의 차이이다. 이것은 코드의 가독성에 아주 큰 차이가 있다. 두번째 차이점은 @ModelAttribute는 사용자가 직접 View에 뿌려질 Model객체를 직접 set하지 않아도 된다는 점이다. @ModelAttribute는 스프링이 직접 ModelMap에 객체를 넣어주기 때문이다. 이러한 바인딩 차이점 이외에 한가지 아주 큰 차이점이 있다.

 

매핑을 보면 @ModelAttribute에는 BindingResult라는 인자가 들어가있는 것을 볼 수 있다. 하지만 @RequestParam은 없다. 과연 무슨 말일까?

 

@ModelAttribute는 사실 파라미터를 검증할 수 있는 기능을 제공한다. 이 말은 파라미터 바인딩에 예외가 발생하여도 혹은 파라미터 바인딩은 문제없이 됬으나 로직처리에 적합하지 않은 파라미터가 들어오는 등 이러한 파라미터를 검증할 수 있는 기능이 있다. 만약 파라미터 바인딩에 예외가 발생하였을 때, @ModelAttribute와 @RequestParam의 차이점을 보자. 전자는 바인딩 예외를 BindingResult에 담아준다. 즉, 4xx 예외가 발생하지 않는 것이다. 하지만 @RequestParam 같은 경우 바인딩 예외가 발생하는 순간 TypeMisMatch 예외를 발생시켜 사용자에게 4xx status code를 전달한다. 

 

그러면 개발자는 파라미터를 검증하는 로직을 컨트롤러 코드와 따로 분리하여 바인딩된 파라미터의 값의 유효성을 검사할 수 있게 해준다. 만약 @RequestParam이라면 직접 비즈니스 로직내에서 파라미터를 검증하는 코드가 들어가야 할 것이다.

 

바인딩 예외 처리의 차이점을 간단한 테스트 코드로 테스트 해보았다.

 

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
32
33
34
35
36
37
38
39
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserControllerTest {
 
    private static final String FINDBYID_MODELATTRIBUTE_REQUEST_URI = "/user/find";
    private static final String FINDBYID_REQUESTPARAM_REQUEST_URI = "/user/find2";
    private User validParamForFindByUserId ;
 
    private MockMvc mockMvc;
 
    @Before
    public void init(){
        this.mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
    }
 
    @Before
    public void initParam(){
        validParamForFindByUserId = new User(1,"yeoseong","yoon",28,Level.valueOf(1));
    }
 
    @Test
    public void paramVaildationWhenInvalidInputParamWithModelAttribute() throws Exception {
        MvcResult createResult = mockMvc.perform(get(FINDBYID_MODELATTRIBUTE_REQUEST_URI+"?id=abc"))
                                        .andDo(print())
                                        .andExpect(content().string("invalidParam"))
                                        .andReturn()
                                        ;
    }
 
    @Test
    public void paramValidationWhenInvalidInputParamWithRequestParam() throws Exception {
        MvcResult createResult = mockMvc.perform(get(FINDBYID_REQUESTPARAM_REQUEST_URI+"?id=abc"))
                                        .andDo(print())
                                        .andExpect(status().is4xxClientError())
                                        .andReturn()
                                        ;
    }
 
}
cs

 

위 테스트를 돌려보면 우리가 기대하는 바인딩 예외처리 테스트가 무사히 통과할 것이다.

posted by 여성게
:
Web/Spring 2019. 8. 17. 19:31

 

스프링에서 빈을 생성할 때, 기본 전략은 모든 빈이 싱글톤으로 생성된다. 즉, 어디에서든지 빈을 주입받는 다면 동일한 빈을 주입받는 것을 보장한다. 하지만 필요에 따라서 빈 주입마다 새로운 빈을 생성해야할 필요가 있을 경우도 있다. 이럴 경우에는 빈 생성시 Scope를 prototype으로 주면 빈 주입마다 새로운 인스턴스가 생성되는 것을 보장한다. 하지만 프로토타입 빈을 사용할 경우 주의해야 할 상황이 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class ABean {
    
    @Autowired
    private BBean b;
 
    public void bMethod() {
        b.print();
    }
    
}
 
@Component
@Scope("prototype")
public class BBean {
    
    public void print() {
        System.out.println("BBean !");
    }
}
cs

 

이런식으로 사용한다면 어떻게 될까? 이것은 사실 프로토타입을 쓰나마나이다. 싱글톤으로 생성된 A빈에 프로토타입 B빈을 주입해봤자 A빈은 더이상 생성되지 않기 때문에 항상 동일한 B빈을 사용하게 되는 것이다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Component
public class ABean {
    
    @Autowired
    private BBean b;
    
    public void print() {
        System.out.println(b.hashCode());
    }
    
}
 
@Component
@Scope("prototype")
public class BBean {
    
}
 
@Component
public class CBean {
    
    @Autowired
    private ABean a;
    
    public void print() {
        a.print();
    }
}
 
@Component
public class DBean {
    
    @Autowired
    private ABean a;
    
    public void print() {
        a.print();
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired private CBean c;
    @Autowired private DBean d;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        c.print();
        d.print();
    }
    
}
cs

 

위 코드를 실행시켜보자. 프로토타입 빈으로 등록된 B빈이지만 항상 어디서든 동일한 해시코드를 반환한다. 즉, 프로토타입빈을 사용하기 위해서는 아래와 같이 사용하자.

 

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
@Service
public class BeanUtil implements ApplicationContextAware {
 
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
 
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}
 
@Component
public class ABean {
    
    public void print() {
        BBean b = BeanUtil.getBean(BBean.class);
        System.out.println(b.hashCode());
    }
    
}
 
cs

 

ApplicationContext 객체에서 직접 빈을 가져와서 메소드 내부에서 사용하도록 하자. 이제는 매번 다른 B빈의 해시코드를 반환할 것이다.

 

2019/02/25 - [Web/Spring] - Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!

 

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때! @Autuwired,@Inject 등의 어노테이션으로 의존주입을 하기 위해서는 해당 객체가 빈으로 등록되어 있어야만 가능하다. 사실..

coding-start.tistory.com

 

posted by 여성게
:
Web/Spring 2019. 8. 17. 18:47

 

오늘 포스팅할 내용은 필드,생성자,세터 의존주입에 대한 내용이다. 우리가 보통 생각하는 의존주입은 무엇인가? 혹은 우리가 평소에 사용하는 의존주입의 방식은 무엇인가? 한번 생각해보고 각각에 대한 내용을 다루어보자.

 

<Field Injection>

 

1
2
3
4
5
6
7
8
9
10
11
@Component
public class ABean {
    
    @Autowired
    private BBean b;
    
    public void bMethod() {
        b.print();
    }
    
}
cs

 

보통 위와 같이 필드에 의존주입할 빈을 선언하고 @Autowired를 붙여 빈 주입을 한다.

 

<Constructor Injection>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class ABean {
    
    private BBean b;
    
    public ABean(BBean b) {
        this.b=b;
    }
    
    public void bMethod() {
        b.print();
    }
    
}
cs

 

생성자를 위한 빈 주입은 위와 같이 생성자의 매개변수로 의존 주입할 빈을 매개변수로 넣어준다. 스프링 4.3 버전 이후로는 생성자 의존주입에 @Autowired를 넣을 필요는 없다.

 

<Setter Injection>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class ABean {
    
    private BBean b;
    
    @Autowired
    public void setB(BBean b) {
        this.b = b;
    }
 
    public void bMethod() {
        b.print();
    }
    
}
cs

 

세터를 이용한 빈주입이다. 의존 주입할 빈 객체에 대한 Setter를 만들어주고 @Autowired를 붙여준다.

 

지금까지 3개의 의존주입 방법을 다루어보았다. 아직은 똑같은 결과물을 내는 다른 방법이라는 것만 느껴진다. 그렇다면 특정 상황을 연출해보자. 바로 Circular Reference(순환참조)인 경우이다. 3가지 의존주입 방법을 모두 활용하여 순환참조 상황을 재연해보자.

 

순환참조

-A빈이 있고 B빈이 있는데, 각각 서로가 서로를 참조하고 있는 상황에서 발생한다. 이러한 상황에서 A빈이 메모리에 올라가기 전에 B빈이 A빈을 의존주입하는 상황이나 혹은 그 반대의 경우 문제가 발생한다.

 

<Field Injection>

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Component
public class ABean {
    
    @Autowired
    private BBean b;
 
    public void bMethod() {
        b.print();
    }
    
    public void print() {
        System.out.println("ABean !");
    }
    
}
 
@Component
public class BBean {
    
    @Autowired
    private ABean a;
    
    public void aMethod() {
        a.print();
    }
    
    public void print() {
        System.out.println("BBean !");
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired
    private ABean a;
    @Autowired
    private BBean b;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        a.bMethod();
        b.aMethod();
    }
    
}
 
cs

 

위는 순환참조 상황을 필드 의존주입으로 재연해본 것이다. 과연 결과를 어떻게 나올 것인가?

 

<Constructor Injection>

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
public class ABean {
    
    private BBean b;
    
    public ABean(BBean b) {
        this.b=b;
    }
 
    public void bMethod() {
        b.print();
    }
    
    public void print() {
        System.out.println("ABean !");
    }
    
}
 
@Component
public class BBean {
    
    private ABean a;
    
    public BBean(ABean a) {
        this.a=a;
    }
    
    public void aMethod() {
        a.print();
    }
    
    public void print() {
        System.out.println("BBean !");
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired
    private ABean a;
    @Autowired
    private BBean b;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        a.bMethod();
        b.aMethod();
    }
    
}
cs

 

순환참조 상황을 생성자 의존주입으로 재연해보았다. 이 또한 어떠한 결과가 발생할까?

 

<Setter Injection>

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Component
public class ABean {
    
    private BBean b;
    
    @Autowired
    public void setB(BBean b) {
        this.b = b;
    }
 
    public void bMethod() {
        b.print();
    }
    
    public void print() {
        System.out.println("ABean !");
    }
    
}
 
@Component
public class BBean {
    
    private ABean a;
    
    @Autowired
    public void setA(ABean a) {
        this.a = a;
    }
 
    public void aMethod() {
        a.print();
    }
    
    public void print() {
        System.out.println("BBean !");
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired
    private ABean a;
    @Autowired
    private BBean b;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        a.bMethod();
        b.aMethod();
    }
    
}
cs

 

마지막으로 세터 의존주입을 이용하여 순환참조 상황을 재연하였다. 3가지 상황에서의 각각의 결과는 어떻게 될까?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
=>Field Injection
BBean !
ABean !
 
=>Constructor Injection
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
The dependencies of some of the beans in the application context form a cycle:
 
   circularReferenceApplication (field private com.example.demo.ABean com.example.demo.CircularReferenceApplication.a)
┌─────┐
|  ABean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/ABean.class]
↑     ↓
|  BBean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/BBean.class]
└─────┘
 
=>Setter Injection
BBean !
ABean !
cs

 

결과는 위와 같다. 어떻게 된것일까? 다 같은 결과를 내는 의존주입인데, 단 하나 생성자 의존주입에서는 애플리케이션이 기동되지 못하고 순환참조 관련 예외가 발생하였다. 이유는 3가지의 객체의 라이프사이클을 떠올려보자. 필드,세터 의존주입은 필드&세터 메소드를 이용하여  의존주입을 하게 된다. 그렇다면 전제가 무엇일까? 바로 해당 객체가 메모리에 적재된 후에 빈을 주입하게 되는 것이다. 그렇다면 생성자 의존주입은 어떨까? 생성자 의존주입은 객체를 생성자로 생성하는 시점에 필요한 빈들을 의존주입한다. 즉, 객체를 생성하는 동시에 빈을 주입하는 것 그리고 객체를 이미 생성한 이후에 빈을 주입하는 것의 차이가 되는 것이다. 위에서 순환참조에 대해 간단히 설명을 하였다. 다시 한번 떠올려보자. 

 

1
2
3
4
5
┌─────┐
|  ABean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/ABean.class]
↑     ↓
|  BBean defined in file [/Users/yun-yeoseong/eclipse-study/circular-reference/target/classes/com/example/demo/BBean.class]
└─────┘
cs

 

위와 같은 예외가 발생한 이유는 A는 B를 필요로하고 B도 A를 필요로 할때 발생하는 문제인데, 단순 서로를 참조하기 때문의 문제가 아니라 서로 참조하는 객체가 생성되지도 않았는데, 그 빈을 참조하기 때문에 발생하는 예외인 것이다. 즉, 순환참조는 당연히 생성자 의존주입에서만 문제가 될 수 밖에 없는 것이다. 생성자 의존주입은 빈 주입을 객체 생성시점에 주입하기 때문이다.

 

여기서 생각해 볼 것이 있다. 그러면 순환참조를 피하기 위해 필드 혹은 세터 의존주입을 사용해야 되는 것인가? 답은 아니다. 순환참조를 유발하는 객체 설계 자체가 잘못 설계된 객체임을 생각해볼 수 있다. 순환참조를 필드,세터 의존주입으로 피하는 것은 단순히 잘못 설계된 객체를 억지로 문제를 회피하여 사용하는 것은 아닌가 생각해볼 필요가 있다는 것이다.

 

객체지향 설계에서 객체의 의존에 순환관계가 있다면 잘못 설계된 객체인지 살펴볼 필요가 있다. 아니다. 왠만하면 리팩토링하자 !

 

<Field Injection vs Constructor Injection>

 

1.단일 책임의 원칙 위반 
의존성을 주입하기가 쉽다. @Autowired 선언 아래 3개든 10개든 막 추가할 수 있으니 말이다. 여기서 Constructor Injection을 사용하면 다른 Injection 타입에 비해 위기감 같은 걸 느끼게 해준다. Constructor의 파라미터가 많아짐과 동시에 하나의 클래스가 많은 책임을 떠안는다는 걸 알게된다. 이때 이러한 징조들이 리팩토링을 해야한다는 신호가 될 수 있다. 

 

2.의존성이 숨는다.
DI(Dependency Injection) 컨테이너를 사용한다는 것은 클래스가 자신의 의존성만 책임진다는게 아니다. 제공된 의존성 또한 책임진다. 그래서 클래스가 어떤 의존성을 책임지지 않을 때, 메서드나 생성자를 통해(Setter나 Contructor) 확실히 커뮤니케이션이 되어야한다. 하지만 Field Injection은 숨은 의존성만 제공해준다.

 

3.DI 컨테이너의 결합성과 테스트 용이성
DI 프레임워크의 핵심 아이디어는 관리되는 클래스가 DI 컨테이너에 의존성이 없어야 한다. 즉, 필요한 의존성을 전달하면 독립적으로 인스턴스화 할 수 있는 단순 POJO여야한다. DI 컨테이너 없이도 유닛테스트에서 인스턴스화 시킬 수 있고, 각각 나누어서 테스트도 할 수 있다. 컨테이너의 결합성이 없다면 관리하거나 관리하지 않는 클래스를 사용할 수 있고, 심지어 다른 DI 컨테이너로 전환할 수 있다. 
하지만, Field Injection을 사용하면 필요한 의존성을 가진 클래스를 곧바로 인스턴스화 시킬 수 없다.

4.불변성(Immutability)
Constructor Injection과 다르게 Field Injection은 final을 선언할 수 없다. 그래서 객체가 변할 수 있다.

5.순환 의존성
Constructor Injection에서 순환 의존성을 가질 경우 BeanCurrentlyCreationExeption을 발생시킴으로써 순환 의존성을 알 수 있다.

 

<Setter Injection vs Construct Injection>

 

1.Setter Injection

Setter Injection은 선택적인 의존성을 사용할 때 유용하다. 상황에 따라 의존성 주입이 가능하다. 스프링 3.x 다큐멘테이션에서는 Setter Injection을 추천했었다.


2.Constructor Injection
Constructor Injection은 필수적인 의존성 주입에 유용하다. 게다가 final을 선언할 수 있으므로 객체가 불변하도록 할 수 있다. 또한 위에서 언급했듯이 순환 의존성도 알 수 있다. 그로인해 나쁜 디자인 패턴인지 아닌지 판단할 수 있다. 
스프링 4.3버전부터는 클래스를 완벽하게 DI 프레임워크로부터 분리할 수 있다. 단일 생성자에 한해 @Autowired를 붙이지 않아도 된다.(완전 편한데?!) 이러한 장점들 때문에 스프링 4.x 다큐멘테이션에서는 더이상 Setter Injection이 아닌 Constructor Injection을 권장한다. 굳이 Setter Injection을 사용한다면, 합리적인 디폴트를 부여할 수 있고 선택적인(optional) 의존성을 사용할 때만 사용해야한다고 말한다. 그렇지 않으면 not-null 체크를 의존성을 사용하는 모든 코드에 구현해야한다.

 

posted by 여성게
:
Web/Spring 2019. 8. 5. 14:43

 

이번 포스팅에서 다루어볼 내용은 spring boot project에서 DB Access관련 설정을 application.properties에 설정 해 놓는데, 기존처럼 평문으로 username/password를 넣는 것이 아니라 특정 알고리즘으로 암호화된 문자열을 넣고 애플리케이션 스타트업 시점에 복호화하여 DB Access를 하는 것입니다. 바로 예제로 들어가겠습니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.example.demo;
 
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
 
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
 
import org.junit.Test;
 
public class EncryptTest {
 
    @Test
    public void encrypt() throws Exception {
        
        String str = "ENCRYPT!@1234";
        String encStr = encAES(str);
        System.out.println(encStr);
        System.out.println(decAES(encStr));
        
    }
    
    public Key getAESKey() throws Exception {
        String iv;
        Key keySpec;
 
        String key = "encryption!@1234";
        iv = key.substring(016);
        byte[] keyBytes = new byte[16];
        byte[] b = key.getBytes("UTF-8");
 
        int len = b.length;
        if (len > keyBytes.length) {
           len = keyBytes.length;
        }
 
        System.arraycopy(b, 0, keyBytes, 0, len);
        keySpec = new SecretKeySpec(keyBytes, "AES");
 
        return keySpec;
    }
 
    // 암호화
    public String encAES(String str) throws Exception {
        Key keySpec = getAESKey();
        String iv = "0987654321654321";
        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes("UTF-8")));
        byte[] encrypted = c.doFinal(str.getBytes("UTF-8"));
        String enStr = new String(Base64.getEncoder().encode(encrypted));
 
        return enStr;
    }
 
    // 복호화
    public String decAES(String enStr) throws Exception {
        Key keySpec = getAESKey();
        String iv = "0987654321654321";
        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes("UTF-8")));
        byte[] byteStr = Base64.getDecoder().decode(enStr.getBytes("UTF-8"));
        String decStr = new String(c.doFinal(byteStr), "UTF-8");
 
        return decStr;
    }
}
 
cs

 

위의 소스는 암복호화 단위테스트 코드입니다. AES-128 방식으로 암복호화하였으며 CBC 방식으로 진행하였습니다. 암호화 알고리즘에 대해서는 추후 포스팅하도록 하겠습니다.

 

1
2
3
#AES128_Encrypt
spring.datasource.username=T7Me97L7JW9YXubNVtNfpQ==
spring.datasource.password=/Wm/Ubs7CniQuA+5Mzq7Qg==
cs

 

위는 암호화된 형태소 Datasource 정보를 넣은 것입니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.example.demo.config;
 
import java.security.Key;
import java.util.Base64;
import java.util.Properties;
 
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
 
public class EncryptEnvPostProcessor implements EnvironmentPostProcessor {
 
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        
        Properties props = new Properties();
        try {
            props.put("spring.datasource.password", decAES(environment.getProperty("spring.datasource.password")));
            props.put("spring.datasource.username", decAES(environment.getProperty("spring.datasource.username")));
        } catch (Exception e) {
            System.out.println("DB id/password decrypt fail !");
        }
 
        environment.getPropertySources().addFirst(new PropertiesPropertySource("myProps", props));
 
    }
    
    public Key getAESKey() throws Exception {
        String iv;
        Key keySpec;
 
        String key = "encryption!@1234";
        iv = key.substring(016);
        byte[] keyBytes = new byte[16];
        byte[] b = key.getBytes("UTF-8");
 
        int len = b.length;
        if (len > keyBytes.length) {
           len = keyBytes.length;
        }
 
        System.arraycopy(b, 0, keyBytes, 0, len);
        keySpec = new SecretKeySpec(keyBytes, "AES");
 
        return keySpec;
    }
 
    // 암호화
    public String encAES(String str) throws Exception {
        Key keySpec = getAESKey();
        String iv = "0987654321654321";
        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes("UTF-8")));
        byte[] encrypted = c.doFinal(str.getBytes("UTF-8"));
        String enStr = new String(Base64.getEncoder().encode(encrypted));
 
        return enStr;
    }
 
    // 복호화
    public String decAES(String enStr) throws Exception {
        Key keySpec = getAESKey();
        String iv = "0987654321654321";
        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes("UTF-8")));
        byte[] byteStr = Base64.getDecoder().decode(enStr.getBytes("UTF-8"));
        String decStr = new String(c.doFinal(byteStr), "UTF-8");
 
        return decStr;
    }
 
}
 
cs

 

위 소스는 springboot application 기동 시점에 암호화된 username/password를 복호화하여 Datasource 객체를 생성하기 위한 후처리 프로세서입니다. EnvironmentPostProcessor라는 인터페이스를 구현한 클래스를 하나 생성하여 오버라이딩된 메소드에 복호화하는 로직을 구현해주시면 됩니다.

 

여기서 중요한 것은 그냥 후처리 프로세서를 등록하면 끝나는 것이 아니라 설정 파일을 넣어 주어야 합니다.

 

 

src/main/resources/META-INF/spring.factories 파일을 생성하여 아래와 같은 정보를 기입해줍니다.

 

1
2
# post processor 적용
org.springframework.boot.env.EnvironmentPostProcessor=com.example.demo.config.EncryptEnvPostProcessor
cs

EncryptEnvPostProcessor라는 클래스의 패키지네임을 포함한 Full Path를 해당 설정파일에 기입해주시면 됩니다.

 

여기까지 application.properties에 암호화된 정보를 넣어 복호화하는 예제를 진행해봤습니다. 사실 Datasource 외에도 다른 설정파일에도 적용가능한 예제입니다. 활용하시길 바랍니다!

posted by 여성게
: