테스트 코드 수정 사항
테스트 코드 수정 사항
레포지토리 옵셔널
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
- ‣


- Spring Boot 3.2.1
1.2. 라이브러리
- Spring Web
- Lombok
- H2 Database ( ID : pc, PW : 2024 )
- 그 외 필요한 라이브러리는
build.gradle
에 추가하시면 됩니다.
라이브러리 추가 시, 어떠한 이유로 추가했는지
Pull Request
에 간단히 적어주시면 됩니다.
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. 폴더 구조 생성

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 로직 구현
- UserController 구현
@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);
}
}
- UserRequest 구현
public class UserRequest {
@Data
public static class SaveDTO {
private String name;
public User toEntity() {
return User.builder().name(name).build();
}
}
}
- UserResponse 구현
public class UserResponse {
@Data
public static class SaveDTO {
private Long id;
public SaveDTO(User user) {
this.id = user.getId();
}
}
}
- UserService 구현
@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);
}
}
- UserRepository 구현
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
public User save(User user) {
em.persist(user);
return user;
}
}
2. 통합테스트 구현
- 통합테스트 코드 설정
@Transactional
: 을 통해 각 테스트 실행 후 롤백됩니다.@SpringBootTest
: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.@AutoConfigureMockMvc
: MockMvc 객체를 자동 설정 및 주입해줌.
/**
* 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;
}
- Controller의 saveUser 테스트
/**
* [성공 테스트] 새로운 유저를 정상적으로 등록할 수 있어야 함.
*/
@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 로직 구현
- UserController 구현
@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);
}
}
- UserResponse 구현
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();
}
}
}
- UserService 구현
@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);
}
}
- UserRepository 구현
Optional
을 사용해 null 처리할 수 있도록 작성
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
public Optional<User> findById(Integer id) {
return Optional.ofNullable(em.find(User.class, id));
}
}
2. 통합테스트 구현
- 통합테스트 코드 설정
@Transactional
: 을 통해 각 테스트 실행 후 롤백됩니다.@SpringBootTest
: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.@AutoConfigureMockMvc
: MockMvc 객체를 자동 설정 및 주입해줌.
/**
* 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;
}
- Controller의 getUser 테스트
/**
* [성공 테스트] 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 로직 구현
- UserController 구현
@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);
}
}
- UserRequest 구현
public class UserRequest {
@Data
public static class UpdateDTO {
private String name;
}
}
- UserResponse 구현
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();
}
}
}
- UserService 구현
@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. 통합테스트 구현
- 통합테스트 코드 설정
@Transactional
: 을 통해 각 테스트 실행 후 롤백됩니다.@SpringBootTest
: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.@AutoConfigureMockMvc
: MockMvc 객체를 자동 설정 및 주입해줌.
/**
* 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;
}
- Controller의 getUser 테스트
/**
* [성공 테스트] 유저의 이름을 정상적으로 수정할 수 있어야 함.
*/
@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 호출에 대한 통합 테스트 코드 작성

