[JPA] 게시판 프로젝트 - 도메인 설계 및 회원 기능 개발 및 테스트 (2)

2024. 12. 30. 21:30·프로젝트/Board 프로젝트

 

이전 개발 단계 보러 가기

https://taetae99.tistory.com/27

 

벡엔드 토이 프로젝트 (게시판) - 프로젝트 명세서 작성 (1)

목차     0. 프로젝트를 시작하며 간단한 프로젝트를 진행하며 학습과 기록에 의미를 두고 있습니다.부족한 부분이 많습니다. 구글링과 강의를 통해 학습을 하며 진행하고 있습니다. 잘못된

taetae99.tistory.com

 

 

저번 프로젝트 명세서를 바탕으로 이번에는 총 3단계로 구분하여 진행했습니다.

 

목차


     

    1. 도메인 설계

     

    1. Member

    
    @Entity
    @Getter
    public class Member extends BaseTimeEntity{
    
        @Id @GeneratedValue
        @Column(name = "member_id")
        private Long id;
    
        @Column(nullable = false)
        private String name;
        @Column(nullable = false, unique = true)
        private String email;
        @Column(nullable = false)
        private String password;
        @Column(nullable = false, unique = true)
        private String nickname;
    
        protected Member() {
        }
    
        public Member(String name, String email, String password, String nickname) {
            this.name = name;
            this.email = email;
            this.password = password;
            this.nickname = nickname;
        }
    
        public static Member createMember(String name, String email, String password, String nickname) {
            return new Member(name, email, password, nickname);
        }
    
        /**
         * 양방향 연관관계를 위해 생성함.
         * mappedBy가 있는 쪽은 외래 키를 관리하지 않는다.
         * mappedBy를 통해 연관 관계의 주인을 명시한다.
         * 외래 키가 어떤 엔티티에서 관리되는지 알려준다.
         */
        @OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
        private List<Post> posts = new ArrayList<>();
    
        //변경 할 수 있는 데이터는 패스워드와 닉네임뿐
        public void update(String password,String nickname){
            this.password = password;
            this.nickname = nickname;
        }
    
    }

     

    • 실무에서는 @Setter 어노테이션 없이 작성하는 것이 좋다고 하여 제외하였다.
    • 정적 팩토리 메서드(createMember)를 사용하여 객체를 생성한다.
    • 컬럼에 맞는 제약 조건들을 추가하였다.(nullable=false 또는 unique= true)
    • Member와 Post는 양방향 관계를 갖도록 설정하였다. mappedBy를 통해 연관관계의 주인을 명시하였는데 이는 Post.member이다.
    • Cascade.All로 설정하여 부모 엔티티에 대한 작업이 자식에게 전파되도록 한다.

     

     

    2. Post

     

    
    @Entity
    @Getter
    public class Post extends BaseTimeEntity{
    
        @Id
        @GeneratedValue
        @Column(name = "post_id")
        private Long id;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
    
        @Column(length = 500, nullable = false)
        private String title;
    
        @Column(length = 1000, nullable = false)
        private String content;
    
        @Column(name = "view_count",nullable = false)
        private Long viewCount=0L;
    
        @Column(nullable = false)
        private String writer;
    
        @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
        private List<Comments> comments = new ArrayList<>();
    
        protected Post() {
    
        }
        public Post(Member member,String title,String content,String writer){
            setMember(member);
            this.title = title;
            this.content = content;
            this.writer = writer;
        }
        public static Post createPost(Member member,String title,String content,String writer) {
            return new Post(member,title,content,writer);
        }
    
        //연관 관계 메서드
        public void setMember(Member member) {
            this.member = member;
            member.getPosts().add(this);
        }
    
        // 연관 관계 해제 메서드
        public void removeMember() {
            this.member.getPosts().remove(this);
            this.member = null;
        }
    
        /**
         * 비즈니스 로직 작성
         */
        //게시글 수정
        public void updatePost(String title, String content) {
            this.title = title;
            this.content = content;
        }
    
        //조회 수 증가
        public void addViewCount() {
            this.viewCount++;
        }
    
    
    
    }

     

    • JoinColumn를 사용하여 외래키의 컬럼을 지정해 주었다.
    • 연관 관계 메서드를 작성하여 변경이 생겼을 때 연관 관계를 일관성 있게 관리한다.

     

     

     

    3. Comments

     

    
    @Entity
    @Getter
    public class Comments extends BaseTimeEntity {
    
        @Id
        @GeneratedValue
        @Column(name = "comments_id")
        private Long id;
    
        @Column(nullable = false)
        private String comment;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "post_id")
        private Post post;
    
        protected Comments() {
        }
    
        public Comments(String comment, Member member, Post post) {
            addComments(post);
            this.member = member;
            this.comment = comment;
        }
    
        public static Comments createComments(String comment, Member member, Post post) {
            return new Comments(comment, member, post);
        }
    
    
        //연관관계 메서드
        public void addComments(Post post) {
            post.getComments().add(this);
            this.post = post;
        }
        public void removePost() {
            this.post.getComments().remove(this);
            this.post = null;
        }
    
        /**
         * 비즈니스 로직
         */
        public void updateComments(String comment) {
            this.comment = comment;
        }
    
    
    }

     

    • Post에서 연관 관계 메서드에서 post를 변경하는 부분이 있어서 따로 메서드를 작성하였다. 

     

     

    4. BaseTimeEntity

     

    
    /**
     * @MappedSuperclass는 BaseTimeEntity를 상속 할 경우
     * 두 필드를 컬럼으로 인식하기 위함이다.
     */
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class BaseTimeEntity {
    
        @CreatedDate
        @Column(name = "created_date")
        private LocalDateTime createdDate;
    
        @LastModifiedDate
        @Column(name = "modified_Date")
        private LocalDateTime modifiedDate;
    }

     

    • BaseTimeEntity : Member, Post, Comments에 컬럼으로 생성 시간과 수정시간이 필요하여 만들었다. 이는 jpa의 시간 관련 필드를 자동으로 관리하기 위한 추상 클래스로 상속받아 사용한다. 

     

     

    2. 회원 기능 개발(MemberRepository, MemberService)

     

    Spring Data JPA를 사용하지 않았습니다.

    JPA 학습을 목표로 하기 때문에 코드를 직접 작성할 예정입니다.

     

     

    1. MemberRepository - SpringJPA를 사용함.

    
    @Repository
    public class MemberRepository {
        @PersistenceContext
        EntityManager em;
    
    
        public void save(Member member) {
            em.persist(member);
        }
    
        public Member findOne(Long id) {
            return em.find(Member.class, id);
        }
    
        public List<Member> findAll() {
            return em.createQuery("select m from Member m", Member.class)
                    .getResultList();
        }
    
        public List<Member> findByNickName(String nickname) {
            return em.createQuery("select m from Member m where m.nickname = :nickname",Member.class)
                    .setParameter("nickname",nickname)
                    .getResultList();
        }
    
        public List<Member> findByName(String name) {
            return em.createQuery("select m from Member m where m.name = :name", Member.class)
                    .setParameter("name", name)
                    .getResultList();
        }
    
    }

     

    • 주요 기능 : 회원 저장, id로 회원 찾기 , 모든 회원 찾기
    • JPQL을 사용하여 닉네임으로 회원 찾기, 이름으로 회원 찾기를 구현하였다.

     

    2. MemberService

    
    @Service
    @Transactional(readOnly = true)
    @RequiredArgsConstructor
    public class MemberService {
    
        /**
         * 생성자 주입을 사용한다.
         */
        private final MemberRepository memberRepository;
        private final PostRepository postRepository;
        private final CommentRepository commentRepository;
    
        @Transactional
        public Long join(Member member){
            //닉네임 검증만 했음.
            validateDuplicateMember(member);
            memberRepository.save(member);
            return member.getId();
        }
    
        @Transactional
        public void updateMember(Long memberId, String password, String nickname) {
            Member member = memberRepository.findOne(memberId);
            if (nickname.equals(member.getNickname())) {
                validateDuplicateMember(member);
            }
            member.update(password, nickname);
        }
    
        public List<Member> findMembers() {
            return memberRepository.findAll();
        }
        public Member findOne(Long memberId) {
            return memberRepository.findOne(memberId);
        }
    
        @Transactional
        public MemberInfoDto getMemberInfo(Member member) {
            MemberInfoDto memberInfoDto = new MemberInfoDto(member);
    
            memberInfoDto.setPosts(postRepository.findAllByMember(member.getId()));
            memberInfoDto.setComments(commentRepository.findByMember(member.getId()));
    
            return memberInfoDto;
        }
    
        private void validateDuplicateMember(Member member) {
            List<Member> findMembers = memberRepository.findByNickName(member.getNickname());
            if (!findMembers.isEmpty()) {
                throw new IllegalStateException("이미 존재하는 닉네임입니다.");
            }
        }
    }

     

    • validateDuplicateMember : MemberRepo에 회원 정보를 저장하기 전 검증을 한다. 이는 중복 닉네임에 대한 검증을 진행한다.   이메일도 unique 제약 조건을 갖고 있기 때문에 검증을 진행해야 한다.

     

    2-1. MemberInfoDto

    
    @Getter
    @Setter
    public class MemberInfoDto {
        private Long memberId;
        private String name;
        private String email;
        private String nickname;
        private List<Post> posts;
        private List<Comments> comments;
    
        public MemberInfoDto(Member member) {
            this.memberId = member.getId();
            this.name = member.getName();
            this.email = member.getEmail();
            this.nickname = member.getNickname();
        }
    }

     

    • 개인 페이지에 간단한 개인정보와 작성한 댓글, 작성한 게시글을 가져오도록 생성하였다.

     

    3. 회원 기능 테스트 - JUnit5

     

    
    @SpringBootTest
    @Transactional
    class MemberServiceTest {
        @Autowired MemberService memberService;
    
        @Test
        public void 회원가입(){
            //given
            Member member = Member.createMember("kim","spring@naver.com",
                    "password123","kinopio");
            //when
            Long savedId = memberService.join(member);
    
            //then
            assertEquals(member, memberService.findOne(savedId));
        }
    
        @Test
        public void 중복_닉네임(){
            //given
            Member member1 = Member.createMember("kim","spring@naver.com",
                    "password123","kinopio");
            Member member2 = Member.createMember("lee","spring@google.com",
                    "password234","kinopio");
            //when
            memberService.join(member1);
            IllegalStateException exception = assertThrows(IllegalStateException.class,
                    () -> memberService.join(member2));
    
            //then
            assertEquals("이미 존재하는 닉네임입니다.", exception.getMessage());
    
        }
    
        @Test
        public void 회원정보수정() {
            //given
            Member member = Member.createMember("kim","spring@naver.com",
                    "password123","kinopio");
            //when
            memberService.join(member);
            memberService.updateMember(member.getId(), "1234", "newname");
            Member changedMember = memberService.findOne(member.getId());
            //then
            assertThat(changedMember.getPassword()).isEqualTo("1234");
            assertThat(changedMember.getNickname()).isEqualTo("newname");
        }
    
        @Test
    //    @Rollback(value = false)
        public void 회원가입_롤백X() {
            //given
            Member member1 = Member.createMember("kim","spring@naver.com",
                    "password123","kinopio");
            Member member2 = Member.createMember("lee","spring@google.com",
                    "password234","mario");
            //when
            memberService.join(member1);
            memberService.join(member2);
    
            //then
            List<Member> members = memberService.findMembers();
            assertThat(members.size()).isEqualTo(2);
        }
    
    }

     

    • 회원가입() : Member를 생성하고 저장한다. 이때 생성한 객체와 검색한 객체가 동일한지 검증한다.
    • 중복_닉네임() : 닉네임이 동일한 객체를 두 개 생성한다. 두 번째 join시에 member2의 닉네임이 중복되므로 Service에서 IllegalStateException이 발생한다.
    • 회원가입_롤백X() :Member 객체를 두 개 생성하고 가입한다. 이때 findAll 메서드를 통해 검색되는 총회원의 수가 2명인지 검증한다. 

     

    테스트 코드에서 @Transactional을 사용하면 코드실행 후 자동으로 데이터를 롤백시킨다. 
    만약 데이터를 직접 확인하고 싶다면 @Rollback(value=false)를 사용한다. 

     

    테스트 통과 모습

     

    @Rollback 어노테이션을 사용하여 데이터가 저장되었음을 확인한다.

     

    반응형

    '프로젝트 > Board 프로젝트' 카테고리의 다른 글

    [JPA] 게시판 프로젝트 - 마이 페이지 구현 및 Dto와 Form 설계 (5)  (0) 2025.01.24
    [JPA] 게시판 프로젝트 - 로그인 및 게시판 메인화면 구현, Spring Security를 사용한 로그인 (4)  (3) 2025.01.17
    [JPA] 게시판 프로젝트 - 게시글,댓글 기능 개발 및 테스트 (3)  (1) 2025.01.09
    [JPA] 게시판 프로젝트 - 프로젝트 명세서 작성 (1)  (4) 2024.12.29
    '프로젝트/Board 프로젝트' 카테고리의 다른 글
    • [JPA] 게시판 프로젝트 - 마이 페이지 구현 및 Dto와 Form 설계 (5)
    • [JPA] 게시판 프로젝트 - 로그인 및 게시판 메인화면 구현, Spring Security를 사용한 로그인 (4)
    • [JPA] 게시판 프로젝트 - 게시글,댓글 기능 개발 및 테스트 (3)
    • [JPA] 게시판 프로젝트 - 프로젝트 명세서 작성 (1)
    taetae99
    taetae99
    우직하게 개발하기
      반응형
    • taetae99
      코드 대장간
      taetae99
    • 전체
      오늘
      어제
      • 분류 전체보기
        • Teck Stack
          • Java
          • Spring
          • DB
          • Redis
          • SpringSecurity
          • Docker
          • HTML
          • AWS
        • 우아한테크코스
        • CS & Architecture
          • DDD
          • CS
          • 디자인 패턴
        • 트러블 슈팅
        • 알고리즘
          • 프로그래머스
          • 백준
        • 프로젝트
          • Board 프로젝트
        • 기타
        • 대회 및 후기
    • 인기 글

    • hELLO· Designed By정상우.v4.10.3
    taetae99
    [JPA] 게시판 프로젝트 - 도메인 설계 및 회원 기능 개발 및 테스트 (2)
    상단으로

    티스토리툴바