스터디 2주차 내용 정리 (04장-06.4장)

박서영·2026년 2월 14일
post-thumbnail

04장. 스프링부트 3와 테스트

4.1 테스트 코드 개념

테스크 코드란?

:작성한 코드가 의도대로 잘 동작하고 예상치 못한 문제가 없는지 확인할 목적으로 작성하는 코드로 test 디렉토리에서 작업하는게 보편적.

테스트 코드에도 다양한 패턴이 존재하며, 해당 책에서는 given-when-then 패턴을 사용함.

  • given-when-then 패턴 : 테스트 코드를 세 단계로 구분해 작성하는 방식
    • given: 테스트 실행을 준비하는 단계

    • when: 테스트를 질행하는 단계

    • then: 테스트 결과를 검증하는 단계

      예:

      @Displayname("새로운 메뉴를 저장한다.")
      @Test
      public void saveMenutest() {
      	//given: 메뉴를 저장하기 위한 준비 과정
      	final String name = "아메리카노";
      	final int price = 2000;
      	final Menu americano = new Menu(name, price);
      	
      	//when: 실제로 메뉴를 저장
      	final long saveId = menuService.save(americano);
      	
      	//then: 메뉴가 잘 추가되었는지 검증
      	final Menu savedMenu = menuService.findById(saveId).get();
      	assertThat(savedMenu.getName()).isEqualTo(name);
      	assertThat(savedMenu.getPrice()).isEqualTo(price);
      }

4.2 스프링부트3와 테스트

  • 스프링부트는 애플리케이션을 테스트하기 위한 도구와 애너테이션을 제공.
    • spring-boot-starter-test 스타터에는 테스트를 위한 도구가 모여있음.

      ✅ 스프링부트 스타터 테스트 목록

    • JUnit: 자바 프로그래밍 언어용 단위 테스트 프레임워크

    • Spring Test & Spring Boot Test: 스프링부트 애플리케이션을 위한 통합 테스트 지원

    • AssertJ: 검증문인 assertion을 작성하는데 사용되는 라이브러리

    • Hamcrest: 표현식을 이해하기 쉽게 만드는ㄷ에 사용되는 Matcher 라이브러리

    • Mockito: 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리, 검증할 수 있게 지원하는 테스트 프레임워크

    • JSONassert: JSON용 assertion 라이브러리

    • JsonPath: JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리

⇒ 위 중 JUnit과 AssertJ를 가장 많이 사용함.

JUnit이란?

: 자바 언어를 위한 단위 테스트 프레임워크. JUnit을 사용하면 단위 테스트를 작성하고 테스트하는데 도움이 됨.

  • 단위테스트: 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것을 의미. → 주로 메서드 단위로 진행됨.
  • 특징
    • 테스트 방식을 구분할 수 있는 애너테이션을 제공
    • @Test 애너테이션으로 메소드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능
    • 예상 결과를 검증하는 assertion 메소드 제공
    • 사용 방법이 단순하고 테스트 코드 작성 시간이 적음
    • 자동 실행, 자체 결과를 확인하고 즉각적인 피드백을 제공
// 'test/JUnitTest.java' 파일

import org.junit.jupiter.api.*;

public class JUnitTest {
    @DisplayName("1+2는 3이다") //테스트 이름
    @Test //테스트 메소드
    public void junitTest(){
        int a = 1;
        int b = 2;
        int sum = 3;
        
        Assertions.assertEquals(sum, a+b); //값이 같은지 확인
    }
}
  • @DisplayName 애너테이션: 테스트 이름을 명시
  • @Test 애너테이션: 테스트를 수행하는 메소드

⇒ JUnit은 테스트끼리 영향을 주지 않도록 각 테스트를 실행할 때마다 테스트를 위한 객체를 만들고 테스트가 종료되면 실행 객체를 삭제함.

  • junitTest() 메소드: JUnit에서 제공하는 검증 메소드인 assertEquals() 로 a+b와 sum의 값이 같은지 확인함.
  • assertEquals(): 첫 번째 인수- 기대하는 값, 두 번째 인수- 실제 검증할 값

실행화면 (테스트 성공):

실행화면 (테스트 실패):

⇒ 테스트 실패 시: 테스트가 실패했다는 표시와 함께 기댓값과 실제 받은 값을 비교하여 알려줌

⇒ JUnit은 테스트 케이스가 하나라도 실패하면 전체 테스트를 실패한 것으로 보여줌.

// test/JUnitCycleTest.java 파일

import org.junit.jupiter.api.*;

public class JUnitCycleTest {
    @BeforeAll //전체 테스트를 시작하기 전에 1회 실행하므로 메소드는 static으로 선언
    static void beforeAll() {
        System.out.println("@BeforeAll");
    }
    
    @BeforeEach //테스트 케이스를 시작하기 전마다 실행
    public void beforeEach() {
        System.out.println("@BeforeEach");
    }
    
    @Test
    public void test1() {
        System.out.println("test1");
    }

    @Test
    public void test2() {
        System.out.println("test2");
    }

    @Test
    public void test3() {
        System.out.println("test3");
    }
    
    @AfterAll //전체 테스트를 마치고 종료하기 전에 1회 실행하므로 메소드는 static으로 선언
    static void afterAll() {
        System.out.println("@AfterAll");
    }
    
    @AfterEach //테스트 케이스를 종료하기 전마다 실행
    public void afterEach() {
        System.out.println("@AfterEach");
    }
}
  • @BeforeAll 애너테이션 : 전체 테스트를 시작하기 전 처음 한 번만 실행. 전체 테스트 실행 주기에서 한 번만 호출되어야 하기 때문에 메소드를 static으로 선언해야함. 예) 데이터베이스를 연결해야하거나 테스트 환경을 초기화할 때 사용됨.
  • @BeforeEach 애너테이션 : 테스트 케이스를 시작하기 전에 매번 실행. 각 인스턴스에 대해 메소드를 호출해야하므로 메소드는 static이 아니어야함. 예) 테스트 메소드에서 사용하는 객체 초기화, 테스트에 필요한 값을 미리 넣을 때 사용
  • AfterAll 애너테이션 :전체 테스트를 마치고 종료하기 전에 한 번만 실행. 전체 테스트 실행 주기에서 한 번만 호출되어야하므로 메소드를 static으로 선언해야함. 예) 데이터베이스 연결 종료, 공통적으로 사용하는 자원의 해제
  • AfterEach 애너테이션 :각 테스트 케이스를 종료하기 전 매번 실행. 메소드는 static이 아니어야함. 예) 테스트 이후 특정 데이터를 삭제해야하는 경우

