저번 주 월요일(3.20) 부터 시작해 계속 이어온 “점프 투 스프링 부트, 게시판 프로젝트” 의 중간 진행 상황 기록 겸 글을 작성해봤습니다.
오늘 집중적으로 구현한 파트는 수정과 삭제
그리고 좋아요 기능 추가
입니다.
기능을 구현하기 위한 모든 과정은 점프 투 스프링 부트에 잘 나와있기 때문에, 다시 언급하는 것은 의미가 없을 것 같습니다.
대신, 실제로 코드를 작성하고 테스트해보면서 겪었던 문제
와 과정
에 대해서 소개하도록 하겠습니다.
가장 먼저 맞딱드린 의문은 수정 시 사용할 폼 객체가 등록 때 사용했던 폼을 그대로 쓴다는 점 이었습니다.
저는 개인적으로 수정할 때와 생성할 때의 폼 객체는 최소한 분리해야 한다고 생각합니다.
글 수정 시에는 비지니스적으로 “제목은 수정할 수 없다” 와 같은 요구사항이 있을 수 있습니다. 그리고 글 생성 시에는 또 다른 요구사항이 계속해서 추가될 가능성이 큽니다.
따라서, 아무리 지금은 작은 서비스라도 dto 객체 둘을 분리하는 것이 맞다고 판단했습니다.
QuestionEdit dto 객체를 기존의 QuestionForm과 분리한 모습입니다.
지금은 따로 제한사항을 두지 않아 두 파일의 내용이 같습니다. 이후에 수정 시에는 “제목은 수정할 수 없다” 같은 요구사항이 생기면, 아래 코드와 같이 바꿀 수 있습니다.
이렇게 되면 다른 개발자가 제가 작업하던 내용을 보고도 어떤 필드만 수정이 가능한지 알 수 있을 것입니다.
@Getter
@Setter
public class QuestionEdit {
@NotEmpty(message = "내용을 입력해주세요.")
private String content;
public QuestionEdit(String content) {
this.content = content;
}
}
점프 투 스프링 부트 책에서는 삭제 시 javascript 코드를 사용하여 다소 복잡하게 alert을 띄워 확인하고, 다시 url을 찾아가는 방법을 쓰고 있는 것 같습니다.
그 대신, 아래 코드와 같이 return confirm('~~message');
같은 식으로 코드를 onclick event에 넣으면 자동으로 alert 창을 띄워 확인과 취소 로직을 수행할 수 있습니다.
‘취소’ 라면, return false 가 되어 뒤의 로직은 수행되지 않아 매우 편리합니다.
<a onclick="return confirm('정말로 삭제하시겠습니까?');" th:href="@{|/question/delete/${question.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
th:text="삭제"></a>
Question 객체와 SiteUser, Answer 객체와 SiteUser 간의 좋아요 기능을 만들기 위해선 이들 간의 양방향 매핑이 필요합니다. 하나의 질문에 여러 유저가 좋아요를 남길 수 있고, 한 사람의 유저가 여러 글에 좋아요를 달 수 있기 때문에 N:M, 즉 ManyToMany 관계입니다.
하지만 ManyToMany 연관 관계 매핑의 경우 실무에서는 지양되고, pk와 fk로 직접 매핑된 중간 테이블을 따로 생성해 개발한다고 공부했던 기억이 있습니다.
아래 글은 이 상황에 왜 문제가 되는지, 그리고 중간에 매핑 테이블을 둬야 하는 이유에 대해 제가 정리한 글입니다.
따라서, 실무 개발을 대비해 기본기를 닦자는 생각으로 중간 테이블 QuestionLikes
, AnswerLikes
테이블을 따로 만들어 OneToMany
, ManyToOne
관계로 풀어서 진행을 해보았습니다.
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AnswerLikes {
@Id
@GeneratedValue
@Column(name = "answer_likes_id")
private Long id;
@ManyToOne(fetch = LAZY)
private Answer answer;
@ManyToOne(fetch = LAZY)
private SiteUser user;
// 생성 메서드
public static AnswerLikes create(Answer answer, SiteUser user) {
AnswerLikes answerLikes = new AnswerLikes();
answer.addLikes(answerLikes);
user.addAnswerLikes(answerLikes);
return answerLikes;
}
}
위와 같이 Answer 객체와 SiteUser 객체 사이에 AnswerLikes
테이블을 두어 각각을 ManyToOne 으로 설정하고, Answer와 SiteUser 객체에서도 다음과 같이 OneToMany로 양방향 연관관계를 걸어주었습니다.
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Answer {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
// ... 생략
@OneToMany(mappedBy = "answer")
private Set<AnswerLikes> answerLikes = new LinkedHashSet<>();
// ... 생략
// 연관관계 메서드
public void addLikes(AnswerLikes answerLikes) {
this.answerLikes.add(answerLikes);
answerLikes.setAnswer(this);
}
}
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SiteUser {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
// ... 생략
@OneToMany(mappedBy = "author", cascade = ALL)
private List<Question> questions;
@OneToMany(mappedBy = "author", cascade = ALL)
private List<Answer> answers;
@OneToMany(mappedBy = "user")
private Set<QuestionLikes> userLikesQuestion;
@OneToMany(mappedBy = "user")
private Set<AnswerLikes> userLikesAnswer;
// ... 생략
//==연관관계 메서드==//
public void addQuestionLikes(QuestionLikes questionLikes) {
this.userLikesQuestion.add(questionLikes);
questionLikes.setUser(this);
}
public void addAnswerLikes(AnswerLikes answerLikes) {
this.userLikesAnswer.add(answerLikes);
answerLikes.setUser(this);
}
}
여기서 중요했던 포인트는 연관관계 메서드를 잘 정의해줘야 했다는 점입니다.
저는 Answer 의 좋아요 수를 증가할 때 AnswerService
클래스에서 AnswerLikes
객체의 create 메서드를 호출해 Answer ↔ AnswerLikes 의 관계, 그리고 SiteUser ↔ AnswerLikes 의 관계를 모두 세팅해 주는 식의 로직을 구성했습니다.
그리고 AnswerLikes
의 레포지토리를 따로 생성해 영속성 컨텍스트에 save 하여 반영하게 했습니다.
Question의 추천 기능 또한 완전히 똑같은 로직으로 진행했습니다. Question의 경우에는 QuestionLikes
테이블을 중간 테이블로 두었습니다.
한 가지 개선할 점은, 한 사람의 유저가 같은 글에 여러 번 추천을 눌러도 현재는 모두 반영되게 동작하는데 이 부분을 개선할 예정입니다.
책과 똑같이 실습하지 않고 위와 같이 이것저것 바꿔보며 진행해서 개발 속도가 조금 느렸습니다.
이 밖에도 글 수정 시 최초의 작성일자를 지우고 (수정됨) 작성자 수정일시
와 같은 포맷으로 변경하도록 하기도 했고, 작성자가 Null 일 경우 “알수없음” 으로 처리하는 등의 UI 요소도 고려했습니다.
이번 주 안으로 남은 앵커
, 마크다운
, 검색 기능
, 추가 기능
까지 완성할 예정입니다.
참고한 사이트: https://wikidocs.net/book/7601