점프투스프링부트 복습 정리(~3-14)

박철현·2023년 10월 16일
0

점프투스프링부트

목록 보기
12/14

개요

  • 점프 투 스프링부트 교재의 2~3장을 복습하며 타임리프 문법, JPA 등을 다시한번 학습했습니다.
  • 좀 이것 저것 막 메모해서, 타임리프 주요 사용 문법등 검색으로 찾아보려고 게시하였습니다. 활용해도 좋을 것 같습니다.
  • 교재와는 다르게 Maria DB를 사용했고 자바스크립트 함수 사용하는 대신 HTML 태그로 해결하였습니다. (국비 수업 내용 반영 하여서 혼자 복습겸 했습니다..!)
  • 두서없이 정리해가지고, 혹시나 점프투스프링부트 교재 보다가 뭔가 좀 이해안가거나, 자바스크립트 함수 너무 어렵다하시면 여기서 해당 코드 찾으면 HTML로만 구현한 버전 확인할 수도 있어 좋을 것 같습니다!
  • 3-15장의 추가 기능은 기능별로 포스팅 했으니 다른 포스팅을 참고해주세요!

끄적임

  • 의존성

    • Spring Boot Devtools : 스프링부트 개발 시 도움을 주는 도구
      • 서버 재시작 없이도 클래스 변경 시 서버가 자동으로 재가동
  • 설정들

    • gradle 파일
      • developmentOnly : 개발환경에만 적용되는 설정
      • compileOnly : 해당 라이브러리가 컴파일 단계에서만 필요한 경우 사용
      • annotationProcessor : 컴파일 단계에서 애너테이션을 분석하고 처리하기 위해 사용
      • runtimeOnly : 해당 라이브러리가 런타임시에만 필요한 경우 사용
      • implementation : 해당 라이브러리 설치를 위해 일반적으로 사용하는 설정, 해당 라이브러리가 변경되더라도 이 라이브러리와 관련된 모든 모듈들을 컴파일하지 않고 직접 관련있는 모듈들만 컴파일하기 때문에 rebuild 속도가 빠르다.
  • ORM : 데이터베이스에 데이터를 저장하는 테이블을 자바 클래스로 만들어 관리하는 기술

    • DB 종류에 상관 없이 일관된 코드를 유지할 수 있어 프로그램 유지보수 편리
    • 안전한 SQL 쿼리를 자동으로 생성해 주므로 개발자가 달라도 통일된 쿼리를 작성할 수 있고 오류 발생률도 줄일 수 있음
  • JPA(Java Persistr API) 사용하여 데이터베이스 처리, ORM의 기술 표준으로 사용하는 인터페이스 모음

    • JPA는 인터페이스, 하이버네이트 : JPA를 대표적으로 구현한 클래스
    • JPA + 하이버네이트 조합으로 사용
  • @ResponseBody : 응답 결과가 String임을 명시

  • @lombok : 자바 클래스에 Getter, Setter, 생성자 등을 자동으로 만들어 주는 도구

  • @RequitedArgsConstructor : 해당 속성을 필요로하는 생성자가 롬복에 의해 자동으로 필요됨(final 키워드 붙은 속성을 생성자에 포함)

    • 의존성 주입에 사용됨
      • 스프링이 객체를 대신 생성하여 주입한다
  • @SpringBootApplication : 스프링부트의 모든 설정 관리됨

@Getter
@Setter
@Entity
public class Question {
	@Id // 기본키 지정
    // 값 생성 자동화 / 값 증가하는 타입으로
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	// 속성 세부 설정 가능
	@Column(length = 200)
	private String subject;

	@Column(columnDefinition = "TEXT")
	private String content;

