Inflearn brand logo image
Inflearn brand logo image
Inflearn brand logo image

타임리프 - 기본 기능

타임리프 소개

1개는 할줄 알아야 된다.

텍스트 - text, utext

제발 쳐라.

 조심

Escape

HTML 엔티티

Unescape

변수 - SpringEL

div 내부에서만 사용 가능

기본 객체들

원래 넘겨야 되는데 자주써서 기본으로 제공해 준다.

유틸리티 객체와 날짜

기본으로 자바8 날짜 라이브러리가 지원됨.

#temporals

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>

리터럴

문자 리터럴은 항상 작은 따옴표('문자')

공백없이 이어나가면 생략할수도 잇다.

리터럴 대체 문법

<li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"></span></li>
<li>리터럴 대체 |hello ${data}| = <span>hello Spring!</span></li>

연산

No-Operation 활용 할수 있다.

속성 값 설정

classappend가 좋다.

checked 가 들어가면 체크가 된다.

개발자 = true false 가 중요해

반복

짝수, 홀수

주석

프로토 타입 잘 안씀

파일에서는 안보임, 서버에서는 보임

블록

div를 딱딱 맞춰서 돌리고 싶다.

th:each는 태그 1개만 가능

안쓰는게 좋지만 어쩔수 없이 써야할 경우가 있다.

자바스크립트 인라인

꼭 라이브 코딩 해보세요.

선언 안됨.

객체 => toString

문제 될만한거 이스케이프 해줌

js 안에서 each

템플릿 레이아웃2

정리

타임리프 - 스프링 통합과 폼

프로젝트 설정

타임리프 스프링 통합

부트에서 자동화 해준다.

입력 폼 처리

검증할때 나타난다.

체크 박스 - 단일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

<input type="checkbox" id="open" class="form-check-input" name="open" value="true"><input type="hidden" name="_open" value="on"/>

itemName=itemC&price=30000&quantity=30&open=true&_open=on

~ checked="checked"> = 체크 표시

엄청 번거 롭다.

체크 박스 - 멀티

HashMap은 순서가 보장이 안된다.
Linked는 stack 개념이니 보장 된다.

한방에 해결하는 방법이 있습니다.

@ModelAttribute

컨트롤러 호출할때마다 자동으로 addAttribute를 함.

성능 최적화 : 

어딘가에 static 필드로 만들어두고 불러쓰는것이 좋다.

이정도는 성능에 그렇게 영향이 있지 않아요.

th:field="*{regions}" 

Id는 루프라서 고정값을 사용할수 없다.field에서 적용해줘야된다.

#ids.prev('regions')

html은 id값은 달라야 하므로 생성되는 id 값을 읽어와서 넣어준다.

 item에는 form 이 없다. 그래서 *{}을 사용할수 없다.

th:field 와 th:value 값이 같으면 checked

라디오 버튼

인라인 단축키 ctrl + alt+n

나도 오류

data 조회 함수가 없음.

프로퍼티 접근법이라 getter가 필요함.

item.itemType=null

한번 입력이 되면 수정에서 무조건 1개가 선택이 되기 때문에 히든이 필요가 없다.

ENUM을 자바에서 접근이 가능하다.

패키지 명시하면 못찾거나 문제가 생긴다.

메시지, 국제화

스프링 메시지 소스 설정

하지마세요.

messages.properties

스프링 메시지 소스 사용

로케일이 없으면 디폴트가 선택 된다.

단축키 : 해당 코드 블럭 실행 ctrl + shift + r

적용 할때는 고민이 많을거임 = 고민고민하지마

웹 애플리케이션에 국제화 적용하기

LocaleResolver 로케일 해결사.

검증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

예를 들어서 가격 qqqq

binding result 가 있을때는 오류페이지를 막아준다.

특정 필드 오류로 처리한다.

BindingResult에 검증 오류를 적용하는 3가지 방법.

주의점

인터페이스가 인터페이스를 상속받을때 extends

정리

10원이 사라진다.

FieldError, ObjectError

목표

나중에 공부 하실때 쓰셔라.

FieldError 생성자가 2가지가 있다.

