컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만, 사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용할 수 있다. 

 

주위! 

메세지 컨버터(HttpMessageConverter)에는 컨버전 서비스가 적용되지 않는다. 

특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 이 부분을 많이 오해하는데, HttpMessageConveter의 역할은 Http메세지 바디의 내용을 객체를 변환하거나 객체를 HTTP메세지 바디에 입력하는 것이다.

 

메세지 컨버터에서 컨버팅할때 제공하는 포멧팅 방식을 써야한다. 

예) JSON -> Jackson과 같은 라이브러리를 사용해야한다. 

 

컨버전 서비스는 @RequestParam @ModelAttribute @PathVariable, 뷰 템플릿 등에서 사용할 수 있다. 

 

스프링 타입 컨버터 

타입을 코드를 통해 바꿔줘야한다 -> 고생을 많이했다 

 

스프링 MVC

@RequestParam 

을 사용해서 컨버터를 해준다 이를 사용하기 위해서는 중간에 스프링 컨버터서비스를 썼기때문에 사용 가능 

 

 

컨버터 인터페이스 

타입컨버터는 스프링을 다른 타입으로 바꾸던 다 바꿀 수 있다. 

-> 굉장히 범용적인 인터페이스이다. 

 

 

컨버전 서비스 

Conveter

ConvbeterFactory 

GenericConverter

ConditionalGenericConverter  

 

여러가지 컨버터를 묶어서 관리 사용할때 간단히 호출해서 사용하면 된다. 

 

 

컨버터 서비스와 컨버터 레지스트리 두가지 서비스로 제공한다. 

 

스프링에서 컨버터를 등록해주기 위해서는 WebMvcConfigurer를  addConvter를 쭉 등록해주면 된다. 

(아래 코드 있음) 

 

 

 

 

포맷터 - Formatter 

 

Converter는 입력과 출력 타입에 제한이 없는, 범용 타입 변환 기능을 제공한다.

이번에는 일반적인 웹 애플리케이션 환경을 생각해보면 불린 타입을 숫자로 바꾼는 것 같은 범용 기능 보다는 개발자는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환 하는 상황이 대부분이다. 

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        //주석처리 우선순위
        //registry.addConverter(new StringToIpPortConverter());
        //registry.addConverter(new IntegerToStringConverter());
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());


        //추가
        registry.addFormatter(new MyNumberFormatter());
        
    }
}

 

 

위에 보면은 주석처리를 해줬는데 그 이유는 컨버터가 먼저 우선순위를 먹기 때문이다. 

 

 

뷰템플릿에도 등록이 가능한데 괄호 2개르 하더나 폼에서 필드를 적용하면 자동으로 등록이 된다. 

 

 

Converter VS Formatter 

Converter는 범용(객체 -> 객체) 

Formatter는 문자에 특화 (객체 -> 문자, 문자 -> 객체) + 현지화(Locale) 

-> Converter의 특별한 버전 

 

  @Data
    static class Form {
        @NumberFormat(pattern = "###,###")
        private Integer number;

        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime localDateTime;

    }

 

이런식으로 포멧을 정해서 사용할 수 있다. 

스프링이 아닌 순수 서블릿 컨테이너는 어떻게 리 

 

서블릿은 다음 2가지 방식 예외처리 

exception 

response.sendError(Http상태 코드, 오류메세지) 

 

 

Exception 

자바 직접 실행 

자바의 메인 메서드를 직접 실행하는 경우 main이라는 이름의 쓰레드가 실행 

실행도중 예외를 잡지 못하고 처음 실행한 main()메서드를 넘어서 예외가 던져지면 예외정보를 남기고 해당 쓰레드는 종료된다. 

 

웹 어플리케이션 

사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 

애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 잡아서 예외를 처리하면 아무런 문제가 없다. 

 

was <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생) 

 

결국 톰캣같은 was까지 예외가 전달된다. was는 예외가 올라오면 어떻게 처리? 

 

 