	// 속성명은 create_date 형태로 저장됨
    // 카멜케이스 형태로 사용
	private LocalDateTime createDate;
}
  • @Entity : 엔티티 어노테이션 적용해야 JPA가 엔티티로 인식함
  • @Id : 기본키로 지정
    • @GeneratedValue : 해당 속성에 값을 따로 세팅하지 않아도 1씩 자동으로 증가하여 저장
      • strategy : 고유번호 생성 옵션, 생략 시 모두 동일한 번호 생성
        • GenerationType.IDENTITY : 해당 컬럼만의 독립적인 시퀀스 생성하여 번호를 증가시킬 때 사용
  • @Column : 컬럼의 세부 설정
    • length : 길이
    • columnDefinition : 컬럼의 속성 정의할 때 사용
    • 사용하지 않더라도 테이블 컬럼 인식, @Transient를 사용하여 인식 안되게 할 수 있음
@Getter
@Setter
@Entity
public class Answer {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@Column(columnDefinition = "TEXT")
	private String content;

	private LocalDateTime createDate;

	// N : 1 관계 설정, ForeignKey 관계 설정
    // 부모, 자식 관계를 갖는 구조에서 사용, 부모는 Question, 자식은 Answer
	@ManyToOne
	private Question question;
}
  • 부모 자식 관계 설정 : OneToMany or ManyToOne

    Question { 
    	// 반대방향 참조 방식
       // 질문 하나에 답변 여러개 1:N 관계는 리스트로 표현
       // mappedBy는 참조 엔티티의 속성명(Answer 엔티티에서 question 속성명을 전달해야 함)
       // 질문 삭제하면 답변들도 모두 삭제하기 위해 cascade = CascadeType.REMOVE 사용
    	@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
    	private List<Answer> answerList;
    }
  • 리포지터리 : 엔티티에 의해 생성된 데이터베이스 테이블에 접근하는 메서드들(findAll, save 등)을 사용하기 위한 인터페이스

  • @Autowired : 객체를 주입하기 위해 사용하는 스프링 어노테이션으로 다른 방법으로는 Setter 또는 생성자를 사용하는 방식이 있다.

    • 순환참조 문제와 같은 이유로 @Autowired 보다는 생성자를 통한 객체 주입방식이 권장된다.
    • 테스트 코드의 경우 생성자를 통한 객체 주입이 불가능하므로 테스트 코드 작성 시에만 @Autowired를 사용, 실제 코드 작성시 생성자를 통한 객체 주입방식을 사용
  • assertEquals : assertEquals(기대값, 실제값)와 같이 사용하고 기대값과 실제값이 동일한지를 조사한다. 만약 기대값과 실제값이 동일하지 않다면 테스트는 실패로 처리된다.

	@Transactional // 이녀석이 있어야 DB세션 유지
    @Test
    void testJpa() {
        Optional<Question> oq = this.questionRepository.findById(2);
        assertTrue(oq.isPresent());
        Question q = oq.get();

        List<Answer> answerList = q.getAnswerList();

        assertEquals(1, answerList.size());
        assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
    }
  • findById 메서드가 끝나면 DB세션이 끊어짐 -> getAnswerList() 메서드는 세션이 종료되어 오류 발생

  • 답변 데이터 리스트는 메소드를 호출하는 시점에 가져오기 때문

    • Lazy 방식 / Eager 방식 : 객체를 조회할 때 답변 리스트를 모두 가져오는 방식
    • fetch=FetchType.Lazy 또는 Eager로 설정할 수 있음
  • 하지만 실제 서버에서 JPA 프로그램 실행할 때 DB 세션이 종료되지 않기 때문에 오류 발생하지 않음

  • 테스트 코드에서 @Transactional 어노테이션 사용하면 메서드 종료될 때까지 DB 세션 유지

  • @BeforeEach : 아래 메서드는 각 테스트케이스가 실행되기 전에 실행된다.

  • @Modifying : 쿼리가 select가 아닐 경우 사용

public interface QuestionRepository extends JpaRepository<Question, Integer> {
    @Transactional // modify 붙을 때 transactional 붙는다.
    // @Modifying // 만약 아래 쿼리가 select가 아니라면 이걸 붙여야 한다.
    @Modifying
    // nativeQuery = true 여야 MySQL 쿼리문법 사용 가능
    @Query(value = "ALTER TABLE question AUTO_INCREMENT = 1", nativeQuery = true)
    void clearAutoIncrement();
  • @OneToMany에는 직접 객체 초기화

