Today's Codekata
// 둘만의 암호
class Solution {
public String solution(String s, String skip, int index) {
StringBuilder answer = new StringBuilder();
Set<Character> skipSet = new HashSet<>();
for (char c : skip.toCharArray()) {
skipSet.add(c);
}
for (char c : s.toCharArray()) {
int count = 0;
char temp = c;
while (count < index) {
temp++;
if (temp > 'z') temp = 'a';
if (!skipSet.contains(temp)) {
count++;
}
}
answer.append(temp);
}
return answer.toString();
}
}
`temp`와 같은 임시변수를 사용하지 않고 바로 `c`를 직접 수정해버리면 정답은 똑같이 나오지만 가독성과 안정성에 영향을 줄 수 있음을 알게 되었다.
# Big Countries
select name, population, area
from World
where area >= 3000000
or population >= 25000000;
# Article Views I
select distinct author_id as id
from Views
where author_id = viewer_id
order by id;
# Invalid Tweets
select tweet_id
from Tweets
where length(content) > 15;
# Replace Employee ID With The Unique Identifier
select eu.unique_id as unique_id, e.name
from Employees e
left join EmployeeUNI eu on e.id = eu.id;
LeetCode SQL 문제들을 풀다 보면 난이도는 비교적 낮지만, 영어권에서 SQL을 어떻게 실무적으로 활용하는지 감을 잡을 수 있었다. 단순한 문법 연습을 넘어서 데이터 중심 사고를 키우는 데도 도움이 된다.
일정 관리 앱
리팩토링
ErrorCode 상수화
@Getter
@AllArgsConstructor
public enum ErrorCode {
// 유저
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U001", "해당 사용자를 찾을 수 없습니다."),
LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "U002", "이메일 또는 비밀번호가 일치하지 않습니다."),
EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "U003", "이미 존재하는 사용자입니다."),
// 일정
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "S001", "해당 일정을 찾을 수 없습니다."),
INVALID_SCHEDULE_UPDATE(HttpStatus.BAD_REQUEST, "S002", "수정할 항목을 하나 이상 입력해주세요."),
// 댓글
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "CM001", "댓글을 찾을 수 없습니다.");
private final HttpStatus status;
private final String code;
private final String message;
}
반복적으로 사용되는 코드들이 여기저기 흩어져 있으면, 유지보수할 때 찾기도 힘들고, 메시지를 변경하게 되면 전역에서 다 수정해야하는 번거로움 때문에 `ErrorCode`를 enum으로 상수화해서 만들어봤다. 이렇게 사용해보니 모든 에러 코드와 메시지가 한 곳에 모여 있어서, 필요 시 enum 내부에서만 수정해주면 간단하게 적용이 가능했고, 새 에러를 추가할 때도 편리함과 일관된 구조를 유지할 수 있음을 느꼈다. 그리고 상수명을 통해 바로 이해할 수 있도록 만들어서 가독성도 좋아졌다. 유저, 일정, 댓글 이렇게 아직 작은 구조인데도, 어디에 예외 처리가 되어있는지 찾는 것에 불편함이 느껴졌는데, 이렇게 enum으로 만들어두니 안정감도 느껴진다.
ErrorResponse로 응답 포맷 만들기
public record ErrorResponse(String code, String message, int status) {
public static ErrorResponse from(ErrorCode errorCode) {
return new ErrorResponse(
errorCode.getCode(),
errorCode.getMessage(),
errorCode.getStatus().value()
);
}
}
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
ErrorCode errorCode = ex.getErrorCode();
return ResponseEntity
.status(errorCode.getStatus())
.body(ErrorResponse.from(errorCode));
}
명명해준 코드와 메시지 그리고 상태코드를 응답하는 DTO를 만들어주고, 정적 팩토리 메서드를 통해 어렵지 않게 수정해줬다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult()
.getAllErrors()
.get(0)
.getDefaultMessage();
return ResponseEntity.badRequest().body(errorMessage);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult()
.getAllErrors()
.get(0)
.getDefaultMessage();
ErrorCode errorCode = ErrorCode.INVALID_INPUT_VALUE;
ErrorResponse errorResponse = ErrorResponse.from(errorCode);
return ResponseEntity.badRequest().body(errorResponse);
}
응답 포맷을 통일하려고 위처럼 수정해줬다. 하지만 이렇게 하니 내가 상수값으로 넣어둔 "잘못된 입력값입니다." 기본 메시지만 응답되고 있다. 요청DTO 어노테이션에 작성해둔 검증 메시지가 출력되도록 만들어줘야해서 코드와 응답코드는 `ErrorCode`에서 가져오고 메시지만 DTO 검증에서 발생한 메시지로 교체해줬다. 최종적으로 아래처럼 수정했다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult()
.getAllErrors()
.get(0)
.getDefaultMessage();
ErrorCode errorCode = ErrorCode.INVALID_INPUT_VALUE;
ErrorResponse errorResponse = new ErrorResponse(
errorCode.getCode(),
errorMessage,
errorCode.getStatus().value()
);
return ResponseEntity.badRequest().body(errorResponse);
}
이제 모든 예외 응답이 동일한 JSON 구조를 가지게 되었고, 메시지도 상황별로 유연하게 변경이 가능하다. "잘못된 입력값입니다."만 응답받아도 이해하기 어렵지는 않지만, 상황에 맞게 메시지를 전달하는 것이 좋은 방향이라는 생각이 들었다.
검증 어노테이션과 컬럼 제약 동기화
public class Comment extends BaseEntity {
@Column(nullable = false, length = 50)
private String content;
}
public class Schedule extends BaseEntity {
@Column(nullable = false, length = 30)
private String title;
@Column(nullable = false, length = 100)
private String content;
}
public class User extends BaseEntity {
@Column(nullable = false, length = 20)
private String username;
}
DTO 검증과 DB 제약을 일치시켜두면 런타임 오류를 줄일 수 있다는 것을 알게 되어서 `length`를 활용해서 DTO의 `@Size`와 일치시켜줬다.
양방향 연관관계 매핑
@OneToMany(mappedBy = "schedule", cascade = CascadeType.REMOVE)
private List<Comment> comments = new ArrayList<>();
기존에 일정 조회 시 댓글을 불러올 때 `@OneToMany`를 활용해 `comments` 필드를 통해 데이터를 가져왔었는데, 양방향 매핑은 의도하지 않은 동작이 발생하거나 성능 저하, 유지보수의 복잡성 등 여러 이유로 실무에서는 꼭 필요한 경우에만 사용되거나 아예 지양하는 경우가 많다는 걸 알게 되었다. 그래서 필드를 삭제하고 다른 방법을 통해 댓글들을 가져와야만 했다.
@Transactional(readOnly = true)
public ScheduleWithCommentResponse findById(Long id) {
Schedule findSchedule = scheduleRepository.findByIdOrElseThrow(id);
List<SimpleCommentResponse> comments = commentRepository.findByScheduleId(id)
.stream()
.map(SimpleCommentResponse::from)
.toList();
return ScheduleWithCommentResponse.from(findSchedule, comments);
}
`findByScheduleId()` 일정ID로 댓글들을 가져올 쿼리메서드를 만들어주고, 조회된 댓글 리스트는 스트림을 활용해 응답DTO 형태로 변환해주었다. 지금 당장은 코드를 실무와 연결짓는 게 어불성설 같지만 알게 된 것들을 잘 기억해둬서 같은 실수를 반복하지 않도록 해야겠다.
Auth 디렉토리 분리

