반응형

개요 : 아주 간단하게 Docker를 이용해서 Spring 프로젝트 이미지를 생성 후 실행해 보도록 하겠습니다.

 

1. spring project build

  - WebController.java

@Controller
public class WebController {

    @GetMapping("/")
    public String sample() {
        return "sample";
    }

}

  - sample.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Docker</title>
</head>
<body>
  <span>Docker!</span>
</body>
</html>

 

- gradle 기준 : 터미널에서 ./gradlew build

 

2. Dockrfile 생성

  - 프로젝트 Root 경로 바로 아래에 Dockerfile 생성

FROM openjdk:11  # 컴파일 할 jdk 버전

WORKDIR /usr/src/app # 모든 작업 파일 기준 위치

ARG JAR_PATH=./build/libs # 변수 사용하듯이 선언해서 사용

COPY ${JAR_PATH}/IPGeoCheck-0.0.1-SNAPSHOT.jar ${JAR_PATH}/IPGeoCheck-0.0.1-SNAPSHOT.jar
# 빌드한 jar 파일을 도커 컨터이너 내부로 옮겨주는 작업

CMD ["java","-jar","./build/libs/IPGeoCheck-0.0.1-SNAPSHOT.jar"]
# jar 파일 실행 명령

 

3.  도커 이미지 빌드

  - 터미널에서 아래와 같이 명령어 실행(도커 파일이 있는 위치에서)

docker build . -t springbootapp (도커 이미지 빌드)

 

 

4. 도커 이미지 실행

  - p 옵션을 넣어서 실행한다.

    도커 내부 네트워크와 외부 네트워크를 연결하기 위한 포트 연결(로컬 포트 / 도커 내부 포트)

    주의 : 포트 옵션을 주지 않으면 도커 이미지를 실행 되었으나 접근이 안되는 현상이 발생된다.

docker run -p 8080:8080 springbootapp(이미지 실행)

※ 문제점 : 어플리케이션 코드의 변경으로 인해서 도커 이미지를 매번 다시 빌드해서 실행해야 한다.

이러한 불편함을 해소하고자 Volume 옵션을 사용한다.

 

 

5. 볼륨(Volume) 옵션을 이용한 도커 이미지 실행

docker run -p 8080:8080 -v $(pwd):/usr/src/app springbootapp(볼륨을 사용한 이미지 실행)
# -v "local 참조할 경로" : "참조할 도커 이미지 경로"
Volume 옵션은 실행에 필요한 파일을들 컨테이너 내부에서 참조할 수 있도록 해줍니다.
로컬 경로에 존재하는 모든 파일들을 도커 컨테이너 내부에서 사용할 수 있습니다.

 

728x90
반응형

'Docker' 카테고리의 다른 글

[Docker] docker compose 이용해서 Wordpress 세팅  (0) 2023.09.26
반응형

오류 처리는 더 견고한 서비스를 위해서 개발자에게 매우 중요합니다.

이번 시간에는 SpringBoot에서는 기본적으로 어떻게 오류 페이지를 처리하고 있는지, 어떻게 커스텀 하게 수정해서 오류 페이지를 관리하는지 알아보도록 하겠습니다.

 

1. SpringBoot에서는 기본적으로 BasicErrorController에서 오류를 관리하고 있습니다.

BasicErrorController.java

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

	private final ErrorProperties errorProperties;

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 */
	public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
		this(errorAttributes, errorProperties, Collections.emptyList());
	}

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 * @param errorViewResolvers error view resolvers
	 */
	public ₩(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
			List<ErrorViewResolver> errorViewResolvers) {
		super(errorAttributes, errorViewResolvers);
		Assert.notNull(errorProperties, "ErrorProperties must not be null");
		this.errorProperties = errorProperties;
	}

	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}
   }

 - @RequestMapping("${server.error.path:${error.path:/error}}")

    -> 설정 파일에 error.path 값이 없으면 default로 /error를 사용합니다.

 - HTML로 응답을 주는 경우 errorHtml 메소드로 응답을 합니다.

 - HTML 외 응답은 error 메소드에서 처리합니다.

 

2. 에러 관련 설정 파일

application.yml