  • 템플릿 : 자바 코드를 삽입할 수 있는 HTML 형식의 파일

  • Model 객체 : 자바 클래스와 템플릿 간 연결고라 역할

    • 컨트롤러 메서드의 매개변수로 지정만 하면 스프링부트가 Model 객체를 자동으로 생성함
  • 자주 사용하는 타임리프 속성

    • 분기문 속성 : null이 아닌경우 엘리먼트 표시
    th:if = "${question !- null}"
    • 반복문 속성 : 반복횟수만큼 해당 엘리먼트 반복하여 표시
    th:each="question:${questionList}"
    th:each ="question, loop : ${questionList}"
    • loop 객체 이용하여 루프 내 아래 속성 사용 가능
      • loop.index - 반복 순서, 0부터 1씩 증가
      • loop.count - 반복 순서, 1부터 1씩 증가
      • loop.size - 반복 객체의 요소 개수(ex) questionList의 요소 개수)
      • loop.first - 루프의 첫번째 순서인 경우 true
      • loop.last - 루프의 마지막 순서인 경우 true
      • loop.odd - 루프의 홀수번째 순서인 경우 true
      • loop.even - 루프의 짝수번째 순서인 경우 true
      • loop.current - 현재 대입된 객체(ex) question)
    • 텍스트 속성
      • th:text = 값 속성 -> 해당 엘리먼트의 텍스트로 값 출력
      th:text = "${question.subject}"
      • 텍스트는 th:text 속성 대신에 대괄호 사용 가능
      <tr th:each="question : ${questionList}">
      	<td>[[${question.subject}]]</td>
          <td>[[${question.createDate}]]</td>
      </tr>
  • redirect:URL => URL로 리다이렉트

    • 리다이렉트 : 완전히 새로운 URL로 요청
  • forward:URL => URL로 포워드

    • 포워드 : 기존 요청 값들이 유지된 상태로 URL 전환
  • 타임리프 링크의 주소 = th:href 속성 사용
<a th:href = "@{|/question/detail/${question.id}|}"></a>
  • 주소를 나타낼 때는 반드시 @{ 문자와 } 문자 사이에 입력해야 함
  • 문자열 /question/detail/${question.id} 값이 조합되어 /question/detail/${question.id}로 만들어짐
  • 문자열과 자바 객체의 값을 더할때는 반드시 || 기호로 좌우를 감싸주어야 한다. (타임리프에서 문자열을 연결할 때 |문자 사용)
	@GetMapping(value = "/question/detail/{id}")
	public String detail(Model model, @PathVariable("id") Integer id) {
		return "question/question_detail";
	}
  • PathVariable에 사용된 매개변수명과 경로에 사용된 id 명이 동일해야 함
	@PostMapping("/create/{id}")
	public String createAnswer(Model model, @PathVariable("id") Integer id, @RequestParam String content) {
		Question question = questionService.getQuestion(id);
		answerService.create(question, content);
		return String.format("redirect:/question/detail/%s", id);
	}
  • textarea의 name속성이 content 이기에 변수명을 동일하게 content로 해야함
<h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
  • #lists.size(이터러블객체)는 타임리프가 제공하는 유틸리티로 객체의 길이 반환
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
  • static 디렉토리가 루트 디렉토리 이기에 static을 생략한 경로로 작성
  <td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td>
  • #temporals.format(날짜객체, 날짜포맷) : 날짜객체를 날짜포맷에 맞게 변환한다.

  • 템플릿 : 표준 HTML 구조를 가질 시 body 엘리먼트 바깥 부분(헤드 등)은 모두 같은내용으로 중복됨

    • CSS 파일 이름이 변경되거나 새로운 CSS 파일이 추가될 때마다 모든 템플릿 파일을 일일이 수정해야 함
    • 중복의 불편함을 해소하기 위해 타임리프에서는 템플릿 상속 기능 제공
      • 기본 틀이 되는 템플릿을 먼저 작성하고 다른 템플릿에서 그 템플릿을 상속해 사용하는 방법
