Git Product home page Git Product logo

-java-webpeaceindex's Introduction

평화지수 프로젝트

  • 평화지수 웹 어플리케이션 코드 저장소 입니다.
  • 국가 간의 관계를 다룬 영문 기사에 대한 대중들의 평가를 바탕으로 국가 간의 평화를 지수화하였습니다.
  • 프로젝트의 최종 단계로서 지수화한 평화지수의 결과를 웹 어플리케이션을 통하여 공유하고자 합니다.
  • 이곳 에서 저의 다른 이력과 프로젝트들도 확인해주세요.

평화지수란?

미중관계 평화지수

image01

  • 미중 평화지수(검정색), 중국의 미국 외 국가와의 평화지수(파랑색), 미국의 중국 외 국가와의 평화지수(붉은색)
  • 미중관계(검정색)은 2019년 7월부터 2020년 10월까지 줄곧 음의 값을 보여주고 있으며 미국과 중국이 맺은 모든 국가간 관계에서 미중관계가 가장 낮은 점수를 보여주었다.
  • 2019년 말과 2020년 1월 코로나바이러스 확산 직전 미중관계의 반등은, 미중 무역 갈등에 대한 1단계 타결 시기와 일치한다.
  • 미중관계는 코로나바이러스가 확산된 2020년에 접어들면서 악화되었고, 2020년 6월 이후 본격적인 미국 대선시즌이 시작되면서 더욱 악화되는 모습을 보여주었다.

한일관계 평화지수

image02

  • 한일 평화지수(검정색), 일본의 한국 외 국가와의 평화지수(파랑색), 한국의 일본 외 국가와의 평화지수(붉은색)
  • 2019년 7월 1일 일본은 한국을 상대로 반도체 부품 관련 수출을 규제하였고, 평화지수에서도 2019년 7월의 한일 평화지수가 가장 낮은 수치를 보였다.
  • 2020년 5월 19일 일본 외무성의 외교청서에 "한국은 중요한 이웃나라"라는 문구가 3년만에 다시 등장, 평화지수에서도 해당 시기에 관계 호전의 가능성을 보였다.
  • 2020년 6월 2일 산업통상자원부가 일본의 수출규제에 대한 세계무역기구 제소를 재실시 하였고, 해당 기간 평화지수 또한 급속도록 하락하는 모습을 보였다.

웹 어플리케이션에 대한 간략한 설명

  • SpringBoot와 Thymeleaf를 이용하여 레이아웃과 화면을 작성하였고 Form을 전송 기능을 구현합니다.
  • JPA(hibernate)를 이용하여 게시판을 조회, RESTful Api를 작성하고 페이지를 검색합니다.
  • Spring Boot으로 웹 출시까지를 바탕으로 구현하였습니다.

환경 및 세팅

  • openjdk version "1.8.0_302"
  • Spring Boot 2.5.3/ Maven Project/ Dependencies: Spring Boot DevTools, Lombok, Spring Web, Thymeleaf, Spring Data JPA
  • MariaDB 10.6.3
  • 보다 자세한 환경은 pom.xml를 확인해주세요.

프로젝트 실행

  • java/~/PeaceindexApplication.java
  • src/main/java/com.diplomacy.peaceindex/PeaceindexApplication.java를 실행시키시면 됩니다.
  • MariaDB id와 비밀번호, 포트 설정 정보를 담은 application.properties는 깃허브에 업로드 하지 않았으므로, 관련 처리가 별도로 필요합니다.

웹 어플리케이션에 대한 자세한 설명

  • 이하에서는 구현된 패키지별로 각 기능에 필요한 코드들에 대한 설명을 덧붙였습니다.
  • 예를 들어 컨트롤러(2.) 패키지에 작성된 Form 전송하기(4)) 관련 코드를 보고 싶으시면, 2. 4) 항목을 확인하시면 됩니다.
  • 각 목차의 첫번째 항목은 디렉토리이거나 java 파일 명입니다.
  • 구현된 패키지와 기능들
    • 패키지
      • 템플릿(1.)
      • 컨트롤러(2.)
      • 모델(3.)
      • 레포지토리(4.)
      • 서비스(5.)
      • 밸리데이터(6.)
      • 컨피그(7.)
    • 기능
      • 타임리프로 화면 작성(1))
      • 타임리프로 레이아웃 만들기(2))
      • JPA로 게시판 조회(3))
      • Form 전송하기(4))
      • 밸리데이션(5))
      • JPA로 API(6))
      • JPA로 페이지 처리 및 검색(7))
      • Spring Security로 로그인 처리(8))
      • JPA로 @OneToMany 관계 설정하기(9))
      • JPA로 FetchType 설정하기(10))
      • 권한에 맞는 화면 구성 및 API 호출(11))
      • JPA 이용한 커스텀 쿼리 만들기(12))

1. 템플릿

  • resources/templates/~
  • Thymeleaf는 템플릿 엔진으로, resources/templates에 html 파일을 이용해 웹페이지를 만든다.
  • html파일의 html 태그에 xmlns:th="http://www.thymeleaf.org" 속성을 추가한다.

1) 타임리프로 화면 작성

  • index.html, fragments/common.html
  • index.html
    • "th:replace="fragments/common :: menu('home')" 속성은 fragments.common.html 에서 th:fragment="menu" 속성이 있는 태그를 찾아서 대체한다.
  • fragments/common.html
    • th:text="${name}": th:text는 태그 내부 텍스트에 대한 속성이며 모델 안의 키값에 해당하는 ${key}으로 모델의 해당 키의 밸류값을 받는다. -> 추후 기능 추가하면서 name에서 title로 수정.
    • th:classappend="${menu} == 'home'? 'active'" menu의 밸류가 'home'이면 class에 active 속성을 추가한다.