오류 화면 제공 - 친절하게 고객에게 보여줄 수 있다. 

 

 

컬럼 셀렉션 모드 -> alt + shift + insert 

한번에 변수 끝까지 선택 -> ctrl + shift + -> 

 

이렇게 예외처리를 하면 필터가 두번 발생한다. 

로그인 인증 체크 할때 사용했기 대문에 또 사용하게 되면은 비효율 적이다. 

 

서블릿은 이렇ㅁㄴ 문제를 해결하기 위해 dispachertype이라는 추가 정보를 제공한다. 

 

dispatcherType 

필터의 경우는 dispatcherTypes라는 옵션을 제공한다. 

 

출력해보면 dispatchertype=error로 나오는것을 확인할 수 있다. 

 

dispatchertypeRequest 

서블릿 

 

prehandle에서 에러가 터지면 postHandle은 호출이 안된다 하지만 afterCompletion은 항상 호출이 된다. 

 

 

@API 예외처리 

API는 생각할 내용이 더 많은데, 오류 페이지는 단순히 고객에게 오류 화면을 보여주고 끝이지만, API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 한다. 

 

 

- @ExceptionHandler 

API Exception Version2 Codes below 

@ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

 

위에 코드에서 return ErrorResult 에서 호출하고 정상흐름으로 끝이 나버린다. 

 

 

 @ExceptionHandler
    public ResponseEntity<ErrorResult> userHanlder(UserException e) {
        log.error("[exceptionHandler] ex", e);
        ErrorResult errorResult = new ErrorResult("USER_EX", e.getMessage());
        return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
    }

 

위의 코드는 회원 예외를 처리하는 코드이다. 위에서 ResponseEntity란 무엇인가? 생각해보자 

 

ResponseEntity는 HttpEntity를 상속받음으로써 HttpHeader와 Body를 가질 수 있다. 이러한 값들을 통해 좀 더 세밀하고 견고한 API 예외처리를 할 수 있다. 

 

 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e){
        log.error("exceptionHandler] ex", e);
        return new ErrorResult("EX", "내부 오류");
    }

 

 

 

ExceptionHandler의 예외 처리 방법 

@ExceptionHanlder애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출 된다. 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다. 

 

 

@ControllerAdvice 

@ExceptionHanlder를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있다. @ControllerAdvice또는 @RestControllerAdvice를 사용하면 둘을 분리 할 수 있다. 

 

@ControllerAdvice는 대상을 지정하지 않으면 모든 컨트롤러에 적용된다 (글로벌 적용_ 

@RestControllerAdvice는 @ControllerAdvice와 같고 @ResponseBody가 추가되어 있다. @Controller @RestController의 차이와 같다.    

 

 

 

WebServerCustomizer가 다시 아용되도록 하기 위해 @Component 어노테이션에 있는 주석을 풀자 이제 was에 예외가 전달되거나, response.sendError()가 호출되면 위에 등록한 예외 페이지 경로가 호출된다. 

 

 

 

@Slf4j
@RestController
public class ApiExceptionController {
    @GetMapping("/api/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id) {
        if (id.equals("ex")) {
            throw new RuntimeException("잘못된 사용자");
        }

        return new MemberDto(id, "hello" + id);
    }


    @Data
    @AllArgsConstructor
    static class MemberDto {
        private String MemberId;
        private String name;
    }
}

 

이렇게 되면은 json으로 통신하게 되면 html오류 페이지가 뜨게된다. -> 이것은 우리가 원하는게 아님, 할 수 있는게 없어서 

클라이언트는 정상이든 오류이든 json이 변환되기를 기대합니다. 

 

어떤 경우가 포스트맨이 json으로 보낸것이고 어떤경우가 postman이 html을 보낸것인가? 

 

 

 

handlerExceptionResolver 의 시작 

 

예외가 발생해서 서블릿을 넘어 was까지 예외가 전달되면 http상태코드가 500으로 처리된다. 발생하는 예외에 따라서 400, 404등등 다른 상태코드도 처리하고 싶다 

오류 메세지 형식들을 api마다 다르게 처리하고 싶다. 

 

상태코드 변환 

예를 들어 IllegerArgumentException을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면 http 상태코드를 400으로 처리하고 싶다.  어떻게 할까 

 

http400상태코드 = 배드 리퀘스트 클라이언트가 잘못한거 파라미터를 잘못보낸것, 즉 400으로 명시를 하고싶다. 

 

 

handlerExceptionResovler 

스프링 mvc는 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 "HanddlerExceptionResolver'를 사용하면 된다. 줄여서 'ExceptionResolver라고 한다. -> 예외를 해결해주는 해결자 역할을 한다. 

 

 

예외가 나면은 예외를 해결하도록 ExceptionResolver가 실행이 된다. -> 정상응답으로 나갈 수 있다. 

 

postHandle()은 호출이 안된다. 

 

 

 

@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

        try {
            if (ex instanceof IllegalArgumentException) {
                log.info("IllegalArgumentException resolver to 400");
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();

            }

        }catch (IOException e) {
            log.error("resolver ex", e);
        }
        return null;
    }
}

 

 

 

