타임리프 - 기본 기능
타임리프 소개
1개는 할줄 알아야 된다.
변수 - SpringEL
div 내부에서만 사용 가능
기본 객체들
원래 넘겨야 되는데 자주써서 기본으로 제공해 준다.
URL 링크
지정 = 주소, 남는애 = 쿼리
<li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>
<li><a href="/hello/data1?param2=data2">path variable + query parameter</a></li>
리터럴
연산
No-Operation 활용 할수 있다.
반복
짝수, 홀수
템플릿 레이아웃2
정리
타임리프 - 스프링 통합과 폼
프로젝트 설정
Java home is different.
https://naelonambul.tistory.com/68
타임리프 스프링 통합
부트에서 자동화 해준다.
입력 폼 처리
검증할때 나타난다.
체크 박스 - 단일1
on = true
item.open=null
이게 문제다.
itemName=itemC&price=30000&quantity=30&open=on
itemName=dsfg&price=123&quantity=123
HTML checkbox 는 값 자체를 보내지 않는다.
트릭을 사용한다.
itemName=itemC&price=30000&quantity=30&_open=on
h.i.web.form.FormItemController : item.open=false
태생적인 한계가 있다.
_open만 들어왓네? = 체크박스 선택이 안됫구나.
너무 불편하다.
체크 박스 - 단일2
체크 박스 - 멀티
HashMap은 순서가 보장이 안된다.
Linked는 stack 개념이니 보장 된다.
한방에 해결하는 방법이 있습니다.
@ModelAttribute
컨트롤러 호출할때마다 자동으로 addAttribute를 함.
성능 최적화 :
어딘가에 static 필드로 만들어두고 불러쓰는것이 좋다.
이정도는 성능에 그렇게 영향이 있지 않아요.
th:field="*{regions}"
Id는 루프라서 고정값을 사용할수 없다.field에서 적용해줘야된다.
#ids.prev('regions')
html은 id값은 달라야 하므로 생성되는 id 값을 읽어와서 넣어준다.
item에는 form 이 없다. 그래서 *{}을 사용할수 없다.
th:field 와 th:value 값이 같으면 checked
메시지, 국제화
검증1 - Validation
검증 직접 처리 - 개발
부정에 부정어자나요. 읽기가 어려워..
//검증에 실패하면 다시 입력 폼으로
if(hasError(errors)){
}
private boolean hasError(Map<String, String> errors) {
return !errors.isEmpty();
}
정리
item에 데이터가 들어와서
빈값을 넘겼던 이유는 검증할때 값을 재사용 할수 있게
?의 이유 = null 일때도 대응하기 위해
safe natigation operator
정리
남은 문제점
Integer != 문자열
프로젝트 준비 V2
단축키 : 폴더 누르고 command + shift + r

