Spring boot

(스프링 부트 입문) 버전 1 - 스프링 부트 익명 블로그 만들어 보기(네이티브 쿼리만 사용) 2. 게시글 목록 보기 만들기

mynote6676 2025. 6. 21. 12:41
package com.tenco.blog.repository;

import com.tenco.blog.model.Board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;


@Repository // IoC 대상
public class BoardNativeRepository {

    // JPA의 핵심 인터페이스
    // 데이터베이스와의 모든 작업을 담당
    private EntityManager em;

    // 생성자를 확인해서 자동으로 EntityManager 객체를 주입 시킨다.
    // DI 처리
    public BoardNativeRepository(EntityManager em) {
        this.em = em;
    }

    // 게시글 목록 조회
    public List<Board> findAll() {
        // 쿼리 기술 --> 네이티브 쿼리
        //  Board.class <-- 형변환을 직접적으로 명시하는 것이 좋다.
        Query query = em.
                createNativeQuery("select * from board_tb order by id desc ", Board.class);
//        List<Board> <---
//        Object[] <---
        // query.getResultList() : 여러 행의 결과를 List 객체로 반환
        // query.getSingleResult(); 단일 결과만 반환 (한 개의 row 데이터만 있을 때)
        // List<Board> list =  query.getResultList();
        return query.getResultList();
    }



    // 트랜잭션 처리
    @Transactional
    public void save(String title, String content, String username) {

        Query query = em.createNativeQuery("insert into board_tb(title, content, username, created_at) " +
                " values(?, ?, ?, now())");

        query.setParameter(1, title);
        query.setParameter(2, content);
        query.setParameter(3, username);

        query.executeUpdate();

    }
}

 

 

JUnit 단위 테스트 개념 및 구현

JUnit 테스트의 개념

JUnit은 Java 기반 애플리케이션의 단위 테스트를 작성하고 실행하기 위한 프레임워크입니다. 단위 테스트는 코드의 개별 단위(메서드, 클래스 등)를 독립적으로 검증하여 예상대로 동작하는지 확인하는 테스트입니다. Spring Boot 프로젝트에서는 JUnit을 사용하여 컨트롤러, 서비스, 리포지토리 등의 동작을 테스트하며, 코드 품질과 안정성을 높입니다.

핵심 특징

  • 어노테이션 기반: @Test, @BeforeEach, @AfterEach 등으로 테스트 설정과 실행을 간단히 관리
  • 단언문(Assertions): assertEquals, assertTrue 등으로 테스트 결과를 검증
  • 독립성: 각 테스트는 독립적으로 실행되어 다른 테스트에 영향을 주지 않음
  • Spring Boot 통합: spring-boot-starter-test 의존성으로 JUnit 5와 Mockito, AssertJ 등을 쉽게 사용

사용하는 이유는?

  • 품질 보증: 코드가 요구사항대로 동작하는지 확인, 버그를 줄임
  • 리팩토링 안전성: 코드 수정 후 기존 기능이 유지되는지 검증, 유지보수 부담 감소
  • 문서화 역할: 테스트 코드가 코드의 사용법과 의도를 보여주는 예시로 작용
  • 버그 조기 발견: 개발 초기에 문제를 찾아 수정, 비용 절감

테스트 종류

  • 단위 테스트: 개별 메서드나 클래스의 동작을 테스트. 외부 의존성(예: DB)은 모킹(Mock) 처리
  • 통합 테스트: 여러 컴포넌트(예: 컨트롤러+서비스+DB)의 상호작용 테스트 등

Given-When-Then 패턴

  • Given: 테스트를 위한 초기 상태 설정(예: 객체 생성, 데이터 준비)
  • When: 테스트 대상 동작 실행(예: 메서드 호출)
  • Then: 결과 검증(예: 예상값과 실제값 비교)

| 단위 테스트 코드 작성해보기

package com.tenco.blog.repository;

import com.tenco.blog.model.Board;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;

import java.util.List;


// @Import - 테스트에 사용할 수 있도록 해당 클래스 로드 한다.
@Import(BoardNativeRepository.class)
@DataJpaTest // JPA 관련 테스트만 로드하는 테스트
public class BoardNativeRepositoryTest {

    @Autowired // DI 처리(의존성 주입)
    private BoardNativeRepository br;

    // DI - 의존성 주입
//    public BoardNativeRepositoryTest(BoardNativeRepository br) {
//        this.br = br;
//    }