server:
  error:
    whitelabel:
      enabled: true           # 화이트 라벨 페이지 유무 (default : true) 
    include-stacktrace: never # 오류 응답에 stacktrace 내용을 포함할 지 여부 (default : always)
    path: /error              # 오류 응답을 처리할 핸들러(ErrorController) path (default : /error)

 

3. 에러 발생하면 에러 페이지 HTML을 노출시키도록 해보겠습니다.

개발 환경

  •  Spring 2.7.5
  •  Thymeleaf

 

기본적으로 오류 페이지를 노출시키는 방법은 간단합니다.

저는 Thymleaf를 사용하고 있기 때문에 resources > templates > error 디렉토리 아래에 에러 코드 이름으로

html 파일을 생성해주면 됩니다.

 * 400번대 에러코드를 모두 커버하고 싶다면 4xx.html로 생성하면 되겠습니다.

 

404.html

참 쉽죠? 이제 직접 ErrorController를 구현해서 에러 페이지에 데이터를 전달해 보도록 하겠습니다.

4. @ErrorController

WebErrorController.java

@Controller
public class WebErrorController implements ErrorController {

    @RequestMapping(value = "/error")
    public ModelAndView handleNoHandlerFoundException(HttpServletResponse response, HttpServletRequest request, Model model) {
        Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        // int status = response.getStatus();

        ModelAndView modelAndView = new ModelAndView();

        if (statusCode != null) {
            Integer status = Integer.valueOf(statusCode.toString());
            if (status == HttpStatus.NOT_FOUND.value()) {
                modelAndView.addObject("errorCode", status);
                modelAndView.setViewName("/error/404");
            } else if (status == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
                modelAndView.addObject("errorCode", status);
                modelAndView.setViewName("/error/500");
            } else if (status == HttpStatus.FORBIDDEN.value()) {
                modelAndView.setViewName("/error/403");
            } else modelAndView.setViewName("/error/common");
        }

        return modelAndView;
    }
}

 - ErrorController를 구현합니다.

 - request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE), response.getStatus()으로 에러 코드를 꺼냅니다.

 

 

404.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h3>어이쿠 오류다!</h3>
    <h2 th:text="${errorCode}"></h2>
</body>
</html>

404.html

추가적으로

@ExceptionHandler를 사용해서 해당 애노테이션이 선언된 예외 및 하위 예외에 대해서 특정 메서드가 처리할 수 있도록 해줍니다.

상황에 맞추어서 리턴 형식을 반환할 수 있습니다.

    @ResponseStatus(HttpStatus.NOT_IMPLEMENTED)
    @ExceptionHandler(Exception.class)
    public Map<String, String> handle(Exception e) {
        Map<String, String> errorAttributes = new HashMap<>();
        errorAttributes.put("code", "NOT_IMPLEMENTED");
        errorAttributes.put("message", e.getMessage());
        return errorAttributes;
    }
728x90
반응형
반응형

아키텍처 흐름

  1. 어느 부분에서 Authentication이 저장되는가? 
  2. AuthenticationManager를 통해서 인증된다.(Authentication)
  3. 결과로 나온 Authentication을 다시 SecurityContextHolder에 저장은 어디서?
  4. Authentication은 UsernamePasswordAuthenticationFilter, SecurityContextPersisenceFilter 에서 저장되는데 그러면 이 필터들은 어디에서 등록되는가?
  5. WebSecurityConfigurerAdapter를 상속받은 SecurityConfig에 설정한 정보가 Filter에 등록되는데 어떻게 url로 요청하면 FilterChainProxy 들어왔는가?
  6. DelegatingFilterProxy를 통해서 FilterChainProxy에 들어온다.
  7. 권한 확인(인가)은 어디서 이루어지는가? AccessDecisionManager
  8. AccessDecisionManager는 어디서 사용하고 있는가? FilterSecurityInterceptor
  9. 인증과 인가처리에 발생한 에러가 어떻게 처리되는지? ExceptionTranslationFilter

SecurityContextHolder와 Authentication

SecurityContextHolder

  • SecurityContext 제공, 기본적으로 ThreadLocal를 사용
  • 한 Thread에 특화되어 있는 정보
  • application 어디에서나 꺼내서 사용할 수 있다.
  • SampleService.findSercurityContextHolder() 참고