<!doctype html>
<html lang="ko">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
    <!-- sbb CSS -->
    <link rel="stylesheet" type="text/css" th:href="@{/style.css}">
    <title>Hello, sbb!</title>
</head>
<body>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
</body>
</html>
  • layout.html파일을 상속하면 <th:block layout:fragment="content"></th:block> 영역에 해당되는 부분만 작성해도 표준 HTML 문서가 됨
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
    <table class="table">
        (... 생략 ...)
    </table>
</div>
</html>
  • layout.html 템플릿을 상속하기 위해 <html layout:decorate="~{layout}"> 처럼 사용함
    • layout:decorate 속성은 부모 템플릿으로 사용할 템플릿 설정
    • 속성값인 ~{layout}은 layout.html 파일을 의미
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
  • 부모 템플릿(layout.html)의 위 부분을 바꾸기 위해 자식 템플릿(question_list.html 등)에서 아래와 같이 상ㅇ
<div layout:fragment="content" class="container my-3">
    (... 생략 ...)
</div>
  • th:block : 태그의 흔적이 남지 않고 어떤 값에 따라 특정 블록이 보이거나 보이지 않거나 하는 등의 작업을 할 때 유용함
  • Spring Boot Validation : 어노테이션을 사용하여 입력 값을 검증할 수 있음
    • @Size : 문자 길이 제한
    • @NotNull : Null 허용 x
    • @NotEmpty : Null 또는 빈문자열("") 허용x
    • @Past : 과거 날짜만 가능
    • @Future : 미래 날짜만 가능
    • @FutureOrPresent : 미래 또는 오늘날짜만 가능
    • @Max : 최대값
    • @Min : 최소값
    • @Pattern : 정규식으로 검증
  • 화면에서 전달되는 입력 값을 검증하기 위해 폼클래스 필요
    • 폼클래스는 입력 값의 검증 뿐만 아닌 화면에서 전달한 입력 값을 바인딩할 때도 사용
@Getter
@Setter
public class QuestionForm {
	@NotEmpty(message = "제목은 필수항목입니다.")
	@Size(max=200)
	private String subject;

	@NotEmpty(message = "내용은 필수항목입니다.")
	private String content;
}
  • @Size : 200byte까지 검증
@PostMapping("/create")
	public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
		if(bindingResult.hasErrors()) {
			return "question/question_form";
		}
		questionService.create(questionForm.getSubject(), questionForm.getContent());
		return "redirect:/question/list"; // 질문 저장 후 질문목록으로 이동
	}
  • questionForm 변수와 bindingResult 변수는 model.addAttribute 없이 바로 뷰에서 접근할 수 있다.
    • 매개변수로 있는 것은 모델로 안넘겨줘도 됨
  • @Valid 어노테이션이 검증 기능 동작, BindingResult 객체는 검증이 수행된 결과를 의미하는 객체
    • 항상 Valid 어노테이션 바로 뒤에 위치해야 함