데이터가 잘 들어왔냐 = true or false

ObjectError는 값이 넘어오고 이런게 없다.

설명

타임리프의 사용자 입력 값 유지 

정상 = model 의 값을 사용.

비정상 = FieldError 값을 사용.

오류 코드와 메시지 처리1

상태값을 읽어와서 properties에서 세팅된 값을 보여줄수도 있게 가능하다.

errorCode, arguments = 오류코드로 메시지를 찾는 용도.

단축키 : command + e 이전파일 이동

배열로 받는 이유 = 없으면 다음거

복선이 있다.

정리

errors_en.properties 도 가능

오류 코드와 메시지 처리2

BindingResult는 뭘 검증해야되는지 알고 있다.

롬복(toString) 으로 로그 찍힘

target=Item(id=null, itemName=, price=null, quantity=null)

log.info("target={}", bindingResult.getTarget());

이름에 규칙이 있따.

문자.객체.속성

rejectValue 설명

= fieldError를 대신 생성 해줌

축약된 오류 코드 (메시지코드 리졸버)

오류 코드와 메시지 처리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

오류 코드 관리 전략

ValidationUtils.rejectIfEmpty(bindingResult, "itemName", "required");

정리

오류 코드와 메시지 처리6

스프링이 직접 만든 오류 메시지 처리

스프링이 직접 검증 오류 = 주로 타입정보

  null 때문에 조건이 2번 걸림

//바인딩 안되면 스프링 오류만 출력해주기
if(bindingResult.hasErrors()){
log.info("bindingResult={}", bindingResult);
return "validation/v2/addForm";
}

정리

여기서 직관을 얻어야된다.

Validator 분리1

검증 로직이 너무 길어

컨트롤러가 너무 비대하다.

public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
//item == clazz
//item == subItem
}

@Component

코드가 지저분해지기 시작하면...

내가 호출하는것은 지양하자.

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 - 시작

jakarta.validation = 인터페이스

hibernate.validator = 구현체

실무에서 하이버네이트를 써라.

메시지는 hibernater 기본 메시지.

Bean Validation - 스프링 적용

검증기가 돌아가네? = 이미 bean 검증기가 동작중

@Validated = beanValidation

자동으로 글로벌 Validator = 저번 강의 마지막 부분

검증 순서

바인딩에 성공한 필드만 Bean Validation

Bean Validation - 에러 코드

에러 코드.

공짜 입니다 여러분.

Bean Validation - 오브젝트 오류

@ScriptAssert()

오브젝트 오류란 = 필드가 아닌 조건들 = 값*수량 < 최대한도

이거  까지 가는거는 저는 조금.. 오버 입니다.

오류? : Warning: Nashorn engine is planned to be removed from a future JDK release

좀 별로다.

복잡한 조건은 자바 코드로 검증하자.

너무 좋은 기술이라도 너무 깊이 있게 건드리면..

Bean Validation - 수정에 적용

인생 쉽지 않다.

Bean Validation - 한계

ㅇ ㅏ ㄱ 덕 

입력과 수정이 다를수 있다.

왜 안되지

수정 = NotNull 맞네 = OK

입력 = Id 가 Null 이네? = NO

사이드 이펙트

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 전송 객체 분리 - 소개

중요하다.

Group 실무에서 사용 안함.

등록시 != 수정시 , 데이터가 안맞음.

기존

장점 : 심플

단점 : 간단한경우만 가능

폼 데이터

장점 : 요청에 맞는 데이터 객체를 만듬

단점 : 변환 과정이 생김

추가 정보를 다른 곳에서 가져와야 할때가 있다.

왠만하면 다 쪼개야지.

합칠까 = 그러지마라

분기가 많아지면 = 나눠야 할 신호다.

Form 전송 객체 분리 - 개발

web에 만드는 이유

조심

단축키 : 이름 바꾸기 shift + f6

bean 이 어떤것인지 추적을 못함.

원래는 html까지 바꾸는건데 공부하는거니까 여기까지 합시다.

생성자에서 하는것을 권장한다.

코드 설명

주의

@Validated @ModelAttribute("item") ItemUpdateForm form,

