본문 바로가기
Spring Boot Project/Plming

[Plming] 태그 관련 연관 관계 매핑

by slchoi 2022. 4. 1.
728x90
SMALL

게시글을 생성할 때 사용자가 게시글과 관련된 태그를 선택할 수 있도록 구현하기로 했다.

이 경우 게시글과 태그를 일대다 관계로 구현하기 위해 두 테이블을 연결하기 위한 연관 Entity가 필요하다. 따라서 태그를 구현하는데 필요한 연관 Entity를 먼저 생성해줄 것이다. (DB 연관관계 설정 부분에서 생성한 Entity를 약간 수정했다.)

 

1. Entity 생성 및 수정

먼저 Tag entity를 생성해볼 것이다. "plming" 패키지 아래 tag 패키지를 생성하고 "entity" 패키지를 생성한 후 Tag 클래스를 생성한다. Tag 클래스 생성이 완료되면 더보기 코드를 작성한다.

더보기
package plming.tag.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Tag {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;    // PK

    @Column(columnDefinition = "varchar")
    private String name;

    @Builder
    public Tag(String name) {
        this.name = name;
    }
}

 

다음으로 같은 패키지 내에 TagRepository 인터페이스를 생성하고, 아래 코드를 작성한다.

package plming.tag.entity;

import org.springframework.data.jpa.repository.JpaRepository;
import plming.board.entity.BoardTag;

import javax.xml.bind.JAXBElement;
import java.util.List;

public interface TagRepository extends JpaRepository<Tag, Long> {

}

 

두 번째 Entity로 BoardTag Entity를 생성할 것이다. "plming.board.entity" 패키지 내에 BoardTag 클래스를 생성한 뒤 더보기 코드를 작성한다. 

더보기
package plming.board.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import plming.tag.entity.Tag;

import javax.persistence.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "post_tag")
public class BoardTag {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = "post_id")
    private Board board;

    @ManyToOne
    @JoinColumn(name = "tag_id")
    private Tag tag;

    @Builder
    public BoardTag(Board board, Tag tag) {
        this.board = board;
        this.tag = tag;
    }
}

 

 

다음으로 같은 패키지 내에 BoardTagRepository 인터페이스를 생성하고, 아래 코드를 작성한다.

package plming.board.entity;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface BoardTagRepository extends JpaRepository<BoardTag, Long> {

}

 

마지막으로 Board entity를 더보기 코드로 수정한다.

더보기
package plming.board.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import plming.user.entity.User;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "post")
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;    // PK

    /* 추가 */
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @Column(columnDefinition = "enum")
    private String category;    // 카테고리

    @Column(columnDefinition = "enum")
    private String status;  // 모집 상태

    @Column(columnDefinition = "varchar")
    private String period;  // 진행 기간

    @Column(columnDefinition = "varchar")
    private String title;   // 제목

    @Column(columnDefinition = "varchar")
    private String content; // 내용

    @Column(columnDefinition = "Integer")
    private Integer participantNum = 0;

    @Column(columnDefinition = "bigint")
    private Long viewCnt = 0L;

    @Column(columnDefinition = "datetime")
    private LocalDateTime createDate = LocalDateTime.now();

    @Column(columnDefinition = "datetime")
    private LocalDateTime updateDate;

    @Column(columnDefinition = "enum")
    private char deleteYn = '0';

    // 역방향
    @OneToMany(mappedBy = "board")
    private List<BoardTag> boardTags;