SecurityContext

  • Authentication 제공.

Authentication

  • Principal과 GrantAuthority 제공.

Principal

  • "누구"에 해당하는 정보
  • UserDetailsService에서 리턴한 그 객체(AccountService.class 참고)
  • 객체는 Userdetails 타입

GrantAuthority

  • "ROLE_USER", "ROLE_ADMIN"등 Principal이 가지고 있는 "권한"을 나타낸다.
  • 인증 이후, 인가 및 권한 확인할때 이 정보를 참조한다.

UserDetails

  • 어플리케이션이 가지고 있는 유저 정보와 스프링 시큐리티가 사용하는 Authentication 객체 사이의 어댑처

UserDetailsService

  • 유저 정보를 UserDetails 타입으로 가져오는 DAO 인터페이스
  • 구현은 마음대로

AuthenticationManager(인증할 때 사용)와 Authentication

  • 스프링 시큐리티에서 인증(Authentication)은 AuthenticationManager가 한다.

Authentication과 SecurityContextHolder

  • UsernamePasswordAuthenticationFilter
    • 폼 인증을 처리하는 시큐리티 필터
    • 인증된 Authentication 객체를 SecurityContextHolder에 넣어주는 필터
    • SecurityContextHolder.getContext().setAuthentication(authentication)
  • SecurityContextPersisenceFilter
    • SecurityContext를 HTTP session에 캐시(기본 전략)하여 여러 요청에서 Authentication을 공유할 수 있는 공유 필터
    • SecurityContextRepository를 교체하여 세션을 HTTP session이 아닌 다른 곳에 저장하는 것도 가능하다.
    • 같은 session에서만 공유된다.

스프링 시큐리티 Filter와 FilterChainProxy

  • 스프링 시큐리티 필터는 FilterChainProxy가 호출한다.
  • 여기에 등록되는 필터들은 SecurityConfig에서 설정한 정보가 SecurityFilterChain을 만드는데 사용된다.(FilterChainProxy.getFilters에 SecurityFilterChain)
  • SecurityConfig 설정에 따라서 등록되는 Filter에 개수가 달라진다.

DelegatingFilterProxy와 FilterChainProxy

DelegatingFilterProxy

  • 일반적인 서블릿 필터(위에서 살펴본 다른 필터들과 같은 서블릿 필터지만 서블릿에 직접 등록되는 필터)
  • 서블릿 필터 처리를 스프링에 들어있는 빈으로 위이함고 싶을 때 사용하는 서블릿 필터
  • 타겟 빈 이름을 설정한다.
  • 스프링 부트 없이 스프링 시큐리티 설정할 때는 AbstractSecurityWebApplicationInitializer를 사용해서 등록
  • 스프링 부트를 사용할 때는 자동으로 등록된다. (SecurityFilterAutoConfiguration)

FilterChainProxy

  • 보통 "springSecurityFilterChain" 이라는 이름의 빈으로 등록된다.
  • DelegatingFilterProxy

AccessDecisionManager

Access Control 결정을 내리는 인터페이스로, 구현체 3가지를 기본적으로 제공

  • AffirmativeBased : 여러 Voter중에 한명이라도 허용하면 허용, 기본 전략
  • ConsensusBased : 다수결
  • UnanimousBased : 만장일치

AccessDecisionVoter

  • 해당 Authentication이 특정한 Object에 접근할 때 필요한 ConfigAttributes를 만족하는지 확인한다.
  • WebExpressionVoter : 웹 시큐리티에서 사용하는 기본 구현체, ROLE_Xxxx가 매치하는지 확인
  • RoleHierarchyVoter : 계층형 ROLE 지원, ADMIN > MANAGER > USER

FilterSecurityInterceptor

  • AccessDecisionManager를 사용하여 Access Control또는 예외 처리하는 필터.
  • 대부분의 경우 FilterChainProxy에 제일 마지막 필터로 들어있다.

ExceptionTranslationFilter

AuthenticationException

  • AuthenticationEntryPoint 실행
  • AbstractSecurityInterceptor 하위 클래스(예, FilterSecurityInterceptor)에서 발생하는 예외만 처리
  • 그렇다면 UsernamePasswordAuthenticationFilter에서 발생한 인증 에러는? UsernamePasswordAuthenticationFilter 자체에서 처리한다.