AssertJ로 검증문 가독성 높이기

  • AssertJ: JUnit과 함께 사용해 검증문의 가독성을 높여주는 라이브러리.
    //Assertion문 비교
    
    Assertions.assertEquals(sum, a+b);
    
    //AssertJ 사용했을 때의 예
    assertThat(a+b).isEqualTo(sum);

⇒ AssertJ에는 값이 같은지를 비교하는 isEqualTo(), isNotEqualTo() 외에도 다양한 메소드를 제공함.

메소드명설명
isEqualTo(A)A값과 같은지 검증
isNotEqualTo(A)A값과 다른지 검증
contains(A)A값을 포함하는지 검증
doesNotContain(A)A값을 포함하지 않는지 검증
startsWith(A)접두사가 A인지 검증
endsWith(A)접미사가 A인지 검증
isEmpty()비어 있는 값인지 검증
isNotEmpty()비어 있지 않은 값인지 검증
isPositive()양수인지 검증
isNegative()음수인지 검증
isGreaterThan(1)1보다 큰 값인지 검증
isLessThan(1)1보다 작은 값인지 검증

❓테스트코드 작성 연습문제1:

❓테스트코드 작성 연습문제2:

4.3 제대로 테스트 코드 작성해보기

Step1: TestController의 테스트 파일 생성하기

Step2: 테스트 코드 작성하기

// test/TestControllerTest.java 파일

package me.summeryoung.springbootdeveloper;
...

@SpringBootTest //테스트용 애플리케이션 컨텍스트 생성
@AutoConfigureMockMvc //MockMvc 생성 및 자동 구성
class TestControllerTest {
   @Autowired
    protected MockMvc mockMvc;

   @Autowired
    private WebApplicationContext context;

   @Autowired
    private MemberRepository memberRepository;

   @BeforeEach //테스트 실행 전 실행하는 메소드
    public void mockMvcSetUp() {
       this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
               .build();
   }

   @AfterEach //테스트 실행 후 실행하는 메소드
    public void cleanUp() {
       memberRepository.deleteAll();
   }
}
  • @SpringBootTest : 메인 애플리케이션 클래스에 추가하는 애너테이션인 @SpringBootApplication이 있는 클래스를 찾고 그 클래스에 포함되어 있는 빈을 찾은 다음 테스트용 애플리케이션 컨텍스트라는 것을 만듬
  • @AutoConfigureMockMvc : MockMvc를 생성하고 자동으로 구성하는 애너테이션. 컨트롤러를 테스트할 때 사용되는 클래스.
    • MockMvc: 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스.
// test/TestControllerTest.java 파일

package me.summeryoung.springbootdeveloper;
...

@DisplayName("getAllMembers: 아티클 조회에 성공한다")
   @Test
    public void getAllMembers() throws Exception {
       //given
       final String url = "/test";
       Member savedMember = memberRepository.save(new Member(1L, "홍길동"));
       
       //when
       final ResultActions result = mockMvc.perform(get(url)
               .accept(MediaType.APPLICATION_JSON));
       
       //then
       result
               .andExpect(status().isOk())
               .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
               .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
   }
Given멤버를 저장
When멤버 리스트를 조회하는 API 호출
Then응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인함
  • perform(): 요청을 전송하는 역할. 결과로 ResultActions 객체를 받으며, ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메소드를 제공함.
  • accept(): 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메소드. JSON, XML 등 다양한 타입 존재.
  • andExpect(): 응답을 검증. TestController에서 만든 API는 응답으로 OK(200)을 반환하므로 이에 해당하는 메소드인 isOk를 이용해 응답코드가 OK(200)인지 확인.
  • jsonPath("$[0].${필드명}"): JSON 응답값의 값을 가져오는 역할. 0번째 배열에 들어있는 객체의 id, name값을 가져와 저장된 값과 같은지 확인.

테스트 코드 패턴 연습하기

//QuizController.java 파일

package me.summeryoung.springbootdeveloper;
...

@RestController
public class QuizController {
    
    @GetMapping("/quiz")
    public ResponseEntity<String> quiz(@RequestParam("code") int code) {
        switch(code) {
            case 1:
                return ResponseEntity.created(null).body("Created!");
            case 2:
                return ResponseEntity.badRequest().body("Bad Request!");
            default:
                return ResponseEntity.ok().body("OK!");
        }
    }
    
    @PostMapping("/quiz")
    public ResponseEntity<String> quiz2(@RequestBody Code code) {
        
        switch(code.value()){
            case 1:
                return ResponseEntity.status(403).body("Forbidden");
            default:
                return ResponseEntity.ok().body("OK!");
        }
    }
}

record Code (int value) {}
  • quiz 패스로 GET 요청이 들어오면 quiz() 메소드에서 요청을 처리.
  • quiz 패스로 POST 요청이 들어오면 quiz2() 메소드에서 요청을 처리
  • quiz() 메소드: 요청 파라미터의 키가 ‘code’이면 int 자료형의 code 변수와 매핑되며, code 값에 따라 다른 응답을 보냄
    code 값응답 코드응답 본문
    1201Created!
    2400Bad Request!
    그 외200OK!
  • quiz2() 메소드: 요청값을 Code라는 객체로 매핑한 후, value 값에 따라 다른 응답을 보냄.
    code 값응답 코드응답 본문
    1403Forbidden!
    그외200OK!
  • record Code (int value) {}: 매핑할 객체로 사용하기 위해 선언한 레코드.
    • 레코드: 데이터 전달을 목적으로 하는 객체를 더 빠르고 간편하게 만들기 위한 기능으로, 필드, 생성자, Getter, equals(), hashCode(), toString() 메소드를 자동으로 생성함
// test/QuizControllerTest.java 파일

@SpringBootTest
@AutoConfigureMockMvc
class QuizControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    public void mockMvcSetup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
    }

}
  • ObjectMapper: Jackson 라이브러리에서 제공하는 클래스로 객체와 JSON 간의 변환을 처리해줌 예:
    Code code = new Code(13)
    objectMapper.writeValueAsString(code)

❓테스트코드 작성 연습문제1:

@DisplayName("quiz(): GET /quiz?code=1 이면 응답코드는 201, 응답 본문은 Created!를 리턴한다.")
    @Test
    public void getQuiz1() throws Exception {
        //given
        final String url = "/quiz";

        //when
        final ResultActions result = mockMvc.perform(get(url)
                .param("code", "1")
        );

        //then
        result
                .andExpect(status().isCreated())
                .andExpect(content().string("Created!"));
    }
    