<form th:action="@{/question/create}" th:object="${questionForm}" method="post">
        <div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
            <div th:each="err : ${#fields.allErrors()}" th:text="${err}" />
        </div>
  • th:object : 폼의 속성들의 QuestionForm의 속성들로 구성됨을 타임리프 엔진에 알려줌
  • #fields.hasAnyErros가 true인 경우에는 QuestionForm 검증이 실패한 경우를 나타냄
    • 오류 메세지는 #fields.allErrors()로 구할 수 있음
  • th:replace 속성 : 공통 템플릿을 템플릿 내에 삽입할 수 있음
        <div th:replace="~{form_errors :: formErrorsFragment}"></div>
  • div 엘리먼트를 form_erros.html파일의 th:fragment 속성명이 formErrosFragment인 엘리먼트로 교체하라는 뜻

  • Pageable 객체 생성할 때 사용

    • PageRequest.of(page, 10) : page는 조회할 페이지 번호, 10은 한 페이지에 보여줄 게시물의 갯수
    • Page 객체에는 아래의 속성이 있으며, 템플릿에서 페이징 처리 시 사용
      • paging.isEmpty : 페이지 존재 여부(게시물이 있으면 false, 없으면 true)
      • paging.totalElements : 전체 게시물 개수
      • paging.totalPages : 전체 페이지 개수
      • paging.size : 페이지당 보여줄 게시물 개수
      • paging.number : 현재 페이지 번호
      • paging.hasPrevious : 이전 페이지 존재 여부
      • paging.hasNext : 다음 페이지 존재 여부
  • 타임리프의 th:classappend="조건식 ? 클래스값" 속성은 조건식이 참인 경우 클래스값을 class 속성에 추가

  • #numbers.sequence(시작, 끝)은 시작 번호부터 끝 번호까지의 루프를 만들어 내는 타임리프의 유틸리티.

  • 페이징 정렬 기준을 넣기 위해서는 PageRequest.of메서드의 세번째 파라미터로 Sort 객체를 전달해야 함

    • Sort.Order 객체로 구성된 리스트에 Sort.Order 객체를 추가하고 Sort.by(소트리스트)로 소트 객체를 생성할 수 있다.
     public Page<Question> getList(int page) {
     // Sort.Order 리스트 생성
           List<Sort.Order> sorts = new ArrayList<>();
           // Sort.Order 객체를 리스트에 추가(정렬 기준)
           sorts.add(Sort.Order.desc("createDate"));
           // Sort.by(리스트)로 Sort 객체 생성
           Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
           return this.questionRepository.findAll(pageable);
       }
  • 인증 : 로그인

  • 권한 : 인증된 사용자가 어떤 것을 할 수 있는지를 의미

  • @Configuration : 스프링의 환경설정 파일임을 의미하는 어노테이션

  • @EnableWebSecurity : 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 어노테이션

    • 내부적으로 SpringSecurityFilterChain 동작하여 URL 필터 동작함
  • 스프링 시큐리티 세부 설정은 SecurityFilterChain 빈을 생성하여 설장할 수 있다.

  • BCryptPasswordEncoder : BCrypt 해싱 함수를 사용해 비밀번호를 암호화 함

      @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    • configuration 어노테이션이 적용된 클래스에 빈으로 저장된 메서드의 리턴값으로 등록하면 빈에 객체가 등록되어 필요한 곳마다 주입받아 사용할 수 있음
@Getter
@Setter
public class UserCreateForm {
	@Size(min = 3, max = 25)
	@NotEmpty(message = "사용자ID는 필수항목입니다.")
	private String username;

	@NotEmpty(message = "비밀번호는 필수항목입니다.")
	private String password1;

	@NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
	private String password2;

	@NotEmpty(message = "이메일은 필수항목입니다.")
	@Email
	private String email;
}
  • @Size(min = 3, max = 25) 어노테이션으로 문자열 길이를 검증할 수 있음.
  • @Email 어노테이션으로 해당 속성이 이메일 형식과 일치하는지 검사
 @PostMapping("/signup")
    public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "signup_form";
        }

        if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
            bindingResult.rejectValue("password2", "passwordInCorrect", 
                    "2개의 패스워드가 일치하지 않습니다.");
            return "signup_form";
        }

        userService.create(userCreateForm.getUsername(), 
                userCreateForm.getEmail(), userCreateForm.getPassword1());

        return "redirect:/";
    }
  • bindingResult.rejectValue(필드명, 오류코드, 에러메세지)를 의미하며 여기서 오류코드는 "passwordInCorrect"로 정의함
    • rejectValue 메서드 : 스프링 MVC에서 사용되는 BindingResult 객체에 필드 수준의 오류를 추가하는 메서드
