반응형

JWT 인증에서 Access Token 자동 갱신 구현 (Axios 기반)

JWT 기반 인증을 사용할 때 Access Token은 일반적으로 짧은 만료 시간을 가집니다. 이는 보안을 강화하기 위한 전략이지만, 토큰이 만료될 때마다 사용자가 다시 로그인해야 한다면 UX는 매우 나빠지겠죠. 이를 해결하기 위해 Refresh Token을 이용한 자동 갱신 방식이 널리 사용됩니다.

이 글에서는 프론트엔드에서 Access Token이 만료되었을 때, 자동으로 /auth/refresh 엔드포인트를 호출해 토큰을 갱신하고 요청을 재시도하는 구현 방법을 설명합니다.

✅ 사용 기술: Axios + JWT + HttpOnly 쿠키 기반 인증


🎯 구현 목표

  1. API 요청 도중 Access Token이 만료되면 자동으로 /auth/refresh 호출
  2. 새 Access Token을 발급받고, 기존 실패한 요청을 재시도
  3. Refresh Token은 HttpOnly 쿠키에 저장되어 자동 포함됨
  4. Refresh 실패 시 로그아웃 처리

🔧 Axios 인스턴스 생성 및 인터셉터 설정

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://your-api.com',
  withCredentials: true, // 쿠키 포함 필수 설정
});

let isRefreshing = false;
let failedQueue: any[] = [];

const processQueue = (error: any, token: string | null = null) => {
  failedQueue.forEach(prom => {
    if (error) prom.reject(error);
    else prom.resolve(token);
  });
  failedQueue = [];
};

