본문 바로가기
Study in Bootcamp/Pre-Project

프리 프로젝트 회고

by Bhinney 2022. 11. 8.

✔️Date : 2022.10.20 ~ 2022.11.07


What did you do?

✅ Spring Security와 JWT를 이용하여 로그인 구현

: Spring Security와 JWT를 이용하여 로그인을 구현하였다. DB에 저장된 유저가 자신의 아이디와 비밀번호로 로그인을 하면, 헤더로 Access Token(Authorization)과 Refresh Token을 발급해주도록 설정하였다. 사실 토큰을 발급받고 흘러가는 흐름을 명확하고 정확하게 100프로 이해했다고 얘기하지는 못한다. 하지만 그래도 전보다는 그래도 어떻게 흘러가는 지는 느껴졌다.

   Security FilterChain을 이용하여 조금 쉽게 로그인 로직을 구현할 수 있었다. 중간 중간 커스텀이 필요한 부분은 직접 커스텀을 하면서 진행하였다. 이 부분에서 가장 기억에 남는 것은 CorsFilter를 구현한 것과 OncePerRequestFilter를 상속받아 구현한 JwtVerificationFilter이다.

    먼저 CorsFilter를 이야기 해보려고 한다. CorsConfigurationSource라는 인터페이스를 이용하여 구현하였다.  .cors(withDefaults)를 선언하면 빈으로 등록한 CorsConfigurationSource에 정의된 대로 cors를 커스텀 할 수 있었다. 그래서 해당 인터페이스를 이용해서 커스텀하였다.  특히 "OPTTIONS"는 진짜 찾는 데 오래 걸렸다. 기본 CRUD 메서드만 선언해 뒀는데, 회원가입 과정에서 계속 문제가 생겼다. 그래서 이것 저것 하다가 OPTIONS가 허용되지 않은 메서드였다. 그래서 뒤늦게 추가를 해 주었다. 또 .addAllowedOriginPattern() 이것은 다른 메서드와 충돌되어 이렇게 바꿔준 것이었다. 그리고 마지막으로 알게 된 .addExposedHeader(). 기본적으로 허용되는 헤더 외에 필요한 헤더를 허용해야했다. 그 해당사항이 Authorization(Access Token) 과 Refresh Token이었다. 이것도 통신하는 과정에서 토큰이 클라이언트의 헤더로 응답이 오지 않으면서 알게 되었다. 특히 이번 프리 프로젝트를 하면서 CORS 가 얼마나 무서운 존재인지 알게 되었다. 사실 이론상으로 봤을 때, 그리고 솔로 프로젝트를 통해 봤을 때에는 이렇게 힘들게 하는 존재인 줄 몰랐다. 하지만 이번에 정말 많은 부분의 통신을 막고 힘들게 하였다. 덕분에 조금 알게 되었고, 다음 메인 프로젝트 때에는 어떻게 시작을 해야할 지 생각을 하게 되었다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		.headers().frameOptions().sameOrigin()
		.and()
		.csrf().disable()
		.cors(withDefaults())
		.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		.and()
		.formLogin().disable()
		.httpBasic().disable()
		.exceptionHandling()
		.authenticationEntryPoint(new UserAuthenticationEntryPoint())
		.accessDeniedHandler(new UserAccessDeniedHandler())
		.and()
		.apply(new CustomFilterConfigurer())
		.and()
		.authorizeHttpRequests(authorize -> authorize
			.antMatchers(HttpMethod.OPTIONS).permitAll()
			// .antMatchers(HttpMethod.POST, "/signup").permitAll()
			// .antMatchers(HttpMethod.PATCH, "/users/**").hasRole("USER")
			// .antMatchers(HttpMethod.GET, "/users/**").hasRole("USER")
			// .antMatchers(HttpMethod.DELETE, "/users/**").hasRole("USER")
			// .antMatchers(HttpMethod.POST, "/questions/ask").hasRole("USER")
			// .antMatchers(HttpMethod.PATCH, "/questions/**").hasRole("USER")
			// .antMatchers(HttpMethod.GET, "/questions").permitAll()
			// .antMatchers(HttpMethod.GET, "/questions/**").permitAll()
			// .antMatchers(HttpMethod.DELETE, "/questions/**").hasRole("USER")
			// .antMatchers(HttpMethod.POST, "/**/answers").hasRole("USER")
			// .antMatchers(HttpMethod.PATCH, "/**/answers/*").hasRole("USER")
			// .antMatchers(HttpMethod.DELETE, "/**/answers/*").hasRole("USER")
			// .antMatchers(HttpMethod.POST, "/**/comments").hasRole("USER")
			// .antMatchers(HttpMethod.PATCH, "/**/comments/*").hasRole("USER")
			// .antMatchers(HttpMethod.DELETE, "/**/comments/*").hasRole("USER")
			.anyRequest().permitAll());
	return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
	CorsConfiguration configuration = new CorsConfiguration();
	configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE", "OPTIONS"));
	UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration);
	configuration.addAllowedHeader("*");
	configuration.addExposedHeader("Authorization");
	configuration.addExposedHeader("Refresh");
	configuration.addAllowedOriginPattern("*");
	configuration.setAllowCredentials(true);
	configuration.setMaxAge(5000L);

	return source;
}

   두 번째로 JwtVerificationFilter와 UserAuthenticationSuccessHandler를 이야기 해보려고 한다. 이 또한 마지막에 클라이언트에게 헤더로 응답이 가고, 클라이언트로부터 헤더로 응답이 오는 과정에서 계속 문제가 발생하고 어려워서 많이 만져보고 고쳐보면서 알게 되었다. 그리고 필요한 부분을 쿠키로 보내는 로직도 구현했다. 주석처리는 원래 구현했던 것이고, 밑에 내용은 팀에 맞추어 수정한 내용이다. 이러면서 다시 한번 더 토큰을 판별하는 것을 상기시킬 수 있었다. 또한 회원 인증에 성공하면 즉, 로그인에 성공했을 때 응답 데이터도 다시 만들어보았다. 생각하는 데로 구현이 되어서 신기했다.

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
	// String authorization = request.getHeader("Authorization");
	String authorization = request.getHeader("Cookie");
	// return authorization == null || !authorization.startsWith("Bearer");
	return authorization == null || !authorization.startsWith("Authorization");

}
private Map<String, Object> verifyJws(HttpServletRequest request) {
	// String jws = request.getHeader("Authorization").replace("Bearer", "");
	String jws = request.getHeader("Cookie").replace("Authorization=Bearer", "");
	String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
	Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();
	return claims;
}

 