2) 타임리프로 레이아웃 만들기

  • board/list.html, fragments/common.html
  • board/list.html
    • index.html을 이용하여 list.html를 생성한다. 생성 시 fragments/common.html 네임스페이스를 추가한다.
    • "th:replace="fragments/common :: head('게시판')"으로 헤드 태그를 대체한다.
  • fragments/common.html
    • th:classappend="${menu} == 'board'? 'active'"으로 조건부로 active 속성을 추가한다.
    • th:if="${menu}=='home'th:if="${menu}=='board'속성으로 활성화된 메뉴일때만 current 지시자가 화면에 표시되도록 한다.
    • th:href="@{/}th:href="@{/board/list}로 링크를 연결해준다.

3) JPA로 게시판 조회

  • board/list.html
  • th:text="${#lists.size(boards)} 전달 받은 boards 변수(List)의 크기를 알려주는 size 문법을 사용한다. -> 페이지 기능 추가하면서 boards 변수를 직접 받고 타임리프 문법에 맞춰 totalElements로 받는 것으로 수정.
  • th:each="board : ${boards}" boards 변수의 각 값을 board로 변수로 선언하여 받는다.

4) Form 전송하기:

  • board/list.html board/form.html
  • board/form.html
    • list.html를 이용하여 form.html를 생성한다. 부트스트랩의 form 예제를 사용한다.
    • form.html의 취소 버튼을 통하여 list.html로 돌아가는 기능 구현
      • <a type="button"></a> 으로 쓰기 버튼을 만들 수 있다. -> 추후에 삭제 취소 기능 구현하면서 class="btn btn-primary"의 버튼으로 수정
      • th:href="@{/board/form}" 속성으로 list.html로 이동하도록 지정한다.
    • 글을 새롭게 생성하는 기능 구현:
      • form.html의 쓰기 버튼을 통하여 컨트롤러에 값들을 보내는 기능 구현
      • th:action="@{/board/form}method=post 으로 데이터를 @PostMapping("/form") 컨트롤러에 포스트 요청을 한다(보낸다).
      • th:object="${board}", th:field="*{title}", th:field="*{content}"으로 속성을 지정해준다.
    • 글을 수정하는 기능 구현
      • 수정하기 위해서는 id값을 바탕으로 원래 값을 받아와야 한다
        • GetMapping("/form")에서 값들을 받아와 화면에 띄우는 기능 구현
        • 전체 폼 태그에서는 th:object="${board}" 겟 매핑 컨트롤러 전달 받은 모델의 board 키를 가진 속성을 사용한다.
        • th:field="*{title}"th:field="*{content}" board 안의 title 속성과 content 속성을 사용하여 form 태그의 세부 div를 채운다.
      • 수정한 값을 보낼 때는 id도 보내야 컨트롤러에서 수정을 한다.
        • hidden type의 th:field="*{id}를 추가하여 수정 시에 id 값을 전달해서 새로운 글이 아니라 수정된 글이 만들어지도록 한다.
  • board/list.html
    • 글을 새롭게 생성하는 기능 구현: id 없이 form.html로 보내는 버튼 생성
      • <a type="button"></a> 으로 쓰기 버튼을 만들 수 있다. th:href="@{/board/form}" 속성으로 form.html로 이동하도록 지정한다.
    • 글을 수정하는 기능 구현: form.html로 id값과 함께 전달하고, 컨트롤러에서 id 값에 맞는 데이터를 조회하도록 한다.
      • list.html의 작성된 글의 제목을 클릭하면 그 글의 id값을 전달하면서 form.html로 보내도록 한다.
      • th:href="@{/board/form{id=${board.id}}}"로 board 클래스의 id 속성을 전달한다. 이는 컨트롤러의 GetMapping("/form")에서 @RequestParam이라는 어노테이션으로 파라매터로 전달 받을 것이다.
      • 이 id 값을 바탕으로 GetMapping("/form")에서 데이터를 조회할 것이다.