AccessDeniedException

  • 익명 사용자라면 AuthenticationEntryPoint 실행
  • 익명 사용자가 아니라면 AccessDeniedHandler에게 위임

정리

DeligatingFilterProxy -> FilterChaninProxy -> 시큐리티 필터 목록들(체인들은 어떻게 만들어지는가? WebSecurity, HttpSecurity를 이용해서 만들어진다. 참고 - WebSecurity 주석) -> 인증 관련된 객체(AuthenticationManager) -> 인가 관련된 객체(AccessDecisionManager) -> SecurityContextHolder -> SecurityContext -> Authentication -> Pricipal, GrantAuthority

 

WebSecurity.java

 

참조 - 스프링 시큐리티(inflearn 백기선님 강의)

728x90
반응형
반응형

Spring에서 @Value 애노테이션은 설정 파일(yml, properties)에 있는 정보를 가져오는데 주로 사용됩니다.

@Value로 설정 파일 값을 가져오는 변수가 null인 경우 점검해보아야 하는 부분을 정리하였습니다. 

 

1. 어느 @Value 애노테이션을 사용했는지 import 확인

lombok이 아닌 spring 애노테이션을 사용하자

import org.springframework.beans.factory.annotation.Value;

 

2. static 변수에는 값을 넣을 수 없다.

예시는 application.properties 파일에서 active.root 값을 Config class에서 path 변수에 저장해서 사용하고 있습니다.

path 변수를 클래스 변수(static 변수)로 지정해서 테스트 해보도록 하겠습니다.

 

Config.java

@Component
public class Config {

    @Value("${active.root}")
    private static String path;

    public String getPath() {
        return path;
    }
}

 

application.properties

active.root=home

 

Test Code

@SpringBootTest
class ConfigServiceTest {

    @Autowired
    Config config;

    @Test
    void findActiveRootTest() {
        String root = "home";
        Assertions.assertEquals(config.getPath(), root);
    }

}

 

Test Code 결과

org.opentest4j.AssertionFailedError: 
Expected :null
Actual   :home

 

null 이 출력되는 것을 확인할 수 있습니다.

Config class에서 static 변수를 일반 전역 변수로 수정하면 문제를 해결됩니다.

 

3. Spring Bean으로 생성된 객체가 아닌 경우

Spring은 애플리케이션 로딩 시점에 스프링 컨테이너 내부에서 모든 빈들을 등록하면서 @Value 애노테이션 안의 값들을 설정 파일에서 읽어들여 변수에 저장합니다.

그 결과 Spring에서 싱글톤으로 관리되는 빈이 아닌 새로운 객체로 생성하게 되면 @Value 애노테이션으로 설정 파일을 읽어들이는 변수는 null로 값이 저장되지 않습니다.

 

Test Code

  - @Autowired를 제거하고 new를 이용해서 Config 인스턴스를 생성하였습니다.

@SpringBootTest
class ConfigServiceTest {

    @Test
    void findActiveRootTest() {

        Config config = new Config();

        String root = "home";
        Assertions.assertEquals(config.getPath(), root);
    }

}

 

Test Code 결과

org.opentest4j.AssertionFailedError: 
Expected :null
Actual   :home

 

Test Code

 - 정상으로 값이 저장되는 경우

 - new 연산자를 이용해서 인스턴스를 생성해서 사용하고 있는 것은 아닌지 확인해 보자!

@SpringBootTest
class ConfigServiceTest {

    @Autowired
    Config config;

    @Test
    void findActiveRootTest() {
        String root = "home";
        Assertions.assertEquals(config.getPath(), root);
    }

}
728x90
반응형
반응형

데코레이터 패턴(Decorator Pattern)

  - 데코레이터 패턴은 프록시 기법을 사용하는 디자인 패턴 중에 하나입니다.

  - GOF 디자인 패턴에서는 의도에 따라 두 가지 패턴으로 구분하였습니다. 

 

프록시의 주요 기능

프록시를 통해서 할 수 있는 일은 크게 2가지로 나뉜다.

1. 접근 제어 (권한에 따른 접근 차단, 캐싱 등)