//    @Builder
//    public Board(User user, String category, String status, String period, String title, String content) {
//        this.user = user;
//        this.category = category;
//        this.status = status;
//        this.period = period;
//        this.title = title;
//        this.content = content;
//    }

    @Builder
    public Board(User user, String category, String status, String period, String title, String content, List<BoardTag> boardTags) {
        this.user = user;
        this.category = category;
        this.status = status;
        this.period = period;
        this.title = title;
        this.content = content;
        this.boardTags = boardTags;
    }

    /**
     * 게시글 수정
     */
    public void update(String title, String content, String category, String status, String period) {
        this.title = title;
        this.content = content;
        this.category = category;
        this.status = status;
        this.period = period;
        this.updateDate = LocalDateTime.now();
    }

    /**
     * 게시글 조회 수 증가
     */
    public void increaseCount() {
        this.viewCnt++;
    }

    /**
     * 게시글 삭제
     */
    public void delete() {
        this.deleteYn = '1';
    }

}

 

Entity를 모두 생성 및 수정하고 나면 연관 관계 매핑까지 완료된 상태가 된다. 이제 게시글을 생성, 수정, 삭제, 조회할 때 각 게시글의 태그 값도 함께 생성, 수정, 삭제, 조회되도록 기존 코드를 수정해볼 것이다.

 

2. 태그 관련 기능 추가

2.1. 게시글 생성

사용자가 게시글을 생성할 때 태그를 선택하면 프론트엔드 분들이 태그 id 값을 리스트화해 Request Body에 담아 넘겨주기로 하셨다.

태그 Id 리스트를 받아올 수 있게 BoardRequestDto 클래스에 tagIds 변수를 추가한다.

private List<Long> tagIds;  // tag ID 리스트

 

게시글을 DB에 저장할 때 기존 코드를 살펴보면 (BoardService의 save( ) 메서드)

/**
 * 게시글 생성
 */ 
@Transactional
public Long save(final BoardRequestDto params) { 
    User user = userRepository.getById(params.getUserId());
    Board entity = boardRepository.save(params.toEntity(user));
    return entity.getId();
}
  • Request Body로 전달된 userId를 사용해 user 정보를 가져와 나머지 정보들과 user 정보를 사용해 Request Body로 전달된 값을 가지는 Board 엔티티를 생성하고, DB에 저장한다.
  • DB에서 방금 생성한 게시글의 id를 반환한다.

 

이 코드에 태그 Id 리스트 값을 가져와 DB의 board_tag 테이블에 게시글 id와 태그 id 리스트 값을 저장하는 코드를 추가할 것이다.

먼저 "plming.board.model" 패키지 아래 BoardTagService 클래스를 생성하고, 더보기 코드를 작성한다.

더보기
package plming.board.model;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import plming.board.entity.Board;
import plming.board.entity.BoardTag;
import plming.board.entity.BoardTagRepository;
import plming.tag.entity.TagRepository;

import java.util.List;

@Service
@RequiredArgsConstructor
public class BoardTagService {

    private final BoardTagRepository boardTagRepository;
    private final TagRepository tagRepository;

    /**
     * 게시글 태그 저장
     */
    @Transactional
    public void save(List<Long> tagIdList, Board entity) {
        for (Long tagId : tagIdList) {
            BoardTag boardTag = BoardTag.builder()
                    .board(entity)
                    .tag(tagRepository.getById(tagId))
                    .build();
            boardTagRepository.save(boardTag);
        }
    }
}

 

코드를 살펴보면

  • 매개변수로 태그 id 리스트와 Board 엔티티를 받아온다.
  • for-each 문에서 태그 id 리스트에서 태그 id 값을 하나씩 가져와 boardTag 엔티티를 생성하고, board_tag 테이블에 저장한다.
  • for 문을 사용하지 않고 구현하려고 했지만, 방법이 생각나지 않아 우선 for문을 사용해서 구현했다. (게시글 태그 개수를 5개로 제한하기로 해서 for문으로 해도 크게 무리 없을 것 같긴 하다..)

 

다음으로 BoardService의 save( ) 메서드를 아래 코드로 수정한다.

/**
 * 게시글 생성
 */
