하나의 서비스에서 여러 데이터베이스를 동시에 처리해야 하는 경우가 있다.

이때 단일 DataSource 기반의 @Transactional만으로는 여러 데이터베이스 간의 트랜잭션 일관성을 보장하기 어렵다.

예를 들어, 하나의 함수에서 A 데이터베이스 수정 → B 데이터베이스 수정 중 오류 발생

이런 상황에서 A와 B 데이터베이스를 모두 함께 롤백하려면 글로벌 트랜잭션(JTA) 이 필요하다.

JTA가 필요없는 예시:
•회원 정보 수정 → A DB만 사용
•인증 정보 수정 → B DB만 사용

이처럼 상황에 따라 특정 데이터베이스만 접근하는 구조라면, 트랜잭션 매니저를 동적으로 선택하는 방식을 참고하는 것이 더 적절할 수 있다.
(관련 내용은 아래 링크 참고)
https://thornapp.tistory.com/10

 

하나의 DB에서 트랜잭션 매니저를 동적으로 선택하는 방법 (Spring AOP)

배경 및 문제점운영 중인 시스템에서 다음과 같은 상황이 있었다.• 하나의 물리 DB• 특정 역할에 따른 DB 계정 분리• MyBatis + Spring Transaction 사용하지만 Spring의 기본 @Transactional 은→ 트랜잭션

thornapp.tistory.com

 

아키텍처 구성

• Spring Boot
• Atomikos (JTA Transaction Manager)
• Oracle XA DataSource
• MyBatis
• 다중 DB (Primary / Secondary / Third)

java 코드 예제
@Configuration
public class DataSourceConfig {

    /* =====================================================
     * XA DataSource 설정
     *
     * - AtomikosDataSourceBean은 XA 트랜잭션을 지원하는 DataSource
     * - 각 DataSource는 서로 다른 DB를 바라보지만
     * - JTA TransactionManager에 의해 하나의 글로벌 트랜잭션으로 묶인다
     * ===================================================== */

    /**
     * Primary XA DataSource
     * - 기본으로 사용될 DataSource
     * - @Primary로 지정하여 주입 시 우선 선택
     */
    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties("spring.datasource.primary")
    public DataSource primaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    /**
     * Secondary XA DataSource
     */
    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    /**
     * Third XA DataSource
     */
    @Bean(name = "thirdDataSource")
    @ConfigurationProperties("spring.datasource.third")
    public DataSource thirdDataSource() {
        return new AtomikosDataSourceBean();
    }

    /* =====================================================
     * JTA Transaction Manager 설정
     *
     * - 여러 XA DataSource를 하나의 글로벌 트랜잭션으로 관리
     * - @Transactional 하나로 다중 DB 트랜잭션을 제어 가능
     * - 내부적으로 2-Phase Commit(2PC) 수행
     * ===================================================== */

    @Bean
    public PlatformTransactionManager transactionManager() throws SystemException {

        // UserTransaction: 트랜잭션의 시작 / 커밋 / 롤백을 제어
        UserTransactionImp userTx = new UserTransactionImp();
        userTx.setTransactionTimeout(30);

        // Atomikos의 핵심 트랜잭션 매니저
        // 여러 XA Resource(DataSource)를 관리
        UserTransactionManager atomikosTxManager = new UserTransactionManager();
        atomikosTxManager.init();

        // Spring에서 사용하는 JTA 트랜잭션 매니저
        // @Transactional 이 이 매니저를 통해 글로벌 트랜잭션을 제어
        return new JtaTransactionManager(userTx, atomikosTxManager);
    }

    /* =====================================================
     * SqlSessionFactory 설정
     *
     * - 각 DataSource마다 별도의 SqlSessionFactory 생성
     * - 하지만 트랜잭션은 모두 JTA(TransactionManager) 하나로 관리됨
     * ===================================================== */

    @Primary
    @Bean(name = "primarySqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory(
            @Qualifier("primaryDataSource") DataSource dataSource,
            ApplicationContext context) throws Exception {

        return createSqlSessionFactory(dataSource, context);
    }

    @Bean(name = "secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactory(
            @Qualifier("secondaryDataSource") DataSource dataSource,
            ApplicationContext context) throws Exception {

        return createSqlSessionFactory(dataSource, context);
    }

