Spring

Spring Project Board(게시판) 1

code2772 2023. 1. 2. 12:15
728x90
반응형

✔ 게시판 프로젝트

게시판 프로젝트

개발환경
- 개발도구 : 인텔리제이
- 소스코드 관리 : Git
- Git 호스팅 : GitHub
- Git GUI : 깃크라켄 (https://www.gitkraken.com/)
- SDK : JDK 17

목표 
- 누구나 사용하기 쉽게 명확한 기능의 요구사항을 만듬
- 요규사항을 구현하는데 도움이 되는 각종 문서 작업을 경험
- 자바 + 스프링부트로 요규사항을 실제로 구현하는 기술적인 방법을 습득
- 최신 버전의 기술을 사용하면서 기술 동향을 파악하고 새로운 기술들을 익혀봄
- 기획과 문서 작성부터 개발, 형상관리, 테스트. 배포까지 개발 프로세스 전반을 경험

문서작업
https://www.diagrams.net/ (draw.io) : 도메인과 ERD 설계
구글 시트 : API 문서 
깃 + 깃헙 : 커밋 메시지 작성, 프로젝트 관리 및 협업 환경 생성

다양한 형태의 문서작업
- 업무의 가이드
- 생산성을 높여줌
- 나은 방법을 제안하거나 오류를 잡아줌
- 내용이 구체적일 수록 동료들의 프로젝트 개발 내용이 잘 동기화되고 진행이 막히지 않음
(주의 : 업데이트가 중요, 잘못된 정보가 주는 혼란)
- 백업이 용이, 문서는 지나간 일을 다시 꺼내야 할 때 쉽게 찾기를 도와줌
- 기억은 짧고 왜곡되지만, 문서는 수정 가능하고 발전하며 오래 감
- 업무 기록을 남김으로써 업무 진척 상황과 내 성과가 잘 드러남

개발 목적
- 고객이 원치 않거나 고객의 문제를 해결해줄 수 없는 개발은 의미가 없음
- 가능한 최신 버전 기술과 트렌드 적인 기술을 사용

테스트와 배포
- 테스트
    개발 요규사항이 빠짐 없이 모두 구현되었는지 확인
    구현도한 요구사항이 오류없이 동작하는 확인
- 베포
    클라우드 서버에 배포(헤로쿠)


애자일(agile methodology)
신속한 반복작업을 통해 실제 작동 가능한 소프트웨어를 개발하여 지속적으로 제공하기 위한 소프트웨어 개발 방식

지라(Jira)
애자일 방법론을 기반으로 만들어진 소프트웨어

깃 브랜치 전략
깃 브랜치를 운영하는 방법론을

gitflow
- 브랜치를 설정하고 운영하는 방식
    Master : 제품으로 출시될 수 있는 브랜치
    Develop : 다음 출시 버전을 개발하는 브랜치
    Feature : 기능을 개발하는 브랜치
    Release : 이전 출시 버전을 준비하는 브랜치
    Hotfix : 출시 버전에서 발샌한 버그를 수정하는 브랜치

github flow
- 브랜치만으로 운영하는 방식
    main : 제품으로 출시될 수 있는 브랜치
    Feature : 기능을 개발하는 브랜치

 

✔ application.yaml

debug: false
management.endpoints.web.exposure.include: "*"

logging:
  level:
    com.koreait.projectboard: debug
    org.springframework.web.servlet: debug
    org.hibernate.type.descriptor.sql.BasicBinder: trace

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/aidev
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    defer-datasource-initialization: true
    hibernate.ddl-auto: create
    show-sql: true
    properties:
      hibernate.format_sql: true
      hibernate.default_batch_fetch_size: 100
#  h2.console.enabled: true
  sql.init.mode: always
server:
  port: 8888

 

✔ config

package com.koreait.projectboard.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import java.util.Optional;


@EnableJpaAuditing
@Configuration
public class JpaConfig {
    @Bean// 데이터를 넣어주기 위해 사용하며 세팅되지 않은
    public AuditorAware<String> auditorAware(){
        return () -> Optional.of("관리자");    // 스프링 시큐리티로 인증 기능을 붙이게 될 때 수정할거임

    }

}

 

✔ ArticleRepository

package com.koreait.projectboard.repository;

import com.koreait.projectboard.domain.Article;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ArticleRepository extends JpaRepository<Article, Long> {
}

JpaRepossitort : Spring FramWork의 데이터 검색 시 편리하게 검색 할 수 있도록 한다. 검색 메소드(findByID)를 사용할 수 있게 된다.  -> JpaRepossitort <엔티티 , ID 유형>  형식으로 작성

 

✔ Domain - Article

@Getter
@ToString
@Table(indexes = { //@Table은 엔티티와 매핑할 테이블을 지정
        @Index(columnList = "title"),
        @Index(columnList = "hashtag"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
        // name을 추가하면 테이블이름이 name값으로 설정이 되고 생략시 Entity이름으로 테이블이 만들어지는 것을 확인할 수 있다.
})
@EntityListeners(AuditingEntityListener.class)
@Entity
//@Entity 어노테이션은 JPA를 사용해 테이블과 매핑할 클래스에 붙여주는 어노테이션이다. 이 어노테이션을 붙임으로써 JPA가 해당 클래스를 관리하게 된다.
public class Article {
    @Id// Entity 입력할 경우 프라이머리 키를 선언해 줘야한다.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 생성을 DB에 위임 (Mysql)
    private Long id;
    @Setter @Column(nullable = false) private String title; // 제목
    @Setter @Column(nullable = false, length = 10000) private String content; // 본문
    @Setter private String hashtag; // 해시태그

    @CreatedDate @Column(nullable = false) private LocalDateTime createdAt; // 생성일시 수정 시 자동 업데이트
    @CreatedBy @Column(nullable = false, length = 100) private String createdBy; // 생성자
    @LastModifiedDate @Column(nullable = false) private LocalDateTime modifiedAt; // 수정일시
    @LastModifiedBy @Column(nullable = false, length = 100) private String modifiedBy; // 수정자

    @ToString.Exclude
    @OrderBy("id")
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
    private final Set<ArticleComment> articleComments = new LinkedHashSet<>();

    protected Article() {}

    private Article(String title, String content, String hashtag) {
        this.title = title;
        this.content = content;
        this.hashtag = hashtag;
    }

    public static Article of(String title, String content, String hashtag) {
        return new Article(title, content, hashtag);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj) return true;
        if(!(obj instanceof Article article)) return false;
        return id != null && id.equals(article.id);
    }
}

 

✔ ArticleCommentRepository

package com.koreait.projectboard.repository;

import com.koreait.projectboard.domain.ArticleComment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ArticleCommentRepository extends JpaRepository<ArticleComment, Long> {

}

 

✔ Domain - ArticleComment

package com.koreait.projectboard.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

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


@Getter
@ToString
@Table(indexes = {
        @Index(columnList = "content"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
@EntityListeners(AuditingEntityListener.class)
@Entity
public class ArticleComment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Setter @ManyToOne(optional = false) private Article article; //게시글(id)
    @Setter @Column(nullable = false, length = 500) private String content; // 본문

    @CreatedDate @Column(nullable = false) private LocalDateTime createdAt;    // 생성일시
    @CreatedBy  @Column(nullable = false) private String createdBy;   // 생성자
    @LastModifiedDate @Column(nullable = false) private LocalDateTime modifiedAt;   //수정일시
    @LastModifiedBy @Column(nullable = false) private String modifiedBy;  // 수정자

    protected ArticleComment() {}
    private ArticleComment (Article article, String content)
    {
        this.article = article;
        this.content = content;
    }

    public static ArticleComment of (Article article, String content) {
        return new ArticleComment(article, content); // 외부에서 받아올 수 있도록 선언해주기
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj) return true;
        if(!(obj instanceof ArticleComment articleComment)) return false;
        return id != null && id.equals(articleComment.id);
    }
}

 

✔ Test - Select

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@Import(JpaConfig.class)
@DisplayName("JPA 연결 테스트")
@DataJpaTest
class JpaRepositoryTest {
    private final ArticleRepository articleRepository;
    private final ArticleCommentRepository articleCommentRepository;

    public JpaRepositoryTest(
            @Autowired ArticleRepository articleRepository,
            @Autowired ArticleCommentRepository articleCommentRepository
    ){
        this.articleRepository = articleRepository;
        this.articleCommentRepository = articleCommentRepository;
    }

    @DisplayName("select 테스트")
    @Test
    void select(){
        List<Article> articles = articleRepository.findAll();
        assertThat(articles).isNotNull().hasSize(1000);

    }

✔ Test - Isert

@DisplayName("insert test")
@Test
void insert(){
    long prevCount = articleRepository.count();
    Article saveArticle = articleRepository.save(Article.of("새로운 글","새로운 글을 등록합니다","#new"));
            //Article.of : 내용 집어넣기
    assertThat(articleRepository.count()).isEqualTo(prevCount+1);
    // 이전 데이터보다 늘어났으면 통과 한다는 의미이다. 추가한 경우 늘어나야 하기 때문이다. 비교하는 것을 의미(assertThat)
}

 

@DisplayName("insert test(해당 콘솔 창에 원하는 이름)") : 사용 시 하단 그림과 같이 어떤 콘솔창이 나오는지 알 수 있다. 

@

✔ Test - Update

@DisplayName("update test")
@Test
void update(){
    Article article =articleRepository.findById(1l).orElseThrow();
    //orElseThrow() ; exception을 던짐 찾는것이 없으면
    String updateHashTag = "#Spring";
    article.setHashtag(updateHashTag);

    Article savedArticle = articleRepository.saveAndFlush(article);// 재 시작 시 드랍되어 보지 못하는 경우 플러시로 확인가능하게
    assertThat(savedArticle).hasFieldOrPropertyWithValue("hashtag",updateHashTag ); // 내가 업데이트 한것이 맞는지
}

 

✔ Test - delete

   @DisplayName("delete test")
    @Test
    void delete(){
        Article article = articleRepository.findById(1l).orElseThrow();
        long preArticleCount = articleRepository.count(); // 1000개 가 들어왔는지 확인
        long preArticleCommentCount = articleCommentRepository.count(); // 댓글 수를 확인
        int deletedCommentsSize = article.getArticleComments().size();// 댁글 수

        articleRepository.delete(article); // 원본 글을 삭제

        assertThat(articleRepository.count()).isEqualTo(preArticleCount -1); // 삭제니 -1인지 확인
        assertThat(articleCommentRepository.count()).isEqualTo(preArticleCommentCount -deletedCommentsSize);
        //뺀 값과 같은지 확인을 해봐야 하니 빼서 값이 맞는지 확인을 해야 하낟.
    }
}
반응형