
+) {{>}} include 같은거
mustache
if 문은 안되는데 조건문은 쓸 수 있음
일단 mustache 는 3가지 문법이 있음
- {{key}} → 출력 (jsp의 el표기법이랑 같음)
- for문 if문 (연산X)
컬렉션 {{#컬렉션}} {{/컬렉션}}→ 반복문
오브젝트 {{#오브젝트}} {{/오브젝트}} ex ) {{#sessionUser}} {{/sessionUser}}
→ 존재하면 혹은 null 이면 처리 → #sesionUser 가 null 이 아니면 실행되게 됨 (이런식으로 인증 조건 준다)
Boolean {{#Boolean}} {{/Boolean}} → true 실행 false 실행안함
{{#sessionUser == 1}} → 이런 조건문은 줄 수 없음 → 불가능
{{#model.user.id == sessionUser.id}} → 이런 것도 불가능함
- 컬렉션
컬렉션<String>→ String 타입의 컬렉션은 → {{#컬렉션 }} {{.}} {{/컬렉션}}
컬렉션<Board> {{#컬렉션}} {{title}} {{/컬렉션}} → #models 안에 있던 title 은 그냥 title 쓰면 출력됨
SessionUser를 사용해서 Header 상단 nav bar 조건주기
header.mustache

이렇게 하고 실행하면 nav 바에 로그인/회원가입만 보이고
로그인하면 nav 바에 글쓰기/로그아웃만 보인다.
(→ sessionUser 가 true 이면 (= sessionUser 가 있으면) 글쓰기/ 로그아웃이 보이도록 설정한 것)
192.168.0.99:8080/board
선생님 서버로 가면

내 브라우저에는 쿠키가 저장되어있지 않으니까
Cookies 가도 JsessionId 가 없음



nav 바를 보면 로그인 되어있는걸 확인할 수 있다.
(로그인 되었을 때는 nav 바가 글쓰기/로그아웃 이렇게 나옴)
++
옛날에는 컴퓨터의 templates 에 sessionId 가 저장되었는데
보안의 문제로 브라우저의 cookie 에 저장되도록 바뀌었다.
UserController

로그아웃 생성
session의 모든 걸 다 제거 시킨다.
내가 요청 안했는데, 서버가 자기 스스로 session의 정보를 삭제할 수 없다.
왜냐 키는 내가 내 브라우저 쿠키에 들고 있으니까.
BoardController
복습 )

/board/save 어? save 왜 붙였어요 ? 라고 물어보면
http 1.0 방식으로 연습해서요!
자바스크립트 없이 스프링으로만 집중해서 연습하고 싶어서 그렇게 ㅁ나들었따.
이게 내가 이번에 만든 포트폴리오의 컨벤션입니다.
지금 제일 최신은 http 3.0 이다.
지금 웹의 트렌드가 3.0이기때문에 다른 회사들도 쓸 것.
왜 2.0이 나오고 3.0이 나왔는지 역사가 있음. 나중에 알아보자
/board/save 리팩토링 하기

BoardRequest 자바파일 생성
BoardRequest

코드 작성
- save 요청할 때 필요한 것을 필드로 만들어 준다 → title, content, user_id
- 날짜는 board 엔티티에 @CreationTimeStamp 붙여주면 자동으로 들어가니까 필드로 X
- insert 할 때는 toEntity 가 필요하다 ! toEntity 매서드 만들어주기.

- 네이티브 쿼리를 쓸거면 상관없지만 persist 를 쓰려면 위의 fk를 설정해주어야 한다.
- 그리고 날짜가 자동으로 생성되도록 createdAt → @CreationTimestamp 어노테이션을 붙여주자

BoardRequest의 toEntity에 sessionUser 받도록 추가

Board 생성자에 user를 추가하면 toEntity 매서드의 에러가 사라진다.
Q ) HttpSession을 Autowired 할 수 있는 이유는 ?
→ session은 싱글톤이기 때문에 Ioc에 저장가능.
편하다. 이렇게 안하면 request.getSession 해서 계속 꺼내써야 한다.

/board/save 를 마저 완성해주고,
BoardRepository 가서

save 매서드 리팩토링 해주고

컨트롤러에 인증 체크 코드 추가

글쓰기 화면으로 가서 (/board/save-form)
로그아웃 된 상태에서 ( 쿠키에 sessionId 업서야 함)
글쓰기를 시도하면
우리가 컨트롤러에 throw 해준 exception이 터진다 :)

++
협업할 때는 아래처럼 접어두는 게 좋음

‘인증’이 안되었을때는 http 상태코드가 401 이뜬다.
‘권한’이 없을때는 http 상태코드 403을 던진다. (forbidden)
유효성 검사 통과못하면 http 상태코드 400을 던진다.
컨트롤러는 유효성검사 해야함.
유효성 검사 할 때 상황에 맞게 설정해주어야한다.
‘인증체크’는 컨트롤러가 할 수 있다. session 값만 조회해서 하면 되니까.
‘권한체크’는 컨트롤러가 아닌 레이어에서 한다. 데이터베이스 정보가 필요하니까
기반정보(디비정보)가 필요한 권한체크 같은거는 컨트롤러가 아닌 다른 레이어에서 한다.
Repository는 데이터베이스 CRUD 하는게 SRP이니까
권한체크는 Repository한테 시키지말고 새로운 레이어한테 시킨다.
(→ Service 한테 시키는데 응답 ? DTO 를 만들어서 return 할거라서
BoardResponse 의 ‘어쩌고DTO 매서드’ 안에서 권한체크를 한다.
board랑 boolean 2개 다 service 의 해당 매서드에서 return 을 못하니까 )
게시글 상세보기 리팩토링

권한체크 추가


로그인해도 버튼이 나오지 않는다.
컨트롤러에서 기본 false 로 fix 해두고 실행시켰기 때문.

login-form → value 추가

join-form → value 추가
이렇게 설정해두면 지금 상세보기 페이지를 작업하는 나는 회원가입/ 로그인 일일이
입력할 필요없이 바로 상세보기로 들어가 확인/작업을 할 수 있다.
보통 팀장이 이런 걸 세팅해준다.
팀장은 패키지세팅부터 잡기술 그리고 위처럼 핵심기능만 구현할 수 있도록
value 세팅 등을 해주고, 팀원은 핵심 기능만 구현하도록 한다.
서비스 레이어의 SRP은 2 + 1가지
비즈니스를 파악하면 바디데이터가 보인다.
먼저 내가 요청하는게 read인지 write인지 파악.
예를 들어 은행 이체를 할때는
이체 요청 → post 요청 → 뭘 등록할 건가 ?
request가 짐을 지고 가는데 get요청할 때는 이게 필요없고
post 요청할 때는 필요. 무슨 짐이 필요할까 ?
최소 key가 3개 필요하다.
sender account id
receiver account id
amount : 3000
Repository의 역할은 2가지
- DB 쿼리 ( 데이터베이스랑 일하기 )
- 파싱 → 테이블 데이터를 자바의 Object 로 바꾸는거 ( 안녕 → 봉주르 )→ JPA 쓰면 파싱은 하이버네이트가 해준다.
중간에 새로 추가될 레이어 → Service
Service의 역할은 2 + 1가지
컨트롤러 내부는 복잡하다.
하지만 외부에 주소를 공개해야 한다. post인지 get인지 request할때 어떤 데이터가 필요한지 등등
을 외부인한테 알려줘야 한다.
ex ) 팥빵이 필요하면 오후 5시에 5천원을 가지고 와.
라고 공표해준다. 그게 인터페이스 ⇒ 컨트롤러
컨트롤러는 인터페이스다.
ex 2) 자동차가 있다고 할 때,
Service는 내부, Repository는 엔진 이고
내부를 통해서 엔진으로 가도록 해주는 통로인 Controller는 엑셀이다.
⇒ 사용자에게는 엑셀(Controller)만 노출함. 그러면 내부와 엔진이 돌아감.
사용자가 Service와 Repository 까지 알 필요는 없음.

트랜젝션 ACID
정리
Service 레이어는
- 트랜젝션 (일의 최소단위) 관리
→ 일의 최소단위는 상대적이다.
누구는 로션만 바르고 누구는 화장까지 하면 둘의 최소단위는 다르다
ex ) 이체하기
2개의 계좌를 조회하고 update 해야한다.
→ findById - findById - Update - Update
→ 이 4개가 이체하기의 트랜젝션이다. (이체하기 를 위해선 최소 이 4개의 과정이 필요하다 )
- 비즈니스 로직 처리
- 레이지 로딩 마무리 ( → 컨트롤러에서는 못하게 설정해둘 거니까 )
3가지 역할을 한다.
++
회사가면 제일 많이하는게
트랜젝션 처리하고 비스지스 로직 처리하는 것이다 !
++
스프링이 시작되면 큰 성이 만들어진다.
성의 창고는 데이터베이스이다. (창고 = 디비)
외부에서 reuqest 요청이 들어왔다.
thread가 만들어지면서 request 객체가 만들어짐.
개개인마다(개별 request 마다)
데이터베이스 커넥션 객체를 만듦 ( 커넥션은 데이터베이스에 접근 할 수 있는 키 같은거 )
→ 커넥션은 해당 thread가 디비에 접근할수있는 권한을 줌
DB 접근 권한 이 성(스프링)에서 놀때 계속 필요한게 아니라 창고 (디비)에 접근할때만 필요하다.
성에 들어오면 커넥션을 받고 컨트롤러 끝날때 커넥션이 닫힌다.(없어진다)
컨트롤러에서는 커넥션이 있으니 DB조회가능
서비스에서도 가능
리퍼지토리에서도 가능
하지만 실무가면 서비스 레이어(Service)가 종료될때 커넥션을 닫아버림
왜?
컨트롤러에서 return될때 미친듯이 레이지 로딩이 일어날 위험성이 있어서
실무에서는 커넥션을 닫아버린다!
(어 미처 가져오지 못한 테이블의 필드가 필요해! lazy loading.
어 나도. 어 나도. 이런게 한 두개가 아니면 return 될때 lazy loading 다 걸릴텐데
그럼 무한 Select 사태가 일어날 수 있음)
자세히 이해하기 위해
아래의 Open in View 를 알아보자.
Open in View

실행시킬 때 나오는 이 경고 WARN.
“뷰가 렌더링 될 때 조심해라! 강제로 disable 해라!”
고 권고하는 문구이다.
왜 스프링부트는 이런 경고를 날릴까 ?
→ lazy loading 이 발생할 수 있으니 위험하다고 경고하는 것이다.
→ spring.jpa.open-in-view=false 설정 해주면 실행 시 WARN 이 뜨지 않는다.
( 아 그거 내가 알고 있는 부분이니 말 안해줘도 돼 라고 하는 것 )
application.properties 에 가서 아래 설정을 추가해보자.

일단 true 로 설정해주자.
- 원래 default 설정이 true 로 되어있긴 함
++
public @responsebody Board test () {}
→ @ responsebody 를 매서드에 붙이면 뷰 리졸버(뷰 해결사)가 발동안함
( = 템플릿 엔진이 발동안함 )
스프링의 메세지 컨버터가 컨트롤러에서 return시에 이 일을 해줌
메세지 컨버터란
자바 객체를 JSON뿐만 아니라 다양한 형식(String, ByteArray 등등 많음)들로 변환가능하게끔 해주는것이다.
@ responsebody 를 붙이면 메세지 컨버터가 발동함
( → 자바 Object를 return 해서 줄 수는 없으니 자바 Object를 문자화( 예. JSON, String ) 함 )

예) Board Object 를 return 할 때 문자화해서 보낼라 하는데 (위 그림처럼)
Board 객체에 가서 일단 getter를 때린다.

→ get ID, get TITLE, get CONTENT , get CREATEDAT 을 때리고 보니 User 필드도 있네.
USER가 있으니 get USER 때림. User가 맞는 순간

User 엔티티로 가서 ID, USERNAME, PASSWORD… 등 쭉 getter로 때림
그렇게 하고나면 Board + User 가 JSON으로 return 됨
→즉, 나는 Board 를 return 시키려고 했을 뿐인데
return 하면서 JSON을 만들려다가.. JSON으로 만드는 과정에서 getter를 때려서 가져옴. Board를 때리는데 이때 USER 도 때ㄹㅐ버림 → GET USER를 때렸으니까 SELECT 유저 매서드가 발동됨 → 레이지 로딩 실행됨 → 브라우저에서 확인하면 응답헤더에 CONTENTTYPE 에 APPLICATION/JSON (→MIME타입) 되어있음
application.properties 의 openInView를 false로 설정했을 때!
컨트롤러에서는 커넥션이 없어져서 레이지 로딩이 안됨
(→ findAll 해서 getter를 때렸지만 DB 커넥션이 닫혔기때문에 select from user_tb 가 발동안함
→ 에러남)
서비스 레이어까지는 켜져있어서 service에서 DB에 필요한거 다 처리하고 와아함!
서비스매서드가 종료되고 컨트롤러로 들어올때 커넥션 꺼지면서 들어옴
그래서 컨트롤러에서 return할때 커넥션이 없어서 SELECT USER 실행안됨
커넥션의 생존범위 그림
++
Controller 에서 리턴할때 레이지로딩 이 발동됨

코드 실습
레이지 로딩이 진짜 return 할 때 발생하는지 보자.
(지금 ap에 설정은 true로 되어있는 상태)

Id는 된다.
findAll 할 때, boardList 에 title, content 등과 함께 user_id는 들고왔기 때문!
그래서 select 실행없이 바로 getUser().getId() 가 가능함

→ 하지만 boardList 에는 user_id 밖에 없으니 getPassword 하면 레이지 로딩이 실행됨
지금은 Open In View 가 켜져 있는 상황이라 lazy loading 이 실행되었다.
Open In View 를 끄고 하면 어떻게 될까 ?

openInView 를 false 로 바꾸고

레이지 로딩을 할 수 없으니 Lazy 어쩌고 에러 발생
컨트롤러에서 getPassword 할 수 없음.
데이터베이스 못가.
에러

→ No session 에러
2024-08-22T11:31:25.010+09:00 ERROR 14700 --- [blog] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.hibernate.LazyInitializationException: could not initialize proxy [shop.mtcoding.blog.user.User#2] - no Session] with root cause
org.hibernate.LazyInitializationException: could not initialize proxy [shop.mtcoding.blog.user.User#2] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:165) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:314) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:44) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at shop.mtcoding.blog.user.User$HibernateProxy$1FYnlx0Z.getPassword(Unknown Source) ~[classes/:na]
at shop.mtcoding.blog.board.BoardController.testBoard(BoardController.java:40) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.11.jar:6.1.11]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.11.jar:6.1.11]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.11.jar:6.1.11]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.11.jar:6.1.11]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.11.jar:6.1.11]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.11.jar:6.1.11]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.11.jar:6.1.11]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.11.jar:6.1.11]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.11.jar:6.1.11]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.11.jar:6.1.11]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:389) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
아래처럼 설정하고 해보기 !
- Eager → board user select
- osiv → false
- controller → lazy loading
→ 잘됨
- Lazy → board select
- osiv → false
- controller → lazy loading (no session)
→ 에러
- Lazy → board select
- osiv → true
- controller → lazy loading (User select 발동 (getter 때리니까) → 정상)
→ 정상 ( but 레이지로딩 발동)
→ 그래서 회사에서는 false로 해두고 컨트롤러에서 레이지로딩이 안되게 함
협업할 때는
Lazy & osiv = false 로 설정 해두고 하자! 라고
프로젝트 시작전에 팀원들과 약속하고 해야함. 코드 개판되니까.
++
양방향 매핑 발생이유 → OPEN IN VIEW 가 TRUE 이니까 !
open inview false로 하면 다 해결됨.
그러기 위해서는 open in view 가 뭔지 알아야 한다.
리팩토링 (계속)