@DisplayName("quiz2(): GET /quiz?code=2 이면 응답코드는 400, 응답 본문은 Bad Request!를 리턴한다.")
    @Test
    public void getQuiz2() throws Exception {
        //given
        final String url = "/quiz";

        //when
        final ResultActions result = mockMvc.perform(get(url)
                .param("code", "2")
        );

        //then
        result
                .andExpect(status().isBadRequest())
                .andExpect(content().string("Bad Request!"));
    }
  • param() 메소드: 쿼리 파라미터 추가하는 방법.

❓테스트코드 작성 연습문제2:

@DisplayName("quiz(): POST /quiz?code=1이면 응답 코드는 403, 응답 본문은 Forbidden!를 리턴한다.")
    @Test
    public void postQuiz1() throws Exception {
        //given
        final String url = "/quiz";

        //when
        final ResultActions result = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(new Code(1)))
        );

        //then
        result
                .andExpect(status().isForbidden())
                .andExpect(content().string("Forbidden!"));
    }

    @DisplayName("quiz(): POST /quiz?code=13이면 응답 코드는 200, 응답 본문은 OK!를 리턴한다")
    @Test
    public void postQuiz13() throws Exception {
        //given
        final String url = "/quiz";

        //when
        final ResultActions result = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(new Code(13)))
        );

        //then
        result
                .andExpect(status().isOk())
                .andExpect(content().string("OK!"));
    }


05장. 데이터베이스 조작이 편해지는 ORM

5.1 데이터베이스란

데이터베이스: 데이터를 매우 효율적으로 보관하고 꺼내볼 수 있는 곳. 많은 사람이 안전하게 데이터를 이용하고 관리할 수 있다는 장점이 존재

데이터베이스 관리자: DBMS

  • DBMS: 데이터베이스를 관리하기 위한 소프트웨어.
    • 데이터베이스 ⇒ 많은 사람이 공유하기에 동시 접근이 가능해야함. 이외에도 많은 요구사항이 존재 → 이런 요구사항을 만족하면서도 효율적으로 데이터베이스를 관리 운영할 수 있는 것이 DBMS
    • 즉, MySQL, 오라클은 DBMS.
    • 관리 특징에 따라 “관계형, 객체-관계형, 도큐먼트형, 비관계형”으로 분류.
    • 관계형 DBMS를 가장 많이 사용함
  1. 관계형 DBMS
    • relational DBMS를 줄여서 RDBMS라고 부름.
    • 관계형 모델을 기반으로함.
    • 테이블 형태로 이루어진 저장소를 생각하면 됨.
    • 예) 회원 테이블 ⇒ 각 행은 고유의 키(아이디)를 가지고 있고, 이메일, 나이와 같은 회원 관련 값이 들어감
      회원 테이블
      ID이메일나이
      1a@test.com10
      2b@test.com20
      3c@test.com30
      ⇒ 데이터 1, a@test.com, 10을 묶어서 한 줄을 행이라하고, ID, 이메일, 나이와 같은 구분을 열이라고 부름.
  1. H2, MySQL
    • H2
      • 자바로 작성되어 있는 RDBMS
      • 스프링부트가 지원하는 인메모리 관계형 데이터베이스.
      • 데이터를 다른 공간에 보관하는 것이 아니라 애플리케이션 자체 내부에 데이터를 저장
      • 즉, 애플리케이션을 다시 실행하면 데이터가 초기화됨
      • 간편하기에 개발 시 테스트 용도로 많이 사용하고, 실제 서비스에서는 사용하지 않음
      • 실제 서비스에서는 MySQL을 주로 사용함

💡 데이터베이스 용어 정리

  • 테이블:
    • 데이터베이스에서 데이터를 구성하기 위한 가장 기본적인 단위.
    • 행과 열로 구성되며 행은 여러 속성으로 구성됨
  • 행(=레코드):
    • 테이블의 구성 요소 중 하나로, 테이블의 가로로 배열된 데이터 집합을 말함.
    • 행은 반드시 고유한 식별자인 기본키를 가짐.
  • 열:
    • 테이블의 구성 요소 중 하나로, 행에 저장되는 유형의 데이터.
    • 열은 각 요소에 대한 속성을 나타내며 무결성을 보장함.
  • 기본키(primary key):
    • 행을 구분할 수 있는 식별자.
    • 해당 값은 테이블에서 유일해야하며 중복값을 가질 수 없음.
    • 보통 데이터를 수정/삭제/조회할 때에 사용
    • 다른 테이블과 관계를 맺어 데이터를 가져올 수 있음.
    • 기본키의 값은 수정되어서는 안되면 유효한 값이 있어야함(=즉, NULL일 수 없음)
  • 쿼리(query)
    • 데이터베이스에서 데이터를 조회하거나 삭제/생성/수정 같은 처리를 하기 위해 사용하는 명령문
    • SQL이라는 데이터베이스 전용 언어를 사용해 작성함

SQL문으로 데이터베이스 조작하는 연습하기

  1. 데이터 조회하기: SELECT

    테이블에 저장한 데이터를 조회할 때는 SELECT문을 사용.

    SELECT <무엇을>
    FROM <어디에서>
    WHERE <무슨>

    예) 손님 정보를 저장하는 customers 라는 테이블.

    idnamephone_numberage
    1김일번010-1111-111115
    2ㅇㅣ이번010-2222-222220
    3정삼번010-3333-333335

    ✔️ 기본구조

    SELECT <이름>
    FROM <customers 테이블>
    WHERE <id가 2>

    SELECT name
    FROM customers
    WHERE id=2
    • 이때 사용하는 name, id은 테이블에서 정의한 컬럼(열) 이름과 맞춰서 써야함.
    • 조건이 없으면 WHERE 절은 생략 가능.
    • 모든 열(컬럼)을 가져오려면 *을 사용하면 됨. 예)
      SELECT *
      FROM customers

❓SQL문 연습문제1

SELECT *
FROM customers
WHERE id=1

❓SQL문 연습문제2

SELECT id, name
FROM customers
WHERE phone_numbers = '010-2222-2222'
  1. 조건 넣어보기: WHERE
명령어설명예시
=특정 값과 동일한 값을 가진 행 조회age = 10
!= 또는 <>특정 값과 동일하지 않은 행 조회age != 10
<, >, <=, >=특정 값과 대소 비교하여 조회age <10
age ≤10
BETWEEN지정된 값의 사이 값을 조회age BETWEEN 10 AND 20
LIKE패턴 매칭을 위해 사용,
%를 사용하면 와일드 카드로 사용할 수 있음name LIKE '김%'
AND두 조건 모두 참이면 조회name LIKE '김%' AND '이%'
OR두 조건 중 하나라도 참이면 조회name LIKE '김%' OR '이%'

| IS NULL,
IS NOT NULL | NULL 값의 존재 여부 검사 | name IS NULL |

