SQL, JPQL 같은 쿼리를 Java 코드로 작성할 수 있게 해주는 타입 안전한 ORM 기반 쿼리 빌더
SQL을 Java 코드로 작성하면서, 가독성과 유지보수성을 대폭 개선할 수 있는 도구
1.2 ☝️ 왜 사용해야할까
1.2.1 SQL/JPQL의 한계 해결
문법 오류 - 쿼리의 오류를 런타임에서만 확인 가능
필드 이름 변경 - 엔티티 필드가 변경되면 문자열 기반 쿼리에서 이를 수동으로 수정해야 함.
동적 쿼리 작성의 어려움 - 조건에 따라 쿼리를 유연하게 작성하려면 코드 복잡도 향상
코드 비교
// JPQL
// 필터가 많아질수록 코드가 복잡
public List<User> findUsersDynamicJPQL(EntityManager em, Integer age, String name) {
StringBuilder jpql = new StringBuilder("SELECT u FROM User u WHERE 1=1");
if (age != null) {
jpql.append(" AND u.age > :age");
}
if (name != null) {
jpql.append(" AND u.name = :name");
}
TypedQuery<User> query = em.createQuery(jpql.toString(), User.class);
if (age != null) query.setParameter("age", age);
if (name != null) query.setParameter("name", name);
return query.getResultList();
}
// QueryDSL
// BooleanBuilder 사용
public List<User> findUsersDynamicQueryDSL(JPAQueryFactory queryFactory, Integer age, String name) {
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder(); // 동적 조건 빌더
if (age != null) {
builder.and(user.age.gt(age));
}
if (name != null) {
builder.and(user.name.eq(name));
}
return queryFactory
.selectFrom(user)
.where(builder) // 동적 조건 적용
.fetch();
}
Spring Data JPA에서 제공하는 Specification 인터페이스를 활용하면 동적 쿼리 구현
✅ 장점
Spring Data JPA와 통합: Spring 프로젝트와 자연스럽게 연동.
동적 조건 작성이 비교적 간단.
❌ 단점
QueryDSL에 비해 표현력이 제한적.
복잡한 쿼리 작성에는 적합하지 않음.
예제코드
public class UserSpecifications {
public static Specification<User> hasAgeGreaterThan(int age) {
return (root, query, cb) -> cb.gt(root.get("age"), age);
}
public static Specification<User> hasStatus(String status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
}
// 사용
Specification<User> spec = Specification.where(UserSpecifications.hasAgeGreaterThan(18))
.and(UserSpecifications.hasStatus("ACTIVE"));
List<User> users = userRepository.findAll(spec);
1.4.3 네이티브 SQL
✅ 장점
유연성: 데이터베이스에 특화된 쿼리를 작성 가능
성능 최적화에 유리
❌ 단점
타입 안전성 부족: 문자열 기반으로 작성
데이터베이스 종속적 → 이식성이 낮음
유지보수가 어려움
예제 코드
@Query(value = "SELECT * FROM users WHERE age > :age AND status = :status", nativeQuery = true)
List<User> findUsersNative(@Param("age") int age, @Param("status") String status);
1.4.4 jOOQ (Java Object Oriented Querying)
jOOQ는 SQL을 Java 코드로 작성하면서도, 데이터베이스와 밀접하게 통합하여 고급 기능을 사용할 수 있는 강력한 도구
✅ 장점
SQL 친화적 설계: SQL의 고급 기능(윈도우 함수, CTE 등)을 Java 코드로 직접 활용 가능.
타입 안전성: SQL 컬럼과 Java 필드 간의 타입을 컴파일 타임에 검증.
DBMS 특화 기능 지원: 데이터베이스에 따라 최적화된 SQL 생성 가능.
코드 자동 생성: 테이블 스키마 기반으로 Java 코드를 생성하여, 변경 사항을 자동 반영.
❌ 단점
SQL 종속성: 데이터베이스 변경 시 쿼리 코드 수정이 필요.
학습 난이도: SQL에 익숙하지 않은 개발자에게는 진입 장벽이 높음.
ORM 부족: 객체 지향적 추상화가 부족하고, SQL 중심의 설계에 초점.
예제 코드
// 동적 조건 추가
public List<User> findUsersDynamicJooq(DSLContext context, Integer age, String name) {
Condition condition = DSL.trueCondition();
if (age != null) {
condition = condition.and(USER.AGE.gt(age));
}
if (name != null) {
condition = condition.and(USER.NAME.eq(name));
}
return context.selectFrom(USER)
.where(condition)
.fetchInto(User.class);
}
라이브러리
특징
장점
단점
QueryDSL
- 타입 안전한 동적 쿼리 작성을 지원 - Q클래스를 통해 메서드 체인 방식으로 쿼리를 작성
- 타입 안전성(Type Safety) 보장 - 가독성 높은 코드 작성 가능 - IDE 자동완성 지원
- 초기 설정 복잡 - Q클래스 생성 필요
Spring Data JPA Specifications
- JPA의 Criteria API를 기반으로 동적 쿼리를 작성 - Specification 인터페이스를 사용
- 추가 라이브러리 없이 Spring Data JPA와 자연스러운 통합 - 동적 쿼리 작성 가능
- 코드가 장황하고 가독성이 떨어질 수 있음
JOOQ
- SQL 빌더 라이브러리로, SQL 문법을 코드로 표현 - 고급 SQL 기능을 지원
- SQL 문법을 코드로 직관적으로 표현 가능 - 복잡한 쿼리와 고급 SQL 기능 활용 가능
DIP를 적용하게 되면, 응용 영역과 도메인 영역의 코드 수정 없이 인프라스트럭처부분만 코드를 추가/수정 및 변경하여 요구사항을 충족시킬 수 있다.
2.4 도메인 영역의 주요 구성요소
구성요소
설명
예시
엔티티(Entity)
고유 식별자를 가지며, 상태와 동작을 포함하는 객체
User, Order, Product
값 객체(Value Object)
고유 식별자가 없으며, 불변 객체로 주로 여러 속성을 하나의 개념으로 묶을 때 사용
Address, Money, DateRange
애그리게잇(Aggregate)
관련된 엔티티와 값 객체의 그룹으로, 일관된 변경을 보장하기 위한 경계를 정의하고 루트 엔티티를 통해 접근
Order (루트: Order, 구성 요소: OrderLine)
리포지토리(Repository)
애그리게잇을 영구 저장소에서 조회하고 저장하는 메커니즘을 제공, 인터페이스와 구현체로 구성
OrderRepository, UserRepository
도메인 서비스(Domain Service)
특정 엔티티에 속하지 않는 도메인 로직을 캡슐화하는 서비스
PricingService, PaymentService
2.4.1 엔티티와 벨류
도메인 모델의 엔티티 vs DB 테이블의 엔티티
도메인 모델의 엔티티는 단순 데이터만 담고 있는 구조가 아닌 데이터와 함께 도메인 기능을 제공
도메인 모델 두개이상의 데이터가 개념적으로 하나인 경우에 Value 타입을 이용해서 표현할 수 있다.
ex) Order → name, email 을 Orderer라는 Value타입 객채를 만들어 관리 가능
2.4.2 애그리거트
지도를 볼 때 매우 상세하게 나온 대축척 지도를 보면 큰 수준에서 어디에 위치하고 있는지 이해하기 어려우므로 큰 수준에서 보여주는 소축척 지도를 함께 봐야 현재 위치를 보다 정확하게 이해할 수 있다. 이와 비슷하게 도메인 모델도 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는데 도움이 된다.
ex) ‘주문’, ‘배송지 정보’, ‘주문자’, ‘주문 목록’, ‘총 결제 금액’의 하위 모델들을 ‘주문’이라는 상위 개념으로 표현할 수 있다.
2.5 요청 처리 흐름
2.6 인프라스트럭처 개요
표현 영역, 응용 영역, 도메인 영역을 지원한다.
DIP에서 언급한 내용처럼 인프라스트럭처의 기능을 직접 사용하는것보다 인터페이스를 만들어서 사용하게 되면 시스템을 더 유연하고 테스트하기 쉽게 만들어준다. 하지만 무조건 인프라스트럭처에 대한 의존을 없애게 되면 오히려 더 복잡하고 어려운 코드를 유도할 수 있다.
ex) 인프라 스트럭처를 직접 사용하지 않기 위해 @Transaction 어노테이션을 사용하지 않고 개발을 하려고 하면 한줄로 처리할 수 있는 코드를 복잡하고 개발시간만 더 늘어날 뿐이다.
2.7 모듈 구성
도메인이 크면 하위 도메인으로 나누고 각 하위 도메인 마다 별도 패키지를 구성한다.
모듈 구성에 대해서는 정답이 없으며, 한 패키지에 10~15개 미만으로 타입 개수를 유지하려고 노력한다.
슬라이딩 윈도우(Sliding Window)는 배열이나 리스트에서 특정 범위(윈도우)에 대한 계산을 반복적으로 수행해야 할 때 유용한 알고리즘 기법
2. 종류
고정 크기 윈도우
주어진 크기의 연속된 구간을 탐색하는 경우
가변 크기 윈도우
특정 조건을 만족하는 구간을 탐색하는 경우
3. 슬라이딩 윈도우 vs 투포인터
3.1 특징
슬라이딩 윈도우
투 포인터
목적
고정되거나 조건에 따라 이동하는 구간(window)을 탐색
두 포인터를 사용해 배열의 특정 조건을 만족하는 값을 탐색
윈도우 크기
고정 또는 가변
일반적으로 가변
주요 동작
윈도우를 한 칸씩 이동하며 필요한 부분만 갱신
두 포인터를 독립적으로 이동하며 구간의 조건을 만족하도록 조정
사용 상황
연속된 구간에 대한 계산이 필요할 때
정렬된 배열에서 특정 조건을 만족하는 값을 찾거나, 구간을 탐색할 때
3.2 공통점
배열이나 리스트에서 효율적으로 탐색
O(N)의 시간 복잡도로 문제 해결 가능
3.3 차이점
탐색 구간
슬라이딩 윈도우
연속된 구간을 유지하며 탐색
투 포인터
두 포인터가 독립적으로 움직이며 조건을 만족하도록 탐색.
조건
슬라이딩 윈도우
윈도우 내 값을 반복적으로 갱신
투 포인터
값이나 조건을 만족할 때까지 포인터를 확장/축소.
3.4 선택 조건
슬라이딩 윈도우를 선택:
구간 내 합, 곱, 최대/최소값을 반복적으로 계산해야 하는 경우.
문제에서 “연속된 서브배열”이라는 조건이 있는 경우.
예: 크기가 K인 서브배열의 최대합.
투 포인터를 선택:
정렬된 배열에서 조건을 만족하는 두 값이나 범위를 찾는 경우.
문제에서 “특정 합”, “차이”, “곱” 등을 만족하는 두 요소를 요구하는 경우.
예: 두 수의 합, 특정 값의 구간 찾기.
4. 예시
4.1 고정 크기 윈도우
import java.util.Arrays;
public class FixedWindowAverage {
public static double[] findAverages(int[] arr, int k) {
if (arr.length < k) {
throw new IllegalArgumentException("Array length must be greater than or equal to k.");
}
double[] result = new double[arr.length - k + 1];
double windowSum = 0;
// 초기 윈도우 합 계산
for (int i = 0; i < k; i++) {
windowSum += arr[i];
}
result[0] = windowSum / k;
// 슬라이딩 윈도우 이동
for (int i = k; i < arr.length; i++) {
windowSum += arr[i] - arr[i - k]; // 새 요소 추가, 오래된 요소 제거
result[i - k + 1] = windowSum / k; // 평균 계산
}
return result;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int k = 3;
double[] averages = findAverages(arr, k);
System.out.println("크기 " + k + "의 서브배열 평균값: " + Arrays.toString(averages));
// 출력: [2.0, 3.0, 4.0]
}
}
4.2 가변 크기 윈도우
public class VariableWindowExample {
public static int minSubArrayLen(int target, int[] arr) {
int n = arr.length;
int left = 0, sum = 0;
int minLength = Integer.MAX_VALUE;
for (int right = 0; right < n; right++) {
sum += arr[right]; // 윈도우 확장
while (sum >= target) { // 조건 만족 시 윈도우 축소
minLength = Math.min(minLength, right - left + 1);
sum -= arr[left];
left++;
}
}
return minLength == Integer.MAX_VALUE ? 0 : minLength;
}
public static void main(String[] args) {
int[] arr = {2, 3, 1, 2, 4, 3};
int target = 7;
int result = minSubArrayLen(target, arr);
System.out.println("합이 " + target + " 이상인 가장 짧은 서브배열의 길이: " + result); // 출력: 2
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
void testAddition() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result); // 실패! 왜? 구현 코드가 없으니까.
}
}
1.2.2 Green: 테스트 통과는 최소 노력으로.
테스트를 통과시키는 코드 작성.
복잡하게 고민하지 말고 “빨리 테스트 통과”에 집중!
public class Calculator {
public int add(int a, int b) {
return a + b; // 통과! 왜? 테스트가 요구한 기능만 구현했으니까.
}
}
1.2.3 Refactor: 이제 진짜 멋지게!
읽기 좋은 코드, 유지보수하기 쉬운 코드로 개선.
코드의 동작은 그대로, 구조는 깔끔하게!
public class Calculator {
// 향후 확장을 고려해 더 나은 구조로 개선
public int add(int a, int b) {
return calculate(a, b, "+");
}
// 공통 로직 처리 메서드 추가
private int calculate(int a, int b, String operation) {
switch (operation) {
case "+":
return a + b;
default:
throw new UnsupportedOperationException("Operation not supported");
}
}
}
✨ 1.3 왜 중요한가요?
🛡 안정성: 모든 기능에 대한 자동화된 테스트 보장.
📈 생산성 향상: 리팩토링 부담 감소.
“TDD를 하면 시간 낭비라고?” 아니요! 미래의 당신이 감사할 겁니다. 😎
🔥 1.4 장단점
장점
코드의 신뢰성 증가.
요구사항 변경에도 흔들리지 않는 튼튼한 구조.
버그 감소로 디버깅 시간 절약.
단점
초기 진입장벽 존재.
테스트 작성에 시간 투자 필요.
잘못된 테스트 설계 시 오히려 발목을 잡을 수 있음.
🙀 1.5 DDD(Domain Driven Design)와의 관계
TDD는 테스트를 통해 코드를 어떻게 구현할지에 초점을 둔 개발 방법론
DDD는 도메인 모델을 설계하여 무엇을 구현할지를 정의하는 철학
질문
TDD
DDD
왜 사용하는가?
기능의 동작을 보장하기 위해
비즈니스 요구사항을 시스템에 반영하기 위해
왜 시작하는가?
코드를 테스트하면서 명확한 피드백을 얻기 위해
요구사항과 도메인 모델을 기반으로 설계하기 위해
왜 중요한가?
코드의 안정성과 품질을 높이기 위해
복잡한 비즈니스 로직을 제대로 반영하기 위해
어떻게 다른가?
테스트 작성 → 코드 작성 → 리팩토링
요구사항 분석 → 도메인 설계 → 코드 작성
2. 테스트 코드
💡2.1 테스트 코드란 무엇인가요?
“내 코드, 정말 제대로 작동할까?”라는 질문에 대한 답을 미리 준비하는 개발자의 무기
프로그램의 기능이 의도대로 동작하는지 자동으로 확인
🛠 2.2 종류
2.2.1 단위 테스트 (Unit Test)
특징: 순수 비즈니스 로직(단일 메서드나 클래스)을 테스트
도구: @Mock, Mockito.
예시: UserService가 UserRepository를 올바르게 호출하는지 테스트
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
@Mock
private UserRepository userRepository;
private UserService userService;
@Test
void should_ReturnUser_When_ValidIdProvided() {
// Mock 초기화
MockitoAnnotations.openMocks(this);
userService = new UserService(userRepository);
// Given
User mockUser = new User(1L, "Alice");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// When
User result = userService.getUserById(1L);
// Then
assertEquals("Alice", result.getName());
verify(userRepository, times(1)).findById(1L);
}
}
2.2.2 통합 테스트 (Integration Test)
특징: 여러 계층(Service, Repository 등)이 올바르게 연동되는지 확인
도구: @SpringBootTest
예시: 데이터 저장 및 조회가 잘 동작하는지 확인.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional // 테스트 후 DB 롤백
class UserIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void should_SaveAndRetrieveUser() {
// Given
User newUser = new User(null, "Bob");
// When
userService.saveUser(newUser);
User retrievedUser = userService.getUserById(newUser.getId());
// Then
assertNotNull(retrievedUser);
assertEquals("Bob", retrievedUser.getName());
}
}
2.2.3 End-to-End 테스트 (E2E Test)
특징: 시스템의 전체 흐름이 사용자 관점에서 잘 작동하는지 검증
도구: @SpringBootTest, TestRestTemplate
예시: 사용자 생성 후 조회 API가 올바르게 동작하는지 확인.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserE2ETest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void should_CreateAndRetrieveUser() {
// Given
String baseUrl = "http://localhost:" + port + "/users";
UserRequest userRequest = new UserRequest("Charlie");
// Step 1: 사용자 생성
UserResponse createdUser = restTemplate.postForObject(baseUrl, userRequest, UserResponse.class);
assertNotNull(createdUser);
assertEquals("Charlie", createdUser.getName());
// Step 2: 생성된 사용자 조회
UserResponse retrievedUser = restTemplate.getForObject(baseUrl + "/" + createdUser.getId(), UserResponse.class);
assertNotNull(retrievedUser);
assertEquals("Charlie", retrievedUser.getName());
}
}
🚦 2.3 Spring Boot 테스트에서 사용하는 Mock의 종류
2.3.1 @Mock
@Mock은 객체의 동작을 정의해서 의존성을 가짜로 만들어 줍니다.
주로 단위 테스트에서 사용하며, Mock 라이브러리인 Mockito를 함께 사용합니다.
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
@Mock
private UserRepository userRepository; // 독립된 Mock 객체 생성
private UserService userService;
@Test
void should_ReturnUser_When_ValidIdProvided() {
// Mock 초기화
MockitoAnnotations.openMocks(this);
userService = new UserService(userRepository);
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Alice")));
// When
User result = userService.getUserById(1L);
// Then
assertEquals("Alice", result.getName());
verify(userRepository, times(1)).findById(1L);
}
}
2.3.2 @MockBean
@MockBean은 Spring Context에 등록된 빈(bean)을 Mock 객체로 바꿔줍니다.
덕분에 Controller, Service, Repository가 연동된 통합 테스트에서도 유용합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.Optional;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean
private OrderRepository orderRepository; // Repository 빈을 Mock으로 대체
@Test
void should_ReturnOrder_When_OrderExists() {
// Given
Order mockOrder = new Order(1L, "ProductA", 3, "CREATED");
when(orderRepository.findById(1L)).thenReturn(Optional.of(mockOrder));
// When
Order result = orderService.getOrderById(1L);
// Then
assertNotNull(result);
assertEquals("ProductA", result.getProductName());
verify(orderRepository, times(1)).findById(1L);
}
}
2.3.3 @Spy
@Spy는 실제 객체를 사용하면서 특정 메서드만 Mocking할 수 있습니다.
로직의 일부만 제어하고 싶을 때 유용합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Spy;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class OrderServiceTest {
@Spy
private OrderService orderService = new OrderService();
@Test
void should_MockSpecificMethod() {
// Given
doReturn(500).when(orderService).calculateTotalPrice(100, 5);
// When
int totalPrice = orderService.calculateTotalPrice(100, 5);
String status = orderService.getOrderStatus(1L); // 실제 메서드 실행
// Then
assertEquals(500, totalPrice); // Mocking된 결과 검증
assertEquals("COMPLETED", status); // 실제 동작 검증
// Verify
verify(orderService, times(1)).calculateTotalPrice(100, 5);
verify(orderService, times(1)).getOrderStatus(1L);
}
}
2.3.4 @SpyBean
@SpyBean은 Spring Context에 등록된 실제 빈을 Spy 객체로 감싸줍니다.
즉, Spring 빈의 실제 동작을 수행하면서 일부 메서드만 Mocking 할 수 있습니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@SpyBean
private OrderService spyOrderService;
@Test
void should_UseSpyBeanToMockSpecificMethod() {
// Given
doReturn(1000).when(spyOrderService).calculateTotalPrice(200, 5);
// When
int mockedResult = spyOrderService.calculateTotalPrice(200, 5); // Mock 동작
String status = spyOrderService.getOrderStatus(1L); // 실제 동작 실행
// Then
assertEquals(1000, mockedResult); // Mocking 결과
assertEquals("COMPLETED", status); // 실제 메서드 결과
// Verify
verify(spyOrderService, times(1)).calculateTotalPrice(200, 5);
verify(spyOrderService, times(1)).getOrderStatus(1L);
}
}
어노테이션
대상
설명
사용 예
@Mock
독립된 가짜 객체
Spring Context와 무관, 가짜 객체 생성
순수 단위 테스트
@MockBean
Spring 빈을 가짜로 대체
Spring Context에서 Mock 객체로 교체
Controller와 Service 연결 테스트
@Spy
실제 객체 (부분만 Mocking)
일부 메서드만 Mocking, 나머지는 실제 실행
실제 객체의 부분 테스트
@SpyBean
Spring 빈을 Spy 객체로 감싸기
Spring 빈을 감싸서 일부만 Mocking
실제 Spring 빈의 동작과 호출 검증
✨ 2.4 테스트코드를 구조적으로 깔끔하게 짜는 꿀팁
2.4.1 @Nested
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("OrderService 단위 테스트")
class OrderServiceTest {
private OrderService orderService = new OrderService();
@Nested
@DisplayName("주문 생성 테스트")
class CreateOrderTests {
@Test
@DisplayName("정상적으로 주문을 생성한다.")
void should_CreateOrder_Successfully() {
Order order = orderService.createOrder("ProductA", 3);
assertNotNull(order);
assertEquals("ProductA", order.getProductName());
}
}
@Nested
@DisplayName("주문 조회 테스트")
class GetOrderTests {
@Test
@DisplayName("존재하는 주문을 조회한다.")
void should_ReturnOrder_When_OrderExists() {
Order order = orderService.createOrder("ProductA", 3);
Order retrievedOrder = orderService.getOrderById(order.getId());
assertEquals(order, retrievedOrder);
}
}
}
2.4.2 @DisplayName
@DisplayName은 테스트의 목적이나 동작을 설명하는 이름을 추가
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("OrderService 테스트")
class OrderServiceTest {
private OrderService orderService = new OrderService();
@Test
@DisplayName("주문 생성 시 상품 이름과 수량이 올바르게 설정된다.")
void should_CreateOrder_With_CorrectDetails() {
Order order = orderService.createOrder("ProductA", 3);
assertNotNull(order);
assertEquals("ProductA", order.getProductName());
assertEquals(3, order.getQuantity());
}
}
2.4.3 @BeforeEach / @AfterEach
@BeforeEach: 각 테스트 실행 전 공통 작업을 수행
@AfterEach: 각 테스트 실행 후 정리 작업을 수행
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class OrderServiceTest {
private OrderService orderService;
@BeforeEach
void setUp() {
orderService = new OrderService();
}
@Test
@DisplayName("주문 생성 시 상품 이름이 설정된다.")
void should_CreateOrder_With_ProductName() {
Order order = orderService.createOrder("ProductA", 3);
assertEquals("ProductA", order.getProductName());
}
@AfterEach
void tearDown() {
System.out.println("테스트 정리 작업 실행");
}
}
2.4.4 @TestMethodOrder + @Order
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(OrderAnnotation.class) // @Order 어노테이션 기준으로 순서 지정
@DisplayName("OrderService 순서 지정 테스트")
class OrderServiceTest {
private static OrderService orderService;
@BeforeAll
static void setUp() {
orderService = new OrderService();
}
@Test
@Order(1)
@DisplayName("1. 주문을 생성한다.")
void testCreateOrder() {
Order order = orderService.createOrder("ProductA", 3);
assertNotNull(order);
assertEquals("ProductA", order.getProductName());
}
@Test
@Order(2)
@DisplayName("2. 주문을 조회한다.")
void testGetOrder() {
Order order = orderService.getOrderById(1L); // 1번 ID의 주문 조회
assertNotNull(order);
assertEquals("ProductA", order.getProductName());
}
@Test
@Order(3)
@DisplayName("3. 주문 상태를 변경한다.")
void testUpdateOrderStatus() {
Order updatedOrder = orderService.updateOrderStatus(1L, "COMPLETED");
assertEquals("COMPLETED", updatedOrder.getStatus());
}
}
2.4.5 @TestMethodOrder(MethodName.class)
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.MethodName;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodName.class) // 메서드 이름 순서대로 실행
class OrderServiceTest {
@Test
void a_testCreateOrder() {
System.out.println("1. 주문 생성");
}
@Test
void b_testGetOrder() {
System.out.println("2. 주문 조회");
}
@Test
void c_testUpdateOrderStatus() {
System.out.println("3. 주문 상태 변경");
}
}