이전에 작성했던 게시판 만들기 글을 이어써보고자 한다. 개발을 마무리한지는 꽤 됐는데 블로그를 쓸 겨를이 없었어서 잠시 우선순위를 낮췄다가 틈 날때 다시 작성한다.
>> 이전 글 보기
2024.03.20 - [공부/Spring] - [Spring Boot] 게시판 만들기(1) - 개요, 기본 기능 구현
[Spring Boot] 게시판 만들기(1) - 개요, 기본 기능 구현
개인 프로젝트로 게시판 만들기에 도전해본다. 지금까지 배운 기술을 이용해 백엔드 기능 구현에 집중해볼 것이고, 디자인 및 프론트엔드는 최소화할 것이다. - 프로젝트명: 게시판 만들기 - 목
efficient-and-clean.tistory.com
이전에 구현하면서 느낀 부족한 기능 및 고민할 거리를 중심으로 리팩토링을 진행했다.
지난 번에 작성한 리팩토링 사항
1. 메시지 국제화 + 2. model에 메시지 전달 부분 메서드화
프로젝트 내 resources 파일에
- templates/message.html
- messages.properties
- messages_en.properties
를 추가해주었다.
그리고 컨트롤러에서 CRUD 기능에다가 모두 message를 리턴하도록 수정해주었다.
예시 코드)
@GetMapping("/board/delete")
public String deleteArticle(@RequestParam("id") Integer id, Model model) {
boardService.deleteArticle(id);
callMessage(model, "글 삭제가 완료되었습니다.");
return "message";
}
3. 글 삭제시 사용자 확인(비밀번호 기능) + 4. 로그인 기능
3번을 진행하려면 회원 가입이 필요했다. 그래서 로그인 기능을 만들었다.
역시 제일 오래 걸리고 제일 복잡한 구현이 되었다.
홈 개인화 기능
일단 보안을 고려해서 쿠키보단 (그나마)세션을 이용해서 사용자를 식별하고자 했다.
그리고 로그인을 하면 "~~님 안녕하세요" 와 같은 개인화 기능을 구현하고 싶었다.
그래서 HomeController에서 로그인 사용자를 구분하여 home과 loginHome 페이지로 분기할 수 있도록 했다.
@Controller
public class HomeController {
@GetMapping("/")
public String home(@SessionAttribute(name = SessionConst.LOGIN_USER, required = false) User loginUser, Model model) {
if (loginUser == null) {
return "home";
}
model.addAttribute("user", loginUser);
return "loginHome";
}
}
회원가입 기능
먼저 회원가입 기능을 UserController로 관리하게끔 구현했다.
@PostMapping("/users/new")
public String create(@Valid @ModelAttribute("userForm") UserForm userForm, BindingResult result) {
//비밀번호와 비밀번호 확인이 같은지 검사
if(!userForm.getPassword().equals(userForm.getConfirmPassword())) {
result.rejectValue("confirmPassword", "error.userForm", "비밀번호와 비밀번호 확인이 일치하지 않습니다.");
}
//오류가 있을시 다시 회원가입 폼으로 이동
if(result.hasErrors()) {
return "/users/createUserForm";
}
User joinUser = User.create(userForm.getLoginId(), userForm.getName(), userForm.getPassword());
userService.join(joinUser);
return "redirect:/";
}
UserForm으로 필요한 데이터를 받아 회원가입을 하게끔 진행했다.
비밀번호와 비밀번호 확인이 같은지 확인하는 로직을 넣었고, 오류가 있을시 다시 회원가입 폼으로 돌아가게 했다.
중복 회원 확인은 UserService에서 진행했다.
정상 흐름시 새로운 유저를 생성하고 리다이렉트했다.
로그인 기능
로그인 기능은 loginController를 만들어서 관리했다.
로그인 유저가 null인 경우(유저 정보가 없거나, 아이디/비밀번호가 맞지 않는 경우) 에러를 뱉어내게 했다.
에러가 나타나면 에러 메시지를 가지고 다시 loginForm으로 돌아가게 만들었다.
정상 흐름인 경우, 세션을 얻어 리다이렉트하게 구현했다.
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm loginForm, BindingResult bindingResult,
HttpServletResponse response, HttpServletRequest request) {
User loginUser = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if (loginUser == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "/login/loginform";
}
if (bindingResult.hasErrors()) {
return "/login/loginform";
}
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_USER, loginUser);
return "redirect:/";
}
로그아웃시에는 세션을 없애고 다시 리다이렉트하게 구현했다.
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
5. 메서드명, Get/PostMapping Rename + 6. 변경 감지(Dirty Checking)로 수정 + 7. DI 생성자 주입으로 변경
데이터 변경시 트랜잭션 내에서 처리할 수 있게끔 Controller에서 데이터를 처리하던 로직을 Service로 옮겼다.
Service에서는 @Transcaction을 붙여 데이터 변경시 트랜잭션을 사용했다.
예시 코드)
@Transactional
public Integer write(Article article) throws IOException {
for (UploadFile uploadFile : article.getImageFiles()) {
uploadFile.setArticle(article);
}
Article saveArticle = boardRepository.save(article);
return saveArticle.getId();
}
그리고, @Autowired로 의존성이 주입된 부분들을 모두 생성자 주입으로 바꿔주었다.
8. 파일 다운로드 기능 + 9. 업로드 사이즈 제한
파일 관련 로직을 많이 수정했다.
기존에는 하나의 파일을 업로드만 가능하게끔 구현되었는데,
수정본에서는 여러 개의 파일을 업로드, 다운로드, 미리보기까지 가능하게끔 변경하고 싶었다.
먼저 List로 MultipartFile을 데이터를 받았다.
@Data
public class ArticleForm {
private Integer id;
private String title;
private String content;
private String writer;
private List<MultipartFile> imageFiles;
}
그리고 file 업로드, 다운로드를 관리하는 FileStore이라는 클래스를 만들었다.
여기서 알게 된 사실이 MultipartFile은 DB에 바로 저장이 안된다는 점이다.
그래서 UploadFile이라는 클래스를 만들어 DB에 파일 이름을 저장할 수 있도록 구현했다.
@Entity
@Table(name = "upload_file")
@Data
public class UploadFile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String uploadFileName;
private String storeFileName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "article_id")
private Article article;
public UploadFile(String uploadFileName, String storeFileName) {
this.uploadFileName = uploadFileName;
this.storeFileName = storeFileName;
}
protected UploadFile() {
}
}
마지막으로 이미지가 사용되는 글 저장, 수정에 이미지 파일을 추가, 수정할 수 있는 기능을 추가했다.
// 게시글 수정
@Transactional
public void updateArticle(Integer id, ArticleForm articleForm, List<MultipartFile> imageFiles) throws IOException {
Article updateArticle = boardRepository.findById(id).orElse(null);
updateArticle.setTitle(articleForm.getTitle());
updateArticle.setContent(articleForm.getContent());
if (imageFiles != null && !imageFiles.isEmpty()) {
updateArticle.setImageFiles(fileStore.storeFiles(imageFiles));
}
}
FileStore의 다운로드 기능
public ResponseEntity<Resource> downloadFiles(Article article, String fileName) throws MalformedURLException {
String uploadFileName = article.getImageFiles()
.stream()
.filter(file -> file.getStoreFileName().equals(fileName))
.findFirst()
.orElse(null)
.getUploadFileName();
UrlResource resource = new UrlResource("file://" + getFullPath(fileName));
String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
추가로, 업로드 사이즈 제한을 aaplication.properties에 추가해주었다.
spring.servlet.multipart.max-file-size=5242880
spring.servlet.multipart.max-request-size=10485760
나머지 내용인 테스트 코드에 대한 설명은 생략하기로 한다.
구현이 힘들진 않았지만 몇 번 막히는 게 있었다. 다음에 구현할 때는 더 잘 할 수 있을 것 같다.
팀 프로젝트를 진행하고 있는데 거기서는 로직 구현은 물론이고, AWS 등에 대해 같이 스터디하면서 많이 배울 것이다.
'공부 > Spring' 카테고리의 다른 글
[Spring Boot] 게시판 만들기(1) - 개요, 기본 기능 구현 (0) | 2024.03.20 |
---|---|
[Spring JPA](5) API 개발 - 지연 로딩과 조회 성능 최적화, 컬렉션 조회 최적화 (2) | 2024.03.05 |
[Spring JPA](4) 웹 계층 개발 (0) | 2024.03.03 |
[Spring JPA](3) 주문 도메인 개발 (0) | 2024.03.03 |
[Spring JPA](2) 회원 & 상품 도메인 개발 (0) | 2024.03.03 |