게시판 만들기 - 데이터 베이스 접근 로직 테스트 정의

정영찬·2022년 8월 4일
0

프로젝트 실습

목록 보기
21/60

도메인 클래스 설계를 통해 게시글과 댓글의 데이터 구성을 설계 했고, 자바 클래스도 작성을 완료한 상태이다. 이제 이 데이터들을 어떻게 데이터베이스에 연결하고 사용할 것인지, 그리고 어떤 데이터베이스를 사용할 것인지를 결정한다.

어떤 데이터 베이스를 선택할 것인가?

mysql을 사용한다. 공부에서 실무까지 두루두루 사용되는 오픈소스 데이터베이스이기 때문에 이를 선택했다. 실무를 진행할 때는 여러가지 판단 기준을 바탕으로 데이터베이스를 선정해야한다.

db-engines라는 사이트에서 현재 개발자들이 자주 사용하는 데이터베이스의 순위를 확인해 볼 수 있다. 이를 통해서 조직에서 원하는 데이터베이스를 찾아내는 것이 중요하다.

https://db-engines.com/en/ranking

깃허브 베타에서 프로젝트 카드에 내용을 작성하고 이슈로 변경했다.

항상 왜? 이렇게 했는지를 다른 개발자들도 알 수 있게 설명을 작성하는 자세를 갖자. 내가 어떤 사이트를 참고해서 이랬다든지, 어떤 자료를 확인해서 이렇게 하는것이 좋겠다라는지를 작성해주는 것이 좋다.

DB에 접근할 수 있는 상태로 환경 세팅하기

이제 이슈를 만들었으면 깃크라켄에 브랜치를 만드는 것도 잊지 말자.

mySQL을 선택하기로 했으니 mySQL을 사용하자. 참고로 인텔리제이 유료버전은 화면 우측에 database라는 항목이 보이지만, 커뮤니티 버전은 지원하지 않으므로 Preference(Setting) -> Plugin 에서 Database Navigator를 설치하고 재시작하면 view -> Tool Windows -> DB Browser를 클릭하면 db 도구를 사용할 수 있다.

+버튼을 눌러서 db를 추가한다.

설정을 마치고 테스트를 돌려보는데...

요런 메시지가 나타나서 조사해보니 "MySQL 버전 5.1.23보다 높은 버전을 사용하면 MySQL 타임존의 시간표현 포맷이 달라져서 connector 에서 인식을 하지 못한다고 하는 듯 하다." 라고 한다.

이럴 때는 명시적으로 어떤 time zone 을 사용하는지 지정해 주어서 해결할 수 있다.
설치된 mySQL command line client로 들어가서 time zone 조회를 해본다.

SELECT @@global.time_zone, @@session.time_zone;

그러면 time_zone의 값을 변동시키는 명령어를 실행시켜본다.

SET GLOBAL time_zone ="Asia/Seoul";
SET time_zone="Asia/Seoul";

내 경우는

이렇게 나타나는데 tiem zone 의 데이터가 mysql에 없어서 생기는 현상이라고 한다. 따라서 다운로드를 받아서 실행해야한다.

https://dev.mysql.com/downloads/timezones.html 여기로 가서 윈도우 사용자의 경우는 Non POSIX with leap seconds 항목을 다운로드 한다.

압축파일의 압축을 풀어주고 mysql 스키마에 실행해준다

source 압축푼 파일의 절대경로

실행되고 난 뒤

select b.name, a.time_zone_id
    -> from mysql.time_zone a, mysql.time_zone_name b
    -> where a.time_zone_id = b.time_zone_id and b.name like '%Seoul';

이런 명령어를 작성후 실행해보면


이렇게 time_zone의 정보가 기록되어있다. 이제 time_zone을 설정해주면 완료된다.

위 이미지와 같이 명령어를 입력하고 나면 설정이 변경되어있을 것이다.

여기서 끝이 아니라 my.ini파일을 들어가서 맨 아랫줄에 해당 문구를 작성한다.

# Default time-zone setting - (기본 time-zone설정)
default-time-zone=Asia/Seoul

my.ini의 위치는 C:\ProgramData\MySQL\MySQL Server 8.0 였다.

그리고 mysql 서버를 재실행한다.