BindingResult1
단축키 : command + p, 매개변수
화면 하기 전에 코드 설명 부터 한번 하고 갑니다.
주의 BindingResult 위치
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="${#fields.globalErrors()}">전체 오류 메시지</p>
</div>
<div class="field-error" th:if="${errors?.containsKey('price')}" th:text="${errors['price']}">
가격 오류
</div>
<input type="text" id="itemName" th:field="*{itemName}"
th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
화면 설명
BindingResult2
FieldError, ObjectError
오류 코드와 메시지 처리1
오류 코드와 메시지 처리2
오류 코드와 메시지 처리3
오류 코드와 메시지 처리4
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
void messageCodesResolverObject(){
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
for (String messageCode : messageCodes) {
System.out.println("messageCode = " + messageCode);
}
}
}
messageCode = required.item
messageCode = required
@Test
void messageCodesResolverField(){
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
for (String messageCode : messageCodes) {
System.out.println("messageCode = " + messageCode);
}
}
messageCode = required.item.itemName
messageCode = required.itemName
messageCode = required.java.lang.String
messageCode = required
String, Integer 같은 공통메시지도 가능할것이다.
정리
DefaultMessageCodesResolver
동작방식
1-4번만 쓰세요.
오류 코드와 메시지 처리5
오류 코드와 메시지 처리6
Validator 분리1
Validator 분리2
Validator 인터페이스를 사용하면 스프링의 도움을 받을수 잇다.
객체 데이터를 바인딩 해주는것
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(itemValidator);
}
요청마다 새로 만들어짐, 요청마다 데이터가 다르니까.
컨트롤러에만 적용됨.
@Validated
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
검증기가 여러개면 스프링 구조처럼 입력값을 판단해서 출력값도 나눠 줘야 한다.
글로벌 설정(모든 컨트롤러)
검증2 - Bean Validation
Bean Validation - 소개
Bean Validation - 시작
Bean Validation - 스프링 적용
Bean Validation - 오브젝트 오류
Bean Validation - 수정에 적용
인생 쉽지 않다.
Bean Validation - 한계
Bean Validation - groups
놓친 것이 있다.
서버에서 무조건 Id 값 같은것도 검증 해야된다.
1. 어떻게 따로 적용할거냐 Group 기능을 이미 제공
2. ItemSaveForm, ItemUpdateForm 같은 별도의 객체를 만들어서 사용한다.
@NotNull(groups = UpdateCheck.class) //수정 요구사항 추가
private Long id;
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
private String itemName; //A
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Range(min = 1000, max=1000000, groups = {SaveCheck.class, UpdateCheck.class})
private Integer price; //A, typeMisMatch = 필드 애러, 빈 검증X
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Max(value=9999, groups = SaveCheck.class) //수정 요구사항 추가
private Integer quantity;
@Validated(UpdateCheck.class)
@Valid 에는 없다.
잘 안써요.
지금이야 단순하지
회원 가입 != 회원 수정이 다름.
Form 전송 객체 분리 - 소개
Form 전송 객체 분리 - 개발
Bean Validation - HTTP 메시지 컨버터
정리
검증 애노테이션 모음 들어가보자.
로그인 처리1 - 쿠키, 세션
로그인 요구사항
요구사항
프로젝트 생성
회원 가입
public Optional<Member> findByLoginId(String longId){
List<Member> all = findAll();
for (Member member : all) {
if(member.getLongId().equals(longId)){
return Optional.of(member);
}
}
return Optional.empty();
}
public Optional<Member> findByLoginId(String longId){
return findAll().stream()
.filter(m->m.getLongId().equals(longId))
.findAny();
}
디버그 : Member(java.lang.reflect) 임포트 조심
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/add")
public String addForm(@ModelAttribute("member") Member member){
return "members/addMemberForm";
}
@PostMapping("/add")
public String save(@Valid @ModelAttribute Member member, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return "members/addMemberForm";
}
memberRepository.save(member);
return "redirect:/";
}
}
IDE 에서 인식을 잘 못함.
크롬에서 바꾸라고 ㅋㅋㅋ
save: member=Member(id=1, loginId=test, name=테스터, password=test!)
로그인 기능
@Service
@RequiredArgsConstructor
public class LoginService {
private final MemberRepository memberRepository;
/**
*
* @return null 로그인 실패
*/
public Member login(String loginId, String password){
Optional<Member> findMemberOptional = memberRepository.findByLoginId(loginId);
Member member = findMemberOptional.get();
if(member.getPassword().equals(password)){
return member;
}else{
return null;
}
}
}
public Member login(String loginId, String password){
return memberRepository.findByLoginId(loginId).filter(m->m.getPassword().equals(password))
.orElse(null);
}
단축키 : 인라인
폼 = 데이터 전송 객체
reject = 글로벌 오류, 왜냐하면 대상 오류가 없다.
벨리데이션에서 복잡한 로직은 코드로 빼라
예(지금 로그인 디비 조회)
정리
로그인 처리하기 - 쿠키 사용
쿠키
new Cookie("memberId", String.valueOf(loginMember.getId()));
쿠키