@PostMapping("/signup")
	public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
		if(bindingResult.hasErrors()) {
			return "user/signup_form";
		}

		if(!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
			bindingResult.rejectValue("password2", "passwordInCorrect", "2개의 패스워드가 일치하지 않습니다.");
			return "user/signup_form";
		}

		try {
			userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(), userCreateForm.getPassword1());
		}catch (DataIntegrityViolationException e) {
			e.printStackTrace();
			bindingResult.reject("signupFailed", "이미 등록된 사용자입니다.");
			return "user/signup_form";
		}catch (Exception e) {
			e.printStackTrace();
			bindingResult.reject("signupFailed", e.getMessage());
			return "user/signup_form";
		}
		return "redirect:/";
	}
  • bindingResult.reject(오류코드, 오류메세지) : 특정 필드의 오류가 아닌 일반적인 오류를 등록할 때 사용
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
  <form th:action="@{/user/login}" method="post">
    <div th:if="${param.error}">
      <div class="alert alert-danger">
        사용자ID 또는 비밀번호를 확인해 주세요.
      </div>
    </div>
    <div class="mb-3">
      <label for="username" class="form-label">사용자ID</label>
      <input type="text" name="username" id="username" class="form-control">
    </div>
    <div class="mb-3">
      <label for="password" class="form-label">비밀번호</label>
      <input type="password" name="password" id="password" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">로그인</button>
  </form>