2. 부가 기능 추가 (다른 로직을 추가하는 등의 부가 기능을 수행, 예) 실행 시간을 측정해서 로그를 남긴다.)

 

* GOF 디자인 패턴에서는 의도에 따라서 프록시 패턴과, 데코레이터 패턴으로 구분한다.

  - 프록시 패턴 : 접근 제어가 목적

  - 데코레이터 패턴 : 새로운 기능 추가가 목적

 

프록시 패턴의 장점

  - 부가 기능을 추가하여서 원하는 흐름에 맞게 조정할 수 있습니다.

 

코드

 * 메시지를 출력하는 기능에 메시지를 꾸며주는 기능을 데코레이터 패턴을 이용해서 적용해 보겠습니다.

 

Component.java

  - interface로 Component 생성

 

public interface Component {
    String operation();

}

 

RealComponent.java

  - 실제 Component 구현체

 

@Slf4j
public class RealComponent implements Component{
    @Override
    public String operation() {
        log.info("RealComponent 실행");
        return "data";
    }
}

 

DecoratorPatternClient.java

  - Component를 실행하는 클라이언트 클래스

 

@Slf4j
public class DecoratorPatternClient {

    private Component component;

    public DecoratorPatternClient(Component component) {
        this.component = component;
    }

    public void execute() {
        String result = component.operation();
        log.info("result={}", result);
    }

}

 

DecoratorPatternTest.java

  - 기본 메시지 출력 기능 테스트

@Slf4j
public class DecoratorPatternTest {

    @Test
    void noDecorator() {
        Component realComponent = new RealComponent();
        DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
        client.execute();
    }
}

결과 : RealComponent 실행

          result=data

 

MessageDecorator.java

  - 데코레이터 패턴을 이용해서 메시지 꾸며주는 기능 추가

  - MessageDecorator는 Component 인터페이스를 구현한다.

 

@Slf4j
public class MessageDecorator implements Component{

    private Component component;

    public MessageDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {
        log.info("MessageDecorator 실행");

        String result = component.operation();
        String decoResult = "<< =====" + result + " ===== >>";
        log.info("MessageDecorator 꾸미기 적용 전={}, 적용 후 ={}",result, decoResult);
        return decoResult;
    }
}

 

DecoratorPatterTest.java

  - 클라이언트가 호출하기 전에 MessageDecorator 클래스를 이용해서 부가 기능을 추가 후 클라이언트에게 messageDecorator를 넘겨준다. messageDecorator도 Component를 구현하고 있기 때문에 클라이언트 입장에서는 전혀 문제가 발생하지 않는다.

 

@Slf4j
public class DecoratorPatternTest {

    @Test
    void decorator1() {
        Component realComponent = new RealComponent();
        Component messageDecorator = new MessageDecorator(realComponent);
        DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
        client.execute();
    }
}

 

결과 :

 

정리 :

 

데코레이터 패턴의 의도 : 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공

프록시 패턴의 의도 : 다른 객체에 대한 접근을 제어하기 위해 대리자를 제공

728x90
반응형
반응형

전략 패턴

탬플릿 메서드 패턴은 부모 클래스에 변하지 않는 템플릿을 두고, 변하는 부분을 자식 클래스에 두어서 상속을 사용해서 문제를 해결하였습니다.
전략 패턴은 변하지 않는 부분을 Context 라는 곳에 두고, 변하는 부분을 Strategy 라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결합니다.

상속이 아니라 위임으로 문제를 해결하는 것이다.

전략 패턴에서 Context 는 변하지 않는 템플릿 역할을 하고, Strategy 는 변하는 알고리즘 역할을 합니다.

 

전략 패턴 의도

알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만들자.

전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있습니다.

 

코드

방식1. 필드에 전략을 보관하는 방식

 ContextV1.java

 

@Slf4j
public class ContextV1 {

    private Strategy strategy; // 전략을 필드 변수로 선언

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        long startTime = System.currentTimeMillis();
        //비지니스 로직 실행
        strategy.call(); //위임
        //비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}

 

변하는 알고리즘

 StrategyLogin1.java

 

@Slf4j
public class StrategyLogin1 implements Strategy {