스프링이 타입 컨버팅 long->Long
public String homeLogin(@CookieValue(name="memberId", required = false) Long memberId, Model model){
로그인 유지
로그인 성공, 이름 출력 성공

@PostMapping("/logout")
public String logout(HttpServletResponse response){
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
보안상 엄청큰 문제가 있음.
로그인 처리하기 - 세션 동작 방식
정리
로그인 처리하기 - 세션 직접 만들기
세션을 개발해보자.
3가지 기능
단축키 : 상수 만들기(ctrl+alt+c)
/**
* 세션 관리
*/
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/**
* 세션 생성
* sessionId 생성 (임의의 추정 불가능한 랜덤 값)
* 세션 저장소에 sessionId와 보관할 값 저장
* sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
*/
public void createSession(Object value, HttpServletResponse response){
//세션 id를 생성하고, 값을 세션에 저장
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
//쿠키 생성
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
}
/**
* 세션 조회
*/
public Object getSession(HttpServletRequest request){
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if(sessionCookie == null){
return null;
}
return sessionStore.get(sessionCookie.getValue());
}
/**
* 세션 만료
*/
public void expire(HttpServletRequest request){
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if(sessionCookie != null){
sessionStore.remove(sessionCookie.getValue());
}
}
public Cookie findCookie(HttpServletRequest request, String cookieName){
Cookie[] cookies = request.getCookies();
if(cookies == null){
return null;
}
return Arrays.stream(cookies)
.filter(cookie->cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
}
요즘 테스트는 public 없어도됨.
HttpServletResponse 가 있어야 되는데 인터페이스라서.. 아.. 애매해요.
이런 경우가 테스트가 어려워요.
백기선씨 github 가짜 객체 만들던 링크 참조.
class SessionManagerTest {
SessionManager sessionManager = new SessionManager();
@Test
void sessionTest(){
//세션 생성 -> 응답이 나갓다.
MockHttpServletResponse response = new MockHttpServletResponse();
Member member = new Member();
sessionManager.createSession(member, response);
//요청에 응답 쿠키 저장 -> 응답이 왓다.
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(response.getCookies());
}
}
//세션 조회
Object result = sessionManager.getSession(request);
assertThat(result).isEqualTo(member);
//세션 만료
sessionManager.expire(request);
Object expired = sessionManager.getSession(request);
assertThat(expired).isNull();
로그인 처리하기 - 서블릿 HTTP 세션1
소개
상수값 생성
public abstract class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
public interface SessionConst {
String LOGIN_MEMBER = "loginMember";
}
옵션
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession(true);
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
true로 하면 세션을 만들기 때문에 false로
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session != null){
session.invalidate();
}
return "redirect:/";
}
처음 들어온 사용자도 세션이 만들어져버림
= 메모리를 생성한다.
= false 로 막아야 한다.
JSESSIONID

남겨 있지만 서버에 없기 때문에 의미가 없다.
로그인 처리하기 - 서블릿 HTTP 세션2
맘에 안들어
@SessionAttribute
@GetMapping("/")
public String homeLoginV3Spring(@SessionAttribute(name=SessionConst.LOGIN_MEMBER, required = false) Member member, Model model){
if(member == null){
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
TrackingModes
http://localhost:8080/;jsessionid=90F4B43534C72D650DA469710F114529
서비스 할때는 빼주는 것이 좋다.
브라우저가 쿠키를 지원하지 않으면?
방금본 URL을 다 붙여 줘야된다.
사실 타임리프가 어느정도 해주긴한다.
서버는 브라우저가 지원하는지 안하는지 모른다.
세션 정보와 타임아웃 설정
설명
세션 타임 아웃 설정
서버에는 정보가 남아 있다.
10만개의 세션
그럼 언제 이 세션을 종료 시켜야 되는가.
30분 뒤에 꺼짐 ㅋㅋ
session.setMaxInactiveInterval(1800);
lastAccessedTime=Mon Jun 06 12:36:57 KST 2022
서버가 마지막 요청을 기준으로 세션을 날리는것입니다.
정리
사용자 * 정보 = 메모리 사용량
Id 정도만 담거나 최소한의 정보(로그인용 맴버 객체를 따로 만들어서 사용)
로그인 처리2 - 필터, 인터셉터
서블릿 필터 - 소개
서블릿 필터 - 요청 로그
서블릿 필터 - 인증 체크
로그인으로 지금 페이지, 앞으로 개발될 페이지에도 접근 못하게 해보자.
default = 구현 안해도 된다.
/**
* 화이트 리스트의 경우 인증 체크X
*/
private boolean isLoginCheckPath(String requestURI){
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
예외를 먹어버리면 정상 흐름처럼 동작함.
여기서도 거를수 있는데 미래에도 설정을 바꾸고 싶지 않다.
그럼 필터가 1번더 적용되니 성능이... 바다에 모래와 같은 영향이다.
return; 다음 서블릿 이나 호출을 안하겟다.
홈->/items
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession(true);
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" +redirectURL;
}
설명
return; //여기가 중요, 미인증 사용자는 다음으로 진행하지 않고 끝!
정리
스프링 시큐리티도 다 그냥 이렇게 돌아 간다.
참고
스프링 인터셉터 - 소개
스프링 인터셉터 - 요청 로그
스프링 인터셉터 - 인증 체크
이래서 필터말고 인터셉터 써야되는구나.
default 는 오버라이드(재정의) 안해도 된다.
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("인증 체크 인터셉터 실행 {}", requestURI);
HttpSession session = request.getSession();
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
log.info("미인증 사용자 요청");
// 로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
경로가 오면 호출 자체가 안됨.
정리
ArgumentResolver 활용
보너스
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if(session == null){
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
등록은 webconfig
supportsParamter = 캐쉬된다.
누가 만들어두고 나머지는 저걸 사용하는 식으로 하는것도 좋다.
정리
예외 처리와 오류 페이지
서블릿 예외 처리 - 시작
서블릿 예외 처리 - 오류 화면 제공
서블릿 예외 처리 - 오류 페이지 작동 원리
WAS가 서버 내부로 request를 보낸다.
에러 뷰 관련된 모든것이 다 호출된다.
단축키 : Column Selection Mode - shift + command + 8
log.info("ERROR_EXCEPTION", request.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_EXCEPTION_TYPE", request.getAttribute(ERROR_EXCEPTION_TYPE));
log.info("ERROR_MESSAGE", request.getAttribute(ERROR_MESSAGE));
log.info("ERROR_REQUEST_URI", request.getAttribute(ERROR_REQUEST_URI));
log.info("ERROR_SERVLET_NAME", request.getAttribute(ERROR_SERVLET_NAME));
log.info("ERROR_STATUS_CODE", request.getAttribute(ERROR_STATUS_CODE));
서블릿 예외 처리 - 필터
정상요청 -> 필터 -> 내부 에서 -> 에러 요청 -> 필터
요청을 구분할수 있어야 한다.
DispatcherType
forward = 예전에 배운
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setFilter(new LogFilter());
filterRegistration.setOrder(1);
filterRegistration.addUrlPatterns("/*");
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
}
필터는 Request가 기본이다.
스프링 부트 - 오류 페이지2
정리
1. 웹페이지를 해결해야하는경우(지금)
2. 내부..
API 예외 처리
시작
이라믄 안된다.
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500APi(HttpServletRequest request,
HttpServletResponse response){
log.info("API errorPage 500");
Map<String, Object> result = new HashMap<>();
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
result.put("status", request.getAttribute(ERROR_STATUS_CODE));
result.put("message", ex.getMessage());
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}
과거 수업에서 2개(URI, Accept)를 기준으로 컨트롤러를 배정한다고 배웠다.
해시맵은 순서를 보장하지 않음.
HandlerExceptionResolver 시작
목표
HandlerExceptionResolver
ExceptionResolver 정상 응답처럼 나감
디버깅 :
RuntimeException -> IllegalArgumentException
throw new IllegalArgumentException("잘못된 입력 값");
return new ModelAndView();
정상 흐름 리턴
등록
설명
try catch = 정상 흐름처럼 바꾸려는것
null -> 다음 서블릿 -> 다음 -> WAS -> 500
활용
@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();
}
if(ex ..){
response.getWriter().write("~~~~~");
}
}catch (IOException e){
log.error("resolver ex", e);
}
return null;
}
}
호출은 된다.
HandlerExceptionResolver 활용
API 예외 처리 활용
올라갓다 내려왓다 이상하다.
2번 찍힘
똑똑하게 처리하려면 2가지(html, json)
JSON = ObjectMapper 필요.
@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;
}
}
Exception Resolver 에서 깔끔하게 처리해버렸다.
정리
스프링이 제공하는 ExceptionResolver1
스프링이 제공하는 ExceptionResolver2
@ExceptionHandler
가장 중요하다.
API 예외 처리의 어려운점
@ExceptionHandler
갓다가 다시 WAS 에서 500
ErrorResult = JSON
http://localhost:8080/api2/members/bad
ExceptionHandlerExceptionResolver
컨트롤러에서 @ExceptionHandler가 있는지 찾아본다.
그리고 있으면 대신 호출
정상흐름으로 바꿔서 정상적으로 리턴을 해줌.
정상은 200
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e){
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id){
if(id.equals("ex")){
throw new RuntimeException("잘못된 사용자");
}
if(id.equals("bad")){
throw new IllegalArgumentException("잘못된 입력 값");
}
if(id.equals("user-ex")){
throw new UserException("사용자 오류");
}
return new MemberDto(id, "Hello "+ id);
}
@Data
@AllArgsConstructor
static class MemberDto{
private String memberId;
private String name;
}
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
좋은점 = 에러 요청을 새로 생성하지 않는다.
그냥 컨트롤러 호출된거랑 거의 똑같다.
@ExceptionHandler(UserException.class)
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
둘다 같다.
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e){
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", "내부 오류");
}
컨트롤러 별로 적용되서 다른 컨트롤러와 상관이 없다.
자식 예외까지 잡는다.
그냥 Exception은 위에서 처리하지 못한 예외를 잡는다.
우선 순위
항상 디테일 한쪽이 우선권을 갖는다.
@ExcpetionHandler는 너무 잘 만들어 놨다. 컨트롤러 처럼
API 처리할때 딱이다.
IllegalArgumentException 처리 흐름
인터널 서버 에러(Exception) 처리 흐름
단점이 있다.
중복 처리
@ControllerAdvice
스프링 타입 컨버터
스프링 타입 컨버터 소개
상당히 많다.
모든 데이터는 문자다.
@RestController
public class HelloController {
@GetMapping("/hello-v1")
public String helloV1(HttpServletRequest request, HttpServletResponse response){
String data = request.getParameter("data"); //문자 타입으로 조회
Integer intValue = Integer.valueOf(data);
System.out.println("intValue = " + intValue);
return "ok";
}
}
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data){
System.out.println("data = " + data);
return "ok";
}
다 문자다.
마음이 이미 괴롭다.
컨버터 인터페이스
자바 언어 차원의 변환기
타입 컨버터 - Converter
converter
문자를 숫자로 만드는 컨버터
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
log.info("convert source={}", source);
return Integer.valueOf(source);
}
}
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
log.info("convert source={}", source);
return String.valueOf(source);
}
}
public class ConverterTest {
@Test
void stringToInteger(){
StringToIntegerConverter converter = new StringToIntegerConverter();
Integer result = converter.convert("10");
assertThat(result).isEqualTo("10");
}
}
@RequestParam 같은데 등록해두면 받을수 있다.
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
@Override
public IpPort convert(String source) {
log.info("convert source={}", source);
//"127.0.0.1:8080"
String[] split = source.split(":");
String ip = split[0];
int port = Integer.parseInt(split[1]);
return new IpPort(ip, port);
}
}
@Test
void stringToIpPort(){
IpPortToStringConverter converter = new IpPortToStringConverter();
IpPort source = new IpPort("127.0.0.1", 8080);
String result = converter.convert(source);
assertThat(result).isEqualTo("127.0.0.1:8080");
}
@Test
void ipPortToString(){
StringToIpPortConverter converter = new StringToIpPortConverter();
String source = "127.0.0.1:8080";
IpPort result = converter.convert(source);
assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
}
equalTo가 되나면
@EqualsAndHashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IpPort)) return false;
IpPort ipPort = (IpPort) o;
return getPort() == ipPort.getPort() && Objects.equals(getIp(), ipPort.getIp());
}
@Override
public int hashCode() {
return Objects.hash(getIp(), getPort());
}
안에있는 값만 같으면 true가 나옴
정리
내가 하나씩 만들면 의미가 없다.
이미 많이 제공한다.
컨버전 서비스 - ConversionService
컨버팅 확인기능, 컨터팅 기능
@Test
void conversionService(){
//등록
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new StringToIntegerConverter());
conversionService.addConverter(new IntegerToStringConverter());
conversionService.addConverter(new StringToIpPortConverter());
conversionService.addConverter(new IpPortToStringConverter());
//사용
assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
assertThat(conversionService.convert("127.0.0.1:8080", IpPort.class))
.isEqualTo(new IpPort("127.0.0.1", 8080));
assertThat(conversionService.convert(new IpPort("127.0.0.1", 8080), String.class))
.isEqualTo("127.0.0.1:8080");
}
정리
숨어서 제공된다.
스프링 빈으로 등록하는 곳을 만들어 두고 의존성 주입이 되게 만든다.
인터페이스 분리 원칙 - 재밌는 원칙
사용과 등록으로 구분
보충: 설명한 이유
스프링에 사용과 분리된 것들이 많이 있다.
스프링에 Converter 적용하기
우리가 만든것
h.t.converter.StringToIntegerConverter : convert source=10
하기전에도 수행이 잘됬다.
기본 컨버터
추가한것 > 기본 변환자.
2022-06-07 22:49:40.780 INFO 42914 --- [nio-8080-exec-2] h.t.converter.StringToIpPortConverter : convert source=127.0.0.1:8080
ipPort IP = 127.0.0.1
ipPort PORT= 8080
@ModelAttribute에도 잘 동작한다.
스프링 디버그 하는법
뷰 템플릿에 컨버터 적용하기
괄호 1개, 2개
2개 = 컨버터 적용
- ${number}: 10000
- ${{number}}: 10000
- ${ipPort}: hello.typeconverter.type.IpPort@59cb0946
- ${{ipPort}}: 127.0.0.1:8080
h.t.converter.IntegerToStringConverter : convert source=10000
h.t.converter.IpPortToStringConverter : convert source=hello.typeconverter.type.IpPort@59cb0946
폼에도 적용이 되네
{} 2개 안했는데 불러왔네.
h.t.converter.IpPortToStringConverter : convert source=hello.typeconverter.type.IpPort@59cb0946
th:text = 컨터버 적용.
th:value = 미적용
정리
문자 -> 객체 -> 문자
IpPortToStringConverter : convert source=hello.typeconverter.type.IpPort@59cb0946
포맷터 - Formatter
포맷터를 지원하는 컨버전 서비스
포맷터 적용하기
스프링이 제공하는 기본 포맷터
파일 업로드
서블릿과 파일 업로드1
Received [GYlB B1«K¼ø|£Ðß=w¶ñ§Üo^â¥êÅu´½tÅÜE*-
------WebKitFormBoundaryabK35vA8zWP4Rzkq
Content-Disposition: form-data; name="itemName"
ìíëª A
------WebKitFormBoundaryabK35vA8zWP4Rzkq
Content-Disposition: form-data; name="file"; filename="áá ³áá ³á á µá«áá £áº 2022-06-06 áá ©áá ® 12.12.14.png"
Content-Type: image/png
------WebKitFormBoundaryabK35vA8zWP4Rzkq--
parts[1개,1개]
사용 옵션
톰캣 구현체 RequestFacade = MVC servlet request 강의
spring.servlet.multipart.enabled=false
h.u.c.ServletUploadControllerV1 : itemName=null
h.u.c.ServletUploadControllerV1 : parts=[]
멀티 파트 기능을 하지말라고 하는것
RequestFacade -> StandardMultipartHttpServletRequest
DispatcherServlet 에서 MultipartResolver 실행
protected void doDispatch(HttpServletRequest request, HttpServletResponse response)
processedRequest = checkMultipart(request);
서블릿과 파일 업로드2
file.dir=/Users/kch/code/file/
단축키 : 루프 iter + tab
@PostMapping("/upload")
public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
log.info("request={}", request);
String itemName = request.getParameter("itemName");
log.info("itemName={}", itemName);
Collection<Part> parts = request.getParts();
log.info("parts={}", parts);
for (Part part : parts) {
log.info("==== PART ====");
log.info("name={}", part.getName());
Collection<String> headerNames = part.getHeaderNames();
for (String headerName : headerNames) {
log.info("header : {}", headerName, part.getHeader(headerName));
}
//편의 메서드
//content-disposition; filename
log.info("submitFilename={}", part.getSubmittedFileName());
}
------WebKitFormBoundaryLBNAHzfH9Nw37iPa
Content-Disposition: form-data; name="itemName"
SpringV2
------WebKitFormBoundaryLBNAHzfH9Nw37iPa
Content-Disposition: form-data; name="file"; filename="áá ¡áá ¡á¼áá ¢áá §á¼.jpg"
Content-Type: image/jpeg
==== PART ====
name=itemName
header : content-disposition
submitFilename=null
size=8
body=SpringV2
==== PART ====
name=file
header : content-disposition
header : content-type
submitFilename=가상배경.jpg
size=64273
servlet 3.0 이전 분들은 이런거 못씀.
단축키 : command + shift + g
파일

정리
주의 큰용량은 로그를 끄자.
logging.level.org.apache.coyote.http11=debug
log.info("body={}", body);
스프링과 파일 업로드
스프링과 파일 업로드 - 기나긴 과정의 종착지
request=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@55efc848
itemName=SpringV3
multipartFile=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@21a21cc5
파일 저장 fullPath=/Users/kch/code/file/가상배경.jpg
실제로 다운로드 까지 해야되.
예제로 구현하는 파일 업로드, 다운로드
이름이 중복 되면?
= 이름은 UUID 같은걸로 안겹치게
라이브 코딩 하다가 메서드를 뽑는다.
여기서 이제
생성자가 없네요.
테스트는 생략
실무에서는 무조건 테스트 코드를 다 짠다.
설명
상품 저장용폼
옵션 멀티플
경로만 저장하는거다.
상대 경로만 저장
없지만 저장했던 이름을 보여준다.
컨트롤러가 없으니까
쪼오금더 어렵다.
헤더에 뭐 추가할게 있어서 ResponseEntity 를 사용. 과거 수업에서는 응답 코드와 메시지를 조작하기 위해 사용햇다.
아이템을 접근 권한이 있는 사람만 받을수 있다면?
헤더를 넣어야 된다.
열어 버린다.
고치니까 나오네
인코딩 :
String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
많이 왓네요.
정리