❓SQL문 연습문제1

WHERE age >= 20

❓SQL문 연습문제2

WHERE age BETWEEN 10 AND 30

❓SQL문 연습문제3

WHERE name LIKE '김%' OR '이%'
  1. 데이터 추가하기: INSERT
  • 데이터베이스의 테이블에 새로운 행을 추가하고 싶을 때는 INSERT문, 삭제하고 싶을 때는 DELETE문을 사용함.
  • INSERT문은 INSERT INTOVALUES 키워드를 사용함

INSERT INTO <어디에>
VALUES <어떤 값을>

예)

idnamephone_numberage
1김일번010-1111-111115
2ㅇㅣ이번010-2222-222220
3정삼번010-3333-333335
INSERT INTO customers (name, phone_number, age)
VALUES ('박사번', '010-4444-4444', 40);
  • 네 번째 행. id가 4인 행에 새 레코드(=행)이 생김.
  • 아이디는 넣지 않아도, 자동으로 추가. AUTO_INCREMENT 속성을 추가함.
  • AUTO_INCREMENT 속성: 해당 열(컬럼)의 값이 추가될 때(=즉, 새 레코드를 생성할 때) 1씩 증가하는 자동값을 만들어주어, 레코드의 고유값으로 관리할 수 있게 해줌.
  1. 데이터 삭제하기: DELETE

DELETE FROM <어디에서> WHERE <어떤 조건으로>;

예) id가 5인 레코드(=행)을 삭제할 때

DELETE FROM customers WHERE id=5;
  1. 데이터 수정하기: UPDATE

특정 레코드의 값을 바꾸고 싶을 때 사용

UPDATE <어디에>
SET <무슨 컬럼을 = 어떤 값으로>
WHERE <어떤 조건으로>

idnamephone_numberage
1김일번010-1111-111115
2ㅇㅣ이번010-2222-222220
3정삼번010-3333-333335

예) name=김일번인 레코드의 age를 11로 바꾸려할 때

UPDATE customers
SET age = 11
WHERE name = '김일번';

❓SQL문 연습문제1

UPDATE customers
SET name = '김일'
WHERE id = 1;

❓SQL문 연습문제2

UPDATE customers
SET phone_number = ''

5.2 ORM이란?

  • ORM(Object-Relational Mapping): 자바의 객체와 데이터베이스를 연결하는 프로그래밍 기법. 예) DB에 name 열에 20, 홍길동이라는 값이 있을 때 ⇒ 이를 자바에서 사용하려면? SQL문을 사용해 데이터를 꺼내야하는데, ORM을 사용하면 데이터베이스의 값을 마치 객체처럼 사용할 수 있음!

✅ ORM의 장단점

#장점

  • SQL을 직접 작성하지 않고 사용하는 언어로 데이터베이스에 접근할 수 있음
  • 객체지향적으로 코드를 작성할 수 있기에 비즈니스 로직에만 집중 가능
  • 데이터베이스 시스템이 추상화되어 있기에 MySQL에서 PostgreSQL로 전환한다해도 추가로 드는 작업이 거의 없음. 데이터베이스 시스템에 대한 종속성이 줄어듬
  • 매핑하는 정보가 명확하기 때문에 ERD에 대한 의존도를 낮출 수 있고 유지보수할 때 유리

#단점

  • 프로젝트의 복잡성이 커질수록 사용 난이도가 올라감
  • 복잡하고 무거운 쿼리는 ORM으로 해결이 불가능할 때가 존재

5.3 JPA와 하이버네이트

  • DBMS에 여러 종류가 있듯 ORM에도 여러 종류가 존재.

  • JPA

    • 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스.
    • 실제 사용을 위해서는 ORM 프레임워크를 추가로 선택해야함.
    • 대표적으로는 ORM 프레임워크로 하이버네이트를 많이 사용함
  • 하이버네이트

    • JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크
    • 내부적으로 JDBC API를 사용
    • 목표: 자바 객체를 통해 데이터베이스 종류에 상관없이 데이터베이스를 자유자재로 사용할 수 있게하는 것.

✅ JPA와 하이버네이트의 역할

  • JPA: 자바 객체와 데이터베이스를 연결해 데이터를 관리. 객체 지향 도메인 모델과 데이터베이스의 다리 역할을 함.
  • 하이버네이트: JPA의 인터페이스를 구현함. 내부적으로는 JDBC API를 사용함

엔티티 매니저란?

  • 엔티티

    • 정의: 데이터베이스의 테이블과 매핑되는 객체를 의미
    • 본직적으로는 자바 객체이므로 일반 객체와 다르지 않지만, 데이터베이스의 테이블과 직접 연결되기에 구분지어 부름
    • 객체이지만, 데이터베이스에 영향을 미치는 쿼리를 실행하는 객체
  • 엔티티 매니저

    • 엔티티 매니저: 엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성/수정/삭제하는 등의 역할

    • 엔티티 매니저 팩토리: 엔티티 매니저를 만드는 곳

    • 예) 회원 2명이 동시에 회원가입을 하려는 경우 엔티티 매니저의 동작

      • 회원1의 요청에 대해 가입처리를 할 엔티티 매니저를 엔티티 매니저 팩토리가 생성 → 가입 ㅓ리 후 데이터베이스에 회원 정보 처리
      • 회원2 역시 같은 방식으로 처리 → 회원1,2를 위해 생성된 엔티티 매니저는 필요 시점에 데이터베이스와 연결한 뒤에 쿼리
    • 스프링부트에서는 하나의 엔티티 매니저 팩토리만 생성해 관리하고 @Persistence Context 또는 @Autowired 애너테이션을 사용해 엔티티 매니저를 사용함

      @PersistentContext
      EntityManager em; //프록시 엔티티 매니저. 필요할 때 진짜 매니저 호출
    • 스프링부트는 기본적으로 빈을 하나만 생성해 공유하기 때문에 동시성 문제가 발생 가능.

      → 따라서 엔티티 매니저가 아닌 실제 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용하고 필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 호출.

      ⇒ 엔티티 매니저는 Spring Data JPA에서 관리하기에 직접 생성하거나 관리할 필요X

