diff --git a/basic-tomcat/index.html b/basic-tomcat/index.html index a10dd6f563..f0f1a6cd89 100644 --- a/basic-tomcat/index.html +++ b/basic-tomcat/index.html @@ -313,7 +313,7 @@

References.

Java EE에서 Jakarta EE로의 전환
Java EE Clients
Architecture
-Apache Tomcat Versions

@Hyeonic
나누면 배가 되고
+Apache Tomcat Versions

@Hyeonic
나누면 배가 되고
Google은 Refresh Token을 쉽게 내주지 않는다.

Google은 Refresh Token을 쉽게 내주지 않는다.

@Hyeonic · August 16, 2022 · 12 min read

Google은 Refresh Token을 쉽게 내주지 않는다.

+

우리 달록은 캘린더를 손쉽게 공유할 수 구독형 캘린더 공유 서비스이다. 현재에는 우리 서비스 내에서만 일정이 등록 가능한 상태이다. 추후 확장성을 고려하여 Google Calendar API와 연동하기 위해 Google에서 제공하는 token 정보를 관리해야 하는 요구사항이 추가 되었다.

+

code를 활용한 AccessToken 및 IdToken 발급

+

Google은 OAuth 2.0 요청 때 적절한 scope(e.g. openid)를 추가하면 OpenID Connect를 통해 Google 리소스에 접근 가능한 Access Token, AccessToken을 재발급 받기 위한 Refresh Token, 회원의 정보가 담긴 IdToken을 발급해준다.

+

Access Token의 경우 짧은 만료 시간을 가지고 있기 때문에 google Access Token 재발급을 위한 Refresh Token을 저장하고 관리해야 한다. Refresh TokenAccess Token보다 긴 만료 시간을 가지고 있기 때문에 보안에 유의해야 한다. 그렇기 때문에 프론트 측에서 관리하는 것 보다 달록 DB에 저장한 뒤 관리하기로 결정 하였다. 참고로 Google은 보통 아래와 같은 이유가 발생할 때 Refresh Token을 만료시킨다고 한다.

+

Refresh Token 만료

+
    +
  • 사용자가 앱의 액세스 권한을 취소한 경우
  • +
  • Refresh Token이 6개월 동안 사용되지 않은 경우
  • +
  • 사용자가 비밀번호를 변경했으며 Gmail scope가 포함된 경우
  • +
  • 사용자가 계정에 부여된 Refresh Token 한도를 초과한 경우
  • +
  • 세션 제어 정책이 적용되는 Google Cloud Platform 조직에 사용자가 속해있는 경우
  • +
+

정리하면 Refresh Token은 만료 기간이 비교적 길기 때문에 서버 측에서 안전하게 보관하며 필요할 때 리소스 접근을 위한 Access Token을 발급 받는 형태를 구상하게 되었다.

+

우리 달록은 아래와 같은 형태로 인증이 이루어진다.

+

+ + + oauth flow + +

+
+

달록팀 후디 고마워요!

+
+

프론트 측에서 OAuth 인증을 위해서는 달록 서버에서 제공하는 OAuth 인증을 위한 페이지 uri을 활용해야 한다. 달록 서버는 해당 uri를 생성하여 전달한다. 로직은 아래 코드로 구현되어 있다.

