
:작성한 코드가 의도대로 잘 동작하고 예상치 못한 문제가 없는지 확인할 목적으로 작성하는 코드로 test 디렉토리에서 작업하는게 보편적.
테스트 코드에도 다양한 패턴이 존재하며, 해당 책에서는 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);
}
spring-boot-starter-test 스타터에는 테스트를 위한 도구가 모여있음.
✅ 스프링부트 스타터 테스트 목록
JUnit: 자바 프로그래밍 언어용 단위 테스트 프레임워크
Spring Test & Spring Boot Test: 스프링부트 애플리케이션을 위한 통합 테스트 지원
AssertJ: 검증문인 assertion을 작성하는데 사용되는 라이브러리
Hamcrest: 표현식을 이해하기 쉽게 만드는ㄷ에 사용되는 Matcher 라이브러리
Mockito: 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리, 검증할 수 있게 지원하는 테스트 프레임워크
JSONassert: JSON용 assertion 라이브러리
JsonPath: JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리
⇒ 위 중 JUnit과 AssertJ를 가장 많이 사용함.
: 자바 언어를 위한 단위 테스트 프레임워크. JUnit을 사용하면 단위 테스트를 작성하고 테스트하는데 도움이 됨.
@Test 애너테이션으로 메소드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능// '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이 아니어야함. 예) 테스트 이후 특정 데이터를 삭제해야하는 경우
//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보다 작은 값인지 검증 |



// 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를 생성하고 자동으로 구성하는 애너테이션. 컨트롤러를 테스트할 때 사용되는 클래스.// 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() 메소드에서 요청을 처리.quiz2() 메소드에서 요청을 처리quiz() 메소드: 요청 파라미터의 키가 ‘code’이면 int 자료형의 code 변수와 매핑되며, code 값에 따라 다른 응답을 보냄
| code 값 | 응답 코드 | 응답 본문 |
|---|---|---|
| 1 | 201 | Created! |
| 2 | 400 | Bad Request! |
| 그 외 | 200 | OK! |
quiz2() 메소드: 요청값을 Code라는 객체로 매핑한 후, value 값에 따라 다른 응답을 보냄.
| code 값 | 응답 코드 | 응답 본문 |
|---|---|---|
| 1 | 403 | Forbidden! |
| 그외 | 200 | OK! |
record Code (int value) {}: 매핑할 객체로 사용하기 위해 선언한 레코드.// 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)@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() 메소드: 쿼리 파라미터 추가하는 방법.
@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!"));
}

데이터베이스: 데이터를 매우 효율적으로 보관하고 꺼내볼 수 있는 곳. 많은 사람이 안전하게 데이터를 이용하고 관리할 수 있다는 장점이 존재
| 회원 테이블 | ||
|---|---|---|
| ID | 이메일 | 나이 |
| 1 | a@test.com | 10 |
| 2 | b@test.com | 20 |
| 3 | c@test.com | 30 |
💡 데이터베이스 용어 정리

데이터 조회하기: SELECT문
테이블에 저장한 데이터를 조회할 때는 SELECT문을 사용.
SELECT <무엇을>
FROM <어디에서>
WHERE <무슨>
예) 손님 정보를 저장하는 customers 라는 테이블.
| id | name | phone_number | age |
|---|---|---|---|
| 1 | 김일번 | 010-1111-1111 | 15 |
| 2 | ㅇㅣ이번 | 010-2222-2222 | 20 |
| 3 | 정삼번 | 010-3333-3333 | 35 |
✔️ 기본구조
SELECT <이름>
FROM <customers 테이블>
WHERE <id가 2>
SELECT name
FROM customers
WHERE id=2
*을 사용하면 됨. 예)SELECT *
FROM customers❓SQL문 연습문제1
SELECT *
FROM customers
WHERE id=1
❓SQL문 연습문제2
SELECT id, name
FROM customers
WHERE phone_numbers = '010-2222-2222'
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 '이%'
INSERT문INSERT문, 삭제하고 싶을 때는 DELETE문을 사용함.INSERT문은 INSERT INTO와 VALUES 키워드를 사용함INSERT INTO <어디에>
VALUES <어떤 값을>
예)
| id | name | phone_number | age |
|---|---|---|---|
| 1 | 김일번 | 010-1111-1111 | 15 |
| 2 | ㅇㅣ이번 | 010-2222-2222 | 20 |
| 3 | 정삼번 | 010-3333-3333 | 35 |
INSERT INTO customers (name, phone_number, age)
VALUES ('박사번', '010-4444-4444', 40);
DELETE문DELETE FROM <어디에서> WHERE <어떤 조건으로>;
예) id가 5인 레코드(=행)을 삭제할 때
DELETE FROM customers WHERE id=5;
UPDATE문특정 레코드의 값을 바꾸고 싶을 때 사용
UPDATE <어디에>
SET <무슨 컬럼을 = 어떤 값으로>
WHERE <어떤 조건으로>
| id | name | phone_number | age |
|---|---|---|---|
| 1 | 김일번 | 010-1111-1111 | 15 |
| 2 | ㅇㅣ이번 | 010-2222-2222 | 20 |
| 3 | 정삼번 | 010-3333-3333 | 35 |
예) 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 = ''
✅ ORM의 장단점
#장점
#단점
DBMS에 여러 종류가 있듯 ORM에도 여러 종류가 존재.
JPA
하이버네이트