5) 밸리데이션

  • board/form.html
  • th:classappend=${#fields.hasErrors('title')} ? 'is-invalid' title과 관련된 이름으로 발생된 에러가 fields에 존재하는지 확인하고 is-invalid 속성 추가
  • th:errors="*{title}" title 관련 에러 관련된 태크 속성

7) JPA로 페이지 처리 및 검색

  • board/list.html
  • 전체 건수: boards.getTotalElements() 값을 타임리프에서는 ${boards.totalElements}로 불러와서 사용한다.
  • li 태그의 속성으로 th:each=로 컨트롤러에서 작성한 페이지 변수값을 가져온다.
  • i : ${#numbers.sequence(startPage, endPage)}로 두 변수값으로 만든 시퀀스의 숫자 값 i를 정의한 후, th:text=${i}로 보여준다.
  • 페이지가 본인 페이지일 때 본인, 페이지가 처음일때 previous 끝일때 next를 클릭하지 못하도록한다.
    • th:classappend="${i == boards.pageable.pageNumber + 1} ? 'disabled'"으로 조건부로 클래스 값을 추가해준다.
      • 조건문을 만들 때, 컨트롤러의 boards에서는 getPageable()getPageNumber()로 가져올 수 있었던 값들을 사용한다.
    • iboards.totalPages1로 바꾸어서 next와 previous 태그에 disabled 속성을 더해준다.
  • 페이지 이동 링크를 th:href=로 주되, 앞에서 컨트롤러에 파라매터로 준 pageable 변수의 입력 값을 GetMapping("/list") 에 전달한다.
    • 타임리프 문법에 따르면 th:href=@{/boards/list(page=${boards.pageable.pageNumber - 1})}()안에 변수의 입력값을 준다.
    • 이 때 현재 페이지를 만들 때 받아온 boards.pageable.pageNumber를 이용한다.
  • 검색 기능 구현
    • "searchTable"이라는 for 속성을 가진 label 태그와 name 속성을 가진 input 태그, button태그를 가진 form을 부트스트랩으로 부터 가져온다.
    • d-flex justify-content-end 클래스로 우측 정렬을 한다.
    • btn-light로 버튼 색깔을 바꾼다.
    • form 태그에만 method="GET", th:action="@{/board/list}"으로 지정해주면, form 태그 내부의 name=searchText 값을 파라매터로 전달한다.
    • th:value=${param.searchText}로 url에 있는 파라매터 값을 보여줄 수 있다.
    • th:href=@{/boards/list(page=${boards.pageable.pageNumber - 1}, searchText=${param.searchText} )}로 페이지를 이동해도 searchText 값을 전달하여 검색이 유지되도록 한다.

8) Spring Security로 로그인 처리

  • /fragments/common.html, /account/login.html, /account/register.html
  • common.html
    • css 파일의 주소를 th:href="@{/css/starter-template.css}로 수정한다.
    • a 태그와 button 클래스를 이용한 Login 버튼을 추가한다.
      • th:href="@{/account/login}"
      • sec:authorize="!isAuthenticated()"로 로그인 되지 않은 사람에게만 보여준다.
    • button 태그를 이용한 Logout 버튼을 추가한다.
      • class="text-white mx-2" 로 흰색 글씨와 x방향 2의 마진을 준다.
      • sec:authorize="isAuthenticated()"로 로그인된 사람에게만 보여준다.
      • 사용자와 권한 span에 sec:authentication을 추가하여 해당 항목을 표시해준다.
      • th:action="@{/logout}" method ="POST"로 포스트 요청을 보내서 로그아웃이 되도록 한다.
    • 메이븐에 타임리프의 SpringSecurityIntegrationModule을 추가한뒤 html 속성에 주소를 달아 sec 네임스페이스를 사용한다.
    • Login 버튼 뿐 아니라 회원가입 버튼도 추가한다.
  • login.html
    • index.html를 복사한 뒤, 헤드와 바디를 부트스트랩 소스를 가져와 수정한다.
    • body에는 class="text-center" 속성을 추가한다.
    • signin.css를 추가한뒤, th:href="@{/css/signin.css}를 지정해준다.
    • img의 이미지 주소를 부트스트랩 원본 소스 파일을 바탕으로 수정해준다.
    • label의 email을 username으로 수정하고, input의 type, id, placeholder 또한 text, username, Username으로 수정해준다.
    • 에러 메시지를 form 태그 위에 넣는다.
      • class = "alert alert-primary"role="alert"를 추가한다.
    • form 태그의 th:action="{@/account/login}"method="post"으로 타임리프 속성을 이용하여 csrf 토큰도 같이 전달해준다.
    • input 속성으로 name을 추가하여 컨트롤러에서 사용할 수 있도록 한다.
    • div의 th:if="${param.error}"th:if="${param.logout}"으로 해당 요청 시 보여줄 요소를 지정한다.
    • 이미지에 홈에 돌아오는 앵커 태그를 추가한다.
  • register.html
    • login.html를 복사한 뒤, Sign in을 회원가입으로 포스트 요청을 보낼 곳을 account/register로 수정한다.
    • 이미지에 홈에 돌아오는 앵커 태그를 추가한다.

9) JPA로 @OneToMany 관계 설정하기

  • list.html, form.html
  • list.html: 기존에는 직접 입력했던 사용자 이름을 th:text="${board.user.username}"으로 대체한다.
  • form.html: api를 사용하고 크롬을 통하여 디버깅을 해보면 hidden type 속성과 _csrf name 속성의 그리고 암호화된 키값의 value 속성을 가진 input이 존재한다. 스프링 시큐리티의 th:action에 csrf 옵션이 활성화되어 있는 것이다. WebSecurityConfig에서 수정가능하다.
  • CSRF(Cross-site Request Forgery): 사이트간 요청 위조
    • 사용자가 자신의 의지와 무관하게 공격자가 의도한 행동을 하여 특정 웹페이지의 보안을 취약하게 하고 수정, 삭제 등의 작업을 하게 만드는 공격법
    • 공격방법: 사용자 패스워드 변경 페이지나 타 시스템과의 로그인 연동 주소 패턴같은 인증 관련 취약점을 찾는다.
      • XSS 공격: 사이트에 스크립트를 넣어서 삽입한 코드를 실행하게 하여, 쿠키-세션-토큰 정보 탈취 및 오작동 유도.
    • 방어방법: HTTP 헤더에 있는 정보인 Referer 체크. 세션에 임의난수 토큰 발급 등

11) 권한에 맞는 화면 구성 및 API 호출

  • form.html, common.html
  • ROLE_ADMIN만 삭제할 수 있도록 한다.
    • 삭제 버튼을 ROLE_ADMIN 에게만 표시되도록 한다: sec:authorize="hasRole('ROLE_ADMIN')"을 버튼의 속성에 더한다.
    • 삭제 기능은 자바스크립트 ajax로 구현한다.
      • th:onclick="|deleteBoard(*{id})| 로 타임리프 문법에 맞게 id 변수를 deleteBoard 함수에 넣는다.
      • $.ajax() 의 url과 type를 입력하고, 성공시 콘솔 출력 함수를 만들어서 BoardApiController의 @DeleteMapping("/boards/{id}")를 호출한다.
      • script의 src의 jqeury 버전을 slim에서 전체로 수정한다.
      • window.location.href = "/board/list"로 이동할 주소를 설정한다.
    • 이 때 버튼만 가리면 postman등을 이용하여 사용자가 삭제할 수 있는 문제점이 있다. 따라서 서버에서 처리할 필요가 있다. 이를 위해 MethodSecurityConfig를 만들고 컨트롤러에 제약을 준다.
  • 자바스크립트 jquery를 불러오는 부분을 footer라고 이름 붙이고 common.html에만 남겨둔채, 다른 템플릿에서는 th:replace="fragments/common :: footer로 구현한다.

2. 컨트롤러

  • java/~/controller/~