    @Override
    public void call() {
        log.info("비즈니스 로직1 실행");
    }
}

 

 StrategyLogin2.java

 

@Slf4j
public class StrategyLogin2 implements Strategy {

    @Override
    public void call() {
        log.info("비즈니스 로직2 실행");
    }
}

 

테스트 코드

ContextV1Test.java

 

@Slf4j
public class ContextV1Test {

    @Test
    void strategyV1() {
        StrategyLogin1 strategyLogin1 = new StrategyLogin1();
        ContextV1 contextV1 = new ContextV1(strategyLogin1);
        contextV1.execute();

        StrategyLogin2 strategyLogin2 = new StrategyLogin2();
        ContextV1 contextV2 = new ContextV1(strategyLogin2);
        contextV2.execute();
    }

}

 

전략 패턴 사용은 선 조립, 후 실행으로 생각하면 보다 간단합니다.

변하지 않는 코드 ContextV1 객체에 변하는 알고리즘인 StrategyLogin1, StrategyLogin2를 생성자에 넣어서 조립하고, 호출이 필요한 시점에 execute()를 이용해서 실행합니다.
스프링 애플리케이션 개발 할 때 애플리케이션 로딩 시점에 의존관계 주입을 통해 필요한 의존관계를 모두 맺어두고
다음에 실제 요청을 처리하는 것과 같은 원리이다.

 

위와 같이 사용하게 되면 변하는 알고리즘인 전략 class가(StrategyLogin1, StrategyLogin2) 계속 생겨날 수밖에 없습니다.

이러한 점을 보완하기 위해서 익명 내부 클래스를 사용합니다.

 

익명 내부 클래스 사용

 

@Slf4j
public class ContextV1Test {

    @Test
    void strategyV2() {
        Strategy strategyLogic1 = new Strategy() {

            @Override
            public void call() {
                log.info("비즈니스 로직1 실행");
            }
        };
        ContextV1 contextV1 = new ContextV1(strategyLogic1);
        contextV1.execute();

        Strategy strategyLogic2 = new Strategy() {

            @Override
            public void call() {
                log.info("비즈니스 로직2 실행");
            }
        };
        ContextV1 contextV2 = new ContextV1(strategyLogic2);
        contextV2.execute();

    }

}

 

람다를 이용해서 보다 더 코드 수를 줄일 수 있습니다.

 

@Slf4j
public class ContextV1Test {

    @Test
    void strategyV3() {
        ContextV1 contextV1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
        contextV1.execute();

        ContextV1 contextV2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
        contextV2.execute();

    }

}

 

방식2. 필드에 전략을 보관하는 방식

 

전략을 파라미터로 전달 받는 방식, 실행할 때 마다 전략을 유연하게 변경할 수 있다.
단점 역시 실행할 때 마다 전략을 계속 지정해주어야 한다.

 

ContextV2.java

 

@Slf4j
public class ContextV2 {

