'JAVA의 정석'의 저자 남궁성의 Spring 강의를 듣고 정리하였습니다.
source: https://github.com/castello/spring_basic/tree/main
loginForm 만들기
id가 asdf이고 비밀번호가 1234이면 홈으로 이동하고, 그렇지않으면 일치하지 않는다는 메세지를 보낸다.
우리는 아이디 기억 기능을 쿠키를 이용하여 구현할 것이다.
css파일은 src/main/webapp/resources 경로안으로 이동하면 된다.
쿠키(Cookie)
이름과 값의 쌍으로 구성된 정보, 아스키 문자만 가능
✅ 한글의 경우 URL Encoding 해야한다.
- 서버에서 생성 후 전송되고 브라우저에 저장된다.
- 유효기간 이후에 자동으로 삭제된다.
쿠키는 오래두면 썩으니까 - 서버에 요청시 domain과 path가 일치하는 경우 자동으로 전송된다.
- ✅ 하위경로를 포함한다.
쿠키 작동 과정
쿠키 등록
1. 브라우저가 서버에게 요청을 보냄
2. 서버는 쿠키를 만들어 응답
3. 브라우저에 쿠키 저장
쿠키 전달
4. 클라이언트가 쿠키의 domain과 path에 일치하는 서버에 요청시 자동으로 쿠키가 포함되어 요청된다.
(페이지 접속시(get요청) 쿠키에 id가 저장되어있다면 서버에서 페이지의 id 칸에 id 이름의 쿠키값을 표시할 수 있다.)
쿠키 생성
서버에서 쿠키를 만들어 응답에 함께 보낸다.
✅ 상대시간과 절대시간을 함께 명시하여 보내는 경우는 서버의 시간이 잘못될때도 쿠키가 폐기되도록 하기 위함이다.
쿠키 삭제
쿠키 삭제는 조금 특이한데, deleteCookie 함수를 쓰는 것이 아니라 유효기간이 0인 새로운 쿠키로 갱신한다(덮어쓴다).
쿠키 변경
쿠키 변경시에도 변경 값을 가진 새로운 쿠키로 갱신한다.
쿠키 읽어오기
getCookies 함수를 이용해 리스트로 가져온다.
쿠키 실습
아이디 기억을 체크하고 로그인하면 쿠키가 저장되어 그 이후부터는 id가 유지되도록 한다.
loginForm.jsp
이메일 입력값을 cookie.id.value로 설정하고, 쿠키가 존재시 input checkbox가 checked 되도록 한다.
(당연히 쿠키값이 없으면 초기값은 빈값이다. id를 입력시에 값이 들어간다.)
✅ checkbox 체크시 String으로 값을 받으면 "on", boolean으로 값을 받으면 true이다.
브라우저에서 쿠키 생성하기
application에 id 이름의 쿠키를 만들었을때 loginForm의 id에 id값의 쿠키값이 입력되고 아이디 기억 체크박스도 체크된다.
컨트롤러에서 쿠키 생성하기
체크박스가 체크되어있으면 쿠키를 생성하고 응답에 저장한다.
그렇지않으면 쿠키를 삭제한다.(유효기간 0인 쿠키로 갱신한다)
세션(Session)
- 서로 관련된 요청들을 하나로 묶은 것 - 쿠키를 이용
- 브라우저마다 개별 저장소(세션 객체)를 서버에서 제공 (브라우저와 서버는 1:1)
✅ 쿠키는 브라우저에 저장되고, 세션은 서버에 저장된다.
서버는 같은 세션 ID를 가진 요청을 묶어 생각할 수 있다.
=> 서로 독립된 요청을 세션ID로 묶어 서버는 그 요청들이 같은 브라우저에서 왔음을 알 수 있다.
✅ login부터 logout까지를 한 세션이라고 생각할 수 있다.
✅ 세션은 쿠키에 담아 보낸다.
❄️ 쿠키 : 서버에 요청시마다 값을 보냄, 이 값으로 서버에서 응답시 사용해줘. (브라우저가 쿠키를 저장해 보냄)
🐬 세션 : 쿠키에서 온 값 중 세션ID를 보면 같은 브라우저에서 온 지 알 수 있다. (서버는 브라우저마다 세션 ID를 저장해서 요청을 분류할 수 있음)
-- 정확히 말하면 쿠키는 브라우저에 저장되고, 세션은 쿠키값들 중 하나로 브라우저에 저장되고 서버에서도 요청을 분류하기 위해 세션을 저장한다.
세션 생성 과정
1. 서버는 세션을 만들어 쿠키로 보낸다. Set-Cookie: JSESSIONID=~~~
2. 브라우저는 요청마다 세션ID를 포함한 쿠키를 보낸다. Cookie: JSESSIONID=~~~
세션 객체 얻기
브라우저마다 세션ID는 다르다.
요청에서 세션을 가져와 세션 저장소에 key-value쌍을 저장할 수 있다.
세션과 관련된 메서드
세션 종료
사용자가 로그아웃을 안할수도 있기때문에 자동 종료는 꼭 넣어줘야한다.
(세션이 끝나면 Standard Manager가 세션 객체 제거해준다.)
예시
세션 종료시 서버에서 새로운 세션을 만들어 응답한다.
브라우저는 새로운 세션을 저장한다.
🔖세션ID가 세션객체(세션저장소) 이름이다.
✅ 서버는 세션 저장소가 브라우저마다 있으므로 부담이 있다. 세션 저장소 사용시 최소한의 데이터만 저장하자.
쿠키 vs 세션
✅ 서버 다중화시 여러 서버에 세션값을 동기화해야하므로 불리하다 => 쿠키 사용하기
세션 실습
세션 id가 존재한다.
세션 ID 삭제시 서버에서 세션을 만들어 보내준다. (set-cookie 응답) => 브라우저는 세션을 저장한다.
그 이후 요청부터는 브라우저는 세션과 함께 보낸다.(Cookie: JSESSIONID=~~~)
url 태그의 중요성
❕ jsp에 url 태그를 붙이면 세션 생성시 세션을 저장하지않도록 설정된 브라우저를 대비해 url과 응답헤더에 세션을 보낸다.
❕ 세션 저장하지않도록 설정된 브라우저에게는 계속 url에 세션ID를 보낸다.
✅ url 태그 안붙이면 쿠키를 저장하지않도록 설정된 브라우저의 경우 계속 새로운 세션 ID가 만들어지고 브라우저는 저장하지 않는다.
세션 실습
boardController
로그인을 한 상태이면 보드로 넘어갈 수 있고 그렇지 않으면 로그인 화면이 뜬다.
요청을 보낸 브라우저가 로그인을 한 브라우저인지 확인하기위해 세션의 id값이 있는지 확인한다.
loginController
로그인 성공시에 세션에 id를 저장한다.
logout
로그아웃시 세션을 종료하고 홈으로 이동한다.
로그인시에는 로그아웃이, 로그아웃시에는 로그인이 뜨도록 한다.
index.jsp
session에 id가 존재하면 login을, 그렇지 않으면 logout을 보여준다.
현재 상황은 보드 클릭 -> 로그인을 안한 상태이면 로그인 후 홈 이동, 로그인시에는 바로 보드 이동이다.
로그인을 한 후에 홈이 아닌 보드로 이동하는 기능을 구현하려고 한다.
=> 그러러면 이전 요청을 알아야한다.
로그인 전 요청으로 리다이렉트
ch2 -> /board/list ->redirect-> /login/login (GET,POST) -> /board/list
홈 화면 -> 보드 요청 -> 로그인 상태가 아니므로 loginForm으로 이동 -> 로그인 성공 후 보드로 이동(loginController)
요청 URL 얻는 방법 : request.getRequestURL()
그 전 요청의 URL 얻는 방법 : request.getHeader("refer")
/login/login에서 /board/list URL을 바로 얻을 수는 없다.
/board/list url을 hidden form으로 /login/login에 값을 넘겨준다.
boardController
/board/list 접속시 로그인을 안한 상태이면 redirect한다. + 요청 url(/board/list)을 쿼리에 함께 보낸다.
loginForm
받은 url value를 input 태그 value로 넣는다.
이 값을 submit 하도록 하면 되는데, 보기 안좋으니 text가 아닌 hidden 태그로 하자.
값 submit 후 loginController는 toURL로 보내면 된다.
loginController
toURL을 파라미터로 받아 값이 존재하면 그 url로 redirect 한다.
결과화면
board에서 로그인안한 상태에서 로그인 후에 다시 board로 돌아온다!(성공)
session = "true", session="false"
session="true" : 세션 없을땐 새로 생성, default
session="false" : 세션 없을때 새로 생성X
=> 1. 세션이 필요없는 JSP 화면
=> 2. session=false가 기존 세션에 영향 X
윗 줄은 세션이 처음부터 끝까지 존재한다. 세션이 필요하지 않는 부분(로그인을 하지 않은 부분)도 존재하므로 좋지않다. 👎
아래줄은 세션을 사용하지 않을때는 세션이 새로 생성되지 않게 했다(session=false).
세션이 사용되는 로그인부터 session=true로 세션이 없을때 세션이 새로 생성되게 했다. 그 이후의 session=false는 이전 세션에 영향을 주지 않고, 만약 세션이 삭제되었다고 해도 새로 세션을 만들지 않도록 하는 기능을 가진다.
❕ session=false는 새로 세션을 생성하지 않는다는 뜻이다. 세션이 끊어지는게 아니다.
session=false일 경우
당연하게도 sessionScope화 pageContext.session은 사용 불가
index화면(홈화면)
@CookieValue
쿠키의 값을 불러와 사용할 수 있다.
예외처리
400 : client 에러
500: Server 에러
예외처리 실습
예외를 던졌을때 catch문으로 잡아 "error.jsp"를 보여주는 코드이다.
❓ 여기서 main함수 옆에 throws를 쓴 이유가 무엇일까? 이미 main함수 안에서 try-catch문으로 잡고있는데 이유가 궁금하다.
throws
throws: 호출한 곳으로 예외처리 떠넘긴다.
✅ public static void main() 함수에서 throws 사용시 JVM으로 예외처리를 떠넘긴다.
예외처리를 하지 않은 것과 같다.
✅ controller에서 예외가 발생할 경우 별도의 예외처리하지않으면 WAS까지 에러가 전달된다.
컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣)
❄️Controller 에러 발생시 전체 흐름
WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러 ->
컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣) ->
WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)
=> 즉 에러 컨트롤러를 한번 더 호출한다.
출처 : https://mangkyu.tistory.com/204
예외처리가 반복될때 - @ExceptionHandler
클래스 내부에서 묶어서 예외처리
코드마다 예외처리하지 않고 한번에 예외처리하는 방법 - @ExceptionHandler
ExceptionHandler(해당 예외 클래스)
: 클래스 내부에서 해당 예외 발생시 처리한다. catch 블록이라고 생각하면 편하다.
예외를 model에 담아 보내기
모델에 담아 view에 전달된다.
jsp
결과
전역 ExceptionHandler 만들기
@ControllerAdvice를 붙이면 모든 컨트롤러에서 발생하는 예외를 처리한다.
각 예외는 겹치지않아야한다.
예외처리하지않은 클래스
여기서 throws를 사용한 이유는 예외를 던지지만 해당 함수 내부에서 처리하지않기때문이다.
만약 global로 예외를 처리하는 핸들러와 클래스 내부에서 예외처리하는 핸들러가 같은 예외를 처리한 경우
가까운 클래스 내부 예외처리 핸들러만 호출된다.
✅ @ControllerAdvice는 기본적으로 모든 패키지에 적용된다.
✅ @ControllerAdvice(패키지명) : 해당 패키지만 적용한다.
유의할 점
Exception 발생하는 main 메서드의 Model m과 예외 발생시 처리하는 catcher 메서드의 Model m은 다른 객체이다.
main메서드의 model을 예외처리시 함께 보낼 수 없다. catcher 메서드는 Model m을 뷰로 전달할때 함께 보낼 수 있다.
예외처리 - 이론
@ExceptionHandler
<%@page isErrorPage="true" %> 사용하면 model에 error 전달 안해도 된다.
@ControllerAdvice : 전역 예외 클래스
@ResponseStatus
1. 예외 처리 메서드에서 예외 처리 후 에러 발생 상태코드인 400,500번대를 반환하도록 지정할 수 있다. (따로 상태코드를 지정하지않으면 예외처리 성공 후 200 OK 반환)
2. 사용자 정의 예외 클래스에서 기본 예외 발생시 500(Internal Server Error)이 반환되지만 원하는 상태코드를 지정해 반환할 수 있다.
예시 - 예외 처리 메서드
상태 코드 지정X
200(OK)이 뜬다.
상태 코드 지정시
예외처리 다 하고 200이 뜨는 것을 500뜨도록 한다.
❕ 컨트롤러가 반환하는 error.jsp의 ErrorPage="true" 속성 지정시 상태코드는 기본 500으로 반환된다.
(상태코드 지정시에는 지정된 상태코드가 반환)
예시 - 사용자 예외 클래스
화면
상태코드 500(RuntimeException)에서 400(Bad request)로 변경되어 뜬다.
❕ GlobalCatcher가 위 /ex3의 MyException을 처리할경우 상태코드는 500이 뜬다.
상태코드별 에러페이지 : web.xml <error-page>
상태코드별 에러페이지를 지정할 수 있다.
/error400.jsp
/error500.jsp
결과
❕화면에 잘 나오는지 체크할때는 무조건 크롬 시크릿 탭을 이용하자. 그렇지 않으면 이전의 값이 캐시로 남아있어 잘못된 결과를 보여준다.
내 시간아..
예외 종류별 에러페이지 : servlet-context.xml
exceptionMappings : 예외 종류별 뷰를 지정한다.
statusCodes : 에러시 뷰를 반환할때 지정할 상태코드이다.
ExceptionContoller2
MyException은 RuntimeException을 extends 했으므로 500번 에러이다.
❄️ MyException 예외 발생시 error400.jsp를 불러오고 400번 에러코드를 발생시키는 코드를 작성해보자.
servlet-context.xml
default 에러뷰는 "error.jsp"로 지정하고, MyException 발생시 /views/error400.jsp를 보여준 후 400번을 반환한다.
/views에 error400.jsp 만들기
✅ isErrorPage="false"로 해야 500이 아닌 400번 코드를 반환할 수 있다.
결과
처음화면
xml 수정 후 화면
400번 에러와 exception400.jsp를 잘 보여준다.
질문
❓ 500번 에러(RuntimeException)를 발생시키면 servlet-context.xml에서 MyException 에러를 잡아 에러 뷰와 상태코드를 알맞게 보여주는데,
400번 에러(Bad Request)를 발생시키면
이전 web.xml에서 지정했던 400번 에러를 처리하는 뷰가 뜬다.
MyException 에러이면 400번 에러여도 servlet-context.xml에서 설정한대로 에러뷰를 보여주어야하는거 아닌지 궁금하다.
혹시 400번 에러 발생시 에러 이름이 MyException이 아닌 Bad Request로 가는건지 ..? 유추해봤다.
✅ web.xml보다 servlet-context.xml에서 지정한 웹뷰를 우선으로 적용하는 것 같다.
ExceptionResolver
Controller에서 예외 발생시 try-catch로 처리하지않고 throws하면 DispatcherServlet가 처리한다.
DispatcherServlet은 예외를 처리여부를 확인하기위해 handlerExceptionResolvers로 등록된 3가지 예외처리 기본 전략을 본다.
✅ DispatcherServlet의 인스턴스 변수로 handlerExceptionResolvers를 가진다.
1. ExceptionHandlerExceptionResolver
발생한 예외를 처리할 수 있는 ExceptionHandler를 찾는다.
못 찾으면 그 다음으로 넘어간다.
2. ResponseStatusExceptionResolver
@ResponsStatus의 상태코드를 변경해주고 web.xml에 해당 상태코드가 있으면 처리한다.
3. DefaultHandlerExceptionResolver
스프링에 정의된 예외의 상태코드를 기본 500에서 400,500번대로 바꿔줌
(예외에 해당하는 상태코드로 바꿔줌)
스프링에서의 예외 처리
try-catch
클래스 내부 - @ExceptionHandler 메서드
클래스 내부의 예외 처리가 가능하다.
@ControllerAdvice - @ExceptionHandler 메서드
지정된 패키지 또는 모든 패키지의 에러처리 적용이 가능하다.
예외 종류별로 뷰 지정 - SimpleMappingExceptionResolver
응답 상태 코드별로 뷰 지정 - web.xml의<error-page>
DispatcherServlet 파헤치기
DispatcherServlet이란?
서블릿과 컨트롤러에서 공통으로 처리할 수 있는 부분을 전처리한다.
Spring MVC의 요청 처리 과정
1. 요청 /ch2/register/add 이 오면 HandlerMapping에게 보내고, 해당 URL과 일치하는 메서드를 얻는다.
✅ 관심사의 분리 : DispatcherServlet이 모든 것을 처리하지않고, HandlerMapping 별도의 기능을 분리한다.
2. DispatcherServlet은 메서드에 따라 적합한 HandlerAdapter를 호출하고, HandlerAdapter는 적합한 Controller(또는 Servlet)를 호출한다. 해당 뷰를 얻는다.
✅ 느슨한 연결 : 변경에 유리하다. DispatcherServlet과 Controller는 느슨한 연결이다.
3. ViewResolver(기본적으로 InternalResourceViewResolver)를 호출하여 실제 뷰 이름을 얻는다.
❄️ InternalResourceViewResolver 기능 : "registerForm" -> "/WEB-INF/views/registerForm.jsp"
4. JstlView를 호출하여 뷰를 호출하고 모델을 전달하면 jsp파일이 모델을 이용해 응답 결과를 만든다.
✅ JstlView : 뷰 인터페이스, 느슨한 연결로 변경에 유리하도록 구조
서블릿 filter - 스프링 interceptor
전처리와 후처리를 담당한다. interceptor는 filter와 비슷하지만 발전된 개념이다.
DispatcherServlet의 소스 분석
✅ 실제 코드를 보는 것은 실력 향상에 도움이 된다.
데이터의 변환과 검증
WebDataBinder
@ModelAttribute는 해당 객체를 모델에 저장해준다.
저장하기 전에 webDataBinder가 1. 타입 변환 2. 데이터 검증을 수행하고, 에러 결과는 BindingResult에 저장한다.
컨트롤러는 BindingResult의 값을 확인하여 적절한 처리를 한다.
1. 타입 변환
날짜의 경우에 형식이 다양하므로 어떤 형식이 입력으로 들어오는지 지정해야한다.
1. 메서드를 만들어 지정해주는 방법 (toDate)
2. 필드 위에 포맷을 적어주는 방법 (@DateTimeFormat)
실습
/register/add
/register/save 접속시(회원가입 누르면) 정보가 User에 저장된다.
registerController
BindingResult result 를 바인딩할 객체에 바로 뒤에 적으면 에러 결과를 담는다. (에러 페이지도 안뜸)
현재 날짜 형식은 yyyy/MM/dd인데, yyyy-MM-dd로 작성하면 에러가 발생한다.
위 에러를 해결하기 위해서는 변환 형식을 알려줘야하는데, 2가지 방법이 있다.
1. 메서드 만들기
날짜 형식을 변환 형식에 추가한다.
2020-10-30하면 오류가 해결해야하는데 에러가 그대로 발생한다..왜지..?
=> chatgpt에게 물어본 결과 Date로 인한 문제같다고 했다. LocalDateTime을 바꿔 사용하면 해결할 것이라 했다.
hobby에서 piano#guitar#cook 값을 입력받았을때 [piano#guitar#cook]을 [piano, guitar, cook]으로 변환해야한다.
#을 구분자로 설정하는 코드
new StringArrayPropertyEditor("#")
: 구분자 #
2. 필드 위에 포맷 명시
결과
날짜가 잘 처리된다.
PropertyEditor
양방향 타입 변환 (String -> Type, Type -> String)
특정 타입이나 이름의 필드에 적용 가능
StringArrayPropertyEditor("구분자")
: 문자열 배열에 담겨진 String을 구분자로 나누어 각각 배열에 저장한다.
✅ 참고 :binder.registerCustomEditor
: "hobby"에만 적용하려면 두번째 인자에 작성해야한다.
- 디폴트 PropertyEditor : 스프링이 기본적으로 제공
- 커스텀 PropertyEditor : 사용자가 직접 구현. PropertyEditorSupport를 상속하면 편리
변환 범위
모든 컨트롤러 내에서의 변환 - WebBindingInitializer
특정 컨트롤러 내에서의 변환 - 컨트롤러에 @InitBinder가 붙은 메서드를 작성
Converter와 ConversionService
PropertyEditor와 다르게 단방향 타입 변환이다.
PropertyEditor는 stateful한데, 자바 객체의 인스턴스 변수를 Property라고 하고, 이를 바꿀때 사용하기 때문이다. 그러므로 싱글톤이 될 수 없다. (변환시마다 변환 함수 객체 만들어야함)
Converter는 stateless 하여 그럴 필요가 없다.
Converter 생성 후 ConversionService에 등록해야한다.
conversionService
출력 화면
Formatter
양방향 타입 변환을 위해 두개의 단방향 타입 변환 함수를 구현한다.
타입 변환기 등록하는 곳
1. 커스텀 PE
2. ConversionService
3. 디폴트 PE
우선순위는 가까운 쪽부터 1>2>3
Validator
객체 검증 인터페이스, 객체 검증기(validator) 구현의 사용
supports : 검증 가능한 객체인지 알려줌 ex) User라면 User 또는 그 자손 객체인지 확인
validate : 객체를 검증 , 파라미터는 검증할 객체와 에러저장소
ValidationUtils.rejectIfEmptyOrWhitespace(error, "pwd", "required");
: 비었거나 공백이면 pwd에 required 에러 코드 저장
Errors
- reject : 객체 전체 에러 저장
- rejectValue : 필드 하나에 대한 에러 저장
Validator를 이용한 검증 - 수동
이전에는 컨트롤러 메서드에 검증 코드가 포함되어 있었지만 검증 부분을 따로 분리하여 더욱 깔끔해졌다.
Validator를 이용한 검증 - 자동
setValidator() : WebDataBinder binder에 검증기를 등록한다.
@Valid : 신규 회원 등록시 객체 앞에 @Valid를 붙인다.
컨트롤러 내부에서만 사용이 가능하다.
글로벌 Validator
servlet-context.xml에 globalValidator를 빈으로 등록하면 된다.
글로벌 Validator 등록 후 따로 로컬 Validator를 추가할 수 있다. addValidators 메서드를 사용하여 validator를 추가하면 된다.
실습
UserValidator 만들기
객체가 User 또는 그 자손인지 확인하고, id와 pwd를 검증한다.
Validator를 이용한 검증 - 수동
Validator를 이용한 검증 - 자동
로컬 validator에 등록
@Valid는 Maven Repository에 있으므로 Maven의 validation API 불러오기
https://mvnrepository.com/artifact/javax.validation/validation-api/2.0.1.Final
프로젝트 - maven - update project해서 적용하기
Global Validator 만들기
servlet-context.xml
userValidator도 등록해주기
결과화면
MessageSource
다양한 리소스(파일, 배열 등등)에서 메시지를 읽기 위한 인터페이스
args: 메세지에 사용될 값 ex) 글자 길이 정보, local : 지역정보
errorCode에서 properties의 key를 명시하면 value값이 출력된다.
ex) user객체의 id 필드 검증시 errorCode="required"
1. required 찾기
2. required.user.id 찾기
3. required.id
4. required.java.lang.String
순으로 찾는다. (객체와 필드 이름 조합)
그래도 없으면 defaultMessage로 메세지 설정
찾은 에러 메세지 출력
실습
경로 중요: src/main/resource이다. src/main/webapp/resources아님 !!!!!!!
resources/error_message.properties
messageSource 등록하기 - servlet-context.xml
검증 메시지의 출력
id에 해당하는 메세지 보여주기
검증 메시지의 출력 - 실습
registerForm.jsp
userValidator는 우선 주석처리해서 globalValidator가 호출되도록 한다.
globalValidator - 인자 넣어주기
0번째 인자 "", 1번째 인자 "5", 2번째 인자 "12"
출력 화면
'Spring > 카테캠 - TIL' 카테고리의 다른 글
TIL [0605-0611]: 페이지네이션, 타임라인, 트랜잭션, 동시성 제어 (0) | 2023.06.09 |
---|---|
TIL [0531 - 0604] : 대용량 처리를 위한 MySQL 이해 (0) | 2023.06.03 |
TIL [0521] Spring MVC 3 (0) | 2023.05.21 |
TIL [0519-0520] Spring MVC 2 (2) | 2023.05.21 |
TIL [0511- 0514] : Spring Basic + Spring MVC 1 (2) | 2023.05.14 |