1) 타임리프로 화면 작성

  • HomeController
  • @Controller로 어노테이션으로 컨트롤러임을 알려준다.
  • @GetMapping("/url") 어노테이션으로 url의 GET 요청을 받는다.
  • 문자열을 리턴하면 templates의 해당 html 파일을 화면에 보여준다.

2) 타임리프로 레이아웃 만들기

  • BoardController
  • @RequestMapping("/board")@GetMapping("/list")"/board/list" 경로 지정
  • @RequestParam 어노테이션으로 url/?의 파라매터 값을 받을 수 있다.
  • 파라매터로 Model를 정의하면 Model 안에 키-밸류값을 넣을 수 있다.

3) JPA로 게시판 조회

  • BoardController
  • 게시판 호출을 위한 데이터 값을 넘겨 받기 위하여 파라매터로 Model model을 추가한다.
  • BoardRepositoryprivate boardRepository으로 선언하고 @Autowired로 DI한다.
  • list 메소드에서 List<Boards> boards = boardRepository.findAll(); 로 데이터를 받고, addAttribute("boards", boards);로 "boards"를 키 값으로 model에 전달한다. 이제 모델에 담긴 데이터를 타임리프에서 사용할 수 있다. -> 페이지 기능 구현시 수정

4) Form 전송하기

  • BoardController
  • @GetMapping("/form")으로 form.html으로 연결한다.
    • JPA로 게시판을 조회할 때처럼 모델 클래스를 받고 속성을 추가하는데, new Board()로 새로운 board 모델 클래스를 속성에 넣어준다.
    • @RequestParam(required=false) Long id 어노테이션으로 url/id쿼리의 형태로 전달된 필수가 아닌 데이터를 받는다.
      • 이 데이터를 바탕으로 id가 없으면, 기존대로 new Board()를 모델에 속성으로 전달한다.
      • id가 있으면, boardRepository.findById(id);로 데이터베이스를 조회하고 없으면 .orElse(null)로 null 값을 board에 넣어서 모델에 속성으로 전달한다.
  • @PostMapping("/form")
    • @ModelAttribute Board board로 인풋 값으로 생성해둔 board 모델 클래스를 전달받는다.
    • JPA로 게시판을 조회할 때처럼 BoardRepository가 기본적으로 제공하는 메소드를 사용하는데 이번에는 findAll();이 아니라 save(board)로 전달받은 board 모델 클래스를 저장한다. 이때 id 값이 있느냐에 따라서 자동으로 INSERT 혹은 UPDATE를 해준다.
    • 완료 시 이동할 페이지는 list인데, return "redirect:/board/list";로 redirect 키워드를 주면 list로 리다이렉트가 되면서 화면이 이동된다.
      • 원래 @GetMapping("/list")컨트롤러에서는 모델에 전달할 키 값 "boards"가 있고 값을 뿌려줘야 했다. 하지만 @PostMapping("/form")에서는 값을 뿌려주지 않고 "board/list"로 바로 이동하면 되기 때문에 리다이렉트로 충분하다.
  • 페이지 전환 기능
    • Forward:
      • 클라이언트가 url1에 request를 보내면, url1원 request 정보를 그대로 유지, url2로 forward 한다. url2에서 response를 보내지만 url2의 정보는 볼 수 없다.
      • 사용자가 최초로 요청한 요청정보가 다음 url 에서도 유효하다.
      • 사용자가 실수로 글쓰기 CGI 응답에서 새로 고침을 누르면 요청정보가 살아 있어서 똑같은 글이 여러번 등록된다.
      • 따라서 시스템(세션, DB)에 변화가 생기지 않는 단순 조회(리스트 보기, 검색)의 경우 사용한다.
    • Redirect:
      • 클라이언트가 url1에 reqeust1을 보내면, url1은 redirect 응답을 하여 url2를 리턴하며 이동하라는 명령을 내린다.
      • 클라이언트는 새로운 request2를 url2에 보내고 응답을 받는다. 따라서 request1의 정보는 유효하지 않게 된다.
      • 처음 request1이 존재하지 않고, 글쓰기 기능을 하는 url1이 아니라 url2로 요청을 보내기 때문에 글쓰기가 여러번 수행되지 않는다.
      • 시스템에 변화가 생기는 요청(로그인, 회원가입, 글쓰기)의 경우 적합하다.

5) 밸리데이션

  • BoardController @PostMapping("/form")
  • @ModelAttribute@Valid로 바꾸어 주고 BindingResult 클래스를 파라매터로 준다.
  • BindingResult.hasErrors()Board에서 @NotNull @Size 어노테이션으로 제한한 사항이 맞는지 체크한다.
    • 에러가 나면 리다이렉트 하지 않고 @GetMapping("/form")으로 연결한다.
  • 밸리데이터 사용 시
    • @AutoWired 어노테이션을 선언하여 DI하고, boardValidator 선언한다.
    • boardRepository.save 전에 boardValidator.validate 메소드에 board와 bindingResult를 파라매터를 건내어 확인한다.

6) JPA로 API

  • BoardApiController
  • @RequestMapping("/api")BoardController와 구분한다.
  • C: @PostMapping어노테이션과 save(); 메소드를 사용한다.
  • R: @GetMapping()어노테이션과 findAll(); findById(); 메소드를 사용한다. 값이 없으면 에러가 발생하게 할 수도 있지만, orElse(null)로 null 값을 리턴하게 한다.
    • 제목(컨텐트)으로 검색하기 위해서는 @RequestParam()으로 title 파라매터를 추가한 뒤, 값이 전달이 되면 해당 값으로 찾아서 검색하도록 한다.
    • defaultValue = "" 옵션으로 기본값을 정의할 수 있다.
    • 검색을 위한 메소드는 @Autowired private으로 선언해 둔 BoardRepository에서 만든다.
  • U: @PutMapping()어노테이션을 사용하고 findById();로 확인한다.
    • 데이터베이스에 값이 존재하면 board.setName(newBoard.getName());로 데이터베이스 저장한다.
    • 존재하지 않으면 orElseGet(()->{ newBoard.setId(id) })로 id 지정 후 save(newBoard)로 저장한다. -> 추후에 Title과 Content로 확장 및 수정
  • D: @DeleteMapping어노테이션

