Java Spring Boot(5)

제이 용·2025년 11월 13일

오늘은 기존에 만든 로그인 기능을 통해 검증 및 권한 로직을 추가할 예정이었다. 로그인 기능을 만들고 나니 생각이 든 부분이 코드가 남들에 비해 굉장히 길었다는 것이다.

비교를 해보니 내가 작성한 코드는 세션에 대한 직접적인 접근이었고, 다른분들은 @SessionAttribute를 통해 굉장한 생략이 있었다는 것이다.

setAtrribute()를 통해 직접적으로 꺼내는 구조라면 getAttribute()를 통해 또 직접적으로 꺼내는 구조로 적용 시킬 수 있다. 다만 @SessionAttribute를 활용함으로써 getAttribute()를 생략할 수 있어 굉장히 유용하였다.


예시

public ResponseEntity<ScheduleCreateResponse> create(
        @SessionAttribute(name = "userId", required = false) Long userId,
        @RequestBody ScheduleCreateRequest request) {

    if (userId == null) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
    return ResponseEntity.status(HttpStatus.CREATED)
            .body(scheduleService.save(request, userId));
}
  • 다음과 같이 해줌으로써 서비스에는 크게 getAttribute와 같이 수정없이 해결할 수 있었다.

  • 다만 세션을 통해 키값을 받아오지 않을 경우 로그인을 안한 상태라고 판단하고 조건처리를 통해 API가 작동하지 않게 설정해준 것이다.


트러블 슈팅(1)

session.setAttribute("userId", user.getId()); 
session.setAttribute("userName", user.getUserName()); 
session.setAttribute("email", user.getEmail());
session.setAttribute("password", user.getPassWord())
  • 기존 키값을 다음과 같이 해주니 뭔가 눈에도 보기 안좋고 password에 키값을 넣는게 맞나? 라는 것이 문득 생각이 들었다.

  • 강의를 다시 보니 SessionUser라는 DTO를 다시 생성을 해주고 이를 통해 한번에 처리하게끔 하는 것을 알아보게 되었다.


session.setAttribute("loginUser", sessionUser);
  • 다음과 같이 한 줄로 볼 수 있게 되었고, 비밀번호가 세션에 저장된다는 것은 보안적으로 맞지 않다는 생각이 들어 제외하게 되었다.

강의도 다시보면 새로운 지식을 얻을 수 있고, 점점 뇌리에 박히는 것이 느껴지는 문제였다.


트러블 슈팅(2)

  • 이후 4Lv을 얼추 틀을 잡았다고 생각할 찰나에 다른 팀원분들과 코드 리뷰를 진행하였다.

  • 여기서 팀원 한분이 말씀하신 내용이

//로그인 기능 @Transactional(readOnly = true) 
public void login(UserLoginRequest request, HttpSession session) { 
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow( () -> new ResponseStatusException(HttpStatus.UNAUTHORIZED,"이메일이 올바르지 않습니다.") ); 
if(!user.getPassword().equals(request.getPassword())){ 
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,"비밀번호가 올바르지 않습니다."); 
} 
SessionUser sessionUser = new SessionUser(
user.getId(), 
user.getUserName(), 
user.getEmail()); 
session.setAttribute("loginUser", sessionUser); 
}
  • 위와 같은 코드를 보고 세션이 생성되는 부분이 서비스가 아니라 컨트롤러에 있어야되는 것이 아니냐? 라는 말씀을 하셨다.

  • 강의를 보고 따라했을 때에는 User user와 같이 편의상 컨트롤러에 만든 것을 보고 아 세션부분도 서비스에 빼줘야되겠구나 라는 생각이 들어 빼준 것이었는데 원리와 논리를 따지고 보면 정말 별 것도 아닌 문제였다.

  • 컨트롤러에서 요청을 받고 서비스에서 로그인에 대한 검증을 받으면, 컨트롤러에서 세션을 생성하여 응답을 해주는게 맞는 원리와 논리라는 것을 깨달았다.

따라서 다시 세션을 밖으로 빼주었다.

@PostMapping("/users/login")
    public ResponseEntity<UserLoginResponse> login(@RequestBody UserLoginRequest request, HttpSession session) {
        UserLoginResponse userLoginResponse = userService.login(request);

        SessionUser sessionUser = new SessionUser(userLoginResponse.getId(), userLoginResponse.getUserName(), userLoginResponse.getEmail());
        session.setAttribute("loginUser", sessionUser);

        return ResponseEntity.ok(userLoginResponse);
    }
  • 마냥 강의를 따라가려고만 하는 잘못된 점이 보인 그런 질문이었다. 다만 내 목표는 강의를 통해 프로젝트를 빠르게 완성하고 다시 부신다음에 흐름을 파악하여 재작성하는 것이 내 목표이기 때문에 그 과정 중 이 일부를 미리 클리어 했다 라고 생각하기로 했다.

  • 그래도 제3자의 입장에서 내 코드가 어떻게 보이는지 코드리뷰는 정말 귀중하다고 느끼는 문제였다.


트러블 슈팅(3)

  • 세션을 적용함에 따라 여러 예외 처리가 발생하고 그걸 컨트롤러와 서비스에 남발함에 따라 점점 서비스와 컨트롤러에 중복되는 예외처리가 발생하면 어쩌지? 라는 생각이 들었다.
public ResponseEntity<Void> deleteUser(
        @SessionAttribute(name = "loginUser", required = false) SessionUser sessionUser,
        @PathVariable Long userId) {

    if (sessionUser == null) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
    if (!sessionUser.getId().equals(userId)) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }

    userService.delete(userId);
    return ResponseEntity.noContent().build();
}


---------------------------------------