    @Test
    public void findAll_test() {
        // given - 테스트를 위한 준비 단계
        // 게시글 목록 조회 정상 작동 하는지 확인 --> data.sql 파일에 데이터 이미 준비

        // when - 실제 테스트를 하는 행위 (전체 게시글 목록 조회)
        List<Board> boardList = br.findAll();
        // then : 결과 검증 (예상하는대로 동작하는지 검증)
        Assertions.assertThat(boardList.size()).isEqualTo(4);
        Assertions.assertThat(boardList.get(3).getUsername()).isEqualTo("ssar");

    }

}

 

방식별 비교

 

| 게시글 목록 보기 뷰 렌더링 (머스태치 코드 수정)

{{> layout/header}}

<div class="container p-5">

   <!--
        {{! boardList}} (머스태치 주석)
        #boardList 리스트가 존재하고 비어있지 않으면 반복 실행
   -->

    {{#boardList}}
    <div class="card mb-3">
        <div class="card-body">
            <!-- {{! title }} 은 현재 Board 객체의 title 멤버 변수에 값을 출력한다
                 머스태치는 자동으로 getTitle() 메서드를 호출 한 것이다.
            -->
            <h4 class="card-title mb-3">{{title}}</h4>
            <a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
        </div>
    </div>
    {{/boardList}}

    <ul class="pagination d-flex justify-content-center">
        <li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
        <li class="page-item"><a class="page-link" href="#">Next</a></li>
    </ul>

</div>

{{> layout/footer}}

 

게시글 목록보기에 시간 출력하기

 

외부 라브러리 추가해 보기
https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.10

 

build.gradle에 의존성 추가

 

implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.10'

 

굳이 스프링 기본 클래스를 이용하지 않는 이유는?

 

java.time 패키지(DateTimeFormatter)는 스레드 안전하지만, java 8 이전 환경에서는 

사용할 수 없음.

 

- Java의 SimpleDateFormat은 스레드 안전하지 않아, 동시 요청이 많은 환경에서 문제가

발생할 수 있음 (예: 포맷팅 오류, 예외).

 

package com.tenco.blog.utils;

import org.apache.commons.lang3.time.DateFormatUtils;

import java.sql.Timestamp;
import java.util.Date;

/**
 * 날짜/시간 관련 유틸리티 클래스
 * static 메서드로 구성해서 객체 생성없이 바로 사용 가능하게 설계
 */
public class MyDateUtil {

    public static String timestampFormat(Timestamp time) {
        // Board 엔티티에 선언된 Timestamp를 Date 객체로 변환
        // getTime() 메서드를 호출해서 밀리초 단위로 시간을 받아 --> Date 객체 생성
        Date currnetDate = new Date(time.getTime());
        // 아파치 Commons 라이브러리 DateFormatUtils 클래스를 활용
        return DateFormatUtils.format(currnetDate, "yyyy-MM-dd HH:mm");
    }

}

 

package com.tenco.blog.model;

import com.tenco.blog.utils.MyDateUtil;
import jakarta.persistence.*;
import lombok.Data;

import java.sql.Timestamp;

@Data
// @Table : 실제 데이터베이스 테이블 명을 지정할 때 사용
@Table(name = "board_tb")
// @Entity : JPA가 이 클래스를 테이터베이스 테이블과 매핑하는 객체(엔티티)로 인식
// 즉, @Entity 어노테이션이 있어야 JPA가 이 객체를 관리 한다.
@Entity
public class Board {

    // @Id 이 필드가 기본키(Primary key) 임을 나타냄
    @Id
    // DENTITY 전략 : 데이터베이스의 기본 전략을 사용한다. -> Auto_Increment
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 별도 어노테이션이 없으면 필드명이 컬럼명이 됨
    private String title;
    private String content;
    private String username;
    private Timestamp createdAt; // created_at (스네이크 케이스로 자동 변환)

    // 머스태치에서 표현할 시간을 포맷기능을(행위) 스르로 만들자
    public String getTime() {
        return MyDateUtil.timestampFormat(createdAt);
    }

}
{{> layout/header}}

<div class="container p-5">

   <!--
        {{! boardList}} (머스태치 주석)
        #boardList 리스트가 존재하고 비어있지 않으면 반복 실행
   -->

    {{#boardList}}
    <div class="card mb-3">
        <div class="card-body">
            <!-- {{! title }} 은 현재 Board 객체의 title 멤버 변수에 값을 출력한다
                 머스태치는 자동으로 getTitle() 메서드를 호출 한 것이다.
            -->
            <h4 class="card-title mb-3">{{title}}</h4>
            <p>{{time}}</p>
            <a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
        </div>
    </div>
    {{/boardList}}

    <ul class="pagination d-flex justify-content-center">
        <li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
        <li class="page-item"><a class="page-link" href="#">Next</a></li>
    </ul>

</div>

{{> layout/footer}}
728x90