7) JPA로 페이지 처리 및 검색

  • BoardController
  • @GetMapping("/list")에서 findAll() 메소드 안에 PageRequest.of(페이지_인덱스, 페이지_크기)를 넣으면 Page<Board> 변수를 얻을 수 있다.
  • boards.getTotalElement() 메소드로 전체 개수 값을 가져온다.
  • 이런 하드 코딩 대신에 Request 파라매터로 Pageable pageable(springframework 패키지)을 받고, findAll(pageable)로 값을 지정한다.
    • @PageableDefault(size=2)로 size에 대한 기본값을 지정해줄 수 있다.
  • @GetMapping("/list")pageable 파라매터를 추가 했으므로 ?page=1&size2 등으로 url을 통하여 page 변수(와 파라매터)를 전달할 수 있다.
  • list.html에 page 속성 전달
    • boards.getPageable().getPageNumber()로 현재 페이지 값을 가져온다.
    • model.addAttribute("startPage", startPage)로 startPage변수를 넣고 같은 방법으로 endPage 변수도 넣는다.
      • Math.maxMath.min을 사용하여, 1과 boards.getTotalPages() 사이의 startPage와 endPage를 구한다.
  • 검색 기능 구현
    • @GetMapping("/list") list.html로부터 받아온 String searchText를 파라매터로 준다. 이 파라매터로 검색하는 기능을 BoardRepository에서 구현한다.
    • 구현한 findByTitleContainingOrContentContaining();를 바탕으로 값을 boards에 넣는다.
    • @RequestParam(required=false, defaultValue="")로 값이 입력되지 않았을 때에도 작동하도록 한다.

8) Spring Security로 로그인 처리

  • AccountController
  • AccountController에는 @RequestMappin("/account"), login() 에는 @GetMapping("/login")하고 "/account/login" 리턴.
  • register에는 @PostMappin(/register)로 폼을 받을 것인데,
    • 그 이전에 user와 role을 받을 모델 클래스가 내부적으로 필요하다. -> User, Role 모델 클래스와 UserRepository를 구현한 후 User를 인자로 받는다.
    • 암호화와 권한 관리 등의 서비스를 처리할 패키지가 필요하다. -> UserService를 @AutoWired로 선언한 후, userService의 save메소드로 user를 저장한다.
    • 이 후 홈으로 돌려보내면 되므로 "redirect:/"를 리턴한다.
    • User를 인자로 받고 있기 때문에 User 클래스에서 선언한 값 username 과 password 에 맞춰서 값을 받게 된다.
  • @GetMapping("/register") 도 하나 추가한다.
    • "/account/register"를 리턴한다.

9) JPA로 @OneToMany 관계 설정하기

  • BoardController, UserApiController
  • BoardController@PostMapping("/form")
    • board.setUser(user);로 board에 유저값을 담으면 유저의 키값을 참조해서 user id값이 담길 것이지만 보안이 문제가 된다.
    • 스프링 시큐리티에서 제공하는 Authentication을 파라미터로 받고 .getName()을 통하여 username 값을 받아서 처리한다.
    • BoardService에 board와 username을 저장해 서비스에서 저장할 수 있도록 한다.
  • UserApiController
    • BoardApiController를 복사한 뒤 리팩토링한다. 게시글을 검색하는 all() 메소드는 간단하게 .findAll()로 수정한다.
    • 기존 @PutMapping("/users/{id}")에서 새로운 값을 부여하는 setTitlesetContent는 주석처리한다.
    • User 클래스의 @OneToMany 매핑에서 사용한다.
    • 매핑이 완료되었으므로 UserApiController로도 boards를 수정할 수 있다.
    • api 요청을 newUser로 받았으므로 user.setBoards(newUser.getBoards());로 값을 저장한다. 그런데 DB에 값이 저장되지 않는다. User 클래스의 boards에 추가적인 설정이 필요하다.
      • -> user.getBoards().clear(); 이후 user.getBoards().addAll(newUser.getBoards());로 기존의 데이터를 삭제한 후 데이터를 새롭게 넣어주는 코드로 변경한다.
    • 설정 후 user.getBoards()안의 board에 user 값을 넣어주면 마리아 DB에 정상적으로 저장이 된다.
    • all() 메소드에서 LAZY 옵션을 확인하도록 Log.debug()를 사용한다. 로그를 보기 위하여 application.properties의 설정도 수정한다.
    • 데이터를 따로 가져오는 LAZY 방식의 문제점은 UserRepository에서 해결한다.
  • QueryDsl
    • 데이터 조회는 fk의 조인, 복잡한 조건 등으로 인해 entity 클래스만으로는 처리하기 어려워 조회용 프레임워크를 추가로 사용한다.
    • 장점: 메소드를 기반으로 쿼리를 생성하기 때문에 타입 안정성이 보장된다. 2. 레퍼런스가 많다.

11) 권한에 맞는 화면 구성 및 API 호출

  • BoardApiController
  • form.html의 자바스크립트 ajax에서 delete 매핑을 호출한다.
  • MethodSecurityConfig에서 호출한 @Secured("ROLE_ADMIN")으로 ROLE_ADMIN 사용자만 delete 매핑을 호출할 수 있도록 한다. 버튼을 누르면 403 에러가 나타난다.