@Slf4j
public class UserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
		HttpServletResponse response, Authentication authentication) throws IOException {
		log.info("# 인증 성공");
		// log.info("response.getHeader() : {}", response.getHeader("Authorization"));

		Gson gson = new Gson();
		String cookieValue = response.getHeader("Authorization");
		Cookie cookie = new Cookie("Authorization", cookieValue);

		UsersDetailService.UsersDetail usersDetail = (UsersDetailService.UsersDetail) authentication.getPrincipal();

		LoginResponse loginResponse =
			new LoginResponse(usersDetail.getId(), usersDetail.getDisplayName(), usersDetail.getAvatarColor());
		response.setHeader("Authorization", response.getHeader("Authorization"));
		response.addCookie(cookie);
		response.setContentType(MediaType.APPLICATION_JSON_VALUE);
		response.setStatus(HttpStatus.OK.value());
		response.setCharacterEncoding("UTF-8");
		response.getWriter().write(gson.toJson(loginResponse, LoginResponse.class));
	}
}

✅ 회원 가입, 수정, 조회, 탈퇴 로직 구현

: USER의 CRUD를 구현하고, 시간에 쫓기다가 마지막에 테스팅을 했다. 그랬더니 랜덤으로 초기값을 준 컬러와 암호화한 패스워드에서 계속 테스팅이 안되었다...ㅎ (미리미리 하자) 다음 프로젝트 때에는 구현이 끝나면 바로 테스팅을 하고 그 다음 작업에 들어가야지! 