✅ JPA와 하이버네이트의 역할
엔티티
엔티티 매니저
엔티티 매니저: 엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성/수정/삭제하는 등의 역할
엔티티 매니저 팩토리: 엔티티 매니저를 만드는 곳
예) 회원 2명이 동시에 회원가입을 하려는 경우 엔티티 매니저의 동작
스프링부트에서는 하나의 엔티티 매니저 팩토리만 생성해 관리하고 @Persistence Context 또는 @Autowired 애너테이션을 사용해 엔티티 매니저를 사용함
@PersistentContext
EntityManager em; //프록시 엔티티 매니저. 필요할 때 진짜 매니저 호출
스프링부트는 기본적으로 빈을 하나만 생성해 공유하기 때문에 동시성 문제가 발생 가능.
→ 따라서 엔티티 매니저가 아닌 실제 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용하고 필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 호출.
⇒ 엔티티 매니저는 Spring Data JPA에서 관리하기에 직접 생성하거나 관리할 필요X
엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장한다는 특징 존재.
영속성 컨텍스트: JPA의 중요한 특징으로 엔티티를 관리하는 가상의 공간. 이를 통해 데이터베이스에서 효과적으로 데이터를 가져올 수 있고, 엔티티를 편하게 사용할 수 있음.
1차 캐시
@Id 애너테이션이 달린 기본키 역할을 하는 식별자이며 값은 엔티티쓰기 지연
변경 감지
지연 로딩
⇒ 데이터베이스의 접근을 최소화해 성능을 높일 수 있음.
⇒ 이런 상태들은 특정 메소드를 호출해 변경할 수 있음 + 필요에 따라 엔티티의 상태를 조절해 데이터를 올바르게 유지 관리 가능
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() 메소드: 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제 가능스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술
스프링 데이터의 인터페이스인 PagingAndSortingRepository를 상속받아 JpaRepository 인터페이스를 만들어 JPA를 더 편리하게 사용하는 메소드 제공
리포지토리 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있음
JpaRepository 인터페이스를 만든 인터페이스에서 상속받고, 제너릭에서 관리할 <엔티티 이름, 엔티티 기본키의 타입>을 입력하면 기본 CRUD 메소드를 사용할 수 있음
public interface MemberRepository extends JpaRepository<Member, Long> {
}
SQL 쿼리에서 조회를 위해서는 SELECT를 사용했지만, JPA에서 데이터를 가져올 때에는 쿼리를 작성하는 대신 findAll() 메소드를 사용
SELECT * FROM member


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

⇒ 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);
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() 메소드와 같은 형태로 사용함.
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는 변경 감지 기능을 통해 엔티티의 필드값이 변경될 때 그 변경 사항을 데이터베이스에 자동으로 반영함@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 애너테이션: 테스트를 위한 설정을 제공. 자동으로 데이터베이스에 대한 트랜잭션 관리를 설정함.//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);
}

addArticle() 메소드가 받아 BlogService 클래스의 save() 메소드를 실행ㅎ시키는 모습.save() 메소드에서 BlogRepository 클래스, Article 클래스를 거쳐 실제 데이터를 저장함.자원을 이름으로 구분해 자원의 상태를 주고받는 API 방식을 말함
REST API는 URL의 설계 방식을 말함
특징: 서버/클라이언트 구조, 무상태, 캐시 처리 기능, 계층화, 인터페이스 일관성 등의 특징이 존재
장단점
자원: 가져오는 데이터를 말함
예) 학생 중 id가 1인 학생의 정보를 가져오는 URL의 설계
- /students/1
- /get-student?student_id=1
⇒ REST API에 더 적합한 URL은 1번. 2번은 get이라는 동사를 사용했기 때문에 부적합함
| 설명 | 적합한 HTTP 메소드와 URL |
|---|---|
| id가 1인 블로그 글을 조회하는 API | GET. /articles/1 |
| 블로그 글을 추가하는 API | POST. /articles |
| 블로그 글을 수정하는 API | PUT. /articles/1 |
| 블로그 글을 삭제하는 API | DELETE. /articles/1 |
목표: 먼저 엔티티를 구성하고, 구성한 엔티티를 사용하기 위한 리포지토리 추가하기
엔티티와 매핑되는 테이블 구조
| 컬럼명 | 자료형 | null 허용 여부 | 키 | 설명 |
|---|---|---|---|---|
| id | BIGINT | N | 기본키 | 일련번호, 기본키 |
| title | VARCHAR(255) | N | 게시물의 제목 | |
| content | VARCHAR(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 , @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을 인수로 넣기
진행상황: 엔티티 구성 끝 ⇒ API를 하나씩 구현하기!
구현과정: 서비스 클래스에서 메소드 구현 ⇒ 컨트롤러에서 사용할 메소드 구현 ⇒ API 실제 테스트
블로그에 글을 추가하는 코드를 서비스 계층에 작성하기:
서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체 생성→ BlogService 클래스 생성 → 블로그 글 추가 메소드인 save() 구현
dto 패키지 생성 및 AddArticleRequest.java 파일 작성
@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를 엔티티로 만들어주는 메소드. 추후 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도로 사용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를 응답하고 테이블에 저장된 객체를 반환✅ 알아두면 좋은 응답코드
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();
}
}
@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으로 직렬화하기contentType() 메소드: 요청을 보낼 때 JSON, XML 등의 타입 중 하나를 골라 요청을 보냄
@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();
}
}
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));
}