이렇게 IOC에서 꺼내쓰는게 DI
DI의 원래 원리는 생성자 주입! (기본생성자를 때려서 생성됨)
IOC에 등록안해두면 내부에 DI를 할 수없음
@Controller 어노테이션을 읽고 디폴트 생성자를 때림
매개변수가 있는 생성자는 그 매개변수가 IOC에 없으니까 못때림…
public TestClass(Hello hello) {
this.hello = hello;
}
위와같은 생성자가 있다면 Hello 가 IOC에 없으니까 DI를 할수 없음.
@Controller @Repository 등 어노테이션의 메타어노테이션에
@Component 가 걸려있어야지 컴포넌트 스캔이 됨(→ 자바로 치면 상속)
→ 그래야 기본생성자를 때려서 객체가 ioc에 등록되어 관리됨
의존성 주입
: Controller가 new 될때 생성자의 매개변수를 ioc에서 가져와서 di를 한다.
final(상수)은 객체가 new가 될때 초기화 됨.
필드로써 초기화 되지 않은 상태로 존재할 수 없다.
→ 프로그램 시작되어 초기화가 되고나면 write 못하고 read 만 가능하도록 함.
의존성 주입할 때는 final을 붙인다. (안전)
오토와이어드보다 깔끔한게 생성자주입
@Component 만 붙여놓으면 IOC 가 들고간다.
IOC에 등록하는 객체 생성할 때, 기본생성자 랑 일반 생성자가 있으면 기본생성자를 때린다.
기본생성자가 없으면 일반생성자를 때린다.
이때 @RequiredArgsConstructor (롬복) 을 클래스에 붙여주면
→ final이 붙은애들은 다 생성자 만들어준다. ( 생성자 주입 매개변수로 넣어줌 )
그래서 ioc에서 가져올 필요가 없는 필드는 final만 지워주면 된다. 그럼 생성자 생성할때 매개변수로
안들어가서 생성안됨
UserController

