안녕하세요? 2차 인증(Two-Factor Authentication)을 위해 OTP에 대해 공부하던 중 알게된 사실들에 대해 정리해보려고 합니다.
OTP(One-Time Password, 일회용 비밀번호)의 동작 원리는 사용자가 인증을 시도할 때마다 한 번만 사용할 수 있는 비밀번호를 생성하고 검증하는 것을 의미 합니다. OTP는 보안성을 높이기 위해 고안된 방식으로, 주요하게 두 가지 방식이 있습니다: 시간 기반 OTP(Time-based OTP, TOTP)와 카운터 기반 OTP(HMAC-based OTP, HOTP). 여기서 TOTP가 더 일반적으로 사용됩니다. 오늘 알아볼 내용도 TOTP에 대해 알아봅니다 :)
제가 알아본 방법은 commons-codec 방식과 warrenstrange/googleauth 라이브러리 방식이 있는데요. 이 둘중에 무엇을 활용할지는 프로젝트 요구사항과 선호하는 개발 스타일에 따라 달라질 수 있습니다. 두 가지 접근 방식의 장단점은 아래와 같습니다.
Commons-Codec 방식
장점:
경량성: commons-codec는 일반적인 인코딩/디코딩 작업을 위한 라이브러리로, OTP 생성에 필요한 HMAC-SHA 알고리즘을 구현하는 데 사용할 수 있습니다. 이 라이브러리는 상대적으로 가볍습니다.
유연성: 라이브러리를 사용하여 직접 OTP 알고리즘을 구현하면, 특정 요구사항에 맞게 커스터마이징할 수 있습니다. HMAC-SHA1, SHA256, SHA512 등의 다양한 해시 알고리즘을 사용할 수 있습니다.
통제: OTP 생성 로직을 직접 구현하기 때문에 세부 사항을 커스텀 할 수 있습니다.
단점:
개발 복잡성: OTP 알고리즘을 직접 구현하려면 추가적인 코딩 작업이 필요하며, 표준 준수 및 보안성을 확보하기 위해 신중한 구현이 필요합니다.
버그 발생 가능성: 직접 구현할 경우 실수로 인한 버그가 발생할 가능성이 있습니다.
Warrenstrange/Googleauth
장점:
간편함: warrenstrange/googleauth는 구글 OTP 알고리즘(TOTP) 구현을 위한 라이브러리로, 설정과 사용이 간편합니다. 복잡한 구현 없이 쉽게 OTP 기능을 추가할 수 있습니다.
신뢰성: 이미 검증된 라이브러리를 사용함으로써 보안성과 신뢰성을 확보할 수 있습니다. 널리 사용되는 라이브러리이기 때문에 커뮤니티와 문서를 쉽게 찾을 수 있다는 장점이 있어요.
통합: 구글 OTP 표준을 준수하므로, 구글 인증 앱 등과 쉽게 통합할 수 있습니다.
단점:
제한된 유연성: 라이브러리의 기능과 설정에 제약이 따릅니다. 즉 특정 요구사항에 맞게 커스터마이징하기 어려울 수 있습니다.
외부 의존성: 외부 라이브러리에 의존하게 되므로, 라이브러리 업데이트 및 유지보수에 대한 추가적인 고려가 필요합니다.
결론
단순하고 빠른 구현이 필요한 경우: warrenstrange/googleauth 라이브러리
커스터마이징과 세부 제어가 필요한 경우: commons-codec
TOTP (Time-based OTP)의 동작원리
TOTP는 시간에 기반하여 OTP를 생성합니다. 사용자는 일정한 시간 간격마다 변경되는 비밀번호를 사용하여 인증을 받아야 하는 방식이죠. 일반적으로 우리는 사용자 측면에서만 TOTP를 이용해왔습니다. 우선 1차 로그인을 하면, 구글 OTP와 같은 앱을 켜서 정해진 시간마다 변동돼는 6자리 숫자를 모바일로 확인해서 웹화면에 입력하죠. 그렇다면 이러한 일련의 과정은 전체적으로 어떤 과정이 일어나기에 가능한걸까요? 이번에는 TOTP의 그 동작 원리에 대해 확인해 봅시다.
공유 비밀 키(Secret Key): 서버와 클라이언트(사용자)는 미리 공유된 비밀 키를 가지고 있어야 합니다.
시간 동기화: 서버와 클라이언트는 시간을 동기화해야 합니다. 일반적으로 Unix 타임스탬프(1970년 1월 1일 00:00:00 UTC부터의 초)를 사용합니다.
시간 슬롯 계산: 현재 시간을 일정한 간격(예: 30초)으로 나누어 슬롯을 계산합니다. 이 간격을 시간 틱(Time Tick)이라고 합니다.
HMAC 계산: 현재 시간 슬롯과 비밀 키를 사용하여 HMAC-SHA1 알고리즘을 적용합니다. 여기서 HMAC은 키를 사용한 해시 기반 메시지 인증 코드입니다.
OTP 생성: HMAC의 결과로부터 특정 길이(예: 6자리)의 숫자를 추출하여 OTP를 생성합니다. 일반적으로 OTP는 HMAC 결과의 일부를 사용하여 6자리 또는 8자리 숫자로 변환됩니다.
검증: 사용자가 입력한 OTP를 서버에서 동일한 방식으로 생성된 OTP와 비교하여 검증합니다. 시간 슬롯이 짧기 때문에 OTP는 매우 짧은 시간 동안만 유효합니다.
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
public class GoogleAuthExample {
public static void main(String[] args) {
// Create an instance of GoogleAuthenticator
GoogleAuthenticator gAuth = new GoogleAuthenticator();
// Generate a new key
GoogleAuthenticatorKey key = gAuth.createCredentials();
String secret = key.getKey();
System.out.println("Secret key: " + secret);
// Generate a QR code URL to be scanned by Google Authenticator app
String userName = "user@example.com";
String issuer = "ExampleIssuer";
String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(issuer, userName, key);
System.out.println("QR Code URL: " + qrCodeUrl);
// Simulate the OTP that the user would provide from their Google Authenticator app
int otp = gAuth.getTotpPassword(secret);
System.out.println("Generated OTP: " + otp);
// Verify the provided OTP
boolean isCodeValid = gAuth.authorize(secret, otp);
System.out.println("Is the OTP valid? " + isCodeValid);
}
}
getOtpAuthURL로부터 알아낸 URL을 실제로 접속하면 QR코드로서 확인해 볼 수 있습니다. 그런데 이렇게 하면 api.qrserver.com에 귀속된 URL을 알려주게 되는데, api.qrserver.com은 구글 서버가 아닙니다. api.qrserver.com은 goqr.me라는 웹사이트에서 제공하는 QR 코드 생성 서비스의 API 라고하네요. goqr.me는 QR 코드를 생성하고 관리하는 데 특화된 서비스로, 다양한 형태의 QR 코드를 쉽게 만들 수 있도록 도와줍니다. 하지만, 이런 의존적인 방식이 마음에 안들경우 직접 QR을 생성할 필요성이 있는데, 이 부분에 대해서는 ZXing 라이브러리를 사용하여 자체적으로 QR을 생성하는 방식으로 해결하였습니다!
애초 제가 해당 프로젝트를 할 때는 그 데이터를 받을 때는 QueryPram으로 받도록 설계했었습니다.
RequestBody로 변경된 이유를 묻자, 취약점 점검 시 해당 부분이 문제점으로 발견되어 고치게 되었다고 하네요...
이슈 처리 바빠서 그 후배에게는 아직 설명을 못해주었지만, GET API를 QueryPram을 RequestBody로 변경한 것은 적절치 못합니다. 오늘은 그 이야기에 대해 포스팅 해보려고 합니다.
과거 프로젝트를 진행할 때 당시 개발자들과의 고민은 "어떻게 해야 MSA 서비스에서 RestFul한 API를 제공할 수 있을지"에 대해서 였습니다. API의 기본 아키텍쳐에 대한 고민이었죠. 그 중 가장 컸던 부분은 필터 기능에 대한 것이었습니다.
간단한 예시를 들자면 게시판에서 여러가지 조건검색을 할 수 있는데 이 필터 조건들을 어떻게 API에게 전달할지에 대한 고민이었죠. 곧장 저희도 RequestBody를 떠올렸습니다. 예를 들면 이러한 JSON구조를 생각해 볼 수 있겠네요.
{
"filterId": "subject", // 어떤 타입의 필터인지? 제목검색, 날짜검색 등등
"filterTarget": "개발", // 어떤 대상으로 필터링할것인지? 내용, 날짜 등등
"filterOrder": "like", // 대상에 대해 어떤 조건으로 필터링할것인지? 포함돼는 문자열, 특정 날짜 범위 등등
}
흠... 확인히 RequestBody로 하면 깔끔하게 보이긴하네요, 쓰고싶어지는 유혹(?)이 듭니다. 하지만 저희는 당시 RequestBody방식을 선택하지 않았습니다. 왜냐하면 이건 RestFul하지 않기 때문이죠. 왜일까요?
기본적으로 RequestBody는 GET에서 사용하길 권장하지 않아요 아래와 같은 이유 때문입니다.
HTTP 표준 준수: HTTP/1.1 표준(RFC 7231)에 따르면, GET 요청은 요청 본문을 포함하지 않아야 합니다. GET 요청의 목적은 리소스를 조회하는 것이며, 본문을 포함하는 것은 표준을 벗어난 사용입니다.
캐싱 문제: GET 요청은 캐시될 수 있어야 합니다. 캐시는 요청 URL을 기준으로 동작하므로, 본문을 포함한 GET 요청은 캐시 시스템에서 올바르게 처리되지 않을 수 있습니다. 본문을 포함한 요청은 캐시 일관성을 유지하기 어렵게 만듭니다.
안전성과 멱등성: GET 요청은 안전하고 멱등적이어야 합니다. 즉, GET 요청은 서버 상태를 변경하지 않아야 하고, 동일한 GET 요청을 여러 번 실행해도 결과가 동일해야 합니다. 본문을 포함하는 GET 요청은 이러한 특성을 깨뜨릴 수 있습니다.
클라이언트와 서버의 지원 부족: 많은 HTTP 클라이언트 라이브러리 및 서버 프레임워크는 GET 요청의 본문을 지원하지 않거나 무시합니다. 예를 들어, 일부 브라우저는 GET 요청 본문을 보내지 않으며, 일부 서버는 본문을 처리하지 않습니다.
표준 툴 및 라이브러리 호환성: 많은 개발 도구와 라이브러리는 GET 요청에 본문이 없다는 가정을 하고 설계되어 있습니다. 따라서, GET 요청에 본문을 포함하면 이러한 도구들과의 호환성 문제를 일으킬 수 있습니다.
또한 첨언을 더 해보자면, GET은 기본적으로 URL에 데이터가 포함되길 기대되며, URL에 요청 파라미터가 포함된 형태 + Body에도 포함된 형태는 일관적이지않으며 API의 복잡도를 오히려 더 늘어나게 된다고 판단했기 때문입니다. 예를들어 다음과 같은 API를 상상해보세요.
URL [GET] http://localhost:8080/api/board/100
Requst Body
{
"filterId": "subject", // 어떤 타입의 필터인지? 제목검색, 날짜검색 등등
"filterTarget": "개발", // 어떤 대상으로 필터링할것인지? 내용, 날짜 등등
"filterOrder": "like", // 대상에 대해 어떤 조건으로 필터링할것인지? 포함돼는 문자열, 특정 날짜 범위 등등
}
100이라는 boardSeq 게시판에 대해 RequestBody로 검색하는 기능인데... pathParam도 있고, Body에도 조건이 포함되어 있어요. 이러한 구조는 API의 직관성을 해친다고 생각합니다.
그럼 저희는 어떻게 했을까요? 저희는 JSON데이터를 URL인코딩하여 QueryPram으로 처리하였습니다.
URL하나만으로 모든 것을 표현할 수 있습니다. 또한 이러한 특징은 사용자가 URL를 타인에게 공유하거나 응용하여 사용 할 때도 도움을 줄 수 있습니다. :) 훨씬 직관적이죠?
그렇다면 보안취약점에 해당 부분이 문제가 된다면 어떻게 해결해야 할까요? 만약 저였다면 문제로 지적된 파라미터에 대해 암호화 처리 등을 통해 해결하였을 것 같아요. ㅎㅎ 아니면 문제가 될 부분이 아닌데 탐지 되었을 가능성도 있으니 그 부분도 고려를 해서 예외처리를 하던가 해야겠지요? 그 후배가 틀렸다는 이야기를하는 것은 결코 아닙니다 :) 개발에는 좋은 방향성은 존재하지만 정답이 있다고 할 수 없기 때문입니다. 어쨌거나 보안취약점은 해결이 되었으니까요 ㅎㅎ 과거의 깊은 고민과 철학들이 담당자가 달라지며 조금씩 조금씩 사라져가는 것은 아주 조금 슬프긴합니다. ㅋㅋ 사람이 변하고 시간이 흐르면 과거의 고민들도 함께 사라져가는 것 같아요.
안녕하세요? 어느날 문득 운영단계로 넘어간 작년의 프로젝트가 잘 운영되고 있는지에 대해 살피던 중 SEO가 여전히 잘 안되고 있음을 느꼈습니다. 비록 지금은 제 손을 잠시(?) 떠난 프로젝트이지만, 이상한 애착이 생겼다고해야 할까요... 회사에서는 SPA를 지향하고 있고 팀에서 경험이 전무했던 Next.js를 과감히 도입 결정했던 이유도 SEO 때문이었는데 마음이 불편했습니다. 어쨌든 회사에서는 제 할 일이 주어져있기에 주말에 시간을 내어 스스로 알아보고 해결할 수 있는 방법을 모색해보기로 했습니다. 사족이 길었습니다만 궁금하잖아요?
SEO(검색 엔진 최적화)는 웹사이트의 가시성을 높이고 검색 엔진 결과 페이지에서 더 높은 순위를 차지하기 위해 중요한 요소입니다. Next.js는 React 기반의 프레임워크로, SEO 최적화에 필요한 다양한 기능을 제공하여 웹사이트를 더욱 검색 엔진 친화적으로 만들 수 있습니다. 이번 블로그 포스트에서는 Next.js를 사용하여 SEO를 최적화하는 방법에 대해 알아보겠습니다. 그리고 간단히 라이트 하우스라는 구글의 익스텐션 툴을 이용해 성능 확인하는 방법을 알아보겠습니다!
1. 동적 라우팅 및 정적 생성
Next.js의 강력한 기능 중 하나는 동적 라우팅(dynamic routing)과 정적 사이트 생성(SSG, Static Site Generation)입니다. 동적 라우팅을 통해 사용자 친화적인 URL 구조를 만들 수 있으며, 정적 사이트 생성을 통해 빠른 로딩 속도와 검색 엔진에 최적화된 페이지를 제공할 수 있습니다.
상기 샘플소스코드는 동적 라우팅을 설정하고, 정적으로 페이지를 생성하는 것을 보여주며 이를 통해 SEO 최적화에 도움을 줍니다.
그렇다면 왜 동적 라우팅이 SEO에 도움이 될까요?
이 부분에는 여러가지 이유가 있겠습니다만 주된 이유로는 아래와 같습니다.
SEO 친화적인 URL 구조:
동적 라우팅을 사용하면 SEO 친화적인 URL을 쉽게 생성할 수 있습니다. 예를 들어, example.com/blog/post-title 같은 구조는 example.com/?id=123와 같은 구조보다 검색 엔진과 사용자 모두에게 더 이해하기 쉽습니다. 검색 엔진은 URL에 포함된 키워드를 분석하여 페이지의 주제를 파악하는 데 도움을 받습니다.
콘텐츠 조직화:
동적 라우팅은 콘텐츠를 카테고리나 태그 별로 정리하는 데 유용합니다. 이는 검색 엔진이 사이트 구조를 더 잘 이해하고 크롤링할 수 있도록 도와줍니다. 예를 들어, 블로그 포스트를 example.com/blog/category/post-title와 같이 구성하면 특정 주제에 대한 모든 관련 포스트를 쉽게 찾을 수 있습니다.
이어서 정적 생성이 SEO에 도움이 되는 이유 뭘까요?
빠른 로딩 속도:
정적으로 생성된 페이지는 빌드 타임에 HTML 파일로 생성되므로, 서버 요청 시 동적으로 생성되는 페이지보다 로딩 속도가 훨씬 빠릅니다. 페이지 로딩 속도는 검색 엔진 순위에 중요한 요소입니다. 구글은 빠른 로딩 속도를 사용자 경험의 중요한 요소로 간주하여 높은 순위를 부여합니다.
예측 가능한 크롤링:
정적으로 생성된 페이지는 빌드 시점에 모든 HTML이 생성되므로, 검색 엔진이 페이지를 크롤링할 때 동적 콘텐츠 로딩 문제 없이 페이지 전체를 쉽게 인덱싱할 수 있습니다. 이는 JavaScript를 통해 동적으로 로드되는 콘텐츠보다 SEO에 더 유리합니다.
페이지 안정성:
정적으로 생성된 페이지는 변경되지 않기 때문에, 검색 엔진이 크롤링할 때 일관된 콘텐츠를 확인할 수 있습니다. 이는 동적으로 생성되는 페이지에서 발생할 수 있는 다양한 변수와 변경 사항으로 인해 발생하는 크롤링 오류를 줄여줍니다.
2. 메타 태그 설정
메타 태그는 검색 엔진이 페이지의 내용을 이해하는 데 중요한 역할을 합니다. Next.js에서는 next/head를 사용하여 메타 태그를 쉽게 설정할 수 있습니다.
이 코드는 페이지의 제목과 설명을 메타 태그로 설정하여 검색 엔진이 페이지의 내용을 더 잘 이해하도록 돕습니다.
소스코드에 보시면 메타 태그 엘리먼트에 og라는 속성 값이 눈에 띄는데 이것은 무엇을 의미할까요?
메타 태그 설정에서 og는 "Open Graph"의 약자입니다. Open Graph 프로토콜은 페이스북이 개발한 표준으로, 웹 페이지가 소셜 미디어 플랫폼에서 어떻게 공유되고 표시되는지를 제어합니다. Open Graph 메타 태그를 사용하면 웹 페이지의 미리보기, 이미지, 제목, 설명 등을 지정할 수 있어 소셜 미디어에서의 콘텐츠 표현을 개선할 수 있습니다. 주요한 오픈 그래프 메타 태그를 아래에 기록해두겠습니다.
og:title:
웹 페이지의 제목을 정의합니다. 이 제목은 페이지가 소셜 미디어에서 공유될 때 표시됩니다.
og:description:
웹 페이지의 간단한 설명을 정의합니다. 이 설명은 페이지가 소셜 미디어에서 공유될 때 표시됩니다.
og:image:
소셜 미디어에서 공유될 때 표시할 이미지 URL을 정의합니다. 이미지가 시각적으로 매력적이면 더 많은 클릭과 공유를 유도할 수 있습니다.
og:url:
웹 페이지의 URL을 정의합니다. 이 URL은 페이지가 소셜 미디어에서 공유될 때 표시됩니다.
og:type:
웹 페이지의 타입을 정의합니다. 예를 들어, 웹 페이지가 기사라면 article로 설정합니다.
Open Graph 메타 태그는 웹 페이지가 소셜 미디어에서 어떻게 보이는지를 제어하는 중요한 도구입니다. Next.js 프로젝트에 이러한 태그를 적절히 설정하여, 콘텐츠가 소셜 미디어에서 더 잘 공유되고 더 많은 트래픽을 유도할 수 있도록 해봅시다..
3. 정적 사이트 생성(SSG) 및 서버 사이드 렌더링(SSR)
Next.js는 SSG와 SSR을 모두 지원하여 SEO 최적화에 유리합니다. SSG는 빌드 타임에 HTML 파일을 생성하여 빠른 로딩 속도를 제공하고, SSR은 서버에서 페이지를 렌더링하여 검색 엔진이 페이지 내용을 쉽게 크롤링할 수 있도록 합니다.
이 설정은 사이트맵과 robots.txt 파일을 자동으로 생성하여 검색 엔진이 사이트를 더 잘 크롤링하도록 합니다.
5. Light house로 성능 검증해보기
모든 설정을 마쳤다면, Light house를 이용해 서비스의 검색엔진 최적화 성능을 확인해보면 좋을 것 같아요
Lighthouse는 웹 페이지의 성능, 접근성, SEO, PWA(Progressive Web App) 등을 분석하여 개선할 수 있는 포인트를 제공하는 오픈 소스 도구입니다. Google Chrome의 개발자 도구에 내장되어 있으며, 별도의 CLI(Command Line Interface) 또는 웹 버전으로도 사용할 수 있습니다. 4장에서는 Lighthouse를 사용하여 검색 엔진 최적화(SEO) 기능을 활용하는 방법에 대해 설명하겠습니다.
Chrome 브라우저에서 Lighthouse 실행
페이지 로드: SEO 분석을 원하는 웹 페이지를 Chrome 브라우저에서 엽니다.
개발자 도구 열기: Ctrl + Shift + I(Windows) 또는 Cmd + Option + I(Mac)를 눌러 개발자 도구를 엽니다.
Lighthouse 탭 선택: 개발자 도구에서 Lighthouse 탭을 선택합니다.
SEO 선택: Lighthouse 탭에서 원하는 카테고리를 선택합니다. SEO 분석을 위해서는 Search Engine Optimization 옵션을 선택합니다.
분석 시작: Generate report 버튼을 클릭하여 분석을 시작합니다. Lighthouse는 페이지를 분석하고 보고서를 생성합니다.
SEO를 적용하기 전과 후를 한번 점검해보시기 바랍니다.
포스팅을 마치며...
Next.js를 사용하여 SEO를 최적화하는 방법을 살펴보았습니다. 동적 라우팅, 메타 태그 설정, 정적 사이트 생성, 서버 사이드 렌더링, 사이트맵 설정 등을 통해 검색 엔진에 최적화된 웹사이트를 구축할 수 있습니다. 개인적으로 공부하면서 느낀 점은 (1) 메타태그 잘쓰기, (2) 검색엔진을 위한 SSR의 적절한 활용, (3) robots.txt와 sitemap.xml 잘 작성해주기.. 가 핵심인 것 같아요. 아, 그리고 팁을 드리자면 요즘의 검색엔진은 메타태그의 keyword에 대해서는 그렇게 높은 점수를 주지 않는 것이 현실인 것 같다고 하네요(스팸등의 이유로...) 아무쪼록 Next.js의 강력한 기능을 활용하여 더 많은 트래픽과 더 높은 검색 엔진 순위를 달성해보세요. :)
자바는 14버전부터 record라는 새로운 기능을 도입하여 데이터 객체(Data Object)를 보다 간결하고 명확하게 정의할 수 있게 되었다. 이번 포스팅에서는 record의 기본 사용법과 특징을 살펴보고, 기존의 DTO(Data Transfer Object)와의 차이점도 함께 알아보겠다.
record란 무엇인가?
record는 불변 객체(immutable object)를 쉽게 생성할 수 있도록 도와주는 자바의 새로운 타입이다. 주로 데이터 전달 객체(Data Transfer Object, DTO)나 값을 담는 컨테이너로 사용된다. record를 사용하면 클래스 작성 시 필수적인 반복 작업들을 자동으로 처리할 수 있다.
기본 사용법
다음은 record를 사용한 간단한 예제이다
public record Person(String name, int age) {}
위의 예제는 Person이라는 레코드를 정의한 것이다. Person 레코드는 두 개의 필드 name과 age를 가진다. 이렇게 정의된 레코드는 다음과 같은 작업들을 자동으로 처리한다.
필드 정의: String name, int age 필드가 자동으로 정의된다.
생성자: 모든 필드를 인수로 받는 생성자가 자동으로 생성된다.
접근자 메서드: name()과 age() 메서드가 자동으로 생성된다.
equals() 및 hashCode(): 필드를 기반으로 한 equals() 및 hashCode() 메서드가 자동으로 생성된다.
toString(): 필드 값을 포함하는 toString() 메서드가 자동으로 생성된다.
사용 예제
public class Main {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
// 자동 생성된 메서드 사용
System.out.println(person.name()); // 출력: Taeho
System.out.println(person.age()); // 출력: 32
System.out.println(person); // 출력: Person[name=Taeho, age=32]
}
}
커스터마이징
레코드 클래스는 커스터마이징할 수 있다. 예를 들어, 추가 메서드를 정의하거나 생성자를 커스터마이징할 수 있다.
public record Person(String name, int age) {
// 추가 메서드
public String greet() {
return "Hello, " + name;
}
// 커스텀 생성자
public Person {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
}
위 예제에서는 greet()라는 추가 메서드와 나이가 음수일 경우 예외를 던지는 커스텀 생성자를 정의하였다.
DTO와 record의 차이점
DTO(Data Transfer Object)와 record는 데이터 객체를 정의하는 데 사용되지만, 두 가지 방식에는 몇 가지 차이점이 있다.
보일러플레이트 코드: DTO는 필드 정의, 생성자, getter/setter 메서드, equals(), hashCode(), toString() 메서드를 수동으로 작성해야 한다. 반면 record는 이러한 메서드들을 자동으로 생성하여 보일러플레이트 코드를 크게 줄여준다.
불변성: DTO는 기본적으로 가변 객체(mutable object)로 설계되는 경우가 많다. 그러나 record는 불변 객체로 설계되어, 한 번 생성된 후 변경할 수 없다. 이는 멀티스레드 환경에서 안전성을 보장해준다.
의도 표현: record는 데이터 객체의 의도를 명확하게 표현할 수 있으며, 데이터만 담고 있는 객체임을 쉽게 알 수 있다. 반면, DTO는 일반 클래스와 구별하기 어려울 수 있다.
결론
자바의 record는 데이터 중심의 클래스를 쉽게 정의하고 관리할 수 있게 해주는 강력한 도구이다. 이를 통해 코드의 가독성과 유지보수성을 높일 수 있다. DTO와 비교했을 때, record는 보일러플레이트 코드를 줄이고 불변성을 보장하는 등 여러 가지 장점을 제공한다. 앞으로 자바로 데이터 객체를 정의할 때 record를 적극적으로 활용해보자.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
public class Main{
public static int N, M, H;
public static int[][][] box;
public static int[] dx = {1, -1, 0, 0, 0, 0};
public static int[] dy = {0, 0, 1, -1, 0, 0};
public static int[] dz = {0, 0, 0, 0, 1, -1};
public static Queue<Tomato> queue = new LinkedList<Tomato>();
static class Tomato {
int x, y, z;
public Tomato(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
M = Integer.parseInt(st.nextToken());
N = Integer.parseInt(st.nextToken());
H = Integer.parseInt(st.nextToken()); /** 1번 문제는 변수 H를 1로 주면 해결된다.. */
box = new int[H][N][M];
for (int k = 0; k < H; k++) {
for (int i = 0; i < N; i++) {
st = new StringTokenizer(br.readLine(), " ");
for (int j = 0; j < M; j++) {
box[k][i][j] = Integer.parseInt(st.nextToken());
if(box[k][i][j] == 1){
queue.offer(new Tomato(i, j, k));
}
}
}
}
bfs();
}
static int isPerfectlyRipe () {
int max = -10000000;
for (int k = 0; k < H; k++) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (box[k][i][j] == 0) {
return 0;
}
max = max > box[k][i][j] ? max : box[k][i][j];
}
}
}
return max;
}
public static void bfs() {
while (!queue.isEmpty()) {
Tomato tomato = queue.poll(); // 큐에서 익은 토마토를 하나 꺼내자
for (int i = 0; i < 6; i++) { // 상하좌우위아래 6개 방향으로 이동했을 때의 각 토마토 확인
int _x = tomato.x + dx[i];
int _y = tomato.y + dy[i];
int _z = tomato.z + dz[i];
if(0 <= _x && _x < N &&
0 <= _y && _y < M &&
0 <= _z && _z < H) { // 이동한 장소가 배열의 크기를 벗어나지 않도록 해야한다
if (box[_z][_x][_y] == 0) { // 아직 안익었다면 큐에 넣어준다
box[_z][_x][_y] = 1; //이제 익었으니까 1로 변환해주어야 다음 탐색을 이어나갈 수 있다
queue.add(new Tomato(_x, _y, _z));
box[_z][_x][_y] = box[tomato.z][tomato.x][tomato.y] + 1; // 날짜정보를 저장해준다 이 정보는 1부터 시작했으니 결과에서 1을 빼주어야한다
}
}
}
}
System.out.println(isPerfectlyRipe() - 1);
}
}
-컨테이너 작동을 하려면 애플리케이션을 움직이기 위해 필요한 바이너리, OS, 네트워크와 같은 ㅇ니프라 설ㅈ어이 모두 포함되어 있는 ‘컨테이너 이미지’를 작성해야 한다. 도커의 경우 DOCKERFILE이라는 텍스트 파일에 구성을 기술, 그것을 빌드한 것을 실행 환경에서 이용가능한 레포지토리로 공유한다.
스텝3클러스터 작성 (실제 환경의 작성)
실제로 컨테이너 애플리케이션을 작동시키는 서버를 셋업한다.
개발 환경이나 테스트 환경에서는 로컬 머신에서 작동시킬 수도 있지만 서비스를 공개할 때는 자신이 보유한 온프레미스 환경에서 시스템을 구축하거나 클라우드 서비스를 이용한다.
2.2 개발 환경 준비
Visual studio code 설치
MS가 제공하는 오픈소스 소스코드 에디터이며 무료이다. Windows, macOS, Linux에서도 작동된다.
비주얼 스튜디오 마켓플레이스에서 제공하는 확장 기능을 넣어 커스터마이징하여 사용 할 수 있다.
이 취약점으로 주말에 잘쉬고 있다가 갑자기 일을 하게 되었다.. 보편적으로 안쓰는 업체가 찾기 힘든 라이브러리에서 크리티컬한 취약점이 발견된 것이다. 이후에도 지속적으로 개발 커뮤니티에도 해당 이슈가 올라와서 나도 내가 아는바에 대한 내용을 기록으로 남겨본다
[개요]
자바 기반으로 만들어지고 전세계적으로, 대중적(?)으로 많이 사용되는 로깅을 위한 라이브러리로 log4j가 있다.
이번 이슈가 된 취약점은 log4j가 넘겨받은 변수를 그대로 로깅하는 것이 아니라 해당 변수를 분석해 실행할 수 있는 lookups 기능에서 발견되었다. lookups란 예를들어 log.error("에러 : " + result); 에서 result가 ${env:PASSWORD}로 설정되어 있다면 "에러 : ${env:PASSWORD}"를 그대로 로깅하는 것이 아니라 이에 해당하는 설정값을 찾아 "에러 : qwer1!"를 로깅하게 되는 기능이다. lookup에는 여러가지 기능들이 있는데, 이번 케이스는 JDNI 룩업을 이용한 공격이 가능하다는 취약점이다. 이른바 Log4Shell 취약으로 불린다.
[취약점 공격 대상]
log4j-core 버전 2.10.0 이상 ~ 2.15.0 미만
[대응방법]
1. log4j를 2.15.0 이상으로 업그레이드 한다. (Java8 이상에서만 기동된다)
2. log4j 2.10.0 이상 사용 시 다음의 방법 중한 가지 이상의 방법을 사용한다. 개인적인 의견으로는 서비스의 운영환경마다 다르겠지만.. 운영하는 웹서비스가 많아 관리 포인트가 많고 복잡할 경우.. 이 방법은 관리측면에서는 별로 좋은 방법 같진 않다. 개발자가 프로젝트 소스코드 외부에서 관리해야하고(서버/인프라 단), 이 설정이 되어있는 이유에 대해 나중에 시간이 많이 지나서 누가 보더라도 알 수 있도록 히스토리를 잘 관리해야 한다는 점..
Java 실행 인자(Arguments) 에 시스템 속성을 추가한다.-Dlog4j2.formatMsgNoLookups=true
Java 실행 계정의 환경 변수 혹은 시스템 변수로LOG4J_FORMAT_MSG_NO_LOOKUPS=true를 설정한다.
3. log4j 2.7.0 이상 사용 시 log4j 설정(log4j.xml 등)에PatternLayout속성에 있는%m부분을%m{nolookups}으로 교체한다.
4. 만약 log4j 버전이 2.10.0보다 낮다면, 이번 이슈의 공격대상은 아니다.. 그렇지만 1버전 대의 경우는 지원이 중단되었으며, 이미 RCE 보안이슈가 존재하므로 가급적 업그레이드 방안을 모색하는 것이 좋다
[팁]
위 대응방법은 기본적인 조치 방법에 대해 설명한 것이고, 이와 더불어 조치 시 아래 오픈소스를 참고하면 빠르게 조치할 수 있다.