카테고리 없음

코딩 자율학습 스프링 부트 3 자바 백엔드 개발 입문_12-14장

haenaneah 2025. 7. 6. 17:55

12장 서비스 계층과 트랜잭션

12.1 서비스와 트랜잭션의 개념

 

- 서비스랑 컨트롤러와 리파지터리 사이에 위치하는 게층으로, 서버의 핵심 기능(비지니스 로직)을 처리하는 순서를 총괄

(식당 예시) 웨이터는 컨트롤러, 주방장은 서비스, 보조 요리사는 리파지터리, 재료는 데이터, 창고는 DB

 

트랜잭션이란 모두 성공해야 하는 일련의 과정

> 트랜잭션이 실패로 돌아갈 경우 진행 초기 단계로 돌리는 것을 롤백

 

 

 

12.2 서비스 계층 만들기

 

ArticleService 클래스에서 객체를 만들면 REST 컨트롤러에서 객체 주입하는 방식으로 서비스 객체를 선언할 수 있게 됨.

 

 

12.2.1 게시글 조회 요청 개선하기

12.2.2 게시글 생성 요청 개선하기

 

- return 문에서 결과로 받은 created가 null이 아니면 good 요청을 보내고 그렇지 않으면 bad 요청을 보내도록 함.

> 조건에 따라 good 또는 bad를 실행할 때는 삼항 연산자를 사용

 

 

12.2.3 게시글 수정 요청 개선하기

12.2.4 게시글 삭제 요청 개선하기


12.3 트랜잭션 맛보기

1) 해당 어노테이션이 선언된 메서드를 트랜잭션으로 묶음

2) 클래스에 이 어노테이션을 선언하면 클래스의 모든 메서드별로 각각의 트랜잭션이 부여

3) 트랜잭션으로 묶인 메서드는 처음부터 끝까지 완전히 실행 OR 아예 실행되지 않거나 둘 중 하나로 동작

4) 중간에 실패하면 롤백해 처음 상태로 되돌아가기 때문


13장 테스트 코드 작성하기

13.1 테스트란

 

- 테스트란 프로그램의 품질을 검증하는 것으로, 의도대로 프로그램이 잘 동작하는지 확인하는 과정

> 테스트 도구를 이용해 반복적인 검증 절차를 자동화할 수 있음

 

- 테스트 도구를 활용해 코드를 검증한다는 것은 테스트 코드를 작성해 실행한다는 말

1. 예상 데이터 작성하기

2. 실제 데이터 획득하기

3. 예상 데이터와 실제 데이터 비교해 검증하기

 

- 테스트를 통과하지 못하면 잘못된 부분을 찾아 고치는 디버깅을 해야 함

 

- 테스트 코드는 다양한 경우를 대비해 작성하는데, 이를 테스트 케이스라고 함

 

- 테스트 주도 개발이란 일단 테스트 코드를 만든 후 이를 통과하는 최소한의 코드부터 시작해 점진적으로 코드를 개선 및 확장해 나가는 개발 방식

 

 

 

13.2 테스트 코드 작성하기

13.2.1 테스트 코드 기본 틀 만들기

package com.example.firstproject.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleServiceTest {
    @Autowired
    ArticleService articleService;
    @Test
    void index() {
    }
}

 

 

13.2.2 index() 테스트하기

1. 예상 데이터 작성하기

2. 실제 데이터 획득하기

3. 예상 데이터와 실제 데이터 비교해 검증하기

(중략)
    void index() {
        //1. 예상 데이터
        Article a = new Article(1L, '가가가가', '1111');
        Article b = new Article(2L, '나나나나', '2222');
        Article c = new Article(3L, '다다다다', '3333');
        List<Article> expected = new ArrayList<Article>(Arrays.asList(a, b,c));;
        //2. 실제 데이터
        List<Article> articles = articleService.index();
        //3. 비교 및 검증
        assertEquals(expected.toString(), articles.toString());
    }
}

 

 

 

 

13.2.3 show() 테스트하기

 @Test
    void show_성공_존재하는_id_입력() {
        //1. 예상 데이터
        Long id = 1L;
        Article article;
        Article expected = new Article(id, "가가가가, "1111");
        //2. 실제 데이터
                Article article = articleService.show(id));
        //3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());

    }
    @Test
    void show_실패_존재하지_않는_id_입력() {
    // 1. 예상 데이터 
        Long id = -1L; 
        Article expected = null; 
    // 2. 실제 데이터 
        Article article = articleService.show(id); 
    // 3. 비교 및 검증 
        assertEquals(expected, article); 
    }

 

 

 