영속성 컨텍스트란?

  • 엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장한다는 특징 존재.

  • 영속성 컨텍스트: JPA의 중요한 특징으로 엔티티를 관리하는 가상의 공간. 이를 통해 데이터베이스에서 효과적으로 데이터를 가져올 수 있고, 엔티티를 편하게 사용할 수 있음.

    • 특징: 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩
  • 1차 캐시

    • 영속성 컨텍스트는 내부에 1차 캐시를 가지고 있음
    • 캐시의 키는 엔티티의 @Id 애너테이션이 달린 기본키 역할을 하는 식별자이며 값은 엔티티
    • 엔티티를 조회하면 1차 캐시에서 데이터를 조회하고 값이 있으면 반환, 값이 없으면 데이터베이스에서 조회해 1차 캐시에 저장한 뒤 반환.
    • 캐시된 데이터를 조회할 때는 데이터베이스를 거치치않아도 되기에 빠르게 데이터 조회 가능
  • 쓰기 지연

    • 트랜잭션을 커밋하기 전까지 데이터베이스에 실제로 질의문을 보내지 않고 쿼리를 모았다가 트랜잭션을 커밋하면 모았던 쿼리를 한 번에 실행하는 것을 의미
    • 예) 데이터 추가 쿼리가 3개라면 영속성 컨텍스트는 트랜잭션을 커밋하는 시점에 3개의 쿼리를 한 꺼번에 전송 ⇒ 적당한 묶음으로 쿼리를 요청하기에 데이터 시스템의 부담을 줄일 수 있음
  • 변경 감지

    • 트랜잭션을 커밋하면 1차 캐시에 저장되어 있는 엔티티의 값과 현재 엔티티의 값을 비교해 변경된 값이 있다면 변경 사항을 감지해 변경된 값을 데이터베이스에 자동으로 반영
    • 쓰기 지연과 마찬가지로 적당한 묶음으로 쿼리 요청 가능하며 데이터베이스 시스템의 부담을 줄일 수 있음
  • 지연 로딩

    • 쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라 필요할 때 쿼리를 날려 데이터를 조회하는 것을 의미

⇒ 데이터베이스의 접근을 최소화해 성능을 높일 수 있음.

엔티티의 상태

  • 4가지 상태 존재
    • 분리 상태: 영속성 컨텍스트가 관리하고 있지 않음
    • 관리 상태: 영속성 컨텍스트가 관리하는 부분
    • 비영속 상태: 영속성 컨텍스트와 전혀 관계 없는 부분
    • 삭제된 상태

⇒ 이런 상태들은 특정 메소드를 호출해 변경할 수 있음 + 필요에 따라 엔티티의 상태를 조절해 데이터를 올바르게 유지 관리 가능

public class EntityManagerTest {
	
	@Autowired
	EntityManager em;
	
	public void example() {
	
		//1) 엔티티 매니저가 엔티티를 관리하지 않는 상태(비영속 상태)
		Member member = new Member(1L, "홍길동");
		
		//2) 엔티티가 관리되는 상태
		em.persist(member);
		
		//3) 엔티티 객체가 분리된 상태
		em.detach(member);
		
		//4) 엔티티 객체가 삭제된 상태
		em.remove(member);
	}
}
  • 엔티티를 처음 만들면 비영속 상태
  • persist() 메소드: 엔티티를 관리 상태로 만들 수 있음
  • detach() 메소드: 분리 상태
  • remove() 메소드: 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제 가능

5.4 스프링 데이터와 스프링 데이터 JPA

  • 스프링 데이터
    • 비즈니스 로직에 더 집중할 수 있게 데이터베이스 사용 기능을 클래스 레벨에서 추상화
    • 스프링 데이터에서 제공하는 인터페이스를 통해 스프링 데이터를 사용할 수 있음
    • 인터페이스에서는 CRUD를 포함한 여러 메소드가 포함되어 있으며, 쿼리를 알아서 만들어줌
    • 페이징 처리 기능과 메소드 이름으로 자동으로 쿼리를 빌딩하는 기능이 제공되는 등의 장점 존재
    • 데이터베이스의 특성에 맞춰 기능을 확장해 제공하는 기술 제공

스프링 데이터 JPA

  • 스프링 데이터 JPA
    • 스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술

    • 스프링 데이터의 인터페이스인 PagingAndSortingRepository를 상속받아 JpaRepository 인터페이스를 만들어 JPA를 더 편리하게 사용하는 메소드 제공

    • 리포지토리 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있음

    • JpaRepository 인터페이스를 만든 인터페이스에서 상속받고, 제너릭에서 관리할 <엔티티 이름, 엔티티 기본키의 타입>을 입력하면 기본 CRUD 메소드를 사용할 수 있음

      public interface MemberRepository extends JpaRepository<Member, Long> {
      }

스프링 데이터 JPA에서 제공하는 메소드 사용해보기

  1. 조회 메소드 사용해보기

SQL 쿼리에서 조회를 위해서는 SELECT를 사용했지만, JPA에서 데이터를 가져올 때에는 쿼리를 작성하는 대신 findAll() 메소드를 사용

SELECT * FROM member

  1. 디버그 툴로 값 보기

SELECT * FROM member WHERE id = 2;

위의 쿼리문을 JPA로 짜는 방법

...
@Sql("/insert-members.sql")
@Test
void getMemberById() {
	//when
	Member member = memberRepository.findById(2L).get();

	//then
	assertThat(member.getName()).isEqualTo("B");
}

  1. 쿼리 메소드 사용해보기

⇒ id가 아니라 name으로 찾고 싶을 때

id는 모든 테이블에서 기본키로 사용하기에 값이 없을 수 없음. 하지만 name은 값이 있거나 없을 수 있으므로 JPA에서 기본으로 name을 찾아주는 메소드를 지원하지 않음. → 하지만 JPA는 메소드 이름으로 쿼리를 작성하는 기능 제공

SELECT * FROM member WHERE name = 'C';

⇒ name의 값이 ‘C’인 멤버를 찾아야하는 경우 필요한 SQL문

목표: 위의 쿼리를 동적 메소드로 만들기!

// MemberRepository.java 파일

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
	Optional<Member> findByName(String name);
}

⇒ 메소드 상속받아 구현하는 작업 없이 그저 정의한 후에 가져다 쓰면됨!

✅ @Query 메소드로 SQL 쿼리문 실행해보기

JPA를 이용해 표현하기 너무 복잡한 쿼리/성능이 너무 중요해서 SQL 쿼리문을 직접 사용해ㅑㅇ하는 경우에는 @Query 메소드를 쓰기도 함.

@Query("select m from Member m where m.name = ?1")
Optional<Member> findByName(String name);
  1. 추가, 삭제 메소드 사용해보기
  • INSERT를 사용하기 위해서는 save() 라는 메소드 사용
    INSERT INTO member (id, name)
    VALUES (1, 'A');

⇒ 위의 쿼리문 JPA로 만들기

// MemberRepositoryTest.java 파일

