<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>문홍의 공부장</title>
    <link>https://moonong.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 29 Jul 2026 04:24:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>moonong</managingEditor>
    <item>
      <title>카카오 소셜 로그인 구현기</title>
      <link>https://moonong.tistory.com/133</link>
      <description>&lt;h2 id=&quot;%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;들어가며&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번 작업은 개인 프로젝트 앱에 카카오 소셜 로그인을 추가한 기록입니다. 기존 이메일/비밀번호 로그인 흐름은 유지하면서, 동일한&amp;nbsp;/api/v1/login&amp;nbsp;엔드포인트로 OAuth 로그인까지 처리하도록 서버와 클라이언트를 함께 확장했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;oauth-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;5&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;OAuth 개념 정리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;OAuth 2.0은 &quot;인증&quot;과 &quot;인가&quot;를 분리해주는 표준 프로토콜입니다. 사용자는 서비스에 비밀번호를 직접 제공하지 않고, 외부 인증 제공자(카카오 등)로부터 발급된 토큰을 통해 서비스 접근 권한을 위임합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인증(Authentication): 사용자가 누구인지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인가(Authorization): 해당 사용자에게 어떤 권한을 부여할지 결정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번 구현에서는 &quot;카카오로 로그인&quot;을 통해 인가 코드를 받고, 서버가 카카오에 토큰을 요청해 사용자 정보를 얻는 흐름을 사용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;%EC%B9%B4%EC%B9%B4%EC%98%A4-oauth-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%ED%94%8C%EB%A1%9C%EC%9A%B0&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;13&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;카카오 OAuth 인증/인가 플로우&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;아래는 카카오 OAuth에서 일반적으로 사용하는 Authorization Code 플로우를 개발중인 프로젝트를 기준으로 정리한 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;div id=&quot;mermaid-container-bac9c5e1-7117-45c7-a3ab-e59b68fcb703&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxjp7A/dJMcadHuN5g/3KIKnhqzuAFENu2IrOcoyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxjp7A/dJMcadHuN5g/3KIKnhqzuAFENu2IrOcoyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxjp7A/dJMcadHuN5g/3KIKnhqzuAFENu2IrOcoyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbxjp7A%2FdJMcadHuN5g%2F3KIKnhqzuAFENu2IrOcoyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1470&quot; height=&quot;368&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h3 id=&quot;%EC%8B%9C%ED%80%80%EC%8A%A4-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;29&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;시퀀스 다이어그램&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이언트와 서버, 카카오가 어떤 순서로 통신하는지 더 직관적으로 표현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sT6uV/dJMcac2TrOz/cfxVT5FHd42xvzbpyfvOak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sT6uV/dJMcac2TrOz/cfxVT5FHd42xvzbpyfvOak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sT6uV/dJMcac2TrOz/cfxVT5FHd42xvzbpyfvOak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsT6uV%2FdJMcac2TrOz%2FcfxVT5FHd42xvzbpyfvOak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1324&quot; height=&quot;888&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;%EC%A0%84%EC%B2%B4-%ED%94%8C%EB%A1%9C%EC%9A%B0&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;60&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;전체 플로우&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;61&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;67&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이언트가 카카오 인가 코드 요청&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;68&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;카카오가 리다이렉트 URI로 인가 코드 전달&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;69&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이언트가 서버&amp;nbsp;/api/v1/login&amp;nbsp;호출 (oauthType, authCode 전달)&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;70&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서버가 카카오에 토큰 요청 &amp;rarr; 사용자 정보 조회&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;71&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용자 저장/조회 후 JWT 발급 및 응답&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EA%B5%AC%ED%98%84&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;67&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이언트 구현&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;1-%EC%9D%B8%EA%B0%80-%EC%BD%94%EB%93%9C-%ED%9A%8D%EB%93%9D&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;68&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1) 인가 코드 획득&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Expo 환경에서&amp;nbsp;expo-web-browser&amp;nbsp;+&amp;nbsp;expo-linking&amp;nbsp;조합으로 인가 코드를 받았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;70&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;79&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;openAuthSessionAsync()로 카카오 로그인 페이지 열기&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;80&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;리다이렉트 URL에서&amp;nbsp;code&amp;nbsp;파싱&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;73&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;관련 코드:&amp;nbsp;client/utils/kakaoAuth.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;const authUrl =
  'https://kauth.kakao.com/oauth/authorize' +
  `?client_id=${encodeURIComponent(REST_API_KEY)}` +
  `&amp;amp;redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
  '&amp;amp;response_type=code';

const result = await WebBrowser.openAuthSessionAsync(authUrl, REDIRECT_URI);
const { queryParams } = Linking.parse(result.url);
const code = queryParams?.code;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-oauth-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9A%94%EC%B2%AD&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;87&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2) OAuth 로그인 요청&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;88&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인가 코드를 서버에 전달하는 전용 함수&amp;nbsp;oauthLogin을 추가했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;89&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;100&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;POST /login&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;101&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;oauthType: 'KAKAO', authCode&amp;nbsp;전달&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;관련 코드:&amp;nbsp;client/api/auth.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;export async function oauthLogin(data: OAuthLoginRequest): Promise&amp;lt;AuthResponse&amp;gt; {
  const res = await apiFetch('/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  return await handleAuthResponse(res);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-ui&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;106&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3) UI&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;107&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;로그인 화면에 카카오 로그인 버튼을 추가했습니다. 관련 코드:&amp;nbsp;client/screens/SignupScreen.tsx&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;&amp;lt;TouchableOpacity style={styles.kakaoButton} onPress={handleKakaoLogin}&amp;gt;
  &amp;lt;Text style={styles.kakaoButtonText}&amp;gt;카카오로 로그인&amp;lt;/Text&amp;gt;
&amp;lt;/TouchableOpacity&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;116&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서버 구현&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;1-oauth-%ED%83%80%EC%9E%85-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%8B%9D%EB%B3%84-%EC%BB%AC%EB%9F%BC-%EC%B6%94%EA%B0%80&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;117&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1) OAuth 타입 및 사용자 식별 컬럼 추가&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;118&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;소셜 로그인을 식별하기 위해&amp;nbsp;users&amp;nbsp;테이블에&amp;nbsp;oauth_type,&amp;nbsp;oauth_provider_id&amp;nbsp;컬럼을 추가했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;ALTER TABLE users ADD COLUMN oauth_type VARCHAR(20);
ALTER TABLE users ADD COLUMN oauth_provider_id VARCHAR(255);
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;125&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;2-kakao-oauth-client-%EC%B6%94%EA%B0%80&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;127&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2) Kakao OAuth Client 추가&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;128&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;카카오 토큰 발급 및 사용자 조회를 담당하는&amp;nbsp;KakaoOAuthClient를 구현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;129&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;145&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;토큰 요청:&amp;nbsp;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://kauth.kakao.com/oauth/token&quot;&gt;https://kauth.kakao.com/oauth/token&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;146&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용자 조회:&amp;nbsp;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://kapi.kakao.com/v2/user/me&quot;&gt;https://kapi.kakao.com/v2/user/me&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;134&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;핵심 포인트:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;135&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;152&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;토큰/사용자 응답이 비어있으면 예외 처리&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;153&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용자 이메일 동의 여부 확인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B6%84%EA%B8%B0-%EC%B2%98%EB%A6%AC&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;138&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3) 로그인 분기 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;139&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기존&amp;nbsp;AuthService.signIn에서&amp;nbsp;oauthType이 존재하면 OAuth 로그인으로 분기했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주요 로직:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;144&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;163&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;oauthType&amp;nbsp;유효성 검증 (현재는&amp;nbsp;KAKAO만 지원)&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;164&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;카카오 사용자 정보 획득&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;165&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기존 계정과의 충돌 방지&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-line=&quot;166&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;166&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동일 이메일이 존재하면 가입 차단&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-line=&quot;167&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;신규 계정이면&amp;nbsp;oauth_type,&amp;nbsp;oauth_provider_id&amp;nbsp;저장&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;168&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;JWT 발급 및 응답&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;151&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;환경 설정&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;%EC%84%9C%EB%B2%84&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;152&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서버&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;153&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;application.yml&amp;nbsp;에 카카오 설정을 추가했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;154&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;176&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kakao.client-id&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;177&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kakao.client-secret&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;178&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kakao.redirect-uri&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;158&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이언트&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;159&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;.env에 아래 값을 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;160&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;184&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;EXPO_PUBLIC_KAKAO_CLIENT_ID&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;185&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;EXPO_PUBLIC_KAKAO_REDIRECT_URI&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;175&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;176&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번 작업으로 기존 이메일 로그인 흐름을 유지하면서 카카오 소셜 로그인을 자연스럽게 통합했습니다. 서버 측에서 사용자 중복 방지와 필수 동의 검증을 추가하면서 안전한 로그인 경험을 만들 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;178&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다음 단계로는 다른 OAuth 제공자(예: Google) 확장과, 기존 이메일 계정을 카카오 계정으로 연결하는 계정 연동 플로우를 고려해볼 수 있을 것 같습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/Java</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/133</guid>
      <comments>https://moonong.tistory.com/133#entry133comment</comments>
      <pubDate>Fri, 16 Jan 2026 22:53:26 +0900</pubDate>
    </item>
    <item>
      <title>2024 개발자 회고</title>
      <link>https://moonong.tistory.com/132</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 포커싱하여 회고를 적어볼까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 달마다 적는 월간회고도 거~의 안적고, 그냥 마냥 바쁘게만 살았던 것 같다. 근데 그러다보니까, 막상 1년이 지나고 내가 뭘 했지? 하니까 떠오르는 게 없다. 그나마 다행인건 8월 이전까지는 데일리 업무일지를 꼬박꼬박 써놨어서 확인이 가능했다. (8월 이후에 한 업무는 가장 최근까지 진행했어서 그래도 기억이 난다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이래서 주기적으로 적고 업데이트를 해둬야해.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;================================================&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024 타임라인&lt;br /&gt;1월 [VoC] 회원가입 프로세스 간소화 &amp;amp; 정보 동기화&lt;br /&gt;2-3월 [VoC] 요금제 전환&lt;br /&gt;4월 신규 클러스터 연동&lt;br /&gt;4-6월 프로젝트 시작 - 분석/설계&lt;br /&gt;4-6월 요금제 전환 안정화&lt;br /&gt;7월 쿠버네티스 포팅 &amp;amp; Java/Spring 버전 업그레이드&lt;br /&gt;8-11월 빌링/정산 분리&lt;br /&gt;12월 빌링/정산 분리 안정화 &amp;amp; 프로젝트 파트 업무 전환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12월 사내 세미나 발표&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;================================================&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1분기, 이직하고 3개월. 회사와 업무에 적응하는 시간이었다.&lt;br /&gt;다양한 서비스를 운영/개발하는 팀에 있다보니, 크게 3가지 부문에서 적응이 필요했다.&lt;br /&gt;근데 참 재밌게도(사실 재미없음) 1년이 지난 지금도 이 세 가지에 대해 계속해서 역량 강화가 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1 운영/유지보수 개발&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;달리는 기차의 바퀴 갈아끼우기&quot;&lt;/li&gt;
&lt;li&gt;1분기에 진행한 VoC 에서 예상하지 못했던 버그와 사이드 이펙트를 생성하면서, &quot;기존에 잘 돌아가고 있던 걸 망가뜨리는 행위&quot; 에 대해 민감하게 반응하게 되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;예상치 못한 사이드 이펙트가 발생했던 이유는?&lt;/i&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1) 도메인 이해도 부족: A 를 고쳤는데 (전혀 관계 없다고 생각한, 아니 정확히는 있는지도 몰랐던) C 에서 버그가 발생했다.&lt;/li&gt;
&lt;li&gt;2) 테스트 부족: 액션이 발생하는 케이스가 너무 많아서 모든 케이스를 테스트하지 못함. 대표 케이스만 테스트했으나, 디테일 옵션에서 버그가 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3분기에 진행한 파트에서는, &quot;기존에 잘 되던게 안되요&quot; 소리가 안나오도록 하고 싶었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1) 잘한 점: 테스트케이스를 자세히 작성 &amp;amp; 통합 테스트 진행 &amp;amp; 테스트 일정을 길게 가져감 (3주)&lt;/li&gt;
&lt;li&gt;2) 못한 점: 코드 수정에 소극적이었다. 리팩토링을 할 만한 부분이 있는 코드도, &quot;지금 잘 돌아가고 있는 걸 건드는 건 리스크&quot; 라고 판단해서 최소 수정으로 리팩토링함.&lt;/li&gt;
&lt;li&gt;-&amp;gt; 잘한 점: 버그가 2건 정도 있었는데, 다행히 보정 기능이 이미 구현되어 있는 케이스였고 이외에는 사용자에게 이슈 리포팅 받은 건이 없었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;-&amp;gt; 여전히 남아있는 문제: &lt;b&gt;어떻게 하면 테스트를 잘 할 수 있을까?&lt;/b&gt; 이게 테스트 코드/ 테스트 자동화로 해결할 수 있는 영역일까? 테스트 커버리지를 높이기 위한 &lt;b&gt;효율적인 방법&lt;/b&gt; 은 무엇일까?&lt;/li&gt;
&lt;li&gt;-&amp;gt; &lt;b&gt;&quot;달리는 기차의 바퀴 갈아끼우기&quot; 는 피할 수 없는 숙명: 과도한 스트레스는 나만 손해. 최선을 다하되, 필요 이상으로 예민하지 말자.&lt;/b&gt;&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;98년도에 퇴사한 박종갑과장님 찾아올 거 아니면 돌아가고 있는 코드는 건들지 않기 vs 그럼에도 불구하고 개발자라면 리팩토링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2 컨텍스트 스위칭&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 서비스가 있고, 여러 유관부서로부터 이슈가 들어온다.&lt;/li&gt;
&lt;li&gt;그 때마다 컨텍스트 스위칭이 일어나고, 스위칭을 위해 필요한 시간을 줄여 나가는 것이 필요했다.&lt;/li&gt;
&lt;li&gt;그게 정말정말 어려워서, A업무 하다가 B업무 확인해야하면 돌아오는데 시간이 걸리고, 또 중간에 회의 다녀오면 앞서 진행했던 컨텍스트가 휘발되고,....&lt;/li&gt;
&lt;li&gt;-&amp;gt; 여전히 남아있는 문제: 시간이 지나면 익숙해지겠지 싶었는데, &lt;i&gt;&lt;b&gt;이렇게 휘발되고 시간 날아가는 현상에만 익숙해졌고 전혀 해결을 못하고 있다&lt;/b&gt;. 조치가 필요한데 뭘 어떻게 해야할지 감도 안온다... 그저 리더님이 다 협의하고 정해주신 내용으로 개발만 하던 때가 그립다...&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3 유관부서 협업&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여전히 어려운 부분 중 하나인데, 전 직장에서는 혼자 (혹은 같은 팀 팀원) 하던 일을 모두 다른 부서/팀과 협업해야 한다.&lt;/li&gt;
&lt;li&gt;최근 들어 커뮤니케이션 역량이 더 필요해졌다. A팀 개발일정 - 우리팀 개발일정 - B팀 개발일정 을 &lt;b&gt;유기적으로 연결하여 확인&lt;/b&gt;해야 하는 일이 많아졌다. (그런데 모든 태스크가 이런 식인)&lt;/li&gt;
&lt;li&gt;여기서 어느 한 파트라도 일정이 변경되면, 우리 팀 일정에도 영향이 가기 때문에 정확한 커뮤니케이션이 필요해졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4 리딩이요? 제가요?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2025년 전보 발령이 났다. 파트 리딩을 해줬으면 한다는 본부장님의 청천벽력같은 말씀. 리딩이요? 제가요?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 전보 발령에 본부장님의 의견이 많이 들어갔고, 본부장님이 나를 픽해서 옮겼다는디....... 도대체 웨.. 저의 뭘 좋게 보셨다는건지? 그냥 써먹기 좋은 나사떼기 1 로 보였던걸까&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;내 일만 잘 하면 되었는데, 이제는 다른 팀원의 진행 상황을 파악하고, 이슈가 있으면 해결방안을 고민하고, &lt;b&gt;내 선에서 해결할 수 없는 일은 빠르게 에스컬레이션 해야 한다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;내 선에서 해결할 수 없는 일&quot; 의 기준이 나와 상사가 다를 때마다, 팀장님이 생각하는 기준이 어디인지 아직 판단하기가 어렵다. 전/현 팀장님의 성향이 꽤나 다른데, 지금의 나는 전 팀장님의 기준에 맞춰서 생각하고 있기 때문인 것 같다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;넵무새가 되지 말자&lt;/b&gt;: 나 혼자 일할 땐 [나: 이거 해주시겠어요? 나: 넵] 하면 되었는데, 이젠 내가 넵하면 팀원이 힘들어지거나 vs 팀장님에게 다시 에스컬레이션 되거나가 되었다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의사결정을 해야 하는 문제에는, 이것이 &lt;b&gt;&quot;누가&quot; &quot;어떤 문제를 해결하기 위해&quot; 나온 요구사항인지, 그래서 우리 팀이 &quot;왜&quot; 이 요청을 받아들여서 해야하는지&lt;/b&gt; 를 생각해보자. &quot;왜&quot; 가 합당하지 못하면 넵 하지 말자.&lt;/li&gt;
&lt;li&gt;넵 할 수 없는 일이면, 대안을 제시하자. &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;부탁이 아니라&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;&lt;span&gt; &lt;/span&gt;솔깃한 제안을 합시다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;회사와 팀에 적응하였다... 라고 생각할 때 쯤 전보 발령이 나서 좀 슬펐는데, 뭐 어쩌겠어 새롭게 다시 잘 해봐야지.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>이야기</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/132</guid>
      <comments>https://moonong.tistory.com/132#entry132comment</comments>
      <pubDate>Thu, 16 Jan 2025 23:15:29 +0900</pubDate>
    </item>
    <item>
      <title>2024년 회고</title>
      <link>https://moonong.tistory.com/131</link>
      <description>&lt;h1&gt;2024 목표설정 - 달성&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eyunZd/btsLQmpj7iL/LcBR8SBbNsOhtgLgG6Js6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eyunZd/btsLQmpj7iL/LcBR8SBbNsOhtgLgG6Js6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eyunZd/btsLQmpj7iL/LcBR8SBbNsOhtgLgG6Js6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeyunZd%2FbtsLQmpj7iL%2FLcBR8SBbNsOhtgLgG6Js6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1162&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해외(1/6)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해외라는 목표에 대해 조금 희미해져서 그런지, 목표했던 것을 하나도 하지 못했다.&lt;/li&gt;
&lt;li&gt;링크드인 꾸미기/네트워킹 정도는 주기적으로 했어야 하는데, 너무 일-집-일-집만 하느라 못한 부분도 있는듯.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;개발(3.5/8)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;개발 파트의 목표는 대부분이 &quot;회사에서 자리잡기&quot; 와 관련이 있었어서, 약간은 멱살잡고 끌려간 목표달성률이긴 하다 (ㅋㅋ)&lt;/li&gt;
&lt;li&gt;사내 발표하기는 내가 자원해서 한 거 였으니 요거 정도는 그래도 좋은 점수를 주고 싶다. 이렇게 많은 사람들 앞에서 발표한 건 처음인 것 같다. 엄청나게 떨릴 줄 알았는데 딱히 그렇지두 않더라 ㅋㅋ&amp;nbsp;&lt;/li&gt;
&lt;li&gt;사이드 프로젝트 릴리즈는 나 혼자만의 영역이 아니었기에 쩜오 점수를 주었다. 역시 일과 병행하는 것은 너무나 어렵다..&lt;/li&gt;
&lt;li&gt;테스트코드 작성은 도대체 언제쯤 할 수 있을까? 일정에 치여서 우선순위에서 밀려나는 일.. 더이상은... 애초에 개발 = 구현 + 테스트코드 라고 생각하고 해야하는데 말이얌.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;다정함(4/7)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;바쁘고 힘들고 정신없는 삶 속에서 다정함을 잃지 않는 것은 참 어렵다.&lt;/li&gt;
&lt;li&gt;이직을 하고 좋은 사람들과의 인연을 이어가고 싶어서, 먼저 연락하기 영역을 잘 지켜나가고 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;건강(3/5)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이건 정말 할말이 많은게........ 8월에 다리를 다쳐서 하반기를 그냥 통으로 날라갔다.&lt;/li&gt;
&lt;li&gt;건강이 무너지니까 삶이 무너지는 게 한순간이더라. 그러고나니까 그냥 당연하게 느낀 게 얼마나 소중한 건지 알겠고... 2025년 새해 목표는 뭐 거창하고 구체적인 계획도 없었다. 그저 건강하고 행복하자 한 마디면 되었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;재미(4/5)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;그런데 재미 파트 달성률이 80% 라는건 상반기에 끝내주게 잘 다녔다는 것이겟죠?&lt;/li&gt;
&lt;li&gt;2024 목표의 큰 키워드가 &quot;재미있는 삶&quot; 이었던 만큼, 재미 만큼은 아주 꽉 잡았다.&lt;/li&gt;
&lt;li&gt;스쿠버다이빙 하면서 영상을 찍기 시작했고, (다치기 전까지) 수영을 주 5회는 기본으로 다녔다. 야구도 틈틈히 보러 다니고, 최근엔 뜨개질을 시작했다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;정신건강(2/2)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이건 좀 평가항목이 애매하다.&lt;/li&gt;
&lt;li&gt;일하면서 짜증내거나 일희일비하는 것은 결국 일에 열정이 있었을 때 가능한 일인데, 2024년의 나는 그렇게 열정적으로 일하지 않았던 것 같아서. 항목을 채웠다고 그것이 성공이냐? 라고 한다면 내 대답은 글쎄&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;운(4/6)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;다정함과 이어지는 항목.&lt;/li&gt;
&lt;li&gt;나는 아주 마냥 밝은 사람이 되지는 못할 것이다. 근데 그래두 밝게 인사할 수는 있잖아! 텐션업업하면 나도 상대도 기분좋아질 때가 있으니까&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;도전(3/7)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;잘한 것: 유튜브를 시작했다!&lt;/li&gt;
&lt;li&gt;못한 것: 책을 정말정말 안읽었다. 단기성 도파민에 절여져서 요즘은 활자를 오래 읽는걸 진짜 못하겠다. 하루하루가 다르게 멍청해지는 이 기분...... 정말 조치가 필요해.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>이야기</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/131</guid>
      <comments>https://moonong.tistory.com/131#entry131comment</comments>
      <pubDate>Thu, 16 Jan 2025 21:44:44 +0900</pubDate>
    </item>
    <item>
      <title>2024년 3월 회고</title>
      <link>https://moonong.tistory.com/130</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;## 1. 진행하던 단기 TF 종료&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3월 말에 드디어 릴리즈를 했다 !&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴리즈 전후 2주는 정말 정신없이 보냈는데, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;릴리즈 하자마자 빵빵 터지는 이슈에&lt;span&gt; 꽤나 고된 하루하루였다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;뭔놈의 핫픽스를 하루걸러 하루로 내보내는지,, 운영 안해본 티가 팍팍 나죠&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런저런 시행착오를 겪으면서 나아가는 거겠지. 괜찮아 괜찮아!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(+) 수치화된 테스트 시나리오 작성과, 테스트 코드의 중요성을 다시 한 번 깨닫는 계기가 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트코드... 진짜찐쨔 짜야찌.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;## 2. 구글 엔지니어는 이렇게 일한다 완독&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 다 읽었다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간중간 내 상황과 안맞는 부분이나 내가 실질적으로 다루지 않는 부분들이 있어서 약간 집중도가 흐려질 때도 있었지만, 그래도 열심히 읽어서 드디어 1회독 완료!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자책에 북마크 해두고 하이라이트 쳐둬서 나중에 또 읽을 때에도 도움이 많이 될 것 같다. 이북파로 갈아타고 많은 장점을 누리는 중 *_*&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;## 3. 웹 개발자를 위한 HTTP 기본지식 수강 완료 (재)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 들었던 거긴 한데 회사에서 추천강의? 로 올라와서 복습하는 차원에서 한 번 더 들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;## 4. 생각정리는 어려워&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 생각정리에 대한 외부 강의를 수강했는데, '머리로는 알고 있지만 잘 실천하지 못하고 있는' 것들에 대한 개념을 이야기하고 실제 연습을 통해 체득하는 과정이 재미 있었다. 진짜 '체득' 하려면 시간이 많이 필요하겠지. 평소에도 5why 나 MECE 와 같은 기법을 적용하기 위해 의식적으로 노력해야겠다. 빙산의 일각만을 바라보지 말 것, 왜 라는 질문은 파고 들어갈수록 표면에 보여지는 이유가 아닌, 새로운 관점으로 볼 수 있게 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>이야기</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/130</guid>
      <comments>https://moonong.tistory.com/130#entry130comment</comments>
      <pubDate>Sun, 7 Apr 2024 23:01:40 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Spring] 비동기 처리 시 ThreadContext 를 관리하는 방법</title>
      <link>https://moonong.tistory.com/129</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션을 개발하다 보면 하나의 클라이언트 요청에 대해 &lt;code&gt;애플리케이션 전반에 걸친 특정 Context를 유지&lt;/code&gt;해야 할 필요성이 생긴다. 대표적으로 로깅 정보나 사용자 정보가 있다. 이 때 흔히 사용하는 방안은 &lt;code&gt;ThreadLocal을 활용&lt;/code&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 &lt;code&gt;Web Request&lt;/code&gt; 가 오게 되면 하나의 쓰레드를 할당해서 해당 작업을 처리하게 된다. 이때 &lt;code&gt;Thread&lt;/code&gt; 에 대한 정보를 &lt;code&gt;ThreadLocal&lt;/code&gt; 에 저장하게 되면 해당 작업이 끝날 때 까지 모든 상황에서 &lt;code&gt;context&lt;/code&gt; 를 유지하고 저장하고 찾아볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal은 쓰레드의 로컬 컨텍스트 변수로 Thread 가 존재하는 한 계속해서 남아 있는 변수이다. 작업 요청이 들어왔을때 하나의 쓰레드가 생성이 되고 작업이 끝나면 쓰레드가 없어진다고 할 때, 쓰레드가 살아있을 동안에 계속 유지되는 변수이다. 즉 쓰레드가 생성되어 있는 동안에, 쓰레드 안에서 계속 참고해서 쓸수 있는 쓰레드 범위의 글로벌 변수라고 볼 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 ThreadLocal은 이름 그대로 Thread별로 값을 유지하기 때문에 비동기 호출 시 해당 값이 유지되지 않는다. &lt;b&gt;비동기 처리를 위해 &lt;code&gt;TaskExecutor&lt;/code&gt; 를 활용하기 시작하면 해당 &lt;code&gt;executor&lt;/code&gt; 는 새로운 &lt;code&gt;Thread&lt;/code&gt; 를 생성한다.&lt;/b&gt; 즉, 기존 쓰레드의 &lt;code&gt;context&lt;/code&gt; 가 넘어가지 않고 새로운(빈) 쓰레드를 사용하게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 처리 시 기존 쓰레드의 값을 유지하려면 어떻게 해야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Spring 4.3&lt;/code&gt; 이상부터 제공되는 &lt;code&gt;TaskDecorator&lt;/code&gt; 를 이용해서 비동기처리하는 &lt;code&gt;taskExecutor&lt;/code&gt; 생성 시 커스터마이징이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UserIdHolder {
    private final static ThreadLocal&amp;lt;Long&amp;gt; USER_ID_HOLDER = new ThreadLocal&amp;lt;&amp;gt;();

    public static Long getUserId() {
        return USER_ID_HOLDER.get();
    }

    public static void setUserId(Long userId) {
        USER_ID_HOLDER.set(userId);
    }

    public static void clear() {
        USER_ID_HOLDER.remove();
    }
}
---
public class ClonedTaskDecorator implements TaskDecorator {
    @Override 
    public Runnable decorate(Runnable task) { 
        long userId = UserIdHolder.getUserId(); // 기존 thread에서 userId를 가져온다.
        return () -&amp;gt; {
            try {
                UserIdHolder.setUserId(userId); // 새로운 thread에서 가져온 userId를 세팅한다.
                runnable.run();
            } finally {
                UserIdHolder.clear(); // 새로운 thread 작업이 완료되면 ThreadLocal 값을 초기화한다.
            }
        };
    } 
}
---
@Bean(name = &quot;test&quot;)  
public Executor testThreadPool() {  
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
    executor.setCorePoolSize(3);  
    executor.setMaxPoolSize(5);  
    executor.setQueueCapacity(5000);  
    executor.setThreadNamePrefix(&quot;default-thread-&quot;); 
    executor.setTaskDecorator(new ClonedTaskDecorator()); 
    executor.initialize();  
    return executor;  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.gangnamunni.com/post/mdc-context-task-decorator/&quot;&gt;Spring 의 동기, 비동기, 배치 처리시 항상 context 를 유지하고 로깅하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bcho.tistory.com/1316&quot;&gt;https://bcho.tistory.com/1316&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Spring</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/129</guid>
      <comments>https://moonong.tistory.com/129#entry129comment</comments>
      <pubDate>Sun, 7 Apr 2024 22:46:31 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Spring] Event publish &amp;amp; Listener</title>
      <link>https://moonong.tistory.com/128</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스 로직 간의 강결합이 주는 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 개발하면서, 한 번의 요청에 의해 2가지 이상의 기능을 수행하는 경우가 빈번하게 발생한다. 혹은, 간단하게 시작된 도메인 로직이 서비스가 확장됨에 따라 여러가지 추가 로직이 늘어나는 경우 역시 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황에서, 다수의 기능을 하나의 메서드에서 코드로 구현하면 기능 간 강결합이 생기게 된다. 강한 결합이 생기면 생길수록 로직을 분리해서 관리하기 어렵고, 특정 기능에 문제가 발생하였을 때 이를 처리하는 로직 역시 섞이게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 회원 가입 요청이 발생하였을 때, 아래와 같이 프로세스가 진행된다고 가정해보자. (로직에서 사용한 메서드는 별도 구현이 되어있음을 전제한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;회원 등록&lt;/li&gt;
&lt;li&gt;가입 축하 메일 발송&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberService { 
    private  final MemberRepository memberRepository;
    private final MailService mailService;

    @Transactional
    public MemberModel save(MemberRegisterModel model){
        // 1. 회원 등록
        Member member = saveMember(model);

        // 2. 가입 축하 메일 발송 (발송 이력 DB 저장)
        mailService.sendMail(member.id, member.email);

        // 3. 어떠한 사유로 인해 exception 발생
        if (&quot;admin&quot;.equals(model.getName())) {
            throw new RuntimeException(&quot;can not use this name.&quot;);
        }

        return new MemberModel(member); 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에는 몇 가지 개선점이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 기능 간의 강한 의존성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 등록과 메일 발송은 각각 MemberService, MailService 로 분리되어 있는 것 처럼 보이지만, 사실은 MemberService 의 메서드 내에서 MailService 를 직접 호출하는 방식이다. 만약, 서비스가 성장함에 따라 MailService 를 별도 모듈/시스템으로 분리해야 할 때, MemberService 의 코드도 함께 수정해야 하는 상황이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 트랜잭션 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서는 &lt;code&gt;saveMember()&lt;/code&gt; 과 &lt;code&gt;sendMail()&lt;/code&gt; 이 단일 트랜잭션으로 묶여 동작한다. 만일 회원 등록은 성공하였으나 메일 전송 시 오류가 발생한다면, 트랜잭션 매니저에 의해 &lt;code&gt;save()&lt;/code&gt; 메서드 전체가 롤백된다. 주요 기능 단위로 트랜잭션을 분리할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 동기 방식의 성능 이슈&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 및 프로세스에 따라 다르겠지만, 회원 가입과 이메일 발송은 굳이 동기 방식으로 동작할 필요는 없는 로직이다. 특히, 메일 발송 서비스는 외부 기능을 이용하는 경우가 대다수이며, 애플리케이션 내부에서 동작하는 기능과 비교하였을 때 소요 시간 역시 오래 걸린다.&lt;br /&gt;회원 가입 성공에 200ms가 소요되었는데, 이메일 발송 로직이 3000ms 가 걸리는 기능이라고 가정할 때, 유저는 응답을 받기 위해 3200ms(+a) 를 기다려야 하는 상황이 발생한다. 이를 비동기 방식으로 처리한다면 성능적인 이점을 가져갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. DB 결과와 무관하게 발송되는 이메일&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 발송이 완료되고, &lt;code&gt;save()&lt;/code&gt; 메서드의 마지막까지 예외가 없을 때 commit 이 일어난다. 위의 코드에서 만일 member 엔티티를 model 로 변환하는 과정에서 에러가 발생하였다면, 가입 축하 이메일을 받았지만 실제 회원은 생성되지 않은 상황이 발생하게 된다. 때문에 이메일 발송은 commit 이 일어난 뒤에 실행 되어야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이벤트 방식을 통해 강결합 제거하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하고, 기능 간의 느슨한 결합을 위해 &lt;b&gt;이벤트 방식&lt;/b&gt; 을 고려할 수 있다. 이벤트 발행을 통해 독립적인 도메인의 비즈니스 로직을 호출하여 서비스 로직 간 강결합을 끊어낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 제공하는 &lt;code&gt;@EventListner&lt;/code&gt;를 통해 이벤트를 구현해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 4.2 이전에는 반드시 이벤트 클래스가 &lt;code&gt;ApplicationEvent&lt;/code&gt; 클래스를 상속받아야 했다. 하지만 4.2부터는 해당 클래스를 상속받지 않고도 이벤트를 발행 및 구독할 수 있도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 이벤트 발행은 &lt;code&gt;ApplicationEventPublisher&lt;/code&gt; 인터페이스가 담당한다. &lt;code&gt;publishEvent()&lt;/code&gt; 메서드는 &lt;code&gt;AbstractApplicationContext&lt;/code&gt; 추상 클래스에 구현되어, 최종적으로 애플리케이션 컨텍스트(&lt;code&gt;ApplicationContext&lt;/code&gt;) 에서 관리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 이벤트 발행은 ApplicationEventPublisher 인터페이스가 담당하는데, 이를 구현하는 것은 결국 애플리케이션 컨텍스트(ApplicationContext)이다. 애플리케이션 컨텍스트는 빈 탐색과 등록, 리소스 처리 등 많은 책임을 제공하고 있는데, 인터페이스 분리 원칙(ISP)에 맞게 이벤트 발행 책임만 처리하는 것이 바로 ApplicationEventPublisher 인터페이스이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ApplicationEventPublisher&lt;/code&gt; 인터페이스와 &lt;code&gt;AbstractApplicationContext&lt;/code&gt; 추상 클래스는 각각 아래와 같다.&lt;br /&gt;AbstractApplicationContext 는 &lt;code&gt;publishEvent()&lt;/code&gt; 메서드 내부에서 &lt;code&gt;ApplicationEventMulticaster&lt;/code&gt; 를 거쳐 &lt;code&gt;multicastEvent()&lt;/code&gt; 메서드를 실행한다. multicastEvent() 에서 &lt;code&gt;invokeListener()&lt;/code&gt;를 통해 이벤트 리스너 실행(구독)을 일으킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 구독은 &lt;code&gt;ApplicationListener&lt;/code&gt; 인터페이스를 구현하거나 &lt;code&gt;@EventListener&lt;/code&gt;를 사용하면 된다.&lt;br /&gt;스프링 4.2 이전에는 이벤트를 구독하려면 반드시 ApplicationListener 인터페이스를 구현해주어야 했다. 하지만 ApplicationListener 인터페이스를 직접 구현하는 방식은 상당히 번거롭다. 스프링 4.2부터 &lt;code&gt;@EventListener&lt;/code&gt; 어노테이션을 제공하여, 편리하게 이벤트를 구독할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;@FunctionalInterface  
public interface ApplicationEventPublisher {  
    default void publishEvent(ApplicationEvent event) {  
        this.publishEvent((Object)event);  
    }  

    void publishEvent(Object event);  
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
    @Nullable  
    private ApplicationEventMulticaster applicationEventMulticaster;
    ...

    protected void publishEvent(Object event, @Nullable ResolvableType eventType) {  
        Assert.notNull(event, &quot;Event must not be null&quot;);  
        Object applicationEvent;  
        if (event instanceof ApplicationEvent) {  
            applicationEvent = (ApplicationEvent)event;  
        } else {  
            applicationEvent = new PayloadApplicationEvent(this, event);  
            if (eventType == null) {  
                eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();  
            }  
        }  

        if (this.earlyApplicationEvents != null) {  
            this.earlyApplicationEvents.add(applicationEvent);  
        } else {  
            this.getApplicationEventMulticaster().multicastEvent((ApplicationEvent)applicationEvent, eventType);  
        }  

        if (this.parent != null) {  
            if (this.parent instanceof AbstractApplicationContext) {  
                ((AbstractApplicationContext)this.parent).publishEvent(event, eventType);  
            } else {  
                this.parent.publishEvent(event);  
            }  
        }  

    }
    ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {  
        if (this.applicationEventMulticaster == null) {  
            throw new IllegalStateException(&quot;ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: &quot; + this);  
        } else {  
            return this.applicationEventMulticaster;  
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {  
        ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);  
        Executor executor = this.getTaskExecutor();  
        Iterator var5 = this.getApplicationListeners(event, type).iterator();  

        while(var5.hasNext()) {  
            ApplicationListener&amp;lt;?&amp;gt; listener = (ApplicationListener)var5.next();  
            if (executor != null) {  
                executor.execute(() -&amp;gt; {  
                    this.invokeListener(listener, event);  
                });  
            } else {  
                this.invokeListener(listener, event);  
            }  
        }  

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 방식 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 코드를 이벤트 방식을 이용하여 개선해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ApplicationEventPublisher.publishEvent()&lt;/code&gt; 를 이용해 이벤트를 발행하고, 이벤트를 구독할 수 있도록 &lt;code&gt;SaveMemberEventListener&lt;/code&gt; 클래스를 생성하였다. &lt;code&gt;@EventListener&lt;/code&gt; 어노테이션을 통해 이벤트를 구독하여 &lt;code&gt;sendMail()&lt;/code&gt; 를 수행할 수 있도록 하였다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberService { 
private final MemberRepository memberRepository;
private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public void save(MemberRegisterModel model) {
         // 1. 회원 등록
        Member member = saveMember(model);

        // 2. 가입 축하 메일 발송 이벤트 발행
        eventPublisher.publishEvent(new SavedMemberEvent(model)); 

        // 3. 어떠한 사유로 인해 exception 발생
        if (&quot;admin&quot;.equals(model.getName())) {
            throw new RuntimeException(&quot;can not use this name.&quot;);
        }
    }
}
---
public class SavedMemberEvent {
    private Long id; 
    private String email; 
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class SaveMemberEventListener {
    private final MailService mailService; 

    @EventListener
    public void sendMail(SavedMemberEvent event){
        // 메일 발송 이벤트 구독
        mailService.sendMail(event.id, event.email);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@EventListener와 @TransactionEventlistener&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EventListener와 @TransactionEventlistener의 차이는 실행시점 차이이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;3. 어떠한 사유로 인해 exception 발생을 했을 경우&lt;/code&gt;, 2. 회원가입 축하 메일 전송 이벤트가&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@EventListener&lt;/code&gt; 로 지정되었을 경우, 호출 즉시 실행되기 때문에 Rollback이 불가능하다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@TransactionEventlistener&lt;/code&gt;로 지정되었을 경우, 이벤트를 잠시 들고있다가 &lt;code&gt;save()&lt;/code&gt; 메서드의 트랜잭션이 커밋된 후 실행된다. (이벤트 실행시점 지정 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에, &lt;b&gt;하나의 트랜잭션 경계 안에서&lt;/b&gt; 트랜잭션에 관련된 이벤트 처리를 하고 싶다면&lt;code&gt;@TransactionalEventListener&lt;/code&gt;를 사용하여 처리할 수 있다. 트랜잭션 간 이벤트 실행 시점은 기본적으로 commit 후에 일어나나, &lt;code&gt;phase&lt;/code&gt; 옵션을 통해 실행 시점을 지정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;AFTER_COMMIT&lt;/code&gt; - 트랜잭션 결과를 반영(commit)하고 난 시점에 이벤트 실행 (default)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AFTER_ROLLBACK&lt;/code&gt; &amp;ndash; 트랜잭션에 rollback이 일어난 시점에 이벤트 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AFTER_COMPLETION&lt;/code&gt; &amp;ndash; 트랜잭션이 끝났을 때(commit이든 rollback이든) 이벤트 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BEFORE_COMMIT&lt;/code&gt; - 트랜잭션이 커밋되기 전에 이벤트 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기 방식으로 @EventListener 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@EventListener&lt;/code&gt; 는 동기적으로 실행되어, 기본적으로 하나의 쓰레드를 공유한다. 비동기로 실행하고자 한다면 &lt;code&gt;@Async&lt;/code&gt; 를 함께 사용하여 별도의 쓰레드에서 동작하도록 하여야 한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class SaveMemberEventListener {
    private final MailService mailService; 

    @Async  
    @EventListener
    public void sendMail(SavedMemberEvent event){
        // 메일 발송 이벤트 구독
        mailService.sendMail(event.id, event.email);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 예외처리에 관한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기로 실행될 경우 새로운 쓰레드를 생성하여 로직이 수행되기 때문에, DispatcherServlet 에서 수행된 로직이 아니기에 &lt;code&gt;@ControllerAdvice&lt;/code&gt; 를 이용한 전역 에러 핸들링에 잡히지 않게 된다. 이를 해결하기 위해&lt;code&gt;AsyncUncaughtExceptionHandler&lt;/code&gt; 를 상속받아 비동기 쓰레드에서 발생하는 예외를 전역적으로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
executor.setCorePoolSize(30);  
executor.setMaxPoolSize(100);  
executor.setQueueCapacity(5000);  
executor.setThreadNamePrefix(&quot;def-async-&quot;);  
executor.initialize();  
return executor;
    }

    @Override 
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { 
        return new AsyncExceptionHandler(); 
    }
}
---
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(AsyncExceptionHandler.class);

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        logger.warn(&quot;비동기 처리중 예외가 발생했습니다\n&quot; +
                &quot;예외 메세지 : &quot; + ex.getMessage() + &quot;\n&quot; +
                &quot;메소드 : &quot; + method.getName() + &quot;\n&quot; +
                &quot;파라미터 : &quot; + Arrays.toString(params));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;References.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@eastperson/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-%EB%A1%9C%EC%A7%81%EA%B0%84-%EA%B0%95%EA%B2%B0%ED%95%A9%EC%9D%84-%ED%95%B4%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95&quot;&gt;스프링 이벤트를 활용해 로직간 강결합을 해결하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://findstar.pe.kr/2022/09/17/points-to-consider-when-using-the-Spring-Events-feature/&quot;&gt;스프링 이벤트 기능을 사용할 때의 고려할 점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/292&quot;&gt;https://mangkyu.tistory.com/292&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sungsan.oopy.io/8feee388-7705-4c89-9785-ed428088b97c&quot;&gt;https://sungsan.oopy.io/8feee388-7705-4c89-9785-ed428088b97c&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Spring</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/128</guid>
      <comments>https://moonong.tistory.com/128#entry128comment</comments>
      <pubDate>Sat, 9 Mar 2024 11:03:07 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 비동기 작업에 사용되는 CompletableFuture</title>
      <link>https://moonong.tistory.com/127</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 는 기존의 &lt;code&gt;Future&lt;/code&gt; 인터페이스의 한계를 보완하기 위해 등장한 클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Future&lt;/code&gt; 인터페이스는 비동기 작업에 대한 결과값을 리턴받기 위해 사용되었으나, 아래와 같은 한계점이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 완료시킬 수 없고, &lt;code&gt;get(long timeout, TimeUnit unit)&lt;/code&gt; 의 timeout 설정으로만 완료 가능&lt;/li&gt;
&lt;li&gt;블로킹 코드(&lt;code&gt;get()&lt;/code&gt;) 을 통해서만 이후 결과를 처리할 수 있음&lt;/li&gt;
&lt;li&gt;여러 &lt;code&gt;Future&lt;/code&gt; 를 조합하거나, 예외 처리 할 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 는 이름 그대로 외부에서 '완료 가능한' Future 이다. 또한, &lt;code&gt;CompletionStage&lt;/code&gt; 인터페이스를 구현하여 작업의 중첩, 완료 후 콜백(Future 간 조합)을 가능하게 하였다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class CompletableFuture&amp;lt;T&amp;gt; implements Future&amp;lt;T&amp;gt;, CompletionStage&amp;lt;T&amp;gt; {
...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture가 갖는 작업의 종류는 크게 다음과 같이 구분할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 작업 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;runAsync: 반환값이 없는 경우&lt;/li&gt;
&lt;li&gt;supplyAsync: 반환값이 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;작업 콜백
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;thenApply
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반환 값을 받아서 다른 값을 반환함&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 Function을 파라미터로 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;thenAccpet
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반환 값을 받아 처리하고 값을 반환하지 않음&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 Consumer를 파라미터로 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;thenRun
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반환 값을 받지 않고 다른 작업을 실행함&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 Runnable을 파라미터로 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;작업 조합
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;thenCompose
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 작업이 이어서 실행하도록 조합하며, 앞선 작업의 결과를 받아서 사용할 수 있음&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 Function을 파라미터로 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;thenCombine
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 작업을 독립적으로 실행하고, 둘 다 완료되었을 때 콜백을 실행함&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 Function을 파라미터로 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;allOf
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 작업들을 동시에 실행하고, 모든 작업 결과에 콜백을 실행함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;anyOf
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 작업들 중에서 가장 빨리 끝난 하나의 결과에 콜백을 실행함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예외 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;exeptionally
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;발생한 에러를 받아서 예외를 처리함&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 Function을 파라미터로 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;handle, handleAsync
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(결과값, 에러)를 반환받아 에러가 발생한 경우와 아닌 경우 모두를 처리할 수 있음&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 BiFunction을 파라미터로 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;CompletableFuture  
        .runAsync(() -&amp;gt; doSomething(), taskExecutor)  
        .thenRun(() -&amp;gt; doSomething2())  
        .exceptionally(e -&amp;gt; { handleError(e);  
            return null;  
            });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/263&quot;&gt;[Java] CompletableFuture에 대한 이해 및 사용법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Java</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/127</guid>
      <comments>https://moonong.tistory.com/127#entry127comment</comments>
      <pubDate>Mon, 4 Mar 2024 23:04:31 +0900</pubDate>
    </item>
    <item>
      <title>2024년 2월 회고: 적응의 시간</title>
      <link>https://moonong.tistory.com/126</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;### 1. 새 회사 적응기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직하고 적응하기 어려운 것이 여러가지가 있지만, 가장 큰 건 회사 규모가 커지면서 나의 역할은 작아졌다는 점이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 스타트업은 한 사람이 여러 업무를 하기 마련인지라, 개발자가 기획자이기도 개발자이기도 데브옵스 엔지니어이기도 QA 이기도 DBA 이기도 했는데, 지금은 모든 역할이 쪼개져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이건 장점이자 단점이다. 전 직장에서 많이 했던 말&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(댁아리를 쥐어싸매며) 제발 기획자 좀 뽑아줘,,,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(셀프 머지와 동시에) 배포하겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(대충 TF 팀방에) 잠깐 DB 작업 좀 하겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(6시 이후) 김개발자 퇴근합니다 김QA 출근했습니다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식이었고 역할이 나눠지지 않고 한 사람이 알아서 다 해야 한다는게 스트레스 였는데, 지금은 다 분업되어 있으니 모든 게 협업의 대상이다. 이전에는 나 혼자 고민하고 나혼자 결정하고 이렇게 매일 울고 웃고 하면 됐는데 이젠 혼자서 할 수 있는 일의 포션 자체가 많이 작아진 것.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 명확함을 내가 원했던 것이긴 해서 불만이라기 보다는 내가 적응해야 할 과제라고 생각한다. 다만 그래도 자전거의 바퀴(너무 과대평가인가) 정도는 되었던 것 같은데 지금은 자동차의 부품 정도의 역할만 가지게 된 것 같아 자기 주도적으로 일하는 성향을 가진 나로썬 조금 아쉽기는 함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 더, 컨텍스트 스위칭 능력이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 일단 스스로 멀티가 잘 안되는 스타일이라고 생각하고 있어서, 이 능력이 더욱 시급하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 직장에서는 하나의 프로젝트에 집중하면 되었었는데, 지금은 운영하는 서비스가 여러가지이다 보니 태스크에 따라 내용이 휙휙 바뀐다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날엔 리더님이 이슈사항 관련해서 툭 치면 바로 튀어나왔는데 지금은 X 가 뭐고 Y 가 뭔지도 헷갈리는 지경이니.. ㅎ_ㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 입사 3개월도 안됐는데 이걸 지금 당장 전 직장에서 했던 레벨만큼 나오는 건 당연히 불가능한거고, 앞서 재직중인 팀원들만큼 하는 것도 당연히 시간이 필요한 일인데 나 스스로 지금 이 &quot;못하는 구간&quot; 이 너무나 크게 보이는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명한 건 지금 고민하는 것들은 모두 시간이 필요한 일이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 당연한데 때때로 너무 고민하고 걱정하는 나를 보면 '실수하면 안된다' 라는 생각에 뒤덮여 있는 것 같다. (제대로 된 운영을 해본적이 없어서 그런듯. 운영하고 있는 서비스가 내 실수로 인해 장애가 발생하면? 이라는 불안감)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조급해 하지 말고 나의 속도로 가자. 다른 달팽이들은 신경 쓰지 말고.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;### 2. 수영을 시작했다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;치열한 수켓팅을 뚫고 입문반 자리를 차지했다!!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짤막하게나마 수영일기를 쓰면서 강습 때 피드백 받은 내용을 적고, 자유수영 때 그 부분을 의식하면서 몸을 움직이려고 노력하고 있다. 코어에 힘을 주면서 발차기를 하는 건 너무 어려워!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다이빙하는 개발자에 이어 수영하는 개발자 타이틀을 겟챠하겠어요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;### 3. 요즘 읽는 책&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 엔지니어는 이렇게 일한다 를 계속 읽고 있다. 이제 한 절반 정도..!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3월 안에 다 읽는 것이 목표.&lt;/p&gt;</description>
      <category>이야기</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/126</guid>
      <comments>https://moonong.tistory.com/126#entry126comment</comments>
      <pubDate>Sat, 2 Mar 2024 20:33:57 +0900</pubDate>
    </item>
    <item>
      <title>2024년 1월 월간 회고: 배울 것이 너무 많아</title>
      <link>https://moonong.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 근황&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직을 하고 한 달이 지났다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배울 것이 정말 많구나 느낀 한 달이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA가 가장 큰 산이 될 거라고 생각했던 내 예상과 달리, JPA 와 Querydsl 은 모르는 개념만 잡고 가면 생각보다 어렵지 않을 것이라는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그거보다 더 큰 산은 Back to the Basic.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 안다고 생각했던 것이, 그 깊이가 고작 요정도 였구나를 깨닫게 되었다. 스프링 기능을 사용하다보면 '스프링이 알아서 해주는' 부분에 대해 알고 넘어가야 한다고 생각해서 코어를 파보긴 했었는데, 스프링은 넓고 내 지식은 좁았다. 아예 몰랐던 기능도 튀어나왔고, 비동기 처리는 전 직장에서는 쓸 일이 거의 없었어서 개념만 알고 있는 수준이었다. 기초부터 탄탄히의 중요성을 또 한 번 깨닫는 계기가 되어 공부해야 하는 키워드 나올 때마다 스프링 파보기 진행 중.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 내가 DDD 라고 해왔던 것은 진짜 수박 겉핥기에 불과했구나 라는 것도 절절히 느꼈다. 첫 PR 을 올리고 코드리뷰를 받으면서 간단한 코드 한 줄이라도 퀄리티를 신경 써야겠다는 생각이 들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 직장에서와 스타일이 다른 부분도 새롭게 적응해나가고 있다. 업무적으로도, 업무 외적으로도!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 요즘 읽는 책&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글&amp;nbsp;엔지니어는&amp;nbsp;이렇게&amp;nbsp;일한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용의자 X의 헌신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두 권을 읽고 있다. 구글.. 은 너무나 유명한 책인데다가 DDD 에 대하여 설명이 잘 되어 있다고 하여 정독할 예정.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>이야기</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/125</guid>
      <comments>https://moonong.tistory.com/125#entry125comment</comments>
      <pubDate>Sun, 28 Jan 2024 22:29:49 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Querydsl @QueryProjection, transform() 을 활용하여 효율적으로 Result Handling 하기</title>
      <link>https://moonong.tistory.com/124</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 개발하다 보면, 하나의 테이블에서 조회하는 단순 조회 쿼리 뿐 아니라, 다수의 테이블을 조인하여 값을 조회하는 경우가 매우 빈번하게 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Querydsl은 결과를 사용자 정의하는 두 가지 방법, 즉 행 기반 변환을 위한 &lt;code&gt;FactoryExpressions&lt;/code&gt;와 집계를 위한 &lt;code&gt;ResultTransformer&lt;/code&gt;를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Querydsl의 &lt;code&gt;FactoryExpression&lt;/code&gt; 구현 기능은 &lt;code&gt;com.mysema.query.types.Projections&lt;/code&gt;클래스를 통해 , &lt;code&gt;ResultTransformer&lt;/code&gt; 는 &lt;code&gt;com.mysema.query.group.GroupBy&lt;/code&gt;를 통해 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Projection&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Projection&lt;/code&gt; 이란, Querydsl을 이용해 &lt;b&gt;entity전체를 가져오는 것이 아니라 조회 대상을 지정해 원하는 값만 조회하는 것&lt;/b&gt;을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 기본값 - Tuple&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Querydsl 에서 다양한 컬럼 값을 가져올 때에, &lt;code&gt;Tuple&lt;/code&gt; 을 사용한다.&lt;br /&gt;튜플은  Querydsl 의 객체이기 때문에, Controller, Service layer 로 이동하기에는 적절하지 않을 뿐더러, &lt;code&gt;tuple.get()&lt;/code&gt; 을 통해 하나씩 직접 값을 꺼내와야 하는 번거로움이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;List&amp;lt;Tuple&amp;gt; result = query.from(employee).list(employee.firstName, employee.lastName);
for (Tuple row : result) {
     System.out.println(&quot;firstName &quot; + row.get(employee.firstName));
     System.out.println(&quot;lastName &quot; + row.get(employee.lastName)); 
}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 개선하기 위해 DTO(Model) 을 사용하는 방법을  채택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. Bean population&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Projections.bean()&lt;/code&gt; 을 이용하면, DTO 객체의 각 필드에 setter 메서드를 통해 Projection 객체를 생성한다.&lt;br /&gt;단순 조회용 DTO 클래스라면 괜찮을 수도 있으나, setter 메서드를 열어두는 것은 데이터의 변경이 어디에서 일어나는지 파악하기 어려워 사용을 지양하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;UserDTO&amp;gt; dtos = query.list(
    Projections.bean(UserDTO.class, user.firstName, user.lastName));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 두 번째 방법은 &lt;code&gt;Projections.fields()&lt;/code&gt; 를 통해 getter/setter 메서드를 사용하지 않고 필드에 직접 값을 주입해주는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;UserDTO&amp;gt; dtos = query.list(
    Projections.fields(UserDTO.class, user.firstName, user.lastName));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. Constructor usage&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Projections.constructor&lt;/code&gt;는 생산자 기반 바인딩이다. 생성자 기반 바인딩이기 때문에 객체의 불변성을 가져갈 수 있다는 장점이 있지, 필드의 개수가 많아지는 경우 실수가 발생하여 오류가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;UserDTO&amp;gt; dtos = query.list(
    Projections.bean(UserDTO.class, user.firstName, user.lastName));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 세 가지 방식 모두, 컴파일 시점에서는 오류를 발견하지 못하고, 런타임 시점에서 해당 메서드를 실행했을 때 오류가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 직관적으로, 간편하게 객체를 바인딩하고 코드를 관리할 수는 없을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-4. @QueryProjection&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO의 생성자에 &lt;code&gt;@QueryProjection&lt;/code&gt; 어노테이션을 추가하여 Q클래스를 생성하도록 한다. 이 방식은 생성자&lt;code&gt;new QDTO()&lt;/code&gt;로 사용하기 때문에 불변 객체임을 보장하고, 런타임 에러뿐만 아니라 컴파일 시점에서도 에러를 잡아주는 장점이 있다. 또한, DTO는 Controller, Service layer 에서 모두 사용되기 때문에 3-layer 아키텍쳐 관점에서도 보다 적절하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class CustomerDTO {
  private Long id; 
  private String name; 

  @QueryProjection
  public CustomerDTO(Long id, String name){
     this.id = id; 
     this.name = name; 
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;QCustomer customer = QCustomer.customer;
List&amp;lt;CustomerDTO&amp;gt; dtos = query
                        .from(customer)
                        .list(new QCustomerDTO(customer.id, customer.name));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. GroupBy&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Result aggregation - transform&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 &lt;code&gt;com.mysema.query.group.GroupBy&lt;/code&gt; 는 쿼리 결과를 메모리에 집계하는 데 사용할 수 있는 집계 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 에서의 &lt;code&gt;GROUP BY&lt;/code&gt; 절과는 달리, &lt;code&gt;groupBy()&lt;/code&gt; 는 &lt;b&gt;aggregation에 필요한 데이터를 select절에 추가&lt;/b&gt;하는 역할과 &lt;b&gt;select한 결과 값을 aggregation 하는 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음과 같이 member 테이블과 team 테이블이 존재하고, team 테이블에 country 라는 속성이 있다고 가정한다. 그리고 나라별로 member 데이터를 모으고 싶을때, transform() 을 사용하지 않는다면 아래와 같이 쿼리한 후, Java Stream API 의 &lt;code&gt;groupingBy()&lt;/code&gt; 를 이용하여 그룹핑해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;@Override public Collection&amp;lt;MemberByCountryProjection&amp;gt; findAllWithCountry() { 
    QMemberByCountryProjection selectExpression = new QMemberByCountryProjection(member, team); 
    return query.from(member)
        .select(selectExpression)
        .leftJoin(team)
        .on(member.teamId.eq(team.id))
        .fetch(); 
    }
---
public Map&amp;lt;String, List&amp;lt;MemberEntity&amp;gt;&amp;gt; groupingByCountry (){
    Map&amp;lt;String, List&amp;lt;MemberEntity&amp;gt;&amp;gt; actual = memberRepository
    .findAllWithCountry()
    .stream()
    .collect(Collectors.groupingBy(MemberByCountryProjection::getCountry, 
                                HashMap::new, 
                        Collectors.mapping(MemberByCountryProjection::getMember, Collectors.toList())));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;transform()&lt;/code&gt; 을 활용하면 번거로움을 줄이고, 그룹핑하여 한번에 쿼리 결과를 뽑아낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Override 
public Map&amp;lt;String, List&amp;lt;MemberEntity&amp;gt;&amp;gt; groupByCountry() { 
    return query.from(member)
        .leftJoin(team)
        .on(member.teamId.eq(team.id))
        .transform(GroupBy.groupBy(team.country)
                    .as(list(member))); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://querydsl.com/static/querydsl/3.2.0/reference/html/ch03s02.html&quot;&gt;http://querydsl.com/static/querydsl/3.2.0/reference/html/ch03s02.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wildeveloperetrain.tistory.com/94&quot;&gt;https://wildeveloperetrain.tistory.com/94&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Java</category>
      <author>moonong</author>
      <guid isPermaLink="true">https://moonong.tistory.com/124</guid>
      <comments>https://moonong.tistory.com/124#entry124comment</comments>
      <pubDate>Sun, 28 Jan 2024 22:11:35 +0900</pubDate>
    </item>
  </channel>
</rss>