12) JPA 이용한 커스텀 쿼리 만들기

  • UserApiController
  • Method 파라매터를 받고 이에 따라 Username을 검색하는 쿼리문을 작성한다.
    • "query": Spring Data JPA의 JPQL의 방식. UserRepository.findByUsernameQuery 메소드를 사용한다.
    • "nativeQuery": SQL 쿼리 방식. UserRepository.findByUsernameNativeQuery 메소드를 사용한다.
    • "querydsl"
      • Maven 세팅 이후, 레포지토리에 QuerydslPredicateExecutor 인터페이스를 implements 후, Predicate 클래스와 QUser를 import 하여 사용.
      • contains 메소드를 통한 like 검색.
      • return 타입을 iterable로 수정해 users로 받는다.
      • BooleanExpression을 이용한 조건문도 구현 가능하다.
      • .findAll 메소드를 사용한다.
    • "querydslCustom"
      • CustomizedUserRepository 인터페이스와 Impl로 끝나는 구현체를 먼저 작성한다.
      • UserRepository에 implements 한 뒤 CustomizedUserRepository에 구현한 findByUsernameCustom 메소드를 사용한다.
    • "jdbc"
      • querydslCustom와 같게 CustomizedUserRepository 인터페이스와 구현체에 메소드를 작성한다.
      • findByUsernameJdbc 메소드를 사용한다.

3. 모델

  • java/~/model/~
  • Entity: 데이터 테이블, column - 필드, row - Entity 객체로 대응된다.

3) JPA로 게시판 조회

  • Board
  • Board 클래스에서 데이터베이스에서 정의해둔 필드들을 private으로 클래스의 멤버 변수로 정의한다.
  • lombok의 @Data 어노테이션으로 게터/세터를 만들어 클래스의 멤버 변수들을 외부에서 사용할 수 있다.
  • 데이터베이스 연동을 위한 모델 클래스임을 알려주기 위하여 @Entity 어노테이션을 추가한다. 컨트롤러에서 Model를 파라매터에서 불러오면 그 때 속성에 넣어줄 수 있다.
  • primary key인 id위에는 Id 어노테이션을 추가하고, 자동 증가를 위하여 @GenerateValue(strategy=GenerationType.IDENTITY) 를 선언한다.
  • IDENTITY 말고 SEQUENCE를 사용하면 성능이 보다 좋지만 추가 작업이 필요한다.

5) 밸리데이션

  • Board
  • @NotNull @Size 어노테이션으로 form에서 들어오는 값을 확인한다.
  • @Size에 최대 최소 길이, 오류 시 출력한 메세지를 지정할 수 있다.

6) JPA로 API

  • Board
  • Board 클래스를 그대로 사용하면 된다.

8) Spring Security로 로그인 처리

  • User, Role
  • User
    • Board와 같이 @Entity@Data 어노테이션 처리를 해준 뒤, id 도 선언한다.
    • user 테이블의 username, password, enabled를 선언한다.
    • JPA를 이용하여 조인을 하여 ManyToMany 매핑을 한다.
      • List<Role> roles로 멤버변수를 선언한다. 이후 UserRepository로 불러온다.
      • @ManyToMany어노테이션 말고도 JoinTable로 "user_role"에 연결될 두 컬럼명을 적어준다.
    • 사용자를 저장할때 권한까지 저장하는게 바람직하지 않아서 cascade 옵션은 주지 않는다.
    • 서비스를 구현한 뒤, roles를 선언할 때, null 예외처리를 피하기 위해 ArrayList로 선언해준다.
  • Role
    • User를 복사해서 만들되 이번에는 name을 선언한다.
    • List<User> users를 가져올 때 @ManyToMany(mappedBy="roles") 로 유저 클래스에서 이미 설정해둔 컬럼명인 roles를 지정하여 같은 설정에 맞게 동일하게 가져오도록 양방향 매핑을 한다.

9) JPA로 @OneToMany 관계 설정하기

  • Board, User, Role
  • Board
    • 기존에는 템플릿에서 사용자 이름을 입력했지만, 이제는 로그인하여 글을 쓴 사용자 정보를 표시하도록 한다.
    • 먼저 Many의 입장인 보드 클래스에서 One의 입장인 유저 클래스를 가져오고 @ManyToOne 어노테이션을 해준다.
    • JoinColumn() 어노테이션으로 마리아 DB의 테이블들을 연결해준다. 이를 위하여 DB의 board 테이블에 user_id 컬럼을 추가하고 user 테이블의 id값과 외래키로 연결해준다. 이 user_id 키값을 이용해서 user의 사용자 이름을 가져오는 로직을 작성하게 된다.
      • name = "user_id"referencedColumnName = "id"로 마리아 DB의 테이블과 컬럼을 지정해줄 수 있다. 다만 id의 경우 User 클래스에서 @Id 어노테이션 지정을 해주었기 때문에 생략할 수 있다.
    • @JsonIgnore로 api 요청 시에 재귀적으로 users가 표시되지 않도록 한다.
  • User
    • @OneToMany 어노테이션을 준 List<Board>를 가져온다.
    • mappedBy = "user"로 Board 클래스의 변수명을 적어주면 user에 적어둔 JoinColumn() 설정을 그대로 사용할 수 있다.
    • BoardUser가 서로를 가지고 있어 서로를 조회할 수 있는 양방향 매핑이 성립되었다.
      • 통상적으로 양방향 매핑의 경우 @ManyToOne에 키에 대한 설정을 적어둔다.
    • cascade = CascadeType.ALL는 Hibernate의 ALL, REMOVE 등의 메소드를 지정해줄 수 있다.
      • 예를 들어 REMOVE 메소드를 사용하면, user값을 제거할 때 연쇄적으로 boards가 먼저 삭제가 되고 user가 삭제가된다.
    • orphanRemoval = true로 api로 데이터를 수정하면서 db에 저장된 기존의 user의 다른 데이터를 자동으로 삭제할 수 있다. 기본값이 false이다.
  • Role
    • @JsonIgnore로 api 요청 시에 재귀적으로 users가 요청되지 않도록 한다.