@RequiredArgsConstructor
붙여주고 생성자 주입으로 DI 할 애들한테 final 상수 붙여줌
이렇게 하면 @Autowried 하던거랑 같음 (생성자 주입으로 바꾸었을뿐)
왜 필드주입, 매서드주입보다 생성자 주입을 선호하는가?
UserRepository

역시 em을 생성자주입으로 바꿈
BoardController

여기도 생성자주입으로 리팩토링
BoardRepository

여기도 생성자 주입으로 리팩토링
끝
BoardService

생성

Board(상세보기)와 Boolean(권한체크) 2개를 return 하지 못하니까 (→ 자바는 안됨)
응답을 위한 DTO를 만든다.
프리젠테이션 계층에 필요한 데이터만 주려고 DTO를 만드는것!
상세보기 페이지에서는 어떤 데이터가 필요한가?
제목, 내용, username, isOwner → 4개 데이터가 상세보기 에서 필요하다.
4개 데이터를 담는 DTO를 만들자.
BoardResponse

생성 후, DetailDTO 를 내부클래스로 생성한다. (@Data = getter, setter, toString )

필요한 4개 필드와 각 테이블의 pk인 boardId, userId 를 필드로 넣었다.
(→ 필요한 필드 외에 테이블의 pk는 넣어줘야함. 그리고 둘은 각각 엔티티에서 id로 쓰이는데
DTO에서 이름이 겹칠 순 없으니 boardId, userId 로 이름지어줌)
응답 DTO를 만드는 원칙은 화면에 필요한 걸 매개변수로 받는다는 것!
→ Board 와 sessionUser 를 매개변수로 받았다.
→ sessionUser 는 권한체크에 필요.

