스프링 부트와 AWS로 혼자 구현하는 웹 서비스
: 인텔리제이, JPA, JUnit 테스트, 그레이들, 소셜 로그인, AWS 인프라로 무중단 배포까지
이동욱 저
Table of Content
코드 수정 이후 발생하는 403 에러
소셜 로그인 구현 이후 로그인→게시글 생성에서 오류가 발생하고 있다. HTTP 응답 코드 중 403번 Forbidden(권한과 관련된 요청 거부) 오류이다.
HTTP Status Code와 오류 해결에 앞서
HTTP 응답은 StatusCode인 숫자로 표현된 코드가 함께 반환된다. 응답 코드와 관련된 정리는 다음과 같다.
특히 오류를 만나는 상황에서는 4XX, 5XX대 오류를 만나게 되며, 가끔 3XX번대의 리다이렉션 관련 오류가 있을 수 있다.
일반적으로 400번대는 프론트엔드 관련 기술과 연결된 상황이 많았다. 좀 더 자세히 살펴보면 HTTP 요청을 보내는 시작점 또는 HTTP 응답을 받는 끝 부분에서 오류가 나타날 수 있기 때문에 완벽하게 프론트엔드만의 문제로 오해하면 안되는것 같다. 이 오류는 서버(백엔드) 콘솔 로그에 기록되지 않는 경우도 있으며, 주로 클라이언트 개발자도구의 콘솔에서 확인 할 수 있다.
500번대 오류는 일반적으로 서버 오류라고 불리우며 주로 백엔드에서의 문제일 가능성이 높다. 서버 실행 단계에서 주로 발생하는 컴파일 에러와 실행 중에 발생하는 런타임 오류 정도로 구분된다. 대부분의 경우 정상적으로 서버를 구성했다고 생각했는데 오류가 발생한다면 오타, 오기입에 서버 로직 자체의 문제일 가능성이 높다. 보통 이런 경우 컴파일 오류로 서버를 시작하는 단계에서부터 컴파일러가 오류를 잡아주는 경우가 많다. 하지만 이 정도로 문제를 발견하고 해결하면 다행이다. 혹시나 모두 정상적으로 점검한 경우에도 오류가 해결되지 않는다면 고려해야 하는 범위가 넓어지는데 원인을 찾기 매우 어려울 수도 있다. 특히 런타임 오류의 경우 우선 라이브러리나 패키지와 관련된 의존성, 버전간의 호환 오류로 까지 볼 수 있고 런타임 환경이 되는 OS자체의 문제까지도 확인해봐야 하기 때문에 다양한 사례들을 찾아보고 진단할 필요성을 느껴왔었다. 특히 유사한 사례를 잘 찾아내서 픽스하는 트러블슈팅을 깔끔하게 마감하는 것이 경험치이며 선배 개발자들의 이런 트러블 슈팅 과정들의 기록들이 빛나는 상황이 많다.
나 또한 이런 사소한 오류들을 타인에게 공유하기 위해서 기록하기도 하지만 나 스스로 찾아내기 쉽고 기억하기 위해서 기록하는 경우가 많다. 특히 트러블슈팅 과정을 정리하여 기록하다보면 동일한 상황에서 대처 속도가 크게 차이났던 경험이 많았다. 초심자 입장에서 처음 보는 오류를 맞이 할 때 마다 덜컥 겁이나는 경우도 많은데 이런 트러블슈팅을 많이 쌓아두어 경험들을 차곡차곡 쌓아두어가는 것도 하나의 재미다.
오류 수정 과정
1. 상태 진단
클라이언트에서의 오류, 요청 실패로 ajax 요청문의 fail 부분에 작성된 alert에 문구가 나타나고 있다.
서버에서는 소셜 회원가입과 로그인이 진행되었으며, 그 이후 게시글 등록과 관련된 insert문은 작동하지 않았으며, 어떤 오류 코드도 발견되지 않고 있다.
우선 403 오류인것에 집중해야 한다. 이전 트러블슈팅 과정에서 오류 코드에 대한 부분을 대충 생각하고 내가 작성했던 코드를 살펴보거나 JDK 버전까지 살펴보는 등 문제 해결 방향을 잘못 잡아서 많은 시간을 허비한 경우가 많았다.
403오류는 Forbidden으로 권한과 관련되있을 가능성이 높다. 또한 로그인은 정상적으로 작동하는데, 게시글을 등록하는 행동에서 해당 오류가 나타나고 있다.
여기서 프로젝트에 최근에 추가한 것을 하나씩 기록을 되짚어 보면
•
AWS EC2 환경을 생성했지만 아직 프로젝트를 넣지 않았다.(전혀 프로젝트와 관련없음)
•
JPQL로 작성된 쿼리문을 QueryDSL로 변경하기 위해서 의존성을 추가하고, 서비스 로직을 QueryDSL을 사용하도록 연결한 과정이 있었다.(로직에 관련 있을 가능성 있음, 하지만 5XX오류가 아닌점에서 아직까지는 가능성이 낮음)
•
구글과 네이버 소셜로그인 기능을 추가했으며 정상 작동하는 것을 확인함(권한과 관련됨, 가장 가능성 높음)
•
CRUD API기능을 구성하고 작동을 확인했음(여기까지 작동되는 것을 정확히 확인했었음)
2. 오류 발생 지점 재확인
분명 소셜 로그인 기능을 추가하면서 부터 게시글 CRUD 기능에 문제가 생긴 것으로 볼 수 있다. 생각해보면 테스트 코드도 소셜로그인을 추가하면서부터 author(작성자) 부분을 하드코딩하지 않고 User 모의객체를 만들고 user를 활용한 것으로 테스트했지만, 실제 로직에서 그 변경에 대한 부분을 수정하지 않은 것으로 기억된다.
우선 git의 장점을 사용 할 수 있는 부분이다. git log를 통해서 커밋id를 찾아낼 수 있고 해당 부분으로 체크하웃하여 버전을 돌아가 볼 수 있다. IntelliJ에서는 GUI로도 아래와 같이 손쉽게 찾아보고 체크아웃 할 수 있다. 터미널을 사용하는 경우 git log —oneline 으로 ID를 알아낸 후 git checkout ~ 로 해당 커밋으로 현재 버전을 옮길 수 있다. 해당 버전으로 체크아웃한 뒤 서버를 실행하여 게시글 기능이 정상 작동하는지 확인했다.
3. 코드 수정 과정을 생각하기
생각해보면 현재 과거 버전을 작동하게 하는 것 중에 ‘작성자’ 부분을 생각해봐야 한다. 게시글의 작성자는 이때 당시 String타입의 author로 선언되어 있었으며 현재는 연관관계를 추가하면서 User타입의 author로 변경되었다.
HTML에서도 게시글을 등록하는 폼에서 작성자가 있는데, 이 부분에 로그인한 유저의 이름이 위치해야 한다. 하지만 현재는 인풋필드에 입력한 값을 ajax로 데이터에 담아 요청하고 있다. 이 부분을 로그인한 유저의 이름이 자동 입력되고, 요청에 전달되도록 변경해야 한다.
하지만 오류를 차분히 다시 생각해보면 오류의 내용은 권한과 관련된 오류가 우선이다. 권한과 관련되면 Spring Security의 설정과 관련된 문제일 가능성이 가장 높다. 따라서 우선순위는 시큐리티와 관련된 설정을 먼저 정상적으로 수정하고, 이후에 위 수정하지 못했던 부분을 해결하는 순서로 해결해야겠다는 방향을 잡았다.
4. Spring Security 설정 확인
우선 접근 권한과 관련된 부분은 http.antMatchers("/api/v1/**").hasRole(Role.USER.name())이 부분과 연관되어 있다. 유저의 역할이 ‘USER’ 인 경우에 /api/v1/~ 라는 요청을 사용 할 수 있는 상태이다.
package com.citefred.ldwspring.config.auth;
import com.citefred.ldwspring.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
Java
복사
5. User 테이블 확인
소셜 로그인을 시도하면 처음 접근한 유저는 자동으로 회원가입되며 로그인이 진행된다. 그럼 회원 가입 당시에 UserRole은 어떻게 주어지는가? 현재 가입된 유저의 역할을 살펴보니 GUEST로 가입되어 있다. 이 부분이 문제의 원인 중 하나다.
우선 정확한 문제인지 확인하기 위하여 강제로 ROLE을 변경해보았다.
UPDATE USER SET ROLE = 'USER' WHERE ID = 1;
하지만 아직도 오류가 발생한다. HTTP요청을 보내는 js에서 로그인된 사람의 정보를 전달하지 않기 때문에 회원자체를 모르는것으로 생각되고 있다.
그럼 기본적으로 GUEST가 아니라 USER로 회원 가입이 가능하도록 변경해보았다. OAuthAttributes.java에서 회원가입시 User의 Role을 결정하고 있다. Role을 User로 변경하여 기본 회원가입이 진행 되면 자동으로 User 권한을 갖도록 변경해보았다.
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
...
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.USER)
.build();
}
}
Java
복사
6. 문제 확인
해당 문제의 정확한 원인은 소셜로그인 구현 이후 “연관관계” 를 설정하면서부터 나타났다. 여러 테스트를 진행했지만 정확히 아래처럼 연관관계를 위해 String Author를 User 타입의 author로 바꾸면서 여러 문제가 발생했으며 해당 코드에서 부터의 잘못된 부분을 다시 고쳐나갈 예정이다.
내가 원하는 것은 테이블의 연관관계를 생성하면서 또한 작성자의 인풋필드는 인덱스 페이지처럼 로그인된 유저로부터 값을 받아오는 것이다. 또한 데이터베이스에서 유저PK인 ID로부터 게시글들을, 게시글로부터 유저의 ID를 양방향으로 조회하는 것을 목표로 한다. 우선 로그인 유저의 정보를 담아내는데 @AuthenticationPrincipal 을 사용했엇는데 이 예제에서는 @LoginUser 라는 커스텀 어노테이션을 생성해서 사용하는데 어떤 차이가 있을지와 작성자가 로그인 유저로부터 연결되고 데이터베이스에서도 정상적으로 양쪽 정보를 가져 올 수 있도록 재구성해야 한다.
연관관계를 통해서 게시글 자체에 User_ID 컬럼이 생겼으며, 기본 상태는 null이 입력되고 있다. 강제로 1번 유저가 등록한것 처럼 나타내면 아래와 같이 나타난다.
주소값을 가져오고 있는데, 그럼 기본적으로 게시글을 등록 할 때 null이 나타나는 것을 로그인된 유저의 id가 입력되도록 만들면 될 것 같다.
because "userDetails" is null
현재 컨트롤러에서 로그인된 유저 정보를 받아오기 위해서 @AuthenticationPrincipal UserDetailsImpl userDetails 를 파라미터로 받아오고있는데 이 부분에서 null이 나타나고있다. 전반적인 스프링 시큐리티 부분을 다시 수정해보고 정상적으로 로그인된 유저의 정보를 받아 올 수 있도록 수정해야 할 필요가 있다.
PostsApiController.java
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public ResponseEntity<MessageDto> save(@RequestBody PostsSaveRequestDto requestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
return ResponseEntity.ok().body(postsService.save(requestDto, userDetails.getUser()));
}
...
}
Java
복사
PostsService.java
...
@Transactional
public MessageDto save(PostsSaveRequestDto requestDto, User loginUser) {
// 로그인한 사용자 확인
validateUserAuthority(loginUser);
Posts newPost = new Posts(requestDto, loginUser);
postsRepository.save(newPost);
return new MessageDto("게시글 추가가 완료되었습니다.");
}
...
Java
복사
Related Posts
Search