Today's Codekata
# Find Followers Count
SELECT user_id, COUNT(*) AS followers_count
FROM Followers
GROUP BY user_id
ORDER BY user_id;
# Biggest Single Number
SELECT COALESCE((
SELECT num
FROM MyNumbers
GROUP BY num
HAVING COUNT(*) = 1
ORDER BY num DESC
LIMIT 1
), null) AS num;
# Customers Who Bought All Products
SELECT c.customer_id
FROM Customer c
GROUP BY c.customer_id
HAVING COUNT(DISTINCT c.product_key) = (SELECT COUNT(*) FROM Product);
// 이진 변환 반복하기
class Solution {
public int[] solution(String s) {
int[] answer = new int[2];
while (!s.equals("1")) {
int ones = 0;
for (char c : s.toCharArray()) {
if (c == '1') {
ones++;
} else {
answer[1]++;
}
}
s = Integer.toBinaryString(ones);
answer[0]++;
}
return answer;
}
}
Today I Learned
HttpMessageConverter
HttpMessageConverter란?
- Spring MVC에서 HTTP 요청/응답의 본문(body)을 자바 객체 ↔ JSON, XML, TEXT 등으로 변환해주는 도구이다.
| 상황 | 어노테이션 |
| 요청 처리 | `@RequestBody`, `HttpEntity<>`, `RequestEntity<>` |
| 응답 처리 | `@ResponseBody`, `HttpEntity<>`, `ResponseEntity<>` |
동작 흐름
1. 클라이언트가 JSON 등 데이터를 보냄
2. Spring이 적절한 Converter를 선택해 자바 객체로 변환
3. 컨트롤러에서 처리 후 객체 반환
4. 다시 Converter가 객체를 JSON 등으로 변환해 응답
어떤 Converter가 선택될까? (우선순위)
Spring은 아래 두 가지 기준으로 Converter를 선택한다.
- 대상 클래스: `byte[]`, `String`, `Object` 등
- MediaType: `Content-Type`, `Accept` 헤더
대표적인 Converter
| Converter | 대상 | MediaType | 응답 타입 |
| `ByteArrayHttpMessageConverter` | `byte[]` | `*/*` | `application/octet-stream` |
| `StringHttpMessageConverter` | `String` | `*/*` | `text/plain` |
| `MappingJackson2HttpMessageConverter` | `ObjectMap` | `application/json` | `application/json` |
동작 위치
직접 쓰는 게 아니라 Spring 내부 컴포넌트가 호출해서 사용하는 도구이다.
- 요청 처리: `ArgumentResolver` 내부에서 사용
- 응답 처리: `ReturnValueHandler` 내부에서 사용
확장 방법: WebMvcConfigurer
Spring MVC 설정을 커스터마이징할 수 있는 인터페이스이다.
| 메서드 | 설명 |
| `addArgumentResolvers()` | 커스텀 파라미터 바인딩 추가 |
| `addReturnValueHandlers()` | 커스텀 응답 처리 추가 |
| `extendMessageConverters()` | 기존 Converter에 기능 추가 |
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 커스텀 Converter 추가 가능
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// 커스텀 ArgumentResolver 추가 가능
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
// 커스텀 ReturnValueHandler 추가 가능
}
}
ArgumentResolver
RequestMappingHandlerAdapter란?
- Spring MVC에서 클라이언트의 HTTP 요청을 적절한 컨트롤러 메서드에 연결하고 실행하는 핵심 컴포넌트
- `@RequestMapping`, `@PostMapping`, `@GetMapping` 등 모든 매핑 어노테이션을 처리함
- 내부적으로 `ArgumentResolver`와 `ReturnValueHandler`를 사용해 요청과 응답을 처리함
ArgumentResolver란?
- 컨트롤러 메서드의 파라미터를 자동으로 생성해주는 인터페이스
- 요청이 들어오면 `RequestMappingHandlerAdapter`가 적절한 `ArgumentResolver`를 선택해 파라미터를 바인딩함
- `supportsParameter()` → 해당 파라미터를 처리할 수 있는지 확인
- `resolveArgument()` → 실제 객체를 생성하여 컨트롤러에 전달
| Resolver | 어노테이션 | 설명 |
| `RequestBodyArgumentResolver` | `@RequestBody` | 요청 body를 객체로 변환 |
| `RequestHeaderArgumentResolver` | `@RequestHeader` | 요청 헤더 값을 바인딩 |
| `HandlerMethodArgumentResolver` | `@LoginUser` | 커스텀 Resolver도 가능 |
@GetMapping("/me")
public UserProfile getProfile(@LoginUser User user) {
return userProfileService.findById(user.getId());
}
ReturnValueHandler란?
- 컨트롤러 메서드가 반환한 값을 HTTP 응답에 맞게 변환하는 인터페이스
- 예: `ModelAndView`, `@ResponseBody`, `HttpEntity<>` 등
- `supportsReturnType()` → 반환 타입을 처리할 수 있는지 확인
- `handleReturnValue()` → 반환값을 HTTP 응답으로 변환
| Handler | 반환 타입 | 설명 |
| `ModelAndViewMethodReturnValueHandler` | `ModelAndView` | 뷰 렌더링 처리 |
| `HttpEntityMethodProcessor` | `HttpEntity<>` | HTTP 응답 본문 처리 |
전체적인 흐름
1. 클라이언트가 HTTP 요청을 보냄
2. `RequestMappingHandlerAdapter`가 적절한 컨트롤러 메서드를 찾음
3. `ArgumentResolver`가 파라미터를 생성해 컨트롤러에 전달
4. 컨트롤러가 로직 처리 후 값을 반환
5. `ReturnValueHandler`가 반환값을 HTTP 응답으로 변환
TypeConverter
TypeConverter란?
- Spring에서 서로 다른 데이터 타입 간의 변환을 자동 또는 수동으로 처리해주는 기능이다.
- 웹 요청은 대부분 문자열로 들어오는데, 이를 Integer, Boolean, Enum, Custom Object 등으로 바꿔주는 역할을 한다.
- 복잡한 타입은 Converter 인터페이스로 직접 구현 가능하다.
- `@RequestParam`, `@PathVariable`, `@ModelAttribute` 자동 적용되지만, `@RequestBody`는 `HttpMessageConverter`가 담당한다.
@GetMapping("/age")
public void getAge(@RequestParam Integer age) {
// "25" → Integer 25로 자동 변환됨
}
ConversionService
ConversionService란
- 스프링의 타입 변환 중앙 허브 역할을 하는 인터페이스이다.
- 다양한 변환기(Converter)를 등록해두고, 필요할 때 호출해 변환 수행한다.
- 스프링 MVC, 데이터 바인딩, 설정 값 처리 등 내부적으로도 광범위하게 사용된다.
- 재사용성, 일관성, 확장성, 관심사 분리(변환 로직과 비즈니스 로직 분리) 등의 장점이 있다.
주요 기능
`canConvert(sourceType, targetType)`: Convert 가능 여부를 확인하는 기능
`convert(source, targetType)`: 실제 변환하는 기능
대표 구현체
DefaultConversionService: 변환기 등록(`ConverterRegistry`) + 변환 사용(`ConversionService`) 모두 지원
GenericConversionService: 확장 가능한 기본 구현체
활용 예시
`@RequestParam`, `@PathVariable` 값 자동 변환
DTO 매핑 시 문자열 → 날짜, enum 등 변환
설정 값(`@Value`) 변환
복잡한 객체 변환 로직을 전역 규칙으로 등록
Formatter
Formatter란?
- 문자열 ↔ 객체 변환 시 특정 포맷을 적용해 변환을 세밀하게 제어하는 기능이다.
- Converter와 차이점: Converter는 단순 타입 변환이고, Formatter는 변환 + 포맷팅(형식)까지 고려한다.
- 날짜, 숫자, 통화, 전화번호, 우편번호 등 사람이 읽는 형식이 중요한 데이터 변환에 많이 사용된다.
동작 구조
- `Formatter<T>` 인터페이스 / `print()`: 객체 → 문자열, `parse()`: 문자열 → 객체
- `Printer` + `Parser` 기능을 모두 포함.
- Locale 지원: 국가·언어별 포맷 차이를 반영 (예: 10,000 vs 10.000)
public class PriceFormatter implements Formatter<Number> {
@Override // 문자열을 숫자로 변환
public Number parse(String text, Locale locale) throws ParseException {
return NumberFormat.getInstance(locale).parse(text); // "10,000" → 10000L
}
@Override // 숫자를 포맷된 문자열로 변환
public String print(Number object, Locale locale) {
return NumberFormat.getInstance(locale).format(object); // 10000L → "10,000"
}
}
등록과 적용
- `WebMvcConfigurer`의 `addFormatters()`에서 등록 → `@RequestParam`, `@PathVariable`, `@ModelAttribute` 자동 적용
- 뷰 템플릿(Thymeleaf 등)에서도 동일하게 포맷 적용 가능
FormattingConversionService
FormattingConversionService란
- ConversionService + Formatter 결합된 기능으로, 변환과 포맷팅을 한 곳에서 처리 가능하다.
- DefaultFormattingConversionService: 여기에 숫자·통화 등 기본 Formatter 추가된 형태
- Spring Boot는 기본적으로 `WebConversionService` 사용한다. (`DefaultFormattingConversionService` 상속)
DefaultFormattingConversionService cs = new DefaultFormattingConversionService();
cs.addConverter(new StringToPersonConverter());
cs.addFormatter(new PriceFormatter());
String result = cs.convert(10000, String.class); // "10,000"
- `convert()` 메서드로 변환과 포맷팅 모두 가능
- MVC 전역 등록 시 `@RequestParam`, `@PathVariable`, `@ModelAttribute` 등에서 자동 적용
Spring 제공 Formatter 어노테이션
- `@NumberFormat` : 숫자 포맷 지정 (#,###.## 등)
- `@DateTimeFormat` : 날짜 포맷 지정 (dd-MM-yyyy 등)
- DTO 필드에 적용 → 폼 데이터·URL 파라미터 변환 시 `ConversionService`가 처리
JSON 처리 시 주의
- `@NumberFormat`, `@DateTimeFormat` → 폼 데이터/쿼리 파라미터 변환에만 적용 (Jackson JSON 직렬화/역직렬화에는 영향 없음)
- JSON 변환 시 방법: @JsonFormat → 날짜·숫자 포맷 지정 (단, 콤마 포함 숫자는 직접 처리 필요)
커스텀 Deserializer → 복잡한 포맷 변환 로직 구현
테스트 코드 이론 세션
테스트 코드를 작성하는 이유
- 테스트 코드는 버그를 조기에 발견하고, 코드 품질과 배포 안정성을 높이며, 리팩토링과 서비스 확장을 안전하게 만드는 필수 도구다. 빠르고 독립적이며 반복 가능한 좋은 테스트를 작성하는 습관이 중요하다.
테스트 코드 작성 원칙
- FIRST 원칙
Fast : 빠르게 실행되어야 한다.
Isolated : 서로 독립적으로 동작해야 한다.
Repeatable : 반복 실행 시에도 항상 동일한 결과를 내야 한다.
Self-validating : 테스트 자체가 자동으로 검증이 가능해야 한다. (출력 확인 X)
Timely : 가능한 한 즉시 작성하는 것이 좋다.
- 테스팅 7원칙
1. 목적은 결함 발견하는 것
2. 모든 경우를 완벽하게 테스트하는 것은 불가
3. 가능한 빠르게 시작
4. 결함은 특정 부분에 집중 (파레토 법칙)
5. 살충제 역설 → 테스트 케이스 주기적 개선 필요
6. 정황 의존적 → 서비스 특성에 맞게
7. 오류가 없다고 해서 코드가 완벽하다는 착각 X
테스트 종류
- 단위 테스트 : 코드 최소 단위 검증, 빠름, 문제 지점 파악 쉬움
- 통합 테스트 : 모듈 간 연동 검증, 실제 환경 포함, 느리고 세팅 복잡
작성 방법
- 다양한 케이스 구성 (정상 케이스, 경계값(엣지) 케이스, 예외 케이스)
- 독립성 보장 → 테스트 간 의존 X, DB는 롤백 처리
- 중요 로직 우선 테스트 → 커버리지 수치보다 질이 중요
- 효율 고려 → 비용 대비 효과 높은 테스트 작성
마치며
가장 먼저는 오늘 Converter에 대해 공부했는데, 문자열 → 숫자 같은 간단한 변환을 굳이 만들어서 사용해야하나 하는 생각이 들었다. 그런데 더 공부하다보니 단순한 변환이라도 Converter로 만들어서 사용하면 유지보수와 확장에 강한 코드가 된다는 걸 자연스럽게 이해할 수 있었다. 그리고 Spring MVC의 데이터 변환 구조가 내부적으로 명확한 역할 분담 속에서 동작한다는 걸 알게 되었다. 앞으로는 변환 로직이 필요한 경우 무조건 컨트롤러 내부에서 처리하기보다 Converter, Formatter를 적절히 활용해서 깨끗하고 확장성 좋은 코드를 작성해봐야겠다고 느꼈습니다.
그리고 테스트 코드에 대한 간단한 이론 수업을 들으면서, 지난 프로젝트들을 진행할 때 마음 한켠에 있던 걱정이 떠올랐다. 바로 예외 처리에 대한 부분이었다. 그동안은 내 머릿속에 떠오르는 예외 상황만 처리했는데, 실제 서비스에서는 훨씬 더 다양한 경우가 존재할 텐데 과연 어떻게 미리 준비하고 대응할 수 있을까 하는 생각이 들었다. 아직 테스트 코드를 제대로 작성하는 방법을 배우진 않았지만, 테스트 코드를 충분히 갖춰두면 리팩토링이나 기능 확장을 할 때 걱정 없이 코드를 수정할 수 있다는 점에서 큰 장점을 느꼈다. 테스트 없이 코드를 짜면 당장은 개발 속도가 빠를 수 있지만, 결국 나중에 수많은 오류를 마주하며 하나하나 수정하는 데 더 많은 시간이 걸린다는 것을 지난 프로젝트를 통해 배웠다. 앞으로는 기능 개발과 동시에 테스트 코드를 작성하는 습관을 들여야겠다. 조금 느리더라도 확실하게 가는 길을 선택해야겠다는 생각이 들었다.