Category: Spring

0

Spring 핵심원리 고급편 - ThreadLocal 적용

목차 Post not found: springboot/spring-aop/threadlocal/threadlocal-01 Post not found: springboot/spring-aop/threadlocal/threadlocal-02 Post not found: springboot/spring-aop/threadlocal/threadlocal-03 Post not found: springboot/spring-aop/threadlocal/threadlocal-04 Post not found: springboot/spring-aop/threadlocal/threadlocal-05 Post not found: springboot/spring-aop/threadlocal/threadlocal-06 ThreadLocal 적용@Slf4jpublic class ThreadLocalLogTrace implements LogTrace { private static final String START_PREFIX = "-->"; private static final String COMPLETE_PREFIX = "<--"; private static final String EX_PREFIX = "<X-"; private ThreadLocal<TraceId> traceIdHolder; // traceId 동기화, 동시성 이슈 @Override public TraceStatus begin(String message) { syncTraceId(); TraceId traceId = traceIdHolder.get(); Long startTimeMs = System.currentTimeMillis(); log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message); return new TraceStatus(traceId, startTimeMs, message); } private void syncTraceId(){ TraceId traceId = traceIdHolder.get(); if(traceId == null){ traceIdHolder.set(new TraceId()); }else{ traceIdHolder.set(traceId.createNextId()); } } @Override public void end(TraceStatus status) { complete(status, null); } @Override public void exception(TraceStatus status, Exception e) { complete(status, e); } private void complete(TraceStatus status, Exception e) { Long stopTimeMs = System.currentTimeMillis(); long resultTimeMs = stopTimeMs - status.getStartTimeMs(); TraceId traceId = status.getTraceId(); if (e == null) { log.info("[{}] {}{} time={}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs); } else { log.info("[{}] {}{} time={}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs, e.toString()); } releaseTraceId(); } private void releaseTraceId() { TraceId traceId = traceIdHolder.get(); if(traceId.isFirstLevel()){ traceIdHolder.remove(); // destroy }else{ traceIdHolder.set(traceId.createPreviousId()); } } private static String addSpace(String prefix, int level) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < level; i++) { sb.append((i == level - 1) ? "|" + prefix : "| "); } return sb.toString(); }} class ThreadLocalLogTraceTest { ThreadLocalLogTrace trace = new ThreadLocalLogTrace(); @Test void begin_end_level2(){ TraceStatus status1 = trace.begin("hello1"); TraceStatus status2 = trace.begin("hello2"); trace.end(status2); trace.end(status1); } @Test void begin_end_level2_exception(){ TraceStatus status1 = trace.begin("hello1"); TraceStatus status2 = trace.begin("hello2"); trace.exception(status2, new IllegalStateException()); trace.exception(status1, new IllegalStateException()); }} 15:20:49.175 [Test worker] INFO com.example.advancedspring.trace.logtrace.ThreadLocalLogTrace - [fb5762b8] hello115:20:49.179 [Test worker] INFO com.example.advancedspring.trace.logtrace.ThreadLocalLogTrace - [fb5762b8] |-->hello215:20:49.180 [Test worker] INFO com.example.advancedspring.trace.logtrace.ThreadLocalLogTrace - [fb5762b8] |<--hello2 time=3ms15:20:49.180 [Test worker] INFO com.example.advancedspring.trace.logtrace.ThreadLocalLogTrace - [fb5762b8] hello1 time=6ms @Configurationpublic class LogTraceConfig {// @Bean// public LogTrace logTrace(){// return new FieldLogTrace();// } @Bean public LogTrace logTrace(){ return new ThreadLocalLogTrace(); }} Thread 별로 로그가 정확하게 나눠진 것을 확인할 수 있다. 2021-12-06 02:15:37.485 INFO 21903 --- [nio-8080-exec-1] c.e.a.t.logtrace.ThreadLocalLogTrace : [0b8754c1] OrderController.request()2021-12-06 02:15:37.485 INFO 21903 --- [nio-8080-exec-1] c.e.a.t.logtrace.ThreadLocalLogTrace : [0b8754c1] |-->OrderService.orderItem()2021-12-06 02:15:37.486 INFO 21903 --- [nio-8080-exec-1] c.e.a.t.logtrace.ThreadLocalLogTrace : [0b8754c1] | |-->OrderRepository.save()2021-12-06 02:15:38.488 INFO 21903 --- [nio-8080-exec-1] c.e.a.t.logtrace.ThreadLocalLogTrace : [0b8754c1] | |<--OrderRepository.save() time=1002ms2021-12-06 02:15:38.488 INFO 21903 --- [nio-8080-exec-1] c.e.a.t.logtrace.ThreadLocalLogTrace : [0b8754c1] |<--OrderService.orderItem() time=1003ms2021-12-06 02:15:38.488 INFO 21903 --- [nio-8080-exec-1] c.e.a.t.logtrace.ThreadLocalLogTrace : [0b8754c1] OrderController.request() time=1003ms

0

Spring 핵심원리 고급편 - ThreadLocal

목차 Post not found: springboot/spring-aop/threadlocal/threadlocal-01 Post not found: springboot/spring-aop/threadlocal/threadlocal-02 Post not found: springboot/spring-aop/threadlocal/threadlocal-03 Post not found: springboot/spring-aop/threadlocal/threadlocal-04 Post not found: springboot/spring-aop/threadlocal/threadlocal-05 Post not found: springboot/spring-aop/threadlocal/threadlocal-06 ThreadLocal ThreadLocal은 해당 Thread 만 접근할 수 있는 저장소다ThreadLocal을 사용하게 되면 각 Thread마다 별도의 저장소를 제공한다. 사용법 값 저장 : ThreadLocal.set(value) 값 조회 : ThreadLocal.get() 값 삭제 : ThreadLocal.remove() 해당 Thread 가 ThreadLocal 을 모두 사용하고 나면 ThreadLocal.remove() 를 호출해 ThreadLocal 에 저장된 값을 제거해 줘야 한다. @Slf4jpublic class ThreadLocalService { private ThreadLocal<String> nameStore = new ThreadLocal<>(); public String logic(String name) { log.info("저장 name={} -> nameStore = {}", name, nameStore.get()); nameStore.set(name); sleep(1000); log.info("조회 nameStore = {}", nameStore.get()); return nameStore.get(); } private void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } }} @Slf4jpublic class ThreadLocalServiceTest { private ThreadLocalService service = new ThreadLocalService(); @Test public void field(){ log.info("main start"); Runnable userA = () -> { service.logic("userA"); }; Runnable userB = () -> { service.logic("userB"); }; Thread threadA = new Thread(userA); threadA.setName("thread - A"); Thread threadB = new Thread(userB); threadB.setName("thread - B"); threadA.start(); sleep(100); threadB.start(); sleep(2000); // 메인 Thread 종료 대기 } private void sleep(int millis){ try { Thread.sleep(millis); }catch (Exception e){ e.printStackTrace(); } }} 15:09:42.290 [Test worker] INFO com.example.advancedspring.trace.threadlocal.ThreadLocalServiceTest - main start15:09:42.293 [thread - A] INFO com.example.advancedspring.trace.threadlocal.code.ThreadLocalService - 저장 name=userA -> nameStore = null15:09:42.397 [thread - B] INFO com.example.advancedspring.trace.threadlocal.code.ThreadLocalService - 저장 name=userB -> nameStore = null15:09:43.301 [thread - A] INFO com.example.advancedspring.trace.threadlocal.code.ThreadLocalService - 조회 nameStore = userA15:09:43.400 [thread - B] INFO com.example.advancedspring.trace.threadlocal.code.ThreadLocalService - 조회 nameStore = userB15:09:44.402 [Test worker] INFO com.example.advancedspring.trace.threadlocal.ThreadLocalServiceTest - main exit

0

Spring 핵심원리 고급편 - 동시성 이슈

목차 Post not found: springboot/spring-aop/threadlocal/threadlocal-01 Post not found: springboot/spring-aop/threadlocal/threadlocal-02 Post not found: springboot/spring-aop/threadlocal/threadlocal-03 Post not found: springboot/spring-aop/threadlocal/threadlocal-04 Post not found: springboot/spring-aop/threadlocal/threadlocal-05 Post not found: springboot/spring-aop/threadlocal/threadlocal-06 동시성 이슈 발생 여러 Thread가 동시에 Application 로직을 호출하게 되면서 Level 상태값이 꼬여서 보이게 된다. Thread 로 구분해서 로그 확인2021-12-06 02:18:26.028 INFO 22044 --- [nio-8080-exec-1] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] OrderController.request()2021-12-06 02:18:26.029 INFO 22044 --- [nio-8080-exec-1] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] |-->OrderService.orderItem()2021-12-06 02:18:26.029 INFO 22044 --- [nio-8080-exec-1] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | |-->OrderRepository.save()2021-12-06 02:18:27.033 INFO 22044 --- [nio-8080-exec-1] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | |<--OrderRepository.save() time=1004ms2021-12-06 02:18:27.033 INFO 22044 --- [nio-8080-exec-1] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] |<--OrderService.orderItem() time=1004ms2021-12-06 02:18:27.033 INFO 22044 --- [nio-8080-exec-1] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] OrderController.request() time=1005ms 2021-12-06 02:18:26.114 INFO 22044 --- [nio-8080-exec-2] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | | |-->OrderController.request()2021-12-06 02:18:26.115 INFO 22044 --- [nio-8080-exec-2] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | | | |-->OrderService.orderItem()2021-12-06 02:18:26.115 INFO 22044 --- [nio-8080-exec-2] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | | | | |-->OrderRepository.save()2021-12-06 02:18:27.119 INFO 22044 --- [nio-8080-exec-2] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | | | | |<--OrderRepository.save() time=1004ms2021-12-06 02:18:27.120 INFO 22044 --- [nio-8080-exec-2] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | | | |<--OrderService.orderItem() time=1005ms2021-12-06 02:18:27.120 INFO 22044 --- [nio-8080-exec-2] c.e.a.trace.logtrace.FieldLogTrace : [c0f5fc0a] | | |<--OrderController.request() time=1006ms Thread 를 이용한 테스트 코드 작성@Slf4jpublic class FieldService { private String nameStore; public String logic(String name){ log.info("저장 name={} -> nameStore = {}", name, nameStore); nameStore = name; sleep(1000); log.info("조회 nameStore = {}", nameStore); return nameStore; } private void sleep(int millis) { try { Thread.sleep(millis); }catch (InterruptedException e){ e.printStackTrace(); } }}