+
@Component
+public class GoogleOAuthUri implements OAuthUri {
+
+    private final GoogleProperties properties;
+
+    public GoogleOAuthUri(final GoogleProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public String generate() {
+        return properties.getOAuthEndPoint() + "?"
+                + "client_id=" + properties.getClientId() + "&"
+                + "redirect_uri=" + properties.getRedirectUri() + "&"
+                + "response_type=code&"
+                + "scope=" + String.join(" ", properties.getScopes());
+    }
+}
+

이제 브라우저에서 해당 uri에 접속하면 아래와 같은 페이지를 확인할 수 있다.

+

+ + + google oauth uri + +

+

계정을 선택하면 redirect uri와 함께 code 값이 전달되고, google의 token을 발급 받기 위해 백엔드 서버로 code 정보를 전달하게 된다. 아래는 실제 code 정보를 기반으로 google token을 생성한 뒤 id token에 명시된 정보를 기반으로 회원을 생성 or 조회한 뒤 달록 리소스에 접근하기 위한 access token을 발급해주는 API이다.

+
@RequestMapping("/api/auth")
+@RestController
+public class AuthController {
+
+    private final AuthService authService;
+
+    public AuthController(final AuthService authService) {
+        this.authService = authService;
+    }
+    ...
+    @PostMapping("/{oauthProvider}/token")
+    public ResponseEntity<TokenResponse> generateToken(@PathVariable final String oauthProvider,
+                                                       @RequestBody final TokenRequest tokenRequest) {
+        TokenResponse tokenResponse = authService.generateToken(tokenRequest.getCode());
+        return ResponseEntity.ok(tokenResponse);
+    }
+    ...
+}
+
    +
  • authService.generateToken(tokenRequest.getCode()): code 정보를 기반으로 google 토큰 정보를 조회한다. 메서드 내부에서 code을 액세스 토큰 및 ID 토큰으로 교환에서 제공된 형식에 맞춰 google에게 code 정보를 전달하고 토큰 정보를 교환한다.
  • +
+

실제 Google에서 토큰 정보를 교환 받는 클라이언트를 담당하는 GoogleOAuthClient이다. 핵심은 인가 코드를 기반으로 GoogleTokenResponse를 발급 받는 다는 것이다.

+
@Component
+public class GoogleOAuthClient implements OAuthClient {
+
+    private static final String JWT_DELIMITER = "\\.";
+
+    private final GoogleProperties properties;
+    private final RestTemplate restTemplate;
+    private final ObjectMapper objectMapper;
+
+    public GoogleOAuthClient(final GoogleProperties properties, final RestTemplateBuilder restTemplateBuilder,
+                             final ObjectMapper objectMapper) {
+        this.properties = properties;
+        this.restTemplate = restTemplateBuilder.build();
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public OAuthMember getOAuthMember(final String code) {
+        // code을 액세스 토큰 및 ID 토큰으로 교환
+        GoogleTokenResponse googleTokenResponse = requestGoogleToken(code);
+        String payload = getPayload(googleTokenResponse.getIdToken());
+        UserInfo userInfo = parseUserInfo(payload);
+
+        String refreshToken = googleTokenResponse.getRefreshToken();
+        return new OAuthMember(userInfo.getEmail(), userInfo.getName(), userInfo.getPicture(), refreshToken);
+    }
+
+    private GoogleTokenResponse requestGoogleToken(final String code) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        MultiValueMap<String, String> params = generateTokenParams(code);
+
+        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
+        return fetchGoogleToken(request).getBody();
+    }
+
+    private MultiValueMap<String, String> generateTokenParams(final String code) {
+        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+        params.add("client_id", properties.getClientId());
+        params.add("client_secret", properties.getClientSecret());
+        params.add("code", code);
+        params.add("grant_type", "authorization_code");
+        params.add("redirect_uri", properties.getRedirectUri());
+        return params;
+    }
+
+    private ResponseEntity<GoogleTokenResponse> fetchGoogleToken(
+            final HttpEntity<MultiValueMap<String, String>> request) {
+        try {
+            return restTemplate.postForEntity(properties.getTokenUri(), request, GoogleTokenResponse.class);
+        } catch (RestClientException e) {
+            throw new OAuthException(e);
+        }
+    }
+
+    private String getPayload(final String jwt) {
+        return jwt.split(JWT_DELIMITER)[1];
+    }
+
+    private UserInfo parseUserInfo(final String payload) {
+        String decodedPayload = decodeJwtPayload(payload);
+        try {
+            return objectMapper.readValue(decodedPayload, UserInfo.class);
+        } catch (JsonProcessingException e) {
+            throw new OAuthException("id 토큰을 읽을 수 없습니다.");
+        }
+    }
+
+    private String decodeJwtPayload(final String payload) {
+        return new String(Base64.getUrlDecoder().decode(payload), StandardCharsets.UTF_8);
+    }
+    ...
+}
+

이제 Google에게 제공 받은 Refresh Token을 저장해보자.

+

Refresh Token에 채워진 null

+

이게 무슨 일인가, 분명 요청 형식에 맞춰 헤더를 채워 디버깅을 해보면 계속해서 null 값으로 전달되고 있는 것이다. 즉, Google 측에서 Refresh Token을 보내주지 않고 있다는 것을 의미한다.

+

+ + + google refresh token null + +

+

다시 한번 액세스 토큰 새로고침 (오프라인 액세스)를 살펴보았다.

+

+ + + google refresh token docs + +

+

정리하면 Google OAuth 2.0 서버로 리디렉션할 때 query parameter에 access_typeoffline으로 설정해야 한다는 것이다. 다시 되돌아 가서 Google 인증 요청을 위한 uri를 생성하는 메서드를 아래와 같이 수정하였다.

+
@Component
+public class GoogleOAuthUri implements OAuthUri {
+
+    private final GoogleProperties properties;
+
+    public GoogleOAuthUri(final GoogleProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public String generate() {
+        return properties.getOAuthEndPoint() + "?"
+                + "client_id=" + properties.getClientId() + "&"
+                + "redirect_uri=" + properties.getRedirectUri() + "&"
+                + "response_type=code&"
+                + "scope=" + String.join(" ", properties.getScopes()) + "&"
+                + "access_type=offline"; // 추가된 부분
+    }
+}
+

이제 다시 요청을 진행해보자! 분명 refresh token이 정상적으로 교환될 것이다.

+

또 다시 Refresh Token에 채워진 null

+

분명 문서에 명시한 대로 설정을 진행했지만 아직도 동일하게 null 값이 채워져 있다.

+

+ + + google refresh token null 2 + +

+
+

해달라는 데로 다해줬는데...

+
+

엄격한 Google

+

Google은 OAuth 2.0을 통해 인증을 받을 때 Refresh Token을 굉장히 엄격하게 다룬다. 사용자가 로그인을 진행할 때 마다 Refresh Token 정보를 주는 것이 아니라, Google에 등록된 App에 최초 로그인 할 때만 제공해준다. 즉, 재로그인을 진행해도 Refresh Token은 발급해주지 않는다.

+

Google의 의도대로 동작하려면 내가 우리 서비스에 최초로 로그인을 진행하는 시점에만 Refresh Token을 발급받고 서버 내부에 저장한 뒤 필요할 때 꺼내 사용해야 한다.

+

하지만 우리 서버는 모종의 이유로 최초에 받아온 Refresh Token을 저장하지 못할 수 있다. 이때 Google OAuth 2.0 서버로 리디렉션할 때 promptconsent로 설정하게 되면 매 로그인 마다 사용자에게 동의를 요청하기 때문에 강제로 Refresh Token을 받도록 지정할 수 있다.

+

이제 진짜 마지막이다. 아래와 같이 수정한 뒤 다시 디버깅을 진행하였다.

+
@Component
+public class GoogleOAuthUri implements OAuthUri {
+
+    private final GoogleProperties properties;
+
+    public GoogleOAuthUri(final GoogleProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public String generate() {
+        return properties.getOAuthEndPoint() + "?"
+                + "client_id=" + properties.getClientId() + "&"
+                + "redirect_uri=" + properties.getRedirectUri() + "&"
+                + "response_type=code&"
+                + "scope=" + String.join(" ", properties.getScopes()) + "&"
+                + "access_type=offline"
+                + "prompt=consent"; // 추가된 부분
+    }
+}
+

+ + + google refresh token success + +

+

정상적으로 발급 되는 것을 확인할 수 있다!

+

문제점

+

하지만 여기서 문제가 하나 있다. 단순히 promptconsent로 설정할 경우 우리 서비스에 가입된 사용자는 Google OAuth 2.0 인증을 진행할 때 매번 재로그인을 진행해야 한다. 이것은 사용자에게 매우 불쾌한 경험으로 다가올 수 있다. 즉 우리는 매번 재로그인을 통해 Refresh Token을 발급 받는 것이 아닌, 최초 로그인 시 Refresh Token을 발급 받은 뒤 적절한 저장소에 저장하고 관리해야 한다.

+

그렇다면 실제 운영 환경이 아닌 테스트 환경에서는 어떻게 해야 할까? 운영 환경과 동일한 Google Cloud Project를 사용할 경우 최초 로그인을 진행할 때 내 권한 정보가 등록된다. 즉 Refresh Token을 재발급 받을 수 없다는 것을 의미한다.

+

우리 달록은 운영 환경과 테스트 환경에서 서로 다른 Google Cloud Project를 생성하여 관리하는 방향에 대해 고민하고 있다. 이미 Spring Profile 기능을 통해 각 실행 환경에 대한 설정을 분리해두었기 때문에 쉽게 적용이 가능할 것이라 기대한다. 정리하면 아래와 같다.

+
    +
  • 운영 환경: Refresh Token 발급을 위해 accept_typeoffline으로 설정한다. 단 최초 로그인에만 Refresh Token을 발급 받기 위해 prompt는 명시하지 않는다.
  • +
  • 개발 환경: 개발 환경에서는 매번 DataBase가 초기화 되기 때문에 Refresh Token을 유지하여 관리할 수 없다. 테스트를 위한 추가적인 Google Cloud Project를 생성한 뒤, accept_typeoffline으로, promptconsent로 설정하여 매번 새롭게 Refresh Token을 받도록 세팅한다.
  • +
+

정리

+

영어를 번역기로 해석한 수준의 문장으로 인해 많은 시간을 삽질하게 되었다. 덕분에 Google에서 의도하는 Refresh Token에 대한 사용 방식과 어디에서 저장하고 관리해야 하는지에 대해 좀 더 깊은 고민을 할 수 있게 되었다. 만약 나와 같은 상황에 직면한 사람이 있다면 이 글이 도움이 되길 바란다!

+

References.

+

dallog repository
+https://github.com/devHudi
+passport.js에서 구글 OAuth 진행 시 Refresh Token을 못 받아오는 문제 해결

@Hyeonic
나누면 배가 되고
+ + \ No newline at end of file diff --git a/identity-strategy/index.html b/identity-strategy/index.html index aa6db2d059..545ed1989d 100644 --- a/identity-strategy/index.html +++ b/identity-strategy/index.html @@ -313,7 +313,7 @@

참고 사항

References.

Returning the Generated Keys in JDBC
Interface Statement
-김영한 지음, 『자바 ORM 표준 JPA 프로그래밍』, 에이콘(2015), p133-135.

@Hyeonic
나누면 배가 되고
+김영한 지음, 『자바 ORM 표준 JPA 프로그래밍』, 에이콘(2015), p133-135.

@Hyeonic
나누면 배가 되고
+나누면 배가 되고
@Hyeonic
나누면 배가 되고

Spring Data JPA Auditing

March 08, 2023

Spring Data JPA Auditing 작성에 사용된 예제 코드는 jpa-auditing에서 확인해볼 수 있다. 언어는 kotlin으로 작성하였다. Spring Data는 엔티티를 하거나 과 를 투명하게 추적할 수 있는 정교한 지원을 제공한다. +해당 기능을 사용하기 위해서는 애노테이션을 사용하거나 인터페이스를 구현하여 정의할 수 있는 auditing…


조금 늦은 2022년 회고

January 12, 2023

2022년은 내게 조금은 특별한 해이다. 단순히 기술적인 성장을 넘어 한 사람으로서의 가치관을 형성할 수 있었던 시기였다. 회고를 통해 지난 1년을 뒤돌아보며 점검할 수 있는 회고를 적어보려 한다. 우아한테크코스 2022년의 시작은 대부분의 시간을 할애한 우테코를 빼놓고 이야기할 수 없을 것 같다. 정말 하고 싶었던 교육이었기 때문에 2021년은 대부분의…


문자열 생성 방식 비교하기

December 11, 2022

Java는 객체지향 언어이기 때문에 기본적으로 제공하는 이 아닌 경우 모두 로 구성되어 있다. 이것은 문자열도 마찬가지다. 다만 은 여타 다른 객체와 차이점을 가지고 +있다. 그것은 바로 을 지원한다는 것이다. 문자열 생성 방법 Java에서 문자열을 생성하는 방법에는 두 가지가 있다. 생성자를 활용한 방식 문자열 리터럴을 활용한 방식 생성자를 활용한 방식 …


스프링이 개선한 트랜잭션 (2)

December 10, 2022

작성에 사용된 예제 코드는 spring-transaction에서 확인해볼 수 있다. 이전 시간에 트랜잭션 추상화를 통해 여러 데이터 접근 기술 변경에 유연한 구조를 만들었다. 또한 트랜잭션 동기화를 통해 멀티 스레드 환경에서도 별도의 커넥션 객체를 사용하여 독립적으로 트랜잭션이 적용될 수 있도록 구현하였다. 이번 시간에는 템플릿 콜백 패턴을 활용한 과 …


스프링이 개선한 트랜잭션 (1)

December 09, 2022

작성에 사용된 예제 코드는 spring-transaction에서 확인해볼 수 있다. 트랜잭션은 논리적인 작업 셋을 모두 완벽하게 처리하거나, 처리하지 못할 경우 원래 상태로 복구하여 작업의 일부만 적용되는 현상(Partial update)을 막아준다. 또한 트랜잭션은 하나의 논리적인 작업 셋의 쿼리 개수와 관계없이 논리적인 작업 셋 자체가 전부 적용(CO…


낙관적 락과 동시성 테스트

December 03, 2022

동시성 이슈를 해결하기 위해서는 다양한 방법이 존재한다. 예를 들면 Java의 , 비관적 락과 낙관적 락, 분산 락 등이 존재한다. 이번에는 충돌이 발생하지 않는다고 낙관적으로 가정한 뒤 락을 처리하는 낙관적 락에 대해 알아보려 한다. 작성에 사용된 예제 코드는 optimistic-locking에서 확인해볼 수 있다. 낙관적 락 트랜잭션 충돌이 발생하지 …


SimpleJpaRepository의 save()는 어떻게 새로운 엔티티를 판단할까?

November 21, 2022

SimpleJpaRepository의 save()는 어떻게 새로운 엔티티를 판단할까? 를 사용하면 JPA 기반의 repository를 쉽게 구현할 수 있다. 대표적으로 를 통해 보다 더 정교한 기능들을 제공한다. 이를 통해 개발자는 데이터 접근 계층을 손쉽게 구현할 수 있다. SimpleJpaRepository 는 인터페이스의 기본 구현이다. 이것은 …


OSIV와 사용하며 직면한 문제

October 24, 2022

OSIV OSIV는 Open Session In View의 준말로, 영속성 컨텍스트를 뷰까지 열어둔다는 것을 의미이다. 영속성 컨텍스트가 유지된다는 의미는 뷰에서도 과 같이 영속성 컨텍스트의 이점을 누릴 수 있다는 것이다. 요청 당 트랜잭션 OSIV의 핵심은 뷰에서도 이 가능하도록 하는 것이다. 가장 단순한 방법은 요청이 들어오자 마자 혹은 를 거치는 …


jdbcTemplate을 만들며 마주한 Template Callback 패턴

October 09, 2022

우아한테크코스 미션 중 Spring의 을 직접 구현해보며 순수한 JDBC만 사용했을 때 들을 분리하며 리팩토링하는 과정을 경험하였다. 미션을 진행하며 실제 Spring의 JdbcTemplate 내부 코드를 살펴보았는데, 특정한 패턴을 가진 코드가 반복되는 것을 확인할 수 있었다. 간단한 예제를 통해 Spring은 반복된 코드를 어떻게 개선 하였는지 알아보…


JDBC

October 08, 2022

는 Java 프로그래밍 언어에서 을 제공한다. 를 사용하면 관계형 데이터베이스에서 스프레드 시트 및 플랫 파일에 이르기까지 거의 모든 데이터 소스에 접근할 수 있다. JDBC 기술은 tools와 alternate interfaces를 구축할 수 있는 common base를 제공한다. 특정 DBMS에서 JDBC API를 사용하려면 JDBC 기술과 데이터베이…

+ISO 8601

@Hyeonic
나누면 배가 되고
properties 객체로 다루기

properties 객체로 다루기

@Hyeonic · July 27, 2022 · 6 min read

properties 객체로 다루기

+

Spring에서 application.yml이나 application.properties에 존재하는 값을 불러오는 방법에는 대표적으로 @Value 애노테이션을 사용한 방법과 @ConfigurationProperties를 사용한 방법이 존재한다. 두 방식을 직접 적용해 본 뒤 차이와 이점에 대해 알아보려 한다.

+

@Value 사용하기

+

@Value는 기본적으로 설정 정보를 단일값으로 주입 받기 위해 사용된다. 아래는 실제 달록 프로젝트에서 적용한 예시이다.

+
@Component
+public class GoogleOAuthClient implements OAuthClient {
+
+    private static final String JWT_DELIMITER = "\\.";
+
+    private final String clientId;
+    private final String clientSecret;
+    private final String grantType;
+    private final String redirectUri;
+    private final String tokenUri;
+    private final RestTemplate restTemplate;
+    private final ObjectMapper objectMapper;
+
+    public GoogleOAuthClient(@Value("${oauth.google.client-id}") final String clientId,
+                             @Value("${oauth.google.client-secret}") final String clientSecret,
+                             @Value("${oauth.google.grant-type}") final String grantType,
+                             @Value("${oauth.google.redirect-uri}") final String redirectUri,
+                             @Value("${oauth.google.token-uri}") final String tokenUri,
+                             final RestTemplateBuilder restTemplateBuilder, final ObjectMapper objectMapper) {
+        this.clientId = clientId;
+        this.clientSecret = clientSecret;
+        this.grantType = grantType;
+        this.redirectUri = redirectUri;
+        this.tokenUri = tokenUri;
+        this.restTemplate = restTemplateBuilder.build();
+        this.objectMapper = objectMapper;
+    }
+		...
+}
+

간단하게 적용이 가능하지만 공통으로 묶인 프로퍼티가 많아질 경우 코드가 지저분해진다. 이러한 프로퍼티 값들을 객체로 매핑하여 사용하기 위한 애노테이션으로 @ConfigurationProperties가 존재한다.

+

@ConfigurationProperties

+

우리는 때때로 DB 설정을 작성하기 위해 application.yml을 통해 관련 정보를 작성하곤 한다. 아래는 간단한 h2 DB를 연결하기 위한 설정을 적은 예시이다.

+
spring:
+  datasource:
+    url: jdbc:h2:~/dallog;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+    username: sa
+

이러한 설정들은 어디서 어떻게 활용되고 있을까? 실제 바인딩 되고 있는 객체를 따라가보자.

+
@ConfigurationProperties(prefix = "spring.datasource")
+public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
+	
+    private ClassLoader classLoader;
+    private boolean generateUniqueName = true;
+	private String name;
+	private Class<? extends DataSource> type;
+	private String driverClassName;
+	private String url;
+    ...
+}
+

DataSourceProperties는 우리가 application.yml에 작성한 설정 정보를 기반으로 객체로 추출하고 있다. 이것은 Spring Boot의 자동설정으로 DataSource가 빈으로 주입되는 시점에 설정 정보를 활용하여 생성된다.

+

간단히 디버깅을 진행해보면 Bean이 주입되는 시점에 아래와 같이 application.yml에 명시한 값들을 추출한 DataSourceProperties를 기반으로 생성하고 있다.

+

+ + + debug 1 + +

+

+ + + debug 2 + +

+

정리하면 우리는 Spring Boot를 사용하며 자연스럽게 @ConfigurationProperties를 활용하여 만든 객체를 사용하고 있는 것이다.

+

이제 우리가 작성한 설정 값을 기반으로 객체를 생성해서 활용해보자. 아래는 실제 프로젝트에서 사용하고 있는 application.yml의 일부를 가져온 것이다.

+
...
+oauth:
+  google:
+    client-id: ${GOOGLE_CLIENT_ID}
+    client-secret: ${GOOGLE_CLIENT_SECRET}
+    redirect-uri: ${GOOGLE_REDIRECT_URI}
+    oauth-end-point: https://accounts.google.com/o/oauth2/v2/auth
+    response-type: code
+    scopes:
+        - https://www.googleapis.com/auth/userinfo.profile
+        - https://www.googleapis.com/auth/userinfo.email
+    token-uri: ${GOOGLE_TOKEN_URI}
+    grant-type: authorization_code
+...
+

이것을 객체로 추출하기 위해서는 아래와 같이 작성해야 한다.

+
@ConfigurationProperties("oauth.google")
+@ConstructorBinding
+public class GoogleProperties {
+
+    private final String clientId;
+    private final String clientSecret;
+    private final String redirectUri;
+    private final String oAuthEndPoint;
+    private final String responseType;
+    private final List<String> scopes;
+    private final String tokenUri;
+    private final String grantType;
+
+    public GoogleProperties(final String clientId, final String clientSecret, final String redirectUri,
+                            final String oAuthEndPoint, final String responseType, final List<String> scopes,
+                            final String tokenUri, final String grantType) {
+        this.clientId = clientId;
+        this.clientSecret = clientSecret;
+        this.redirectUri = redirectUri;
+        this.oAuthEndPoint = oAuthEndPoint;
+        this.responseType = responseType;
+        this.scopes = scopes;
+        this.tokenUri = tokenUri;
+        this.grantType = grantType;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public String getClientSecret() {
+        return clientSecret;
+    }
+
+    public String getRedirectUri() {
+        return redirectUri;
+    }
+
+    public String getoAuthEndPoint() {
+        return oAuthEndPoint;
+    }
+
+    public String getResponseType() {
+        return responseType;
+    }
+
+    public List<String> getScopes() {
+        return scopes;
+    }
+
+    public String getTokenUri() {
+        return tokenUri;
+    }
+
+    public String getGrantType() {
+        return grantType;
+    }
+}
+
    +
  • @ConfigurationProperties: 프로퍼티에 있는 값을 클래스로 바인딩하기 위해 사용하는 애노테이션이다. @ConfigurationProperties는 값을 바인딩하기 위해 기본적으로 Setter가 필요하다. 하지만 Setter를 열어둘 경우 불변성을 보장할 수 없다. 이때 생성자를 통해 바인딩 하기 위해서는 @ConstructorBinding을 활용할 수 있다.
  • +
  • @ConstructorBinding: 앞서 언급한 것 처럼 생성자를 통해 바인딩하기 위한 목적의 애노테이션이다.
  • +
+
@Configuration
+@EnableConfigurationProperties(GoogleProperties.class)
+public class PropertiesConfig {
+}
+
    +
  • @EnableConfigurationProperties: 클래스를 지정하여 스캐닝 대상에 포함시킨다.
  • +
+

개선하기

+
@Component
+public class GoogleOAuthClient implements OAuthClient {
+
+    private static final String JWT_DELIMITER = "\\.";
+
+    private final GoogleProperties googleProperties;
+    private final RestTemplate restTemplate;
+    private final ObjectMapper objectMapper;
+
+    public GoogleOAuthClient(final GoogleProperties googleProperties, final RestTemplateBuilder restTemplateBuilder,
+                             final ObjectMapper objectMapper) {
+        this.googleProperties = googleProperties;
+        this.restTemplate = restTemplateBuilder.build();
+        this.objectMapper = objectMapper;
+    }
+    ...
+}
+

이전 보다 적은 수의 필드를 활용하여 설정 정보를 다룰 수 있도록 개선되었다.

+

정리

+

우리는 application.yml 혹은 application.properties에 작성하여 메타 정보를 관리할 수 있다. 클래스 내부에서 관리할 경우 수정하기 위해서는 해당 클래스에 직접 접근해야 한다. 하지만 설정 파일로 분리할 경우 우리는 환경에 따라 유연하게 값을 설정할 수 있다. 또한 @ConfigurationProperties 애노테이션을 사용할 경우 클래스로 값을 바인딩하기 때문에 연관된 값을 한 번에 바인딩할 수 있다.

+

References.

+

달록 repository
+[Spring] @Value와 @ConfigurationProperties의 사용법 및 차이 - (2/2)
+appendix.configuration-metadata.annotation-processor

@Hyeonic
나누면 배가 되고
+ + \ No newline at end of file diff --git a/rss.xml b/rss.xml index 1c866191c6..1a37da68fe 100644 --- a/rss.xml +++ b/rss.xml @@ -1,4 +1,4 @@ -<![CDATA[RSS Feed of 나누면 배가 되고]]>https://hyeonic.github.ioGatsbyJSSun, 11 Feb 2024 14:54:11 GMT<![CDATA[jdbcTemplate을 만들며 마주한 Template Callback 패턴]]>https://hyeonic.github.io/template-callback/https://hyeonic.github.io/template-callback/Sun, 09 Oct 2022 00:00:00 GMT<p>우아한테크코스 미션 중 Spring의 <code class="language-text">JdbcTemplate</code>을 직접 구현해보며 순수한 JDBC만 사용했을 때 <code class="language-text">중복되는 로직</code>들을 분리하며 리팩토링하는 과정을 경험하였다.</p> +<![CDATA[RSS Feed of 나누면 배가 되고]]>https://hyeonic.github.ioGatsbyJSSun, 11 Feb 2024 15:23:38 GMT<![CDATA[jdbcTemplate을 만들며 마주한 Template Callback 패턴]]>https://hyeonic.github.io/template-callback/https://hyeonic.github.io/template-callback/Sun, 09 Oct 2022 00:00:00 GMT<p>우아한테크코스 미션 중 Spring의 <code class="language-text">JdbcTemplate</code>을 직접 구현해보며 순수한 JDBC만 사용했을 때 <code class="language-text">중복되는 로직</code>들을 분리하며 리팩토링하는 과정을 경험하였다.</p> <p>미션을 진행하며 실제 Spring의 JdbcTemplate 내부 코드를 살펴보았는데, 특정한 패턴을 가진 코드가 반복되는 것을 확인할 수 있었다. 간단한 예제를 통해 Spring은 반복된 코드를 어떻게 개선 하였는지 알아보려 한다.</p> <p>구현 코드는 <a href="https://github.com/hyeonic/jwp-dashboard-jdbc/tree/step1">jwp-dashboard-jdbc</a>에서 확인할 수 있다.</p> <h2>데이터베이스와 통신하기</h2> diff --git a/search/index.html b/search/index.html index b529fc9664..a7312ce3a2 100644 --- a/search/index.html +++ b/search/index.html @@ -70,9 +70,9 @@ .jECwfK{margin-top:20px;}/*!sc*/ @media (max-width:768px){.jECwfK{padding:0 15px;}}/*!sc*/ data-styled.g64[id="search__SearchWrapper-sc-1ljtwq8-0"]{content:"jECwfK,"}/*!sc*/ -나누면 배가 되고

There are 13 posts.

Spring Data JPA Auditing

March 08, 2023

Spring Data JPA Auditing 작성에 사용된 예제 코드는 jpa-auditing에서 확인해볼 수 있다. 언어는 kotlin으로 작성하였다. Spring Data는 엔티티를 하거나 과 를 투명하게 추적할 수 있는 정교한 지원을 제공한다. -해당 기능을 사용하기 위해서는 애노테이션을 사용하거나 인터페이스를 구현하여 정의할 수 있는 auditing…


조금 늦은 2022년 회고

January 12, 2023

2022년은 내게 조금은 특별한 해이다. 단순히 기술적인 성장을 넘어 한 사람으로서의 가치관을 형성할 수 있었던 시기였다. 회고를 통해 지난 1년을 뒤돌아보며 점검할 수 있는 회고를 적어보려 한다. 우아한테크코스 2022년의 시작은 대부분의 시간을 할애한 우테코를 빼놓고 이야기할 수 없을 것 같다. 정말 하고 싶었던 교육이었기 때문에 2021년은 대부분의…


스프링이 개선한 트랜잭션 (2)

December 10, 2022

작성에 사용된 예제 코드는 spring-transaction에서 확인해볼 수 있다. 이전 시간에 트랜잭션 추상화를 통해 여러 데이터 접근 기술 변경에 유연한 구조를 만들었다. 또한 트랜잭션 동기화를 통해 멀티 스레드 환경에서도 별도의 커넥션 객체를 사용하여 독립적으로 트랜잭션이 적용될 수 있도록 구현하였다. 이번 시간에는 템플릿 콜백 패턴을 활용한 과 …


문자열 생성 방식 비교하기

December 10, 2022

Java는 객체지향 언어이기 때문에 기본적으로 제공하는 이 아닌 경우 모두 로 구성되어 있다. 이것은 문자열도 마찬가지다. 다만 은 여타 다른 객체와 차이점을 가지고 -있다. 그것은 바로 을 지원한다는 것이다. 문자열 생성 방법 Java에서 문자열을 생성하는 방법에는 두 가지가 있다. 생성자를 활용한 방식 문자열 리터럴을 활용한 방식 생성자를 활용한 방식 …


스프링이 개선한 트랜잭션 (1)

December 09, 2022

작성에 사용된 예제 코드는 spring-transaction에서 확인해볼 수 있다. 트랜잭션은 논리적인 작업 셋을 모두 완벽하게 처리하거나, 처리하지 못할 경우 원래 상태로 복구하여 작업의 일부만 적용되는 현상(Partial update)을 막아준다. 또한 트랜잭션은 하나의 논리적인 작업 셋의 쿼리 개수와 관계없이 논리적인 작업 셋 자체가 전부 적용(CO…


낙관적 락과 동시성 테스트

December 03, 2022

동시성 이슈를 해결하기 위해서는 다양한 방법이 존재한다. 예를 들면 Java의 , 비관적 락과 낙관적 락, 분산 락 등이 존재한다. 이번에는 충돌이 발생하지 않는다고 낙관적으로 가정한 뒤 락을 처리하는 낙관적 락에 대해 알아보려 한다. 작성에 사용된 예제 코드는 optimistic-locking에서 확인해볼 수 있다. 낙관적 락 트랜잭션 충돌이 발생하지 …


SimpleJpaRepository의 save()는 어떻게 새로운 엔티티를 판단할까?

November 21, 2022

SimpleJpaRepository의 save()는 어떻게 새로운 엔티티를 판단할까? 를 사용하면 JPA 기반의 repository를 쉽게 구현할 수 있다. 대표적으로 를 통해 보다 더 정교한 기능들을 제공한다. 이를 통해 개발자는 데이터 접근 계층을 손쉽게 구현할 수 있다. SimpleJpaRepository 는 인터페이스의 기본 구현이다. 이것은 …


OSIV와 사용하며 직면한 문제

October 24, 2022

OSIV OSIV는 Open Session In View의 준말로, 영속성 컨텍스트를 뷰까지 열어둔다는 것을 의미이다. 영속성 컨텍스트가 유지된다는 의미는 뷰에서도 과 같이 영속성 컨텍스트의 이점을 누릴 수 있다는 것이다. 요청 당 트랜잭션 OSIV의 핵심은 뷰에서도 이 가능하도록 하는 것이다. 가장 단순한 방법은 요청이 들어오자 마자 혹은 를 거치는 …


jdbcTemplate을 만들며 마주한 Template Callback 패턴

October 09, 2022

우아한테크코스 미션 중 Spring의 을 직접 구현해보며 순수한 JDBC만 사용했을 때 들을 분리하며 리팩토링하는 과정을 경험하였다. 미션을 진행하며 실제 Spring의 JdbcTemplate 내부 코드를 살펴보았는데, 특정한 패턴을 가진 코드가 반복되는 것을 확인할 수 있었다. 간단한 예제를 통해 Spring은 반복된 코드를 어떻게 개선 하였는지 알아보…


JDBC

October 08, 2022

는 Java 프로그래밍 언어에서 을 제공한다. 를 사용하면 관계형 데이터베이스에서 스프레드 시트 및 플랫 파일에 이르기까지 거의 모든 데이터 소스에 접근할 수 있다. JDBC 기술은 tools와 alternate interfaces를 구축할 수 있는 common base를 제공한다. 특정 DBMS에서 JDBC API를 사용하려면 JDBC 기술과 데이터베이…

+나누면 배가 되고

There are 18 posts.

Spring Data JPA Auditing

March 08, 2023

Spring Data JPA Auditing 작성에 사용된 예제 코드는 jpa-auditing에서 확인해볼 수 있다. 언어는 kotlin으로 작성하였다. Spring Data는 엔티티를 하거나 과 를 투명하게 추적할 수 있는 정교한 지원을 제공한다. +해당 기능을 사용하기 위해서는 애노테이션을 사용하거나 인터페이스를 구현하여 정의할 수 있는 auditing…


조금 늦은 2022년 회고

January 12, 2023

2022년은 내게 조금은 특별한 해이다. 단순히 기술적인 성장을 넘어 한 사람으로서의 가치관을 형성할 수 있었던 시기였다. 회고를 통해 지난 1년을 뒤돌아보며 점검할 수 있는 회고를 적어보려 한다. 우아한테크코스 2022년의 시작은 대부분의 시간을 할애한 우테코를 빼놓고 이야기할 수 없을 것 같다. 정말 하고 싶었던 교육이었기 때문에 2021년은 대부분의…


문자열 생성 방식 비교하기

December 11, 2022

Java는 객체지향 언어이기 때문에 기본적으로 제공하는 이 아닌 경우 모두 로 구성되어 있다. 이것은 문자열도 마찬가지다. 다만 은 여타 다른 객체와 차이점을 가지고 +있다. 그것은 바로 을 지원한다는 것이다. 문자열 생성 방법 Java에서 문자열을 생성하는 방법에는 두 가지가 있다. 생성자를 활용한 방식 문자열 리터럴을 활용한 방식 생성자를 활용한 방식 …


스프링이 개선한 트랜잭션 (2)

December 10, 2022

작성에 사용된 예제 코드는 spring-transaction에서 확인해볼 수 있다. 이전 시간에 트랜잭션 추상화를 통해 여러 데이터 접근 기술 변경에 유연한 구조를 만들었다. 또한 트랜잭션 동기화를 통해 멀티 스레드 환경에서도 별도의 커넥션 객체를 사용하여 독립적으로 트랜잭션이 적용될 수 있도록 구현하였다. 이번 시간에는 템플릿 콜백 패턴을 활용한 과 …


스프링이 개선한 트랜잭션 (1)

December 09, 2022

작성에 사용된 예제 코드는 spring-transaction에서 확인해볼 수 있다. 트랜잭션은 논리적인 작업 셋을 모두 완벽하게 처리하거나, 처리하지 못할 경우 원래 상태로 복구하여 작업의 일부만 적용되는 현상(Partial update)을 막아준다. 또한 트랜잭션은 하나의 논리적인 작업 셋의 쿼리 개수와 관계없이 논리적인 작업 셋 자체가 전부 적용(CO…


낙관적 락과 동시성 테스트

December 03, 2022

동시성 이슈를 해결하기 위해서는 다양한 방법이 존재한다. 예를 들면 Java의 , 비관적 락과 낙관적 락, 분산 락 등이 존재한다. 이번에는 충돌이 발생하지 않는다고 낙관적으로 가정한 뒤 락을 처리하는 낙관적 락에 대해 알아보려 한다. 작성에 사용된 예제 코드는 optimistic-locking에서 확인해볼 수 있다. 낙관적 락 트랜잭션 충돌이 발생하지 …


SimpleJpaRepository의 save()는 어떻게 새로운 엔티티를 판단할까?

November 21, 2022

SimpleJpaRepository의 save()는 어떻게 새로운 엔티티를 판단할까? 를 사용하면 JPA 기반의 repository를 쉽게 구현할 수 있다. 대표적으로 를 통해 보다 더 정교한 기능들을 제공한다. 이를 통해 개발자는 데이터 접근 계층을 손쉽게 구현할 수 있다. SimpleJpaRepository 는 인터페이스의 기본 구현이다. 이것은 …


OSIV와 사용하며 직면한 문제

October 24, 2022

OSIV OSIV는 Open Session In View의 준말로, 영속성 컨텍스트를 뷰까지 열어둔다는 것을 의미이다. 영속성 컨텍스트가 유지된다는 의미는 뷰에서도 과 같이 영속성 컨텍스트의 이점을 누릴 수 있다는 것이다. 요청 당 트랜잭션 OSIV의 핵심은 뷰에서도 이 가능하도록 하는 것이다. 가장 단순한 방법은 요청이 들어오자 마자 혹은 를 거치는 …


jdbcTemplate을 만들며 마주한 Template Callback 패턴

October 09, 2022

우아한테크코스 미션 중 Spring의 을 직접 구현해보며 순수한 JDBC만 사용했을 때 들을 분리하며 리팩토링하는 과정을 경험하였다. 미션을 진행하며 실제 Spring의 JdbcTemplate 내부 코드를 살펴보았는데, 특정한 패턴을 가진 코드가 반복되는 것을 확인할 수 있었다. 간단한 예제를 통해 Spring은 반복된 코드를 어떻게 개선 하였는지 알아보…


JDBC

October 08, 2022

는 Java 프로그래밍 언어에서 을 제공한다. 를 사용하면 관계형 데이터베이스에서 스프레드 시트 및 플랫 파일에 이르기까지 거의 모든 데이터 소스에 접근할 수 있다. JDBC 기술은 tools와 alternate interfaces를 구축할 수 있는 common base를 제공한다. 특정 DBMS에서 JDBC API를 사용하려면 JDBC 기술과 데이터베이…

외부와 의존성 분리하기

외부와 의존성 분리하기

@Hyeonic · July 24, 2022 · 8 min read

외부와 의존성 분리하기

+

도메인 로직은 우리가 지켜야할 매우 소중한 비즈니스 로직들이 담겨있다. 이러한 도메인 로직들은 변경이 최소화되어야 한다. 그렇기 때문에 외부와의 의존성을 최소화 해야 한다.

+

인터페이스 활용하기

+

우선 우리가 지금까지 학습한 것 중 객체 간의 의존성을 약하게 만들어 줄 수 있는 수단으로 인터페이스를 활용할 수 있다. 간단한 예시로 JpaRepository를 살펴보자.

+
public interface MemberRepository extends JpaRepository<Member, Long> {
+
+    Optional<Member> findByEmail(final String email);
+
+    boolean existsByEmail(final String email);
+}
+

이러한 인터페이스 덕분에 우리는 실제 DB에 접근하는 내부 구현에 의존하지 않고 데이터를 조작할 수 있다. 핵심은 실제 DB에 접근하는 행위이다.

+

아래는 Spring Data가 만든 JpaRepository의 구현체 SimpleJpaRepository의 일부를 가져온 것이다.

+
@Repository
+@Transactional(readOnly = true)
+public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
+
+	private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
+
+	private final JpaEntityInformation<T, ?> entityInformation;
+	private final EntityManager em;
+	private final PersistenceProvider provider;
+
+	private @Nullable CrudMethodMetadata metadata;
+	private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
+
+	public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
+
+		Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
+		Assert.notNull(entityManager, "EntityManager must not be null!");
+
+		this.entityInformation = entityInformation;
+		this.em = entityManager;
+		this.provider = PersistenceProvider.fromEntityManager(entityManager);
+	}
+  ...
+}
+

해당 구현체는 entityManger를 통해 객체를 영속 시키는 행위를 진행하고 있기 때문에 영속 계층에 가깝다고 판단했다. 즉 도메인의 입장에서 MemberRepository를 바라볼 때 단순히 JpaRepository를 상속한 인터페이스를 가지고 있기 때문에 영속 계층에 대한 직접적인 의존성은 없다고 봐도 무방하다. 정리하면 우리는 인터페이스를 통해 실제 구현체에 의존하지 않고 로직을 수행할 수 있게 된다.

+

관점 변경하기

+

이러한 사례를 외부 서버와 통신을 담당하는 우리가 직접 만든 인터페이스인 OAuthClient에 대입해본다. OAuthClient의 가장 큰 역할은 n의 소셜에서 OAuth 2.0을 활용한 인증의 행위를 정의한 인터페이스이다. google, github 등 각자에 맞는 요청을 처리하기 위해 OAuthClient를 구현한 뒤 로직을 처리할 수 있다. 아래는 실제 google의 인가 코드를 기반으로 토큰 정보에서 회원 정보를 조회하는 로직을 담고 있다.

+
public interface OAuthClient {
+
+    OAuthMember getOAuthMember(final String code);
+}
+
@Component
+public class GoogleOAuthClient implements OAuthClient {
+
+    private static final String JWT_DELIMITER = "\\.";
+
+    private final String googleRedirectUri;
+    private final String googleClientId;
+    private final String googleClientSecret;
+    private final String googleTokenUri;
+    private final RestTemplate restTemplate;
+    private final ObjectMapper objectMapper;
+
+    public GoogleOAuthClient(@Value("${oauth.google.redirect_uri}") final String googleRedirectUri,
+                             @Value("${oauth.google.client_id}") final String googleClientId,
+                             @Value("${oauth.google.client_secret}") final String googleClientSecret,
+                             @Value("${oauth.google.token_uri}") final String googleTokenUri,
+                             final RestTemplate restTemplate, final ObjectMapper objectMapper) {
+        this.googleRedirectUri = googleRedirectUri;
+        this.googleClientId = googleClientId;
+        this.googleClientSecret = googleClientSecret;
+        this.googleTokenUri = googleTokenUri;
+        this.restTemplate = restTemplate;
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public OAuthMember getOAuthMember(final String code) {
+        GoogleTokenResponse googleTokenResponse = requestGoogleToken(code);
+        String payload = getPayloadFrom(googleTokenResponse.getIdToken());
+        String decodedPayload = decodeJwtPayload(payload);
+
+        try {
+            return generateOAuthMemberBy(decodedPayload);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    private GoogleTokenResponse requestGoogleToken(final String code) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        MultiValueMap<String, String> params = generateRequestParams(code);
+
+        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
+        return restTemplate.postForEntity(googleTokenUri, request, GoogleTokenResponse.class).getBody();
+    }
+
+    private MultiValueMap<String, String> generateRequestParams(final String code) {
+        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+        params.add("client_id", googleClientId);
+        params.add("client_secret", googleClientSecret);
+        params.add("code", code);
+        params.add("grant_type", "authorization_code");
+        params.add("redirect_uri", googleRedirectUri);
+        return params;
+    }
+
+    private String getPayloadFrom(final String jwt) {
+        return jwt.split(JWT_DELIMITER)[1];
+    }
+
+    private String decodeJwtPayload(final String payload) {
+        return new String(Base64.getUrlDecoder().decode(payload), StandardCharsets.UTF_8);
+    }
+
+    private OAuthMember generateOAuthMemberBy(final String decodedIdToken) throws JsonProcessingException {
+        Map<String, String> userInfo = objectMapper.readValue(decodedIdToken, HashMap.class);
+        String email = userInfo.get("email");
+        String displayName = userInfo.get("name");
+        String profileImageUrl = userInfo.get("picture");
+
+        return new OAuthMember(email, displayName, profileImageUrl);
+    }
+}
+

보통의 생각은 인터페이스인 OAuthClient와 구현체인 GoogleOAuthClient를 같은 패키지에 두려고 할 것이다. GoogleOAuthClient는 외부 의존성을 강하게 가지고 있기 때문에 domain 패키지와 별도로 관리하기 위한 infrastructure 패키지가 적합할 것이다. 결국 인터페이스인 OAuthClient 또한 infrastructure에 위치하게 될 것이다. 우리는 이러한 생각에서 벗어나 새로운 관점에서 살펴봐야 한다.

+

앞서 언급한 의존성에 대해 생각해보자. 위 OAuthClient를 사용하는 주체는 누구일까? 우리는 이러한 주체를 domain 내에 인증을 담당하는 auth 패키지 내부의 Authservice로 결정 했다. 아래는 실제 OAuthClient를 사용하고 있는 주체인 AuthService이다.

+
@Transactional(readOnly = true)
+@Service
+public class AuthService {
+
+    private final OAuthEndpoint oAuthEndpoint;
+    private final OAuthClient oAuthClient;
+    private final MemberService memberService;
+    private final JwtTokenProvider jwtTokenProvider;
+
+    public AuthService(final OAuthEndpoint oAuthEndpoint, final OAuthClient oAuthClient,
+                       final MemberService memberService, final JwtTokenProvider jwtTokenProvider) {
+        this.oAuthEndpoint = oAuthEndpoint;
+        this.oAuthClient = oAuthClient;
+        this.memberService = memberService;
+        this.jwtTokenProvider = jwtTokenProvider;
+    }
+
+    public String generateGoogleLink() {
+        return oAuthEndpoint.generate();
+    }
+
+    @Transactional
+    public TokenResponse generateTokenWithCode(final String code) {
+        OAuthMember oAuthMember = oAuthClient.getOAuthMember(code);
+        String email = oAuthMember.getEmail();
+
+        if (!memberService.existsByEmail(email)) {
+            memberService.save(generateMemberBy(oAuthMember));
+        }
+
+        Member foundMember = memberService.findByEmail(email);
+        String accessToken = jwtTokenProvider.createToken(String.valueOf(foundMember.getId()));
+
+        return new TokenResponse(accessToken);
+    }
+
+    private Member generateMemberBy(final OAuthMember oAuthMember) {
+        return new Member(oAuthMember.getEmail(), oAuthMember.getProfileImageUrl(), oAuthMember.getDisplayName(), SocialType.GOOGLE);
+    }
+}
+

지금 까지 설명한 구조의 패키지 구조는 아래와 같다.

+
└── src
+    ├── main
+    │   ├── java
+    │   │   └── com
+    │   │       └── allog
+    │   │           └── dallog
+    │   │               ├── auth
+    │   │               │   └── application
+    │   │               │       └── AuthService.java
+    │   │               ...
+    │   │               ├── infrastructure
+    │   │               │   ├── oauth
+    │   │               │   │   └── client
+    │   │               │   │       ├── OAuthClient.java
+    │   │               │   │       └── GoogleOAuthClient.java
+    │   │               │   └── dto
+    │   │               │       └── OAuthMember.java     
+    │   │               └── AllogDallogApplication.java
+    |   |
+    │   └── resources
+    │       └── application.yml
+

결국 이러한 구조는 아래와 같이 domain 패키지에서 infrastructure에 의존하게 된다.

+
...
+import com.allog.dallog.infrastructure.dto.OAuthMember; // 의존성 발생!
+import com.allog.dallog.infrastructure.oauth.client.OAuthClient; // 의존성 발생!
+...
+
+@Transactional(readOnly = true)
+@Service
+public class AuthService {
+	...
+    private final OAuthClient oAuthClient;
+    ...
+
+    @Transactional
+    public TokenResponse generateTokenWithCode(final String code) {
+        OAuthMember oAuthMember = oAuthClient.getOAuthMember(code);
+        ...
+    }
+    ...
+}
+

Separated Interface Pattern

+

분리된 인터페이스를 활용하자. 즉 인터페이스구현체를 각각의 패키지로 분리한다. 분리된 인터페이스를 사용하여 domain 패키지에서 인터페이스를 정의하고 infrastructure 패키지에 구현체를 둔다. 이렇게 구성하면 인터페이스에 대한 종속성을 가진 주체가 구현체에 대해 인식하지 못하게 만들 수 있다.

+

아래와 같은 구조로 인터페이스와 구현체를 분리했다고 가정한다.

+
└── src
+    ├── main
+    │   ├── java
+    │   │   └── com
+    │   │       └── allog
+    │   │           └── dallog
+    │   │               ├── auth
+    │   │               │   ├── application
+    │   │               │   │   ├── AuthService.java
+    │   │               │   │   └── OAuthClient.java
+    │   │               │   └── dto
+    │   │               │       └── OAuthMember.java         
+    │   │               ...
+    │   │               ├── infrastructure
+    │   │               │   ├── oauth
+    │   │               │       └── client
+    │   │               │           └── GoogleOAuthClient.java
+    │   │               └── AllogDallogApplication.java
+    |   |
+    │   └── resources
+    │       └── application.yml
+

자연스럽게 domain 내에 있던 infrastructure 패키지에 대한 의존성도 제거된다. 즉 외부 서버와의 통신을 위한 의존성이 완전히 분리된 것을 확인할 수 있다.

+
...
+import com.allog.dallog.auth.dto.OAuthMember; // auth 패키지 내부를 의존
+...
+@Transactional(readOnly = true)
+@Service
+public class AuthService {
+	...
+    private final OAuthClient oAuthClient;
+    ...
+
+    @Transactional
+    public TokenResponse generateTokenWithCode(final String code) {
+        OAuthMember oAuthMember = oAuthClient.getOAuthMember(code);
+        ...
+    }
+    ...
+}
+

References.

+

Separated Interface

@Hyeonic
나누면 배가 되고
+ + \ No newline at end of file diff --git a/sitemap-0.xml b/sitemap-0.xml index 6bce9aeacc..3b6f1e0bee 100644 --- a/sitemap-0.xml +++ b/sitemap-0.xml @@ -1 +1 @@ -https://hyeonic.github.io/local-date-time/daily0.7https://hyeonic.github.io/identity-strategy/daily0.7https://hyeonic.github.io/basic-tomcat/daily0.7https://hyeonic.github.io/jdbc/daily0.7https://hyeonic.github.io/template-callback/daily0.7https://hyeonic.github.io/osiv/daily0.7https://hyeonic.github.io/save-persist-merge/daily0.7https://hyeonic.github.io/optimistic-locking/daily0.7https://hyeonic.github.io/spring-transaction-1/daily0.7https://hyeonic.github.io/spring-transaction-2/daily0.7https://hyeonic.github.io/java-string/daily0.7https://hyeonic.github.io/2022-retrospect/daily0.7https://hyeonic.github.io/jpa-auditing/daily0.7https://hyeonic.github.io/series/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EA%B0%9C%EC%84%A0%ED%95%9C-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98/daily0.7https://hyeonic.github.io/daily0.7https://hyeonic.github.io/search/daily0.7https://hyeonic.github.io/series/daily0.7https://hyeonic.github.io/tags/daily0.7 \ No newline at end of file +https://hyeonic.github.io/why-jdbc-template/daily0.7https://hyeonic.github.io/spring-jdbc-batch/daily0.7https://hyeonic.github.io/local-date-time/daily0.7https://hyeonic.github.io/identity-strategy/daily0.7https://hyeonic.github.io/separated-interface/daily0.7https://hyeonic.github.io/properties-to-object/daily0.7https://hyeonic.github.io/google-refresh-token/daily0.7https://hyeonic.github.io/basic-tomcat/daily0.7https://hyeonic.github.io/jdbc/daily0.7https://hyeonic.github.io/template-callback/daily0.7https://hyeonic.github.io/osiv/daily0.7https://hyeonic.github.io/save-persist-merge/daily0.7https://hyeonic.github.io/optimistic-locking/daily0.7https://hyeonic.github.io/spring-transaction-1/daily0.7https://hyeonic.github.io/spring-transaction-2/daily0.7https://hyeonic.github.io/java-string/daily0.7https://hyeonic.github.io/2022-retrospect/daily0.7https://hyeonic.github.io/jpa-auditing/daily0.7https://hyeonic.github.io/series/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EA%B0%9C%EC%84%A0%ED%95%9C-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98/daily0.7https://hyeonic.github.io/daily0.7https://hyeonic.github.io/search/daily0.7https://hyeonic.github.io/series/daily0.7https://hyeonic.github.io/tags/daily0.7 \ No newline at end of file diff --git a/spring-jdbc-batch/index.html b/spring-jdbc-batch/index.html new file mode 100644 index 0000000000..44dbfa55da --- /dev/null +++ b/spring-jdbc-batch/index.html @@ -0,0 +1,492 @@ +Spring JDBC로 batch 활용하기

Spring JDBC로 batch 활용하기

@Hyeonic · May 24, 2022 · 6 min read

+

개요

+

batch란 데이터를 실시간으로 처리하는 것이 아니라 일괄적으로 모아 한번에 처리하는 것을 의미한다. JdbcTemplateupdate 메서드와 batchUpdate를 비교하여 배치로 진행한 것과 일반적으로 처리한 것에 어떠한 차이가 있는지 알아보려 한다.

+

프로젝트 세팅

+

github repository 바로가기

+

우선 Spirng 환경에서 jdbc와 h2 DB를 활용하기 위해 아래와 같이 build.gradle에 의존성을 추가하였다.

+
plugins {
+    id 'org.springframework.boot' version '2.7.0'
+    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
+    id 'java'
+}
+
+group = 'me.hyeonic'
+version = '0.0.1-SNAPSHOT'
+sourceCompatibility = '11'
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
+
+    runtimeOnly 'com.h2database:h2'
+
+    testImplementation 'org.springframework.boot:spring-boot-starter-test'
+}
+
+tasks.named('test') {
+    useJUnitPlatform()
+}
+

단순한 예제를 작성하기 위해 domain 패키지 하위에 지하철역을 나타내는 Station 객체를 추가한다.

+
public class Station {
+
+    private final Long id;
+    private final String name;
+
+    public Station(Long id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    public Station(String name) {
+        this(null, name);
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
+

JdbcTemplate의 update 메서드

+

보통 JdbcTemplateupdate의 메서드를 활용하여 데이터를 insert하기 위해 아래와 같이 작성할 수 있다.

+
@Repository
+public class JdbcTemplateStationDao {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    public JdbcTemplateStationDao(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    public void save(Station station) {
+        String sql = "insert into STATION (name) values (?)";
+        jdbcTemplate.update(sql, station.getName());
+    }
+}
+

여러번의 insert를 테스트하기 위해 아래와 같이 테스트 코드를 작성한 뒤 실행해보았다.

+
@JdbcTest
+class JdbcTemplateStationDaoTest {
+
+    private final JdbcTemplateStationDao jdbcTemplateStationDao;
+
+    @Autowired
+    public JdbcTemplateStationDaoTest(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplateStationDao = new JdbcTemplateStationDao(jdbcTemplate);
+    }
+
+    @DisplayName("batch 사용하지 않고 저장한다.")
+    @Test
+    void batch_사용하지_않고_저장한다() {
+        long start = System.currentTimeMillis();
+
+        for (int i = 0; i < 10000; i++) {
+            String name = String.valueOf(i);
+            jdbcTemplateStationDao.save(new Station(name));
+        }
+
+        long end = System.currentTimeMillis();
+        System.out.println("수행시간: " + (end - start) + " ms");
+    }
+}
+
수행시간: 402 ms
+

여러번의 insert를 진행할 때 아래와 같은 형태로 쿼리가 요청될 것이다.

+
insert into STATION (name) values (?)
+insert into STATION (name) values (?)
+insert into STATION (name) values (?)
+insert into STATION (name) values (?)
+insert into STATION (name) values (?)
+insert into STATION (name) values (?)
+insert into STATION (name) values (?)
+...
+

JdbcTemplate의 batchUpdate 메서드

+

JdbcTemplate batchUpdate를 활용하면 아래와 같이 일괄적으로 한 번에 처리가 가능하다.

+
insert into STATION (name) 
+values (?),
+       (?),
+       (?),
+       (?),
+       (?),
+       (?),
+       ...
+

이것을 달성하기 위해서는 아래와 같이 코드를 작성해야 한다.

+
@Repository
+public class JdbcTemplateStationDao {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    public JdbcTemplateStationDao(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    public void saveAll(List<Station> stations) {
+        String sql = "insert into STATION (name) values (?)";
+
+        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
+            @Override
+            public void setValues(PreparedStatement ps, int i) throws SQLException {
+                Station station = stations.get(i);
+                ps.setString(1, station.getName());
+            }
+
+            @Override
+            public int getBatchSize() {
+                return stations.size();
+            }
+        });
+    }
+}
+

batchUpdate의 첫 번째 매개변수로 배치 처리하기 위한 쿼리문이 들어가고 두 번째 매개 변수에는 BatchPreparedStatementSetter의 구현체가 들어간다.

+
    +
  • setValues: 준비된 쿼리의 매개 변수 값을 설정할 수 있다. getBatchSize에서 명시한 횟수 만큼 호출한다.
  • +
  • getBatchSize 현재 배치의 크기를 제공한다.
  • +
+

이제 배치를 활용하여 앞서 진행한 테스트와 동일한 데이터를 기반으로 테스트를 진행한다.

+
@JdbcTest
+class JdbcTemplateStationDaoTest {
+
+    private final JdbcTemplateStationDao jdbcTemplateStationDao;
+
+    @Autowired
+    public JdbcTemplateStationDaoTest(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplateStationDao = new JdbcTemplateStationDao(jdbcTemplate);
+    }
+
+    @DisplayName("batch 사용하고 저장한다.")
+    @Test
+    void batch_사용하여_저장한다() {
+        List<Station> stations = IntStream.range(0, 10000)
+                .mapToObj(String::valueOf)
+                .map(Station::new)
+                .collect(toList());
+
+        jdbcTemplateStationDao.saveAll(stations);
+    }
+}
+

위 테스트의 수행 시간은 아래와 같다.

+
수행시간: 221 ms
+

정리하면 배치를 이용한 insert가 일반적으로 빠른 것을 확인 할 수 있다.

+

NamedParameterJdbcTemplate을 활용한 batch

+

NamedParameterJdbcTemplate을 활용한 배치 처리도 가능하다.

+
@Repository
+public class NamedParameterJdbcTemplateStationDao {
+
+    private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+    public NamedParameterJdbcTemplateStationDao(JdbcTemplate jdbcTemplate) {
+        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
+    }
+
+    public void save(Station station) {
+        String sql = "insert into STATION (name) values (:name)";
+        SqlParameterSource params = new MapSqlParameterSource("name", station.getName());
+        namedParameterJdbcTemplate.update(sql, params);
+    }
+
+    public void saveAll(List<Station> stations) {
+        String sql = "insert into STATION (name) values (:name)";
+        SqlParameterSource[] batch = generateParameters(stations);
+        namedParameterJdbcTemplate.batchUpdate(sql, batch);
+    }
+
+    private SqlParameterSource[] generateParameters(List<Station> stations) {
+        return stations.stream()
+                .map(this::generateParameter)
+                .toArray(SqlParameterSource[]::new);
+    }
+
+    private SqlParameterSource generateParameter(Station station) {
+        return new MapSqlParameterSource("name", station.getName());
+    }
+}
+

대부분 사용법은 유사하지만 NamedParameterJdbcTemplatebatchUpdate의 두번째 매개 변수로 추가적인 인터페이스를 구현하지 않고 단순히 SqlParameterSource[]가 들어간다.

+

또한 SqlParameterSourceUtils를 활용하면 리스트를 활용하여 간편하게 SqlParameterSource[]을 만들 수 있다.

+
@Repository
+public class NamedParameterJdbcTemplateStationDao {
+    ...
+    public void saveAll(List<Station> stations) {
+        String sql = "insert into STATION (name) values (:name)";
+        namedParameterJdbcTemplate.batchUpdate(sql, SqlParameterSourceUtils.createBatch(stations));
+    }
+}
+

이 또한 테스트를 진행해보면 아래와 같이 유의미한 차이를 확인할 수 있었다.

+
@JdbcTest
+class NamedParameterJdbcTemplateStationDaoTest {
+
+    private final NamedParameterJdbcTemplateStationDao namedParameterJdbcTemplateStationDao;
+
+    @Autowired
+    public NamedParameterJdbcTemplateStationDaoTest(JdbcTemplate jdbcTemplate) {
+        this.namedParameterJdbcTemplateStationDao = new NamedParameterJdbcTemplateStationDao(jdbcTemplate);
+    }
+
+    @DisplayName("batch 사용하지 않고 저장한다.")
+    @Test
+    void batch_사용하지_않고_저장한다() {
+        long start = System.currentTimeMillis();
+
+        for (int i = 0; i < 10000; i++) {
+            String name = String.valueOf(i);
+            namedParameterJdbcTemplateStationDao.save(new Station(name));
+        }
+
+        long end = System.currentTimeMillis();
+        System.out.println("수행시간: " + (end - start) + " ms");
+    }
+
+    @DisplayName("batch 사용하고 저장한다.")
+    @Test
+    void batch_사용하여_저장한다() {
+        long start = System.currentTimeMillis();
+
+        List<Station> stations = IntStream.range(0, 10000)
+                .mapToObj(String::valueOf)
+                .map(Station::new)
+                .collect(toList());
+
+        namedParameterJdbcTemplateStationDao.saveAll(stations);
+
+        long end = System.currentTimeMillis();
+        System.out.println("수행시간: " + (end - start) + " ms");
+    }
+}
+
수행시간: 531 ms
+수행시간: 236 ms
+

References.

+

3.5. JDBC Batch Operations

@Hyeonic
나누면 배가 되고
+ + \ No newline at end of file diff --git a/static/3206413c52066864f6bf338e4a3e2fd6/0b5a5/debug-2.png b/static/3206413c52066864f6bf338e4a3e2fd6/0b5a5/debug-2.png new file mode 100644 index 0000000000..babbcb95e9 Binary files /dev/null and b/static/3206413c52066864f6bf338e4a3e2fd6/0b5a5/debug-2.png differ diff --git a/static/3206413c52066864f6bf338e4a3e2fd6/e7570/debug-2.png b/static/3206413c52066864f6bf338e4a3e2fd6/e7570/debug-2.png new file mode 100644 index 0000000000..026098c489 Binary files /dev/null and b/static/3206413c52066864f6bf338e4a3e2fd6/e7570/debug-2.png differ diff --git a/static/3206413c52066864f6bf338e4a3e2fd6/f46e7/debug-2.png b/static/3206413c52066864f6bf338e4a3e2fd6/f46e7/debug-2.png new file mode 100644 index 0000000000..cc8d5a304f Binary files /dev/null and b/static/3206413c52066864f6bf338e4a3e2fd6/f46e7/debug-2.png differ diff --git a/static/40821173b75b8bc6f832d00faa388a42/02d09/google-refresh-token-null.png b/static/40821173b75b8bc6f832d00faa388a42/02d09/google-refresh-token-null.png new file mode 100644 index 0000000000..d64fbfde04 Binary files /dev/null and b/static/40821173b75b8bc6f832d00faa388a42/02d09/google-refresh-token-null.png differ diff --git a/static/40821173b75b8bc6f832d00faa388a42/7c559/google-refresh-token-null.png b/static/40821173b75b8bc6f832d00faa388a42/7c559/google-refresh-token-null.png new file mode 100644 index 0000000000..cee4077f49 Binary files /dev/null and b/static/40821173b75b8bc6f832d00faa388a42/7c559/google-refresh-token-null.png differ diff --git a/static/40821173b75b8bc6f832d00faa388a42/9d567/google-refresh-token-null.png b/static/40821173b75b8bc6f832d00faa388a42/9d567/google-refresh-token-null.png new file mode 100644 index 0000000000..2e0293ef40 Binary files /dev/null and b/static/40821173b75b8bc6f832d00faa388a42/9d567/google-refresh-token-null.png differ diff --git a/static/40821173b75b8bc6f832d00faa388a42/ca1dc/google-refresh-token-null.png b/static/40821173b75b8bc6f832d00faa388a42/ca1dc/google-refresh-token-null.png new file mode 100644 index 0000000000..2534ae26c1 Binary files /dev/null and b/static/40821173b75b8bc6f832d00faa388a42/ca1dc/google-refresh-token-null.png differ diff --git a/static/40821173b75b8bc6f832d00faa388a42/e7570/google-refresh-token-null.png b/static/40821173b75b8bc6f832d00faa388a42/e7570/google-refresh-token-null.png new file mode 100644 index 0000000000..4e85c3b9b1 Binary files /dev/null and b/static/40821173b75b8bc6f832d00faa388a42/e7570/google-refresh-token-null.png differ diff --git a/static/40821173b75b8bc6f832d00faa388a42/f46e7/google-refresh-token-null.png b/static/40821173b75b8bc6f832d00faa388a42/f46e7/google-refresh-token-null.png new file mode 100644 index 0000000000..61ffbf1f4b Binary files /dev/null and b/static/40821173b75b8bc6f832d00faa388a42/f46e7/google-refresh-token-null.png differ diff --git a/static/4c02b6ded86cbf0190013659f8371f6c/02d09/oauth-flow.png b/static/4c02b6ded86cbf0190013659f8371f6c/02d09/oauth-flow.png new file mode 100644 index 0000000000..1d4ffe861d Binary files /dev/null and b/static/4c02b6ded86cbf0190013659f8371f6c/02d09/oauth-flow.png differ diff --git a/static/4c02b6ded86cbf0190013659f8371f6c/9d567/oauth-flow.png b/static/4c02b6ded86cbf0190013659f8371f6c/9d567/oauth-flow.png new file mode 100644 index 0000000000..4a86112476 Binary files /dev/null and b/static/4c02b6ded86cbf0190013659f8371f6c/9d567/oauth-flow.png differ diff --git a/static/4c02b6ded86cbf0190013659f8371f6c/c7a69/oauth-flow.png b/static/4c02b6ded86cbf0190013659f8371f6c/c7a69/oauth-flow.png new file mode 100644 index 0000000000..8620b01ac7 Binary files /dev/null and b/static/4c02b6ded86cbf0190013659f8371f6c/c7a69/oauth-flow.png differ diff --git a/static/4c02b6ded86cbf0190013659f8371f6c/ca1dc/oauth-flow.png b/static/4c02b6ded86cbf0190013659f8371f6c/ca1dc/oauth-flow.png new file mode 100644 index 0000000000..ed5a2a3a91 Binary files /dev/null and b/static/4c02b6ded86cbf0190013659f8371f6c/ca1dc/oauth-flow.png differ diff --git a/static/4c02b6ded86cbf0190013659f8371f6c/e7570/oauth-flow.png b/static/4c02b6ded86cbf0190013659f8371f6c/e7570/oauth-flow.png new file mode 100644 index 0000000000..9e5e99b5e4 Binary files /dev/null and b/static/4c02b6ded86cbf0190013659f8371f6c/e7570/oauth-flow.png differ diff --git a/static/4c02b6ded86cbf0190013659f8371f6c/f46e7/oauth-flow.png b/static/4c02b6ded86cbf0190013659f8371f6c/f46e7/oauth-flow.png new file mode 100644 index 0000000000..511060720e Binary files /dev/null and b/static/4c02b6ded86cbf0190013659f8371f6c/f46e7/oauth-flow.png differ diff --git a/static/7b4590a4b46a14708d45e7589d075bf7/b2dbf/google-oauth-uri.png b/static/7b4590a4b46a14708d45e7589d075bf7/b2dbf/google-oauth-uri.png new file mode 100644 index 0000000000..2b740c9dc8 Binary files /dev/null and b/static/7b4590a4b46a14708d45e7589d075bf7/b2dbf/google-oauth-uri.png differ diff --git a/static/7b4590a4b46a14708d45e7589d075bf7/ca1dc/google-oauth-uri.png b/static/7b4590a4b46a14708d45e7589d075bf7/ca1dc/google-oauth-uri.png new file mode 100644 index 0000000000..eed116b405 Binary files /dev/null and b/static/7b4590a4b46a14708d45e7589d075bf7/ca1dc/google-oauth-uri.png differ diff --git a/static/7b4590a4b46a14708d45e7589d075bf7/e7570/google-oauth-uri.png b/static/7b4590a4b46a14708d45e7589d075bf7/e7570/google-oauth-uri.png new file mode 100644 index 0000000000..175bfb3b44 Binary files /dev/null and b/static/7b4590a4b46a14708d45e7589d075bf7/e7570/google-oauth-uri.png differ diff --git a/static/7b4590a4b46a14708d45e7589d075bf7/f46e7/google-oauth-uri.png b/static/7b4590a4b46a14708d45e7589d075bf7/f46e7/google-oauth-uri.png new file mode 100644 index 0000000000..86a4e7a8a7 Binary files /dev/null and b/static/7b4590a4b46a14708d45e7589d075bf7/f46e7/google-oauth-uri.png differ diff --git a/static/ac7268ed5711abec2e8ce73110f7c556/ca1dc/google-refresh-token-success.png b/static/ac7268ed5711abec2e8ce73110f7c556/ca1dc/google-refresh-token-success.png new file mode 100644 index 0000000000..190bf53374 Binary files /dev/null and b/static/ac7268ed5711abec2e8ce73110f7c556/ca1dc/google-refresh-token-success.png differ diff --git a/static/ac7268ed5711abec2e8ce73110f7c556/e4da8/google-refresh-token-success.png b/static/ac7268ed5711abec2e8ce73110f7c556/e4da8/google-refresh-token-success.png new file mode 100644 index 0000000000..bd72008500 Binary files /dev/null and b/static/ac7268ed5711abec2e8ce73110f7c556/e4da8/google-refresh-token-success.png differ diff --git a/static/ac7268ed5711abec2e8ce73110f7c556/e7570/google-refresh-token-success.png b/static/ac7268ed5711abec2e8ce73110f7c556/e7570/google-refresh-token-success.png new file mode 100644 index 0000000000..7f0977c642 Binary files /dev/null and b/static/ac7268ed5711abec2e8ce73110f7c556/e7570/google-refresh-token-success.png differ diff --git a/static/ac7268ed5711abec2e8ce73110f7c556/f46e7/google-refresh-token-success.png b/static/ac7268ed5711abec2e8ce73110f7c556/f46e7/google-refresh-token-success.png new file mode 100644 index 0000000000..b784406de9 Binary files /dev/null and b/static/ac7268ed5711abec2e8ce73110f7c556/f46e7/google-refresh-token-success.png differ diff --git a/static/dba28d9d177e6ad40d96eb7c5dd3623d/02d09/debug-1.png b/static/dba28d9d177e6ad40d96eb7c5dd3623d/02d09/debug-1.png new file mode 100644 index 0000000000..64e94fcd9d Binary files /dev/null and b/static/dba28d9d177e6ad40d96eb7c5dd3623d/02d09/debug-1.png differ diff --git a/static/dba28d9d177e6ad40d96eb7c5dd3623d/8bee4/debug-1.png b/static/dba28d9d177e6ad40d96eb7c5dd3623d/8bee4/debug-1.png new file mode 100644 index 0000000000..41fa029d95 Binary files /dev/null and b/static/dba28d9d177e6ad40d96eb7c5dd3623d/8bee4/debug-1.png differ diff --git a/static/dba28d9d177e6ad40d96eb7c5dd3623d/ca1dc/debug-1.png b/static/dba28d9d177e6ad40d96eb7c5dd3623d/ca1dc/debug-1.png new file mode 100644 index 0000000000..ef66641c62 Binary files /dev/null and b/static/dba28d9d177e6ad40d96eb7c5dd3623d/ca1dc/debug-1.png differ diff --git a/static/dba28d9d177e6ad40d96eb7c5dd3623d/e7570/debug-1.png b/static/dba28d9d177e6ad40d96eb7c5dd3623d/e7570/debug-1.png new file mode 100644 index 0000000000..7694cbd342 Binary files /dev/null and b/static/dba28d9d177e6ad40d96eb7c5dd3623d/e7570/debug-1.png differ diff --git a/static/dba28d9d177e6ad40d96eb7c5dd3623d/f46e7/debug-1.png b/static/dba28d9d177e6ad40d96eb7c5dd3623d/f46e7/debug-1.png new file mode 100644 index 0000000000..db5ec30e50 Binary files /dev/null and b/static/dba28d9d177e6ad40d96eb7c5dd3623d/f46e7/debug-1.png differ diff --git a/static/f0a948c22b5527c308e2c44e13f5f8d7/5b400/google-refresh-token-null-2.png b/static/f0a948c22b5527c308e2c44e13f5f8d7/5b400/google-refresh-token-null-2.png new file mode 100644 index 0000000000..d45f4fa3d0 Binary files /dev/null and b/static/f0a948c22b5527c308e2c44e13f5f8d7/5b400/google-refresh-token-null-2.png differ diff --git a/static/f0a948c22b5527c308e2c44e13f5f8d7/ca1dc/google-refresh-token-null-2.png b/static/f0a948c22b5527c308e2c44e13f5f8d7/ca1dc/google-refresh-token-null-2.png new file mode 100644 index 0000000000..c46dd43c55 Binary files /dev/null and b/static/f0a948c22b5527c308e2c44e13f5f8d7/ca1dc/google-refresh-token-null-2.png differ diff --git a/static/f0a948c22b5527c308e2c44e13f5f8d7/e7570/google-refresh-token-null-2.png b/static/f0a948c22b5527c308e2c44e13f5f8d7/e7570/google-refresh-token-null-2.png new file mode 100644 index 0000000000..f7ad204f27 Binary files /dev/null and b/static/f0a948c22b5527c308e2c44e13f5f8d7/e7570/google-refresh-token-null-2.png differ diff --git a/static/f0a948c22b5527c308e2c44e13f5f8d7/f46e7/google-refresh-token-null-2.png b/static/f0a948c22b5527c308e2c44e13f5f8d7/f46e7/google-refresh-token-null-2.png new file mode 100644 index 0000000000..070064cf30 Binary files /dev/null and b/static/f0a948c22b5527c308e2c44e13f5f8d7/f46e7/google-refresh-token-null-2.png differ diff --git a/static/fd2dd90efd7002666575df63734befc8/ca1dc/google-refresh-token-docs.png b/static/fd2dd90efd7002666575df63734befc8/ca1dc/google-refresh-token-docs.png new file mode 100644 index 0000000000..514a02b62b Binary files /dev/null and b/static/fd2dd90efd7002666575df63734befc8/ca1dc/google-refresh-token-docs.png differ diff --git a/static/fd2dd90efd7002666575df63734befc8/dff2b/google-refresh-token-docs.png b/static/fd2dd90efd7002666575df63734befc8/dff2b/google-refresh-token-docs.png new file mode 100644 index 0000000000..f9136d019a Binary files /dev/null and b/static/fd2dd90efd7002666575df63734befc8/dff2b/google-refresh-token-docs.png differ diff --git a/static/fd2dd90efd7002666575df63734befc8/e7570/google-refresh-token-docs.png b/static/fd2dd90efd7002666575df63734befc8/e7570/google-refresh-token-docs.png new file mode 100644 index 0000000000..c495d84776 Binary files /dev/null and b/static/fd2dd90efd7002666575df63734befc8/e7570/google-refresh-token-docs.png differ diff --git a/static/fd2dd90efd7002666575df63734befc8/f46e7/google-refresh-token-docs.png b/static/fd2dd90efd7002666575df63734befc8/f46e7/google-refresh-token-docs.png new file mode 100644 index 0000000000..cb3ad517a8 Binary files /dev/null and b/static/fd2dd90efd7002666575df63734befc8/f46e7/google-refresh-token-docs.png differ diff --git a/tags/index.html b/tags/index.html index 523b206fc4..7970abd5c0 100644 --- a/tags/index.html +++ b/tags/index.html @@ -50,7 +50,7 @@ .khRKr{margin-top:20px;}/*!sc*/ @media (max-width:768px){.khRKr{padding:0 15px;}}/*!sc*/ data-styled.g53[id="tags__TagListWrapper-sc-1p0kse9-0"]{content:"khRKr,"}/*!sc*/ -나누면 배가 되고 +나누면 배가 되고 JdbcTemplate는 어디에?

JdbcTemplate는 어디에?

@Hyeonic · April 28, 2022 · 9 min read

+

개요

+

웹 체스 미션을 진행하던 중 Spring Jdbc를 도입하기 위해 이전에 연결된 JDBC에 대한 의존성을 제거한 뒤 Spring-jdbc에서 제공하는 JdbcTemplate을 활용하여 SQL 쿼리를 사용하였다. 하지만 나는 JdbcTemplate에 대한 Bean 등록을 진행하지 않았다. 그렇다면 누가 자동으로 등록한 것일까?

+

+

JDBC (Java DataBase Connectivity)

+

우선 이전에 사용하던 JDBC에 대해 간단히 알아본다. JDBC는 Java와 데이터베이스를 연결하기 위한 Java 표준 인터페이스이다. 아래 그림과 같이 MySql, oracle 등 다양한 DB의 미들웨어의 드라이버를 제공하고 있다. 덕분에 어떤 DB에 연결되는지에 따라 드라이버를 선택하여 적용할 수 있다. 또한 어떤 DB의 드라이버인지 상관없이 일관적인 방식으로 사용할 수 있도록 도와준다.

+

+

일반적인 JDBC를 그대로 사용하게 되면 아래와 같은 흐름으로 사용하게 된다.

+
    +
  • JDBC 드라이버를 로드
  • +
  • DB를 연결
  • +
  • DB의 데이터 조회 및 쓰기
  • +
  • DB 연결 종료
  • +
+

덕분에 DB에 접근하여 SQL 쿼리를 실행하기 위해 복잡한 코드를 동반하게 된다.

+
public class JdbcPieceDao implements PieceDao {
+
+    private static final String URL = "jdbc:mysql://localhost:3306/chess";
+    private static final String USER = "user";
+    private static final String PASSWORD = "password";
+
+    @Override
+    public void save(PieceDto pieceDto) {
+        String sql = "INSERT INTO piece (id, piece_type) VALUES (?, ?)";
+
+        try (Connection connection = getConnection();
+             PreparedStatement statement = connection.prepareStatement(sql)) {
+
+            statement.setString(1, pieceDto.getId());
+            PieceType pieceType = pieceDto.getPieceType();
+            statement.setString(2, pieceType.getType());
+
+            statement.executeUpdate();
+        } catch (SQLException e) {
+            throw new IllegalArgumentException("기물의 위치는 중복될 수 없습니다.");
+        }
+    }
+    ...
+        private Connection getConnection() {
+        Connection connection = null;
+        try {
+            connection = DriverManager.getConnection(URL, USER, PASSWORD);
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        return connection;
+    }
+}
+

Spring JDBC

+

Spring JDBC는 Driver 및 DB 연결과 Connection 객체의 관리를 수행하는 DataSource를 설정을 통해 생성하며 위에서 사용한 것 처럼 JDBC API를 직접 사용했을 때 불편했던 것들을 쉽게 사용할 수 있도록 도와준다.

+

정리하면 JDBC API의 모든 저수준 처리를 Spring Framework에 위임하기 때문에 위에서 작성한 반복되는 처리를 개발자가 직접 처리하지 않고 Database에 대한 작업을 수행할 수 있도록 도와준다.

+

Data Access with JDBC

+

JBDC 데이터베이스 접근의 기초를 형성하기 위해 여러 접근 방식을 선택할 수 있다.

+
    +
  • JdbcTemplate: 고전적이고 가장 인기 있는 Spring JDBC 방식이다. lowest-level 접근법과 다른 모든 것들은 JdbcTemplate를 사용한다.
  • +
  • NamedParameterJdbcTemplate: 기존 JDBC ? 표시자 대신 명명된 매개 변수를 제공하기 위해 JdbcTemplate을 랩핑한다. 이러한 접근 방식은 SQL 문에 대한 매개 변수가 여러 개 일 때 더 나은 문서화와 사용 편의성을 제공한다.
  • +
  • SimpleJdbcInsert: 데이터베이스 메타데이터를 최적화하여 필요한 구성 양을 제한한다. 해당 방법을 사용하면 테이블 또는 프로시저의 이름만 제공하고 column 이름과 일치하는 맵을 제공해야 하므로 코딩이 매우 간소화된다. 하지만 이것은 데이터베이스가 적절한 메타데이터를 제공하는 경우에만 작동한다. 데이터베이스가 이 메타데이터를 제공하지 않는 경우 매개 변수의 명시적 구성을 제공해야 한다.
  • +
+

JdbcTemplate는 어디에?

+

이제 JDBC와 Spring JDBC에 대한 간단한 개념 정리를 진행했다. 본론으로 넘어와 JdbcTemplate를 자동으로 등록한 곳을 찾아보려 한다.

+

Spring Boot의 자동 구성은 애플리케이션에 적용할 수 있는 여러 구성 클래스로 작동한다. 이런 모든 구성은 Spring 4.0의 조건부 구성 지원 기능을 이용하여 런타임 시점에 구성을 사용할지 여부를 결정한다.

+

아래는 org.springframework.boot.autoconfigure.jdbc 패키지에 위치한 JdbcTemplateConfiguration 클래스이다.

+
package org.springframework.boot.autoconfigure.jdbc;
+
+...
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnMissingBean(JdbcOperations.class)
+class JdbcTemplateConfiguration {
+
+	@Bean
+	@Primary
+	JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
+		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
+		JdbcProperties.Template template = properties.getTemplate();
+		jdbcTemplate.setFetchSize(template.getFetchSize());
+		jdbcTemplate.setMaxRows(template.getMaxRows());
+		if (template.getQueryTimeout() != null) {
+			jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
+		}
+		return jdbcTemplate;
+	}
+}
+

jdbcTemplate(DataSource dataSource, JdbcProperties properties) 메서드는 @Bean 애너테이션 덕분에 JdbcTemplate Bean을 구성해준다. 하지만 주목해야 할 것은 @ConditionalOnMissingBean(JdbcOperations.class) 부분이다.

+

@ConditionalOnMissingBean(JdbcOperations.class)

+

@ConditionalOnMissingBean은 속성으로 전달된 JdbcOperations 타임의 Bean이 없을 때만 동작한다. JdbcTemplate은 바로 JdbcOperations의 구현체이다.

+
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
+    ...
+}
+

만약 개발자가 명시적으로 JdbcOperations 타입의 Bean을 구성했다면 @ConditionalOnMissingBean 애너테이션의 조건에 만족하지 못하므로 해당 메서드는 사용되지 않는다.

+

정리하면 나는 명시적으로 JdbcTempalate를 등록하지 않았다. 그렇기 때문에 @ConditionalOnMissingBean 애너테이션의 조건에 만족하여 자동 구성에서 제공하는 JdbcTemplate를 Bean으로 등록하여 사용하고 있는 것이다.

+

관련 키워드를 검색하기 위해 구글링하던 중 stackOverflow에서 관련 된 글을 찾아볼 수 있었다.

+

Question

+

How does spring boot inject the instance of ApplicationContext and JdbcTemplate using @Autowired without @Component annotation and xml configuration?

+

Spring Boot에서 @Component 및 xml 구성 없이 @Autowired를 사용하여 JdbcTemplate 인스턴스를 주입하는 방법은 무엇인가?

+

i'm in a spring boot app building rest controller.i find that ApplicationContext and JdbcTemplate source code,these 2 classes do not have any annotation.But they can be correctly injected into constructor.i am not using any configuration file like 'applicationContext.xml'.When do these 2 classes get scanned by spring ioc container?

+

ApplicationContext 및 JdbcTemplate 소스 코드, 이 두 클래스에는 annotation이 없다. 그러나 constructor에 올바르게 삽입할 수 있다. 'applicationContext.xml'과 같은 구성 파일을 사용하지 않았다.이 두 클래스는 언제 spring ioc 컨테이너로 스캔되는가?

+

Answer

+

Spring Boot does a lot of auto configuration.

+

Sprign Boot는 많은 자동 구성을 수행한다.

+

I assume that you are using spring-data-jdbc or spring-data-jpa and there for the JdbcTemplate is auto configured.

+

spring-data-jdbc 또는 spring-data-jpa를 사용하고 있으며 JdbcTemplate에 대해 자동 구성되었다고 가정한다.

+

The most interesting project is: spring-boot-autoconfigure where all the magic happens.

+

가장 흥미로운 프로젝트는 spring-boot-autoconfigure이다. 모든 마술이 일어나는 곳이다!

+

And there you will find JdbcTemplateConfiguration.java

+

또한 JdbcTemplateConfiguration.java 관련 설정을 찾아볼 수 있다.

+

정리

+

정리하면 우린 spring-boot-autoconfigure 덕분에 명시적으로 JdbcTemplate을 Bean으로 등록하지 않아도 자동 설정되므로 사용가능하다.

+

이러한 자동 구성 덕분에 우리는 편리하게 기능에만 집중할 수 있게 된다. 만약 추가적인 JdbcTempalate에 대한 설정이 필요하다면 명시적인 등록을 추가하여 Bean으로 작성하기만 하면 된다.

+

References.

+

Spring Data Access
+Infra layer with Spring — Spring jdbc 개념과 예시 코드
+Spring JDBC
+How does spring boot inject the instance of ApplicationContext and JdbcTemplate using @Autowired without @Component annotation and xml configuration?

@Hyeonic
나누면 배가 되고
+ + \ No newline at end of file