로그인 상태를 유지하는 방법

 

1.  쿠키

HTTP 응답 시 쿠키에 데이터를 담아서 브라우저에 전달하면 브라우저의 쿠키 저장소에 쿠키가 저장이 되어 이후 요청을 보낼 때 쿠키를 같이 전송한다.

쿠키를 생성할 때는 일정 시간동안 유지되는 영속 쿠키, 브라우저 종료시까지 유지되는 세션 쿠키(만료 날짜 생략)가 있다. 또한 @CookieValue 애노테이션을 사용하면 편리하게 쿠키를 조회할 수 있다.

 

쿠키의 문제점

클라이언트가 쿠키 값을 임의로 변경할 수 있기 때문에 보안에 취약하다.

쿠키에는 임의의 예측 불가능한 값의 토큰을 생성해서 노출하고 서버에서는 해당 토큰을 매핑해서 중요한 데이터를 관리하는 방법이 있다.

이렇게 서버에 중요한 데이터를 보관하고 연결을 유지하는 방법을 세션이라 한다.

 

2.  세션

세션을 생성하고 sessionId로 쿠키를 생성해서 클라이언트에 전달하면, 클라이언트는 쿠키를 보관하면서 이후 요청 시 쿠키를 통해 세션 저장소에 저장된 값을 조회할 수 있고 세션이 만료되면 연결이 끊어지게 된다.

서블릿이 제공하는 HttpSession은 JSESSIONID 라는 이름의 쿠키를 생성하고 값은 추정 불가능한 랜덤 값을 가진다.

getSession(boolean create) 의 경우 기본 값이 true 옵션이라 로그인 확인을 하려면 false로 바꾸는 것이 좋다.

//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
//getSession(boolean create)
// true : 세션이 없으면 새로 생성해서 반환, false : 세션이 없으면 null 반환
HttpSession session = request.getSession(default: true);

//세션에 로그인 회원 정보 보관
session.setAttribute("loginMember", loginMember);

 

스프링이 지원하는 @SessionAttribute를 사용하면 직접 생성할 필요 없이 다음과 같이 편하게 사용할 수 있다.

 

@GetMapping("/login")
public String homeLogin (
	@SessionAttribute("loginMember", required = false) Member loginMember, Model model) {
	...
}

 

세션은 session.invalidate() 를 호출하면 삭제가 되는데 HTTP는 비연결성이기 때문에 로그아웃을 누르지 않고 브라우저를 종료할 경우 세션 데이터가 삭제되지 않고 남아있게 된다.

세션기본적으로 메모리에 생성되기 때문에 필요한 경우에 생성했다 삭제하는 것이 좋은데 서블릿의 HttpSession은 클라이언트에서 서버에 마지막으로 요청한 시간을 기준으로 일정 시간동안 세션을 유지하는 방법을 사용한다. (기본 설정 30분, 만료 시 WAS가 내부에서 해당 세션을 제거)

특정 세션 단위로 시간을 설정하거나 스프링부트를 통해 application.properties로 글로벌 설정을 할 수 있다.

 

// 특정 세션에 적용
session.setMaxInactiveInterval(1800);

// application.properties
server.servlet.session.timeout=1800

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

Validation

 

기존 클라이언트에서 전송한 데이터 오류 발생 시 문제점

 

뷰 템플릿에서 중복으로 처리할 것이 많고 숫자 타입에 문자가 입력되는 등의 타입 오류 시 컨트롤러에 진입하기 전에 예외 발생한다.

타입 오류 발생 시에 이전에 입력한 값을 보여주기 힘들다.

 

 

BindingResult

 

스프링이 제공하는 인터페이스(Error 인터페이스 상속)로 @ModelAttribute에 데이터 바인딩 오류가 발생해도 컨트롤러가 호출된다.

addError() 메서드의 파라미터로 오류 정보를 담은 FiledError(), ObjectError()를 생성한다.

FiledError, ObjectError의 생성자는 errorCode, arguments를 가지는데 이는 오류 메시지를 생성할 때 사용된다.

 

