🎯 Mockito @Captor 애노테이션 정리

- 호출된 메서드의 인자를 캡처하고 검증하는 도구


✅ 1. @Captor란?

@Captor는 Mockito에서 메서드가 호출될 때 전달된 인자(argument)를 캡처하기 위한 어노테이션입니다.
내부적으로는 ArgumentCaptor<T> 객체를 생성해주며, 테스트 대상이 호출한 메서드의 인자를 검증할 수 있게 해줍니다.


💡 왜 사용하나요?

  • 단순히 verify()만으로는 인자 값이 무엇이었는지 정확히 확인하기 어려움
  • 테스트 대상 내부에서 만들어진 객체를 외부로 전달할 때 그 전달된 객체의 필드 값을 검증하고 싶을 때 사용

🧩 2. 기본 사용 방법

📌 의존성 (Gradle 기준)

dependencies {
    testImplementation 'org.mockito:mockito-core'
    testImplementation 'org.mockito:mockito-junit-jupiter' // JUnit 5 사용 시
}

📌 예제 코드

@ExtendWith(MockitoExtension.class)
class MemberServiceTest {

    @Mock
    private MemberRepository memberRepository;

    @InjectMocks
    private MemberService memberService;

    @Captor
    private ArgumentCaptor<Member> memberCaptor;

    @Test
    void 회원가입_테스트() {
        // when
        memberService.join("철수");

        // then
        verify(memberRepository).save(memberCaptor.capture());
        Member captured = memberCaptor.getValue();
        assertEquals("철수", captured.getName());
    }
}

🧠 흐름 요약

  1. memberService.join("철수") 호출
  2. 내부적으로 memberRepository.save(new Member("철수")) 실행
  3. verify()로 save()가 호출됐는지 확인
  4. @Captor로 넘겨진 인자(Member)를 꺼내서 검증

🔍 @Captor vs ArgumentCaptor.forClass(...)

방식 설명
@Captor 선언만 해두면 Mockito가 자동으로 초기화
ArgumentCaptor.forClass() 직접 객체 생성 (더 번거로움)
// 수동 방식
ArgumentCaptor<Member> captor = ArgumentCaptor.forClass(Member.class);
verify(repo).save(captor.capture());

✅ 가독성과 유지보수 면에서는 @Captor 방식이 더 선호됨


⚠️ 사용 시 주의사항

항목 설명
@Captor 초기화 @ExtendWith(MockitoExtension.class) 또는 MockitoAnnotations.openMocks(this) 필요
제네릭 타입 명확히 지정 ArgumentCaptor<List<String>>처럼 사용할 때는 타입 명확히 작성 필요
capture()는 verify()와 함께 verify(mock).method(captor.capture()) 형태로 같이 사용해야 함

🧪 실전 상황 예시: 이벤트 발행 여부 검증

@Captor
ArgumentCaptor<Event> eventCaptor;

@Test
void 이벤트_검증() {
    service.doSomething();
    verify(eventPublisher).publish(eventCaptor.capture());

    Event captured = eventCaptor.getValue();
    assertEquals("SOME_TYPE", captured.getType());
}

✔ 서비스 내부에서 정확한 이벤트가 발행되었는지 확인할 수 있음


✅ 마무리 정리

항목 설명
어노테이션 @Captor
제공자 Mockito
목적 메서드 호출 시 전달된 인자 캡처 및 검증
내부 객체 ArgumentCaptor<T>
초기화 방법 MockitoExtension or openMocks() 필요
사용 위치 @Mock, @InjectMocks와 함께 사용하는 테스트 클래스 내부

 

@Captor는 단순히 메서드 호출을 "했는가"를 넘어서
"무엇을" 호출했는지를 정밀하게 검증하는 데 필수적인 도구입니다.
특히 객체 생성 후 전달하는 로직이 많은 도메인에서는 매우 유용하게 쓰입니다. 😊

 

'방법론 > Test' 카테고리의 다른 글

ReflectionTestUtils  (2) 2025.07.16