...
@Test
    void saveMember() {
        //given
        Member member = new Member(1L, "A");

        //when
        memberRepository.save(member);
        
        //then
        assertThat(memberRepository.findById(1L).get().getName()).isEqualTo("A");
    }
  • 여러 엔티티를 한 꺼번에 저장하려면 saveAll() 메소드 활용
// MemberRepositoryTest.java 파일

...
@Test
    void saveMembers() {
        //given
        List<Member> members = List.of(new Member(2L, "B"),
                new Member(3L, "C"));

        //when
        memberRepository.saveAll(members);

        //then
        assertThat(memberRepository.findAll().size()).isEqualTo(2);
    }

  • 멤버 삭제하기: DELETE문 사용을 위해서는 deleteById() 메소드 활용
DELETE FROM member WHERE id = 2;

⇒ 위의 쿼리문 JPA로 만들기

// MemberRepositoryTest.java 파일

...
@Sql("/insert-members.sql")
    @Test
    void deleteMemberById() {
        //when
        memberRepository.deleteById(2L);

        //then
        assertThat(memberRepository.findById(2L).isEmpty()).isTrue();
    }

✅ 디버깅 툴 사용해 구문 실행 해보기

  • 모든 데이터를 삭제하기: deleteAll() 메소드 사용하기

    DELETE FROM member
    // MemberRepositoryTest.java 파일
    
    ...
    @Sql("/insert-members.sql")
        @Test
        void deleteAll() {
            //when
            memberRepository.deleteAll();
    
            //then
            assertThat(memberRepository.findAll().size()).isZero();
        }

    deleteAll() 메소드는 모든 데이터를 삭제하기에 실제 서비스 코드에서는 거의사용하지 않고, 테스트 간의 격리를 보장하기 위해 사용됨.

  • 따라서 보통 @AfterEach 애너테이션을 붙여 cleanUp() 메소드와 같은 형태로 사용함.

  1. 수정 메소드 사용해보기
UPDATE member
SET name = 'BC'
WHERE id = 2;

⇒ 위의 쿼리문을 JPA로 사용할 때: JPA에서 데이터를 수정할 때는 트랜잭션 내에서 데이터를 수정해야하기에, 그냥 메소드만 사용하는 것이 아니라 @Transactional 애너테이션을 메소드에 추가해야함

//Member.java 파일

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;
    
    public void changeName(String name) {
        this.name = name;
    }
}
  • changeName() 메소드가 @Transactional 애너테이션이 포함된 메소드에서 호출되면 JPA는 변경 감지 기능을 통해 엔티티의 필드값이 변경될 때 그 변경 사항을 데이터베이스에 자동으로 반영함
  • 만약 엔티티가 영속 상태일 때 필드값을 변경하고 트랜잭션이 커밋되면 JPA는 변경사항을 데이터베이스에 자동으로 적용
@Sql("insert-members.sql")
    @Test
    void update() {
        //given
        Member member = memberRepository.findById(2L).get();
        
        //when
        member.changeName("BC");
        
        //then
        assertThat(memberRepository.findById(2L).get().getName()).isEqualTo("BC");
    }
  • @Transactional 애너테이션을 붙이지 않았는데도 실행되는 이유? ⇒ @DataJpatest 애너테이션을 사용했기 때문
  • @DataJpatest 애너테이션: 테스트를 위한 설정을 제공. 자동으로 데이터베이스에 대한 트랜잭션 관리를 설정함.

5.5 예제 코드 살펴보기

//Member.java 파일

@NoArgsConstructor(access = AccessLevel.PROTECTED) //기본 생성자
@AllArgsConstructor
@Getter 
@Entity //엔티티로 지정
public class Member {
    @Id //id 필드를 기본키로 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) //기본키를 자동으로 1씩 증가
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "name", nullable = false) //name이라는 not null 열(컬럼)과 매핑
    private String name;

    public void changeName(String name) {
        this.name = name;
    }
}
  • @Entity 애너테이션: Member 객체를 JPA가 관리하는 엔티티로 지정함. 즉, Member 클래스와 실제 데이터베이스의 테이블을 매핑시킴.
    • @Entity 의 속성 중에 name을 사용하면 name의 값을 가진 테이블 이름과 매핑되고, 테이블 이름을 지정하지 않으면 클래스 이름과 같은 이름의 테이블과 매핑됨

    • 위의 코드에서는 테이블 이름을 지정하지 않았으므로, 클래스 이름과 같은 데이터베이스의 테이블인 memeber 테이블과 매핑됨.

    • @Entity 애너테이션의 name 속성 사용하기

      @Entity(name = "member_list")
      public class Article {
      	...
      }
  • protected 기본 생성자: 엔티티는 반드시 기본 생성자가 필요하며 접근 제어자는 public/protected 여야함.
  • @Id 애너테이션 : Long 타입의 id 필드를 테이블의 기본키로 지정
  • @GeneratedValue: 기본키의 생성 방식을 결정함. 위의 코드에서는 자동으로 기본키가 증가되도록 설정함. ✅ 자동키 생성 결정 방식 - AUTO: 선택한 데이터베이스 방언에 따라 방식을 자동으로 선택(기본값) - IDENTITY: 기본키 생성을 데이터베이스에 위임 (=AUTO_INCREMENT) - SEQUENCE: 데이터베이스 시퀀스를 이용해 기본키를 할당하는 방법. 오라클에서 주로 사용 - TABLE: 키 생성 테이블 사용
  • @Column 애너테이션: 데이터베이스의 컬럼과 필드를 매핑해줌. ✅ 대표적인 `@Column` 애너테이션의 속성 - name: 필드와 매핑할 컬럼 이름. 설정하지 않으면 필드 이름으로 지정해줌 - nullable: 컬럼의 null 허용 여부. 설정하지 않으면 true(nullable) - unique: 컬럼의 유일한 값 여부. 설정하지 않으면 false(non-unique) - columnDefinition: 컬럼 정보 설정. default 값을 줄 수 있음
//MemberRepository.java 파일

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByName(String name);
}
  • 리포지토리는 엔티티에 있는 데이터들을 조회/저장/변경/삭제할 때 사용하는 인터페이스로 스프링 데이터 JPA에서 제공하는 인터페이스인 JpaRepository 클래스를 상속받아 간단히 구현 가능함.
  • JpaRepository 클래스를 상속받을 때, 엔티티 Member와 엔티티의 기본키 타입인 Long을 인수로 넣어줌.

06장. 블로그 기획하고 API 만들기