@Transactional
public Long save(final BoardRequestDto params) {

    User user = userRepository.getById(params.getUserId());
    Board entity = boardRepository.save(params.toEntity(user));
    List<Long> boardTagIds = params.getTagIds();
    boardTagService.save(boardTagIds, entity);

    return entity.getId();
}
  • boardTagIds 변수로 태그 id 리스트 값을 받아오고, boardService의 save 메서드의 매개변수로 boardTagIds와 Board 엔티티를 전달한다.
  • 게시글 저장이 끝나면, 생성한 게시글의 id를 반환한다.

 

2.2. 게시글 삭제

게시글을 삭제 기능은 실제로 DB에서 데이터를 지우는 것이 아니라 post 테이블의 delete_yn의 값을 1로 변경해주는 작업이었다. 하지만 게시글이 삭제되면, 게시글 태그 정보를 담고 있는 post_tag 테이블에서는 실제로 데이터를 지울 것이다.

먼저, BoardTagRepository에 아래 메서드를 추가한다.

/**
 * 게시글 ID 기준 태그 id 삭제
 */
void deleteAllByBoardId(final Long boardId);

 

  • 게시글 id를 매개변수로 받아와 post_tag 테이블의 post_id 값과 비교해 매개변수로 받은 값과 같은 값을 가지는 데이터를 삭제한다. 

 

 

 

다음으로 BoardService의 delete( ) 메서드를 아래 코드로 수정한다.

/**
 * 게시글 삭제
 */
@Transactional
public Long delete(final Long id) {

    Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));
    entity.delete();
    boardTagRepository.deleteAllByBoardId(id);	// 추가
    return id;
}
  • deleteAllByBoardId 메서드를 호출하는 코드를 추가한다.

 

2.3. 게시글 수정

게시글을 수정할 때 태그 값이 변경될 수 있으므로 태그 값도 같이 업데이트되어야 한다. 가장 쉽게 구현할 수 있는 방법을 찾다 보니 수정하려는 게시글의 id를 가져와 현재 post_tag 테이블에서 수정하려는 게시글 id값과 같은 post_id 값을 가지는 데이터를 모두 삭제한 뒤 태그 id 값을 다시 받아와 post_tag에 저장하는 방법으로 구현하기로 했다.

BoardService의 update( ) 메서드를 아래 코드로 수정한다.

/**
 * 게시글 수정
 */
@Transactional
public Long update(final Long id, final BoardRequestDto params) {

    Board entity = boardRepository.findById(id).orElseThrow(() -> new CustomException(ErrorCode.POSTS_NOT_FOUND));
    entity.update(params.getTitle(), params.getContent(), params.getCategory(), params.getStatus(), params.getPeriod());

    boardTagRepository.deleteAllByBoardId(id);	// 추가
    boardTagService.save(params.getTagIds(), entity);	// 추가

    return id;
}
  • 게시글 삭제 부분에서 구현한 태그 값을 삭제해주는 메서드인 deleteAllByBoardId를 호출하는 코드를 추가한다.
  • 다시 태그 값을 저장하기 위해 게시글 생성 부분에서 구현한 태그 값을 추가해주는 메서드인 save 메서드를 추가한다.

 

2.4. 게시글 조회

이제 게시글을 조회할 경우 게시글의 태그 값도 함께 반환해주어야 한다. 그러기 위해 BoardResponseDto 클래스의 코드를 더보기 코드와 같이 수정한다.

package plming.board.dto;

import lombok.Getter;
import plming.board.entity.Board;

import java.time.LocalDateTime;
import java.util.List;

@Getter
public class BoardResponseDto {

    private Long id;
    private Long userId;
    private String category;
    private String status;
    private String period;
    private String title;
    private String content;
    private Integer participantNum;
    private Long viewCnt;
    private LocalDateTime createDate;
    private LocalDateTime updateDate;
    private char deleteYn;
    private List<String> tags;