10) JPA로 FetchType 설정하기

  • User
  • @OneToMany()fetch = FetchType.EAGAR 옵션은 사용자 조회 시에 boards 클래스에 대한 데이터를 같이 가져올 지(EAGAR) 아니면, 나중에 필요할 때 가져올지(LAZY) 에 대한 설정이다.
    • @OneToOne@ManyToOne 기본값이 EAGAR이며, @OneToMany@ManyToMany의 기본값은 LAZY이다. 자동으로 불러와야할 컬럼이 하나인지 여러개 인지에 따라서 성능상의 부하를 고려한 기본값이다.
    • api 테스트를 위하여 roles는 @JsonIgnore로 데이터가 표시되지 않도록 한다.
    • LAZY로 설정하고 UserApiControllerall() 메소드를 사용하면 모든 보드(N)에 유저(1)까지 호출을 하는 N+1 문제가 발생하지 않는다.

4. 레포지토리

  • java/~/repository/~
  • 레포지토리: Entity에 의해 생성된 DB에 접근하는 메서드(ex) findAll()) 들을 사용하기 위한 인터페이스.
  • Entity를 선언함으로써 데이터베이스 구조를 만들었다면, 여기에 어떤 값을 넣거나, 넣어진 값을 조회하는 등의 CRUD(Create, Read, Update, Delete)를 해야 쓸모가 있는데, 이것을 어떻게 할 것인지 정의해주는 계층

3) JPA로 게시판 조회

  • BoardRepository
  • BoardRepository: JpaRepository<Board, Long>를 상속받아서 레포지토리를 만든다. 앞서 생성한 Board 모델 클래스를 불러와준다.
  • 메소드들의 이름을 규칙에 따라서 필드명을 포함시켜 만들면, 인터페이스를 구현할 필요없이 데이터를 가져올 수 있다.
  • 컨트롤러에서 레포지토리로 만든 Board 모델의 리스트를 속성에 넣어줄 수 있다.

6) JPA로 API

  • BoardRepository
  • 제목으로 검색을 하기 위한 메소드는 List<Board> findBy필드명으로 인터페이스만 정의하면 알아서 구현해준다.
  • findById필드명Or필드명이나 findById필드명And필드명으로 여러 조건으로 검색하는 메소드를 구현할 수 있다. Spring Data JPA의 Query Creation 참고하면 된다.
  • @Query(value="", nativeQuery=true)로 커스텀 메소드를 만들 수 있다.

7) JPA로 페이지 처리 및 검색

  • BoardRepository
  • 검색 기능 구현
    • Page<Board> findByTitleContainingOrContentContaining(String title, String content, Pageable pageable);로 title과 content를 가진 데이터를 검색하여 pageable에 넣는 메소드를 구현한다.

8) Spring Security로 로그인 처리

*UserRepository

  • 빈 UserRepository를 설정한 뒤, AccountController의 register에서 이용한다.

9) JPA로 @OneToMany 관계 설정하기

  • UserRepository
  • findByUsername로 username이라는 컬럼에서 데이터를 찾아오도록 한다.
  • 새롭게 findAll(); 메소드 명을 만든다. @EntityGraph(attributePaths = { "boards" })로 LAZY 옵션을 준 boards를 입력하면, FetchType을 무시하고 join방식으로 데이터를 한꺼번에 불러와서 해결한다. N+1의 쿼리가 만들어져 성능 상의 문제가 일어날 경우 이렇게 join 방식으로 쿼리를 짜주는 @EntityGraph 어노테이션으로 해결할 수 있다.
  • N+1 문제: 연관 관계가 설정된 Entity를 조회할 경우 조회된 데이터 갯수(n)만큼 연관 관계의 조회 쿼리가 추가로 발생한다.

12) JPA 이용한 커스텀 쿼리 만들기

  • UserRepository, CustomizedUserRepository, CustomizedUserRepositoryImpl
  • JPQL 방식
    • Spring Data JPA의 문서를 참고한다.
    • UserRepository의 해당 메소드에 @Query()안에 JPQL 쿼리를 작성하면 쿼리 문을 수행할 수 있다.
    • 'u'가 '*' 대신에 사용된다.
    • "%?!%"로 앞뒤로 like 검색을 건다.
  • NativeQuery 방식: UserRepository의 해당 메소드의 @Query(value = , nativeQuery = true) 안에 작성한다.
  • QueryDsl 방식: QuerydslPredicateExecutor<User> 인터페이스를 implements
  • QueryDsl 커스텀 방식:
    • CustomizedUserRepository인터페이스 작성.
      • findByUsernameCustom 메소드 작성
    • CustomizedUserRepositoryImpl 구현체 작성.
      • @PersistenceContext private EntityManager 생성 및 의존성 주입
      • 스프링 깃허브의 custom dir를 참고하여 EntityManager 이용하여 작성한다.
      • 수많은 커스텀이 가능하지만, QueryDsl 방식과 유사하게 구현한다. qUser를 사용하여 contains로 like 검색을 한다.
      • .fetch()하면 List가 리턴된다.
    • CustomizedUserRepositoryUserRepository에 implement한다.
  • JDBC
    • CustomizedUserRepository에 findByUsernameJdbc 메소드 작성
    • CustomizedUserRepositoryImpl
      • @Autowired JdbcTemplate 생성 및 의존성 주입
      • findByUsernameJdbc 메소드 작성
        • jdbcTemplatequery 메소드를 사용한다.
          • 첫번째 파라매터로 쿼리문을 작성한다.
          • 두번째 파라매터로 new Object[]{"%" + username + "%"}로 받아온다. "%"는 전달할 파라매터에 작성되어야 한다.
          • 세번째 파라매터로 new BeanPropertyRowMapper로 담아온다.

5. 서비스

  • java/~/service/~