api.interceptors.response.use(
  res => res,
  async err => {
    const originalRequest = err.config;

    if (err.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(() => api(originalRequest));
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        await api.post('/auth/refresh'); // Refresh Token은 쿠키에 자동 포함됨
        processQueue(null);
        return api(originalRequest); // 실패한 요청 재시도
      } catch (refreshError) {
        processQueue(refreshError, null);
        window.location.href = '/login'; // 리프레시 실패 시 로그아웃 처리
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(err);
  }
);

export default api;

🔁 흐름 정리

  1. 유저가 API 요청을 보냄
  2. 서버가 Access Token 만료로 401 Unauthorized 응답
  3. Axios 인터셉터가 이를 감지하고 /auth/refresh 호출
  4. Refresh Token은 HttpOnly 쿠키에 자동 포함됨
  5. 새 Access Token이 발급되면 원래 요청을 재시도
  6. Refresh 실패 시 로그인 페이지로 이동

✅ 보안 고려 사항

항목설명

withCredentials: true 쿠키를 요청에 포함하려면 반드시 설정
HttpOnly 쿠키 JS에서 토큰 접근 불가 → XSS 방어
SameSite=Strict 또는 Lax CSRF 방어용 쿠키 설정
Refresh Token 저장 위치 반드시 쿠키 (localStorage 금지)
Refresh 실패 시 처리 로그아웃 처리 or 알림 후 로그인 페이지 이동

🧠 마무리

Axios 인터셉터를 활용하면 토큰 갱신 로직을 모든 요청에 일관되게 적용할 수 있어, 보안과 UX 모두를 만족시키는 인증 흐름을 만들 수 있습니다.

'컴퓨터 > JWT' 카테고리의 다른 글

쿠키(Cookie) 옵션 완벽 정리  (0) 2025.05.19
반응형

🔐 쿠키(Cookie) 옵션 완벽 정리: HttpOnly, Secure, SameSite의 모든 것

웹 개발에서 인증과 보안을 고민할 때 가장 먼저 마주치는 개념 중 하나가 바로 **쿠키(Cookie)**입니다. 하지만 쿠키 설정 시 등장하는 HttpOnly, Secure, SameSite 옵션이 정확히 뭘 의미하고, 어떤 보안 이슈를 막아주는지 확실하게 아는 사람은 많지 않습니다.

이 글에서는 실무에서 꼭 알아야 할 쿠키 옵션들의 기능, 보안 목적, 사용 예시를 정리해드릴게요.


🍪 쿠키란?

쿠키는 클라이언트(브라우저)에 저장되는 작은 데이터 조각입니다.

  • 서버가 Set-Cookie 헤더로 값을 내려주면,
  • 브라우저는 쿠키를 저장하고,
  • 이후 같은 도메인 요청마다 자동으로 쿠키를 전송합니다.

대표적인 활용 예:

  • 로그인 상태 유지 (세션 ID / JWT)
  • 장바구니 상태 저장
  • 사용자 설정 기억 (언어, 테마 등)

✅ 주요 쿠키 옵션 정리

옵션명설명방어하는 보안 위협
HttpOnly JS에서 쿠키 접근 불가 XSS (스크립트 탈취)
Secure HTTPS 연결에서만 전송 패킷 도청, MITM
SameSite 외부 요청 시 쿠키 전송 여부 제어 CSRF (요청 위조)
Path 특정 경로에만 쿠키 전송 제한된 범위 적용
Domain 서브도메인 공유 여부 도메인 스코프 설정
Max-Age / Expires 만료 시간 설정 자동 삭제
SameParty 제3자 쿠키 제한 회피 (실험적) Chrome Privacy Sandbox 대응
 

🔐 HttpOnly: JavaScript 접근 차단

Set-Cookie: token=abc123; HttpOnly
  • ✅ 쿠키를 JavaScript에서 document.cookie로 접근 불가
  • ✅ XSS 공격 시에도 쿠키를 훔칠 수 없음
  • ❌ JS로 토큰을 꺼내서 Authorization 헤더로 직접 보내야 하는 경우 사용 불가

💡 실무 팁: 인증 관련 쿠키(세션 ID, Access Token)는 반드시 HttpOnly 설정!


🔒 Secure: HTTPS에서만 전송

Set-Cookie: token=abc123; Secure
  • ✅ HTTPS 연결일 때만 서버로 쿠키 전송
  • ❌ HTTP 요청에는 전송되지 않음 (MITM 방지)
  • 📌 Secure이 없는 쿠키는 평문 HTTP 요청에 노출될 수 있음

💡 실무 팁: Secure 쿠키를 쓰려면 HTTPS 환경이 기본 전제입니다. 운영환경이라면 무조건 사용하세요.


🛡️ SameSite: CSRF 방어의 핵심

Set-Cookie: token=abc123; SameSite=Strict
값설명
Strict 다른 사이트에서 유입된 요청은 모두 쿠키 차단 (가장 안전)
Lax GET이나 a 링크, form 전송 등은 허용 / JS fetch 등은 차단
None 모든 외부 요청에 허용 (단, Secure 필수)
 

💡 실무 팁: 인증 관련 쿠키에는 보통 SameSite=Strict 또는 Lax를 설정합니다. None은 제3자 쿠키 용도로만 사용하세요.


📁 Path, Domain: 적용 범위 제어

Set-Cookie: theme=dark; Path=/settings
  • Path=/settings: /settings 이하에서만 전송
  • Domain=example.com: sub.example.com에서도 공유 가능

💡 실무 팁: 보안성 향상을 위해 불필요하게 넓은 범위는 피하세요.


⏰ Max-Age, Expires: 쿠키 수명

Set-Cookie: token=abc123; Max-Age=900
  • Max-Age=900: 900초(15분) 후 만료
  • Expires=Wed, 21 Oct 2025 07:28:00 GMT: 지정된 날짜/시간에 만료

설정이 없으면 세션 쿠키가 되어 브라우저 종료 시 삭제됩니다.


✅ 실무 예시: 로그인 쿠키 설정

Set-Cookie: access_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
  • 🔐 JS 접근 불가 (HttpOnly)
  • 🔒 HTTPS 환경에서만 전송 (Secure)
  • 🛡️ CSRF 방지 (SameSite=Strict)
  • ⏰ 15분 후 자동 만료 (Max-Age)

🧠 마무리 요약

옵션설명권장 여부
HttpOnly JS 접근 차단 ✅ 항상 사용
Secure HTTPS에서만 전송 ✅ 운영환경 필수
SameSite 외부 요청 제한 ✅ CSRF 방어
Max-Age 유효 기간 설정 ✅ 필요시 사용
 

💬 마치며

쿠키는 단순한 저장소가 아니라, 보안 설정 하나로도 전체 서비스의 안정성을 좌우할 수 있는 민감한 요소입니다. 특히 인증과 세션 처리에 쿠키를 사용하는 경우에는 HttpOnly, Secure, SameSite는 사실상 필수 옵션이라고 생각하셔야 합니다.

보안은 구현보다 구성입니다.
조금만 신경 쓰면 더 안전한 서비스를 만들 수 있어요.

'컴퓨터 > JWT' 카테고리의 다른 글

JWT 인증에서 Access Token 자동 갱신 구현 (Axios 기반)  (0) 2025.05.19
반응형

Gmail SMTP 사용 방법

이 글에서는 Gmail 계정을 이용해 SMTP로 메일을 발송하는 방법을 상세히 정리합니다.
Spring Boot, Node.js 등 어떤 환경에서도 적용할 수 있도록 준비했습니다.


✅ Gmail SMTP란?

Gmail SMTP 서버를 사용하면, Gmail 계정을 통해 외부 애플리케이션에서 이메일을 전송할 수 있습니다.
개인 프로젝트부터 상용 서비스 초기 테스트까지 다양하게 활용할 수 있습니다.


✅ Gmail SMTP 사용 준비 단계

1. Gmail 계정 준비

  • 메일 발송에 사용할 Gmail 계정을 준비합니다.
  • 기존 계정을 사용해도 무방합니다.


2. 2단계 인증(2FA) 활성화

Gmail은 보안 강화를 위해, 2단계 인증을 필수로 요구합니다.

2단계 인증이 켜져 있어야 "앱 비밀번호"를 생성할 수 있습니다.


3. 앱 비밀번호(App Password) 생성

Gmail에서는 일반 비밀번호로 SMTP 로그인할 수 없습니다.
앱 비밀번호(App Password) 를 발급받아야 합니다.

  • Google 계정 보안 설정 → 앱 비밀번호 메뉴로 이동합니다.
  • "앱 선택" → 메일, "디바이스 선택" → 기타 → 원하는 이름 입력 후 생성합니다.
  • 생성된 16자리 비밀번호를 복사합니다.

⚡ 이 앱 비밀번호를 SMTP 로그인 비밀번호로 사용합니다.


✅ Gmail SMTP 서버 설정 정보

항목값
SMTP 서버 주소 smtp.gmail.com
포트 (TLS) 587
포트 (SSL) 465
사용자 이름 Gmail 이메일 주소
비밀번호 발급받은 앱 비밀번호
인증 필요 여부 YES

✅ 예제 코드

Spring Boot (application.yml)

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: your-email@gmail.com
    password: your-app-password
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
spring: mail: host: smtp.gmail.com port: 587 username: your-email@gmail.com password: your-app-password properties: mail: smtp: auth: true starttls: enable: true
  • TLS(587포트)를 사용하며, 인증과 StartTLS를 활성화합니다.

Node.js (Nodemailer)

import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: 'your-email@gmail.com',
    pass: 'your-app-password',
  },
});