Bean Validation - HTTP 메시지 컨버터

제발 공부해

@RestController => @ResposeBody

스프링 컨트롤러는 기본적으로 뷰를 찾는데 

그걸 HTTP 메시지로 변환해 주는 어노테이션이고 값을 직접 보낸다.

컨트롤러 호출이 안된다.

객체 자체가 만들수 없는 상황이라 에러를 낸것

API의 경우 3가지로 생각

검증 오류

이대로 반환하면 안되고 필요한 것만 뽑아서 전송

rejectedValue 만 뽑아서 사용.

GET,HTML FORM과 POST의 차이

저 모양도 바꿀수 있다.

정리

검증 애노테이션 모음 들어가보자.

로그인 처리1 - 쿠키, 세션

로그인 요구사항

요구사항

프로젝트 생성

이렇게 설계한 이유

도메인의 정의

구조의 이유

도메인은 유지 해야된다.

form 을 Web에 넣어둠

item 이 아니라 itemsaveform 을 넘기면 의존적으로 변한다.

컨버팅 했던 이유

사용하되 의존적으로 역전되면 안된다.

회원 가입

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 - 필터, 인터셉터

서블릿 필터 - 소개

서블릿-필터, 스프링-인터셉터

접근 관리

여러 로직에서 공통으로 원하는것

서블릿필터, 스프링 인터셉터를 사용하는것이 좋다.

필요한 정보 = Http Header, URL, 쿠키

서블릿필터, 스프링인터셉터는 HttpServletRequest를 제공한다.

AOP는 부가적인 기능은 없다.

모든 고객의 요청 로그를 남겨라.

필터를 로그를 남기는 것도 가능하다.

디스패처 서블릿

필터 체인 가능

로그필터, 로그인필터

서블릿 필터 - 요청 로그

HttpServletRequest 의 부모

chain.doFilter(request, response); //넘어감

참고 : 순서 문제

같은 요청의 로그에 같은 식별자를 남기는 방법은 logback mdc 를 검색

서블릿 필터 - 인증 체크

로그인으로 지금 페이지, 앞으로 개발될 페이지에도 접근 못하게 해보자.

 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; //여기가 중요, 미인증 사용자는 다음으로 진행하지 않고 끝!

정리

스프링 시큐리티도 다 그냥 이렇게 돌아 간다.

참고

스프링 인터셉터 - 소개

소개

많은 기능 제공, 더 좋다.

로그인 여부 체크 좋다.

실제로 dofilter 를 안부릅니다.

시점 + 컨트롤러 정보를 제공받는다.

예외 상황

afterCompletion은 예외가 발생해도 호출된다.

정리

왠만하면 인터셉터 쓰세요.

스프링 인터셉터 - 요청 로그

단축키 : 오버라이드 ctrl + O

단축키 : 한줄 지우기 ctrl + D

싱글톤 맴버함수 조심해서 써야됨.

final static 안쓸거면 안쓰는게 좋다.

핸들러의 대부분의 것을 사용할수 있다.

등록하는 방법이 좀 다름.

호출 시점이 분리 되서 변수 사용할때 주의 해야 한다.

request.addAttribute()

PathPattern 공식문서

스프링 인터셉터 - 인증 체크

이래서 필터말고 인터셉터 써야되는구나.

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 = 캐쉬된다.

누가 만들어두고 나머지는 저걸 사용하는 식으로 하는것도 좋다.

정리

정리

@Component ->WebConfig-> @Autowired -> setFilter(A); 할수 잇다.

실제로 이런 방식으로 많이 사용한다?

예외 처리와 오류 페이지

서블릿 예외 처리 - 시작

Exception

Exception은 못잡으면 부모까지 올라간다.

이게 톰캣 이었구나.

response.sendError

오류 상태코드, 메시지를 가지고 있는다.

예외 터진게 아니라 return -> return -> return 된다.

관리가 잘 안되네..

서블릿 예외 처리 - 오류 화면 제공

이거를 등록을 해야한다.

ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");

서블릿 예외 처리 - 오류 페이지 작동 원리

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가 기본이다.

서블릿 예외 처리 - 인터셉터

