트러블슈팅

Session 로그인 적용하면서 API 구조 리팩토링하기

chungmani 2026. 7. 9. 13:32

이번 일정관리 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해졌다.
  • 권한 검증을 공통 메서드로 분리하여 중복 코드를 제거했다.
  • 계층 간 역할이 더욱 명확해졌다.