User user = userRepository.findById(userId)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다."));

        if (!user.getPassword().equals(password)) {
            throw new IllegalArgumentException("비밀번호가 올바르지 않습니다.");
        }
        if (!user.getId().equals(loginUserId)) {
            throw new IllegalArgumentException("해당 유저의 권한은 없습니다.");
        }
        
        
        
 ---------------------------------------
 
 else if(!sessionUser.getId().equals(userId)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); }
 
 
 
  • 다음과 같이 여러 에러를 보고 만들다보니 유저 아이디를 검증하는 로직이 중복되는 상황이 발생했다.

  • 그래서 고민을 해보았다. 결국엔 컨트롤러에 역할과 서비스에 역할이 뭐지? 이러한 문제가 발생안되게 하기 위해 객체지향인 자바를 쓰고 있는 것이 아닌가? 라는

  • 따라서 컨트롤러는 로그인 여부에 대한 검증, 비밀번호나 아이디 일치, 해당된 유저만 할 수있는 권한 검증은 서비스에서 처리하도록 하였다.

  • 구글링을 통해 비즈니스 로직을 다루는 서비스 라는 정의가 있기에 대부분의 예외처리는 서비스 에서 다루되 로그인에 대한 유무를 체크하는 것은 컨트롤러에서 담당하도록 설계하였다.

  • 이로써 설계하기전에 구상을 잘 해야되는 이유인 것 같다.


트러블 슈팅(4)

  • 이후 만든 코드들을 테스트하기 위해 포스트맨을 활용하는데, 위에 앞서 얘기한 것들과 같이 로그인한 상태에서 내가 아닌 다른유저의 일정을 수정/삭제할 수 있는 관리자급 권한을 쥐어주는 진귀한 상황을 만날 수 있었다.

  • 코드를 짜면서 지속적으로 테스트하는 것도 좋지만, 어 설마? 하는 부분도 테스트해보니 당연히 예외처리안한 오류가 발생하게 되었다.

if (!user.getId().equals(loginUserId)) {
            throw new IllegalArgumentException("해당 유저의 권한은 없습니다.");
        }

 if (!schedule.getId().equals(loginUserId)) {
            throw new IllegalArgumentException("해당 유저의 권한은 없습니다.");
        }
  • 다음과 같은 코드를 넣어주었지만 또 문제가 발생하였다.. 유저는 잘 작동하였으나, 일정관리에서 오류가 떴다.

  • 코드를 다시 읽어보니 user에서 쓴 코드와 schedule에서 쓴 코드가 길이가 거의 일치했다는 것이다.

  • 가만생각해보니 스캐줄은 user의 주인인데 어떻게나 user로부터 가져와야하는 입장에서 user랑 코드 길이가 비슷하면 되나? 라는 생각이 들었는데


schedule.getId()SchedulePK(id)
loginUserId는 로그인한 유저의 PK(id)
  • 그래서 Schedule id와 User id를 비교하는 구조인데, 이 둘은 당연히 다르다는게 보였다. 따라서 권한 에러가 발생하는 것이었다..
 if (!schedule.getUser().getId().equals(loginUserId)) {
            throw new IllegalArgumentException("해당 유저의 권한은 없습니다.");
        }
  • 다음과 같이 바꿔주니 잘 작동하였다.

  • 오류가 나도 당황하지말고 코드를 잘 비교해가며 뜯어보자.


트러블 슈팅(5)

  • 또 포스트맨을 통해 기능들을 테스트해보면서 삭제 기능을 써보니 user를 삭제하면서 오류가 떴다.

  • 오류 코드

java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails
  • 뭔가 SQL이 있고 Cannot delete가 있으며,, forekgn key가 적혀있는거 보니 DB에 있는 데이터를 지울 수가 없고 이유는 뭔가 FK에 관련되 있는 것 같았다.

  • 어디서 본 것 같은 느낌이 들었는데 전에 과제를 만들 때에도 보았던 똑같은 오류코드였다.

  • 일정을 생성한 user로부터 user를 삭제하려고 하니 schedule과 연관관계가 맺어져 있는 키값이 관계가 끊어지려고 하니 오류가 난 것이었다.

  • 저번에 배운
    orphanRemoval, cascade = CascadeType.ALL
    두 기능을 통해 해결해보려고 다시 찾아보니 결국에는 user에서도 scheudule을 알아야하고 schedule에서도 user을 알아야하는 상황이라 양방향이어야지만 가능하다는 것을 알게되었다..

  • 한마디로 자식 컬랙션이 부모에 있어야 동작한다는 제한 조건이 있기 때문이다.

  • 따라서 전에 했던 방법인 유저에 있는 스캐줄을 먼저 삭제하고, 유저를 삭제하도록 코드를 수정하도록 하였다.

scheduleRepository.deleteAllByUser(user);
userRepository.delete(user);
  • 쿼리 메서드를 통해서 어떻게 해결하는 방법도 있긴 하겠지만, 아직은 내가 가진 상식 내에서 해결해야 조금 의미가 있지 않을까라는 고집이 있었다.

  • 다음엔 피드백이든 구글링이든 통해서 더 효율적인 방법을 모색해야겠다.


지속적으로 테스트해보며 끝도없는 예외 처리 지옥에 빠지게 되었고, 이는 키오스크 프로젝트를 하면서도 동일했다. 다음 과정으로 빠지기 위해 예외처리를 여기서 잠깐 스탑하고 내일 이 예외처리들을 모두 throw new IllegalArgumentException()처리를 해놨기 때문에 커스텀 예외 클래스와 전역 예외 처리 클래스를 생성해서 도전기능으로 빠르게 넘어가야겠다. 오늘 문제가 많았어서 해결하면서도 뿌듯한 날이었다. 다만 처리하지못한, 보지못한 예외부분들이 신경이 쓰여 아쉽기도 한 날 이었다.

0개의 댓글