이번 일정관리 API를 만들면서 가장 큰 변화는 로그인(Session) 기능을 적용하면서 URL 구조가 바뀐 것이었다.
처음에는 로그인 기능이 없었기 때문에 모든 요청은 URL에 userId를 포함해야 했다.
예를 들어,
POST /users/1/schedules
GET /users/1/schedules
GET /users/1/schedules/3
PUT /users/1/schedules/3
DELETE /users/1/schedules/3
컨트롤러도 자연스럽게 이렇게 작성했다.
@PostMapping
public ResponseEntity<CreateScheduleResponse> createSchedule(
@PathVariable Long userId,
@RequestBody CreateScheduleRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(scheduleService.create(userId, request));
}
그런데 로그인(Session)을 적용하면?
로그인에 성공하면 서버는 Session에 로그인한 사용자의 정보를 저장한다.
SessionUser sessionUser = new SessionUser(user.getUserId(), user.getEmail());
session.setAttribute("loginUser", sessionUser);
즉, 누가 로그인했는지를 서버가 이미 알고 있게 된다.
그러면 굳이
/users/1
처럼 URL에 userId를 넘길 필요가 없어진다.
로그인한 사람이 누구인지는 Session이 이미 가지고 있기 때문이다.
그래서 URL도 훨씬 단순해졌다.
변경 전
POST /users/1/schedules
GET /users/1/schedules
PUT /users/1/schedules/3
↓
변경 후
POST /schedules
GET /schedules
PUT /schedules/3
DELETE /schedules/3
URL이 훨씬 REST스럽고 간결해졌다.
Controller에서 Session을 사용하는 이유
Session 정보는 Controller에서만 꺼낸다.
@PostMapping
public ResponseEntity<CreateScheduleResponse> createSchedule(
@SessionAttribute(name = "loginUser") SessionUser loginUser,
@RequestBody CreateScheduleRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(scheduleService.create(loginUser.getId(), request));
}
여기서 중요한 점은
Controller는 Session을 알고 있지만,
Service는 Session을 모른다는 것이다.
처음에는 Service에서도 SessionUser를 넘겼다.
처음에는 Service 메서드도 이렇게 작성했다.
public CreateScheduleResponse create(
SessionUser loginUser,
CreateScheduleRequest request
)
그러면 Service 안에서도
loginUser.getId()
를 계속 사용해야 했다.
하지만 생각해보면 Service는 Session을 알 필요가 없다.
Service가 필요한 것은 로그인한 사용자의 ID뿐이다.
Session이라는 개념은 웹(Web) 계층의 개념이다.
반면 Service는 비즈니스 로직을 담당하는 계층이다.
즉, 웹 기술(Session)을 Service가 알게 되면계층 간 결합도가 높아진다.
그래서 이렇게 리팩토링했다.
변경 전
scheduleService.create(loginUser, request);
↓
변경 후
scheduleService.create(loginUser.getId(), request);
그리고 Service도
public CreateScheduleResponse create(
Long userId,
CreateScheduleRequest request
)
로 변경했다.
이렇게 되면 Service는
로그인했는지
Session인지
Cookie인지
JWT인지
전혀 신경 쓰지 않는다.
오직
userId
만 전달받아서 비즈니스 로직만 수행한다.
Service는 정말 필요한 정보만 받는다.
예를 들어
User user = userOrThrow(userId);
처럼 필요한 User를 조회하고,
Schedule schedule =
new Schedule(request.getTitle(), request.getContent(), user);
를 수행하면 끝이다.
Service 입장에서는 userId만 있으면 충분하다.
권한 검사도 동일하게 리팩토링했다.
처음에는 코드가 여러 곳에 중복되어 있었다.
if (!schedule.getUser().getUserId().equals(userId)) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN,
"잘못된 접근입니다."
);
}
조회
수정
삭제
모든 메서드에서 동일한 코드가 반복되었다.
그래서 공통 메서드로 분리했다.
private void validateOwner(
Schedule schedule,
Long userId
) {
if (!schedule.getUser().getUserId().equals(userId)) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN,
"잘못된 접근입니다."
);
}
}
이후에는
validateOwner(schedule, userId);
한 줄만 호출하면 된다.
중복 코드가 사라지고,
권한 검증 로직도 한 곳에서 관리할 수 있게 되었다.
이번 리팩토링으로 얻은 점
- Session을 사용하는 계층은 Controller까지만 제한했다.
- Service는 웹 기술(Session)을 모른다.
- Service는 userId만 전달받아 비즈니스 로직에 집중한다.
- URL이 더 RESTful해졌다.
- 권한 검증을 공통 메서드로 분리하여 중복 코드를 제거했다.
- 계층 간 역할이 더욱 명확해졌다.
'트러블슈팅' 카테고리의 다른 글
| error: cannot find symbol @Value("${jwt.secret.key}") 해결 (0) | 2026.07.22 |
|---|---|
| [Spring] 고객 조회 기능 구현하며 겪은 트러블슈팅 - Pageable과 JPQL 적용기 (0) | 2026.07.13 |
| [IntelliJ IDEA 기능] Compact Middle Packages / 패키지 설정 (0) | 2026.07.07 |
| [Spring] 전체조회시, 조건 전체조회 (feat.@RequestParam) (0) | 2026.06.30 |
| ArrayList 기반 장바구니 구현 (0) | 2026.06.16 |