8) Spring Security로 로그인 처리

  • UserService
  • @Service 어노테이션으로 비즈니스 로직을 구현하고, 테스트를 구현하기도 용이하다.
  • AccountController에서 선언하여 사용하게 된다.
  • User를 인풋과 아웃풋으로 하는 save 메소드를 작성한다. 이때 @AutoWired로 UserRepository를 선언하고 userRepository.save(user)를 리턴한다.
  • 패스워드를 암호화한다.
    • WebSecurityConfig에서 선언해둔 PasswordEncoder@AutoWired로 선언하여 사용한다.
    • user.getPassword()로 가져온 패스워드를 passwordEncoder.encode()메소드로 암호화한다.
    • 이 값을 user.getPassword()로 새롭게 저장해준다.
    • user.setEnabled()도 기본적으로 가입하면 true로 해준다.
  • 아이디와 패스워드 뿐만이 아니라 role 또한 저장한다.
    • 어레이리스트 형태로 불러온 role를 저장하려고 하는데, 데이터베이스에서 또다시 불러오기 위하여 Repository를 또 만들기 보다는, id를 하드코딩을 해서 데이터베이스에 만들어둔 role 값을 불러와서 넣어준다.
    • 그러면 user_role 테이블에 role 테이블 값이 저장된다.

9) JPA로 @OneToMany 관계 설정하기

  • BoardService
  • boardRepository를 선언하고 save 메소드를 만들면 BoardController@PostMapping에서 save(username, board);를 사용할 수 있다.
  • 컨트롤러에서 보내준 username과 board를 바탕으로, 레포지토리에서 정의한 findByUsername 메소드를 사용하여 user 클래스를 받는다.
  • 이를 board 안에 user에 세팅한 후 boardRepository에 저장한 값을 리턴한다.

6. 밸리데이터

  • java/~/validator/~

5) 밸리데이션

  • BoardValidator
  • Board 클래스의 어노테이션을 통한 구현 이상을 원할 때, 커스텀 클래스를 만든다.
  • Validator를 상속 받아 구현해야할 메소드들을 구현한다. Spring Boot Validator를 참고하여 형식을 맞춰 준다.
  • validate메소드
    • obj 인풋을 Board로 형변환을 해준 후 필요한 에러를 체크해준다.
    • DI를 위하여 @Component로 등록해준다.

7. 컨피그

  • java/~/config/~

8) Spring Security로 로그인 처리

  • WebSecurityConfig
  • WebSecurityConfigurerAdapter 를 상속 받은 WebSecurityConfig@Configuration @EnableWebSecurity 어노테이션을 추가한다.
    • @Configuration 어노테이션을 추가해주면 userDetailsService()에서 @Bean 어노테이션을 사용할 수 있다. 이제 다른 클래스에서 @AutoWired 어노테이션을 사용해서 이 메소드를 사용할 수 있다. 현 프로젝트에서는 마리아 DB에 만든 사용자 테이블에서 사용자를 조회해서 로그인 처리를 할 것이므로 userDetailsService()가 필요하지 않다.
  • WebSecurityConfigurerAdapter에서 이미 정의된 configure() 메소드에서는 HttpSecurity를 인자로 받아서 보안 설정을 해준다.
    • authorizeRequests() 에서는
      • antMatcher("url").permitAll()로 해당 url를 모든 사용자에게 열어둔다.
        • /static/css 폴더 또한 추가를 해준다.
        • /account/register 회원가입 페이지도 추가한다.
      • anyRequest().authenticated로 다른 요청은 로그인을 해야 페이지를 볼수 있도록 한다.
      • and()로 관련 설정을 끝낸다.
    • formLogin()로 로그인 페이지를 설정해준다.
      • loginPage("/account/login") 다른 페이지를 가면 자동으로 템플릿에 맞춘 로그인 페이지로 리다이렉트하도록 한다.
      • 로그인 되지 않은 사용자이므로 permitAll()을 해준다.
    • logout() 도 유사하게 설정한다.
  • configureGlobal
    • AuthenticationManagerBuilder를 인자로 받고 내부에서 설정을 하게 되면, 인스턴스를 받아서 스프링에서 인증처리를 해준다.
    • private DataSource datasource@Autowired 어노테이션을 통하여 인스턴스를 주입한다. application.properties에 정의된 데이터 소스를 사용하게 한다. 이 데이터 소스를 jdbcAuthentication().dataSource()에 전달하면 알아서 인증처리를 해준다.
    • userByUsernameQuery()를 통하여 마리아 db의 user 테이블의 username, password, enabled 순서대로 값을 가져온다.
    • 스트링에 여백을 두어야 쿼리에서 오류가 나지 않으며, ?에는 값을 넣어준다.
    • authoritiesByUsernameQuery에서는 마리아 db의 role 테이블을 통하여 권한 처리를 위해 필요한 쿼리를 작성한다.
      • role과 user를 연결하는 user_role 테이블을 만든 후, user_role.user_id와 user.id를 inner join 해준다.
      • user_role.role_id와 role.id또한 inner join 해준다.
  • PasswordEncoder를 통하여 비밀번호를 안전하게 암호화하는 기능을 스프링에서 제공해주고 있다.
    • 이 인스턴스 또한 jdbcAuthentication()passwordEncoder()에 넣어주면 이 인코더를 통하여 암호처리를 해준다.
  • Authentication은 인증으로 로그인 관련된 처리, Authorization은 로그인 이후에 권한 처리를 의미한다.

9) JPA로 @OneToMany 관계 설정하기

  • WebSecurityConfig
  • .permitAll()antMatchers에 더하여 api 테스트를 가능하게 한다.
  • .csrf.disable()로 테스트를 가능하게 한다.

11) 권한에 맞는 화면 구성 및 API 호출

  • MethodSecurityConfig
  • GlobalMethodSecurityConfiguration을 상속받고 사용할 방법을 @EnableGlobalMethodSecurity어노테이션 옵션으로 설정해준다. 이제 BoardApiController에서 @Secured어노테이션을 사용할 수 있다.

-java-webpeaceindex's People

Contributors

hsjung93 avatar

Stargazers

 avatar

Watchers

 avatar

-java-webpeaceindex's Issues

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.