13.2.4 create() 테스트하기
 @Test
    void create_성공_title과_content만_있는_dto_입력() {
        // 1. 예상 데이터
        String title = "라라라라";
        String content = "4444";
        ArticleForm dto = new ArticleForm(null, title, content);
        Article expected = new Article(4L, title, content);
        // 2. 실제 데이터
        Article article = articleService.create(dto);
        // 3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());
    }

   void create_실패_id가_포함된_dto_입력()
    // 1. 예상 데이터
    Long id = 4L; 
    String title = "라라라라";  값 임의 배정
    String content = "4444"; 
    ArticleForm dto = new ArticleForm(id, title, content); 
    Article expected = null; 
    // 2. 실제 데이터 
    Article article = articleService.create(dto); 
    // 3. 비교 및 검증 
    assertEquals(expected, article); 
}

 

 

 

13.2.5 여러 테스트 케이스 한 번에 실행하기


14장 댓글 엔티티와 리파지터리 만들기

14.1 댓글 기능의 개요

 

14.1.1 댓글과 게스글의 관계

- 일대다관계 : (하나의 게시글에 수많은 댓글이 달림)

- 다대일관계 : (댓글 입장에서 보면 여러 댓글이 하나의 게시글에 달리므로 many-to-one)

두 테이블은 Id를 기준으로 관계를 맺고 있음.

 

- 두 테이블 모두 자신을 대표하는 id가 있는데, id와 같이 자신을 대표하는 속성을 대표키라고 함

> 대표키는 동일 데이블 내에서 중복된 값이 없어야 함

 

- 외래키 : article_id와 같이 연관 대상을 가리키는 속성

 

 

 

14.1.2 댓글 엔티티와 리파지터리 설계

 

- 엔티티: DB 데이터를 담는 자바 객체로, 엔티티를 기반으로 테이블 생성
- 리파지터리: 엔티티를 관리하는 인터페이스로, 데이터 CRUD 등의 기능 제공

 

Comment 엔티티(댓글), Article 엔티티(게시글)의 관계

 

1. Comment 엔티티를 만들었다면 CommentRepository도 만들어야 함 (3장에서 ArticleRepository를 만들 때는 CrudRepository를 상속받음)

2. CrudRepository 대신 JpaRepository를 상속받음

3. JpaRepository는 ListCrudRepository와 ListPagingAndSortingRepository를 상속받은 인터페이스로,

> CRUD(생성, 조회, 수정, 삭제) 기능 + 엔티티를페이지 단위로 조회 및 정렬하는 기능과 JPA에 특화된 여러 기능 등을 제공

 

- Repository: 최상위 리파지터리 인터페이스
- CrudRepository 및 ListCrudRepository: 엔티티의 CRUD 기능 제공
- PagingAndSortingRepository 및 ListPagingAndSortingRepository: 엔티티의 페이징 및 정렬 기능 제공
- JpaRepository: 엔티티의 CRUD 기능과 페이징 및 정렬 기능뿐만 아니라 JPA에 특화된 기능을 추가로 제공

 


14.2 댓글 엔티티 만들기

14.2.1 댓글 엔티티 만들기

package com.example.firstproject.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity // 해당 클래스가 엔티티임을 선언, 클래스 필드를 바탕으로 DB에 테이블 생성
@Getter // 각 필드 값을 조회할 수 있는 getter 메서드 자동 생성
@ToString // 모든 필드를 출력할 수 있는 toString 메서드 자동 생성
@AllArgsConstructor // 모든 필드를 매개변수로 갖는 생성자 자동 생성
@NoArgsConstructor// 매개변수가 아예 없는 기본 생성자 자동 생성

public class Comment {

    @Id // 대표키 지정
    @GeneratedValue(strategy= GenerationType.IDENTITY) // DB가 자동으로 1씩 증가 private Long id;
    private Long id; // 대표키


    @ManyToOne // Comment 엔티티와 Article 엔티티를 다대일 관계로 설정
    @JoinColumn(name="article_id") // 외래키 생성, Article 엔티티의 기본키(id)와 매핑
    private Article article;


    @Column
    private String nickname;
  @Column
    private String body;







}

 

 

14.2.2 더미 데이터 추가하기

INSERT INTO article(title, content) VALUES('당신의 인생 영화는?', '댓글 고');
INSERT INTO article(title, content) VALUES('당신의 소울 푸드는?', '댓글 고고');
INSERT INTO article(title, content) VALUES('당신의 취미는?', '댓글 고고고');

INSERT INTO comment(article_id, nickname, body) VALUES(4, 'Park', '굿 윌 헌팅');
INSERT INTO comment(article_id, nickname, body) VALUES(4, 'Kim', '아이 엠 샘');
INSERT INTO comment(article_id, nickname, body) VALUES(4, 'Choi', '쇼생크 탈출');

INSERT INTO comment(article_id, nickname, body) VALUES(5, 'Park', '치킨');
INSERT INTO comment(article_id, nickname, body) VALUES(5, 'Kim', '샤브샤브');
INSERT INTO comment(article_id, nickname, body) VALUES(5, 'Choi', '초밥');

