어떤 자연수 N(1 ≤ N ≤ 10,000,000)에 대해서, 이 N을 몇 개의 연속된 자연수의 합으로 나타내는 가지수를 구하는 문제
연속된 수의 조합이고 N의 범위가 크기 때문에 투 포인터(lt, rt)를 사용해서 조합의 합이 N보다 작으면 rt를 증가, N보다 크면 lt를 증가 시키면서 이동한다.
곧바로 떠오르는 고려할 점은 어디까지 수를 체크 할 것이냐인데 최소한의 연속된 수로 나타낼 수 있는 경우는 N의 절반이 될 것이다.
연속된 숫자가 1개인 경우도 포함되기 때문에 자기 자신도 포함하면 기본값은 1이 될 것
int N = Integer.parseInt(br.readLine());
int end = N / 2;
int count = 1;
int lt = 1;
int rt = 2;
int sum = lt + rt;
while (lt <= end) {
if (sum > N) {
sum -= lt;
++lt;
}
if (sum == N && rt != N) {
++count;
}
if (sum <= N) {
++rt;
sum += rt;
}
}
OAuth(Open Authorization)란 표준 인증 프로토콜로 사용자들이 특정 플랫폼에 있는 자신의 정보에 대해 접근 권한을 부여하는 수단으로 사용이 된다.
사용자 인증(Authentication) 처리를 카카오, 네이버, 구글과 같이 OAuth 로그인 API를 제공하는 대형 서비스에게 맡기고 필요한 권한(Authorization)을 사용자 동의를 통해 받아서 필요한 Resource를 받아오는 것이다.
SecurityConfig
이전에는 스프링 시큐리티 설정을 WebSecurityConfigurerAdapter의 configure()로 했었지만 현재는 deprecated 되어서 SecurityFilterChain을 Bean으로 등록해야 한다.
oauth2Login() 밑에 부분을 보면 userService()에 CustomOAuth2UserService를 등록하는데 이는 함수형 인터페이스인 OAuth2UserService의 loadUser() 메서드를 구현한 것으로 로그인 성공 이후 Access Token을 이용해서 유저 정보(resource owner)를 받아서 OAuth2User(AuthenticatedPrincipal)를 반환한다.
CustomOAuth2UserService
사용자가 성공적으로 인증이 되면 OAuth2User에는 GrantedAutority Set이 포함되어 있는데 이 GrantedAuthority 컬렉션은 반환할 OAuth2AuthenticationToken에 매핑하는데 사용 될 수 있다.
매핑 옵션으로는 GrantedAuthoritiesMapper 또는 OAuth2UserService를 사용한 Delegation-based 전략이 있는데 후자의 경우 OAuth2UserRequest 및 OAuth2User에 대한 액세스를 제공하기 때문에 더 유연하고 향상된 방법이라고 한다. 나는 DefaultOAuth2UserService(표준 OAuth 2.0 공급자를 지원하는 OAuth2UserService의 구현)를 사용하였다.
DefaultOAuth2UserService 생성자를 통해 userRequest로 OAuth2User를 가져오고 Attributes 정보로 User Entity를 조회(기존 회원)하여 가져오거나 저장(새로운 회원)한다.
UserNameAttributeName()은 키 값으로 카카오의 경우 id가 된다
마지막으로 OAuth2User(AuthenticatedPrincipal)를 생성하여 반환한다.
아래는 전체적인 순서도와 내부적으로 어떻게 처리가 되는지 정리를 해봤다.
순서도
먼저 로그인을 누르면 프론트에서 "/oauth2/authorization/kakao"로 요청을 보내고 OAuth2AuthorizationRequestRedirectFilter는 기본 경로로 받아서 yml 파일에 정의해둔 authorization url로 요청을 보낸다.
이때 앱의 Rest API 키 값인 client_id랑 Authorization Code를 받을 redirect_url, response_type을 같이 보내는데 이 정보는 yml 파일에 저장을 해놓는다.
참고로 스프링 시큐리티가 yml 파일을 읽어서 OAuth2ClientProperties를 생성하고 ClientRegistration 객체(kakao, google, naver ..)를 생성해서 InMemoryClientRegistrationRepository에 저장한다.
위에서 말한 OAuth2AuthorizationRequestRedirectFilter는 OAuth2AUthorizationRequestResolver의 resolve()를 호출하는데 로그인 요청 URL에서 registrationId를 확인하고 InMemoryClientRegistrationRepository에서 ClientRegistration 객체를 꺼내 OAuth2AuthorizagtionRequest 객체를 만든다.
그리고 OAuth2AuthorizationRequest를 받아서 redirect를 보내면 카카오 로그인 창이 뜨게 되는 것이다.
사용자가 카카오 로그인 인증을 성공하면 카카오 서버에서는 위에서 요청시 보낸 redirect_url로 인가 코드(Authorization Code)를 보내기 때문에 OAuth2LoginAuthenticationFilter의 기본 경로에 맞춰서 redirect_url을 yml 파일에 정의해두었다.
OAuth2LoginAuthenticationFilter도 OAuth2AuthorizationRequestRedirectFilter처럼 registrationId를 통해 ClientRegistation을 가져오고 Authorization Code를 사용해서 Access Token을 가져온다.
OAuth2LoginAuthenticationProvider는 authenticate() 메서드를 실행하여 Access Token을 받고 loadUser 메서드를 통해 유저 정보를 가져오는 처리까지 하는데 이때 loadUser()를 실행하는 UserService가 처음에 SecuriryConfig에서 userService()에 주입해준 CustomUserService가 된다.
이전에 Swagger로 API 문서를 만들어봤는데 UI도 예쁘게 만들어주고 API 테스트도 해볼 수 있어서 편리하게 사용했었다.
하지만 컨트롤러에 설정 코드가 많이 들어가고 동기화가 되지 않는 단점이 있어서 이번에는 Spring Rest Docs를 공부해서 적용을 해보았다.
Asciidoctor로 만든 문서와 Spring MVC Test로 생성된 AsciiDoc 스니펫을 결합하여 API 문서(HTML)를 만든다고 나와있는데 원하는 경우 Markdown을 사용하도록 Spring REST Docs를 구성할 수도 있다고 한다.
1. 설정
Asciidoctor Plugin
*.adoc으로 생성된 asciidoc 파일을 html로 변환해주는 플러그인, 처음에 "org.asciidoctor.convert" 를 사용했다가 오류가 떠서 확인해보니 Gradle 7 부터는 "org.asciidoctor.jvm.convert"를 사용한다고 한다.
그리고 asciidoctorExtensions 설정도 추가해준다.
snippetsDir에 테스트 빌드로 생성되는(outputs) 스니펫들이 저장될 위치를 지정해준다.
dependsOn은 해당 작업에 의존한다는 의미로 bootJar를 실행하면 asciidoctor가 먼저 실행이 된다. 그리고 finalizedBy는 후행 작업을 명시하는 것으로 마지막에 build/docs/asciidoc/ 에 생성된 html 파일을 static 경로에 복사한다.
블로그 보면서 해보다가 설정 안 맞아서 고생했는데 블로그 글을 쓰면서 공식 문서를 살펴보니 다시 한번 공식 문서의 중요성을 느끼게 된다.
2. 테스트 코드
@AutoConfigureRestDocs 애노테이션으로 Rest Docs 여러 설정을 간편하게 할 수 있다.
우선 순위에 따라 @AutoConfigureRestDocs(), OperationRequestPreprocessor로 url 설정을 할 수 있다.
MockMvc, Rest Assured 등으로 테스트를 작성하면 되는데 API 명세를 ResourceSnippetParameters builder()로 만든다,
3. 결과
퀄리티는 만들기 나름이라고 하는데.. Swagger를 썼을 때의 UI에 비하면 아주 심심한 화면이다. 그래서 더 괜찮은 방법이 없나 찾아보다가 Rest Docs랑 Swagger UI를 결합하는 방식을 소개하는 글을 발견해서 결국 해당 방식으로 변경을 했다!
지도 API에 사용할 좌표 데이터를 DB에 관리하려고 하는 중 지원하는 기능이 없나 찾아보니 MySQL에서 Point 자료형과 JPA에서 Hibernate Spatial을 사용하여 좌표 데이터를 편리하게 다룰 수 있다.
위에 문서를 보면 Hibernate Spatial은 지리 데이터 저장 및 쿼리 기능에 대한 표준화된 교차 데이터베이스 인터페이스를 제공하며 Oracle 10g/11g, PostgreSQL/PostGIS, MySQL 등의 데이터베이스에서 지원 가능하다고 한다. Hibernate Spatial은 JTS와 geolatte-geom이라는 두 라이브러리를 지원하는데 JTS는 사실상의 표준, Geolatte-geom은 JTS에서는 사용할 수 없는 많은 기능을 지원하는 최신 라이브러리다.
Jts, GeoLatte
build.gradle
build.gradle 설정은 간단하다. (원래는 build.gradle에 hibernate 버전을 맞춰서 적어줘야 되는데 Hibernate 5.2.x 이후부터는 org.hibernate:hibernate-spatial만 추가하면 된다고 한다)
Hibernate Spatial은 데이터베이스의 공간 기능을 HQL 및 JPQL 내에서 사용할 수 있도록 Hibernate ORM 방언을 확장할 수 있는데5.6.1 이전의 MySQL 버전은 공간 연산자에 대한 지원이 제한적이었지만5.6.1 버전 이후공간 연산자가 도입되면서 방언MySQLSpatial56Dialect를 통해 새롭고 정확한 연산자를 지원한다.