    public void execute(Strategy strategy) {
        long startTime = System.currentTimeMillis();
        //비지니스 로직 실행
        strategy.call(); //위임
        //비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}

 

테스트 코드

ContextV2Test.java

 

@Slf4j
public class ContextV2Test {


    /**
     * 전략 패턴 적용
     */
    @Test
    void strategyV1() {
        ContextV2 context = new ContextV2();
        context.execute(new StrategyLogin1());
        context.execute(new StrategyLogin2());
    }

    /**
     * 전략 패턴 익명 내부 클래스
     */
    @Test
    void strategyV2() {
        ContextV2 context = new ContextV2();
        context.execute(new Strategy() {
            @Override
            public void call() {
                log.info("비즈니스 로직 1 실행");
            }
        });
        context.execute(new Strategy() {
            @Override
            public void call() {
                log.info("비즈니스 로직 2 실행");
            }
        });
    }

    /**
     * 전략 패턴 익명 내부 클래스2, 람다
     */
    @Test
    void strategyV3() {
        ContextV2 context = new ContextV2();
        context.execute(() -> log.info("비즈니스 로직 1 실행"));
        context.execute(() -> log.info("비즈니스 로직 2 실행"));
    }

}

 

참고로 ContextV2 형식의 전략 패턴은 스프링 내부에서 템플릿 콜백 패턴이라고 부른다.

스프링에서 jdbcTemplate, RestTemplate, ~Template 처럼 다양한 템플릿 콜백 패턴이 사용된다.

 

 

참조

 - inflearn(스프링 핵심 원리 - 고급편, 김영한)

728x90
반응형
반응형

이번 시간에는 Spring Framework에서 동시성 문제를 해결해 줄 ThreadLocal에 대해서 알아보겠습니다.

스프링은 빈을 싱글톤으로 등록한다. 애플리케이션에 딱 1개만 존재한다는 뜻이다.

이렇게 하나만 있는 인스턴스에 하나의 필드에 여러 쓰레드가 동시에 접근하여 수정하게 되면 A 사용자가 B 사용자의 데이터를 보게 되는 사고가 발생될 수 있다. 아래 목차와 같이 순서대로 동시성 원인부터 해결 방안까지 알아보겠습니다. 


<목차>

1. 동시성 문제 발생하는 예시

2. ThreadLocal을 이용해서 동시성 문제 해결 방안

3. ThreadLocal의 주의사항

 


1. 동시성 문제 발생하는 예시

FieldService.java

  1. nameStore을 맴버변수로 선언

  2. nameStore 변수에 값을 저장하고 1초 뒤에 저장한 nameStore 출력

 

@Slf4j
public 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();
        }
    }

}

 

FieldServiceTest.java

  1. threadA.setName("thread-A");   -- Thread 이름 설정

  2. Thread A 시작하고 2초 잠시 멈추기 때문에 동시성 발생 안함.

  3. Thread B 시작하고 sleep(2000) 안하면 쓰레드 끝나기 전에 테스트 코드가 종료되서 모든 로그가 출력이 안되서 추가해야 한다.

 

@Slf4j
public class FieldServiceTest {

    private FieldService fieldService = new FieldService();

    @Test
    void field() {
        log.info("main start");

        Runnable userA = () -> {
            fieldService.logic("userA");
        };
        Runnable userB = () -> {
            fieldService.logic("userB");
        };

        Thread threadA = new Thread(userA);
        threadA.setName("thread-A"); // 1. thread 이름 설정
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        sleep(2000); // 2. 동시성 문제 발생x
        threadB.start();

        sleep(2000); // 3. 메인 쓰레드 종료 대기
        log.info("main exit");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

 

TestCode 결과

  1. thead-A, thread-B 위에서 설정한 이름

  2. 정상적으로 thread-A에 userA thread-B에 userB 출력

 

 

동시성 문제 발생

FieldServiceTest.java

  1. ThreadA가 시작하고 0.1초만에 ThreadB 접근

 

@Slf4j
public class FieldServiceTest {

    private FieldService fieldService = new FieldService();

    @Test
    void field() {
        log.info("main start");

        Runnable userA = () -> {
            fieldService.logic("userA");
        };
        Runnable userB = () -> {
            fieldService.logic("userB");
        };

        Thread threadA = new Thread(userA);
        threadA.setName("thread-A");
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        sleep(100); // 1. 동시성 문제 발생
        threadB.start();

        sleep(2000);
        log.info("main exit");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

 

TestCode 결과

  1. thread-A에 조회할 때 그 전에 thread-B가 nameStore을 수정해서 동시성 문제 발생 

 

 

 


2. ThreadLocal을 이용해서 동시성 문제 해결 방안

ThreadLocal은 해당 쓰레드만 접근할 수 있는 별도의 특별한 내부 저장소이다.

 

ThreadLocalService.java

  1. String nameStore -> ThreadLocal<String> nameStore 변경

  2. nameStore.set() - 값 저장

      nameStore.get() - 값 조회

      nameStore.remove() - 값 제거

 

@Slf4j
public 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();
        }
    }

}

 

ThreadLocalServiceTest.java

  1. FieldService만 ThreadLocalService로 수정

 

@Slf4j
public class ThreadLocalServiceTest {

    private ThreadLocalService threadLocal = new ThreadLocalService();