그리고 명령어를 입력해서 확인해보면?

이렇게 time_zone의 값이 적용된 것을 확인 할 수 있다.

인텔리제이로 돌아와서 다시 테스트를 해보면?

굿! 성공적으로 연결된 것이 확인 되었다.

이제 DB browser를 사용해서 db 조작을 해본다. 나는 workbench를 사용했다.
먼저 root 계정말고 다른 계정 하나를 생성한다.

create user '본인이 정하고싶은 유저 이름'@'localhost' identified by '본인이 정하고 싶은 패스워드!';

내 경우는 jyc라는 이름으로 계정을 하나 만들었다.

select `user`, host from mysql.user;
or
select `user` from `mysql`.`user`;

을 작성하면 계정 목록을 확인할 수 있다.

권한을 확인해 볼 수도 있다.

show grants for '계정 이름'@'localhost';

현재는 권한이 없는 상태임을 뜻하므로, 권한을 부여해준다.

grant all on `board`.* to '계정'@'localhost' with grant option 

board 데이터베이스 에 한에서만, 해당 계정은 모든권한을 부여 받음과 동시에(grant all), 다른 계정에게도 그와 같은 권한을 부여해줄수 있게 하라는 명령어(with grant option)이다.

이러고 나서 다시 권한을 확인해 보면?

이렇게 권한이 추가된 모습을 볼 수 있다.

만약 권한을 쥐어줬는데고 제 기능을 발휘하지 않을 때는
flush privileges라는 명령어를 실행해주면 된다.

이제 콘솔 작업은 여기까지 하고, 스프링 부트에다가 DB에 접근하기 위해서 어떤 기술을 사용할 것이나면, JPA, 커넥터로써 mySQL 드라이버, 테스트용 인메모리 데이터 베이스를 따로 만들어서 분리된 환경에서 테스트를 진행할 것이다. 분리된 환경에서 테스트를 진행하면, 테스트를 진행하면서 DB에 크고 작은 변화가 일어나도 실제 프로젝트에는 영향을 끼치지 않게 된다.

https://start.spring.io/ 로 가서 Dependencies를 검색하여 추가한다.

인메모리 데이터베이스로 H2를 사용한다.

이전에는 generate를 눌러서 다운로드 했지만, 이번에는 dependencies만 필요로 하기 때문에 explore를 누르면 해당 옵션으로 파일을 만들면 어떤 모습이 될지 미리 볼 수 있다.

여기서 dependencies코드만 복사해서 스프링 프로젝트의 내용에 추가를 하는 것이다.
1~3번째 줄 내용을 복사해서build.gradle파일의 dependencies항목에 붙여 넣었다.

bulletin-board/build.gradle

저장후 gradle을 새로고침하면 Dependencies에 추가된 것을 확인할 수 있다.

그럼 이제 환경세팅은 마무리.

인텔리제이 왼쪽에 못보던 탭이 생기는데, JPA structure, jpa기술을 자바스프링으로 사용할 때 필요로 하는 업무를 간편화 시켜주는 도구이다. 이것은 JPA buddy라는 플러그인을 다운로드 해서 생긴 것이다.

테스트

이전에 작성한 도메인들을 데이터베이스의 엔티티로 변환시킨다. 그 전에 설정옵션을 바꾸자. jpa를 추가시켰기 때문에 resorce/application.properties에다가 jpa에 접근하기위한 property를 세팅해야한다.

property 파일을 사용하기 편하기 yaml파일로 변경한다.

application.yaml

debug: false // 로그백이 디버그 로그를 찍게 할것이냐?
management.endpoints.web.exposure.include: "*"  // 액츄에이터 엔드포인트중에서 감춰져있는 요소들을 활성화 시킴

