💻문제점1
클라이언트에서 다음과 같이 결제가 성공하면 POST 요청을 보내도록 했다.
if (rsp.paid_amount === data.response.amount) {
alert('결제가 완료되었습니다.');
const requestBody = {
points: parseInt(churOption.substring(0, churOption.length - 2)),
};
try {
console.log("충천 포인트: ", churOption.substring(0, churOption.length - 2));
const { data } = await axios.post('http://localhost:8080/points', requestBody, { headers });
} catch(error) {
console.error('Error while points request:', error);
}
} else {
alert('결제가 실패했습니다.');
}
그런데 CORS 오류가 떴다...?
기존에 CORS를 허용해주는 코드는 다음과 같다.
CORSFilter
@Configuration
@EnableWebFlux
public class CORSFilter implements WebFluxConfigurer {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.exposedHeaders("Access_Token", "Refresh_Token");
}
}
WebfluxSecurityConfiguration
@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class WebfluxSecurityConfiguration {
private final AuthenticationManager authenticationManager;
private final SecurityContextRepository securityContextRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity serverHttpSecurity) {
serverHttpSecurity
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/members/signup", "/members/login", "ws/**").permitAll()
.pathMatchers("/broadcasts/**", "/streams/**", "/verifyIamport/**").permitAll()
.anyExchange().authenticated())
.securityContextRepository(securityContextRepository)
.authenticationManager(authenticationManager)
.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
.accessDeniedHandler((swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
.authenticationEntryPoint((swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))));
return serverHttpSecurity.build();
}
}
🔍문제점1-해결
다음과 같이 CorsWebFilter를 빈으로 등록했다.
CorsWebFilter는 SPring Webflux에서 제공하는 내장된 CORS 필터다.
여기서 CorsConfiguration 객체를 생성해서 필요한 구성 정보를 설정했다.
CORSFilter
@Configuration
@EnableWebFlux
public class CORSFilter {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Collections.singletonList("*"));
corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT"));
corsConfig.setAllowedHeaders(Collections.singletonList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
corsConfig.setExposedHeaders(Arrays.asList("Access_Token", "Refresh_Token"));
return new CorsWebFilter(source);
}
}
WebfluxSecurityConfiguration
CORSFilter를 주입하고, addFilterAt을 사용해서 필터를 등록했다.
여기서 두 번째 인자인 SecurityWebFiltersOrder.CORS는 내가 추가하려는 필터를 SecurityWebFiltersOrder.CORS에 위치시키는 것이다.
입력된 인자를 통해 필터를 생성하고 순서에 맞게 필터가 배치된다.
@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class WebfluxSecurityConfiguration {
private final AuthenticationManager authenticationManager;
private final SecurityContextRepository securityContextRepository;
private final CORSFilter corsFilter; //추가
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity serverHttpSecurity) {
serverHttpSecurity
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/members/signup", "/members/login", "ws/**").permitAll()
.pathMatchers("/broadcasts/**", "/streams/**", "/verifyIamport/**").permitAll()
.anyExchange().authenticated())
.securityContextRepository(securityContextRepository)
.authenticationManager(authenticationManager)
.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
.accessDeniedHandler((swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
.authenticationEntryPoint((swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))))
.addFilterAt(corsFilter.corsWebFilter(), SecurityWebFiltersOrder.CORS); //추가
return serverHttpSecurity.build();
}
}
public enum SecurityWebFiltersOrder {
FIRST(Integer.MIN_VALUE),
HTTP_HEADERS_WRITER,
/**
* {@link org.springframework.security.web.server.transport.HttpsRedirectWebFilter}
*/
HTTPS_REDIRECT,
/**
* {@link org.springframework.web.cors.reactive.CorsWebFilter}
*/
CORS,
//생략
}
💻문제점2
MemberService
@Transactional
public Mono<ResponseEntity<String>> signup(SignupRequestDto signupRequestDto) {
return memberRepository
.existsByUserId(signupRequestDto.getUserId())
.flatMap(exists -> {
if (exists) return Mono.error(new IllegalArgumentException("중복된 아이디입니다."));
else {
return memberRepository
.existsByNickname(signupRequestDto.getNickname())
.flatMap(duplicated -> {
if (duplicated) return Mono.error(new IllegalArgumentException("중복된 닉네임입니다."));
else return memberRepository
.save(new Member(signupRequestDto, passwordEncoder.encode(signupRequestDto.getPassword()), MemberRoleEnum.USER))
.onErrorResume(exception -> {
return Mono.error(new RuntimeException("회원 정보 저장 오류"));
})
.flatMap(member -> {
return channelRepository
.save(new Channel(member.getNickname()))
.onErrorResume(exception -> {
return Mono.error(new RuntimeException("회원 정보 저장 오류"));
});
})
.thenReturn(ResponseEntity.ok(signupRequestDto.getNickname() + "님 회원 가입 완료"));
});
}
});
}
이제 서비스에서 결제 시스템을 사용하여 츄르... 즉 포인트를 구매할 수 있기 때문에 회원가입할 때 Point 테이블에 컬럼을 추가해주어야 했다. 현재 return channelRepository.save()를 하는 flatMap 이후에 또다시 faltMap을 하면 channel 객체가 들어와서 어디서 pointRepository.save를 해줄까 고민이었다.
📃문제점2-시도
channelRepository에 save를 한 뒤 다시 flatmap을 통해 기존 member 객체를 계속 사용하도록 했다. 이렇게 되면 channelRepository save() 후에 pointRepository가 save() 가 호출된다. 사실 저장 순서가 필요하지 않는 작업인데.. 흠
@Transactional
public Mono<ResponseEntity<String>> signup(SignupRequestDto signupRequestDto) {
return memberRepository.existsByUserId(signupRequestDto.getUserId())
.flatMap(exists -> {
if (exists) {
return Mono.error(new IllegalArgumentException("중복된 아이디입니다."));
} else {
return memberRepository.existsByNickname(signupRequestDto.getNickname())
.flatMap(duplicated -> {
if (duplicated) {
return Mono.error(new IllegalArgumentException("중복된 닉네임입니다."));
} else {
return memberRepository.save(new Member(signupRequestDto, passwordEncoder.encode(signupRequestDto.getPassword()), MemberRoleEnum.USER))
.flatMap(member -> {
return channelRepository.save(new Channel(member.getNickname()))
.flatMap(channel -> {
return pointRepository.save(new Point(member.getUserId(), 0))
.thenReturn(ResponseEntity.ok(signupRequestDto.getNickname() + "님 회원 가입 완료"));
});
});
}
});
}
});
}
zip을 사용해서 세 가지 작업을 병렬적으로 처리하고 마지막에 ResonseEntity를 반환하게도 짜봤다.
@Transactional
public Mono<ResponseEntity<String>> signup(SignupRequestDto signupRequestDto) {
return memberRepository.existsByUserId(signupRequestDto.getUserId())
.flatMap(exists -> {
if (exists) {
return Mono.error(new IllegalArgumentException("중복된 아이디입니다."));
} else {
return memberRepository.existsByNickname(signupRequestDto.getNickname())
.flatMap(duplicated -> {
if (duplicated) {
return Mono.error(new IllegalArgumentException("중복된 닉네임입니다."));
} else {
Mono<Member> memberMono = memberRepository.save(new Member(signupRequestDto, passwordEncoder.encode(signupRequestDto.getPassword()), MemberRoleEnum.USER))
.onErrorResume(exception -> Mono.error(new RuntimeException("회원 정보 저장 오류")));
Mono<Channel> channelMono = memberMono.flatMap(member -> channelRepository.save(new Channel(member.getNickname()))
.onErrorResume(exception -> Mono.error(new RuntimeException("회원 정보 저장 오류"))));
Mono<Point> pointMono = memberMono.flatMap(member -> pointRepository.save(new Point(member.getUserId(), 0)));
return Mono.zip(memberMono, channelMono, pointMono)
.thenReturn(ResponseEntity.ok(signupRequestDto.getNickname() + "님 회원 가입 완료"));
}
});
}
});
}
🔍문제점2-해결
일단은 다음과 같이 코드를 짰다.
memberRepository에 save를 하고 반환한 Mono<Member> 객체를 사용해 flatMap으로 channelRepository와 pointRepository에 저장을 수행하고 Mono.zip을 통해 channelMono와 pointMono 작업을 병렬적으로 수행하게 했다.
@Transactional
public Mono<ResponseEntity<String>> signup(SignupRequestDto signupRequestDto) {
return memberRepository.existsByUserId(signupRequestDto.getUserId())
.flatMap(exists -> {
if (exists) {
return Mono.error(new IllegalArgumentException("중복된 아이디입니다."));
} else {
return memberRepository.existsByNickname(signupRequestDto.getNickname())
.flatMap(duplicated -> {
if (duplicated) {
return Mono.error(new IllegalArgumentException("중복된 닉네임입니다."));
} else {
Mono<Member> memberMono = memberRepository.save(new Member(signupRequestDto, passwordEncoder.encode(signupRequestDto.getPassword()), MemberRoleEnum.USER))
.onErrorResume(exception -> Mono.error(new RuntimeException("회원 정보 저장 오류")));
return memberMono
.flatMap(member -> {
Mono<Channel> channelMono = channelRepository.save(new Channel(member.getNickname()))
.onErrorResume(exception -> Mono.error(new RuntimeException("회원 정보 저장 오류")));
Mono<Point> pointMono = pointRepository.save(new Point(member.getUserId(), 0));
return Mono.zip(channelMono, pointMono)
.thenReturn(member);
}).thenReturn(ResponseEntity.ok(signupRequestDto.getNickname() + "님 회원 가입 완료"));
}
});
}
});
}
테이블을 생성해주고 PostMan으로 테스트 해보자
CREATE TABLE points
(
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
points INTEGER NOT NULL
);
'TIL' 카테고리의 다른 글
[TIL - 20230622] K6 성능 테스트 시나리오 작성 (0) | 2023.06.23 |
---|---|
[TIL - 20230621] Webflux R2DBC 후원 동시성 문제 해결 (0) | 2023.06.22 |
[TIL - 20230616] (0) | 2023.06.17 |
[TIL - 20230615] EC2 + Docker Compose 통한 배포 (0) | 2023.06.16 |
[TIL - 20230614] docker compose (0) | 2023.06.15 |