@PostMapping("/add")
public String createItem(@ModelAttribute Item item, BindingResult bindingResult) {
    if (!StringUtils.hasText(item.getItemName())) {
                bindingResult.addError(new FieldError("item", "itemName",
        item.getItemName(), false, new String[]{"required.item.itemName"}, null,
        null));
}

errors.properties 파일을 별도로 생성 (메시지 방식과 동일함)

<!-- errors.properties -->
required.item.itemName=상품명을 입력하세요.

 

rejectValue() 를 사용하면 FieldError를 직접 생성하지 않고 오류를 관리할 수 있다.

내부적으로 MessageCodeResolver가 동작하면서 errorCode를 보고 errors.properties에서 오류 메시지를 찾는데

객체, 필드명, errorCode를 조합해서 구체적인 것에서 덜 구체적인 순으로 조회를 한다.

 

//void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
if (!StringUtils.hasText(item.getItemName())) {
	/**
    *	우선 순위
    *	1. required.item.itemName
    *	2. required.itemName
    *	3. required.java.lang.String
    *	4. required
    */
	bindingResult.rejectValue("itemName", "required");
}

 

타임리프

 

타임리프는 BindingResult를 활용해서 편리하게 검증 오류를 표현하는 기능 제공한다.

  • #fields : BindingResult 가 제공하는 검증 오류에 접근할 수 있다. (th:if="${#fields.hasGlobalErrors()}")
  • th:errors : 해당 필드에 오류가 있는 경우에만 태그를 출력한다. (th:if 편의 버전)
  • th:errorclass : th:field 에서 지정한 필드에 오류가 있는 경우에만 class 정보를 추가한다.
  • th:filed : 평상시 model 객체의 값을 사용, 오류 발생 시 FieldError에 보관한 값(rejectedValue)을 사용

 


 

Validator

BindingResult의 rejectValue()로 편해지긴 했지만 컨트롤러에서 작성하는 검증 로직이 매우 복잡하다.

Validator를 사용해서 검증 로직을 별도의 클래스로 분리 가능한데, Validator를 사용해서 컨트롤러에서 검증 로직을 분리시켜도 검증 코드를 하나하나 작성하는 건 변하지 않는다.

Bean Validation을 사용하면 검증 로직을 애노테이션으로 깔끔하게 해결 가능하다.

 

Bean Validation

Bean Validation 2.0(JSR-380) 표준 기술로, 실무에서는 보통 하이버네이트 validator(구현체)를 사용한다.

  • build.gradle 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'

 

'spring-boot-starter-validation' 라이브러리를 추가하면 스프링 부트는 자동으로 Bean Validator를 스프링에 통합해준다.

필드 위에 검증 애노테이션을 입력만 하면 Bean Validation이 오류 발생 시 FeildError를 자동 생성한다.

스프링 부트는 Bean Validation을 글로벌 Validator로 사용하기 때문에 @ModelAttribute 앞에 @Validated만 추가하면 적용이 된다.

 

@Data
public class Item {
    private Long id;
    
    @NotBlank
    private String itemName;
    
    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;
}
@PostMapping("/add")
    public String addItem(@Validated @ModelAttribute Item item, BindingResult
bindingResult) {
	...
}

 

먼저 @ModelAttribute 각각의 필드에 타입 변환이 되면 Bean Validation이 적용되고 타입 변환에 실패할 경우 해당 필드는 typeMismatch(errorCode)로 FiledError가 생성된다. (나머지 필드는 Bean Validation 검증 시작)

 

Bean Validation은 검증 오류 발생 시 애노테이션명을 errorCode로 사용해서 FieldError 생성하기 때문에 errors.properties에 해당 애노테이션명(errorCode)로 오류 메시지를 추가해서 활용 가능하다.

 

NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank

Range.item.price
Range.price
Range.java.lang.Integer
Range

처음 바인딩 되는 {0}은 필드명, {1}, {2}.. 는 애노테이션마다 다르다. (@Range의 경우 가격 범위 바인딩)

//Bean Validation 추가

NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용

 

ObjectError의 경우는 bean Validation 적용이 복잡해서 bindingResult.reject() 로 직접 생성해주는 것이 좋다. 

Bean Validation은 각각의 경우에 따라 검증 방식을 나눌 수 있는 groups 기능을 제공하지만 별도의 폼 객체(saveForm, updateForm.. )를 만들어서 사용하는 것이 좋다.

 

 

HttpMessageConverter(@RequestBody)에도 적용이 가능한데 FieldError 객체를 JSON으로 변환해서 전달한다. (필요한 데이터만 뽑아서 별도의 API 스펙으로 객체를 만들어서 반환)

@ModelAttribute는 @Valiated 적용 시 각각의 필드에 세밀하게 적용이 되어 특정 필드에 타입 오류가 발생해도 나머지 필드는 정상 처리가 된다.

반면, HttpMessageConverter전체 객체 단위로 적용이 되기 때문에 하나라도 문제가 생기면 예외가 발생하고 컨트롤러도 호출되지 않는다.

서블릿 (Servlet)

  • 동적 웹 페이지를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술
  • 웹 요청과 응답의 흐름을 간단한 메서드 호출만으로 체계적으로 다룰 수 있게 지원
  • HTTP 요청을 통해 매핑된 URL이 호출되면 service() 메서드를 실행
  • HTTP 요청, 응답 메시지를 편리하게 사용할 수 있도록 도와주는 HttpServletRequest, HttpServletResponse 객체

 

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
	@Override
    protected void service(HttpServletRequest request, HttpServletResponse
  response) throws ServletException, IOException {
  	...
   
  }
}

 