Exception을 처리해서 정상 흐름처럼 변경하는 것이 목적이다. 이름 그대로 Exception을 Resolver(해결) 하는것이 목적이다. 

 

illegralArgumentException이 발생하면 response.sendError(400)을 호출해서 http상태코들르 400으로 지정하고 빈 ModelAndView를 반환한다. 

 

반환 값에 따른 동작 방식 

빈 ModelandView - new ModelAndView()처럼 빈 ModelAndView를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다. 

 

ModelandView지정 - ModelAdnView에 View Model등의 정보를 지정해서 반환하면 뷰를 렌더링 한다 - 오류페이지 같은 경우를 렌더링 할 수 있디. 

null: null을 변환하면 다음 ExceptionResolver를 찾아서 실행한다. 

 

 

ExceptionResolver를 활용 

예외상태 코드 변환 

예외를 response.sendError(xxx)호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임 

이후 was는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한 /error가 호출됨 

 

뷰 템플릿 처리 

ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공 

 

Api응답처리 

response.getWriter().println("hello"); 처럼 Http응답 바디에 직접 데이터를 넣어주는것도 가능하다. 여기에 JSON으로 응답하면 API응답 처리를 할 수 있다. 

 

 

 

 ex를 하면은 500에러가 뜨는데 그 이유가 런타임 Exception오류가 터졋기 때문이다 

 

 

API에외 처리 - HandlerExceptionResolver활용 

 

예외를 여기서 마무리하기 

예외가 발생하면 was까지 예외가 던져지고 was에서 오류 페이지 정보를 ㅊ자아서 다시 /error를 호출하는 과정은 너무 복잡하다. 

 

ExceptionResolver를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결 할 수 있다. 

 

 

@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {


    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try{
            if (ex instanceof UserException) {
                log.info("UserException resolver to 400");
                String acceptHeader = request.getHeader("accept");
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);

                if ("application/json".equals(acceptHeader)) {
                    Map<String, Object> errorResult = new HashMap<>();
                    errorResult.put("ex", ex.getClass());
                    errorResult.put("message", ex.getMessage());
                    String result = objectMapper.writeValueAsString(errorResult);

                    response.setContentType("application/json");
                    response.setCharacterEncoding("utf-8");
                    response.getWriter().write(result);
                    return new ModelAndView();

                }else {
                    //text/html
                    return new ModelAndView("error/500");
                }
            }
        }catch(IOException e) {
            log.error("resolver ex", e);

        }

        return null;


    }
}

 

 

위의 코드 덕분에 여기서 postman을 통해서 json형식의 데이터를 받으면은 500에러가 뜨고 아니면은 html에러가 뜬다. 

 

exceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리해버린다. 

따라서 예왹 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 mvc에서 예외처리는 끝이 난다. 

결과적으로 was입장에선느 정상 처리가 된 것이다. 이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심이다. 

 

서블릿 컨테이너까지 예외가 올라가면 복잡하고 지저분하게 추가 프로세스가 실행된다. 반면 ExceptionResolver를 사용하면 예외처리가 상당히 깔끔해진다. 

 

그런데 직접 ExceptionResolver를 구현하려고 하니 상당히 복잡하다 스프링이 제공하는 ExceptionResolver를 알아보자. 

 

 

 

 

스프링부트의 ExceptionResolver1 

스플이 부트가 기본으로 제공하는 ExceptionREsolver는 다음과 같다. 

1. ExceptionHanlderExceptionResolver

- @ExceptionHanlder을 처리한다. API 예외처리는 대부분 이 기능으로 해결한다. 

 

 

2. ResponseStatusExceptionResolver

- http상태 코드를 지정해준다 

@ResponseStatus(value = HttpStatus.NOT_FOUND

 

3. DefaultHandlerExceptionResolver - > 우선순위가 제일 낮음 

스프링 내부 기본 예외를 처리한다. 

 

ExceptionResolver2 

 

이번에는 DefaultHandlerExceptionResolver  

대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생하는데, 이경우에는 예외가 발생하기때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고 결과적으로 500 오류가 발생한다.  

 

 

그런데 hanlderExceptionResolver를 직접 사용하기에는 복잡하다 api오류 응답일 경우 response에 직접 데이털르 넣어야해서 매우 불편하고 번거로움 

 

   

 

 

 

 

 

 

 

 

 

 

 

 


인텔리제이 단축키 

  • 컨트롤 + 알트 + 씨 -> 상수로 만들기
  • 컨트롤 + 알트 + 엔    -> 코드 합치기 
  • 컨트롤 + 쉬프트 + 티 -> 테스트 코드 만들기  
  • 알트 + 엔터 -> 코드 줄이는 단축키 

 

로그인 필기 

 

optional에서 get이라고 하면 안에있는게 나오게 된다. 
login get password가 같은지 확인할때 쓰는 것 -> .equals 

@RequiredArgsContructor -> final이 붙거나 @NotNull이 붙은 필드의
생성자를 자동 생성해주는 롬복 어노테이션 

생성자 주입은 a객체가 b객체를 사용하는 코드가 있을때, b객체를 생성
하여 a객체게 사용할 수 있도록 관계를 형성해 주는 것이다. 

login 맴버를 못찾거나 이이디 패스워드가 안맞는 것인데 이런 경우는 
if(loginMember == null) {
bidningResult.rehect("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.);
return "login/loginForm"; 
}

쿠키에는 영속쿠키와 세션 쿠키가 있다.

 

  • 영속쿠키: 만료날짜를 입력하면 해당 날짜까지 유지
  •  세션큐키: 만료날짜를 생략하면 브라우저 종료시 까지만 유지

 

public String homeLogin(@CookieValue(name= "memberId", required = false) Long memberId, Model model) {

required = false 를 쓰는 이유가 뭔가  

Member loginMember = memberRepository.findById(memberId);
        if(loginMember == null) {
            return "home"; 
        }



디비에 없는 경우도 있기 때문에 loginMember==null을 집어 넣는다. 

쿠키의 보안문제 

  1. 쿠키의 값이 임의로 변경
  2. 쿠키에 보관된 정보는 훔쳐갈 수 있다. 
  3. 해커가 쿠리를 한번 훔쳐가면 평생 사용할 수 있다. 

이러한 문제를 해결하기위해 세션이라는 친구가 나온다. 


새션 관리 
- 세션 생성 
sessionid생성(임의의 추정 불가능한 랜덤 값) 
세션 저장소에 sessionid와 보관할 값 저장 
sessionId로 응답 쿠키를 생성해서 클라이언트에 전달 
- 세션 조회 
클라이언트가 요청한  sessionid쿠키의 값으로, 세션 저장소에 보관한 값 조회 
- 세션 만료 
클라이언트가 요청한 sessionis쿠키의 값으로, 세션 저장소에 보관한 sessionid의 값 제거 


해쉬 맵도 괜찮지만   동시성이 있는 경우는 ConcurrentHashMap을 사용해야한다. 


findFirst() -> 순서중에서 가장 먼저 나온 애 
findAny() -> 순서 상관없이 빨리 나온애 


HttpServletResponse 는 인터페이스이고 구현체가 있지만  테스트 만들기가 어렵다 하지만 


세션의 create 옵션에 대해 알아보자.

request.getSession(true)
세션이 있으면 기존 세션을 반환한다.
세션이 없으면 새로운 세션을 생성해서 반환한다.

request.getSession(false)
세션이 있으면 기존 세션을 반환한다.
세션이 없으면 새로운 세션을 생성하지 않는다. null 을 반환한다.


        HttpSession session = request.getSession(false);

세션이 없는것이 목적이기 때문에 false를 집어넣는다 
true이면 세션을 생성    

            session.invalidate();


세션이랑 그 안에 있는 데이터가 다 날라가는 구문 

 

로그인 처리하기 

@SessionAttribute -> 스프링에서 제공하는 세션을 더 편리하게 사용할 수 있도록 지원하는 기능

 

 

log.info("sessionId={}", session.getId());
log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
log.info("getCreationTime={}", new Date(session.getCreationTime()));
log.info("lastAccessedTime ={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());

 

 

 

 

http는 비연결성으므로 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한 것인지 아닌지를 인식할 수 없다. 따라서 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기가 어렵다. 

 

문제점 

  • 세션과 관련된 쿠키를 탈취 당했을 경우 오랜시간이 지나도 해당 쿠키로 요청 가능 
  • 세션이 메모리에 생성되기때문, 용량때문에 필요한 경우에만 생성해서 사용 

 

세션의 종료 시점 

  • 30분에 한시간정도 잡는게 좋다. 30분 지나면 사라지게끔 -> 문제점이 있는데 사용중임에도 불구하고 세션이 날라가서 또 로그인해야한다. 
  • 해결법 -> 최근에 요청한 시간을 기준으로 30분 정도로 만든다. 

 

 

 

<label for="itemName" th:text="#{item.itemName}"></label>

<label for="itemName" th:text="#{item.itemName}"></label>

메세지 기능을 사용안하면 하나 하나 모든 메세지들을 하드코딩으로 바꿔줘야 한다. -> 시간이 많이듬, 귀찮음 

 

하드코드를 하면 값들이 직관적이지만 유지보수가 어렵다. 그렇기 때문에 메세지 파일을 따로 만들어서 관리 할 필요가 있다, 

 

message.properties라는 파일을 하나 따로 만든 뒤 

item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량

각 html에서 해당 데이터를 key값으로 불러와서 메세지 기능을 사용 할 수 있다. 

 

<label for="itemName" th:text="#{item.itemName}"></label>

<label for="itemName" th:text="#{item.itemName}"></label>

 


국제화란 메세지 파일과 마찬가지로 파일을 따로 만들어서 나라별로 언어를 간편하게 변경할 수 있는 기능이다. 

 

각 나라별로 만들 수 있다는 특징이 있다. 

 

- messages_en.properties 

item=Item
item.id=Item ID
item.itemName=Item Name
item.price=price
item.quantity=quantity

 

- messages_ko.properties 

item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량

사용자가 어떤 언어를 사용하는지 어떻게 알 수 있냐면 http accpt-language헤더 값을 사용한다. 

사용자가 직접 인터넷 브라우저 언어를 설정 할 수 있다. 

 

 

+ Recent posts