Today's Codekata
class Solution {
public int[] solution(String[] park, String[] routes) {
int height = park.length;
int width = park[0].length();
int x = 0, y = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
if (park[i].charAt(j) == 'S') {
x = i;
y = j;
break;
}
}
}
for (String route : routes) {
char direction = route.charAt(0);
int distance = route.charAt(2) - '0';
int moveX = 0, moveY = 0;
switch (direction) {
case 'N': moveX = -1; break;
case 'S': moveX = 1; break;
case 'W': moveY = -1; break;
case 'E': moveY = 1; break;
}
boolean isValid = true;
for (int i = 1; i <= distance; i++) {
int tempX = x + moveX * i;
int tempY = y + moveY * i;
if (tempX < 0 || tempY < 0 || tempX >= height || tempY >= width) {
isValid = false;
break;
}
if (park[tempX].charAt(tempY) == 'X') {
isValid = false;
break;
}
}
if (isValid) {
x += moveX * distance;
y += moveY * distance;
}
}
return new int[]{x, y};
}
}
# Queries Quality and Percentage
SELECT query_name,
ROUND(AVG(rating / position), 2) AS quality,
ROUND(SUM(CASE WHEN rating < 3 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS poor_query_percentage
FROM Queries
GROUP BY query_name;
# Monthly Transactions I
SELECT DATE_FORMAT(trans_date, '%Y-%m') AS month,
country,
COUNT(*) AS trans_count,
COUNT(CASE WHEN state = 'approved' THEN 1 END) AS approved_count,
SUM(amount) AS trans_total_amount,
SUM(CASE WHEN state = 'approved' THEN amount ELSE 0 END) AS approved_total_amount
FROM Transactions
GROUP BY month, country;
# Immediate Food Delivery II
SELECT ROUND(SUM(
CASE WHEN order_date = customer_pref_delivery_date THEN 1
ELSE 0 END) * 100 / COUNT(*), 2) AS immediate_percentage
FROM Delivery
WHERE (customer_id, order_date) IN (
SELECT customer_id, MIN(order_date)
FROM Delivery
GROUP BY customer_id
);
뉴스피드 프로젝트
Mapper 클래스
@Mapper(componentModel = "spring")
public interface UserMapper {
User toEntity(SignupRequest signupRequest);
UserResponse toResponse(User user);
}
팀원 한명이 매퍼클래스에 대해 알려주고, 해당 클래스를 활용해서 간단하게 DTO ↔ Entity 간의 변환을 자동화해줄 수 있었다. `@Mapper` 어노테이션은 MapStruct 라이브러리에서 제공하는 어노테이션으로, 해당 인터페이스가 매핑 기능을 수행하는 매퍼 클래스임을 지정한다. `componentModel = "spring"` 속성을 추가하면, MapStruct가 생성한 구현체를 Spring Bean으로 등록해주기 대문에, 서비스나 컨트롤러에서 `@RequiredArgsConstructor`를 통해 의존성 주입이 가능해진다. 복잡한 변환 로직을 작성할 필요 없이, 선언만 해두면 자동으로 구현체를 생성해서 관리해주는 구조이다.
로그인 유저 자동 주입 기능 구현
@Component
@RequiredArgsConstructor
public class UserHandlerArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
private final UserRepository userRepository;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Object userId = httpSession.getAttribute("LOGIN_USER");
if (Objects.isNull(userId)) {
throw new Exception("유저를 찾을 수 없습니다.");
}
Optional<User> optionalUser = userRepository.findById((Long) userId);
return optionalUser.orElseThrow(() -> new Exception("유저를 찾을 수 없습니다."));
}
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUserResolver {}
뉴스피드 프로젝트를 진행하면서, 컨트롤러에서 로그인된 유저 정보를 매번 직접 꺼내는 번거로움을 줄이기 위해 커스텀 파라미터 리졸버를 도입했다. `@Target(ElementType.PARAMETER)` 이 어노테이션이 파라미터에만 적용되도록 제한했고, `@Retention(RetentionPolicy.RUNTIME)` 을 설정하여 런타임 시에도 어노테이션 정보가 유지되도록 했다. 이렇게 하면 스프링이 리플렉션을 통해 해당 어노테이션을 인식하고 처리할 수 있다.
작동 흐름은 다음과 같다.
1. 사용자가 로그인하면 HttpSession에 "LOGIN_USER"라는 키로 유저 ID를 저장한다.
2. 이후 컨트롤러에서 @LoginUserResolver User user처럼 파라미터를 선언하면,
3. UserHandlerArgumentResolver가 해당 파라미터를 감지하고,
4. 세션에서 유저 ID를 꺼낸 뒤, UserRepository를 통해 실제 유저 객체를 조회하여 주입한다.
이렇게 하면 컨트롤러에서 로그인 유저를 직접 꺼내는 로직 없이도, 필요한 유저 정보를 바로 사용할 수 있어 코드가 훨씬 깔끔해지고 유지보수도 쉬워진다.
비밀번호 유효성 검사 로직
/**
* 비밀번호 유효성 검사를 수행하는 Validator 클래스
*/
@Component
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
// 비밀번호 정규식 패턴
private static final String PASSWORD_PATTERN =
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,20}$";
// 유효성 검사 로직
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (password == null) {
return false;
}
return password.matches(PASSWORD_PATTERN);
}
}
/**
* 비밀번호 유효성 검사를 위한 커스텀 어노테이션
* - 최소 8자, 최대 20자
* - 영문 대소문자, 숫자, 특수문자 각각 최소 1개 포함
*/
@Documented
@Constraint(validatedBy = PasswordValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
String message() default "비밀번호는 8~20자이며, 대소문자/숫자/특수문자를 최소 1글자씩 포함해야 합니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
먼저 DTO의 `password`에 붙여줄 `@ValidPassword` 어노테이션을 만들어주고, `@Constraint(validatedBy = PasswordValidator.class)`를 통해 실제 검증 로직은 Validator 클래스에서 수행하도록 설정해줬다. 정규식 패턴을 이용해 사용될 수 있는 비밀번호에 대한 제한사항을 설정해주고, `isValid` 메서드를 통해 입력 받은 비밀번호가 패스워드 패턴 조건에 부합하는 지 확인하는 것이다. 사용된 정규식 패턴을 아래와 같다.
| 조건 | 설명 |
| (?=.*[a-z]) | 소문자 최소 1개 포함 |
| (?=.*[A-Z]) | 대문자 최소 1개 포함 |
| (?=.*\\d) | 숫자 최소 1개 포함 |
| (?=.*[@$!%*#?&]) | 특수문자 최소 1개 포함 |
| [A-Za-z\\d@$!%*#?&]{8,20} | 전체 길이 8~20자, 허용된 문자만 포함 |
이 구조를 통해 컨트롤러나 서비스에서 별도의 검증 코드 없이, DTO에 어노테이션만 붙이면 자동으로 검증이 수행된다. 코드도 깔끔해지고 유지보수성과 확장성 모두 뛰어난 방식임을 느꼈다.
마치며
오늘 프로젝트는 팀원들과 소통하며 함께 성장하는 시간이었다. 처음엔 GitHub를 활용해 브랜치를 나누고, 이슈를 등록하며 협업의 흐름을 잡아보려 했지만, 모두에게 익숙하지 않은 방식이라 시행착오도 많았다. 하지만 여러 번의 커밋과 푸시, 머지를 거치며 PR을 올리고 리뷰하고, 서로의 코드를 보며 의견을 나누는 과정 속에서 점점 팀워크가 단단해져감을 느낄 수 있었다. 컨벤션을 문서화하고, PR 템플릿과 이슈 템플릿도 만들어 적용해보면서 협업이 무엇인지 경험해볼 수 있었다. 무엇보다 좋았던 건, 다양한 사고방식을 직접 마주할 수 있었다는 점이다. 내가 생각한 방향이 항상 정답은 아니었고, 팀원들과 의견을 나누다 보면 더 나은 방법이 보이기도 했다. 혼자 만드는 결과물이 아니라, 함께 고민하고 만들어가는 과정 자체가 의미 있는 경험이었다. 앞으로도 이런 협업의 흐름 속에서, 기술뿐 아니라 사람과 함께하는 개발의 본질을 더 깊이 이해해가고 싶다.