HTTP 통신 과정 변화

  • 서버 TCP/IP 연결 대기, 소켓 연결
  • HTTP 요청 메시지 파싱 (헤더부, 바디부 ..)Request 객체
  • 비즈니스 로직
  • HTTP 응답 메시지 생성Response 객체
  • TCP/IP 응답 전달, 소켓 종료

 

MVC 패턴

  • 비즈니스 로직과 뷰 렌더링을 한번에 처리 -> 너무 많은 역할
  • 비즈니스 로직을 수정하는 일과 UI를 수정하는 일은 각각 다르게 발생할 경우가 많고 대부분 서로에게 영향을 주지 않음
  • MVC 패턴Controller와 View 영역으로 서로 역할을 나눈 것
  • Controller : HTTP 요청을 받아서 파라미터 검증, 비즈니스 로직(Service 계층), View에 전달할 데이터를 Model에 저장
  • Model : View에 전달할 데이터를 저장
  • View : Model에 담겨있는 데이터를 사용해서 화면을 렌더링하는데 집중

 

Servlet + 기본 MVC 패턴의 문제점

  • View로 이동하는 dispatcher.forward(request, response) 코드 중복 호출
  • 모든 컨트롤러에 Servlet 선언 (종속성)
  • 공통 처리가 어려움 -> 컨트롤러 호출 전에 공통 기능을 처리해줄 무언가 필요 (프론트 컨트롤러)

 

프론트 컨트롤러

  • 스프링 MVC의 DispatcherServlet 역할
  • 하나의 서블릿으로 사용 (공통 처리)
  • URL 요청을 전부 이곳에서 받아 요청에 맞는 컨트롤러를 호출 -> 서블릿 종속성 제거
  • 각각의 컨트롤러에서 View 네임을 반환하면 프론트 컨트롤러에서 경로(viewPath)를 생성 -> 코드 중복 제거
  • 프론트 컨트롤러에서만 서블릿 선언을 해서 나머지 컨트롤러에서 서블릿 종속성 제거
  • 모든 컨트롤러에서 Model을 반환하지 않도록 프론트 컨트롤러에서 따로 Model 객체를 생성해서 전달
  • 여러 컨트롤러를 처리할 수 있도록 핸들러 어댑터 생성

 

@WebServlet(name = "FrontControllerSerlvetV5", urlPatterns = "/front-controller/v/*")
public class FrontControllerSerlvetV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    // 핸들러 매핑 등록, 핸들러 어댑터 등록
    public FrontControllerSerlvetV5() {
        initHandlerMappingMap();
        initHandlerAdapters();
    }


    // 핸들러(컨트롤러) 초기화
    private void initHandlerMappingMap() {
    	//v3 버전 컨트롤러
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

		//v4 버전 컨트롤러
        handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

    // 어댑터 초기화
    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 핸들러 조회
        Object handler = getHandler(request);

        // 요청 URL에 맞는 핸들러 객체가 없으면 종료
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 3. 핸들러에 맞는 핸들러 어댑터 조회
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        // 5. 핸들러 어댑터에 전달
        // 어댑터는 request 파라미터를 Map 객체에 담고 model 객체를 따로 생성해서 전달
        // 핸들러는 Servlet 종속성이 제거되고 ViewName만 반환하면 됨
        ModelView mv = adapter.handle(request, response, handler);

        // 6. ViewName(논리 이름)으로 View Resolver(풀 경로 생성) 실행
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

        // 7. View 렌더링
        view.render(mv.getModel(), request, response);
    }

    // 2. 요청 URL에 맞는 핸들러 객체를 handlerMappingMap에서 조회, 핸들러 객체 반환
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    // 4. 해당 핸들러를 지원하는 핸들러 어댑터가 있는지 확인
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("Error");
    }

    //ViewPass생성
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

 