다시 CRUD를 구현하면서 섹션 3의 Spring MVC를 복습할 수 있었다. 정말 기본 of 기본을 구현해서 사실 기본 기능만 작동하게 구현되어 있다. 이 외에 다른 기능들은 없...다... 그나마 다른게 있다면, 회원 탈퇴 로직? 회원이 아예 삭제되는 것이 아닌, 회원의 상태만 바꾸었다. 근데 이거는 먼저 Role을 정했다면, Role로 정했으면 좋았을 것 같다. 그러면 상대적으로 security에서 거를 수 있는 게 많으니까! 다음에 유저를 하게 된다면 한 번 도전해 보는 것도 나쁘지 않을 것 같다. 회원 가입 수정 조회는 사실 크게 어렵지 않았다. CORS 오류만 빼고는 사실 기본 클래스를 따라서 구상했고, 흘러갔다. 다른 팀원 분들이 검색하고 찾아서 발전시키는 걸 보면서 다음에 나도 저렇게 구현해봐야지 하는 다짐도 했다.

     사실 나보다는 질문 조회 파트가 할 게 많았다. 그래서 그 파트의 코드도 계속 보면서 다시 복습하고 있는데 확실히 어렵다. 처음에 이론상으론 가능하다고 생각했지만 질문에서 답변과 댓글의 모든 것을 받아오는게 어려웠다. 또 이렇게 하면서 댓글과 답변의 페이지 조회는 패스했는데, 한번에 데려오니 정렬이나 페이지를 나누는 것이 거의 불가능이라는 것을 깨달았다. 추천을 하는 부분도 확인할 것도 많고, 구현을 위해 반복문을 돌린 부분도 존재했다. 나는 그 코드를 이해하는 데에도 벅찼는데, 로직을 짠 팀원 분은 정말 대단하신 것 같다.... (존경)


✅ 연관관계 매핑, 예외 처리

: 공통의 클래스를 초반에 먼저 구현하고 시작했다. 그 때 나는 예외 처리 파트를 하고, 그 이후에 연관관계도 매핑하였다. 이 때 많은 것들이 있었다. 연관관계 매핑은 중간에 한 번 수정했다. 처음에는 일대일로 생각하고 했으나, 로직을 생각하고 관계성을 다시 한 번 더 생각해보니 일대 다 인관계였다. 그래서 수정하고 작업했다. 그리고 이 때, 당시에는 제대로 이해하지 못한 연관관계 메서드를 다시 한 번 더 공부할 수 있는 기회가 주어졌다. 그 부분이 좋았다. 팀원 분의 부탁에 설명을 각각의 메서드를 설명  했으나, 내가 개떡같이 설명해서 조금 창피해서 더 공부를 했다. 연관관계 매핑은 근데 확실히 생각하면 할 수록 어려운 파트인 것 같다. 

   예외처리는 사실 다음에 꼭 바꾸고 싶은 부분이 존재한다. 사실 처음에 그냥 아무런 생각 없이 그동안 짰던 로직을 바탕으로 설계를 해서 처리를 했다. 그러다 보니 커스텀 예외 처리의 오류가 200으로 뜨는 것. 해당 오류를 사실 바꿀 수 있었는데, 이미 프론트 분들이 작업을 하셔서 그냥 냅뒀다. (이건 내 실수다 명백히..에효) 그래서 이번에 예외 처리를 하게 된다면 꼭 이 부분은 제대로 수정하고 넘어가고 싶다!


돌아보기

: 마지막으로 끝내면서 노션에 처음 적었던 소개 글을 다시 읽어보았다. 버스 기사는 못되어도 오른팔이 되겠다던 다짐... 솔직히 잘 지켰는지는 모르겠다. 솔직히 내가 운전기사였으면 이 버스 전복이다..ㅎ 그리고 오른 팔이 될 만큼의 성과는 못 낸 거 같다... 내가 구현하고자 하는 부분들을 다 못했으니까...

그래도 하차하지 않고, 끊기 있게 버티는 중이다. 중간 중간 어려운 부분이 있지만 그래도 나름 재밌게 해 나가고 있는 거 같다. 사실 회고를 지금 몰아서 쓰는 이유는 어쩌다보니 프로젝트 매일매일이 회고처럼 쓰여져서.. 결국 같은말 반복이지 않나 하는 생각 때문이다...ㅎ

처음에 프리 프로젝트를 들어갈 때에는 걱정이 정말 산더미였다. 내가 누군가의 짐이 되어버릴까봐 걱정도 많이 되었다. 그만큼 내가 부족하다는걸 너무 많이 알았고, 그 상태로 프로젝트를 들어가는 게 맞나 라는 생각이 계속 들었던 것 같다. 하지만 프로젝트를 진행하면서 확실히 많이 알게되고, 새롭게 이해되고 알게 되는 것들이 많아 해보는게 좋은거구나 하는 생각을 하게 되었다.

이제 메인 프로젝트를 들어가는데 이번에 못해서 아쉬웠던 부분들을 보완하고, 부족했던 부분들을 다시 채워가면서 준비하고 싶다. 

그래도 좋은 팀원 분들을 만나서 같이 성장하고 배워나갈 수 있고, 그 안에서 자극도 받으면서 뜻 깊은 2주였던 것 같다!

댓글