🔍 ReflectionTestUtils란?

- 테스트에서 private 필드와 메서드도 강제로 다뤄야 할 때!


✅ 1. ReflectionTestUtils란?

ReflectionTestUtils는 Spring Test 모듈에서 제공하는 유틸리티 클래스입니다.
주로 테스트 코드에서 접근 제한(private) 되어 있는 필드나 메서드를
리플렉션(reflection)을 이용해 강제로 접근하거나 값을 설정할 때 사용됩니다.

📍 의존성 추가 (Gradle)

dependencies {
    testImplementation 'org.springframework:spring-test'
}
  • Spring Boot를 사용 중이라면 별도 추가 필요 없음
    → spring-boot-starter-test 안에 이미 포함돼 있습니다.
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

✔ 위 starter에는 spring-test, junit, mockito, assertj 등 대부분의 테스트 유틸리티가 함께 포함되어 있어 별도로 추가하지 않아도 됩니다.


💡 왜 필요할까?

  • 객체의 private 필드 값을 강제로 세팅해야 할 때
  • setter나 생성자가 없는 클래스를 테스트해야 할 때
  • 캡슐화된 내부 상태를 검증하거나 조작해야 할 때

즉, 테스트 목적상 "강제 접근" 이 필요한 경우에 사용하는 도구입니다.


🧩 2. 주요 기능 요약

메서드 설명
setField() private 필드의 값을 강제로 설정
getField() private 필드 값을 강제로 가져옴
invokeMethod() private 메서드를 실행함

🧪 3. 사용 예제

📍 예제 클래스

public class UserService {
    private String secretKey = "default";

    private String encode(String value) {
        return value + ":" + secretKey;
    }
}

📌 setField() — 필드 값 강제 주입

UserService service = new UserService();

// private 필드 secretKey 값 변경
ReflectionTestUtils.setField(service, "secretKey", "newKey");

📌 getField() — 필드 값 읽기

String key = (String) ReflectionTestUtils.getField(service, "secretKey");
System.out.println(key); // newKey

📌 invokeMethod() — private 메서드 실행

String result = (String) ReflectionTestUtils.invokeMethod(service, "encode", "token");
System.out.println(result); // token:newKey

🛠 4. 생성자 없는 클래스 테스트

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Token {
    private String value;
}

➡ 테스트 코드에서 강제로 인스턴스 생성 및 필드 주입 가능:

Token token = new Token();
ReflectionTestUtils.setField(token, "value", "abc123");

⚠️ 5. 주의사항

항목 설명
리플렉션은 느리다 성능이 중요한 상황에는 지양
캡슐화 깨짐 실제 코드에서는 절대 사용 금지 (테스트 전용 도구)
필드명/메서드명 오타 주의 컴파일러가 체크하지 않음 (런타임 오류)

✅ 마무리 정리

항목 설명
클래스 org.springframework.test.util.ReflectionTestUtils
주요 용도 테스트에서 private 필드/메서드 조작
주요 메서드 setField(), getField(), invokeMethod()
주 사용 상황 생성자/세터 없는 객체, 내부 로직 검증, 레거시 테스트
주의사항 오용 주의, 성능 이슈, 리플렉션 한계 인식 필요

 

ReflectionTestUtils는 테스트의 "편법"이자 "마지막 수단"입니다.
하지만 꼭 필요한 경우에만 제한적으로 사용하면,
접근할 수 없었던 코드의 테스트 가능성을 넓혀주는 강력한 도구가 될 수 있습니다. 🛠

'방법론 > Test' 카테고리의 다른 글

Mockito @Captor 애노테이션  (1) 2025.07.16

📄 Spring Data JPA 페이징 처리 완벽 정리

- Pageable, Page, Slice를 활용한 효율적인 Pagination


✅ 1. 왜 페이징 처리가 필요할까?

대량의 데이터를 한 번에 모두 불러오는 것은
📉 성능 저하, 😵 메모리 낭비, ⛔ 네트워크 부담을 일으킵니다.