다시 BoardService로 가서 DTO로 받는 코드로 수정하자
BoardService

package shop.mtcoding.blog.board;
// C -> S -> R
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shop.mtcoding.blog.user.User;
@RequiredArgsConstructor
@Service
public class BoardService {
// DI 생성자 주입
private final BoardRepository boardRepository;
// 기능명은 한글로 적는다.
// 매서드는 행위 -> 동사
public BoardReponse.DetailDTO 상세보기(int id, User sessionUser) { // 매개변수는 컨트롤러로부터 받는다.
// final Httpsession으로 받으면 session.getAttriute해서 user로 다운캐스팅 해야해서 귀찮아지니까 이렇게 안하고
// 세션 인증 체크는 컨트롤러에서 할거고 지금 서비스에서는 권한 체크를 할거니까 sessionuSER는 매개변수로 받아도 충분
Board board = boardRepository.findById(id); // 조인한 쿼리 쓰니까 (Board - User) 둘다 있음
// 여기 null 처리 할 필요가 없음. findById 에서 null 이 들어오면 throw로 던지니까 서비스에서 board 값으로 null을 받을 일이 없음
return new BoardReponse.DetailDTO(board, sessionUser); // board 와 boolean 2개 리턴 불가능 해 dto 만들기
}
}
코드가 매우 깔끔해짐
BoardController