await transporter.sendMail({
  from: 'your-email@gmail.com',
  to: 'receiver@example.com',
  subject: '테스트 메일',
  text: '이메일 본문입니다.',
});

✅ SMTP 서버 테스트 방법

  1. redis-server처럼, redis-cli와 유사하게 별도의 서버 구동 없이 바로 사용할 수 있습니다.
  2. smtp.gmail.com을 통해 메일 발송을 시도하면 정상 작동합니다.

✅ 주의 사항

  • 앱 비밀번호를 발급하지 않고 일반 비밀번호로 접속하면 에러가 발생합니다. (예: 535-5.7.8 Authentication Error)
  • 일반 Gmail 계정은 하루 약 500건 정도의 발송 제한이 있습니다.
  • 기업용 Google Workspace 계정은 별도 SMTP 서버 설정이 필요할 수 있습니다.
  • 너무 많은 발송 시 스팸 처리될 수 있으니 테스트용 외에는 주의해야 합니다.

✅ 한눈에 요약

절차설명
Gmail 계정 준비 메일 발송용 Gmail
2단계 인증 설정 Google 보안 페이지에서 활성화
앱 비밀번호 발급 메일용 앱 비밀번호 생성
SMTP 서버 설정 smtp.gmail.com, 포트 587/465, 인증 필요
메일 발송 발급받은 앱 비밀번호 사용

🎯 마치며

Gmail SMTP를 활용하면 별도의 SMTP 서버를 구축하지 않고도
빠르게 메일 발송 기능을 구현할 수 있습니다.

다만,

  • 테스트용 / 소규모 프로젝트에는 충분히 유용하지만,
  • 대규모 서비스에서는 Sendgrid, Mailgun, Brevo 같은 별도 SMTP 서비스 도입을 고려하는 것이 좋습니다.

이 글을 참고하여 여러분의 프로젝트에 메일 발송 기능을 쉽고 빠르게 추가해보세요!


📌 참고 링크


반응형

자바스크립트의 새로운 가능성

프론트엔드 개발을 하면서 "자바스크립트는 브라우저에서만 동작하는 언어"라고 생각하신 적 있으신가요? Node.js는 그 생각을 완전히 뒤바꿔놓았습니다. 이 글에서는 Node.js가 무엇인지, 어떤 특징을 가지고 있으며 어디에 주로 사용되는지 자세히 소개드릴게요.


✅ Node.js란?

Node.jsChrome의 V8 JavaScript 엔진을 기반으로 만들어진 서버 사이드 JavaScript 런타임입니다. 쉽게 말해, 자바스크립트를 브라우저가 아닌 서버 환경에서도 실행할 수 있게 해주는 플랫폼입니다.


🔧 Node.js의 주요 특징

1. 이벤트 기반(Event-driven) 구조

Node.js는 이벤트 루프를 기반으로 작동합니다. 요청이 들어오면 이벤트 큐에 등록하고, 완료되면 콜백 함수를 호출하는 방식입니다.