6.0 그림으로 이해하는 프로젝트

  • 목표: 블로그 기능 하나씩 구현하기 - 블로그 글 1개 추가하는 과정에 필요한 클래스&메소드&DB 테이블

  • 웹 브라우저가 POST 요청을 보내고, 이 요청을 BlogApiController 클래스의 특정 메소드인 addArticle() 메소드가 받아 BlogService 클래스의 save() 메소드를 실행ㅎ시키는 모습.
  • 이후 save() 메소드에서 BlogRepository 클래스, Article 클래스를 거쳐 실제 데이터를 저장함.

6.1 사전 지식: API와 REST API

  • API: 클라이언트의 요청을 서버에 전달하고, 서버의 결과물을 클라이언트에 돌려주는 역할 (클라이언트와 서버의 매개 같은 느낌?)

REST API: 웹의 장점을 최대한 활용

  • 자원을 이름으로 구분해 자원의 상태를 주고받는 API 방식을 말함

  • REST API는 URL의 설계 방식을 말함

  • 특징: 서버/클라이언트 구조, 무상태, 캐시 처리 기능, 계층화, 인터페이스 일관성 등의 특징이 존재

  • 장단점

    • 장정
      • URL만 보고 무슨 동작을 하는 API인지 명확하게 알 수 있음
      • 상태가 없어 클라이언트와 서버의 역할이 명확하게 분리됨
      • HTTP 표준을 사용하는 모든 플랫폼에서 사용 가능
    • 단점
      • HTTP 메소드(GET, POST와 같은 방식)의 개수에 제한이 존재
      • 설계를 하기 위해 공식적으로 제공되는 표준 규약이 없음

REST API를 사용하는 방법

  • 규칙1. URL에는 동사를 쓰지 말고, 자원을 표시해야함
    • 자원: 가져오는 데이터를 말함

    • 예) 학생 중 id가 1인 학생의 정보를 가져오는 URL의 설계
      - /students/1
      - /get-student?student_id=1

      ⇒ REST API에 더 적합한 URL은 1번. 2번은 get이라는 동사를 사용했기 때문에 부적합함

  • 규칙2. 동사는 HTTP 메소드로
    • HTTP 메소드: 서버에 요청하는 방법의 구분법
      • 주로 사용하는 메소드: POST, GET, PUT, DELETE ⇒ 보통 이것들을 묶어서 CRUD라고 부름
    • 예) 블로그에 글을 쓰는 설계
      설명적합한 HTTP 메소드와 URL
      id가 1인 블로그 글을 조회하는 APIGET. /articles/1
      블로그 글을 추가하는 APIPOST. /articles
      블로그 글을 수정하는 APIPUT. /articles/1
      블로그 글을 삭제하는 APIDELETE. /articles/1

6.2 블로그 개발을 위한 엔티티 구성하기

목표: 먼저 엔티티를 구성하고, 구성한 엔티티를 사용하기 위한 리포지토리 추가하기

프로젝트 준비하기

  • 디렉토리 분리: 보통 디렉토리는 계층별로 분리하거나 도메인 단위로 구분해서 분리 ⇒ 여기서는 계층별로 코드를 디렉토리에 넣어 분리
    • 프레젠테이션 계층: controller
    • 비즈니스 계층: service
    • 퍼시스턴스 계층: repository
    • 데이터베이스와 연결되는 DAO: domain

엔티티 구성하기

엔티티와 매핑되는 테이블 구조

컬럼명자료형null 허용 여부설명
idBIGINTN기본키일련번호, 기본키
titleVARCHAR(255)N게시물의 제목
contentVARCHAR(255)N내용
// main/domain/Article.java 파일

@Entity
public class Article {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //기본키 자동 1씩 증가
    @Column(name="id", updatable = false)
    private Long id;
    
    @Column(name="title", nullable = false)
    private String title;
    
    @Column(name="content", nullable = false)
    private String content;
    
    @Builder //빌더 패턴으로 객체 생성
    public Article (String title, String content) {
        this.title = title;
        this.content = content;
    }
    
    protected Article() {} //기본생성자

    //Getter
    public Long getId() {
        return id;
    }
    public String getTitle() {
        return title;
    }
    
    public String getContent() {
        return content;
    }
}
  • @Builder 애너테이션: 롬복에서 지원하는 애너테이션. 생성자 위에 해당 애너테이션 입력 시 빌더 패턴 방식으로 객체를 생성할 수 있어 편리.
  • 빌더 패턴: 객체를 유연하고 직관적으로 생성할 수 있기에 개발자들이 많이 사용함
    • 즉, 어느 필드에 어떤 값이 들어가는지 명시적으로 파악할 수 있음
    • 예)
      //빌더 패턴 X
      new Article ("abc", "def");
      
      //빌더 패턴
      Article.buidler()
      	.title("abc")
      	.content("def")
      	.build()
  • 롬복을 사용해 코드를 더 깔끔하게 바꾸기
    • Getter 메소드들을 @Getter , @NoArgsConstructor 애너테이션으로 대치
//최종 코드

@Getter
@NoArgsConstructor
@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //기본키 자동 1씩 증가
    @Column(name="id", updatable = false)
    private Long id;

    @Column(name="title", nullable = false)
    private String title;

    @Column(name="content", nullable = false)
    private String content;

    @Builder //빌더 패턴으로 객체 생성
    public Article (String title, String content) {
        this.title = title;
        this.content = content;
    }
}

리포지토리 만들기

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

⇒ 클래스 상속 시 Article 엔티티와 기본키 타입인 Long을 인수로 넣기

6.3 블로그 글 작성을 위한 API 구현하기

진행상황: 엔티티 구성 끝 ⇒ API를 하나씩 구현하기!

구현과정: 서비스 클래스에서 메소드 구현 ⇒ 컨트롤러에서 사용할 메소드 구현 ⇒ API 실제 테스트

서비스 메소드 코드 작성하기

블로그에 글을 추가하는 코드를 서비스 계층에 작성하기:

서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체 생성→ BlogService 클래스 생성 → 블로그 글 추가 메소드인 save() 구현

  1. dto 패키지 생성 및 AddArticleRequest.java 파일 작성

    • DTO: 계층끼리 데이터를 교환하기 위해 사용하기 객체 ⇒ 단순히 데이터를 옮기기 위해 사용하기에 별도 비즈니스 로직을 포함하지 않음
    • DAO: 데이터베이스와 연결되고 데이터를 조회/수정하는데에 사용하는 객체
    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    public class AddArticleRequest {
        
        private String title;
        private String content;
        
        public Article toEntity() {
            return Article.builder()
                    .title(title)
                    .content(content)
                    .build();
        }
    }
    • toEntity() 메소드: 빌더 패턴을 사용해 DTO를 엔티티로 만들어주는 메소드. 추후 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도로 사용
  2. service 패키지 생성 및 BlogService.java 파일 작성

    @RequiredArgsConstructor //final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
    @Service //빈으로 등록
    public class BlogService {
        
        private final BlogRepository blogRepository;
        
        //블로그 글 추가 메소드
        public Article save(AddArticleRequest request) {
            return blogRepository.save(request.toEntity());
        }
    }
    • @RequiredArgsConstructor 애너테이션: 빈을 생성자로 생성하는 롬복에서 지원. final 키워드나 @NotNull 이 붙은 필드로 생성자를 만들어줌
    • @Service 애너테이션: 해당 클래스를 빈으로 서블릿 컨테이너에 등록해줌
    • save() 메소드: AddArticleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장함