logging: 
  level: 
    com.jycproject.bulletinboard: debug //루트 패키지에서 발생하는 로그는 디버그 레벨로 보겠다.
    org.springframework.web.servlet: debug // 스프링 프레임워크 웹 서블렛에서 발생하는 로그는 디버그 레벨로 보겠다.
    org.hibernate.type.descriptor.sql.BasicBinder: trace //jpa 기술을 사용할때 쿼리로그를 디버그 로그로 관찰 할 수 있다. 이때, 바인딩 파라미터는 전부 '?'로 나타나는데 그 애들을 관찰 할수 있게 해준다. 개발단계 에서는 꼭 필요함

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/board
    username: jyc
    password: 본인이 정한 패스워드
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    open-in-view: false 
    defer-datasource-initialization: true // 테스트용 데이어베이스 데이터를 생성한다. 해당 데이터는 resource 폴더에 새 파일을 만들어서 쿼리를 작성할 것이다. 이름은 data.sql 테스트용 데이터베이스를 띄울 때 맨 처음에 초기값으로 실행되었으면 하는 내용을 작성해놓는다!
    hibernate.ddl-auto: create // 엔티티의 내용을 바탕으로 자동으로 ddl을 만들어준다. 테스트가 실행될때 자동으로 article과 articlecomment 테이블이 만들어진다.
    show-sql: true // sql 문장 보여줄까?
    properties: // 내가 사용하는 구현체에서 따로 전용으로 사용되는 프로퍼티가 있을 경우 여기에 집어 넣어서 활성화 시킨다.
      hibernate.format_sql: true // 한줄로 주르륵 나오는 디버그 쿼리문을 예쁘게 포멧팅해서 보여주는 기능
      hibernate.default_batch_fetch_size: 100 // jpa에서 뭔가 복잡한 쿼리를 사용하게 될때 한번에 벌크로 셀렉트하게 해준다. ex) 테이블 안에 조인테이블로 가져와야하는 로우 수가 10개가 될 때, 쿼리가 10개나 만들어지는데 그 대신에 id in query로 변경해서 하나의 쿼리로 만들어 주는 것이다. 여기서는 100으로 했으니 최대 100개의 쿼리를 하나의 쿼리로 묶어줄수 있게 설정한 것이다.
  h2:
    console:
      enabled: false // h2에서 제공하는 h2 console이 있는데 이거 쓸꺼니? 아니.
  sql.init.mode: always // 리소스에 만들었던 data.sql을 언제 작동 시킬꺼니? 실제 db를 띄우고 볼때고 테스트 데이터를 밀어놓고 볼수 있게 할 것이다.

--- // 이 세개의 직선을 기준으로 다른 다큐멘트가 작성된다. yaml의 특징

spring: //테스트 db용 프로필
  config.activate.on-profile: testdb // 여기에 작성된 무언가가 활성화 될때 이 문서를 읽어라 라는 뜻
  datasource: // 만약 testdb 프로파일을 사용한다면 데이터소스로 이걸 사용해라!
    url: ${JAWSDB_URL}
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa.hibernate.ddl-auto: create
  sql.init.mode: always

이렇게 작성하면 프로퍼티 세팅은 일단 끗

이제 연동이 잘 되어있는지 테스트를 돌려서 확인해본다.
test/java 에있는 jycBulletinBoardApplicationTests내부에서 contextLoads를 run 하면
script 에는 빈 내용이 존재해선 안된다는 오류가 뜨니까 일단 data.sql은 지워놨다.

테스트에는 문제 없고, 현재 인메모리 데이터베이스가 뜬 상태이지만, 현재 리포지토리나, 도메인, 엔티티가 하나도 잡히지 않았기 때문에 실제로 어떻게 연결되어있는지 관측이 어려운 상태이므로, 엔티티를 먼저 생성한다.

현재 domain/Articledomain/ArticleComment는 데이터만 적어놓은 상태이며, JPA annotation을 이용해서 엔티티로 변경시킨다.

erd diagram을 수정하기 위해서 remote에 #11-db 브랜치를 생성하기위해 push하고 난뒤 diagram.net에서 파일을 불러왔다.

게시글과 댓글의 본문 데이터를 너무 크게 잡아서 10000,500으로 각각 줄였다.

이전에 변경된 내용들이 있어도, 무시하고 pull을 사용해서 바로 변화를 적용시킬 수 있다.

나중에 검색을 수월하게 하기 위해서 데이터중 일부를 인덱싱한다.
domain/Article.java

@Getter
@ToString
@Table(indexes = {
        @Index(columnList ="title"),
        @Index(columnList ="hashtag"),
        @Index(columnList ="createdAt"),
        @Index(columnList ="createdBy")
})

