From 6962b3a8cc78f9bdab3195b5249e0c3ce7bc09b0 Mon Sep 17 00:00:00 2001 From: hyeonic Date: Mon, 12 Feb 2024 00:23:40 +0900 Subject: [PATCH] Updates --- basic-tomcat/index.html | 2 +- google-refresh-token/index.html | 494 ++++++++++++++++++ identity-strategy/index.html | 2 +- index.html | 6 +- java-string/index.html | 2 +- local-date-time/index.html | 11 +- page-data/2022-retrospect/page-data.json | 2 +- page-data/basic-tomcat/page-data.json | 2 +- page-data/google-refresh-token/page-data.json | 1 + page-data/identity-strategy/page-data.json | 2 +- page-data/index/page-data.json | 2 +- page-data/java-string/page-data.json | 2 +- page-data/jdbc/page-data.json | 2 +- page-data/jpa-auditing/page-data.json | 2 +- page-data/local-date-time/page-data.json | 2 +- page-data/optimistic-locking/page-data.json | 2 +- page-data/osiv/page-data.json | 2 +- page-data/properties-to-object/page-data.json | 1 + page-data/save-persist-merge/page-data.json | 2 +- page-data/search/page-data.json | 2 +- page-data/separated-interface/page-data.json | 1 + page-data/series/page-data.json | 2 +- page-data/spring-jdbc-batch/page-data.json | 1 + page-data/tags/page-data.json | 2 +- page-data/template-callback/page-data.json | 2 +- page-data/why-jdbc-template/page-data.json | 1 + properties-to-object/index.html | 415 +++++++++++++++ rss.xml | 2 +- search/index.html | 6 +- separated-interface/index.html | 477 +++++++++++++++++ sitemap-0.xml | 2 +- spring-jdbc-batch/index.html | 492 +++++++++++++++++ .../0b5a5/debug-2.png | Bin 0 -> 3845 bytes .../e7570/debug-2.png | Bin 0 -> 1797 bytes .../f46e7/debug-2.png | Bin 0 -> 4116 bytes .../02d09/google-refresh-token-null.png | Bin 0 -> 34484 bytes .../7c559/google-refresh-token-null.png | Bin 0 -> 23306 bytes .../9d567/google-refresh-token-null.png | Bin 0 -> 48245 bytes .../ca1dc/google-refresh-token-null.png | Bin 0 -> 23044 bytes .../e7570/google-refresh-token-null.png | Bin 0 -> 4000 bytes .../f46e7/google-refresh-token-null.png | Bin 0 -> 10478 bytes .../02d09/oauth-flow.png | Bin 0 -> 52431 bytes .../9d567/oauth-flow.png | Bin 0 -> 73949 bytes .../c7a69/oauth-flow.png | Bin 0 -> 77330 bytes .../ca1dc/oauth-flow.png | Bin 0 -> 30221 bytes .../e7570/oauth-flow.png | Bin 0 -> 4972 bytes .../f46e7/oauth-flow.png | Bin 0 -> 12483 bytes .../b2dbf/google-oauth-uri.png | Bin 0 -> 28023 bytes .../ca1dc/google-oauth-uri.png | Bin 0 -> 31859 bytes .../e7570/google-oauth-uri.png | Bin 0 -> 5272 bytes .../f46e7/google-oauth-uri.png | Bin 0 -> 13014 bytes .../ca1dc/google-refresh-token-success.png | Bin 0 -> 38177 bytes .../e4da8/google-refresh-token-success.png | Bin 0 -> 19617 bytes .../e7570/google-refresh-token-success.png | Bin 0 -> 7716 bytes .../f46e7/google-refresh-token-success.png | Bin 0 -> 17294 bytes .../02d09/debug-1.png | Bin 0 -> 51232 bytes .../8bee4/debug-1.png | Bin 0 -> 29655 bytes .../ca1dc/debug-1.png | Bin 0 -> 29828 bytes .../e7570/debug-1.png | Bin 0 -> 4662 bytes .../f46e7/debug-1.png | Bin 0 -> 11966 bytes .../5b400/google-refresh-token-null-2.png | Bin 0 -> 19140 bytes .../ca1dc/google-refresh-token-null-2.png | Bin 0 -> 35073 bytes .../e7570/google-refresh-token-null-2.png | Bin 0 -> 7172 bytes .../f46e7/google-refresh-token-null-2.png | Bin 0 -> 15402 bytes .../ca1dc/google-refresh-token-docs.png | Bin 0 -> 80843 bytes .../dff2b/google-refresh-token-docs.png | Bin 0 -> 53488 bytes .../e7570/google-refresh-token-docs.png | Bin 0 -> 10488 bytes .../f46e7/google-refresh-token-docs.png | Bin 0 -> 30627 bytes tags/index.html | 2 +- why-jdbc-template/index.html | 339 ++++++++++++ 70 files changed, 2258 insertions(+), 29 deletions(-) create mode 100644 google-refresh-token/index.html create mode 100644 page-data/google-refresh-token/page-data.json create mode 100644 page-data/properties-to-object/page-data.json create mode 100644 page-data/separated-interface/page-data.json create mode 100644 page-data/spring-jdbc-batch/page-data.json create mode 100644 page-data/why-jdbc-template/page-data.json create mode 100644 properties-to-object/index.html create mode 100644 separated-interface/index.html create mode 100644 spring-jdbc-batch/index.html create mode 100644 static/3206413c52066864f6bf338e4a3e2fd6/0b5a5/debug-2.png create mode 100644 static/3206413c52066864f6bf338e4a3e2fd6/e7570/debug-2.png create mode 100644 static/3206413c52066864f6bf338e4a3e2fd6/f46e7/debug-2.png create mode 100644 static/40821173b75b8bc6f832d00faa388a42/02d09/google-refresh-token-null.png create mode 100644 static/40821173b75b8bc6f832d00faa388a42/7c559/google-refresh-token-null.png create mode 100644 static/40821173b75b8bc6f832d00faa388a42/9d567/google-refresh-token-null.png create mode 100644 static/40821173b75b8bc6f832d00faa388a42/ca1dc/google-refresh-token-null.png create mode 100644 static/40821173b75b8bc6f832d00faa388a42/e7570/google-refresh-token-null.png create mode 100644 static/40821173b75b8bc6f832d00faa388a42/f46e7/google-refresh-token-null.png create mode 100644 static/4c02b6ded86cbf0190013659f8371f6c/02d09/oauth-flow.png create mode 100644 static/4c02b6ded86cbf0190013659f8371f6c/9d567/oauth-flow.png create mode 100644 static/4c02b6ded86cbf0190013659f8371f6c/c7a69/oauth-flow.png create mode 100644 static/4c02b6ded86cbf0190013659f8371f6c/ca1dc/oauth-flow.png create mode 100644 static/4c02b6ded86cbf0190013659f8371f6c/e7570/oauth-flow.png create mode 100644 static/4c02b6ded86cbf0190013659f8371f6c/f46e7/oauth-flow.png create mode 100644 static/7b4590a4b46a14708d45e7589d075bf7/b2dbf/google-oauth-uri.png create mode 100644 static/7b4590a4b46a14708d45e7589d075bf7/ca1dc/google-oauth-uri.png create mode 100644 static/7b4590a4b46a14708d45e7589d075bf7/e7570/google-oauth-uri.png create mode 100644 static/7b4590a4b46a14708d45e7589d075bf7/f46e7/google-oauth-uri.png create mode 100644 static/ac7268ed5711abec2e8ce73110f7c556/ca1dc/google-refresh-token-success.png create mode 100644 static/ac7268ed5711abec2e8ce73110f7c556/e4da8/google-refresh-token-success.png create mode 100644 static/ac7268ed5711abec2e8ce73110f7c556/e7570/google-refresh-token-success.png create mode 100644 static/ac7268ed5711abec2e8ce73110f7c556/f46e7/google-refresh-token-success.png create mode 100644 static/dba28d9d177e6ad40d96eb7c5dd3623d/02d09/debug-1.png create mode 100644 static/dba28d9d177e6ad40d96eb7c5dd3623d/8bee4/debug-1.png create mode 100644 static/dba28d9d177e6ad40d96eb7c5dd3623d/ca1dc/debug-1.png create mode 100644 static/dba28d9d177e6ad40d96eb7c5dd3623d/e7570/debug-1.png create mode 100644 static/dba28d9d177e6ad40d96eb7c5dd3623d/f46e7/debug-1.png create mode 100644 static/f0a948c22b5527c308e2c44e13f5f8d7/5b400/google-refresh-token-null-2.png create mode 100644 static/f0a948c22b5527c308e2c44e13f5f8d7/ca1dc/google-refresh-token-null-2.png create mode 100644 static/f0a948c22b5527c308e2c44e13f5f8d7/e7570/google-refresh-token-null-2.png create mode 100644 static/f0a948c22b5527c308e2c44e13f5f8d7/f46e7/google-refresh-token-null-2.png create mode 100644 static/fd2dd90efd7002666575df63734befc8/ca1dc/google-refresh-token-docs.png create mode 100644 static/fd2dd90efd7002666575df63734befc8/dff2b/google-refresh-token-docs.png create mode 100644 static/fd2dd90efd7002666575df63734befc8/e7570/google-refresh-token-docs.png create mode 100644 static/fd2dd90efd7002666575df63734befc8/f46e7/google-refresh-token-docs.png create mode 100644 why-jdbc-template/index.html 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 0000000000000000000000000000000000000000..babbcb95e9021f7a8a1f2f29e88090c0fb39ed47 GIT binary patch literal 3845 zcmV+g5Bl(lP)Bg zhE|2k#Y0L(Y=u{7epG3HRZCV#N>fN|gjPO6LPbnPTy9W3KtXGRR#06^TWn8VZcj;4 zM?y(NRb)*-Mng|qNoRdjLP$hcW=%aoLi>-4NKi*VL_=$WR!USzT53-~MMG(TRb_iq zM^8p%c~eD8MO0%xhMDl#$wje~yokmY0~hxw%zjOohqAZG~6KdU#1uM`C4WVNOp}NlQLKLRfWkMp08y zM@ej)nQE7l`HqTNOHFxLSaViaTun}WT3Ux*UOq!YXn$2|QdEp!V9$Jd_l=1{K|_OF zU2~|UKtw}ofmWAfWZ;8=?umuKcXYgUb52G`qG@SLMMhI>Yg%}Afx^9HPf@dQZ+2H# zr)z6ZM@dCPMY?oyV|i1OV`HCaXnTEroSmM6gM?XJU1DTqZBtd#3ynGe000SaNLh0L z01FcU01FcV0GgZ_000c&Nkliy5aEILYUiovky64nr3<;3;62JMqhlOXVs;jHN>F$vmz?Lmr zwrtt5Wy_W=TefWdF7?6rf1u94ZCAq9Yv}xukH0u_uu+Lw2RWeu*_5(4OwGQ7;KYt##3KkGi1Fw2jus2CZ5bh9c~<rI<;@8u@VL5><+3Xaxe@Z>cqg%eN$TnZzFBO=!?%kK5{Te zr{C?lBn@T!8b$&s0O#qkQ7$FdoXym7wfkOzW4WbyqxJZhRqGCNihXVq?AcLL$&)q) zbJfDBz2Pigk-{jg20&7*zEOhl)@a?xh3xi&@k$D_DNE9@#3ulJPD6NSnB)7YM;RuO^%C?}DR#l47St~{mBM09Pv3wtNY9SEY!w|aD6d<;m zYjk521YwS!WFcXQRgNi?J1Rn`xUAMu_g3Sn1=fbhoRfypDyfMq6_~BWy7S$9kWn$h z+m2?= zFh`&H2QAv~6k_DuWmT#v<#2FAJs7zjEaT4&i9$S(OSWoRSemC4T!8)*T`B6dj*5{u zq0Tk^o^c`Ao*>(qEYV%{(R|o^^cW6EJ%x9hj#WrREYV zc~P8SNsQE5(8tGst6Uh#$sL%ILhO<%F_n&0787$44|UhE=_t-OGDdUURC%`65W>ra zkrXoKT*PingsV`OCWsg!3v6{E5v3f(C>$LR1Of?I;F&t$n%E1{y3j=|R`E+ybah(n zr@NHQqKxpYicydZC{;@0V@CX*{&rs5lVrvX^HrC(8qYyCupQSaNcs%O^E}u3nVwt-JIM1-EN`r7N6%Ei;SZz>s(91Nq znG1DcG>Vp9w%F5!)`*du)+!u5TCdkJs{a%?JNlw#ZfoR1aLe?<|CWKH^IaG{(igS= z{gvv!hJ7;jFSS9R3pyL&){vZ-#?)at0Rk46>sQBOfycqbOG0#b)(gopl)3sD(x4S( zpgYI~1*3L`PNstix{ht(&1@aJ(MYAClRrbm0*T0F3MeDA>c^$p>2wxBIKh+!mwP-4 zUamngf&gKQ=Yn4%Yh^e$%hH})(H6Ccs8*@w!@bdbgmtRHlyCAgYNorKo z8kA)$)1Z)VcJGbQk}h;r!mJD!d0ARG-4Ik8r_gnXuIU(Ax7DS$b&lTqtP7)etoKGL zMh$X?KFWv@ech1eHl*&o(Ey!)qVz^RQ4pO;2pYa5v55D&4<6z{?g%NSKC;49HN z<)%D}^`$1{x2B0m=L|{(LMD$8K9@ahp=`j8<7Bf25;dDyUg zvcco_b@hbQ-;2>Pd~;+WN5krkL5beD;8*uX)EmvIw-V11BSTK=n2iyi&$mY(;6D1& z>6TNC7#B4Sl7?$c&MwJmUw?!%Y#Q89<7VdR;<7=4j$?G6P~0>Rt)Wa9q(ySQ7?t>` z0&T&7k(^dXgOz4Y##D?BYRi%%g(85NlV9Gt(mfmfiRoY6i(0!f?HHyQCif~1**BfG&v===$2zOQ^3pE~hLy1sE!^p?G8&N3&{#vH#LX74D6eC#V zGe9vK+KW0IV|26U8t?`NBrMm_CPw$hf>EXfVgv(x1&kPtxjcenaUL!WSJFX3)&%gm zsMGCOhiPpelw68X+y??G119gw{D6_y0Q15IykKSrL`G#R{WXg$N@Op#b zY_X-up z$c<n(w}jV~h6RK$ppoYtViNs#TxTF$D{{e#-F;z*+i9?rzi zzVq@$4U*#=H$X8O&5?PWh8=5roc}f~xp0deU>J?kr535Cm(*|52yPV0d3;KE5ZZ){ zQj7vHj-46Hkvsx$X)rS}hZyBL45JY;%`k%gJRy0ChKmqsyoFazvGDr$Yp!(&(Wx;N z&@d8aIYNp!il4M4w8d@X6mwcH5ik8df#g={`amy*Gy3;Np>sUp`UGZyIWljlnbqUT zH}rL?Cnp#E*j|ty=kTtAky&oU$d_YY-1bsd&Q4l0e7DyL6XNu!ac@I;m?N&uD((`T zXgSbZ1iegjN{ggMjX7d}VYh}9$L1GdO2%l{kk2WG(STEiDWG=$ptgEB(kTMCfAytb zen$x1^50hI5S4cv`FF9`T))(jcW6oZJ4r$4GNWN)xH?r&ZR0BMFh>c6Ug2x{SV>7v zEQzg@zMR~#uuUJ6MR_T8KXmAAif3|oWi46oB`9$z;t^A?Z>x0)IYdhbQ<>gGj$LiN z=nu4?{i>*?TJaV0@o&D&Vw5Z9q%;)5zrmk=E@|JWw`MJF5Bg>OVqAZOHh; zea+UP!N`0T`{Mr0<0sGHyYF`%KmATQnZ3V1^X@Hxe?Gc$_8~mmIlVLU&BMntPwuN$ zX32+kN9Orr0H77-*QTuw^4dBCTkHeRVxRr_=FU&>bm!JbJKxJEvyaD5U)$e-i;us$ z{#E?y<@lp(-{8N8S8l0RU^=(W4(%qntw^*|4b(Qu&7##oUR!_s7ybj!Vm~>1^FIE1 z<)^DxznPIwX44CC;y6r*bZGyeoQ`>ASW-9&$d6z8C^%u9q+ z?TR0Aj!b8<0iNI5d3v96q7bBF^y8JA_b+x~q*{U1xouQrl7u$%K^U?zI_w;o&tmWI zU%m0@IP9Oj{{EA9%^2Ny(uI*~Mb&d|yAbw>Q(ST~A91YN7#(tsdd^~BI~%|9&JDPH z_g?(bbseM2@vooVjmMA2Kl;{ykz!@3>Jm?KgAkl2sY!cpbhtUPI*WbrIs15ivB%+Z z?=Hx~N|PvWv6G@*>cjoM_AAd~+p=ZLmMvShY}vA9%dGzcygn_8B5!y600000NkvXX Hu0mjfoBul= literal 0 HcmV?d00001 diff --git a/static/3206413c52066864f6bf338e4a3e2fd6/e7570/debug-2.png b/static/3206413c52066864f6bf338e4a3e2fd6/e7570/debug-2.png new file mode 100644 index 0000000000000000000000000000000000000000..026098c48932e068e4af6855ddb4a2f4bd0056cc GIT binary patch literal 1797 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 S#u|kH0000Z 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 V+Q<{1rXKYD%UIt+53lPI|1UKxS1tem literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2e0293ef4049b5c67da581b8ba259c8d15e24331 GIT binary patch literal 48245 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�qilAGOH<@kWhu-sa496~;HfXkhEdJMNTQ8(^Pe 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