📢 오늘의 목표 📢
✔️ 알고리즘, SQL 문제 풀이
✔️ 알고리즘 코드카타
✔️ SQL 코드카타
✔️ 프로그래머스 Level 2
✔️ 스프링 강의 숙련 주차
✔️ 3주차 완강
⏱️ 오늘의 일정 ⏱️
9:00 ~ 10:00 - 알고리즘 코드 카타
10:00 ~ 13:00 - 스프링 숙련 강의 듣기
13:00 ~ 14:00 - 점심시간
14:00 ~ 18:00 - 스프링 숙련 강의 듣기
18:00 ~ 19:00 - 저녁 시간
19:00 ~ 20:00 - 스프링 숙련 강의 듣기
20:00 ~ 21:00 - TIL 작성
📜 Chapter 1. 알고리즘 코드 카타
📜 Chapter 2. 스프링 숙련 강의 듣기 3주 차
✔️ 3-9 필터
Client로 부터 오는 요청과 응답에 대해 최초/최종 단계. 요청과 응답의 정보를 변경하거나 부가적인 기능을 추가할 수 있다. 주로 범용적으로 처리해야 하는 작업들(e.g. 로깅 및 보안) 처리에 활용한다. 인증, 인가와 관련된 로직들을 처리할 수도 있는데, 이를 사용하면 비즈니스 로직과 분리하여 관리할 수 있다는 장점이 있다.
@Slf4j(topic = "LoggingFilter")
@Component
@Order(1)
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 전처리
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI();
log.info(url);
chain.doFilter(request, response); // 다음 Filter 로 이동
// 후처리
log.info("비즈니스 로직 완료");
}
}
로그 기능 사용을 위해 @Slf4j 어노테이션 추가.
@Order 어노테이션으로는 필터의 순서 지정 가능.
필터를 만들 경우 doFilter 메서드를 구현해야 하는데 전처리 작업을 하고 chain.doFilter 호출 후 (다음 filter로 이동한다.) 후처리 작업을 해주면 된다.
여기서는 url를 로그에 찍어주고 있다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI();
if (StringUtils.hasText(url) &&
(url.startsWith("/api/user") || url.startsWith("/css") || url.startsWith("/js"))
) {
// 회원가입, 로그인 관련 API 는 인증 필요없이 요청 진행
chain.doFilter(request, response); // 다음 Filter 로 이동
} else {
// 나머지 API 요청은 인증 처리 진행
// 토큰 확인
String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest);
if (StringUtils.hasText(tokenValue)) { // 토큰이 존재하면 검증 시작
// JWT 토큰 substring
String token = jwtUtil.substringToken(tokenValue);
// 토큰 검증
if (!jwtUtil.validateToken(token)) {
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 사용자 정보 가져오기
Claims info = jwtUtil.getUserInfoFromToken(token);
User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
new NullPointerException("Not Found User")
);
request.setAttribute("user", user);
chain.doFilter(request, response); // 다음 Filter 로 이동
} else {
throw new IllegalArgumentException("Not Found Token");
}
}
}
또 인증 인가를 위한 필터도 만들었다.
회원가입, 로그인 관련 API와 css, js에서는 이 로직을 제외한다.
✔️ 3-10 'Spring Security' 프레임워크
스프링 시큐리티는 filter로 작동.
@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
// .requestMatchers("/api/user/**").permitAll() // 로그인, 회원가입 허용
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
// 로그인 사용
http.formLogin(Customizer.withDefaults());
return http.build();
}
인증 인가를 편하게 할 수 있다.
resources(위에서 css, js를 제외했던 것) 접근은 인증 인가 처리에서 제외한다.
또한 user 아래의 주소 api도 제외하고 있다.
그리고 http.formLogin은 시큐리티에서 제공하는 기본적인 로그인 기능이다.
이 동작 원리에 대해서는 정말 뭔 말인지 모르겠어서 그냥 한 귀로 듣고 한 귀로 흘렸다...
강의 다 듣고 다시 들어봐야겠다.
이래서 4주차부터 들으라 하였구나.
✔️ 3-11 Spring Security 로그인
// 로그인 사용
http.formLogin((formLogin) ->
formLogin
// 로그인 View 제공 (GET /api/user/login-page)
.loginPage("/api/user/login-page")
// 로그인 처리 (POST /api/user/login)
.loginProcessingUrl("/api/user/login")
// 로그인 처리 후 성공 시 URL
.defaultSuccessUrl("/")
// 로그인 처리 후 실패 시 URL
.failureUrl("/api/user/login-page?error")
.permitAll()
);
디폴트가 아닌 직접 만든 로그인 페이지를 넣고 싶을 때 이렇게 매핑한다.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
return new UserDetailsImpl(user);
}
}
public class UserDetailsImpl implements UserDetails {
private final User user;
public UserDetailsImpl(User user) {
this.user = user;
}
public User getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
시큐리티 디폴트인 UserDetails와 UserDetailsService 대신 구현체를 만들어 사용한다.
@Controller
@RequestMapping("/api")
public class ProductController {
@GetMapping("/products")
public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
// Authentication 의 Principle
User user = userDetails.getUser();
System.out.println("user: " + user.getUsername());
return "redirect:/";
}
}
Principal에 UserDetail이 있기 때문에 @AuthenticationPrincipal 어노테이션으로 가져올 수 있음.
✔️ 3-12 Spring Security JWT 로그인
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도");
try {
LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getUsername(),
requestDto.getPassword(),
null
)
);
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("로그인 성공 및 JWT 생성");
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String token = jwtUtil.createToken(username, role);
jwtUtil.addJwtToCookie(token, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info("로그인 실패");
response.setStatus(401);
}
}
기본 방식은 세션 방식이기 때문에 UsernamePasswordAuthenticationFilter를 구현해 JWT 방식으로 커스텀한다.
그리고 듣다가 정말... 뭔 말인지 안들어와서 그냥 스킵했다 그냥 4주차 듣고 듣자...
✔️ 3-13 접근 불가 페이지 만들기
스프링 시큐리티 부분이라 여기도 스킵했다.
✔️ 3-14 Validation
오 이건 입문 주차 과제를 하면서 했던 부분이라 익숙하다.
@Getter
public class ProductRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Positive(message = "양수만 가능합니다.")
private int price;
@Negative(message = "음수만 가능합니다.")
private int discount;
@Size(min=2, max=10)
private String link;
@Max(10)
private int max;
@Min(2)
private int min;
}
RequestDto 객체에 유효성 검사 애너테이션을 추가하고
@PostMapping("/validation")
@ResponseBody
public ProductRequestDto testValid(@RequestBody @Valid ProductRequestDto requestDto) {
return requestDto;
}
컨트롤러에서 검사 할 RequestDto 앞에 @Valid 애너테이션을 달아준다.
✔️ 3-15 Validation 예외처리
@PostMapping("/user/signup")
public String signup(@Valid SignupRequestDto requestDto, BindingResult bindingResult) {
// Validation 예외처리
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
if(fieldErrors.size() > 0) {
for (FieldError fieldError : bindingResult.getFieldErrors()) {
log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
}
return "redirect:/api/user/signup";
}
userService.signup(requestDto);
return "redirect:/api/user/login-page";
}
예외가 발생하면 BindingResult 객체에 오류에 대한 정보가 담긴다.
bindingResult.getFieldErrors()로 발생한 오류들에 대한 정보가 담긴 List<FieldError> 리스트를 가져온다.
📜 Chapter 3. 스프링 숙련 강의 듣기 4주 차
✔️ 4-1 RestTemplate이란 무엇일까?
Spring에서는 서버에서 다른 서버로 간편하게 요청할 수 있도록 RestTemplate 기능을 제공하고 있다.
다른 API를 쓰기 위해 사용.
✔️ 4-2 RestTemplate의 Get 요청
private final RestTemplate restTemplate;
public RestTemplateService(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
RestTemplate을 주입을 받을 순 없다.
대신 Builder를 주입 받아 RestTemplate을 Build하면 됨.
public ItemDto getCallObject(String query) {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/get-call-obj")
.queryParam("query", query)
.encode()
.build()
.toUri();
log.info("uri = " + uri);
ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class);
log.info("statusCode = " + responseEntity.getStatusCode());
return responseEntity.getBody();
}
입력 받은 query를 넣어 동적으로 URL을 만들 수 있다.
resttemplate에서도 json 형식을 자동으로 직렬화 역직렬화 해준다.
중첩 json 형태로 받아와 자동 역직렬화가 안될 때는
public List<ItemDto> fromJSONtoItems(String responseEntity) {
JSONObject jsonObject = new JSONObject(responseEntity);
JSONArray items = jsonObject.getJSONArray("items");
List<ItemDto> itemDtoList = new ArrayList<>();
for (Object item : items) {
ItemDto itemDto = new ItemDto((JSONObject) item);
itemDtoList.add(itemDto);
}
return itemDtoList;
}
public ItemDto(JSONObject itemJson) {
this.title = itemJson.getString("title");
this.price = itemJson.getInt("price");
}
이렇게 직접 변환하여 Dto 객체 리스트로 만들어주기도 한다.
그리고 api를 제공하는 서버에서는
@Getter
public class ItemResponseDto {
private final List<Item> items = new ArrayList<>();
public void setItems(Item item) {
items.add(item);
}
}
이런식으로 Dto의 리스트가 아닌, 엔티티 리스트를 가지고 있는 Dto도 만들 수 있다.
이런 형식도 자주 사용한다고 한다.
✔️ 4-3 RestTemplate의 Post 요청
public ItemDto postCall(String query) {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/post-call/{query}")
.encode()
.build()
.expand(query)
.toUri();
log.info("uri = " + uri);
User user = new User("Robbie", "1234");
ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);
log.info("statusCode = " + responseEntity.getStatusCode());
return responseEntity.getBody();
}
방금은 queryparam 방식으로 주소를 만들었는데 이번엔 path variable 방식을 사용한다.
또한 Post방식을 사용하였는데 uri 다음에 보낼 객체를 넣어준다는 차이가 있다. 그 외 딱히 다른 점 없다.
✔️ 4-4 RestTemplate의 echange
public List<ItemDto> exchangeCall(String token) {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/exchange-call")
.encode()
.build()
.toUri();
log.info("uri = " + uri);
User user = new User("Robbie", "1234");
RequestEntity<User> requestEntity = RequestEntity
.post(uri)
.header("X-Authorization", token)
.body(user);
ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
return fromJSONtoItems(responseEntity.getBody());
}
메서드 명시가 되지 않은 exchange의 경우에는 위에서 RequestEntity라는 객체를 만들어 보낼 때 사용하는데, 대신 이 객체에 메서드, uri, 헤더, 바디를 넣는다.
예제에서는 헤더에 쿠키를 넣어 보내고 있다.
✔️ 4-5 Naver Open API
https://developers.naver.com/products/intro/plan/
네이버에서도 오픈 API를 제공한다.
얘는 이전에도 써본 적이 있어서 익숙하다... 애초에 원래 클라이언트 프로그래머였다 보니 API를 쓰는 건 익숙하다.
오픈 API도 사용하는 방법도 별반 다르지 않다.
🌙 오늘을 마치며 🌙
힘들다...