03H. 엔티티 변경
03H. 엔티티 변경 관련
게시판의 질문, 답변에는 누가 글을 작성했는지 알려주는 "글쓴이" 항목이 필요하다. 이번에는 Question
과 Answer
엔티티에 "글쓴이"에 해당되는 author
속성을 추가해 보자.
Question
속성 추가
먼저 Question
엔티티에 author
속성을 추가하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/question/
Question.java
// (... 생략 ...)
import jakarta.persistence.ManyToOne;
import com.mysite.sbb.user.SiteUser;
// (... 생략 ...)
public class Question {
// (... 생략 ...)
@ManyToOne
private SiteUser author;
}
author
속성은 SiteUser
엔티티를 @ManyToOne
으로 적용했다.
여러개의 질문이 한 명의 사용자에게 작성될 수 있으므로
@ManyToOne
관계가 성립한다.
Answer
속성 추가
Question
엔티티와 같은 방법으로 Answer
엔티티에도 author
속성을 추가하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/answer/
Answer.java
// (... 생략 ...)
import com.mysite.sbb.user.SiteUser;
// (... 생략 ...)
public class Answer {
// (... 생략 ...)
@ManyToOne
private SiteUser author;
}
테이블 확인
위와 같이 Question
, Answer
엔티티를 변경하고 H2 콘솔에 접속하여 question
, answer
테이블을 확인해 보자.
author
저장
이제 Question
, Answer
엔티티에 author
속성이 추가되었으므로 질문과 답변 저장시에 author
도 함께 저장할 수 있다.
질문, 답변에 글쓴이를 추가한다는 느낌으로 작업을 진행하자.
답변에 작성자 저장하기
먼저 답변을 작성하는 AnswerController
를 수정하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerController.java
// (... 생략 ...)
import java.security.Principal;
// (... 생략 ...)
public class AnswerController {
// (... 생략 ...)
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
// (... 생략 ...)
}
}
현재 로그인한 사용자에 대한 정보를 알기 위해서는 스프링 시큐리티가 제공하는 Principal
객체를 사용해야 한다. 위와 같이 createAnswer
메서드에 Principal
객체를 매개변수로 지정하면 된다.
principal.getName()
을 호출하면 현재 로그인한 사용자의 사용자명(사용자ID)을 알수 있다.
principal
객체를 사용하면 이제 로그인한 사용자의 사용자명을 알수 있으므로 사용자명을 통해 SiteUser
객체를 조회할 수 있다. 먼저 User
서비스를 통해 SiteUser
를 조회할 수 있는 getUser
메서드를 UserService
에 추가하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/user/
UserService.java
// (... 생략 ...)
import java.util.Optional;
import com.mysite.sbb.DataNotFoundException;
// (... 생략 ...)
@Service
public class UserService {
// (... 생략 ...)
public SiteUser getUser(String username) {
Optional<SiteUser> siteUser = this.userRepository.findByusername(username);
if (siteUser.isPresent()) {
return siteUser.get();
} else {
throw new DataNotFoundException("siteuser not found");
}
}
}
UserRepository
에 이미 findByusername
을 선언했으므로 쉽게 만들수 있다. 그리고 답변 저장시 작성자를 저장할 수 있도록 다음과 같이 AnswerService
를 수정하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerService.java
// (... 생략 ...)
import com.mysite.sbb.user.SiteUser;
// (... 생략 ...)
public class AnswerService {
// (... 생략 ...)
public Answer create(Question question, String content, SiteUser author) {
Answer answer = new Answer();
answer.setContent(content);
answer.setCreateDate(LocalDateTime.now());
answer.setQuestion(question);
answer.setAuthor(author);
this.answerRepository.save(answer);
return answer;
}
}
create
메서드에 SiteUser
객체를 추가로 전달받아 답변 저장시 author
속성에 세팅했다. 이제 답변을 작성하면 작성자도 함께 저장될 것이다.
이제 다음과 같이 AnswerController
의 createAnswer
메서드를 완성해 보자.
파일명:
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerController.java
// (... 생략 ...)
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
// (... 생략 ...)
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
private final UserService userService;
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if (bindingResult.hasErrors()) {
model.addAttribute("question", question);
return "question_detail";
}
this.answerService.create(question, answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s", id);
}
}
principal
객체를 통해 사용자명을 얻은 후에 사용자명을 통해 SiteUser
객체를 얻어서 답변을 등록하는 AnswerService
의 create
메서드에 전달하여 답변을 저장하도록 했다.
질문에 작성자 저장하기
질문도 답변과 동일한 방법이므로 빠르게 작성해 보자. 먼저 작성자 정보를 저장하기 위해 QuestionService
를 다음과 같이 수정하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/question/
QuestionService.java
// (... 생략 ...)
import com.mysite.sbb.user.SiteUser;
// (... 생략 ...)
public class QuestionService {
// (... 생략 ...)
public void create(String subject, String content, SiteUser user) {
Question q = new Question();
q.setSubject(subject);
q.setContent(content);
q.setCreateDate(LocalDateTime.now());
q.setAuthor(user);
this.questionRepository.save(q);
}
}
create
메서드에 SiteUser
매개변수를 추가하여 Question
데이터를 생성했다.
이어서 QuestionController
도 다음과 같이 수정하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/question/
QuestionController.java
// (... 생략 ...)
import java.security.Principal;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
// (... 생략 ...)
public class QuestionController {
private final QuestionService questionService;
private final UserService userService;
// (... 생략 ...)
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult bindingResult, Principal principal) {
if (bindingResult.hasErrors()) {
return "question_form";
}
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
}
이제 다시 로컬 서버를 시작하고 로그인한 다음 질문 · 답변 등록을 테스트해보자. 잘 될 것이다.
SbbApplicationTests.java
QuestionService
의 create
메서드의 매개변수로 SiteUser
가 추가되었기 때문에 이전에 작성한 테스트 케이스가 오류가 발생할 것이다. 테스트 케이스의 오류를 임시 해결하기 위해 다음과 같이 수정하자.
package com.mysite.sbb;
//(... 생략 ...)
@Test
void testJpa() {
for (int i = 1; i <= 300; i++) {
String subject = String.format("테스트 데이터입니다:[%03d]", i);
String content = "내용무";
this.questionService.create(subject, content, null);
}
}
}
로그인이 필요한 메서드
하지만 로그아웃 상태에서 질문 또는 답변을 등록하면 다음과 같은 서버오류(500 오류)가 발생한다.
이 문제를 해결하려면 principal
객체를 사용하는 메서드에 @PreAuthorize("isAuthenticated()")
애너테이션을 사용해야 한다.@PreAuthorize("isAuthenticated()")
애너테이션이 붙은 메서드는 로그인이 필요한 메서드를 의미한다. 만약 @PreAuthorize("isAuthenticated()")
애너테이션이 적용된 메서드가 로그아웃 상태에서 호출되면 로그인 페이지로 이동된다.
QuestionController
를 다음과 같이 수정하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/question/
QuestionController.java
package com.mysite.sbb.question;
// (... 생략 ...)
import org.springframework.security.access.prepost.PreAuthorize;
// (... 생략 ...)
public class QuestionController {
// (... 생략 ...)
@PreAuthorize("isAuthenticated()")
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult bindingResult, Principal principal) {
// (... 생략 ...)
}
}
로그인이 필요한 메서드들에 @PreAuthorize("isAuthenticated()")
애너테이션을 적용했다.
AnswerController
도 다음과 같이 수정하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerController.java
// (... 생략 ...)
import org.springframework.security.access.prepost.PreAuthorize;
// (... 생략 ...)
public class AnswerController {
// (... 생략 ...)
@PreAuthorize("isAuthenticated()")
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id, @Valid AnswerForm answerForm,
BindingResult bindingResult, Principal principal) {
// (... 생략 ...)
}
}
그리고 @PreAuthorize
애너테이션이 동작할 수 있도록 SecurityConfig
를 다음과 같이 수정해야 한다.
파일명:
/sbb/src/main/java/com/mysite/sbb/SecurityConfig.
java
// (... 생략 ...)
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
// (... 생략 ...)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
// (... 생략 ...)
}
SecurityConfig
에 적용한 @EnableMethodSecurity
애너테이션의 prePostEnabled = true
설정은 QuestionController
와 AnswerController
에서 로그인 여부를 판별하기 위해 사용했던 @PreAuthorize
애너테이션을 사용하기 위해 반드시 필요하다.
이렇게 수정한후 로그아웃 상태에서 질문을 등록하거나 답변을 등록하면 자동으로 로그인 화면으로 이동하는 것을 확인할 수 있을 것이다.
로그인 하지 않은 상태에서 "질문 등록" 버튼을 누르면 "로그인" 화면으로 이동한다. 그리고 로그인을 진행하면 원래 하려고 했던 "질문 등록" 화면으로 이동한다. 이것은 로그인 후에 원래 하려고 했던 페이지로 리다이렉트 시키는 스프링 시큐리티의 기능이다.
disabled
한가지 더 생각해 봐야 할 것이 있다. 현재 질문 등록은 로그아웃 상태에서는 아예 글을 작성할 수 없어서 만족스럽다. 하지만 답변 등록은 로그아웃 상태에서도 글을 작성할 수 있다. 물론 답변 작성 후 [저장하기]
를 누르면 자동으로 로그인 화면으로 이동되므로 큰 문제는 아니지만 작성한 답변이 사라지는 문제가 있다.
작성한 글이 사라지는 문제를 해결하려면 로그아웃 상태에서는 아예 답변 작성을 못하게 막는 것이 좋을 것이다.
.question_detail.html
파일을 다음과 같이 수정하자.
파일명:
/sbb/src/main/resources/templates/
question_detail.html
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<!-- (... 생략 ...) -->
<!-- 답변 작성 -->
<form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">
<div th:replace="~{form_errors :: formErrorsFragment}"></div>
<textarea sec:authorize="isAnonymous()" disabled th:field="*{content}" class="form-control" rows="10"></textarea>
<textarea sec:authorize="isAuthenticated()" th:field="*{content}" class="form-control" rows="10"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
</div>
</html>
로그인 상태가 아닌 경우 textarea
태그에 disabled
속성을 적용하여 입력을 못하게 만들었다. sec:authorize="isAnonymous()"
, sec:authorize="isAuthenticated()"
속성은 현재 사용자의 로그인 상태를 체크하는 속성이다.
sec:authorize="isAnonymous()"
- 현재 로그아웃 상태sec:authorize="isAuthenticated()"
- 현재 로그인 상태