[참고]인프런 김영한님 강의를 공부한 내용입니다.

빈 생명주기 콜백

  • 스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음 초기화 작업을 할 수 있는 준비가 된다.
  • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 기능을 제공한다.
  • 또한 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 주어 안전하게 종료 작업을 할 수 있게 지원한다.

 

스프링이 지원하는 빈 생명주기 콜백 방법

 

1. 인터페이스

  • InitializingBean, DisposableBean
  • 스프링 초창기에 주로 사용하던 방법
  • 스프링 인터페이스에 의존적, 외부 라이브러리 적용 불가 등의 단점

 

2. 설정 정보

  • @Bean(initMethod = "init", destroyMethod = "close")
  • 메서드 이름 자유롭게 사용, 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 적용 가능
  • destroyMehtod의 경우 따로 적지 않아도 close, shutdown 이라는 메서드 이름을 추론해서 호출

 

3. 애노테이션

  • @PostConstruct, @PreDestroy
  • 자바 표준 기술로 가장 권장하는 방법
  • 유일한 단점으로는 외부 라이브러리에 적용 못하기 때문에 이 경우는 @Bean 설정 정보 사용

 

 

빈 스코프

  • 생성된 빈이 존재할 수 있는 범위
  • 일반적으로 스프링 빈은 싱글톤 스코프로 생성
  • @Scope

 

스프링이 지원하는 스코프

  • 싱글톤 : 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 기본 스코프
  • 프로토 타입 : 스프링 컨테이너가 의존관계 주입, 초기화까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • 웹 스코프 : request, session, application 등 특정 웹 요청까지 유지되는 스코프

 

프로토타입 빈

 

1. 특징

  • 스프링 컨테이너 생성 시점에 초기화가 되는 싱글톤 빈과 달리, 프로토타입 빈은 스프링 컨테이너에서 빈은 조회할 때 생성
  • 조회를 할 때마다 새로운 스프링 빈이 생성, 초기화
  • 프로토타입 빈을 조회한 클라이언트가 해당 빈을 관리

 

2. 문제점

  • 스프링 컨테이너 의존관계 주입 시점에 싱글톤 빈에 프로토타입 빈이 생성되어 주입
  • 이후 클라이언트가 프로토타입 빈의 메서드 요청 시 새로운 프로토타입 빈이 생성되는 것이 아니라 기존 빈 호출
  • 프로토타입 빈이 싱글톤 빈과 함께 계속 유지되는 문제 발생

 

3. 해결 방안

  • 의존관계 주입(D.I)이 아닌 의존관계 조회(Depengency Lookup)을 통해 필요시마다 프로토타입 빈을 새로 조회, 생성해야 한다.
  • 스프링이 제공하는 ObjectProvider (기존 ObjectFactory + 편의 기능 추가)
  • 지정한 빈을 컨테이너에서 대신 찾아주는 D.L 기능 제공
  • getObject() 메서드를 통해 항상 새로운 프로토타입 빈이 생성
  • 스프링에 의존적이지만 기능이 단순해서 단위테스트, mock 테스트 만들기 편리
  • 싱글톤 스코프 빈을 거의 사용하기 때문에 직접적으로 프로토타입 빈을 사용할 경우는 적음 

 

 

스코프와 프록시

 

  • @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
  • 스프링 컨테이너가 CGLIB (바이트코드를 조작하는 라이브러리)를 사용해서 가짜 프록시 객체를 생성
  • 실제 객체를 상속받은 프록시 객체를 스프링 빈으로 등록, 싱글톤 빈처럼 사용
  • 싱글톤을 사용하는 것 같지만 실제로는 다르게 동작하므로 주의해서 사용
  • 프록시 객체는 요청이 올 경우 그 때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
  • ObjectProvider를 사용하든, Proxy를 사용하든 핵심은 실제 객체 조회를 필요한 시점까지 지연 처리 한다는 점

 

 

 

참고(https://www.inflearn.com/users/@yh)

+ Recent posts