    public BoardResponseDto(Board entity, List<String> tags) {
        this.id = entity.getId();
        this.userId = entity.getUser().getId();
        this.category = entity.getCategory();
        this.status = entity.getStatus();
        this.period = entity.getPeriod();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.participantNum = entity.getParticipantNum();
        this.viewCnt = entity.getViewCnt();
        this.createDate = entity.getCreateDate();
        this.updateDate = entity.getUpdateDate();
        this.deleteYn = entity.getDeleteYn();
        this.tags = tags;
    }
}
  • 프론트엔드에서 태그를 전달받을 때 태그의 id 값이 아닌 name으로 전달받고 싶다고 하셔서 태그 값은 List<String>으로 전달하기로 했다.

 

다음으로 BoardTagRepository에 아래 메서드를 추가한다.

/**
 * 게시글 ID 기준 태그 id 리스트 조회
 */
List<BoardTag> findAllByBoardId(final Long boardId);
  • boardId를 매개변수로 전달하면 post_tag 테이블에서 전달받은 boardId와 같은 값을 가진 post_id의 tag_id를 리스트화해 반환한다.

 

다음으로 BoardTagService에 아래 메서드를 추가한다.

/**
 * 게시글 id로 태그 이름 조회
 */
public List<String> findTagNameByBoardId (final Long id) {
    List<BoardTag> boardTagList = boardTagRepository.findAllByBoardId(id);
    return boardTagList.stream().map(BoardTag::getTag).map(Tag::getName).collect(Collectors.toList());
}
  • 게시글 id를 매개변수로 전달하면, findAllByBoardId 메서드를 호출해 해당 id와 post_tag의 post_id가 같은 BoardTag 엔티티를 리스트로 가져온다.
  • BoardTag 엔티티 리스트의 모든 BoardTag 엔티티에 대해 getTag 메서드를 적용해 Tag 엔티티를 가져오고 getName메서드를 적용해 태그 이름을 가져온 후 리스트화해 반환한다.

 

다음으로 BoardService에 아래 메서드를 추가한다.

/**
 * 각 게시글의 태그 이름 조회
 */
private List<BoardResponseDto> getTagName(List<Board> list) {
    List<BoardResponseDto> result = new ArrayList<BoardResponseDto>();
    // List<List<String>> tags= list.stream().map(Board::getId).map(boardTagService::findTagNameByBoardId).collect(Collectors.toList());
    for (Board post : list) {
            List<String> tagName = boardTagService.findTagNameByBoardId(post.getId());
            result.add(new BoardResponseDto(post, tagName));
    }
    return result;
}
  • Board 엔티티 리스트가 매개변수로 들어오면, 리스트에 있는 게시글의 id로 해당 게시글의 tag를 찾고 게시글과 태그를 BoardResponseDto의 매개변수로 전달해 BoardResponseDto를 생성한다.
  • 생성된 모든 BoardResponseDto를 리스트에 담아 반환한다.
  • 여기서도 for문을 사용해서 구현했는데 이 부분도 개선할 수 있는 방법이 있는지 생각해봐야겠다.

 

마지막으로 BoardService의 findAll( ), findAllByUserId( ), findAllByDeleteYn( ) 메서드를 더보기와 같이 수정한다.

더보기
/**
 * 게시글 리스트 조회
 */
public List<BoardResponseDto> findAll() {

    Sort sort = Sort.by(DESC, "id", "createDate");
    List<Board> list = boardRepository.findAll(sort);
    return getTagName(list);
}

/**
 * 게시글 리스트 조회 - (사용자 ID 기준)
 */
public List<BoardResponseDto> findAllByUserId(final Long userId) {

    Sort sort = Sort.by(DESC, "id", "createDate");
    List<Board> list = boardRepository.findAllByUserId(userId, sort);
    return getTagName(list);
}

/**
 * 게시글 리스트 조회 - (삭제 여부 기준)
 */