인증/인가와 관련된 로직은 유저 도메인과는 성격이 다르기 때문에, 책임을 명확하게 하기 위해서 별도의 `auth` 디렉토리로 분리해줬다. 기존에는 `User`에 로그인, 로그아웃, 인증 관련 클래스들이 섞여 있었는데, 따로 분리해서 독립적으로 관리할 수 있게 되었다.
마치며
리팩토링은 결과만 보면 간단해 보이지만 그 과정은 늘 쉽지 않다. 무엇이 문제인지 파악하고, 어떤 방향으로 개선할 수 있을지 고민하는 시간은 생각보다 오래 걸린다. 이번 일정 관리 앱을 리팩토링하면서도 여러 개념들이 머릿속에서 뒤섞여 있었고, 답에 도달하기까지 시행착오도 많았다. 하지만 그럼에도 하나하나 구조를 나누고 책임을 분리해보면서 조금씩 설계에 대한 감각이 생겨나는 걸 느낄 수 있었다. 작은 리팩토링들이 모여 더 안정적이고 유지보수하기 쉬운 구조로 발전해가는 과정이 꽤나 즐거웠다. 아직은 실무와 완전히 연결지어 생각하기는 어렵지만 이렇게 기록하고 정리해두면 언젠가 비슷한 상황을 마주했을 때, 더 정확하게 판단할 수 있는 기반이 되어줄 거라 생각한다. 앞으로도 기능 구현에만 집중하기보다는 왜 이렇게 설계해야 하는지에 대한 고민을 놓치 않으려고 한다.