0

Spring 핵심원리 고급편 - 필드 동기화 적용

목차 Post not found: springboot/spring-aop/threadlocal/threadlocal-01 Post not found: springboot/spring-aop/threadlocal/threadlocal-02 Post not found: springboot/spring-aop/threadlocal/threadlocal-03 Post not found: springboot/spring-aop/threadlocal/threadlocal-04 Post not found: springboot/spring-aop/threadlocal/threadlocal-05 Post not found: springboot/spring-aop/threadlocal/threadlocal-06 필드 동기화 적용Bean 으로 등록@Configuration public class LogTraceConfig { @Bean public LogTrace logTrace() { return new FieldLogTrace(); } } @RestController@RequiredArgsConstructorpublic class OrderControllerV3 { private final OrderServiceV3 orderService; private final LogTrace trace; @GetMapping("/v3/request") public String request(String itemId){ System.out.println(itemId); TraceStatus status = null; try { status = trace.begin("OrderController.request()"); orderService.orderItem(itemId); trace.end(status); return "ok"; }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } }} @Service@RequiredArgsConstructorpublic class OrderServiceV3 { private final OrderRepositoryV3 orderRepository; private final LogTrace trace; public void orderItem (String itemId){ TraceStatus status = null; try { status = trace.begin("OrderService.orderItem()"); orderRepository.save(itemId); trace.end(status); }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } }} @Repository@RequiredArgsConstructorpublic class OrderRepositoryV3 { private final LogTrace trace; public void save(String itemId){ TraceStatus status = null; try { status = trace.begin("OrderRepository.save()"); // 저장 로직 if(itemId.equals("ex")){ throw new IllegalStateException("예외 발생!"); } sleep(1000); trace.end(status); }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } } private void sleep(int millis) { try{ Thread.sleep(millis); }catch (InterruptedException e){ e.printStackTrace(); } }}

0

Spring 핵심원리 고급편 - 필드 동기화

목차 Post not found: springboot/spring-aop/threadlocal/threadlocal-01 Post not found: springboot/spring-aop/threadlocal/threadlocal-02 Post not found: springboot/spring-aop/threadlocal/threadlocal-03 Post not found: springboot/spring-aop/threadlocal/threadlocal-04 Post not found: springboot/spring-aop/threadlocal/threadlocal-05 Post not found: springboot/spring-aop/threadlocal/threadlocal-06 필드 동기화public interface LogTrace { TraceStatus begin(String message); void end(TraceStatus status); void exception(TraceStatus status, Exception e);} 필드 동기화를 위한 로그 분석기 만들기 TraceId 객체가 없을 경우 새로 생성한다. TraceId 객체가 있을 경우 Level 을 증가 시킨다. private void syncTraceId(){ if(traceIdHolder == null){ traceIdHolder = new TraceId(); }else{ traceIdHolder = traceIdHolder.createNextId(); }} TraceId 내 Level 이 0일 경우 해당 객체를 null 로 초기화 한다. TraceId 내 Level 이 0이 아닐 경우 Level을 1 감소시킨다. private void releaseTraceId() { if(traceIdHolder.isFirstLevel()){ traceIdHolder = null; // destroy }else{ traceIdHolder = traceIdHolder.createPreviousId(); }} TraceId 객체는 파라미터로 전달되는 것이 아니라 traceIdHolder 필드에 저장된다.

0

Spring 핵심원리 고급편 - Logging Filter 파라미터 동기화 개발

목차 Post not found: springboot/spring-aop/log-trace/log-trace-01 Post not found: springboot/spring-aop/log-trace/log-trace-02 Post not found: springboot/spring-aop/log-trace/log-trace-03 Spring 핵심원리 고급편 - Logging Filter 파라미터 동기화 개발Logging Filter 파라미터 동기화 개발@Slf4j@Componentpublic class HelloTraceV2 { private static final String START_PREFIX = "-->"; private static final String COMPLETE_PREFIX = "<--"; private static final String EX_PREFIX = "<X-"; public TraceStatus begin(String message) { TraceId traceId = new TraceId(); Long startTimeMs = System.currentTimeMillis(); log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message); return new TraceStatus(traceId, startTimeMs, message); } public TraceStatus beginSync(TraceId beforeTraceId, String message) { TraceId nextId = beforeTraceId.createNextId(); Long startTimeMs = System.currentTimeMillis(); log.info("[{}] {}{}", nextId.getId(), addSpace(START_PREFIX, nextId.getLevel()), message); return new TraceStatus(nextId, startTimeMs, message); } public void end(TraceStatus status) { complete(status, null); } public void exception(TraceStatus status, Exception e) { complete(status, e); } private void complete(TraceStatus status, Exception e) { Long stopTimeMs = System.currentTimeMillis(); long resultTimeMs = stopTimeMs - status.getStartTimeMs(); TraceId traceId = status.getTraceId(); if (e == null) { log.info("[{}] {}{} time={}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs); } else { log.info("[{}] {}{} time={}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs, e.toString()); } } private static String addSpace(String prefix, int level) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < level; i++) { sb.append((i == level - 1) ? "|" + prefix : "| "); } return sb.toString(); }} @RestController@RequiredArgsConstructorpublic class OrderControllerV2 { private final OrderServiceV2 orderService; private final HelloTraceV2 trace; @GetMapping("/v2/request") public String request(String itemId){ System.out.println(itemId); TraceStatus status = null; try { status = trace.begin("OrderController.request()"); orderService.orderItem(status.getTraceId(), itemId); trace.end(status); return "ok"; }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } }} @Service@RequiredArgsConstructorpublic class OrderServiceV2 { private final OrderRepositoryV2 orderRepository; private final HelloTraceV2 trace; public void orderItem(TraceId traceId, String itemId){ TraceStatus status = null; try { status = trace.beginSync(traceId, "OrderService.orderItem()"); orderRepository.save(status.getTraceId(),itemId); trace.end(status); }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } }} @Repository@RequiredArgsConstructorpublic class OrderRepositoryV2 { private final HelloTraceV2 trace; public void save(TraceId traceId, String itemId){ TraceStatus status = null; try { status = trace.beginSync(traceId,"OrderRepository.save()"); // 저장 로직 if(itemId.equals("ex")){ throw new IllegalStateException("예외 발생!"); } sleep(1000); trace.end(status); }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } } private void sleep(int millis) { try{ Thread.sleep(millis); }catch (InterruptedException e){ e.printStackTrace(); } }}

0

Spring 핵심원리 고급편 - Logging Filter

목차 Post not found: springboot/spring-aop/log-trace/log-trace-01 Post not found: springboot/spring-aop/log-trace/log-trace-02 Post not found: springboot/spring-aop/log-trace/log-trace-03 Spring 핵심원리 고급편 - Logging Filter@RestController@RequiredArgsConstructorpublic class OrderControllerV1 { private final OrderServiceV1 orderService; private final HelloTraceV1 trace; @GetMapping("/v1/request") public String request(String itemId){ TraceStatus status = null; try { status = trace.begin("OrderController.request()"); orderService.orderItem(itemId); trace.end(status); return "ok"; }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } }} @Service@RequiredArgsConstructorpublic class OrderServiceV1 { private final OrderRepositoryV1 orderRepository; private final HelloTraceV1 trace; public void orderItem(String itemId){ TraceStatus status = null; try { status = trace.begin("OrderService.orderItem()"); orderRepository.save(itemId); trace.end(status); }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } }} @Repository@RequiredArgsConstructorpublic class OrderRepositoryV1 { private final HelloTraceV1 trace; public void save(String itemId){ TraceStatus status = null; try { status = trace.begin("OrderRepository.save()"); // 저장 로직 if(itemId.equals("ex")){ throw new IllegalStateException("예외 발생!"); } sleep(1000); trace.end(status); }catch (Exception e){ trace.exception(status, e); throw e; // 예외를 반드시 던져 줘야 한다. } } private void sleep(int millis) { try{ Thread.sleep(millis); }catch (InterruptedException e){ e.printStackTrace(); } }}

0

Spring 핵심원리 고급편 - Logging Filter

목차 Post not found: springboot/spring-aop/log-trace/log-trace-01 Post not found: springboot/spring-aop/log-trace/log-trace-02 Post not found: springboot/spring-aop/log-trace/log-trace-03 Spring 핵심원리 고급편 - Logging FilterTraceId - 트랜잭션 Id와 level을 관리public class TraceId { private String id; // 트랜잭션 ID private int level; // 깊이를 표현할 수 있는 Level public TraceId() { this.id = creatId(); this.level = 0; } private TraceId(String id, int level){ this.id = id; this.level = level; } public String creatId() { return UUID.randomUUID().toString().substring(0, 8); } public TraceId createNextId(){ return new TraceId(id, level+1); } // 트랜잭션 ID 는 기존과 같고 Level 은 하나 감소시킨다. public TraceId createPreviousId(){ return new TraceId(id, level-1); } public boolean isFirstLevel(){ return level == 0; }} TraceStatus - 로그 상태 정보를 나타낸다. traceId 내부에 트랜잭션 Id 와 Level 을 가지고 있다. startTimeMs 로그 시작 시간이다. 로그 종료시 이 시작 시간을 기준으로 시작 ~ 종료까지 수행시간을 구할 수 있다. message 시작시 사용한 메시지다. 로그 종료시에 사용해 출력한다. public class TraceStatus { private TraceId traceId; private Long startTimeMs; private String message; public TraceStatus(TraceId traceId, Long startTimeMs, String message) { this.traceId = traceId; this.startTimeMs = startTimeMs; this.message = message; } public TraceId getTraceId() { return traceId; } public Long getStartTimeMs() { return startTimeMs; } public String getMessage() { return message; }} @Slf4j@Componentpublic class HelloTraceV1 { private static final String START_PREFIX = "-->"; private static final String COMPLETE_PREFIX = "<--"; private static final String EX_PREFIX = "<X-"; public TraceStatus begin(String message) { TraceId traceId = new TraceId(); Long startTimeMs = System.currentTimeMillis(); log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message); return new TraceStatus(traceId, startTimeMs, message); } public void end(TraceStatus status) { complete(status, null); } public void exception(TraceStatus status, Exception e) { complete(status, e); } private void complete(TraceStatus status, Exception e) { Long stopTimeMs = System.currentTimeMillis(); long resultTimeMs = stopTimeMs - status.getStartTimeMs(); TraceId traceId = status.getTraceId(); if (e == null) { log.info("[{}] {}{} time={}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs); } else { log.info("[{}] {}{} time={}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs, e.toString()); } } private static String addSpace(String prefix, int level) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < level; i++) { sb.append((i == level - 1) ? "|" + prefix : "| "); } return sb.toString(); }} 테스트 코드 작성

0

Spring Boot - WebMvcConfigurer

Matrix Variable 설정 참고 https://www.baeldung.com/spring-mvc-matrix-variables WebMvcConfigurer Spring MVC 설정을 추가, 변경하기 위해 사용하는 인터페이스WebMvcConfigurer 인터페이스는 CORS, Interceptor, ViewResolver, MessageConverter 등 여러가지 설정을 변경, 추가할 수 있게 도와준다. WebMvcConfigurer 인터페이스내 메소드들은 default 로 선어돼 있어 필요한 것만 구현하면 된다. add~: 기본 설정이 없는 Bean에 대하여 새로운 설정 Bean을 추가함 configure~: 기존 설정 대신 새롭게 정의한 설정 Bean을 사용한다. (set 메소드) extend~: 기존 설정에 새롭게 정의한 설정 Bean을 추가한다. (add 메소드) WebMvcConfigurer.java public interface WebMvcConfigurer { default void configurePathMatch(PathMatchConfigurer configurer) {} default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {} default void configureAsyncSupport(AsyncSupportConfigurer configurer) {} default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {} default void addFormatters(FormatterRegistry registry) {} default void addInterceptors(InterceptorRegistry registry) {} default void addResourceHandlers(ResourceHandlerRegistry registry) {} default void addCorsMappings(CorsRegistry registry) {} default void addViewControllers(ViewControllerRegistry registry) {} default void configureViewResolvers(ViewResolverRegistry registry) {} default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {} default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {} default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {} default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {} default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {} default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {} @Nullable default Validator getValidator() {return null;} @Nullable default MessageCodesResolver getMessageCodesResolver() {return null;}} CORS 설정Spring Boot 에서는 기본적으로 CORS에 대한 설정이 없기 때문에 CORS 를 사용하기 위해서는 WebMvcConfigurer 내 addCorsMappings 메소드를 이용해 설정해줘야 한다.

0

Spring Rest API - Spring Boot Swagger 사용하기

목차 Spring Rest API - Spring Boot Swagger 사용하기 Spring Rest API - ExceptionHandling Spring Rest API - HTTP Status Code 다루기 Spring Rest API - 도메인 및 서비스 로직 작성하기 Spring Rest API - Rest API 시작하기 Controller 작성 Spring Boot Rest API - Spring Boot Swagger 사용하기 Swagger 란 Open API Specification(OAS) 를 위한 프레임워크다. 개발자가 REST API 서비스를 설계, 빌드, 문서화 할 수 있도록 도와준다. Swagger를 사용하기 위한 의존성 추가하기implementation "io.springfox:springfox-boot-starter:3.0.0" Swagger를 사용하기 위한 설정 추가 Docket Swagger 설정의 핵심이 되는 Bean apis ApiSelectorBuilder 를 생성 명시된 Package나 어노테이션를 대상으로 Swagger API 문서를 만든다. RequestHandlerSelectors 클래스 메소드를 통해 설정한다. any : 전부 basePackage : 명시된 패키지를 대상으로 Swagger API 문서를 만든다. withClassAnnotation : 명시된 어노테이션이 붙은 Class만 Swagger API 문서를 만든다. withMethodAnnotation : 명시된 어노테이션이 붙은 Method만 Swagger API 문서를 만든다. paths apis 에서 명시된 대상에서 paths 조건에 맞는 것만 Swagger API 문서를 만들 수 있도록 필터링 한다. Open API 3.0 사용한 방법

0

Spring Security - UsernamePasswordAuthenticationFilter & AbstractAuthenticationProcessingFilter

목차 Spring Security 권한 계층 사용하기 - @RoleHierarcy Spring Security - DelegateFilterProxy Spring Security - Remember Me와 관련된 인증 API - RememberMeConfigurer Spring Security - RembmerMeAuthenticationFilter Spring Security - SessionManagementFilter & ConcurrentSessionFilter Spring Security - 인가 API ExpressionUrlAuthorizationConfigurer Spring Security - Security 설정을 위한 WebSecurityConfigurerAdatper Spring Security - AuthenticationProvider Spring Security - AuthenticationManager Spring Security - UsernamePasswordAuthenticationFilter & AbstractAuthenticationProcessingFilter Spring Security - SecurityContextHolder 와 SecurityContext Spring Security - Authentication 객체 참고 https://spring.io/guides/topicals/spring-security-architecture AbstractAuthenticationProcessingFilter 인증 과정 AbstractAuthenticationProcessingFilter 는 Spring Security Filter에서 인증 과정을 구현한 추상 Class 다. 로그인을 진행하는 URL로 로그인 요청이 들어왔는지 확인한다. 요청에 대한 인증을 진행하고 Authentication 객체를 반환 받는다. Session 전략에 따라 적절한 처리를 진행한다. (Session Id 변경) 인증이 완료 됐으면 Authentication 객체를 SecurityContext에 저장한다. AbstractAuthenticationProcessingFilter.java @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);}private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 로그인을 진행하는 URL로 로그인 요청이 들어왔는지 확인한다. if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { // 인증을 진행하고 생성된 Authentication 객체를 반환받는다. // AbstractAuthenticationProcessingFilter 를 상속받은 Class에 의해 구현되는 부분 Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { return; } // Session에 대한 설정을 한다. Session Id를 변경 this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 인증 후 처리를 진행한다. successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed unsuccessfulAuthentication(request, response, ex); }}

0

Spring Security - 인가 API ExpressionUrlAuthorizationConfigurer

목차 Spring Security 권한 계층 사용하기 - @RoleHierarcy Spring Security - DelegateFilterProxy Spring Security - Remember Me와 관련된 인증 API - RememberMeConfigurer Spring Security - RembmerMeAuthenticationFilter Spring Security - SessionManagementFilter & ConcurrentSessionFilter Spring Security - 인가 API ExpressionUrlAuthorizationConfigurer Spring Security - Security 설정을 위한 WebSecurityConfigurerAdatper Spring Security - AuthenticationProvider Spring Security - AuthenticationManager Spring Security - UsernamePasswordAuthenticationFilter & AbstractAuthenticationProcessingFilter Spring Security - SecurityContextHolder 와 SecurityContext Spring Security - Authentication 객체 인가 API ExpressionUrlAuthorizationConfigurer 를 통해 자원에 접근하기 위한 다양한 인가 방식을 지원한다.역할(ROLE) 기반 인가 방식, 권한(Authority) 기반 인가 방식, IP 를 통한 인가 방식등 다양한 API를 제공한다. hasRole 명시된 권한에 접미사 ROLE_ 을 붙이고 해당 권한을 가진 사용자만 자원에 접근을 허용한다. hasAuthority 와 비슷한 개념이지만 hasRole 은 USER, MANAGER, ADMIN과 같은 역할 에 따라 자원에 대한 접근을 허용한다. hasAnyRole 명시된 권한들에 접미사 ROLE_ 을 붙이고 사용자가 하나라도 권한을 갖고 있으면 해당 자원에 대한 접근을 허용한다. hasAuthority 해당 권한을 가진 사용자만 자원에 접근을 허용한다. hasRole 와 비슷한 개념이지만 hasAuthority는 CREATE, READ, WRITE, DELETE 와 같이 권한 에 따라 자원에 대한 접근을 허용한다. hasAnyAuthority 명시된 권한들 중 하나라도 권한이 있는 사용자만 자원에 접근을 허용한다. hasIpAddress 특정 IP 에 대해 자원에 대한 접근을 허용할 때 사용한다. permitAll 권한에 대한 검증 없이 자원에 대한 접근을 허용한다. anonymous 익명 사용자의 접근을 허용 익명 사용자는 기본적으로 ROLE_ANONYMOUS 권한이 부여돼 있다. rememberMe Remember-Me 를 통해 인증된 사용자가 자원에 대한 접근을 허용한다. denyAll 모든 접근을 허용하지 않는다. authenticated 인증된 사용자만 자원에 대한 접근을 허용한다. fullyAuthenticated Remember-Me 를 통해 인증된 사용자를 제외하고 인증된 사용자에 대한 접근을 허용한다. ExpressionUrlAuthorizationConfigurerpublic ExpressionInterceptUrlRegistry hasRole(String role) { return access(ExpressionUrlAuthorizationConfigurer .hasRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, role));}public ExpressionInterceptUrlRegistry hasAnyRole(String... roles) { return access(ExpressionUrlAuthorizationConfigurer .hasAnyRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, roles));}public ExpressionInterceptUrlRegistry hasAuthority(String authority) { return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));}public ExpressionInterceptUrlRegistry hasAnyAuthority(String... authorities) { return access(ExpressionUrlAuthorizationConfigurer.hasAnyAuthority(authorities));}public ExpressionInterceptUrlRegistry hasIpAddress(String ipaddressExpression) { return access(ExpressionUrlAuthorizationConfigurer.hasIpAddress(ipaddressExpression));}public ExpressionInterceptUrlRegistry permitAll() { return access(permitAll);}public ExpressionInterceptUrlRegistry anonymous() { return access(anonymous);}public ExpressionInterceptUrlRegistry rememberMe() { return access(rememberMe);}public ExpressionInterceptUrlRegistry denyAll() { return access(denyAll);}public ExpressionInterceptUrlRegistry authenticated() { return access(authenticated);}public ExpressionInterceptUrlRegistry fullyAuthenticated() { return access(fullyAuthenticated);}public ExpressionInterceptUrlRegistry access(String attribute) { if (this.not) { attribute = "!" + attribute; } interceptUrl(this.requestMatchers, SecurityConfig.createList(attribute)); return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;}

0

Spring Security - SessionManagementFilter & ConcurrentSessionFilter

목차 Spring Security 권한 계층 사용하기 - @RoleHierarcy Spring Security - DelegateFilterProxy Spring Security - Remember Me와 관련된 인증 API - RememberMeConfigurer Spring Security - RembmerMeAuthenticationFilter Spring Security - SessionManagementFilter & ConcurrentSessionFilter Spring Security - 인가 API ExpressionUrlAuthorizationConfigurer Spring Security - Security 설정을 위한 WebSecurityConfigurerAdatper Spring Security - AuthenticationProvider Spring Security - AuthenticationManager Spring Security - UsernamePasswordAuthenticationFilter & AbstractAuthenticationProcessingFilter Spring Security - SecurityContextHolder 와 SecurityContext Spring Security - Authentication 객체 SessionManagementFilter SessionManagementFilter 는 인증된 사용자에 대한 Session 관리 와 Session 공격으로 부터의 보호 를 위한 Filter Session 관리 인증시 사용자의 Session 등록, 조회, 삭제 등의 이력을 관리한다. 동시성 Session 제어 동일 계정으로 접속하는 최대 Session 수를 설정한다. Session 고정 보호 인증할때 마다 새로운 Session 쿠키를 새로 발급해 쿠키 조작을 방지한다. Session 생성 정책 SessionManagementFilter.java private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (request.getAttribute(FILTER_APPLIED) != null) { chain.doFilter(request, response); return; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (!this.securityContextRepository.containsContext(request)) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && !this.trustResolver.isAnonymous(authentication)) { try { // 최대 생성 개수를 넘어섰는지?, Session 고정 공격이 들어왔는지? 등 // Session에 대한 인증을 진행한다. this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response); } catch (SessionAuthenticationException ex) { this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex); SecurityContextHolder.clearContext(); this.failureHandler.onAuthenticationFailure(request, response, ex); return; } this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response); } else { if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) { if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Request requested invalid session id %s", request.getRequestedSessionId())); } if (this.invalidSessionStrategy != null) { this.invalidSessionStrategy.onInvalidSessionDetected(request, response); return; } } } } chain.doFilter(request, response);} Session 관리를 위한 SessionAuthenticationStrategy SessionAuthenticationStrategy 는 Session 이 존재하는지 확인하거나 Session 고정 공격에 대한 보호를 위해 Session Id를 변경한다.

0

Spring Security - RembmerMeAuthenticationFilter

목차 Spring Security 권한 계층 사용하기 - @RoleHierarcy Spring Security - DelegateFilterProxy Spring Security - Remember Me와 관련된 인증 API - RememberMeConfigurer Spring Security - RembmerMeAuthenticationFilter Spring Security - SessionManagementFilter & ConcurrentSessionFilter Spring Security - 인가 API ExpressionUrlAuthorizationConfigurer Spring Security - Security 설정을 위한 WebSecurityConfigurerAdatper Spring Security - AuthenticationProvider Spring Security - AuthenticationManager Spring Security - UsernamePasswordAuthenticationFilter & AbstractAuthenticationProcessingFilter Spring Security - SecurityContextHolder 와 SecurityContext Spring Security - Authentication 객체 참고 https://docs.spring.io/spring-security/reference/servlet/authentication/rememberme.html RembmerMeAuthenticationFilter Client 로부터 Request(요청) 이 오게 되면 Request 객체 Cookie 에 Remember Me Token 이 있는지 확인한다. Token 을 이용해 인증을 진행한 후 SecurityContext 에 Authentication 객체를 저장한다. RememberMeAuthenticationFilter.java private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // SecurityContext 내 Authentication 객체가 있는지 확인 한다. if (SecurityContextHolder.getContext().getAuthentication() != null) { this.logger.debug(LogMessage .of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'")); chain.doFilter(request, response); return; } // Remember Me 인증을 진행한 후 Authentication 객체를 반환받는다. Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { // Attempt authenticaton via AuthenticationManager try { // RememberMeAuthenticationToken 객체내 Key 값에 대한 Hash 비교를 통해 유효성 인증을 진행 후 객체를 그대로 반환받는다. rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth); // SecurityContextHolder 에 인증 받은 Authentication 객체를 저장한다. SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); // 인증 성공 후 후작업을 진행한다. onSuccessfulAuthentication(request, response, rememberMeAuth); this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'")); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( SecurityContextHolder.getContext().getAuthentication(), this.getClass())); } if (this.successHandler != null) { this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth); return; } } catch (AuthenticationException ex) { this.logger.debug(LogMessage .format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager " + "rejected Authentication returned by RememberMeServices: '%s'; " + "invalidating remember-me token", rememberMeAuth), ex); this.rememberMeServices.loginFail(request, response); onUnsuccessfulAuthentication(request, response, ex); } } chain.doFilter(request, response);} AbstractRememberMeServices

0

Spring boot - MultipartFile 에서 발생하는 예외 처리

목차 Spring boot - StreamingResponseBody Spring boot - ResourceRegion Spring boot - 파일 다운로드 서비스 구현하기 Spring boot - 파일 업로드 서비스 구현하기 Spring boot - Resource 추상화 Spring boot - MultipartFile 에서 발생하는 예외 처리 Spring boot - MultipartFile 를 이용한 파일 업로드 Spring boot - Part 객체를 이용한 파일 업로드 MaxUploadSizeExceededException 예외MaxUploadSizeExceededException 예외가 발생하는 경우는 크게 두 가지가 있다. 첫 번째는 FileSizeLimitExceededException 예외가 발생했을 때 두 번째는 SizeLimitExceededException 예외가 발생했을 때다. FileSizeLimitExceededException 서버로 전송되는 각 파일 크기가 제한을 초과했을 때 발생하는 예외, 기본값은 1MB다.org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes. SizeLimitExceededException 서버로 전송되는 모든 파일 크기가 제한을 초과했을 때 발생하는 예외, 기본값은 10MB다.org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (29404104) exceeds the configured maximum (10485760)