그리고 엔티티를 생성한다.

@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //mysql의 increment 방식이 identity이므로 값을 identity로 변경해줘야한다.
    private Long id; 


    @Setter private String title; // 제목
    @Setter private String content; // 본문

    @Setter private String hashtag; // 해시태그

    private LocalDateTime createdAt; // 생성일시
    private String createdBy; // 생성자
    private LocalDateTime modifiedAt; // 수정일시
    private String modifiedBy; // 수정자
}

※ 그냥 전체에다가 Setter 걸지 뭐하러 따로따로 하느냐?

물론 그렇게 해도 되지만, id같은 경우는 내가 정하는 것이 아닌 사용자가 회원으로서 얻은 고유 번호이다. 생성일시와 생성자와 같은 값들도 내가 값을 조절할 이유가 없는 항목이기 때문에 따로 구분한 것이다.

생성과 수정 관련 항목들은 자동으로 세팅하게 만들려고 하는데 이때 사용하는 것이 jpa auditing이다.

main/java/com.프로젝트이름에 config패키지를 생성하고, 스프링 부트의 config를 만들때는 여기에 만들어 넣을 것이다.

package com.jycproject.bulletinboard.config;


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

@EnableJpaAuditing
@Configuration // configuration bin이 된다.
public class JpaConfig {

}

JpaAuditing?

java에서 ORM 기술인 JPA를 사용해서 도메인을 관계형 데이터베이스 테이블에 매핑할 때 공통적으로 도메인들이 가지고 있는 필드나 컬럼들이 존재하는데, 도메인마다 공통으로 존재한다는 의미는 코드가 중복된다는 뜻이기 때문에 데이터베이스에서 누가,언제했는가에 대한 기록을 남겨놓는것이 중요하다. 그래서 JPA에는 '감시하다'라는 뜻을 가진 Audit이라는 기능을 제공하는데, Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능이다. 도메인을 영속성 컨텍스트에 저장하거나 조회를 수행한 후에 update를 하는 경우 매번 시간 데이터를 입력해줘야하는데, audit을 이용하면 자동으로 시간을 매핑해서 데이터베이스 테이블에 넣어주게 된다.

다시 Article.java로 돌아와서 생성,수정 관련 데이터 항목에 annotation을 달아준다.

   @CreatedDate private LocalDateTime createdAt; // 생성일시
   @CreatedBy private String createdBy; // 생성자
   @LastModifiedDate private LocalDateTime modifiedAt; // 수정일시
   @LastModifiedBy private String modifiedBy; // 수정자

글이 작성되거나 수정될 때, auditing이 자동으로 이루어져서 데이터를 해당 변수에 넣어주게 된다.
생성일시 까지는 뭐 그냥 그럴수 있겠지만 현재 스프링 서큐리티나 다른 인증 기능을 사용하지 않았기 때문에 '누가?' 라는 데이터를 가져오는 것은 힘들다. 그래서 아까 생성한 JpaConfig에 세팅을 하는 것이다. auditing할 때, 사람의 이름 정보를 넣어주기 위한 config를 세팅 할 수 있다.

public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorAware() {
        return () -> Optional.of("jyc"); // TODO: 스프링 시큐리티로 인증 기능을 붙이게 될 때, 수정할 것!
    }
}

이름에 대한 데이터가 시간에 따라 업데이트 될 때 이름의 값을 jyc라고 해놓겠습니다 라는 뜻인데, 위 내용에서 붙여진 주석처럼 인증 기능을 추가했을 때는 독립적으로 회원가입을 통해 들어온 이름값을 지정해줘야 하기 때문에 나중에 수정을 필요로 하다는 정보를 추가해놓은 것이다.

그럼이제 생성자와 수정자의 값은 모두 jyc로 남게 될 것이다.

다음으로 @Column을 사용해서 정책들을 반영시킨다. 이때 nullable의 기본값은 true 이기 때문에 null값을 가져서는 안되는 데이터에만 작성해도 된다. 또한 Column에 아무옵션이 없을 경우에는 생략해도 된다.

public class Article {
  
      @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; // 수정자
   
}