👉 그래서 데이터를 페이지 단위로 나눠서 요청하는 페이징(Pagination)이 필요합니다!


🔧 2. Spring Data JPA가 제공하는 페이징 기능

Spring Data JPA는 페이징을 위해 다음 두 가지를 제공합니다:

타입 특징
Pageable 페이징 조건 (page 번호, 크기, 정렬 기준 등)
Page<T> 페이징 결과 (컨텐츠 + 전체 개수 + 페이지 정보 포함)
Slice<T> 페이징 결과 (컨텐츠 + 다음 페이지 여부만 제공, count 쿼리 없음)

🧩 3. 페이징 기본 사용법

📍 ① Repository 메서드 정의

Page<Member> findByTeamName(String teamName, Pageable pageable);

✔ Pageable을 파라미터로 추가하면 자동으로 페이징 기능이 활성화됩니다.


📍 ② Controller에서 요청 받기

@GetMapping("/members")
public Page<MemberDto> getMembers(@RequestParam String teamName, Pageable pageable) {
    return memberRepository.findByTeamName(teamName, pageable)
                           .map(MemberDto::from);
}

✔ Pageable은 스프링이 자동으로 바인딩해줍니다.
예: /members?teamName=A&page=0&size=5&sort=age,desc


📍 ③ Page 객체로 응답 받기

Page는 다음 정보를 포함합니다:

  • getContent() : 현재 페이지의 데이터 리스트
  • getTotalElements() : 전체 데이터 수
  • getTotalPages() : 전체 페이지 수
  • getNumber() : 현재 페이지 번호
  • hasNext() / isFirst() / isLast() : 페이지 상태

🛠 4. 예제

📌 Repository

public interface MemberRepository extends JpaRepository<Member, Long> {
    Page<Member> findByTeamName(String teamName, Pageable pageable);
}

📌 Controller

@GetMapping("/members")
public Page<MemberDto> getMembers(@RequestParam String teamName, Pageable pageable) {
    return memberRepository.findByTeamName(teamName, pageable)
                           .map(MemberDto::from);
}

📌 Request URL 예시

/members?teamName=dev&page=0&size=5&sort=age,desc

🔄 5. Page, Slice, List 차이점

반환 타입 count 쿼리 다음 페이지 여부 전체 페이지 수
Page<T> ✅ 실행함 ✅ 포함 ✅ 포함
Slice<T> ❌ 없음 ✅ 포함 ❌ 없음
List<T> ❌ 없음 ❌ 없음 ❌ 없음
 

✔ Slice는 무한 스크롤이나 다음 페이지 여부만 필요한 경우에 적합


🎯 6. 정렬 사용하기

PageRequest.of(page, size, Sort.by(...))를 활용해 정렬도 적용할 수 있습니다.

Pageable pageable = PageRequest.of(0, 10, Sort.by("age").descending());

✔ 여러 필드 정렬도 가능:

Sort sort = Sort.by("teamName").ascending().and(Sort.by("age").descending());

🎨 7. 프론트 UI 연동 팁

  • Spring에서 Page<T>를 그대로 반환하면 JSON으로 다음과 같이 직렬화됨:
{
  "content": [...],
  "totalElements": 120,
  "totalPages": 12,
  "size": 10,
  "number": 0,
  "sort": {...},
  "first": true,
  "last": false
}

✔ 프론트에서는 이 데이터를 기반으로 페이지네이션 버튼이나 "다음" 버튼을 구성할 수 있습니다.


✅ 마무리 요약

항목 내용
페이징 도구 Pageable, Page<T>, Slice<T>
기본 사용 방식 리포지토리에서 Pageable 받는 메서드 선언
정렬 방식 URL 파라미터 or PageRequest.of(..., Sort)
반환 정보 Page는 전체 정보, Slice는 다음 페이지 여부 중심
활용 예 목록 조회, 검색 결과, 무한 스크롤 등
 