INSERT INTO comment(article_id, nickname, body) VALUES(6, 'Park', '조깅');
INSERT INTO comment(article_id, nickname, body) VALUES(6, 'Kim', '유튜브 시청');
INSERT INTO comment(article_id, nickname, body) VALUES(6, 'Choi', '독서');

 

 

14.2.3 댓글 조회 쿼리 연습하기


14.3 댓글 리파지터리 만들기

14.3.1 댓글 리파지터리 만들기

 

- 쿼리를 메서드로 작성하다니 가능? 가능!

> 이러한 멤서드를 네이티브 쿼리 메서드라고 함

 

- 네이티브 쿼리 메서드는 직접 작성한 SQL 쿼리를 리파지터리 메서드로 실행할 수 있게 해 줌

만드는 방법

1. @Query 어노테아션

2. orm.xml 파일을 이용하는 방법

 

package com.example.firstproject.repository;

import com.example.firstproject.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> {
    // 특정 게시글의 모든 댓글 조회
    @Query(value = "SELECT * FROM comment WHERE article_id = :articleId",
            nativeQuery = true) // value 속성에 실행하려는 쿼리 작성
    List<Comment> findByArticleId(Long articleId);

    // 특정 닉네임의 모든 댓글 조회
    List<Comment> findByNickname(String nickname);

}
<?xml version="1.0" encoding="utf-8"?>
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm
https://jakarta.ee/xml/ns/persistence/orm/orm_3_0.xsd"
version="3.0">
<named-native-query
name="Comment.findByNickname"
result-class="com.example.firstproject.entity.Comment">
<query>
<![CDATA[
SELECT * FROM comment WHERE nickname = :nickname
]]>
</query>
</named-native-query>
</entity-mappings>

 

 

 

14.3.2 댓글 리파지터리 테스트 코드 작성하기

package com.example.firstproject.repository;

import com.example.firstproject.entity.Comment;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class CommentRepositoryTest {
    @Autowired
    CommentRepository commentRepository;
    @Test
    @DisplayName("특정 게시글의 모든 댓글 조회")
    void findByArticleId() {
        /* Case 1: 4번 게시글의 모든 댓글 조회 */
        {
            // 1. 입력 데이터 준비
            Long articleId = 4L;
            // 2. 실제 데이터
            List<Comment> comments = commentRepository.findByArticleId(articleId);
            // 3. 예상 데이터
            Article article = new Article(4L, "당신의 인생 영화는?", "댓글 고");
            Comment a = new Comment(1L, article, "Park", "굿 윌 헌팅");
            Comment b = new Comment(2L, article, "Kim", "아이 엠 샘");
            Comment c = new Comment(3L, article, "Choi", "쇼생크 탈출");
            List<Comment> expected = Arrays.asList(a, b, c);
            // 4. 비교 및 검증
            assertEquals(expected.toString(), comments.toString(), "4번 글의 모든 댓글을 출력!");
        }

        /* Case 2: 1번 게시글의 모든 댓글 조회 */
        {
        // 1. 입력 데이터 준비
            Long articleId = 1L;
        // 2. 실제 데이터
            List<Comment> comments = commentRepository.findByArticleId(articleId);
        // 3. 예상 데이터
            Article article = new Article(1L, "가가가가", "1111");
            List<Comment> expected = Arrays.asList();
        // 4. 비교 및 검증
            assertEquals(expected.toString(), comments.toString(),
                    "1번 글은 댓글이 없음");
        }
    }

        @Test
        @DisplayName("특정 닉네임의 모든 댓글 조회")
        void findByNickname() {
            /* Case 1: "Park"의 모든 댓글 조회 */
            {
            // 1. 입력 데이터 준비
                String nickname = "Park";
            // 2. 실제 데이터
                List<Comment> comments = commentRepository.findByNickname(nickname);
            // 3. 예상 데이터
                Comment a = new Comment(1L, new Article(4L, "당신의 인생 영화는?", "댓글 고"),
                        nickname, "굿 윌 헌팅");
                Comment b = new Comment(4L, new Article(5L, "당신의 소울 푸드는?", "댓글 고고"),
                        nickname, "치킨");
                Comment c = new Comment(7L, new Article(6L, "당신의 취미는?", "댓글 고고고"),
                        nickname, "조깅");
                List<Comment> expected = Arrays.asList(a, b, c);
            // 4. 비교 및 검증
                assertEquals(expected.toString(), comments.toString(), "Park의 모든 댓글을 출력!");
            }
        }
    }

https://github.com/HaenaneaH/-ECC_2025-1_BackEnd-StringBoot3_Chap12-14/blob/main/README.md?plain=1