본문 바로가기
Spring Boot Project/Plming

[Plming] DB 연관 관계 설정

by slchoi 2022. 3. 30.
728x90
SMALL

이제 게시판 테이블과 관련된 테이블의 연관 관계를 설정할 것이다. 게시판 DB와 연관관계를 설정해야 할 DB는 아래와 같다.

user 테이블

  • 사용자 정보를 가지고 있는 테이블
  • 기존의 게시판 테이블에서는 user 컬럼에 사용자의 이름을 값으로 직접 가지고 있었지만, post 테이블과 user 테이블 연관 관계를 설정해 post 테이블의 user_id 컬럼에서 게시글 작성자의 id 값을 가지고 있도록 변경할 것이다.
  • tag: tag 테이블의 컬럼으로는 id와 name이 있으며

tag 테이블

  • 게시글에 달린 tag의 정보를 가지고 있는 테이블
  • 컬럼으로 id와 name이 있다.

post_tag 테이블

  • 게시글에 달린 태그의 정보를 저장하는 테이블
  • 컬럼으로 id, post_id, tag_id를 가진다.
  • post_id 컬럼은 post 테이블의 id를 외래키로 가지고, tag_id 컬럼은 tag 테이블의 id를 외래키로 가진다.

 

1. MySQL에서 테이블 생성

가장 먼저 user 테이블을 생성한다.

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `nickname` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '닉네임',
  `email` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `password` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '비밀번호',
  `image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '프로필사진파일명',
  `introduce` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '자기소개',
  `github` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '깃허브ID',
  `social` tinyint DEFAULT NULL COMMENT '소셜로그인방법',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

그다음으로 기존에 있던 post 테이블을 drop 하고 아래 코드로 다시 생성한다.

CREATE TABLE `post` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL COMMENT '작성자ID',
  `title` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '제목',
  `category` enum("공모전", "스터디", "프로젝트", "기타") NOT NULL COMMENT '카테고리',
  `status` enum("모집 중", "모집 완료") NOT NULL COMMENT '모집상태',
  `participant_num` int default 0 COMMENT '모집인원수',
  `period` varchar(50) COLLATE utf8mb4_unicode_ci not null COMMENT '예상모임기간',
  `content` varchar(1000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '내용',
  `view_cnt` bigint default 0 COMMENT '조회수',
  `create_date` datetime NOT NULL COMMENT '생성일자',
  `update_date` datetime COMMENT '마지막수정일자',
  `delete_yn` enum('0', '1') not null default '0' comment '삭제 여부',
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `post_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

 

세 번째로 tag 테이블을 생성한다.

CREATE TABLE `tag` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(40) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

마지막으로 post_tag 테이블을 생성한다.

CREATE TABLE `post_tag` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `post_id` bigint NOT NULL COMMENT '게시글ID',
  `tag_id` int NOT NULL COMMENT '태그ID',
  PRIMARY KEY (`id`),
  KEY `post_id` (`post_id`),
  KEY `tag_id` (`tag_id`),
  CONSTRAINT `post_tag_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE,
  CONSTRAINT `post_tag_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;select * from board;

 

2. Entity 생성하기

tag와 user Entity를 먼저 생성해볼 것이다.

"main.java.plming" 패키지 내에 tag와 user 패키지를 생성하고 각각의 패키지 내에 entity 패키지를 생성한 후 Tag, User 클래스를 생성한다.

폴더 구조

그다음 Tag와 User 클래스에 더보기 코드를 작성한다.

Tag 클래스

  • Tag 클래스 같은 경우, DB에서 tag 값을 읽어오는데 필요하기 때문에 Builder 메서드는 생성하지 않아도 된다.
더보기
package plming.tag.entity;

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

import javax.persistence.*;

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

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

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

 

User 클래스

더보기
package plming.user.entity;

import lombok.*;

import javax.persistence.*;

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

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

    @Column(name= "nickname", nullable = false)
    private String nickname; // 닉네임, 수정O

    @Column(name= "email", nullable = false)
    private String email; // 이메일, 수정X

    @Column(name= "password")
    private String password; // 비밀번호, 수정O

    @Column(name= "image")
    private String image; // 프로필사진파일명, 수정O

    @Column(name= "introduce")
    private String introduce; // 자기소개, 수정O

    @Column(name= "github")
    private String github; // 깃허브ID, 수정O

    @Column(name= "role", nullable = false)
    private String role; // ROLE_USER, ROLE_ADMIN, 수정X

    @Column(name= "social", nullable = false)
    private int social; // 회원가입방법(0 : 기본, 1 : 구글, 2 : 카카오, 3 : 깃허브), 수정X

    @Builder
    public User(String nickname, String email, String password, String image, String introduce, String github, String role, int social){
        this.nickname = nickname;
        this.email = email;
        this.password = password;
        this.image = image;
        this.introduce = introduce;
        this.github = github;
        this.role = role;
        this.social = social;
    }
}