1. Filter 구현
- SpecialCharacterFilter 구현
- 허용금지 url 요청시 응답 로직 분리
- 응답 메시지 제외 모든 문자열은 이넘을 사용
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(); // 명확하게 버퍼 응답
}
}
- FilterConfig 구현
- Spring Boot 애플리케이션의 서블릿 필터(Servlet Filter)를 등록
- 스프링 애플리케이션 구동 시 이
FilterConfig
클래스가@Configuration
으로 스캔되어 빈으로 등록되고, specialCharacterFilter()
메서드가 반환하는FilterRegistrationBean
덕분에- 이후 요청이 들어올 때마다 톰캣이 서블릿 필터 체인 순서대로 필터를 호출하는데,
order(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;
}
}
내장 톰캣(또는 외부 서블릿 컨테이너)의 필터 체인에
SpecialCharacterFilter
가 등록됩니다.2. 통합테스트 구현
- 통합테스트 코드 설정
@Transactional
: 을 통해 각 테스트 실행 후 롤백됩니다.@SpringBootTest
: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.@AutoConfigureMockMvc
: MockMvc 객체를 자동 설정 및 주입해줌.
/**
* SpecialCharacterFilter의 통합 테스트 클래스입니다.
* <p>
* - MockMvc를 사용해 실제 컨트롤러 요청에 필터가 정상 동작하는지 검증합니다.
* <p>
*
* @Transactional: 을 통해 각 테스트 실행 후 롤백됩니다.
*/
@Transactional
/**
* @SpringBootTest: 실제 스프링 컨텍스트를 로딩하여 통합 테스트를 수행할 수 있게 함.
* (내부적으로 @SpringBootConfiguration, @EnableAutoConfiguration 등을 포함)
*/
@SpringBootTest
/**
* @AutoConfigureMockMvc: MockMvc 객체를 자동 설정 및 주입해줌.
* (MockMvc는 실제 HTTP 요청 없이 컨트롤러 계층 테스트 가능)
*/
@AutoConfigureMockMvc
public class SpecialCharacterFilterTest {
@Autowired
private MockMvc mvc;
}
- SpecialCharacterFilter 테스트
/**
* [실패 테스트] 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'

1. AOP 구현
- LogHandler 구현
@Aspect
: 이 클래스가 AOP(관점 지향 프로그래밍) Aspect임을 나타냅니다.- 특정 포인트컷에 대해 공통 관심 사항(Advice)을 적용할 수 있습니다.
@Component
: 스프링 빈으로 등록되어 DI(의존성 주입) 및 관리가 가능하도록 합니다.@RequiredArgsConstructor
: final 또는 @NonNull 필드에 대해 생성자를 자동 생성합니다.- (Lombok 어노테이션)
log.info
를 사용해 로깅 구현
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);
}
}
- LogUserAgent 어노테이션 구현
- 포인트 컷에 사용할 커스텀 어노테이션
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 적용
- UserController
@LogUserAgent
: 어노테이션을 붙여서 AOP 적용
@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);
}
}
- SpecialCharacterFilter 테스트
/**
* [실패 테스트] 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점)

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

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 로직 구현
- LottoController 구현
@RequiredArgsConstructor
@RestController
public class LottoController {
private final LottoService lottoService;
@PostMapping("/lottos")
public ResponseEntity<?> generateLottoNumbers() {
LottoResponse.DTO respDTO = lottoService.generateLottoNumbers();
return Resp.ok(respDTO);
}
}
- LottoResponse 구현
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()
);
}
}
}
- LottoService 구현
@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);
}
}
- generateUniqueRandomNumbers 메서드 분리
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);
}
}
- LottoRepository
@RequiredArgsConstructor
@Repository
public class LottoRepository {
private final EntityManager em;
public Lotto save(Lotto lotto) {
em.persist(lotto);
return lotto;
}
}
3. 통합테스트 코드 구현
- 통합테스트 코드 세팅
/**
* 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~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로 구현한다

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 구현
- Application 세팅
@EnableScheduling
: Spring에서 스케줄링 기능을 활성화할 때 사용하는 어노테이션@Scheduled
애노테이션이 붙은 메서드들을 주기적으로 실행할 수 있도록 해주는 기능을 켜줍니다.- 일반적으로
@Configuration
클래스에 붙입니다. - 이걸 붙이지 않으면
@Scheduled
는 작동하지 않습니다. - 내부적으로
ScheduledAnnotationBeanPostProcessor
라는 빈을 등록해서, @Scheduled
가 붙은 메서드를 감지하고 스프링의 TaskScheduler를 통해 주기적으로 실행하게 만듭니다.
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);
}
}
- LottoScheduler 구현
@Scheduled(cron = "0 0 0 * * SUN")
: 주기적으로 실행- cron 표현식 구조 (Spring 기준, 6자리)
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();
}
}
초 분 시 일 월 요일
└── ─┴─── ┴─── ┴─── ┴─── ┴─────
0 0 0 * * SUN
- LottoSchedulerService 구현
- 스케줄 로직 작성
@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);
}
}
- getRank 메서드 분리
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; // 낙첨
};
}
}
- WinnerRepository 작성
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
객체입니다.batchUpdate
가winners
리스트를 순회하며,- 각
Winner
객체의lotto
필드에서getId()
를 호출해lotto_id
값을 세팅하고, rank
값을 두 번째 파라미터로 세팅합니다.- 이렇게 준비된 SQL이 DB에 일괄 처리되어 여러 건의 INSERT가 효율적으로 수행됩니다.
- 성능: 다중 insert를 한 번에 처리해서 DB 통신 횟수를 줄임
- 안정성: PreparedStatement를 사용해 SQL Injection 위험 감소
- 유연성: JPA 매핑과 달리 직접 SQL을 작성해 세밀한 제어 가능
winner.getLotto()
및winner.getLotto().getId()
가null
이 아니어야 합니다.winner_tb
테이블과 컬럼명이 정확히 맞아야 합니다.- 트랜잭션 범위 내에서 호출하는 것이 좋습니다.
batchUpdate
는 여러 파라미터 세트를 묶어서 한 번에 DB에 보내는 역할- 네트워크 비용, DB 처리 비용을 줄여서 성능 최적화에 도움
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. 주요 구성 요소
3. 동작 원리
4. 장점
5. 주의사항
보통 여러 번 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에 보내는 거예요.
정리
3. 통합테스트 구현
- 통합테스트 세팅
@ExtendWith(MockitoExtension.class)
JUnit 5
(Jupiter)에 Mockito 기능을 확장합니다.- 테스트 클래스에
@Mock
,@InjectMocks
등을 사용할 수 있게 해줍니다. - Mockito의 의존성 주입 및 Mock 초기화를 자동으로 수행합니다.
- 없으면
@Mock
이나@InjectMocks
가 동작하지 않음. - @Mock
LottoRepository
인터페이스를 가짜(Mock) 객체로 만듭니다.- 실제 구현체를 쓰지 않고, 테스트에 필요한 동작만 설정하여 사용합니다.
- 레포지토리에 있는 메서드실행은 가능하지만 기능은 없음
- 테스트 대상(Service)의 의존성을 격리할 수 있게 해줍니다.
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class LottoSchedulerServiceTest {
@Mock
private LottoRepository lottoRepository;
}
- 단위 테스트 구현
/**
* [단위 테스트] 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