Spring + Mockito 환경에서 @Mock, @InjectMocks, 그리고 Stream API의 Collectors.toMap 사용법을 정리해봤다.
실습 코드를 기반으로 테스트 코드 작성법과 Stream API 사용 이유를 함께 이해해보자.
📑 예제 코드 소개
아래는 공통코드 그룹핑 서비스 테스트 예제:
﹡CmmnCodeServiceTest
package wpo.wpms.common;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.extension.ExtendWith;
import wpo.wpms.common.domain.dto.CmmnCodeDTO;
import wpo.wpms.common.domain.dto.GroupedCodeResponseDTO;
import wpo.wpms.common.mapper.CmmnCodeMapper;
import wpo.wpms.common.service.CmmnCodeService;
@ExtendWith(MockitoExtension.class)
class CmmnCodeServiceTest {
@Mock
private CmmnCodeMapper mapper;
@InjectMocks
private CmmnCodeService service;
@Test
@DisplayName("그룹별 공통코드 묶기")
void getGroupedCodes() {
// given
List<CmmnCodeDTO> mockData = List.of(
createCmmnCodeDTO("position", "dev", "개발자"),
createCmmnCodeDTO("position", "design", "디자이너"),
createCmmnCodeDTO("skill_level", "senior", "고급")
);
when(mapper.selectCodesByTypes(List.of("position", "skill_level")))
.thenReturn(mockData);
// when
List<GroupedCodeResponseDTO> result = service.getGroupedCodes(List.of("position", "skill_level"));
// then
assertThat(result).hasSize(2);
Map<String, List<String>> resultMap = result.stream()
.collect(Collectors.toMap(
GroupedCodeResponseDTO::getGroupCode,
group -> group.getCodes().stream().map(GroupedCodeResponseDTO.CodeItem::getCode).toList()
));
assertThat(resultMap.get("position")).containsExactlyInAnyOrder("dev", "design");
assertThat(resultMap.get("skill_level")).containsExactly("senior");
}
private CmmnCodeDTO createCmmnCodeDTO(String type, String value, String name) {
CmmnCodeDTO code = new CmmnCodeDTO();
code.setCodeTy(type);
code.setCodeValue(value);
code.setCodeNm(name);
return code;
}
}
﹡GroupedCodeResponseDTO
public class GroupedCodeResponseDTO {
private String groupCode;
private List<CodeItem> codes;
public static class CodeItem {
private String code;
private String name;
}
}
1️⃣ Collectors.toMap 사용법 하나하나 뜯어보기
Map<String, List<String>> resultMap = result.stream()
.collect(Collectors.toMap(
GroupedCodeResponseDTO::getGroupCode,
group -> group.getCodes().stream().map(GroupedCodeResponseDTO.CodeItem::getCode).toList()
));
이 코드는 List<GroupedCodeResponseDTO> → Map<String, List<String>>으로 바꾸는 과정이다.
🔸 단계별 설명
단계 | 설명 |
result.stream() | List<GroupedCodeResponseDTO>를 스트림으로 변환. |
Collectors.toMap() | 스트림을 Map으로 변환하기 위한 Collector를 사용. |
GroupedCodeResponseDTO::getGroupCode | Map의 key로 사용할 값. 즉, "position", "skill_level" 같은 그룹 코드. |
group -> group.getCodes().stream().map(GroupedCodeResponseDTO.CodeItem::getCode).toList() | Map의 value에 들어갈 List<String>을 만듭니다. 코드 목록("dev", "design", "senior" 등)을 뽑아낸다. |
✅ GroupedCodeResponseDTO.CodeItem::getCode란?
이건 사실 아래 코드를 간단히 줄여 쓴 것이다.
item -> item.getCode()
즉, CodeItem 객체에서 getCode() 메서드를 호출해 반환값을 얻는 람다 표현식.
📌 어떻게 작동하냐면:
예를 들어 다음과 같은 리스트가 있다고 하자:
List<CodeItem> codes = List.of(
new CodeItem("dev"),
new CodeItem("design"),
new CodeItem("pm")
);
그리고 이 리스트에서 code 값만 뽑고 싶다면:
1. 람다 방식
List<String> codeStrings = codes.stream()
.map(item -> item.getCode())
.toList();
2. 메서드 레퍼런스 방식
List<String> codeStrings = codes.stream()
.map(GroupedCodeResponseDTO.CodeItem::getCode)
.toList();
동일한 결과가 나오고, 두 번째 방식이 더 간결.
💡 왜 이렇게 쓸까?
- 반복되는 람다 (x -> x.getSomething())를 간결하게 표현하려고
- 코드를 읽기 쉽게 만들기 위해
🔧 구조로 보면
public class GroupedCodeResponseDTO {
private String groupCode;
private List<CodeItem> codes;
public static class CodeItem {
private String code;
public String getCode() {
return code;
}
}
}
여기서 CodeItem::getCode는 Function<CodeItem, String> 타입의 함수로 취급된다.
즉, CodeItem을 받아서 그 안의 code 값을 리턴하는 함수.
💡 예시 변환 결과
[
{
groupCode: "position",
codes: [
{ code: "dev", name: "개발자" },
{ code: "design", name: "디자이너" }
]
},
{
groupCode: "skill_level",
codes: [
{ code: "senior", name: "고급" }
]
}
]
{
"position": ["dev", "design"],
"skill_level": ["senior"]
}
2️⃣ @ExtendWith(MockitoExtension.class) + @Mock + @InjectMocks 이해하기
🔹 @ExtendWith(MockitoExtension.class)
JUnit5에서 Mockito를 쓸 수 있도록 확장해주는 애너테이션.
@Mock, @InjectMocks 등을 인식하게 해준다.
🔹 @Mock
@Mock
private CmmnCodeMapper mapper;
- 실제 Mapper 대신 가짜(mock) 객체를 생성해준다.
- DB에 접근하지 않고도 when(mapper.xxx).thenReturn(...) 식으로 원하는 값을 직접 지정할 수 있다.
🔹 @InjectMocks
@InjectMocks
private CmmnCodeService service;
- CmmnCodeService를 생성하면서, @Mock으로 만들어진 객체(mapper)를 주입한다.
- 즉, 의존성을 자동으로 넣어준다. (수동으로 new 하지 않아도 됨).
3️⃣ collect(Collectors.toMap(...)) vs map()
🔹 map()은 변환
list.stream().map(x -> x.toUpperCase()).toList();
- 각각의 원소를 하나씩 변환한 결과를 새로운 List로 리턴
- 1:1 매핑
🔹 collect(Collectors.toMap(...))은 변환 + 집계
stream.collect(Collectors.toMap(keyFunction, valueFunction));
- 스트림의 각 요소를 key/value 쌍으로 변환해서 Map으로 집계
- 즉, List → Map
항목 | map() | collect(Collectors.toMap()) |
목적 | 요소 변환 (1:1) | Map으로 변환 (집계) |
리턴 타입 | Stream<R> | Map<K, V> |
사용 시점 | 중간 연산 (intermediate operation) | 종단 연산 (terminal operation) |
예: 문자열 길이 뽑기 | map(String::length) | toMap(str -> str, str -> str.length()) |
✅ 마무리 요약
개념 | 한 줄 요약 |
@Mock | 테스트용 가짜 객체 |
@InjectMocks | 가짜 객체를 자동으로 주입 |
@ExtendWith(MockitoExtension.class) | Mockito 기능 활성화 |
map() | 요소 하나씩 변환 |
Collectors.toMap() | 스트림을 Map으로 바꿈 |
'spring' 카테고리의 다른 글
스프링시큐리티 [SpringBoot 3.x (Spring Security 6.x)] (0) | 2025.04.28 |
---|---|
Mockito (0) | 2025.04.07 |
싱글톤 Context에서 전략 패턴 사용 시 동시성 문제 (0) | 2025.03.27 |
ThreadLocal, 안 쓰면 큰일 나는 이유 (멀티스레드 동시성 문제 해결법) (0) | 2025.03.20 |
스프링 MVC 학습목록 (2) (0) | 2025.01.08 |
댓글