다른 팀원이 넘겨준 코드를 그대로 가져와 사용하려고 하는데 DB에서 생성하지 않은 컬럼(role)이 보여 DB의 user 테이블에 해당 컬럼을 추가한다.

alter table user add role varchar(20) not null

 

그다음으로 "board.entity" 내에 postTag 클래스를 생성한다. postTag는 게시글에 설정된 태그 값을 저장하는 테이블로 id, 게시글 id, 태그 id 값을 가진다.

package plming.board.entity;

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

import javax.persistence.*;

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

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

    @Column(columnDefinition = "bigint")
    private Long postId;

    @Column(columnDefinition = "bigint")
    private Long tagId;

    @Builder
    public PostTag(Long postId, Long tagId) {
        this.postId = postId;
        this.tagId = tagId;
    }
}

 

3. Repository 인터페이스 생성하기

조금 전에 생성한 User 클래스에 대한 Repository 인터페이스를 생성할 것이다. UserRepository 인터페이스는 User 클래스와 동일한 경로에 생성해야 한다.

package plming.user.entity;

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

public interface UserRepository extends JpaRepository<User, Long> {

}

 

4. Board 클래스와 User 클래스 연관관계 매핑하기

객체는 Board.user라는 참조 변수를 이용해 회원 객체와 단방향의 관계를 맺는다. 테이블에서는 post와 user가 외래키를 이용해 서로 양방향 관계를 가지지만, 객체 관계에서는 User는 Board에 대한 정보를 알 수 없다. 따라서 연관관계 매핑을 위해 회원 클래스를 추가한 것이다. 또한 Board와 User는 다대일 관계이다.

이제 Board 클래스에 다대일 연관 매핑 설정을 추가할 것이다. 기존의 Board 클래스를 더보기 코드처럼 변경한다.

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;

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

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

//    @Column(columnDefinition = "bigint")
//    private Long userId;    // 작성자 id

    /* 추가 */
    @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';

    @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;
    }

    /**
     * 게시글 수정
     */
    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';
    }
}
  • 연관 관계를 매핑하기 위해 user 변수를 선언했고, 다대일 관계 설정을 위한 @ManyToOne 어노테이션을 추가했다. 또한, 외래 키 매핑을 위한 @JoinColumn 어노테이션을 추가했다.
  • @Builder 부분에서 기존에 사용하던 userId 대신 user 변수로 변경한다.

 

4.1. 연관 관계 매핑 테스트

연관 관계 매핑 테스트를 위해 "src.test.java.plming.board" 패키지에 RelationMappingTest를 추가하고 더보기 코드를 작성한다.

더보기
package plming.board;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import plming.board.entity.Board;
import plming.board.entity.BoardRepository;
import plming.user.entity.User;
import plming.user.entity.UserRepository;

@SpringBootTest
public class RelationMappingTest {

    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testManyToOneInsert() {
        User user1 = User.builder()
                .email("email@email.com")
                .github("github")
                .image("no image")
                .introduce("introduce")
                .nickname("nickname")
                .password("password")
                .role("ROLE_USER")
                .social(1)
                .build();
        userRepository.save(user1);

        User user2 = User.builder()
                .email("email2@email.com")
                .github("github2")
                .image("no image")
                .introduce("introduce2")
                .nickname("nickname2")
                .password("password2")
                .role("ROLE_ADMIN")
                .social(1)
                .build();

        userRepository.save(user2);

        for(int i = 1; i<=3; i++) {
            Board post = Board.builder()
                    .user(user1)
                    .content("사용자1의 " + i + "번째 게시글입니다.")
                    .period(i + "개월")
                    .category("스터디")
                    .status("모집 중")
                    .title("사용자1의 게시글" + i)
                    .build();
            boardRepository.save(post);
        }

        for(int i = 1; i<=3; i++) {
            Board post = Board.builder()
                    .user(user2)
                    .content("사용자2의 " + i + "번째 게시글입니다.")
                    .period(i + "개월")
                    .category("프로젝트")
                    .status("모집 중")
                    .title("사용자2의 게시글" + i)
                    .build();
            boardRepository.save(post);
        }
    }
}

테스트를 실행시켜보면 잘 실행되는 것을 확인할 수 있다.

연관 관계 매핑 테스트 결과

MySQL Workbench에서 user 테이블을 확인해보면 데이터가 잘 들어간 것을 확인할 수 있다.