데이터의 길이를 각각 다르게 설정했기 때문에 데이터의 길이 설정인 length를 통해서 이전에 svg로 그렸던 erd 다이어그램을 참고해서 길이를 설정했다.

여기까지가 엔티티를 구성하는 필드를 설정하는 방법이고, 엔티티로서의 기본기능을 만족시키기 위한 내용을 추가한다. 모든 JPA 엔티티들은 hibernate 구현체를 사용하는 기준으로 설명하면, 기본 생성자를 가지고 있어야 한다. 평소에는 오픈하지 않기 때문에 protected를 사용한다.

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

private 생성자를 통해서 해당 도메인과 관련된 정보만 오픈하는 방식으로 생성자를 통해 만들수 있게 유도하는 것으로 팩토리 메소드를 통해서 new키워드를 사용하지않아도 되게끔 public static으로 의도를 전달하는 것이다. 당신이 도메인 Article을 생성하려고 할 때 어떤 값을 필요로 하는지를 알려주는 것이다.

Article을 만들었지만, 만약 이 도메인을 컬렉션에서 사용한다면? 게시글의 리스트를 이용해서 게시판을 만든다던지 할 때 리스트를 통해 데이터를 받아오고 내보내줘야 하는 수요가 발생할 수 있다. 그러면 리스트에 넣거나 리스트나 컬렉션에 중복을 제거하거나 정렬을 해야할때는 비교를 할수 있어야 한다. 따라서 동일성 동등성 검사를 할수 있는 기능을 구현해야하는데, lombok의 @EqualsAndHashCode을 사용하면 쉽게 구현이 가능하다.

하지만, 이것을 사용하면 도메인에 있는 전체 필드를 모두 비교해서 기본적인 방식으로 @EqualsAndHashCode를 구현하게 된다.

엔티티에서만큼은 이 방식 말고 조금 독창적인 방식으로 구현을 해보려고 한다.

alt + ins를 동시에 눌러서 equals() and @hashcode()를 선택해서 비교해야할 대상을 선택해주는데, 게시글에서 서로 "다르다"라는 것을 비교하기위한 기준은 딱 하나면 된다! 바로 id값! 따라서 이전에 서술한 lombok을 통한 코드대신에 이 방법을 선택해서 퍼포먼스를 높이는 것이다! id값을 non-null 로 할것인지 null로 할것인지를 선택할수 있는데, id값을 nullable로 선택하면 Object를 이용해서 검사를 한다.

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Article)) return false;
        Article article = (Article) o;
        return Objects.equals(id, article.id);
    }

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

여기서 return Objects.equals의 equals의 상세 코드 내용을 보면

 public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

요렇게 생겼는데,비교대상이 null인 경우에도 대비가 되어있는 모습을 볼 수 있다. null일때(a와 b가 같다) a가 null이 아니면 그때서야 equals를 호출하는 방식으로 npe가 발생하지 않게 만든 것이다.

내 경우는 id를 non-null로 선택해서 생성할 것이다.

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

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

아까 생성한 것과 내용이 살짝 다르다. 그리고

 if (!(o instanceof Article)) return false;
        Article article = (Article) o;
        return id.equals(article.id);

두번째 줄의 내용이 보일러 플레이트처럼 보여지는데, 사실 아랫줄을 제거하고

if (!(o instanceof Article article) ) return false;
return id.equals(article.id);

요렇게 써도 문제 없다.

현재 equals() and hashCode()는 id가 무조건 not null 이라는 조건을 전제로 하고 있지만, id가 null 일때를 대비 해야한다. 영속화를 하지 않았을때, 데이터와 데이터베이스를 연결하지 않았을 때, insert 하기 전의 엔티티는 id를 부여받지 않았기 때문에 null일 수 있다.

따라서 return문을

 return id != null && id.equals(article.id);

요렇게 바꿔주는 것이다! 이 코드는
"새로 만들어진 엔티티, 즉 영속화 되지 않은 엔티티는 동등성 검사에서 탈락시킬거야!"라는 뜻이다. 글의 내용과 날짜같은 필드의 데이터들이 모두 동일할 지언정 id값이 없거나 동일하지 않다? 그러면 탈락시키게 된다.

profile
개발자 꿈나무

0개의 댓글