Spring Data JPA의 페이징은 단순한 기능 그 이상입니다.
성능 최적화와 사용자 경험을 모두 고려한 정교한 데이터 처리 전략입니다.

 

❓왜 HTML 폼 <form>은 PUT, DELETE 요청을 직접 보낼 수 없을까? - HTTP는 지원하지만, HTML은 제한한다?


✅ 1. 기본 개념: HTML <form>의 method 속성

HTML의 <form> 태그는 서버로 데이터를 전송할 때 사용하는 기본 요소입니다.

<form action="/users" method="post">
  <input type="text" name="username">
  <button type="submit">등록</button>
</form>

여기서 method는 전송 방식(HTTP 메서드)을 의미합니다.


📌 2. HTML에서 지원하는 method는 단 2개뿐

HTML 사양(HTML5 기준)에서는 <form> 태그의 method 속성에 다음 2가지만 허용합니다:

지원되는 method 의미
GET 조회 요청
POST 데이터 전송 요청

🔒 PUT, DELETE, PATCH는 명시적으로 허용되지 않음!
즉, 브라우저는 <form method="put"> 같은 걸 이해하지 못함


 

💡 왜 PUT과 DELETE는 <form>에서 지원되지 않을까?

📍 이유 1. HTML 표준의 역사적 제약

HTML은 처음부터 웹 폼을 간단한 데이터 입력용으로 설계했습니다.
초기 웹은 단순한 문서 공유 중심이었기 때문에 GET과 POST만으로 충분했습니다.
PUT, DELETE는 비교적 후대의 RESTful API 설계에서 더 중요하게 다뤄짐


📍 이유 2. 브라우저 구현 간의 호환성

  • 모든 브라우저에서 GET과 POST는 기본 동작으로 구현되어 있음
  • PUT, DELETE는 브라우저에서 직접 폼 요청으로 처리하지 않음
    → HTML 폼과 브라우저가 지원하지 않기 때문

🔄 3. 그렇다면 PUT/DELETE 요청은 어떻게 보낼까?

✅ 방법 1: JavaScript + Fetch / Axios

fetch('/users/1', {
  method: 'DELETE'
});

 

✔ 클라이언트에서 JS 코드로 명시적 요청 가능


✅ 방법 2: _method 파라미터와 서버측 변환 (Spring 방식)

Spring MVC 등에서는 HiddenHttpMethodFilter를 사용하여 POST 요청을 PUT, DELETE로 변환할 수 있습니다.

<form action="/users/1" method="post">
  <input type="hidden" name="_method" value="delete">
  <button type="submit">삭제</button>
</form>
// Spring Boot 설정에서 자동 등록됨
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new HiddenHttpMethodFilter();
}

✔ 서버가 이 hidden 필드를 보고 DELETE로 내부 처리함
✔ HTML 폼의 제한을 우회하는 대표적인 전략


🔧 4. Spring MVC 예시

@DeleteMapping("/users/{id}")
public String deleteUser(@PathVariable Long id) {
    userService.delete(id);
    return "redirect:/users";
}

위처럼 서버는 DELETE 매핑을 하고,
HTML은 POST 방식으로 보내되 hidden 필드로 _method=delete를 전송합니다.


📌 요약 정리

항목 설명
HTML <form>이 지원하는 method GET, POST 만 가능
PUT, DELETE가 안 되는 이유 HTML 표준 제한 + 브라우저 미지원
우회 방법 1 JavaScript로 요청 (fetch, axios 등)
우회 방법 2 _method 파라미터 + 서버 필터 사용 (Spring 등)
실제 권장 방식 REST API는 JS 요청, 폼 기반 앱은 _method 전략 사용

✅ 마무리

HTML 폼은 간단한 입력 처리를 위해 만들어졌기 때문에
HTTP 전체 메서드를 직접 지원하지 않습니다.
하지만, RESTful 설계를 지키기 위해
Spring과 같은 프레임워크는 이를 보완할 수단을 제공합니다.

 

