[SB] 33. Spring 코딩 테스트 정리

최재원's avatar
Jun 11, 2025
[SB] 33. Spring 코딩 테스트 정리
테스트 코드 수정 사항
💡
테스트 코드 수정 사항
레포지토리 옵셔널
DTO는 공유 되면 공유하기 update랑 select 랑 똑같다
직접 응답 보낼때 write 만 작성하면 flush 하지 않는다 마지막 문자열에 \n을 넣던가 아니면 println을 사용하자 처리 로직이 있다면 메서드로 분리하자 이넘을 사용할 수 있으면 문자열 말고 이넘 쓰자 (컨텐트타입도 이넘이 있다)
리스폰스 엔티티 가 그냥 데이터를 리턴하기 때문에 @responsebody 를 붙일 필요 없다 예외처리할 때 400 401 403 404 뺀 마지막 처리에는 e.message를 사용하면 안됀다 그냥 내가 '임의로 알 수 없는 오류입니다' 라고 적어야 한다 필요없는 401 403 은 작성할 필요 없다
로또 스케줄 서비스 테스트 코드는 서비스 내부 코드를 그대로 긁어와서 사용한다 당첨번호도 임의로 만들고 db에서 조회하는 로또번호도 임의로 만들어서 이 번호가 다음 로직이 지나면 5등이 된다 아니면 저장이 안된다 등 이런방식으로 테스트 한다
 
💡
나중에 배치를 하게 되면 엄청난 양의 데이터를 돌리기 때문에 아예 자바 파일로 따로 배치 코드를 작성하고 서버에서 그 자바 파일(.jar)을 실행하는 로직을 사용한다

1. 시작하기

1.1. 개발 환경

co.kr.metacoding.backendtest 프로젝트 생성
  • OpenJDK 17
notion image
notion image
  • Spring Boot 3.2.1

1.2. 라이브러리

  • Spring Web
  • Lombok
  • H2 Database ( ID : pc, PW : 2024 )
  • 그 외 필요한 라이브러리는 build.gradle에 추가하시면 됩니다.
라이브러리 추가 시, 어떠한 이유로 추가했는지 Pull Request에 간단히 적어주시면 됩니다.
notion image
 

2.2. 기본 문제 (50점)

1. 라이브러리 추가

  • JPA(ORM)를 사용하기 위해 해당 jpa 스타터 패키지를 설치
    • implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

2. application.properties 작성

server.port=8080 # vscode console highlight spring.output.ansi.enabled=always # utf-8 server.servlet.encoding.charset=utf-8 server.servlet.encoding.force=true # DB spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:test spring.datasource.username=pc spring.datasource.password=2024 spring.h2.console.enabled=true # JPA table create or none spring.jpa.hibernate.ddl-auto=create # query log spring.jpa.show-sql=true # dummy data spring.sql.init.data-locations=classpath:db/data.sql # create dummy data after ddl-auto create spring.jpa.defer-datasource-initialization=true # sql formatter spring.jpa.properties.hibernate.format_sql=true

3. User 엔티티 작성

package co.kr.metacoding.backendtest.user; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @NoArgsConstructor @Getter @Table(name = "user_tb") @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Builder public User(Long id, String name) { this.id = id; this.name = name; } }

4. User 더미 생성

insert into user_tb(name) values ('ssar'), ('cos');

5. 폴더 구조 생성

notion image

6. utils 생성

  • Resp
    • package co.kr.metacoding.backendtest._core.utils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import java.util.HashMap; import java.util.Map; public class Resp { /** * 성공 응답 생성 메서드 * <p> * HTTP 상태코드 200(OK)와 함께 body를 반환합니다. * * @param body 클라이언트에게 응답할 데이터 * @param <T> 응답 데이터의 타입 (예: DTO, Map 등) * @return ResponseEntity<T> - 상태코드 200 + body 포함 */ public static <T> ResponseEntity<T> ok(T body) { return ResponseEntity.ok(body); } /** * 실패 응답 생성 메서드 * <p> * HTTP 상태코드와 실패 사유(reason)를 담아 클라이언트에게 반환합니다. * * @param status 응답할 HTTP 상태 코드 (예: 400, 403, 404 등) * @param reason 클라이언트에게 전달할 실패 사유 메시지 * @return ResponseEntity<Map < String, String>> - 상태코드 + reason 포함 */ public static ResponseEntity<Map<String, String>> fail(HttpStatus status, String reason) { Map<String, String> body = new HashMap<>(); body.put("reason", reason); return ResponseEntity.status(status).body(body); } }
    • ResponseEntity 객체를 return

user 등록 API 구현 (8점)

  • /users/{id} API를 호출하면, {"id": ?, "name": "?"}을 응답한다.
  • /users/{id} API에 대한 통합 테스트 코드 작성

1. API 로직 구현

  1. UserController 구현
    1. @RequiredArgsConstructor @RestController public class UserController { private final UserService userService; @PostMapping("/users") public ResponseEntity<?> saveUser(@RequestBody UserRequest.SaveDTO reqDTO) { UserResponse.SaveDTO respDTO = userService.saveUser(reqDTO); return Resp.ok(respDTO); } }
  1. UserRequest 구현
    1. public class UserRequest { @Data public static class SaveDTO { private String name; public User toEntity() { return User.builder().name(name).build(); } } }
  1. UserResponse 구현
    1. public class UserResponse { @Data public static class SaveDTO { private Long id; public SaveDTO(User user) { this.id = user.getId(); } } }
  1. UserService 구현
    1. @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; @Transactional public UserResponse.SaveDTO saveUser(UserRequest.SaveDTO reqDTO) { User user = reqDTO.toEntity(); User userPS = userRepository.save(user); return new UserResponse.SaveDTO(userPS); } }
  1. UserRepository 구현
    1. @RequiredArgsConstructor @Repository public class UserRepository { private final EntityManager em; public User save(User user) { em.persist(user); return user; } }