    @Test
    void field() {
        log.info("main start");

        Runnable userA = () -> {
            threadLocal.logic("userA");
        };
        Runnable userB = () -> {
            threadLocal.logic("userB");
        };

        Thread threadA = new Thread(userA);
        threadA.setName("thread-A");
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        sleep(100); // 동시성 문제 발생x
        threadB.start();

        sleep(2000); // 메인 쓰레드 종료 대기
        log.info("main exit");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

 

TestCode 결과

  1. ThreadLocal이라는 별도의 쓰레드 저장소를 사용함으로써 동시성 문제를 해결하였습니다.

 

 


3. ThreadLocal의 주의사항

ThreadLocal 값을 사용 후 제거하지 않고 그냥 두면 WAS에서 쓰레드 풀을 사용하는 경우 심각한 문제가 발생할 수 있다.

보통 was는 쓰레드 생성 비용이 비싸기 때문에 쓰레드를 사용하면 반납하고 재사용하게 된다. 

그러므로 thead-A를 사용하고 쓰레드 값을 제거하지 않을 상태에서 반납 후 다른 사용자 HTTP 요청이 thead-A에 할당되면 thead-A 보관소에 그대로 값이 있어서 다른 사용자에게 잘못된 정보가 노출될 수 있다.

이런 문제를 예방하기 위해서는 사용자 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove()를 통해서 꼭 제거해야 한다.

* Spring Framework 같은 경우 Filter나 Interceptor를 이용해서 깔끔하게 제거해 주는 것이 안전하다.

 

 

 

참조

  - inflearn 강의(스프링 핵심 원리 - 고급편, 김영한)

728x90
반응형
반응형

 

SpringBoot에서 쉽고 빠르게 Interceptor를 적용하는 방법을 알아보겠습니다.

스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술입니다.

서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술입니다.

둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다릅니다.

 

Spring에서 서블릿 필터를 적용하고 싶다면 아래 링크 클릭!

[Spring Filter 적용하기]

인터셉터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿(dispatcherservlet) -> 스프링 인터셉터 -> 컨트롤러

인터셉터 특징

 1. 스프링 인터셉터에 URL 패턴을 적용할 수 있는데, 서블릿 URL 패턴과는 다르고, 매우 정밀하게 설정할 수 있습니다.

 2. 서블릿 필터의 경우 단순하게 doFilter() 하나만 제공되지만 인터셉터는 컨트롤러 호출 전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 단계적으로 잘 세분화 되어 있다.

  ** (예외 발생 시 postHandle은 실행이 안되고, preHandle, afterCompletion만 실행된다.)

 3. 서블릿 필터의 경우 단순히 request , response 만 제공했지만, 인터셉터는 어떤 컨트롤러( handler )가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있다.

 

소스 코드

TestInterceptor.java

  - HadlerInterceptor를 구현하면 Spring Interceptor를 사용할 수 있다.

 

package com.bumblebee.dailyspecial.domain.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class TestInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle");
        String requestURI = request.getRequestURI();
        log.info(requestURI);
        return true; // return true면 정상 호출, 다음 인터셉터나 컨트롤러 호출한다.
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion");
    }
}

 

위에서 작성한 인터셉터를 등록해줘야 합니다.

WebConfig.java

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new TestInterceptor())
                .order(1) // Interceptor 적용 순서
                // filter와 다르게 매우 정밀하게 url 패턴 적용이 가능하다.
                .addPathPatterns("/**") // 적용될 url 패턴
                .excludePathPatterns("/css/**", "/*.ico", "/error"); //제외될 url 패턴
    }
}

 

TestInterceptor 출력 화면

 

 

 

 ** Interceptor 주의 사항

서블릿 필터의 경우 (init, doFilter, destroy) 메소드에서 공유할 변수를 지역변수로 해결이 가능하지만,

스프링 인터셉터는 호출 시점이 완전히 분리되어 있습니다.

따라서 preHandle 에서 지정한 값을 postHandle , afterCompletion 에서 함께 사용하려면 어딘가에 담아두어야 합니다. TestInterceptor 도 싱글톤 처럼 사용되기 때문에 맴버변수를 사용하면 위험합니다.

따라서 request 에 담아두었다가( request.setAttribute(test, 123) ).

이 값은 afterCompletion 에서 request.getAttribute(test) 로 찾아서 사용하면 됩니다.

 

728x90
반응형

+ Recent posts