게시판 프로젝트 - 중간 진행 상황

최민수·2023년 3월 29일
0

[개발] Java Spring

목록 보기
12/18
post-thumbnail

게시판 프로젝트 with Spring boot

저번 주 월요일(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>

좋아요 기능 추가 시 ManyToMany 어노테이션 사용

Question 객체와 SiteUser, Answer 객체와 SiteUser 간의 좋아요 기능을 만들기 위해선 이들 간의 양방향 매핑이 필요합니다. 하나의 질문에 여러 유저가 좋아요를 남길 수 있고, 한 사람의 유저가 여러 글에 좋아요를 달 수 있기 때문에 N:M, 즉 ManyToMany 관계입니다.

하지만 ManyToMany 연관 관계 매핑의 경우 실무에서는 지양되고, pk와 fk로 직접 매핑된 중간 테이블을 따로 생성해 개발한다고 공부했던 기억이 있습니다.

아래 글은 이 상황에 왜 문제가 되는지, 그리고 중간에 매핑 테이블을 둬야 하는 이유에 대해 제가 정리한 글입니다.

https://velog.io/@lucaschoi/관계형-데이터-모델링-NM-의-처리

따라서, 실무 개발을 대비해 기본기를 닦자는 생각으로 중간 테이블 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

profile
CS, 개발 공부기록 🌱

0개의 댓글