실무에서는 HTML 폼으로는 POST까지만,
그 외 HTTP 메서드는 JS 기반 요청이나 서버 필터 우회 전략을 사용하세요. 💡

'백엔드' 카테고리의 다른 글

객체지향 프로그래밍(OOP)  (0) 2024.12.03

🔍 JPA findById() vs getReferenceById() 완벽 정리

- 둘 다 "ID로 조회"하지만, 작동 방식은 완전히 다르다?

 

✅ 개념부터 정리

메서드 설명
findById(id) 데이터베이스에서 실제 엔티티를 즉시 조회
getReferenceById(id) 프록시 객체를 반환하고, 실제 접근 시점에 DB 조회 (지연 로딩)

 


📌 findById(): 즉시 로딩 (Eager Loading)

Optional<Member> memberOpt = memberRepository.findById(1L);
Member member = memberOpt.get(); // 여기서 DB 즉시 조회
  • findById()는 바로 데이터베이스에 쿼리를 날리고, 엔티티 객체를 반환
  • 존재하지 않는 ID이면 Optional.empty() 반환
  • 즉시 로딩이라 N+1 문제 가능성 있음

📌 getReferenceById(): 지연 로딩 (Lazy Loading)

Member proxy = memberRepository.getReferenceById(1L); // 쿼리 X (프록시 반환)
String name = proxy.getName(); // 이 시점에 실제 DB 조회 발생!
  • Hibernate 프록시 객체 반환
  • 실제 데이터를 사용할 때까지는 DB 쿼리를 보내지 않음
  • 마치 가짜 객체처럼 보이지만, 내부에서 실제 엔티티로 연결됨

⚠️ 주요 차이점 요약

항목 findById() getReferenceById()
즉시 DB 조회 ✅ O ❌ X
프록시 반환 ❌ 아니오 ✅ 예
사용 즉시 값 확인 가능 ✅ 예 ❌ 프록시 초기화 필요
존재하지 않는 ID일 경우 Optional.empty() ❗ 런타임 오류 (EntityNotFoundException)
언제 사용? 값이 필요할 때 ID만 필요할 때 or 참조만 필요할 때

 

🧠 왜 getReferenceById를 쓸까?

Order order = new Order();
order.setMember(memberRepository.getReferenceById(1L)); // 단순 참조만 필요할 때

orderRepository.save(order); // 실제로는 member의 전체 정보가 필요 없음

✔ 성능 최적화를 위해 전체 데이터를 조회하지 않고,
ID 기반 참조만 필요한 상황에서는 getReferenceById()가 더 효율적입니다.


🧪 실전 예제 비교

log.info("=== findById ===");
Member m1 = memberRepository.findById(1L).get(); // DB 접근 O
log.info("이름: {}", m1.getName());

log.info("=== getReferenceById ===");
Member m2 = memberRepository.getReferenceById(1L); // DB 접근 X
log.info("이름: {}", m2.getName()); // 이 시점에 DB 접근

💣 주의해야 할 점

  • getReferenceById()로 가져온 프록시는 트랜잭션 범위를 벗어나면 LazyInitializationException 발생
    → 즉, 프록시는 DB와 연결이 살아있을 때만 안전하게 사용 가능
  • 존재하지 않는 ID를 getReferenceById()로 가져오면 실제로 사용할 때 예외 발생
Member m = memberRepository.getReferenceById(999L); // 여기선 오류 없음
m.getName(); // ❗ 여기서 EntityNotFoundException 발생

 

✅ 마무리 정리

구분 findById(id) getReferenceById(id)
DB 조회 시점 즉시 지연 (프록시 초기화 시점)
반환 객체 실제 엔티티 프록시 객체
반환 타입 Optional<T> T
성능 측면 다소 부담 가볍고 빠름 (단, 사용 시점에 주의)
사용 시점 실제 값이 필요할 때 단순 참조 or ID만 필요할 때

 

findById()는 "바로 쓰는 엔티티",
getReferenceById()는 "나중에 꺼내는 참조권"이라고 생각하면 이해하기 쉽습니다.

 