user 테이블 확인

post 테이블을 확인해보면 user_id 부분에 게시글을 작성한 user의 id가 잘 들어가 있는 것을 확인할 수 있다.

 

4.2. API 테스트

기존에 생성한 게시글 생성, 수정, 삭제, 리스트 조회, 상세 조회 API에서 잘 작동하는지 확인해볼 것이다. 테스트를 시작하기 전에 Response DTO와 Request DTO 클래스를 수정해야 한다.

BoardRequestDTO

package plming.board.dto;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import plming.board.entity.Board;
import plming.user.entity.UserRepository;

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

    @Autowired
    private UserRepository userRepository;

    private Long userId;    // 사용자
    private String category;    // 카테고리
    private String status;  // 모집 상태
    private String period;  // 진행 기간
    private String title;   // 제목
    private String content; // 내용

    public Board toEntity() {
        return Board.builder()
                .title(title)
                .user(userRepository.findById(userId).get())
                .status(status)
                .category(category)
                .period(period)
                .content(content)
                .build();
    }

}

BoardResponseDTO

package plming.board.dto;

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

import java.time.LocalDateTime;

@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;

    public BoardResponseDto(Board entity) {
        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();
    }
}

 

먼저, 게시글을 리스트를 응답하는 findAll( ) 메서드를 테스트할 것이다.

findAll( ) 테스트 결과

 GET 메서드를 사용해 요청을 보내면 삭제되지 않은 게시글만 리스트화해 응답해주는 것을 확인할 수 있다.

 

다음으로, 게시글의 상세 페이지를 보여주는 findById( ) 메서드를 테스트해볼 것이다.

findById( ) 테스트 결과

id에 해당되는 게시글의 정보를 응답해주는 것을 확인할 수 있다. 게시글 조회 수도 잘 증가되었다.

 

다음으로, 게시글을 삭제시키는 delete( ) 메서드를 테스트해볼 것이다. id가 13인 게시글을 삭제시킨 후 findAll( ) 메서드를 실행해보자.

delete 요청 결과
findAll( ) 실행 결과

findAll( ) 실행 결과를 보면 삭제한 게시글을 제외하고 나머지 게시글을 리스트화해 응답하는 것을 확인할 수 있다.

DB 확인

DB에서 확인해보면 id가 13인 게시글의 delete_yn의 값이 1로 변경된 것을 확인할 수 있다.

 

다음으로 게시글을 수정하는 update( ) 메서드를 테스트해볼 것이다. id가 12인 게시글을 수정해보자.

update( ) 테스트 결과

응답으로 수정한 게시글의 id를 반환하는 것을 확인할 수 있다.

DB 확인

DB에서 12번 게시글을 확인해보면 내용이 잘 수정되고, update_date 값도 잘 들어간 것을 확인할 수 있다.

 

마지막으로 게시글을 생성하는 save( ) 메서드를 테스트해볼 것이다. id가 4인 사용자가 게시글을 생성했다고 가정하고 테스트해보자.

save( ) 메서드 테스트
save( ) 오류 메시지

내부 서버 오류라는 메시지가 뜬다... 오류 메시지를 확인해보니 NullPointerException 오류다..

어디서 난 오류인지 확인하기 위해 디버깅을 해봤더니 userRepository를 통해 user 정보를 확인하는 부분이 BoardRequestDto 부분에 들어가 있는데, Postman에서 Request body 부분에 userRepository 정보를 넣어주지 않아서 이 부분에서 NullPointException이 발생한 것이다.

이 에러를 해결하기 위해 BoardRequestDto 클래스 코드와 BoardService 클래스의 service( ) 메서드 코드를 아래와 같이 변경한다.

BoardRequestDto 클래스

package plming.board.dto;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import plming.board.entity.Board;
import plming.user.entity.User;

import java.util.Optional;

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

    private Long userId;    // 사용자
    private String category;    // 카테고리
    private String status;  // 모집 상태
    private String period;  // 진행 기간
    private String title;   // 제목
    private String content; // 내용

    public Board toEntity(User user) {
        return Board.builder()
                .user(user)
                .title(title)
                .status(status)
                .category(category)
                .period(period)
                .content(content)
                .build();
    }

}

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();
}

코드를 수정한 뒤 서버를 재실행시키고 Postman을 실행한다.

save( ) 메서드 실행 결과

응답으로 생성된 게시글의 id를 잘 반환해주는 것을 확인할 수 있다.

DB 확인

DB에서 확인해보면 게시글이 잘 들어가 있는 것을 확인할 수 있다.

728x90
LIST

댓글