2. 논블로킹(Non-blocking) I/O

파일 읽기나 데이터베이스 조회 같은 I/O 작업을 기다리지 않고, 다른 작업을 먼저 처리할 수 있어 빠른 응답성을 자랑합니다.

3. 싱글 스레드 구조

Node.js는 단일 스레드로 동작하지만, 비동기 이벤트 처리 덕분에 높은 동시성 처리 능력을 가집니다.

4. NPM(Node Package Manager)

수많은 오픈소스 모듈을 쉽게 설치할 수 있는 패키지 관리자입니다. Express, Socket.IO, Mongoose 등 다양한 라이브러리를 활용할 수 있습니다.


💼 Node.js의 대표적인 사용 사례

분야활용 예시
웹 서버 REST API, SSR
실시간 애플리케이션 채팅 서버, 게임 서버
백엔드 시스템 인증, 파일 업로드, 데이터 가공
CLI 도구 제작 Node 기반 자동화 스크립트

🧠 왜 Node.js를 사용할까요?

  • 자바스크립트 하나로 프론트와 백엔드를 모두 개발할 수 있어 생산성이 높습니다.
  • 가볍고 빠르며, 실시간 기능 구현에 강력한 장점을 가집니다.
  • 대규모 커뮤니티와 NPM 덕분에 다양한 라이브러리와 플러그인을 활용할 수 있습니다.

🖼 Node.js 개념 요약

아래 Node.js의 구조와 특징을 한눈에 확인해보세요👇


마무리

Node.js는 단순히 '서버에서도 자바스크립트를 실행한다'는 것을 넘어서, 현대 웹 개발의 큰 축으로 자리 잡았습니다. 실시간 처리와 높은 성능이 필요한 프로젝트를 계획하고 있다면, Node.js는 훌륭한 선택이 될 수 있습니다!

궁금한 점이 있으시면 댓글로 남겨주세요 😊


반응형

Spring @Transactional이 내부 메소드 호출에서 동작하지 않는 이유

Spring Boot에서 @Transactional을 사용할 때 한 가지 흔히 겪는 함정이 있습니다.
바로, 같은 클래스 내에서 메소드를 호출하면 트랜잭션이 적용되지 않는다는 점입니다.

이번 포스팅에서는 이 현상이 왜 발생하는지, 그리고 어떻게 해결할 수 있는지 실제 예제를 통해 자세히 알아보겠습니다.


🧪 상황 예제: 트랜잭션이 안 먹는다?

@Service
public class AService {

    @Transactional
    public void methodA() {
        methodB(); // 내부 호출
    }

    public void methodB() {
        // 예외 발생
        throw new RuntimeException("예외 발생!");
    }
}
 

이 코드를 보면, methodA()는 @Transactional이 붙어 있어 트랜잭션이 시작되고,
그 안에서 호출되는 methodB()에서 예외가 발생하니 당연히 롤백될 것 같죠?

하지만 실제 실행해보면...
👉 롤백되지 않고 커밋됩니다!


🤔 왜 이런 일이 벌어질까?

이유는 Spring이 @Transactional을 프록시 기반 AOP로 구현하고 있기 때문입니다.

Spring의 트랜잭션 처리 흐름은 이렇게 됩니다:

  1. Spring이 @Transactional이 붙은 클래스를 감싸는 프록시 객체를 만듭니다.
  2. 외부에서 해당 메소드를 호출하면 프록시가 감지하여 트랜잭션을 시작합니다.
  3. 하지만, 같은 클래스 안에서 메소드를 호출하면 프록시를 거치지 않고 this.method() 형태로 직접 호출하게 됩니다.
  4. 따라서 트랜잭션을 적용할 기회를 놓쳐버립니다!

📌 즉, 프록시가 개입해야 트랜잭션이 적용되는데, 내부 호출은 프록시를 생략하므로 적용되지 않음!


💡 쉽게 말해서?

마치 건물 입구에 감시카메라(프록시)가 달려 있다고 생각해보세요.

  • 외부에서 들어오면 감시카메라가 감지해서 트랜잭션을 시작함
  • 하지만 내부에서 옆방으로 이동하면 감시카메라가 그걸 감지 못함
    → 트랜잭션 안 걸림

🛠 해결 방법

✅ 방법 1. 메소드를 다른 클래스로 분리

@Service 
public class BService { 

	@Transactional 
	public void methodB() { 
		throw new RuntimeException("예외 발생!"); 
	} 
    
}
@Service
public class AService {

    private final BService bService;

    public AService(BService bService) {
        this.bService = bService;
    }

    public void methodA() {
        bService.methodB(); // 외부 호출 → 프록시 적용됨!
    }
}

✅ 방법 2. ApplicationContext를 통해 프록시 자신을 가져와 호출