@GetMapping("/board/{id}")
public String detail(@PathVariable("id") Integer id, HttpServletRequest request) {
/* Board board = boardRepository.findById(id);
request.setAttribute("model", board); // 1건 이니까 그냥 model로 적어주기!
request.setAttribute("isOwer", false);
*/
User sessionUser = (User) session.getAttribute("sessionUser");
BoardReponse.DetailDTO detailDTO = boardService.상세보기(id, sessionUser);
request.setAttribute("model", detailDTO);
return "board/detail";
}
BoardController도 리팩토링 해줌.
Service.상세보기 에서 return 받은 detailDTO를 model에 담고, detail.mustache 이동
detail.mustache

기존에 코드는
model.setAttribute(”isOwner”, 값)
model.setAttribute(”model”, boardList)
로 보내서
detail 페이지에서
{{#isOwner}}
{{/isOwner}}
{{model.user.username}}
이런식으로 값을 꺼냈어야 했는데
이제 model 에 담긴게 Board 객체가 아닌 detailDTO 로 바꿨으니까
model에서 값을 꺼내는 코드를 수정한다.
BoardReponse
package shop.mtcoding.blog.board;
import lombok.Data;
import shop.mtcoding.blog.user.User;
public class BoardReponse {
@Data
public static class DetailDTO {
// 화면에서 안쓰더라도 pk는 무조건 들고가도록 한다.
private Integer boardId; // pk
private String title;
private String content;
private Boolean isOwner;
private Integer userId; // pk
private String username;
public DetailDTO(Board board, User sessionUser) {
this.boardId = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isOwner = false;
// service에서 응답을 1번만 해야하니까 board와 sessionUser 를 dto에 담아서 한번에 return 한다.
// 권한체크
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true;
}
this.userId = board.getUser().getId();
this.username = board.getUser().getUsername();
}
}
}
위의 boardResponse 의 내부 클래스 DetailDTO 에
지역변수로
userId,
username
이 들어있으니까 ( DTO 만들때 “상세보기 페이지”에서 username이 필요하다는걸 미리 생각했기 때문에 지역변수로 넣음 )
detailDTO.user.username 이 아니라
detailDTO.username 으로 바로 꺼낼 수 있는거임
detail.mustache
{{>layout/header}}
<div class="container p-5">
{{#model.isOwner}}
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<a href="/board/{{model.boardId}}/update-form" class="btn btn-warning me-1">수정</a>
<form action="/board/{{model.boardId}}/delete" method="post">
<button class="btn btn-danger">삭제</button>
</form>
</div>
{{/model.isOwner}}
<div class="d-flex justify-content-end">
<b>작성자</b> : {{model.username}}
</div>
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr/>
<div class="m-4 p-2">
{{model.content}}
</div>
</div>
</div>
{{>layout/footer}}
detail.mustache 에서 model 데이터 타입 (detailDTO) 의 구조에 맞게 값을 꺼내도록 수정하면
상세보기 잘 나온다.

여기까지 상세보기 마무리
++
서버사이드랜더링( 서버 측에서 해석 ) SSR
클라이언트사이드랜더링 CSR
++
만약에 request.setAttrivute 를 한 10번 때리고 걍 막 만들었다.
뷰에서 if로 로직을짜서 어떻게 완성시켰다.
그리고 이걸 앱으로 출시 하려면
앱은 뷰가 아니라 JSON을 응답해줘야 하는데 뷰에서 연산하도록 하면 안된다.
- 비즈니스 로직을 서비스에서 완벽하게 짜서 주기
- 필요한 데이터만 정제해서 주기
위의 원칙을 지켜야한다.
서비스까지 잘 만들어두면 (= 연산을 서비스에서 다해서 데이터만 넘겨주면)
컨트롤러는 데이터를 받아서 뷰에서 뿌리기만 하면됨.
뷰는 순수하게 렌더링 용도로만 쓴다!
로직이 뷰에 들어가는 순간 웹을 앱으로 바꿀때 다 수정해야한다….
최근에 백앤드 개발자는 뷰를 만들지 않고, json 같은 데이터를 리턴하는 코드를 주로짬.
브라우저가 아닌 다른 모든곳에 보내는 건 HTML이 아니라 (JSON)데이터 형태로 보냄 (?)
[ 정리 ]

이체하기의 일의 최소단위가
1계좌
2계좌
둘 다 update 해야하는 것이라면 → 일의 최소단위가 2개- > 트랜젝션 → 트랜젝션을 관리해야함
모든 일이 다 완료되면 커밋
안되었으면 롤백
상황마다 일의 최소단위가 달라진다.
a → (이체 ) → b → (이체) → c
2번째 이체에서 실패하면 롤백이 안되어서 repository에서 트랜젝션을 안한다!
( → 트랜젝션 ACID 중 A 해당하는 원자성)
이체하기 할 때 잔액 검증이 필요.
쿼리를 잘 짜면 비즈니스 로직을 잘 짤수있다.
그래서 처음 회사가면 조인, 이런거 해서 쿼리부터 맡는다. 쿼리를 잘짜야한다.
쿼리 짜는게 중요하다
++
필터.
필터자체는 프록시로 할 필요가 없다.
나중에 AOP로 만들예정.
++
스트림
양방향매핑

- DS 앞에 filter 를 만들어서 인증처리를 할 수 있다.
- Dispatcher Servlet 은 throw 된 exception을 한방에 처리한다.
- Controller
- 요청 ( 요청을 잘 받으려고 유효성 검사를 한다. 유효성 검사, 인증 체크, 주소랑 BODY 데이터 잘받기)
- 응답 (VIEW는 @controller가 return 하는거, Data 는 @RestController 가 return 하는 거)
→ RestController 의 메타 어노테이션에 @ResponseBody 와 @Controller 가 걸려있다.
→ 데이터를 리턴하는 어노테이션이라는 것
- Service (→ 매서드는 동사로 만든다)
- 트랜젝션 관리
- DTO 만들기 → 레이지로딩은 서비스까지! 응답DTO를 만들어서 Service 레이어에서 사용.
- Repository ( → 기능명으로 이름짓지 않는다)
++
실무에 가면
DB 테이블을 T01 T02 T03
컬럼명을 a01 a02 a03
이렇게 짜놓는다.
그래서 VIEW 를 사용해서
User_Tb
id, username 이런식으로 바꿔서 사용한다
예외처리

위의 구조로 패키지를 생성하고 error 패키지 안에 GlovalExceptionHandler를 만들어준다.
++
지금 util 빈 패키지 만들어 두었는데 빈폴더만 만들어두면 git 에 푸쉬안된다.

package shop.mtcoding.blog.core.error;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
// 만약 ControllerAdvice로 걸면 아래에 @Responsebody 걸어주면된다.
@RestControllerAdvice // 모든 throw는 이제 애한테 날아온다.
public class GlobalExceptionHandler {
// 런타임 익셉션이 발생하면 여길 때림
@ExceptionHandler(RuntimeException.class) // 지금 이렇게 하고 나중에 구분짓기
public String ex(Exception e) {
// restControllerAdvice는 데이터로 응답한다 (-> 자바스크립트 코드로 응답한ㄷ)
String errMsg = """
<script>
alert('$msg');
history.back();
</script>
""".replace("$msg", e.getMessage()); // 새로 나온 문법이어서 스트림 빌더보다 나음. msg가 e.getMessage로 치환된ㄷ.
// history.back 하면 이전 페이지로 돌아간다.
// 예외처리할 때, 404페이지를 만들어서 return 할수도 있지만
// 그냥 메시지 보여주고 return 시키는게 깔끔
return errMsg;
}
}
이렇게 하면 지금 상태로는 모든 RuntimeException이 발생하면
이 msg 가 alert로 뜨고 이전의 페이지로 back 하게 된다!
예외처리 해보기 끝!
Share article