    @Bean(name = "thirdSqlSessionFactory")
    public SqlSessionFactory thirdSqlSessionFactory(
            @Qualifier("thirdDataSource") DataSource dataSource,
            ApplicationContext context) throws Exception {

        return createSqlSessionFactory(dataSource, context);
    }

    /**
     * SqlSessionFactory 공통 생성 메서드
     *
     * - DataSource만 다르고 MyBatis 설정은 동일
     * - 유지보수를 위해 공통 메서드로 분리
     */
    private SqlSessionFactory createSqlSessionFactory(
            DataSource dataSource,
            ApplicationContext context) throws Exception {

        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setConfigLocation(
                context.getResource("classpath:mybatis/mybatis-config.xml"));
        factoryBean.setMapperLocations(
                context.getResources("classpath:mapper/**/*.xml"));
        return factoryBean.getObject();
    }

    /* =====================================================
     * SqlSessionTemplate 설정
     *
     * - MyBatis에서 실제 SQL 실행에 사용되는 객체
     * - 각각 다른 DB를 바라보지만
     * - 트랜잭션은 JTA에 의해 하나로 묶인다
     * ===================================================== */

    @Primary
    @Bean(name = "primarySqlSessionTemplate")
    public SqlSessionTemplate primarySqlSessionTemplate(
            @Qualifier("primarySqlSessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }

    @Bean(name = "secondarySqlSessionTemplate")
    public SqlSessionTemplate secondarySqlSessionTemplate(
            @Qualifier("secondarySqlSessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }

    @Bean(name = "thirdSqlSessionTemplate")
    public SqlSessionTemplate thirdSqlSessionTemplate(
            @Qualifier("thirdSqlSessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }
}
DB 설정 yml 예제
spring:
  datasource:
    primary:
      unique-resource-name: primary-db
      xa-datasource-class-name: oracle.jdbc.xa.client.OracleXADataSource
      xa-properties:
        URL: jdbc:oracle:thin:@...
        user: primary_user
        password: primary_password
      min-pool-size: 1
      max-pool-size: 50
      borrow-connection-timeout: 30
      maintenance-interval: 60
      max-lifetime: 3600
      max-idleTime: 60

    secondary:
      unique-resource-name: secondary-db
      xa-datasource-class-name: oracle.jdbc.xa.client.OracleXADataSource
      xa-properties:
        URL: jdbc:oracle:thin:@...
        user: secondary_user
        password: secondary_password
      min-pool-size: 1
      max-pool-size: 50
      borrow-connection-timeout: 30
      maintenance-interval: 60
      max-lifetime: 3600
      max-idleTime: 60
      
    third:
      unique-resource-name: third-db
      xa-datasource-class-name: oracle.jdbc.xa.client.OracleXADataSource
      xa-properties:
        URL: jdbc:oracle:thin:@...
        user: third_user
        password: third_password
      min-pool-size: 1
      max-pool-size: 50
      borrow-connection-timeout: 30
      maintenance-interval: 60
      max-lifetime: 3600
      max-idleTime: 60

lifetime 등 설정값은 찾아보시고 본인 프로젝트에 맞게 변경하시기 바랍니다.

적용예시
@Transactional
public void process() {
    primaryMapper.insertA();
    secondaryMapper.insertB();
    thirdMapper.insertC();
    // 하나라도 실패하면 전체 롤백
}

 

주의사항

• unique-resource-name 중복 금지
• 커스텀한 로컬 트랜잭션과 혼용해서 사용 X
• 성능 오버헤드 고려
• XA 지원 DB만 가능

JTA방식 VS AOP기반 트랜잭션 동적 선택방식
구분 Atomikos + JTA AOP 기반 트랜잭션 선택
트랜잭션 범위 여러 DB를 하나의 글로벌 트랜잭션으로 묶음 DB별 로컬 트랜잭션
정합성 보장 ✅ 완전 보장 (2PC) ❌ 직접 처리 필요
실패 시 처리 자동 전체 롤백 보상 트랜잭션 필요
구현 난이도 높음 중~낮음
성능 상대적으로 느림 빠름
DB 제약 XA 지원 DB 필요 제약 거의 없음
추천 사용처 금융, 정산, 주문, 결제 조회 위주, 비핵심 데이터

글로벌 서비스를 개발하다 보면, 사용자마다 접속 지역이 달라 시간이 서로 다르게 표시될 수 있습니다.

특히 다국어·다국가 관리자 시스템의 경우, 단순히 브라우저 로컬 시간대로 시간을 보여주는 것은 한계가 있습니다.

예를들어 시스템은 utc 기준이고 java의 Date타입을 그대로 클라이언트로 전송할 경우 클라이언트의 지역 시간대로 자동변환되어 사용자에게 혼란을 줄 수 있습니다.

혼란을 줄수 있는 상황 재현
1. Oracle → Java
•Oracle의 DATE 필드는 Java에서 java.util.Date 또는 LocalDateTime으로 받아 사용
•서버는 기본적으로 UTC 타임존에서 동작하도록 설정

2. Java → JavaScript
•서버는 클라이언트로 시간 정보를 timestamp (long) 형태로 전달 (date.getTime())
•예: 2025-05-29 12:00:00 UTC → 1748510400000

3. JavaScript 클라이언트 처리
const date = new Date(1748510400000);
console.log(date.toString());
•한국(UTC+9): Thu May 29 2025 21:00:00
•인도네시아(UTC+7): Thu May 29 2025 19:00:00

동일한 시간 정보라도, 클라이언트의 시간대 설정에 따라 자동으로 다르게 보입니다.
만약 한국시간으로 설정된 pc를 사용하여 한국사람이 인도네시아 서버를 관리하는 경우 시간이 자동으로 한국시간으로 노출되어 인도네시아에서 접속한 사람과의 혼란이 발생합니다.

문제가 되는 상황
예약 기능, 마감일 등지역에 따라 시각이 달라져 예약 시간 오류 발생
날짜 비교 로직타임존 차이로 하루 차이 등 오작동 발생
날짜 포맷 표시같은 데이터라도 사용자에 따라 시간 표시가 달라져 혼란 초래


이를 해결하기 위해 DB와 서버는 UTC 기준, 사용자 계정에 설정된 국가의 타임존으로 시간 변환, 클라이언트에서는 변환된 문자열을 그대로 표시하는 구조를 설명합니다.

1. DB는 UTC 기준으로 저장

•모든 DATE, TIMESTAMP는 UTC 기준으로 저장
•SYSTIMESTAMP AT TIME ZONE 'UTC' 등을 활용

2. Java 서버에서 시간대 변환

•각 사용자 계정에 맞는 타임존 정보 사용 (예: "Asia/Seoul", "Asia/Jakarta")
•시간 변환 후 String 형식으로 변환하여 반환

ZonedDateTime utcZoned = instant.atZone(ZoneId.of("UTC"));
ZonedDateTime localTime = utcZoned.withZoneSameInstant(ZoneId.of(userTimeZone));
String formatted = localTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

3. 클라이언트에서는 문자열 그대로 표시
<span>2025-05-29 21:00:00</span>

•프론트에서는 JS Date 객체로 다시 변환하지 않음
•시각적으로 오해 없이 “국가 기준 시간”으로 고정됨

주의할 점

•타임존 변경 시 서버에서 매번 ZonedDateTime 변환이 필요
•사용자별 타임존 설정이 누락되지 않도록 유효성 검증 필요
•서버에서 포맷된 문자열은 반드시 일관된 포맷으로 제공 (yyyy-MM-dd HH:mm:ss 등)


다국가, 다시간대 사용자를 지원하는 시스템에서는 단순한 시간 처리로는 문제를 피할 수 없습니다.
“UTC로 저장 → 사용자 기준 타임존으로 변환 → 문자열 반환” 전략을 적용하였지만 사실 다른 방법도 많습니다.

무엇보다 중요한 것은 ‘시간 처리 방식의 통일성’입니다.
여러 계층(클라이언트, 서버, DB)에서 중복되거나 서로 다른 방식으로 시간을 처리하게 되면, 유지보수가 어려워지고 오류 가능성도 커집니다.
하나의 일관된 기준을 정하고, 그에 따라 시간을 저장하고 변환하는 구조를 갖추는 것이 안정적인 시스템 운영의 핵심입니다.

JWT 정의

JWT(JSON Web Token)는 웹 표준에 따른 인증 토큰 방식으로, 주로 사용자 인증 및 정보 전달에 사용된다.

JWT는 서버가 클라이언트의 상태를 저장하지 않고도 인증 정보를 주고받을 수 있게 해주는 방법으로, 특히 RESTful API와 함께 많이 사용된다.

특징

•토큰 자체에 정보를 담고 있어, 서버에 세션을 저장할 필요 없음
•URL, HTTP 헤더 등에 넣기 적합한 짧은 문자열
•사용자 정보나 권한 등을 토큰 안에 포함할 수 있음
•비밀키를 통해 위변조 여부 검증 가능

주의사항

•중요한 정보는 Payload에 넣지말기 (Base64 인코딩일 뿐 암호화 아님)
•서명 검증은 항상 필수
•적절한 만료 시간 설정 필요
•HTTPS 환경에서 사용 권장

JWT 구조

JWT는 3개의 파트 Header, Payload, Signature로 구성되며 각 파트 별 역할은 아래와 같다.

구성 요소 설명 예시
Header 토큰의 타입의 서명에 사용된 해싱 알고리즘을 지정
일반적으로 HS256 또는 RS256 등을 사용함
{
  "alg": "HS256",
  "typ": "JWT"
}
Payload 실제 담고 싶은 정보를 포함하는 부분
{
  "sub": "user123",
  "name": "홍길동",
  "exp": 1718868300
}
Signature 토큰이 위변조되지 않았는지를 검증하기 위한 서명
Header와 Payload를 base64 인코딩 후 비밀 키를 이용해 서명
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

 
아래는 0.12.6 버전을 예시로 작성한 java 코드입니다.

의존성 추가 maven기준
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.12.6</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.12.6</version>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId> <!-- 또는 jjwt-gson 중 택1 -->
  <version>0.12.6</version>
  <scope>runtime</scope>
</dependency>

https://mvnrepository.com/ maven 관련 참고.

java 코드, 생성과 검증
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

public class JwtValidator {

    // 256비트(32바이트) 이상의 시크릿 키
    private static final String SECRET = "your-256-bit-secret-your-256-bit-secret";
    private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));

    public static void main(String[] args) {
        String jwt = generateToken("testUser");
        System.out.println("생성된 JWT: " + jwt);

        Claims claims = validateToken(jwt);
        if (claims != null) {
            System.out.println("검증 성공! Subject: " + claims.getSubject());
            System.out.println("만료 시간: " + claims.getExpiration());
        }
    }

    // JWT 생성
    public static String generateToken(String subject) {
        long now = System.currentTimeMillis();
        return Jwts.builder()
                .subject(subject)
                .issuedAt(new Date(now))
                .expiration(new Date(now + 1000 * 60 * 60 * 24)) // 하루 뒤 만료
                .signWith(KEY, Jwts.SIG.HS256)
                .compact();
    }

    // JWT 검증
    public static Claims validateToken(String token) {
        try {
            JwtParser parser = Jwts.parser()
                    .verifyWith(KEY)
                    .build();

            return parser.parseSignedClaims(token).getPayload();

        } catch (ExpiredJwtException e) {
            System.out.println("JWT 만료됨: " + e.getMessage());
        } catch (UnsupportedJwtException e) {
            System.out.println("지원되지 않는 JWT: " + e.getMessage());
        } catch (MalformedJwtException e) {
            System.out.println("잘못된 형식의 JWT: " + e.getMessage());
        } catch (SecurityException e) {
            System.out.println("서명 검증 실패: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("JWT 검증 중 오류: " + e.getMessage());
        }
        return null;
    }
}


JWT 는 토큰이 발급자에 의해 만들어졌고, 중간에 변조되지 않았음을 보장한다, 하지만 JWT 만으로는 사용자 인증을 완전히 보장하지 않으므로 JWT를 발급받을때의 보안절차 + JWT 유효기간 관리를 하여야 한다.

Java에서 문자열을 이어붙이는 방법은 여러 가지가 있지만, 가장 흔히 쓰이는 방식은 다음 두 가지가 있다.
1. StringBuilder.append() 사용
2. 문자열 덧셈 연산자 + 사용

두 방식 모두 동일한 결과를 만들어내지만, 성능과 내부 처리 방식에서는 큰 차이가 있다. 차이점과 어떤 상황에서 어떤 방식이 더 효율적인지 정리해보려 한다.

문자열은 불변(Immutable)이다

Java의 String 클래스는 불변(immutable)이다.
즉, 한 번 생성된 문자열은 수정될 수 없고, 문자열을 더할 경우 항상 새로운 String 객체가 생성된다.

String str = "Hello";
str = str + " World"; // 새로운 String 객체가 생성됨

변수에 문자열을 덧붙이면 기존 문자열은 버려지고 새로운 객체가 만들어지기 때문에,
반복적으로 수행하면 성능 저하와 메모리 낭비가 발생됨

하지만, 컴파일 타임 상수일 경우는 예외
String str = "Hello" + " World"; // 컴파일 타임에 "Hello World"로 합쳐짐

컴파일러는 리터럴 상수끼리의 문자열 덧셈은 컴파일 시점에 미리 계산하여 단일 문자열 리터럴로 처리함.
이 경우에는 불필요한 객체 생성이 발생하지 않음

StringBuilder.append() 방식은 객체 새로 만들지 않음
StringBuilder sb = new StringBuilder();
sb.append("Hello, ");
sb.append("world!");
sb.append(" Have a nice day!");

내부적으로 가변 배열을 사용하여 객체를 새로 만들지 않고 문자열을 추가.

문자열 덧셈 vs StringBuilder.append() 비교표
코드 간결성✅ 매우 간단하고 직관적임❌ 다소 길고 복잡함
가독성✅ 좋음❌ 비교적 낮음
컴파일 타임 최적화✅ 상수끼리는 최적화됨❌ 해당 없음
성능 (반복문 내부)❌ 느림 (객체 계속 생성됨)✅ 빠름 (하나의 객체 사용)
메모리 효율❌ 낮음 (불필요한 객체 생성)✅ 높음 (재사용 가능)
불변성(Immutable)✅ String은 불변이라 안전함❌ StringBuilder는 가변 객체
멀티스레드 안전성✅ 불변이므로 안전함❌ thread-safe 아님 (필요시 StringBuffer)
주 사용 용도간단한 문자열 조합ok대량 문자열 처리, 반복문

JWT에 대해 사용해보게 되었는데 JWT에서 시그니처 부분을 암호화하기 위한 방식으로 HMAC 암호화를 사용하였습니다. 그래서 정의 및 예제코드를 작성하여 정리하였습니다.

HMAC이란
HMAC(Hash-based Message Authentication Code)은
메시지의 **무결성(Integrity)**과 **인증(Authentication)**을 보장하기 위한 암호화 방식

•무결성: 메시지가 중간에 변조되지 않았음을 확인
•인증: 메시지가 신뢰할 수 있는 주체로부터 왔음을 확인

동작원리
1. 비밀 키 (Secret Key)
2. 해시 알고리즘 (예: SHA-256)

이 두 가지를 조합하여 고정된 길이의 해시값(MAC)을 생성하여
수신자가 동일한 키로 다시 계산하여 데이터 변조 여부를 검증

검증 시 동일한 키를 사용하여야 되므로 대칭키 방식이며 해시 함수가 충돌을 방지하여 안정성이 확보됨.

코드예제
import javax.annotation.PostConstruct;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import lombok.Synchronized;
import org.apache.commons.codec.binary.Base64;

public class SecurityUtil {

    // 대칭 키: 실제 운영에서는 안전한 난수 값으로 대체해야 합니다.
    private static final String SYMMETRIC_KEY = "symmetericKey";

    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
    private static Mac mac;

    /**
     * 클래스 초기화 시 HMAC 해시 객체를 한 번만 생성하고 초기화합니다.
     */
    @PostConstruct
    public void init() throws Exception {
        mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
        mac.init(new SecretKeySpec(SYMMETRIC_KEY.getBytes("utf-8"), HMAC_SHA256_ALGORITHM));
    }

    /**
     * 메시지를 HMAC-SHA256 방식으로 해싱하고 Base64로 인코딩합니다.
     * 동기화로 멀티스레드 환경에서도 안전하게 동작합니다.
     */
    @Synchronized
    public static String encodeHMacSha256(final String message) {
        byte[] hash = mac.doFinal(message.getBytes());
        return Base64.encodeBase64String(hash);
    }
}

•@PostConstruct는 Bean 생성 후 호출되어 Mac 객체를 한 번만 초기화
•Mac 객체는 스레드에 안전하지 않기 때문에 @Synchronized로 doFinal() 호출을 보호합니다.
•실제 운영 환경에서는 SYMMETRIC_KEY를 고정된 문자열이 아니라 안전하게 관리되는 키 저장소 또는 환경변수로부터 주입받는 것이 좋음

Syncronized 사용이유
Mac 객체는 내부 상태를 갖고 있어서 doFinal()이 호출되면 상태가 변경된다.
여러 스레드가 동시에 doFinal()을 호출할 경우 충돌이 발생하거나 예외가 발생할 수 있으므로, 반드시 동기화 처리가 필요.


참고사항
https://docs.oracle.com/javase/8/docs/api/javax/crypto/Mac.html

Mac (Java Platform SE 8 )

Finishes the MAC operation. A call to this method resets this Mac object to the state it was in when previously initialized via a call to init(Key) or init(Key, AlgorithmParameterSpec). That is, the object is reset and available to generate another MAC fro

docs.oracle.com

java 8버전 기준으로 doFinal 에 대한 설명을 보면 같은 키에 대해서는 여러번 재사용이 가능하다고 명시되어 있어 재사용 하였고, doFinal 사용 시 내부 상태가 변경되기 때문에 동기화 처리를 추가하였음.

http 통신 결과 response 값을 봤을때

... -> …
" -> â
® -> ®
• -> •
위와같이 이상한 문자로 오는것을 확인하였습니다
https://javadevhub.com/reading-utf-8-encoded-files-with-java/

위 링크를 확인해보니


이 내용을 확인할 수 있는데요 utf8을 적용하면
해결되는 문제였습니다

EntityUtils.toString( httpResponse.getEntity(), "UTF-8")
을 사용하여 받은 문자에 utf8을 적용해주시면 됩니다.



'java > 오류' 카테고리의 다른 글

자바(java)data isn't an object id (tag = 48) 에러 해결방법  (0) 2025.05.16

ssl인증서를 담은 http통신을 하기 위해 Keystore.load 를 호출 하는 과정에서
특정 ssl 인증서를 접근하는 경우에 저런 에러( data isn't an object id (tag = 48) )가 나왔는데요

p12 확장자 형식의 인증서인 경우 jdk 8 버전에서 인증서 열 때 오류가 있다고 합니다.
하지만 회사나 대규모의 프로젝트를 운영하는 도중에 자바 버전을 바꾸기에는
리스크가 너무 크기때문에 자바버전을 바꿀수 없어서 인증서를 바꾸는 방식을 채택하였습니다.

해결 방법은 다음과 같습니다.

1. jdk 버전을 p12 인증서 load가 가능한 버전으로 uprade
2. 1번을 할 상황이 안되는 경우 인증서 확장자를 변환. 방법은 아래와 같습니다
keytool -importkeystore -srckeystore (변경전 인증서).p12 -srcstoretype pkcs12 -destkeystore (변경할 인증서).jks -deststoretype jks

2번 방법 사용하실 경우 현재 pc의 java 버전도 프로젝트의 자바버전과 동일해서 변경되지 않을 가능성이 있습니다.
변경되지 않으신 경우 jdk 다운로드 후 환경변수 설정을 통해 자바 버전 바꾸시고 keytool 명령어를 사용하셔야 됩니다.

하단 링크를 통해 jdk 17또는 19버전 다운로드 받으신 후 재시도 해주세요
https://www.oracle.com/kr/java/technologies/downloads/#java19

또한 p12인증서로 개발진행하신 경우 KeyStore.getInstance("pkcs12")로 인증서저장소를 불러오셨을 텐데
jks인증서 형식으로 바꿨기 때문에 KeyStore.getInstance("JKS")로 바꾸셔야 됩니다.



'java > 오류' 카테고리의 다른 글

java 이상한 깨진 문자(… â ® •) 해결방법  (0) 2025.05.16

+ Recent posts