2. 통합테스트 구현

  1. 통합테스트 코드 설정
    1. /** * UserController의 REST API 테스트 클래스입니다. * <p> * - 각 테스트는 통합 테스트로 실행되며, MockMvc를 이용하여 컨트롤러의 실제 동작을 검증합니다. * <p> * * @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다. */ @Transactional /** * @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함. * (내부적으로 @SpringBootConfiguration, @EnableAutoConfiguration 등을 포함) */ @SpringBootTest /** * @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌. * (MockMvc는 실제 HTTP 요청 없이 컨트롤러 계층 테스트 가능) */ @AutoConfigureMockMvc public class UserControllerTest { @Autowired private MockMvc mvc; @Autowired private ObjectMapper om; }
      • @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다.
      • @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.
      • @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌.
  1. Controller의 saveUser 테스트
    1. /** * [성공 테스트] 새로운 유저를 정상적으로 등록할 수 있어야 함. */ @Test public void save_user_test() throws Exception { // given UserRequest.SaveDTO reqDTO = new UserRequest.SaveDTO(); reqDTO.setName("test"); String requestBody = om.writeValueAsString(reqDTO); System.out.println("requestBody: " + requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .post("/users") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println("responseBody: " + responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(3)); }

user 조회 API 구현 (8점)

  • /users/{id} API를 호출하면, {"id": ?, "name": "?"}을 응답한다.
  • /users/{id} API에 대한 통합 테스트 코드 작성

1. API 로직 구현

  1. UserController 구현
    1. @RequiredArgsConstructor @RestController public class UserController { private final UserService userService; @GetMapping("/users/{id}") public ResponseEntity<?> getUser(@PathVariable("id") Integer id) { UserResponse.DTO respDTO = userService.getUser(id); return Resp.ok(respDTO); } }
  1. UserResponse 구현
    1. public class UserResponse { @Data public static class DTO { private Integer id; private String name; public DTO(User user) { this.id = user.getId(); this.name = user.getName(); } } }
  1. UserService 구현
    1. @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; public UserResponse.DTO getUser(Integer id) { User userPS = userRepository.findById(id); return new UserResponse.DTO(userPS); } }
  1. UserRepository 구현
    1. @RequiredArgsConstructor @Repository public class UserRepository { private final EntityManager em; public Optional<User> findById(Integer id) { return Optional.ofNullable(em.find(User.class, id)); } }
      • Optional 을 사용해 null 처리할 수 있도록 작성

2. 통합테스트 구현

  1. 통합테스트 코드 설정
    1. /** * UserController의 REST API 테스트 클래스입니다. * <p> * - 각 테스트는 통합 테스트로 실행되며, MockMvc를 이용하여 컨트롤러의 실제 동작을 검증합니다. * <p> * * @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다. */ @Transactional /** * @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함. * (내부적으로 @SpringBootConfiguration, @EnableAutoConfiguration 등을 포함) */ @SpringBootTest /** * @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌. * (MockMvc는 실제 HTTP 요청 없이 컨트롤러 계층 테스트 가능) */ @AutoConfigureMockMvc public class UserControllerTest { @Autowired private MockMvc mvc; @Autowired private ObjectMapper om; }
      • @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다.
      • @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.
      • @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌.
  1. Controller의 getUser 테스트
    1. /** * [성공 테스트] id로 유저를 정상적으로 조회할 수 있어야 함. */ @Test public void get_user_test() throws Exception { // given Integer id = 1; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/users/{id}", id) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println("responseBody: " + responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("ssar")); }

user 수정 API 구현 (8점)

  • /users/{id} API를 호출하면, {"id": ?, "name": "?"}을 응답한다.
  • /users/{id} API에 대한 통합 테스트 코드 작성

1. API 로직 구현

  1. UserController 구현
    1. @RequiredArgsConstructor @RestController public class UserController { private final UserService userService; @PutMapping("/users/{id}") public ResponseEntity<?> updateUser(@PathVariable("id") Integer id, @RequestBody UserRequest.UpdateDTO reqDTO) { UserResponse.DTO respDTO = userService.updateUser(id, reqDTO); return Resp.ok(respDTO); } }
  1. UserRequest 구현
    1. public class UserRequest { @Data public static class UpdateDTO { private String name; } }
  1. UserResponse 구현
    1. public class UserResponse { @Data public static class DTO { private Integer id; private String name; public DTO(User user) { this.id = user.getId(); this.name = user.getName(); } } }
  1. UserService 구현
    1. @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; @Transactional public UserResponse.UpdateDTO updateUser(Integer id, UserRequest.UpdateDTO reqDTO) { User userPS = userRepository.findById(id); userPS.updateName(reqDTO.getName()); return new UserResponse.DTO(userPS); } }

