겉바속촉

[점프투스프링부트] 3-08. 엔티티 변경 본문

IT 일기 (상반기)/SPRING 기초

[점프투스프링부트] 3-08. 엔티티 변경

겉바속촉 2023. 3. 13. 15:39
728x90
반응형

 

점프투스프링부트 3-08

 

 

 

목표 : 게시판의 질문, 답변에는 누가 글을 작성했는지 알려주는 "글쓴이" 항목 추가기

 

 

 

 

 

1. 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 관계가 성립한다.

 

 

2. 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;
}

 

 

3. 테이블 확인

위와 같이 Question, Answer 엔티티를 변경하고

H2 콘솔에 접속하여 question, answer 테이블을 확인해 보자.

 

question, answer 테이블에 author_id 컬럼이 생성된 것을 확인할 수 있다.

이 컬럼에는 site_user 테이블의 id 값이 저장되어 SiteUser 엔티티와 연결된다.

 

 

 

4. author 저장

이제 Question, Answer 엔티티에 author 속성이 추가되었으므로 질문과 답변 저장시에 author도 함께 저장할 수 있다.

질문, 답변에 글쓴이를 추가한다는 느낌으로 작업을 진행하자.

 

4-1. 답변에 작성자 저장하기

 

먼저 답변을 작성하는 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 메서드에 전달하여 답변을 저장하도록 했다.

 

 

4-2. 질문에 작성자 저장하기

 

질문도 답변과 동일한 방법이므로 빠르게 작성해 보자.

먼저 작성자 정보를 저장하기 위해 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);
        }
    }
}

 

5. 로그인이 필요한 메서드

 

하지만 로그아웃 상태에서 질문 또는 답변을 등록하면 다음과 같은 서버오류(500 오류)가 발생한다.

 

 

이 오류는 principal 객체가 널(null)값이라서 발생한 오류이다.

principal 객체는 로그인을 해야만 생성되는 객체이기 때문이다.

 

이 문제를 해결하려면

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 애너테이션을 사용하기 위해 반드시 필요하다.

 

이렇게 수정한후

로그아웃 상태에서 질문을 등록하거나 답변을 등록하면

자동으로 로그인 화면으로 이동하는 것을 확인할 수 있을 것이다.

 

로그인 하지 않은 상태에서 "질문 등록" 버튼을 누르면 "로그인" 화면으로 이동한다.
그리고 로그인을 진행하면 원래 하려고 했던 "질문 등록" 화면으로 이동한다.
이것은 로그인 후에 원래 하려고 했던 페이지로 리다이렉트 시키는 스프링 시큐리티의 기능이다.

 

 

 

6. 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()" - 현재 로그인 상태

 

다음은 로그아웃 상태에서 disabled가 적용된 화면이다.

 

 

 

 

728x90
반응형