실제 프로젝트에서는 두 메서드를 필요에 따라 전략적으로 사용하여
불필요한 쿼리를 줄이고 성능을 최적화할 수 있습니다. 💡

 

 

🔀 Java의 정렬 방법 총정리

- Comparable, Comparator부터 Collections.sort, Stream.sorted까지


✅ 자바에서 정렬이 필요한 이유

데이터를 다루는 대부분의 프로그램에서는 정렬이 핵심 작업입니다.
Java에서는 다양한 방식으로 정렬을 지원하며,
기본 타입부터 사용자 정의 객체까지 정렬이 가능합니다.


📚 자바의 대표적인 정렬 방법

정렬 방법 설명
Arrays.sort() 배열 정렬
Collections.sort() 리스트 정렬
List.sort() 자바 8 이후 리스트 정렬 메서드
Stream.sorted() 스트림에서 정렬
PriorityQueue 자동 정렬 큐
TreeSet, TreeMap 정렬이 보장되는 컬렉션

 

🧩 1. 기본 정렬: Arrays.sort() & Collections.sort()

📌 배열 정렬

int[] arr = {5, 2, 4, 1};
Arrays.sort(arr); // 오름차순 정렬
System.out.println(Arrays.toString(arr)); // [1, 2, 4, 5]

📌 리스트 정렬

List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names); // 오름차순
System.out.println(names); // [Alice, Bob, Charlie]

 


🧠 2. 사용자 정의 정렬: Comparable & Comparator

📍 Comparable (내부 정렬 기준 정의)

class Person implements Comparable<Person> {
    String name;
    int age;

    public int compareTo(Person o) {
        return this.age - o.age; // 나이 오름차순
    }
}
  • Collections.sort() 또는 Arrays.sort()에서 자동 사용됨

📍 Comparator (외부 정렬 기준 지정)

List<Person> list = ...
list.sort(Comparator.comparingInt(p -> p.age));

또는

list.sort((p1, p2) -> p2.age - p1.age); // 나이 내림차순

✔ 정렬 기준을 유연하게 바꾸고 싶다면 Comparator 사용!


⚙️ 3. Java 8+ 스타일 정렬

📍 List.sort()

list.sort(Comparator.comparing(Person::getName));

📍 Stream.sorted()

List<Person> result = list.stream()
    .sorted(Comparator.comparing(Person::getAge))
    .collect(Collectors.toList());

🔎 스트림 안에서도 정렬 가능 — 특히 필터, 맵핑, 정렬 후 수집하는 경우 유용


 

🔁 4. 정렬이 유지되는 자료구조

자료구조 특징
PriorityQueue 자동 정렬 큐 (min-heap 구조)
TreeSet 자동 정렬된 Set
TreeMap 자동 정렬된 Map
Set<Integer> set = new TreeSet<>();
set.add(3); set.add(1); set.add(2);
System.out.println(set); // [1, 2, 3]
 

🧪 실전 예제: 객체 리스트 정렬

class Student {
    String name;
    int score;
}

List<Student> students = ...

// 점수 기준 오름차순
students.sort(Comparator.comparingInt(s -> s.score));

// 점수 기준 내림차순 + 이름 기준 오름차순
students.sort(Comparator.comparingInt(Student::getScore).reversed()
                        .thenComparing(Student::getName));

 

✅ 마무리 요약

정렬 도구 용도
Arrays.sort() 배열 정렬
Collections.sort() 리스트 정렬
Comparator, Comparable 사용자 정의 정렬 기준
List.sort(), Stream.sorted() 자바 8 이후의 정렬 방식
PriorityQueue, TreeSet 자동 정렬 유지되는 컬렉션

 

자바는 정렬을 위한 다양한 도구와 전략을 제공합니다.
목적에 맞게 정렬 기준을 정의하고,
성능과 가독성 모두를 챙긴 코드를 작성해보세요! 😊

+ Recent posts