</div>
</html>
  • 스프링 시큐리티, id : username / pw : password 로 name 설정을 해야 함
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<SiteUser> _siteUser = this.userRepository.findByusername(username);
        if (_siteUser.isEmpty()) {
            throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
        }
        SiteUser siteUser = _siteUser.get();
        List<GrantedAuthority> authorities = new ArrayList<>();
        if ("admin".equals(username)) {
            authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
        } else {
            authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
        }
        return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
    }
}
  • 스프링 시큐리티를 활용하여 DB에 저장된 사용자로 로그인 처리를 하기 위해 아래 2가지를 설정해야 한다.
	@Bean
	SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.formLogin(
				formLogin -> formLogin
					// 로그인폼 url 알려줌, 없으면 기본 페이지 url은 "/login"임(GET)
					.loginPage("/user/login")
					// 로그인 폼 처리 url을 알려줌 POST, form의 th:action과 맞춰야 함
					// loginPage와 주소가 같아서 생략 가능
					// .loginProcessingUrl("/user/login")
					.defaultSuccessUrl("/") // 로그인 성공 시 이동
			)
			.logout(
				logout -> logout
					.logoutUrl("/user/logout")
					.logoutSuccessUrl("/")
					.invalidateHttpSession(true) // 로그아웃 시 세션키를 날림
			);
		return http.build();
	}
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {

	private final UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		Optional<SiteUser> _siteUser = this.userRepository.findByUsername(username);
		if (_siteUser.isEmpty()) {
			throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
		}
		SiteUser siteUser = _siteUser.get();
		List<GrantedAuthority> authorities = new ArrayList<>();
		if ("admin".equals(username)) {
			authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
		} else {
			authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
		}
		return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
	}
}
    1. filterChain에서 FormLogin 설정으로 로그인 페이지 지정(커스터마이징 할 경우)
  • 2.UserDetailsService 인터페이스를 구현해야 함.

    • loadUserByUsername메서드(사용자명으로 비밀번호 조회하여 리턴하는 메서드) 구현 강제
    • 스프링 시큐리티는 loadUserByUsername 메서드에 의해 리턴된 User 객체의 비밀번호가 화면으로부터 입력 받은 비밀번호와 일치하는지 검사하는 로직을 내부적으로 가지고 있음
    1. filterChain에서 logout 설정으로 로그아웃 URL 지정
  • 현재 로그인한 사용자의 정보를 알기 위해서 스프링 시큐리티가 제공하는 Principal 객체 이용

    • Principal.getName() 호출하면 현재 로그인한 사용자의 사용자명(ID)를 알 수 있음
    • Principal 객체를 사용하는 메서드에 @PreAuthorize("isAuthenticated()")어노테이션을 사용해야 null값을 방지할 수 있음(로그인 된 사용자만 이용 가능)
    @Configuration
    @EnableWebSecurity
      // @PreAuthirize 어노테이션을 사용하기 위해 필요
    @EnableMethodSecurity(prePostEnabled = true) 
    public class SecurityConfig {
  • @PreAuthorize 어노테이션을 사용하기 위해 @EnableMethodSecurity 어노테이션 필요

  • 자바스크립트 코드는 </body>위에 삽입 추천

    • 화면 렌더링 완료된 후 자바스크립트 실행되기 때문
                <a href="javascript:void(0);" th:data-uri="@{|/question/delete/${question.id}|}"
                   class="delete btn btn-sm btn-outline-secondary" sec:authorize="isAuthenticated()"
                   th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
                   th:text="삭제"></a>
  • 자바스크립트 함수에서 this.dataset.uri로 데이터셋 접근 가능(th:data-uri)

    <script type='text/javascript'>
    const delete_elements = document.getElementsByClassName("delete");
    Array.from(delete_elements).forEach(function(element) {
       element.addEventListener('click', function() {
           if(confirm("정말로 삭제하시겠습니까?")) {
               location.href = this.dataset.uri;
           };
       });
    });
    </script>
  • bean으로 등록된 컴포넌트는 템플릿에서 바로 사용할 수 있음

  • th:utext는 HTML태그들을 이스케이프 하지 않음

        <div class="row my-3">
           <div class="col-6">
               <a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
           </div>
           <div class="col-6">
               <form>
                   <div class="input-group">
                       <input type="text" class="form-control" name="kw" placeholder="검색어" th:value="${param.kw}">
                       <button class="btn btn-outline-secondary">찾기</button>
                   </div>
               </form>
           </div>
       </div>
    • 부트스트랩 : 12그리드 사용
    • col로 숫자 지정하려면 부모에는 row를 반드시 써야함
      • col-6 / col-6 : 반반씩 사용하겠다는 클래스
  • th:with : 태그 내에서 변수로 사용

    <!-- 페이징처리 시작 -->
       <!-- th:with : 변수 선언 -->
       <div th:if="${!paging.isEmpty()}" th:with="queryStrBase = '?kw=' + ${param.kw != null ? param.kw : ''}">
           <ul class="pagination justify-content-center">
               <li class="page-item" th:classappend="${paging.number == 0} ? 'disabled'">
                   <a class="page-link"
                      th:href="@{|${queryStrBase}&page=0|}">
                       <span>처음</span>
                   </a>
               </li>
               <li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
                   <a class="page-link"
                      th:href="@{|${queryStrBase}&page=${paging.number-1}|}">
                       <span>이전</span>
                   </a>
               </li>
               <li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
                   th:if="${page >= paging.number-5 and page <= paging.number+5}"
                   th:classappend="${page == paging.number} ? 'active'"
                   class="page-item">
                   <a th:text="${page}" class="page-link" th:href="@{|${queryStrBase}&page=${page}|}"></a>
               </li>
               <li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
                   <a class="page-link" th:href="@{|${queryStrBase}&page=${paging.number+1}|}">
                       <span>다음</span>
                   </a>
               </li>
               <li class="page-item" th:classappend="${paging.number == paging.totalPages - 1} ? 'disabled'">
                   <a class="page-link"
                      th:href="@{|${queryStrBase}&page=${paging.totalPages-1}|}">
                       <span>마지막</span>
                   </a>
               </li>
           </ul>
       </div>
       <!-- 페이징처리 끝 -->
  • 검색 후 페이지 이동시 검색어 유지하도록 설정

  • 감 잡을 겸 복습 했는데 새로우면서 재미가있다. 원티드 프로젝트 준비 완료~!

  • 추가 기능은 이전에 포스팅 하였습니다!

  • 출처 : 점프 투 스프링부트 2~3장

profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글