inblog logo
|
jjack1
    JavaSpring Boot

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

    최재원's avatar
    최재원
    Jun 11, 2025
    [SB] 33. Spring 코딩 테스트 정리
    Contents
    1. 시작하기1.1. 개발 환경1.2. 라이브러리2.2. 기본 문제 (50점)user 등록 API 구현 (8점)user 조회 API 구현 (8점)user 수정 API 구현 (8점)필터 구현 (12점)Spring AOP를 활용한 로깅 구현 (14점)2.1. 공통 (20점)@ControllerAdvice, @ExceptionHandler를 이용하여, 잘못된 요청에 대한 응답을 처리한다. (4점)2.3. 구현 문제 (30점)로또 번호 발급 API 구현 (10점)로또 번호 당첨자 검수 Batch 구현 (20점)
    테스트 코드 수정 사항
    💡
    테스트 코드 수정 사항
    레포지토리 옵셔널
    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
      • download.java.net
        download.java.net

        download.java.net

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

        3. 동작 원리
      2. batchUpdate가 winners 리스트를 순회하며,
      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
    Contents
    1. 시작하기1.1. 개발 환경1.2. 라이브러리2.2. 기본 문제 (50점)user 등록 API 구현 (8점)user 조회 API 구현 (8점)user 수정 API 구현 (8점)필터 구현 (12점)Spring AOP를 활용한 로깅 구현 (14점)2.1. 공통 (20점)@ControllerAdvice, @ExceptionHandler를 이용하여, 잘못된 요청에 대한 응답을 처리한다. (4점)2.3. 구현 문제 (30점)로또 번호 발급 API 구현 (10점)로또 번호 당첨자 검수 Batch 구현 (20점)

    jjack1

    RSS·Powered by Inblog