@Service
public class AService {

    private final ApplicationContext context;

    public AService(ApplicationContext context) {
        this.context = context;
    }

    public void methodA() {
        AService proxy = context.getBean(AService.class);
        proxy.methodB(); // 프록시를 통한 호출
    }

    @Transactional
    public void methodB() {
        throw new RuntimeException("예외 발생!");
    }
}

✅ 결론

@Transactional은 프록시를 거쳐야만 동작한다!
내부에서 자기 메소드를 호출하면 프록시를 거치지 않으므로 트랜잭션이 동작하지 않는다.

 

같은 클래스 내 메소드 간 호출에는 트랜잭션이 적용되지 않는다는 점,
항상 기억하고 설계하시길 바랍니다.


🔁 정리

구분트랜잭션 적용됨?설명
외부에서 AService.methodA() 호출 ✅ 적용됨 프록시가 감지함
AService.methodA() → 내부에서 methodB() 호출 ❌ 적용 안 됨 프록시를 거치지 않음
AService → BService.methodB() 호출 ✅ 적용됨 외부 클래스 프록시 통과
AService → context.getBean(AService.class).methodB() 호출 ✅ 적용됨 프록시를 수동으로 거침

반응형

오늘은 동기 / 비동기에 대해 완벽하게 정리해 보겠습니다.

 

동기(Sync) / 비동기(Async)


동기(Sync)란?

동기란 직렬적으로 작업을 수행하는 방식을 요청을 보낸 후 응답을 받아야지만 다음 동작이 이루어집니다.

즉, 다른 요청을 처리하는 동안 나머지 작업들은 대기 상태가 됩니다.