public List<BoardResponseDto> findAllByDeleteYn(final char deleteYn) {

    Sort sort = Sort.by(DESC, "id", "createDate");
    List<Board> list = boardRepository.findAllByDeleteYn(deleteYn, sort);
    return getTagName(list);
}
  • 기존의 return 값 대신에 getTagName( ) 메서드의 반환값을 반환하도록 수정한다.

 

게시글 태그 관련 설정은 끝났다. 이제 Postman을 사용해서 API를 테스트해볼 것이다. 

 

3. API 테스트

첫 번째로 Create를 테스트해보자.

create 테스트

Postman에서 Post 요청을 보내면 응답으로 생성한 게시글의 id를 보내주는 것을 확인할 수 있다.

DB 확인

MySQL에서 post 테이블을 조회해보면 입력한 내용대로 게시글이 잘 생성된 것을 확인할 수 있고, post_tag 테이블에도 post_id와 입력한 tag_id 값이 잘 들어가 있는 것을 확인할 수 있다.

 

두 번째로 Read 테스트를 해보자. 먼저 게시글 리스트를 조회하는 findAll( ) 메서드를 테스트해볼 것이다.

findAll( ) 테스트

Postman에서 쿼리 파라키터로 deleteYn의 값을 지정하고 GET 요청을 보내면 응답으로 게시글 리스트가 반환되는 것을 확인할 수 있다.

다음으로 사용자가 작성한 게시글 리스트를 반환하는 findByUserId( ) 메서드를 테스트할 것이다. id가 15인 사용자의 게시글 리스트를 조회해보자.

findByUserId( ) 테스트

Postman에서 쿼리 파라미터로 userId를 지정하고 GET 요청을 보내면 해당 사용자가 작성한 게시글 리스트를 응답으로 보내주는 것을 확인할 수 있다.

다음으로 게시글의 상세 정보를 반환하는 findById( )를 테스트해볼 것이다.

findById( ) 테스트

Postman에서 게시글 id를 URI에 넣어주고 GET 요청을 하면 게시글의 상세 정보가 반환되는 것을 확인할 수 있다.

 

세 번째로 Update를 테스트해보자. id가 57인 게시글을 수정해볼 것이다. 제목, 카테고리, 기간, 내용, 모집 상태, 태그까지 모든 내용을 변경해보자.

Update 테스트

Postman으로 변경 내용을 Request Body에 입력한 뒤 Patch 요청을 하면 수정된 게시글의 id가 반환되는 것을 확인할 수 있다.

post 테이블

MySQL에서 post 테이블을 조회해보면 57번 게시글이 요청한 내용대로 잘 수정된 것을 확인할 수 있다.

post_tag 테이블

post_tag 테이블을 확인해보면 57번 게시글에 해당하는 tag_id 값이 잘 변경되어 있는 것을 확인할 수 있다.

수정한 게시글의 tag 값을 확인하기 위해 Postman에서 57번 게시글의 상세 정보를 요청해보자.

수정한 게시글 상세 정보 요청

수정된 태그 id 값을 가지는 태그가 반환되는 것을 확인할 수 있다.

 

마지막으로 Delete를 테스트해볼 것이다. id가 57인 게시글을 삭제해보자.

delete 테스트

Postman으로 삭제하고 싶은 게시글 id를 URI에 넣어준 뒤 Delete 요청을 하면 삭제한 게시글의 id를 반환하는 것을 확인할 수 있다.

DB 확인

DB에서 post 테이블을 조회해보면 57번 게시글의 delete_yn의 값이 1로 잘 변경된 것을 확인할 수 있다.

post_tag 테이블

post_tag 테이블을 조회해보면 게시글을 삭제할 때 삭제한 게시글의 id에 해당하는 태그 정보도 함께 삭제된 것을 확인할 수 있다.

게시글 리스트 조회

Postman으로 다시 게시글 리스트를 조회해보면 삭제된 57번 게시글은 반환하지 않는 것을 확인할 수 있다.

728x90
LIST

댓글