인터셉터는 디스패처 타입을 세팅 할수 없다.

강력한 excludePathPatterns가 있다.

스프링 부트 - 오류 페이지1

기본제공

주의

BasicErrorConrtoller 내부에 이미 만들어져 있다.

스프링 부트 - 오류 페이지2

고객에게 문제가 될수 있다.

always, never, on_param

그럴리가.

오류는 서버에 로그로 남겨서 확인

스프링부트 오류 관련 옵션

참고 : 확장 포인트

크게 등록 안함

정리

정리

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;
}
}
"message": "사용자 오류"

Exception Resolver 에서 깔끔하게 처리해버렸다.

정리

스프링이 제공하는 ExceptionResolver1

순서

ExceptionHandlerExceptionResolver = (404 같은 코드 에러 메시지)

 에러 메시지를 코드화 할수 있음.

UTF-8 문제 생김

소스에서 찾아온다.

@ResponseStatus(code= HttpStatus.BAD_REQUEST, reason = "error.bad")
error.bad=잘못된 요청 오류입니다. 메시지 사용

ExceptionResolver 가 처리해준다.

스프링이 제공하는 ExceptionResolver2

DefaultHandlerExceptionResolver = 스프링내부
대표적으로 타입이 안맞을 경우

TypeMismatchException

"error": "Bad Request",

원래는 서버 내부 오류니까 500이다.

.w.s.m.s.DefaultHandlerExceptionResolver

다 알아서 바꿔준다.

정리

@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

@ControllerAdvice 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞이는것을 @ControllerAdvice 또는 @RestControllerAdvice를 사용하면 둘로 분리 할수 있다.

코드 분리해도 잘되네

대상을 적용하디 않으면 모든 컨트롤러에 다 적용됩니다.

대상을 지정할수도 있다.

보통 패키지 정도는 지정을 해준다.

너무 좋네

실무에서 너무 좋다.

정리

ExceptionResolver = 정상으로 바꿀수 있는 기회가 생겼다.

DispatcherServlet에서 3가지에 따라 처리하게 나뉜다.

사용자 정의 오류 추가.

여기서 끝내자. = ExceptionResolver

그리고 API 예외

스프링 내부의 500을 다 잡아서 400,500으로 변경

그러다가

@ExceptionHandler가 나옵니다.

AOP 처럼 동작 하는 Exception Handler

스프링 타입 컨버터

스프링 타입 컨버터 소개

상당히 많다.

모든 데이터는 문자다.

@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

불린->숫자.

문자 -> 다른타입, 다른타입 -> 문자.

쉼표 넣어주세요.

포맷터 = 컨버터의 특별한 버전

1000 -> "1,000" -> 1000

string 을 제외한걸 넣어주면 된다.

Number(부모) -> Integer, double(자식)

단축키 : 인라인 ctrl+alt+n

정리

컨버전 서비스에 등록해서 써야된다. = 편리하네

포맷터를 지원하는 컨버전 서비스

FormattingConversionService

public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {

포맷터 적용하기

주석 처리의 이유

문자 -> 숫자, 숫자->문자

우리가 만든것도 문자->숫자, 숫자->문자

h.t.formatter.MyNumberFormatter          : object=10000, locale=ko

h.t.formatter.MyNumberFormatter          : text=10,000, locale=ko

원래는 개입이 들어가야 되는 부분

스프링이 제공하는 기본 포맷터

기본 포맷터

구현체 보기

객체의 필드마다 다른형식을 지정하는것은 어렵다. 

@NumberFormat, @DataTimeFormat

너무 좋다.

Model.addAttribute() 안해도 된다.

th:field = 타입 컨버터 자동 적용.

오류 안났다 = 변환이 제대로 됬다. = form에 들어왔다.

나갈때도, 들어올때도 적용이 된다.

정리

컨버전 서비스

주의!

메시지 컨버터에는 컨버전 서비스가 적용되지 않는다.

잭슨 라이브러리한테 물어봐라.

파일 업로드

서블릿과 파일 업로드1

Received [GœYlB˜ BŒ1«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);

많이 왓네요.

정리