(출처:https://poiemaweb.com/es6-promise)

동기 실행의 동작 방식 및 처리 순서

콜 스택(Call Stack):

  • JavaScript  엔진은 현재 실행중인 모든 동기 작업을 콜 스택에 쌓습니다.
  • 콜 스택은 LIFO(Last In, First Out) 구조로 가장 마지막에 추가된 작업이 가장 먼저 처리됩니다.
  • 한 작업이 실행되면, 그 작업이 완료될 때 까지 콜 스택의 맨 위에 위치하며, 완료되면 콜 스택에서 제거됩니다.

블로킹(Blocking):

  • 동기 작업 중 하나가 긴 시간을 필요로 하는 작업일 경우, 그 작업이 완료될 때까지 콜 스택이 차단됩니다.
  • 이로 인해 후속 작업들은 차단 작업이 완료되기 전까지 실행될 수 없으며, UI업데이트나 다른 이벤트 처리가 지연될 수 있습니다.

작업 처리 순서:

  • 작업1이 콜 스택에 들어가서 실행됩니다.
  • 작업1에서 데이터를 서버에서 가져오는 동기작업을 요구하느 경우, 해당 작업이 완료되기 전까지 다음 작업은 실행되지 않습니다.
  • 작업1이 완료되고 콜 스택에서 제거되면, 작업2가 콜 스택에 들어가 실행됩니다.

예시:

console.log('Task 1 시작'); // 콜 스택에 들어감
// 서버에서 데이터를 동기적으로 가져옴 (블로킹)
const data = getDataFromServer(); // 작업이 완료될 때까지 콜 스택 차단
console.log('Task 1 완료'); // 작업 1 완료 후 콜 스택에서 제거

console.log('Task 2 시작'); // 작업 1이 완료된 후 콜 스택에 들어감
doSomethingWith(data); // 작업 2 실행
console.log('Task 2 완료'); // 작업 2 완료 후 콜 스택에서 제거

 

비동기(Async)란?

비동기병렬적으로 작업 수행하는 방식으로 요청을 보낸 후 응답의 수락 여부와 상관없이 다음 작업을 동작하느 방식입니다. 비동기 요청시 응답 후 처리할 콜백함수를 함께 알려주어 작업이 완료되어 응답을 받으면 콜백함수가 호출됩니다.

 

(출처:https://poiemaweb.com/es6-promise)

비동기 동작 방식

메인 스레드: JavaScript코드가 실행될 때, 동기적으로 실행되는 작업들은 콜 스택(call stack)에서 처리되고 작업이 완료되면, 콜 스택에서 제거됩니다

 

이벤트 루프: 비동기 작업은 바로 콜 스택에 추가되지 않습니다. 대신 해당 작업은 Web API 또는 C++ API에 의해 처리되고, 완료되면 콜백 함수가 테스크 큐(task queue) 또는 마이크로테스크큐(microtask queue)에 추가됩니다.

 

테스크 큐와 마이크로테스크 큐:

  • 태스크 큐(Task Queue): setTimeout, setInterval, I/O 작업과 같은 비동기 작업의 콜백이 대기하는 공간입니다.
  • 마이크로태스크 큐(Microtask Queue): Promisethen, catch, finally 콜백이나 MutationObserver 콜백 같은 더 높은 우선순위를 가진 작업들이 대기하는 공간입니다.

이벤트 루프의 역할: 이벤트 루프는 콜 스택이 비어 있고 실행할 준비가 되었을 때, 태스크 큐 또는 마이크로태스크 큐에서 대기 중인 작업을 콜 스택으로 이동시킵니다. 마이크로태스크 큐의 작업은 태스크 큐의 작업보다 우선 처리됩니다.

 

비동기 작업 처리 순서

  1. 현재 실행 중인 동기 작업이 콜 스택에서 완료되고 제거됩니다.
  2. 콜 스택이 비어 있으면, 이벤트 루프는 마이크로태스크 큐에서 작업을 콜 스택으로 이동시켜 실행합니다.
  3. 마이크로태스크 큐가 비었을 때, 이벤트 루프는 태스크 큐에서 다음 작업을 콜 스택으로 이동시켜 실행합니다.

이 과정을 통해 JavaScript는 단일 스레드임에도 불구하고 비동기 작업을 효과적으로 관리하고 실행할 수 있습니다. 이러한 메커니즘 덕분에 JavaScript 애플리케이션이 동시성을 가지고 동작할 수 있으며, 사용자 인터페이스를 블로킹하지 않고 부드럽게 작동할 수 있습니다.

 
 
 

 

반응형

java 21이 2023년 9월 19일에 정식으로 출시되었습니다.

 

차기 LTS 버전으로 중요한 변경점이 있는지 확인해보도록 하겠습니다.


1. 자바 컬렉션을 핸들링하기 쉬워졌습니다.

 

이와같이 컬렉션들의 상위 인터페이스로 SequenceCollection, SequencedSet, SequencedMap을 상속받아 사용해서 공통된 기능을 제공합니다.

 

SequencedCollection

sequencedCollection은 처음과 마지막 요소에 대한 공통된 기능을 제공합니다.

reversed()는 기존 컬렉션에서 iterator()나 forEach(), stream()와 같은 기능에서 역순으로 원소들을 처리할 수 있게 됩니다. 

interface SequencedCollection<E> extends Collection<E> {
    //기존 컬렉션에서 역순으로 원소들을 처리할 수 있게 됩니다.
    SequencedCollection<E> reversed();
    //첫번째에 원소를 추가합니다.
    void addFirst(E);
    //마지막에 원소를 추가합니다.
    void addLast(E);
    //첫번째 원소를 가져옵니다.
    E getFirst();
    //마지막 원소를 가져옵니다.
    E getLast();
    //첫번째 원소를 제거합니다.
    E removeFirst();
    //마지막 원소를 제거합니다.
    E removeLast();
}

 

SequencedSet

SequencedSet은 중복된 원소를 갖지 않는 sequencedCollection에 해당하는 set입니다.

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}

 

SequencedMap

SequencedMap은 정해진 순서의 원소에 대한 공통 기능을 제공합니다. 또한 putFirst(K,V)와 putLast(K,V) 메소드는 원소가 이미 존재하는 경우에 적절한 위치로 재배치 되도록 합니다.

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

2. Virtual Thread

가상 쓰레드는 처리량이 많은 동시성 애플리케이션을 개발하고 모니터링하고 유지 및 관리하는데 드는 비용을 획기적으로 줄여줄 경량 쓰레드입니다. 

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

자바는 이를 JVM에서 처리하도록 해서 키워드를 명시해주지 않아도 JVM이 알아서 논블로킹 처리를 해주기 때문에 편리하게 사용할 수 있습니다.

스프링은 3 버전부터 지원해주는 걸로 알고 있습니다. 자세한 내용은 추후에 포스팅해서 정리하도록 하겠습니다.

 

 

3.Record

레코드에 타입 패턴을 함께 적용하여 레코드의 값을 손쉽게 처리할 수 있도록 도와줍니다.

자바 16에서는 instace of 연산자에 타입 패턴을 적용하여 패턴을 매칭시키도록 개선해서 하위 블록에 직접적으로 타입 캐스팅 할 필요가 없어졌습니다.

// Prior to Java 16
if (obj instanceof String) {
    String s = (String)obj;
    ... use s ...
}

// As of Java 16
if (obj instanceof String s) {
    ... use s ...
}

자바 21부터는 레코드 타입에 대해 보다 간편하게 사용할 수 있도록 변경됩니다.

//Java 16
record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}

//Java 21
tatic void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}

위와 같은 패턴 매칭이 개선되어서 기존의 instance of에 if-else문법을 사용하지 않고 스위치 문에서 보다 간편하게 타입여부를 검사할 수 있습니다.