2. 통합테스트 구현

  1. 통합테스트 코드 설정
    1. /** * UserController의 REST API 테스트 클래스입니다. * <p> * - 각 테스트는 통합 테스트로 실행되며, MockMvc를 이용하여 컨트롤러의 실제 동작을 검증합니다. * <p> * * @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다. */ @Transactional /** * @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함. * (내부적으로 @SpringBootConfiguration, @EnableAutoConfiguration 등을 포함) */ @SpringBootTest /** * @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌. * (MockMvc는 실제 HTTP 요청 없이 컨트롤러 계층 테스트 가능) */ @AutoConfigureMockMvc public class UserControllerTest { @Autowired private MockMvc mvc; @Autowired private ObjectMapper om; }
      • @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다.
      • @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.
      • @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌.
  1. Controller의 getUser 테스트
    1. /** * [성공 테스트] 유저의 이름을 정상적으로 수정할 수 있어야 함. */ @Test public void update_user_test() throws Exception { // given Integer id = 1; UserRequest.UpdateDTO reqDTO = new UserRequest.UpdateDTO(); reqDTO.setName("test"); String requestBody = om.writeValueAsString(reqDTO); System.out.println("requestBody: " + requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .put("/users/{id}", id) .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println("responseBody: " + responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("test")); }

필터 구현 (12점)

  • URL에 ? & = : //를 제외한 특수문자가 포함되어 있을 경우 접속을 차단하는 Filter 구현한다.
  • /users/{id}?name=test!! API 호출에 대한 통합 테스트 코드 작성
notion image

1. Filter 구현

  1. SpecialCharacterFilter 구현
    1. package co.kr.metacoding.backendtest._core.filter; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; public class SpecialCharacterFilter implements Filter { // 허용 문자 정규식 패턴 (영문자, 숫자, ? & = : / 만 허용) private static final Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9?&=:/]*$"); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse res = (HttpServletResponse) servletResponse; String uri = req.getRequestURI(); // 경로 (예: /api/user) String query = req.getQueryString(); // 쿼리 스트링 (예: id=123&name=abc) // 검사할 대상 문자열 생성 (URI + ? + query) String fullUrl = uri + (query == null ? "" : "?" + query); // h2-console 경로는 필터 통과 if (uri.startsWith("/h2-console")) { filterChain.doFilter(servletRequest, servletResponse); return; } // 정규식으로 허용되지 않은 문자 검사 if (!ALLOWED_PATTERN.matcher(fullUrl).matches()) { sendForbiddenResponse(res); return; } filterChain.doFilter(servletRequest, servletResponse); } /** * 상태코드 403 및 url 허용금지 메시지 응답 * * @param res * @throws IOException */ void sendForbiddenResponse(HttpServletResponse res) throws IOException { // 응답 상태코드 403 res.setStatus(HttpServletResponse.SC_FORBIDDEN); // utf-8 설정 res.setCharacterEncoding(StandardCharsets.UTF_8.name()); // json 응답 res.setContentType(MediaType.APPLICATION_JSON_VALUE); // 응답 본문 출력 및 flush PrintWriter writer = res.getWriter(); writer.println("{\"reason\": \"URL에 허용되지 않는 특수문자가 포함되어 있습니다.\"}"); // \n 이 명확하게 포함되야함 writer.flush(); // 명확하게 버퍼 응답 } }
      • 허용금지 url 요청시 응답 로직 분리
      • 응답 메시지 제외 모든 문자열은 이넘을 사용
  1. FilterConfig 구현
    1. package co.kr.metacoding.backendtest._core.config; import co.kr.metacoding.backendtest._core.filter.SpecialCharacterFilter; import lombok.RequiredArgsConstructor; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @RequiredArgsConstructor @Configuration public class FilterConfig { // URL에 허용되지 않은 특수문자가 포함된 요청을 차단하는 필터 등록 @Bean public FilterRegistrationBean<SpecialCharacterFilter> specialCharacterFilter() { FilterRegistrationBean<SpecialCharacterFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new SpecialCharacterFilter()); // 실제 필터 객체 등록 registrationBean.addUrlPatterns("/*"); // 모든 URL 요청에 필터 적용 registrationBean.setOrder(1); // 필터 실행 순서 설정 (숫자가 작을수록 먼저 실행) return registrationBean; } }
      • Spring Boot 애플리케이션의 서블릿 필터(Servlet Filter)를 등록
      • 스프링 애플리케이션 구동 시 이 FilterConfig 클래스가 @Configuration으로 스캔되어 빈으로 등록되고,
      • specialCharacterFilter() 메서드가 반환하는 FilterRegistrationBean 덕분에
        • 내장 톰캣(또는 외부 서블릿 컨테이너)의 필터 체인에 SpecialCharacterFilter가 등록됩니다.
      • 이후 요청이 들어올 때마다 톰캣이 서블릿 필터 체인 순서대로 필터를 호출하는데, order(1)이므로 가장 먼저 실행됩니다.

2. 통합테스트 구현

  1. 통합테스트 코드 설정
    1. /** * SpecialCharacterFilter의 통합 테스트 클래스입니다. * <p> * - MockMvc를 사용해 실제 컨트롤러 요청에 필터가 정상 동작하는지 검증합니다. * <p> * * @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다. */ @Transactional /** * @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함. * (내부적으로 @SpringBootConfiguration, @EnableAutoConfiguration 등을 포함) */ @SpringBootTest /** * @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌. * (MockMvc는 실제 HTTP 요청 없이 컨트롤러 계층 테스트 가능) */ @AutoConfigureMockMvc public class SpecialCharacterFilterTest { @Autowired private MockMvc mvc; }
      • @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다.
      • @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.
      • @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌.
  1. SpecialCharacterFilter 테스트
    1. /** * [실패 테스트] URL에 허용되지 않는 특수문자가 포함되어 필터에서 차단되어야 함. */ @Test public void do_filter_test() throws Exception { // given Integer id = 1; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/users/{id}?name=test!!", id) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.reason").value("URL에 허용되지 않는 특수문자가 포함되어 있습니다.")); }

Spring AOP를 활용한 로깅 구현 (14점)

  • user 등록, 조회, 수정 API에 대해 Request시 Console에 Client Agent를 출력한다.
  • AOP 사용을 위한 aop 스타터 패키지 설치
    • implementation 'org.springframework.boot:spring-boot-starter-aop'
notion image

1. AOP 구현

  1. LogHandler 구현
    1. package co.kr.metacoding.backendtest._core.log; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; /** * @Aspect: 이 클래스가 AOP(관점 지향 프로그래밍) Aspect임을 나타냅니다. * 특정 포인트컷에 대해 공통 관심 사항(Advice)을 적용할 수 있습니다. * @Component: 스프링 빈으로 등록되어 DI(의존성 주입) 및 관리가 가능하도록 합니다. * @RequiredArgsConstructor: final 또는 @NonNull 필드에 대해 생성자를 자동 생성합니다. * (Lombok 어노테이션) */ @Slf4j @Aspect @Component @RequiredArgsConstructor public class LogHandler { private final HttpServletRequest req; /** * @param joinPoint 실행되는 메서드의 join point 정보 * @LogUserAgent 애노테이션이 붙은 메서드 실행 전에 동작하는 AOP advice. * <p> * HTTP 요청 헤더에서 User-Agent 값을 가져와 로그로 출력합니다. */ @Before("@annotation(co.kr.metacoding.backendtest._core.log.anno.LogUserAgent)") public void logUserAgent(JoinPoint joinPoint) { // HTTP 요청 헤더에서 User-Agent 정보 추출 String userAgent = req.getHeader("User-Agent"); // User-Agent 정보 로그 출력 log.info("Client Agent: " + userAgent); } }
      • @Aspect: 이 클래스가 AOP(관점 지향 프로그래밍) Aspect임을 나타냅니다.
        • 특정 포인트컷에 대해 공통 관심 사항(Advice)을 적용할 수 있습니다.
      • @Component: 스프링 빈으로 등록되어 DI(의존성 주입) 및 관리가 가능하도록 합니다.
      • @RequiredArgsConstructor: final 또는 @NonNull 필드에 대해 생성자를 자동 생성합니다.
        • (Lombok 어노테이션)
      • log.info 를 사용해 로깅 구현
  1. LogUserAgent 어노테이션 구현
    1. package co.kr.metacoding.backendtest._core.log.anno; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 사용자 요청의 User-Agent 헤더를 로깅하기 위한 커스텀 어노테이션입니다. * <p> * 이 어노테이션은 메서드에만 적용 가능하며, 런타임 시 AOP 등을 통해 로깅 처리를 수행합니다. */ @Target(ElementType.METHOD) // 메서드에만 붙일 수 있음 @Retention(RetentionPolicy.RUNTIME) // 런타임에 동작하는 AOP 등에 사용할 수 있음 public @interface LogUserAgent { }
      • 포인트 컷에 사용할 커스텀 어노테이션

2. AOP 적용

  1. UserController
    1. @RequiredArgsConstructor @RestController public class UserController { private final UserService userService; @LogUserAgent @PostMapping("/users") public ResponseEntity<?> saveUser(@RequestBody UserRequest.SaveDTO reqDTO) { UserResponse.SaveDTO respDTO = userService.saveUser(reqDTO); return Resp.ok(respDTO); } @LogUserAgent @GetMapping("/users/{id}") public ResponseEntity<?> getUser(@PathVariable("id") Integer id) { UserResponse.DTO respDTO = userService.getUser(id); return Resp.ok(respDTO); } @LogUserAgent @PutMapping("/users/{id}") public ResponseEntity<?> updateUser(@PathVariable("id") Integer id, @RequestBody UserRequest.UpdateDTO reqDTO) { UserResponse.DTO respDTO = userService.updateUser(id, reqDTO); return Resp.ok(respDTO); } }
      • @LogUserAgent: 어노테이션을 붙여서 AOP 적용
  1. SpecialCharacterFilter 테스트
    1. /** * [실패 테스트] URL에 허용되지 않는 특수문자가 포함되어 필터에서 차단되어야 함. */ @Test public void do_filter_test() throws Exception { // given Integer id = 1; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/users/{id}?name=test!!", id) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.reason").value("URL에 허용되지 않는 특수문자가 포함되어 있습니다.")); }

2.1. 공통 (20점)

@ControllerAdvice, @ExceptionHandler를 이용하여, 잘못된 요청에 대한 응답을 처리한다. (4점)

notion image

1. ExceptionApi 구현

  1. ExceptionApi{Code} 구현
    1. public class ExceptionApi400 extends RuntimeException { public ExceptionApi400(String message) { super(message); } }
      • 오버라이드 메서드 생성자 추가
  1. GlobalExceptionHandler 구현
    1. @ControllerAdvice // 모든 컨트롤러에 대한 전역 예외 처리기 public class GlobalExceptionHandler { // 400 - 클라이언트의 잘못된 요청 처리 @ExceptionHandler(ExceptionApi400.class) // JSON 형식으로 응답 (뷰가 아닌 응답 본문으로 처리) public ResponseEntity<?> exApi400(ExceptionApi400 e) { return Resp.fail(HttpStatus.BAD_REQUEST, e.getMessage()); } // 404 - 요청한 리소스를 찾을 수 없음 @ExceptionHandler(ExceptionApi404.class) public ResponseEntity<?> exApi404(ExceptionApi404 e) { return Resp.fail(HttpStatus.NOT_FOUND, e.getMessage()); } // 그 외 알 수 없는 모든 예외 처리 @ExceptionHandler(Exception.class) public ResponseEntity<?> exUnknown(Exception e) { return Resp.fail(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 오류가 발생하였습니다. 관리자에게 문의 주세요."); } }
      • 필요한 응답만 작성
        • 400, 404
      • Exception.class 을 사용해 나머지 모든 예외 처리
        • 메시지는 보여주지 말아야함
      • 응답 코드는 이넘을 사용
      @ControllerAdvice는 전역 예외 처리기입니다.
      • 전역적으로 컨트롤러에서 발생하는 예외를 처리할 수 있게 도와주는 어노테이션입니다.
      • 하지만, 기본적으로는 @Controller 계열로 인식되기 때문에, 리턴값을 뷰 이름(view name)으로 해석합니다.
      ✅ 그래서 @ResponseBody를 붙여야 합니다.
      • 예외 처리 메서드의 리턴값이 뷰 이름이 아니라 JSON 등 응답 바디로 직렬화되길 원할 때, @ResponseBody가 필요합니다.
      • 이걸 붙이지 않으면 String, Map, ResponseEntity 등을 리턴해도 뷰 이름으로 오해해서 JSP나 타임리프 같은 뷰 템플릿을 찾으려고 합니다.
      ✅ 그러나 ResponseEntity<?>를 사용한다면 @ResponseBody 가 필요 없다
      • ResponseEntity는 Spring MVC에서 이미 응답 본문(body)을 직접 제어하는 객체입니다.
      • 따라서 @ResponseBody가 없어도, 반환된 ResponseEntity 객체 내부의 내용이 HTTP 응답 본문으로 바로 전송됩니다.
      • @ResponseBody는 보통 DTO, 문자열, 객체 등을 리턴할 때 뷰가 아닌 JSON/직접본문으로 변환하기 위해 붙이는데,
        • ResponseEntity를 반환하면 그 역할이 내장되어 있습니다.
      • @ResponseBody는 메서드 반환 값을 HTTP 응답 바디로 직렬화하도록 하는 역할
      • ResponseEntity는 HTTP 상태 코드, 헤더, 바디를 포함한 응답 전체를 제어하는 클래스
      • 그래서 ResponseEntity 반환 시 @ResponseBody는 불필요한 중복

      ✅ 요약
      어노테이션 조합
      설명
      @ControllerAdvice
      예외는 잡지만, 응답은 뷰 이름으로 처리됨
      @ControllerAdvice + @ResponseBody
      예외를 잡고, 응답을 JSON 등 데이터로 처리
      @RestControllerAdvice
      위 둘을 합친 축약형. 가장 많이 사용됨

2. 예외처리 적용

  1. UserService
    1. @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; @Transactional public UserResponse.SaveDTO saveUser(UserRequest.SaveDTO reqDTO) { // 1. name 체크 Optional<User> checkUser = userRepository.findByName(reqDTO.getName()); if (checkUser.isPresent()) { throw new ExceptionApi400("이미 존재하는 name 입니다"); } // 2. 유저 객체 생성 User user = reqDTO.toEntity(); // 3. 유저 저장 User userPS = userRepository.save(user); // 4. 유저 응답 return new UserResponse.SaveDTO(userPS); } public UserResponse.DTO getUser(Integer id) { // 1. 유저 조회 User userPS = userRepository.findById(id).orElseThrow(() -> new ExceptionApi404("존재하지 않는 user 입니다")); // 2. 유저 응답 return new UserResponse.DTO(userPS); } @Transactional public UserResponse.DTO updateUser(Integer id, UserRequest.UpdateDTO reqDTO) { // 1. name 체크 Optional<User> checkUser = userRepository.findByName(reqDTO.getName()); if (checkUser.isPresent() && checkUser.get().getName().equals(reqDTO.getName())) { throw new ExceptionApi400("이미 존재하는 name 입니다"); } // 2. 유저 조회 User userPS = userRepository.findById(id).orElseThrow(() -> new ExceptionApi404("존재하지 않는 user 입니다")); // 3. 이름 수정 userPS.updateName(reqDTO.getName()); // 4. 유저 응답 return new UserResponse.DTO(userPS); } }

3. API 실패 통합테스트 구현

  1. user
public class UserControllerTest { @Autowired private MockMvc mvc; @Autowired private ObjectMapper om; /** * [실패 테스트] 이미 존재하는 name으로 회원 가입 시 예외가 발생해야 함. */ @Test public void save_user_fail_test() throws Exception { // given UserRequest.SaveDTO reqDTO = new UserRequest.SaveDTO(); reqDTO.setName("ssar"); String requestBody = om.writeValueAsString(reqDTO); System.out.println("requestBody: " + requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .post("/users") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println("responseBody: " + responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.reason").value("이미 존재하는 name 입니다")); } /** * [실패 테스트] 존재하지 않는 id로 유저 조회 시 예외가 발생해야 함. */ @Test public void get_user_fail_test() throws Exception { // given Integer id = 5; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/users/{id}", id) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println("responseBody: " + responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.reason").value("존재하지 않는 user 입니다")); } /** * [실패 테스트] 이미 존재하는 name으로 수정하려 하면 예외가 발생해야 함. */ @Test public void update_user_fail_test() throws Exception { // given Integer id = 1; UserRequest.UpdateDTO reqDTO = new UserRequest.UpdateDTO(); reqDTO.setName("ssar"); String requestBody = om.writeValueAsString(reqDTO); System.out.println("requestBody: " + requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .put("/users/{id}", id) .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println("responseBody: " + responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.reason").value("이미 존재하는 name 입니다")); } }
  • 등록, 조회, 수정 실패 코드 작성

2.3. 구현 문제 (30점)

로또 번호 발급 API 구현 (10점)

  • POST /lottos API를 호출하면, {"numbers": [?, ?, ?, ?, ?, ?]}을 응답한다.
  • POST /lottos API에 대한 통합 테스트 코드 작성

Request

curl -X POST -H "Content-Type: application/json" http://localhost:8080/lottos

Response

{  "numbers": [?, ?, ?, ?, ?, ?] }
notion image

1. Lotto 엔티티 구현

package co.kr.metacoding.backendtest.lotto; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "lotto_tb") @NoArgsConstructor @Getter public class Lotto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "number_1") private Integer number1; @Column(name = "number_2") private Integer number2; @Column(name = "number_3") private Integer number3; @Column(name = "number_4") private Integer number4; @Column(name = "number_5") private Integer number5; @Column(name = "number_6") private Integer number6; @Builder public Lotto(Long id, Integer number1, Integer number2, Integer number3, Integer number4, Integer number5, Integer number6) { this.id = id; this.number1 = number1; this.number2 = number2; this.number3 = number3; this.number4 = number4; this.number5 = number5; this.number6 = number6; } }

2. API 로직 구현

  1. LottoController 구현
    1. @RequiredArgsConstructor @RestController public class LottoController { private final LottoService lottoService; @PostMapping("/lottos") public ResponseEntity<?> generateLottoNumbers() { LottoResponse.DTO respDTO = lottoService.generateLottoNumbers(); return Resp.ok(respDTO); } }
  1. LottoResponse 구현
    1. public class LottoResponse { @Data public static class DTO { private List<Integer> numbers; public DTO(Lotto lotto) { this.numbers = List.of( lotto.getNumber1(), lotto.getNumber2(), lotto.getNumber3(), lotto.getNumber4(), lotto.getNumber5(), lotto.getNumber6() ); } } }
  1. LottoService 구현
    1. @RequiredArgsConstructor @Service public class LottoService { private final LottoRepository lottoRepository; @Transactional public LottoResponse.DTO generateLottoNumbers() { // 1. 로또 번호 배열 생성 List<Integer> lottoNumbers = RandomNumberUtils.generateUniqueRandomNumbers(6, 1, 45); // 2. 로또 객체 생성 Lotto lotto = Lotto.builder() .number1(lottoNumbers.get(0)) .number2(lottoNumbers.get(1)) .number3(lottoNumbers.get(2)) .number4(lottoNumbers.get(3)) .number5(lottoNumbers.get(4)) .number6(lottoNumbers.get(5)) .build(); // 3. 로또 저장 Lotto lottoPS = lottoRepository.save(lotto); // 4. 로또 응답 return new LottoResponse.DTO(lottoPS); } }
  1. generateUniqueRandomNumbers 메서드 분리
    1. package co.kr.metacoding.backendtest._core.utils; import java.util.*; public class RandomNumberUtils { /** * 랜덤 숫자 배열 생성 * * @param count 생성할 숫자의 개수 * @param min 최소값 (포함) * @param max 최대값 (포함) * @return List<Integer> 고유한 랜덤 정수 리스트 */ public static List<Integer> generateUniqueRandomNumbers(int count, int min, int max) { Random random = new Random(); Set<Integer> numbers = new HashSet<>(); while (true) { numbers.add(random.nextInt(max - min + 1) + min); if (numbers.size() == count) break; } return new ArrayList<>(numbers); } }
  1. LottoRepository
    1. @RequiredArgsConstructor @Repository public class LottoRepository { private final EntityManager em; public Lotto save(Lotto lotto) { em.persist(lotto); return lotto; } }

3. 통합테스트 코드 구현

  1. 통합테스트 코드 세팅
    1. /** * LottoController의 REST API 테스트 클래스입니다. * <p> * - 각 테스트는 통합 테스트로 실행되며, MockMvc를 이용하여 컨트롤러의 실제 동작을 검증합니다. * <p> * * @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다. */ @Transactional /** * @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함. * (내부적으로 @SpringBootConfiguration, @EnableAutoConfiguration 등을 포함) */ @SpringBootTest /** * @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌. * (MockMvc는 실제 HTTP 요청 없이 컨트롤러 계층 테스트 가능) */ @AutoConfigureMockMvc public class LottoControllerTest { @Autowired private MockMvc mvc; @Autowired private ObjectMapper om;
  1. 통합테스트 코드 구현
    1. /** * [성공 테스트] 로또 번호 생성 시, 1~45 사이의 중복되지 않은 숫자 6개가 반환되어야 함. */ @Test public void generate_lotto_numbers_test() throws Exception { // given // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .post("/lottos") .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.numbers", hasSize(6))) // 숫자가 6개 있는지 .andExpect(MockMvcResultMatchers.jsonPath("$.numbers[*]", everyItem( allOf( greaterThanOrEqualTo(1), // 숫자가 1보다 같거나 큰지 lessThanOrEqualTo(45) // 숫자가 45보다 같거나 작은지 )))); }

로또 번호 당첨자 검수 Batch 구현 (20점)

  • 랜덤하게 로또 번호를 발급하여, 당첨 번호와 비교하여 당첨자를 검수하는 Batch를 구현한다.
    • 당첨자의 등수는 1등, 2등, 3등, 4등, 5등이 있다.
    • 당첨자의 등수는 당첨 번호와 일치하는 번호의 개수로 판단한다.
    • 당첨자 정보는 winner 테이블에 저장한다.
  • Batch는 매주 일요일 0시에 실행되도록 구현한다.
  • Batch에 대한 통합 테스트 코드 작성
  • Batch를 scheduler로 구현한다
notion image

1. Winner 엔티티 구현

package co.kr.metacoding.backendtest.lotto.winner; import co.kr.metacoding.backendtest.lotto.Lotto; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @NoArgsConstructor @Getter @Table(name = "winner_tb") @Entity public class Winner { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) private Lotto lotto; private Integer rank; // 1등, 2등, 3등, 4등, 5등 @Builder public Winner(Long id, Lotto lotto, Integer rank) { this.id = id; this.lotto = lotto; this.rank = rank; } }

2. Scheduler 구현

  1. Application 세팅
    1. package co.kr.metacoding.backendtest; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling @SpringBootApplication public class BackendtestApplication { public static void main(String[] args) { SpringApplication.run(BackendtestApplication.class, args); } }
      • @EnableScheduling: Spring에서 스케줄링 기능을 활성화할 때 사용하는 어노테이션
      • @Scheduled 애노테이션이 붙은 메서드들을 주기적으로 실행할 수 있도록 해주는 기능을 켜줍니다.
      • 일반적으로 @Configuration 클래스에 붙입니다.
      • 이걸 붙이지 않으면 @Scheduled는 작동하지 않습니다.
      • 내부적으로 ScheduledAnnotationBeanPostProcessor라는 빈을 등록해서,
      • @Scheduled가 붙은 메서드를 감지하고 스프링의 TaskScheduler를 통해 주기적으로 실행하게 만듭니다.
  1. LottoScheduler 구현
    1. package co.kr.metacoding.backendtest.scheduler; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** * 매주 일요일 00시에 로또 당첨자 검사를 수행하는 스케줄러입니다. */ @RequiredArgsConstructor @Component public class LottoScheduler { private final LottoSchedulerService lottoSchedulerService; /** * 매주 일요일 자정(00:00:00)에 실행됩니다. * <p> * 로또 당첨자 확인 로직을 호출합니다. */ @Scheduled(cron = "0 0 0 * * SUN") public void runSchedule() { lottoSchedulerService.checkWinners(); } }
      • @Scheduled(cron = "0 0 0 * * SUN"): 주기적으로 실행
      • cron 표현식 구조 (Spring 기준, 6자리)
        • 초 분 시 일 월 요일 └── ─┴─── ┴─── ┴─── ┴─── ┴───── 0 0 0 * * SUN
  1. LottoSchedulerService 구현
    1. @RequiredArgsConstructor @Service public class LottoSchedulerService { private final LottoRepository lottoRepository; private final WinnerRepository winnerRepository; @Transactional public void checkWinners() { // 1. 당첨 번호 생성 List<Integer> winningNumbers = RandomNumberUtils.generateUniqueRandomNumbers(6, 1, 45); // 2. 모든 로또 번호 불러오기 List<Lotto> lottosPS = lottoRepository.findAll(); // 3. 비교 및 등수 계산 List<Winner> winners = new ArrayList<>(); // 배치 저장을 위한 배열 for (Lotto lotto : lottosPS) { // 로또 번호 배열 생성 List<Integer> lottoNumbers = List.of( lotto.getNumber1(), lotto.getNumber2(), lotto.getNumber3(), lotto.getNumber4(), lotto.getNumber5(), lotto.getNumber6() ); // 로또 번호 매칭 개수 int matchCount = (int) lottoNumbers.stream() .filter(number -> winningNumbers.contains(number)) .count(); // 등수 계산 Integer rank = LottoRankUtils.getRank(matchCount); if (rank != null) { winners.add(Winner.builder() .lotto(lotto) .rank(rank) .build()); } } // 4. 당첨자 저장 winnerRepository.saveAllWithJdbc(winners); } }
      • 스케줄 로직 작성
  1. getRank 메서드 분리
    1. package co.kr.metacoding.backendtest._core.utils; public class LottoRankUtils { /** * matchCount 에 따라 rank 를 돌려준다 * * @param matchCount * @return Integer (1, 2, 3, 4, 5) or null when no match */ public static Integer getRank(Integer matchCount) { return switch (matchCount) { case 6 -> 1; case 5 -> 2; case 4 -> 3; case 3 -> 4; case 2 -> 5; default -> null; // 낙첨 }; } }
  1. WinnerRepository 작성
    1. package co.kr.metacoding.backendtest.lotto.winner; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; @RequiredArgsConstructor @Repository public class WinnerRepository { private final EntityManager em; private final JdbcTemplate jdbcTemplate; public List<Winner> findAll() { return em.createQuery("SELECT w FROM Winner w", Winner.class) .getResultList(); } /** * JDBC를 사용하여 Winner 목록을 일괄 저장합니다. * <p> * - 빈 리스트가 들어오면 저장을 수행하지 않습니다. * <p> * - batchUpdate를 통해 성능을 향상시킵니다. * * @param winners 저장할 Winner 목록 */ public void saveAllWithJdbc(List<Winner> winners) { if (winners.isEmpty()) return; String sql = "INSERT INTO winner_tb (lotto_id, rank) VALUES (?, ?)"; jdbcTemplate.batchUpdate( sql, winners, winners.size(), (ps, winner) -> { ps.setLong(1, winner.getLotto().getId()); ps.setInt(2, winner.getRank()); } ); } }
      1. 목적
      List<Winner> 형태로 여러 개의 당첨자 데이터를 한 번에 DB에 빠르게 저장하기 위한 배치(batch) 인서트 메서드입니다.
      JPA의 EntityManager 대신 JdbcTemplate을 이용해 직접 SQL을 실행하여 성능을 최적화 할 수 있습니다.

      2. 주요 구성 요소
      • String sql
        • 실행할 SQL 쿼리.
        • 여기서는 winner_tb 테이블에 lotto_idrank 컬럼에 값을 넣는 INSERT 쿼리입니다.
        • 물음표(?)는 바인딩 파라미터 자리 표시자입니다.
      • jdbcTemplate.batchUpdate(...)
        • Spring JDBC의 배치 처리 메서드로, 한 번에 여러 쿼리를 실행할 때 사용합니다.
        • 첫 번째 파라미터는 SQL 쿼리
        • 두 번째는 배치 대상인 데이터 리스트 (winners)
        • 세 번째는 배치 사이즈 (winners.size())
        • 네 번째는 각 리스트 요소를 SQL에 바인딩하는 콜백 (람다식)
      • (ps, winner) -> { ... }
        • PreparedStatementSetter 역할을 하며,
        • ps는 SQL 쿼리 내 물음표 위치에 값 세팅하는 객체
        • winner는 리스트에서 현재 처리 중인 Winner 객체입니다.

      3. 동작 원리
    2. batchUpdatewinners 리스트를 순회하며,
    3. Winner 객체의 lotto 필드에서 getId()를 호출해 lotto_id 값을 세팅하고,
    4. rank 값을 두 번째 파라미터로 세팅합니다.
    5. 이렇게 준비된 SQL이 DB에 일괄 처리되어 여러 건의 INSERT가 효율적으로 수행됩니다.

    6. 4. 장점
      • 성능: 다중 insert를 한 번에 처리해서 DB 통신 횟수를 줄임
      • 안정성: PreparedStatement를 사용해 SQL Injection 위험 감소
      • 유연성: JPA 매핑과 달리 직접 SQL을 작성해 세밀한 제어 가능

      5. 주의사항
      • winner.getLotto()winner.getLotto().getId()null이 아니어야 합니다.
      • winner_tb 테이블과 컬럼명이 정확히 맞아야 합니다.
      • 트랜잭션 범위 내에서 호출하는 것이 좋습니다.
      보통 여러 번 insert 할 때 (비효율적)
      INSERT INTO winner_tb (lotto_id, rank) VALUES (1, 1); INSERT INTO winner_tb (lotto_id, rank) VALUES (2, 2); INSERT INTO winner_tb (lotto_id, rank) VALUES (3, 3); INSERT INTO winner_tb (lotto_id, rank) VALUES (4, 4);
      이렇게 각각 DB에 따로따로 쿼리를 보내면, 네트워크 왕복과 쿼리 처리 비용이 4번 발생합니다.

      batchUpdate가 하는 일 (한 번에 묶음)
      실제로 DB에 따라 다르지만, JDBC 드라이버가 지원하는 경우는 이런 식으로 묶어서 보낼 수 있어요:
      INSERT INTO winner_tb (lotto_id, rank) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
      이처럼 하나의 INSERT 쿼리에 여러 행(row)을 한꺼번에 넣는 형태로 변환해서 DB에 보내는 거예요.

      정리
      • batchUpdate는 여러 파라미터 세트를 묶어서 한 번에 DB에 보내는 역할
      • 네트워크 비용, DB 처리 비용을 줄여서 성능 최적화에 도움

3. 통합테스트 구현

  1. 통합테스트 세팅
    1. import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class LottoSchedulerServiceTest { @Mock private LottoRepository lottoRepository; }
      • @ExtendWith(MockitoExtension.class)
        • JUnit 5(Jupiter)에 Mockito 기능을 확장합니다.
        • 테스트 클래스에 @Mock, @InjectMocks 등을 사용할 수 있게 해줍니다.
        • Mockito의 의존성 주입 및 Mock 초기화를 자동으로 수행합니다.
        • 없으면 @Mock이나 @InjectMocks가 동작하지 않음.
      • @Mock
        • LottoRepository 인터페이스를 가짜(Mock) 객체로 만듭니다.
        • 실제 구현체를 쓰지 않고, 테스트에 필요한 동작만 설정하여 사용합니다.
          • 레포지토리에 있는 메서드실행은 가능하지만 기능은 없음
        • 테스트 대상(Service)의 의존성을 격리할 수 있게 해줍니다.
  1. 단위 테스트 구현
    1. /** * [단위 테스트] checkWinners 내부 로직을 직접 수행하여, * 특정 로또 번호에 대해 당첨 등수가 정확히 계산되는지 검증한다. * <p> * - 임의로 로또 번호 리스트를 Mock하여 리턴하도록 설정한다. * <p> * - 3등에 해당하는 당첨 번호를 만들어서 검증한다. */ @Test public void check_winners_test() { // given // DB에 저장된 번호 (3등 당첨 가능하게) Lotto lottoPS = Lotto.builder() .number1(1) .number2(2) .number3(3) .number4(4) .number5(5) .number6(6) .build(); // lottoRepository.findAll() 호출 시 위 리스트 리턴하도록 Mock 설정 when(lottoRepository.findAll()).thenReturn(List.of(lottoPS)); // 1. 당첨 번호 생성 (3등 맞추기 위해 예시 번호 설정) List<Integer> winningNumbers = List.of(1, 2, 3, 4, 11, 12); // when // 2. 모든 로또 번호 불러오기 List<Lotto> lottosPS = lottoRepository.findAll(); // 3. 비교 등수 계산 List<Winner> winners = new ArrayList<>(); for (Lotto lotto : lottosPS) { // 로또 번호 배열 생성 List<Integer> lottoNumbers = List.of( lotto.getNumber1(), lotto.getNumber2(), lotto.getNumber3(), lotto.getNumber4(), lotto.getNumber5(), lotto.getNumber6() ); // 로또 번호 매칭 개수 int matchCount = (int) lottoNumbers.stream() .filter(number -> winningNumbers.contains(number)) .count(); // 등수 계산 Integer rank = LottoRankUtils.getRank(matchCount); if (rank != null) { winners.add(Winner.builder() .lotto(lotto) .rank(rank) .build()); } } // then assertThat(winners).isNotEmpty(); Winner firstWinner = winners.get(0); // matchCount가 3개니까 3등이어야 함 assertThat(firstWinner.getRank()).isEqualTo(3); // lotto 객체가 정확히 연결되어 있는지 assertThat(firstWinner.getLotto()).isEqualTo(lottoPS); }
 
Share article

jjack1