컨트롤러 메소드 코드 작성하기

목표: URL에 매핑하기 위한 컨트롤러 메소드 추가하기

@RequiredArgsConstructor
@RestController //HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
    
    private final BlogService blogService;
    
    //HTTP 메소드가 POST일 때 전달받은 URL과 동일하면 메소드로 매핑
    @PostMapping("/api/articles")
    //@RequestBody로 요청 본문 값 매핑
    public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request) {
        Article savedArticle = blogService.save(request);
        
        //요청한 자원이 성공적으로 생성되었으며 저장된 블로그 글 정보를 응답 객체에 담아 전송
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(savedArticle);
    }
} 
  • @RestController 애너테이션: HTTP 응답으로 객체 데이터를 JSON 형식으로 반환함
  • @RequestBody 애너테이션: HTTP를 할 때 응답에 해당하는 값을 @RequestBody 애너테이션이 붙은 대상 객체인 AddArticleRequest에 매핑함
  • ResponseEntity.status().body() : 응답코드 201, 즉 Created를 응답하고 테이블에 저장된 객체를 반환

✅ 알아두면 좋은 응답코드

  • 200 OK: 요청이 성공적으로 수행됨
  • 201 Created:
  • 400 Bad Request
  • 403 Forbidden
  • 404 Not Found
  • 500 Internal Server Error

API 실행 테스트하기

  • 실제 데이터 확인을 위해서는 H2 콘솔을 활성화해야함 → 속성 파일을 수정해줘야함
spring:
  jpa:
    #전송 쿼리 확인
    show-sql: true
    properties:
      hibernate:
        format_sql: true

    #테이블 생성 후에 data.sql 실행
    defer-datasource-initialization: true
    
    datasource:
      url: jdbc:h2:mem:testdb
    
    h2:
      console:
        enabled: ture

반복 작업을 줄여줄 테스트 코드 작성하기

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {
    
    @Autowired
    protected MockMvc mockMvc;
    
    @Autowired
    protected ObjectMapper objectMapper;
    
    @Autowired
    private WebApplicationContext context;
    
    @Autowired
    BlogRepository blogRepository;
    
    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
        blogRepository.deleteAll();
    }
}
  • ObjectMapper 클래스: 해당 클래스로 만든 객체는 자바 객체를 JSON 데이터로 변환하는 직렬화 또는 반대로 JSON 데이터를 자바에서 사용하기 위해 자바 객체로 변환하는 역직렬화를 할 때 사용함
@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {
		...
    
    @DisplayName("addArticle: 블로그 글 추가에 성공한다")
    @Test
    public void addArticle() throws Exception {
        // given
        final String url = "/api/articles";
        final String title = "title";
        final String content = "content";
        final AddArticleRequest userRequest = new AddArticleRequest(title, content);

        // 객체 → JSON 직렬화
        final String requestBody = objectMapper.writeValueAsString(userRequest);

        // when
        ResultActions result = mockMvc.perform(
                post(url)
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .content(requestBody)
        );

        // then
        result.andExpect(status().isCreated());

        List<Article> articles = blogRepository.findAll();

        assertThat(articles.size()).isEqualTo(1);
        assertThat(articles.get(0).getTitle()).isEqualTo(title);
        assertThat(articles.get(0).getContent()).isEqualTo(content);
    }
}
  • writeValueAsString 메소드: 객체를 JSON으로 직렬화하기
  • MockMvc: HTTP 메소드, URL, 요청 본문, 요청 타입 등 설정한 뒤에 설정 내용 바탕으로 테스트 요청 보내기
  • contentType() 메소드: 요청을 보낼 때 JSON, XML 등의 타입 중 하나를 골라 요청을 보냄

6.4 블로그 글 목록 조회를 위한 API 구현하기

서비스 메소드 코드 작성하기

@RequiredArgsConstructor //final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service //빈으로 등록
public class BlogService {

    private final BlogRepository blogRepository;

    //블로그 글 추가 메소드
    public Article save(AddArticleRequest request) {
        return blogRepository.save(request.toEntity());
    }
    
    public List<Article> findAll() {
        return blogRepository.findAll();
    }
}
  • JPA 지원 메소드인 findAll()을 호출해 article 테이블에 저장되어 있는 모든 데이터를 조회

컨트롤러 메소드 코드 작성하기

/api/articles GET 요청이 오면 글 목록을 조회할 findAllArticles() 메소드를 작성

@Getter
public class ArticleResponse {
    
    private final String title;
    private final String content;
    
    public ArticleResponse (Article article) {
        this.title = article.getTitle();
        this.content = article.getContent();
    }
}
@RequiredArgsConstructor
@RestController //HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {

    private final BlogService blogService;

    //HTTP 메소드가 POST일 때 전달받은 URL과 동일하면 메소드로 매핑
    @PostMapping("/api/articles")
    //@RequestBody로 요청 본문 값 매핑
    public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request) {
        Article savedArticle = blogService.save(request);

        //요청한 자원이 성공적으로 생성되었으며 저장된 블로그 글 정보를 응답 객체에 담아 전송
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(savedArticle);
    }
    
    @GetMapping("/api/articles")
    public ResponseEntity<List<ArticleResponse>> findAllArticles() {
        List<ArticleResponse> articles = blogService.findAll()
                .stream()
                .map(ArticleResponse::new)
                .toList();
        
        return ResponseEntity.ok()
                .body(articles);
    }
}

실행 테스트하기

테스트 코드 작성하기

@DisplayName("findAllArticles: 블로그 글 목록 조회에 성공한다.")
    @Test
    public void findAllArticles() throws Exception {
        //given
        final String url = "/api/articles";
        final String title = "title";
        final String content = "content";

        blogRepository.save(Article.builder()
                .title(title)
                .content(content)
                .build());

        //when
        final ResultActions resultActions = mockMvc.perform(get(url)
                .accept(MediaType.APPLICATION_JSON));

        //then
        resultActions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].content").value(content))
                .andExpect(jsonPath("$[0].title").value(title));
    }

profile
이불 밖은 위험해.

0개의 댓글