// Prior to Java 21
static String formatter(Object obj) {
    String formatted = "unknown";
    if (obj instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (obj instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (obj instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (obj instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

// As of Java 21
static String formatterPatternSwitch(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> obj.toString();
    };
}

 

4.Null Check

기존에는 파라미터가 null이면 NPE(NullPointerException)을 던지기 때문에, null에 대한 검사를 외부에서 수행했어야 하지만 자바21부터는 null에 해당하는 케이스를 내부에서 검사할 수 있게 되었다

// Prior to Java 21
static void testFooBarOld(String s) {
    if (s == null) {
        System.out.println("Oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

// As of Java 21
static void testFooBarNew(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

 

5.case 세분화

case문은 여러 값에 대한 검사를 필요로 하기 때문에 상당히 복잡한 구조로 만들 수 밖에 없었지만 자바 21에서는 보기 좋게 변경되었습니다. 

// As of Java 21
static void testStringOld(String response) {
    switch (response) {
        case null -> { }
        case String s -> {
            if (s.equalsIgnoreCase("YES"))
                System.out.println("You got it");
            else if (s.equalsIgnoreCase("NO"))
                System.out.println("Shame");
            else
                System.out.println("Sorry?");
        }
    }
}

// As of Java 21
static void testStringNew(String response) {
    switch (response) {
        case null -> { }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

 

6. enum개선

가독성이 좋게 enum이 개선되었습니다.

// As of Java 21
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
    switch (c) {
        case Suit s when s == Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit s when s == Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit s when s == Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit s -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

// As of Java 21
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

그  외

[ JEP 439: Generational ZGC ]

ZGC는 짧은 지연 시간과 높은 확정성을 위해 고안된 GC 알고리즘으로 Java 15부터 프로덕션 환경에서 사용할 수 있게 되었다. 약한 세대 가설(Weak Generational Hypothesis)을 따라 대부분의 객체는 금방 죽기 때문에, 금방 죽는 Young 영역과 오래 살아남는 Old 영역을 분리하여 관리하는 것이 좋다. 그래서 Java 21에서는 이러한 방식을 통해 ZGC의 기능을 확장하여 성능을 개선시키고자 하였다. ZGC와 관련된 자세한 내용은 추후의 별도 포스팅을 통해 살펴볼 예정이다.

 

[ JEP 449: Deprecate the Windows 32-bit x86 Port for Removal ]

이후 릴리스에서 Windows 32bit x86 port를 제거하기 위해, 이를 Deprecate 시켰다. 이제 Windows 32bit 용 빌드를 구성하려고 시도할 때 오류 메세지가 표시된다.

$ bash ./configure
...
checking compilation type... native
configure: error: The Windows 32-bit x86 port is deprecated and may be removed in a future release. \\
Use --enable-deprecated-ports=yes to suppress this error.
configure exiting with result code 1
$

 

[ JEP 451: Prepare to Disallow the Dynamic Loading of Agents ]

자바는 agent를 통해 애플리케이션 코드를 동적으로 변경하도록 지원해왔고, 이를 통해 애플리케이션을 모니터링하고 관찰하는 많은 방법들이 탄생하게 되었다. 대표적으로 Pinpoint와 같은 도구는 자바 에이전트를 기반으로 바이트 코드를 조작하여 모니터링을 돕는 APM 도구이다.

이러한 자바 애플리케이션을 프로파일링하는 정상적인 방법들은 애플리케이션이 실행될 때 불러와지고, 애플리케이션이 실행되는 중간에 불러와지는 경우가 거의 없다. 따라서 자바 21에서는 실행 중인 JVM에 에이전트가 동적으로 로드될 때 경고를 발행시키도록 수정되었다. 물론 JVM 시작 시에 에이전트를 로드하는 것은 경고를 발생시키지 않는다.

 

[ JEP 452: Key Encapsulation Mechanism API ]

Java21에는 공개 키 암호화를 사용하여 대칭 키를 보호하는 암호화 기술인 KEM(Key Encapsulation Mechanism) API가 도입되었다. 기존의 기술은 무작위로 생성된 대칭 키를 공개 키로 암호화하는 것이지만, 패딩이 필요하고 보안을 증명하기 어려울 수 있다. 대신 KEM은 공개 키의 속성을 사용하여 패딩이 필요 없는 관련 대칭 키를 도출한다.

KEM은 양자 공격을 방어하기 위한 핵심 도구가 될 것이다. 다른 보안 제공업체들은 이미 표준 KEM API에 대한 필요성을 표명했고, 자바 역시 이를 공식적으로 도입하기로 결정하였다.

package javax.crypto;

public class DecapsulateException extends GeneralSecurityException;

public final class KEM {

    public static KEM getInstance(String alg)
        throws NoSuchAlgorithmException;
    public static KEM getInstance(String alg, Provider p)
        throws NoSuchAlgorithmException;
    public static KEM getInstance(String alg, String p)
        throws NoSuchAlgorithmException, NoSuchProviderException;

    public static final class Encapsulated {
        public Encapsulated(SecretKey key, byte[] encapsulation, byte[] params);
        public SecretKey key();
        public byte[] encapsulation();
        public byte[] params();
    }

    public static final class Encapsulator {
        String providerName();
        int secretSize();           // Size of the shared secret
        int encapsulationSize();    // Size of the key encapsulation message
        Encapsulated encapsulate();
        Encapsulated encapsulate(int from, int to, String algorithm);
    }

    public Encapsulator newEncapsulator(PublicKey pk)
            throws InvalidKeyException;
    public Encapsulator newEncapsulator(PublicKey pk, SecureRandom sr)
            throws InvalidKeyException;
    public Encapsulator newEncapsulator(PublicKey pk, AlgorithmParameterSpec spec,
                                        SecureRandom sr)
            throws InvalidAlgorithmParameterException, InvalidKeyException;

    public static final class Decapsulator {
        String providerName();
        int secretSize();           // Size of the shared secret
        int encapsulationSize();    // Size of the key encapsulation message
        SecretKey decapsulate(byte[] encapsulation) throws DecapsulateException;
        SecretKey decapsulate(byte[] encapsulation, int from, int to,
                              String algorithm)
                throws DecapsulateException;
    }

    public Decapsulator newDecapsulator(PrivateKey sk)
            throws InvalidKeyException;
    public Decapsulator newDecapsulator(PrivateKey sk, AlgorithmParameterSpec spec)
            throws InvalidAlgorithmParameterException, InvalidKeyException;

}

'컴퓨터 > Java' 카테고리의 다른 글

Java8 / Java 11 차이  (2) 2021.11.07
반응형

Homebrew로 macOS에 MariaDB Server 설치

Contents

  1. MariaDB 업그레이드
  2. 소스로부터 MariaDB Server 빌드
  3. 다른 자료들

Homebrew 패키지 매니저를 이용해서 MariaDB Server를 macOS (이전 Mac OS X) 설치할 수 있습니다.

MariaDB Server는 미리 컴파일된 Homebrew "bottle" 패키지로 이용 가능하며, 소스 빌드가 필요 없어 시간을 절약해줄 수 있습니다.

Homebrew 설치 후에는 MariaDB Server 는 다음과 같이 설치 가능합니다.

- brew install mariadb

 

설치 후에는 다음으로 MariaDB Server를 시작합니다.:

- mysql.server start

 

MariaDB Server 자동 시작하려면 다음과 같이 Homebrew 서비스 기능을 이용할 수 있습니다. (이 서비스 기능은 launchd 의 launchctl 유틸리티를 이용합니다) :

- brew services start mariadb

 

MariaDB Server 시작후에는 사용자 계정으로 로그인하면 됩니다.:

- mysql

 

또는 루트 계정으로 로그인할 수도 있습니다.:

- sudo mysql -u root

MariaDB 업그레이드

우선 brew 를 업데이트 합니다

- brew update

 

그리고, 다음과 같이 MariaDB Server를 업데이트 합니다. :

- brew upgrade mariadb

소스로부터 MariaDB Server 빌드

"bottled" MariaDB Server 패키지는 Homebrew로도 가능하지만 소스로부터 MariaDB를 빌드할 수도 있습니다. 이는 bottle 패키지에 포함되지 않은 다른 버전의 특징적인 기능들을 사용하고자 할때 유용합니다.

bottle 패키지에 포함되지 않은 두 개의 컴퓨넌트들은 CONNECT와 OQGRAPH 엔진인데, 이는 비표준 의존성을 가지고 있기 떄문입니다. 이 엔진으로 MariaDB Server 를 빌드하려면 우선, boost  judy를 설치해야 합니다. 2016년 12월 현재, judy는 Homebrew "boneyard" 단계이나 macOS Sierra 상에서 동작합니다. 의존성을 가진 상태로 서버를 빌드 및 설치하려면 다음 단계를 따라주세요.:

brew install boost homebrew/boneyard/judy brew install mariadb --build-from-source

또한 Homebrew를 이용하여 MariaDB Server의 프리-릴리즈 버전을 빌드 및 설치할 수 있습니다 (예를 들어, MariaDB Server 10.2, MariaDB Server 10.1의 가장 최신의 GA 버전). MariaDB Server 의 "개발" 버전을 빌드 및 설치하려면 다음과 같이 합니다.:

- brew install mariadb --devel

'컴퓨터 > Mac' 카테고리의 다른 글

brew 설치  (3) 2021.11.07

+ Recent posts