리플렉션 어노테이션 방식 (이전글 2.2 참조)로 
mongoDB 필드를 암호화하면, mongoTemplate.save에는 적용되지만, mongoTemplate.update에는 반영되지 않는다.

이걸 우회하는 방법으로 새로운 암호화전용 mongoTemplate을 만들 수 있는데

@Component
public class EncryptingMongoTemplate {

    private final MongoTemplate mongoTemplate;
    private final IpoCreditMasterService cryptoService;

    // 캐시: 엔티티별 암호화 필드명 목록
    private final ConcurrentMap<Class<?>, Set<String>> encryptedFieldCache = new ConcurrentHashMap<>();

    public EncryptingMongoTemplate(MongoTemplate mongoTemplate,
                                   IpoCreditMasterService cryptoService) {
        this.mongoTemplate = mongoTemplate;
        this.cryptoService = cryptoService;
    }

    // === 외부에서 이 메서드들을 써서 update === //

    public UpdateResult updateFirst(Query query, Update update, Class<?> entityClass) {
        encryptUpdateFields(entityClass, update);
        return mongoTemplate.updateFirst(query, update, entityClass);
    }

    public UpdateResult updateMulti(Query query, Update update, Class<?> entityClass) {
        encryptUpdateFields(entityClass, update);
        return mongoTemplate.updateMulti(query, update, entityClass);
    }

    public UpdateResult upsert(Query query, Update update, Class<?> entityClass) {
        encryptUpdateFields(entityClass, update);
        return mongoTemplate.upsert(query, update, entityClass);
    }

    // 필요하면 find, save 등도 래핑해서 여기에 모아줄 수 있음

    // === 핵심: Update 안의 @EncryptedOrNotText 필드 값 암호화 === //

    private void encryptUpdateFields(Class<?> entityClass, Update update) {
        if (update == null) return;

        Set<String> encryptedFields = getEncryptedFieldNames(entityClass);
        if (encryptedFields.isEmpty()) return;

        // Spring Data Mongo의 Update는 내부에 Document를 가지고 있음
        Document root = update.getUpdateObject();

        // 여기서는 .set()만 쓴다고 하셨으니 $set만 처리
        Document setDoc = (Document) root.get("$set");
        if (setDoc == null) {
            return;
        }

        for (String fieldName : encryptedFields) {
            if (!setDoc.containsKey(fieldName)) continue;

            Object raw = setDoc.get(fieldName);
            if (raw instanceof String str) {
                setDoc.put(fieldName, cryptoService.encrypt(str));
            }
        }
    }

    private Set<String> getEncryptedFieldNames(Class<?> entityClass) {
        return encryptedFieldCache.computeIfAbsent(entityClass, this::scanEncryptedFields);
    }

    private Set<String> scanEncryptedFields(Class<?> type) {
        Set<String> result = new HashSet<>();
        Class<?> current = type;
        while (current != null && current != Object.class) {
            for (Field field : current.getDeclaredFields()) {
                if (field.isAnnotationPresent(EncryptedOrNotText.class)
                        && field.getType() == String.class) {
                    // @Field("otherName") 고려 안 한 버전
                    result.add(field.getName());
                }
            }
            current = current.getSuperclass();
        }
        return result;
    }
}

 

 

사용예제:

public class UserService {

    private final EncryptingMongoTemplate encryptMt;

    public void updateUserPhone(String userId, String newPhone) {
        Query q = Query.query(Criteria.where("_id").is(userId));
        Update u = new Update()
                .set("phone", newPhone); // phone이 @EncryptedOrNotText

        encryptMt.updateFirst(q, u, UserEntity.class);
        // → 호출 전에 EncryptingMongoTemplate이 newPhone을 encrypt해서 MongoDB에 저장
    }
}
Posted by yongary
,

1. Postgres + JPA: AttributeConverter 예시 (OK)

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {

    @Override
    public String convertToDatabaseColumn(String attribute) {
        if (attribute == null) return null;
        return IpoCreditMasterService.encrypt(attribute);
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        if (dbData == null) return null;
        try {
            return IpoCreditMasterService.decrypt(dbData);
        } catch (Exception e) {
            // 섞인 데이터 방어
            return dbData;
        }
    }
}

 

 

@Entity
public class EncTest {

    @Id
    private String 계약계정;

    private int yymm;

    @Convert(converter = EncryptedStringConverter.class)
    private String 고객이름;
}

 

 

2. mongoDB 에는 위 방식은 적용안되고, 유사방식 추천: Mongo용 Converter 두 개

(이것도 필요없음: 아래 2-2 방식이 더 좋음)

import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;

@WritingConverter
public class EncryptedStringWritingConverter implements Converter<String, String> {
    @Override
    public String convert(String source) {
        if (source == null) return null;
        return CryptoUtil.encrypt(source);
    }
}

@ReadingConverter
public class EncryptedStringReadingConverter implements Converter<String, String> {
    @Override
    public String convert(String source) {
        if (source == null) return null;
        try {
            return CryptoUtil.decrypt(source);
        } catch (Exception e) {
            // 섞인 데이터 방어
            return source;
        }
    }
}


2-1 mongoDB 설정 변경

(이건 전체 설정용이므로, 필드 설정에선 필요없음)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;

import java.util.List;

//@Configuration
//public class MongoConfig {

//    @Bean
//    public MongoCustomConversions mongoCustomConversions() {
        return new MongoCustomConversions(List.of(
                new EncryptedStringWritingConverter(),
                new EncryptedStringReadingConverter()
        ));
    }
//}

 

2-2 위는 모든 필드 적용이므로, mongoDB 특정필드에만 적용 방식
  (A: 어노테이션 하나 추가)

package com.vwsai.project2.entity.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)          // 필드에만 붙이기
@Retention(RetentionPolicy.RUNTIME) // 런타임에 리플렉션으로 읽어야 하니까 RUNTIME
public @interface EncryptedOrNotText {
}


  (B: 리스터 구현)

import com.vwsai.project2.entity.annotation.EncryptedOrNotText;
// import com.vwsai.project2.entity.annotation.EncryptedText;  // 이건 더 안 쓸 거면 삭제

@Component
@Slf4j
public class EncryptionMongoEventListener extends AbstractMongoEventListener<Object> {

    private final IpoCreditMasterService cryptoService;

    public EncryptionMongoEventListener(IpoCreditMasterService cryptoService) {
        this.cryptoService = cryptoService;
    }

    @Override
    public void onBeforeConvert(BeforeConvertEvent<Object> event) {
        processFields(event.getSource(), true);
    }

    @Override
    public void onAfterConvert(AfterConvertEvent<Object> event) {
        processFields(event.getSource(), false);
    }

    private void processFields(Object entity, boolean encrypt) {
        if (entity == null) return;

        Class<?> current = entity.getClass();
        while (current != null && current != Object.class) {
            for (Field field : current.getDeclaredFields()) {

                // 🔽 여기 어노테이션 타입만 바꿔주면 됨
                if (!field.isAnnotationPresent(EncryptedOrNotText.class)) {
                    continue;
                }
                if (field.getType() != String.class) {
                    continue;
                }

                field.setAccessible(true);
                try {
                    String value = (String) field.get(entity);
                    if (value == null || value.isEmpty()) {
                        continue;
                    }

                    String processed;
                    if (encrypt) {
                        processed = cryptoService.encrypt(value);
                    } else {
                        try {
                            processed = cryptoService.decrypt(value);
                        } catch (Exception e) {
                            log.error("복호화 실패: field={}, value={}", field.getName(), value);
                            processed = value; // 섞인 데이터 방어
                        }
                    }
                    field.set(entity, processed);
                } catch (Exception e) {
                    log.error("암/복호화 처리 실패: field={}", field.getName(), e);
                }
            }
            current = current.getSuperclass();
        }
    }
}

 

Posted by yongary
,

public String decryptOrPlain(AWSKMS kms, String input) {

    // 1) base64 decode 먼저 시도 (빠르고 로컬이므로 비용 없음)
    byte[] decoded;
    try {
        decoded = Base64.getDecoder().decode(input);
    } catch (IllegalArgumentException e) {
        // Base64가 아님 → 평문
        return input;
    }

    // 2) KMS 복호화 시도
    try {
        DecryptRequest request = new DecryptRequest()
                .withCiphertextBlob(ByteBuffer.wrap(decoded));
        ByteBuffer plainBytes = kms.decrypt(request).getPlaintext();
        return StandardCharsets.UTF_8.decode(plainBytes).toString();

    } catch (cohttp://m.amazonaws.services.kms.model.InvalidCiphertextException e) {
        // "이건 암호문이 아니다" 라고 AWS가 확정해주는 예외
        return input;

    } catch (Exception e) {
        // 나머지는 실제 장애 → 평문으로 오인하면 절대 안 됨
        throw new RuntimeException("KMS decrypt failed", e);
    }
}

Posted by yongary
,

현재 log_statment 조회:
psql에서(아무 db에서) 
> SHOW log_statement;
==> 보통은 이것만 세팅하면 됨. 
 (이거 세팅 하면 log_min_duration_statement값에 상관없이 다 찍힘)


🔹 pgaudit 없이 “누가 어떤 query 실행했는지” 남기는 log_statement 최소 세팅

핵심은 두 가지야:

  1. 쿼리를 로그로 남기고(log_statement / log_min_duration_statement)
  2. 로그에 유저명을 찍게 만드는 것(log_line_prefix)

1) RDS 파라미터 그룹에서 바꿀 것

RDS > Parameter groups에서 해당 Postgres 파라미터 그룹 편집해서 대략 이렇게:

 
-- 필수
log_statement = 'none'          -- 또는 'mod' / 'ddl' / 'all' 중 선택
log_min_duration_statement = 0  -- 또는 N ms (느린 쿼리만)

-- 유저/DB 정보 찍기 위해
log_line_prefix = '%t [%p] %u %d %h '
  • %u : 유저명 (누가)
  • %d : DB명
  • %h : 클라이언트 호스트(IP)
  • %t : 시간
  • %p : 프로세스 ID

“정말 다 보고 싶다”면

log_statement = 'all'
log_min_duration_statement = -1
 

모든 쿼리가 “유저 + 쿼리 내용” 형태로 CloudWatch/로그에 다 찍힘.
(트래픽 많으면 로그 엄청 늘어나는 건 주의!)

“쓰기류만 보고 싶다”면 (실무에서 많이 씀)

 
log_statement = 'mod'           -- INSERT/UPDATE/DELETE만
log_min_duration_statement = -1

→ SELECT는 안 찍고, 데이터 변경 쿼리만 유저와 함께 기록.


🔹 그럼 로그는 어떻게 생기냐?

설정이 잘 되면 RDS 로그/CloudWatch에 이런 식으로 찍혀:

 
2025-12-04 10:23:45 [12345] appuser mydb 10.0.0.12 LOG:  statement: SELECT * FROM orders WHERE id = 1;
2025-12-04 10:24:01 [12346] admin   mydb 10.0.0.13 LOG:  statement: UPDATE users SET ...;

이 한 줄만으로:

  • 누가(appuser/admin)
  • 어느 DB에서(mydb)
  • 어디서(10.0.0.12)
  • 어떤 SQL을(SELECT/UPDATE …)

까지 다 파악 가능.


🔹 pgaudit vs 기본 로그 비교

항목                                기본 로그 (log_statement 등)                          pgaudit
설정 난이도 비교적 간단 조금 더 복잡 (확장 설치/설정)
볼 수 있는 내용 쿼리 텍스트 + 유저 쿼리 + 권한/객체 단위 Audit 정보
용도 “누가 어떤 SQL 날렸냐” 수준 컴플라이언스/보안 감사용(정교한 audit)

지금 말한 것처럼

“누가 어떤 query를 날렸는지만”
알고 싶은 거라면 기본 로그 설정만으로 충분하고, 훨씬 간단해.


🔚 요약

  • 정말 간단하게 하고 싶다 → pgaudit 말고 log_statement + log_line_prefix로 해결
  • 최소 세팅 예:
     
  • RDS 파라미터 그룹 변경 후, 인스턴스 Reboot 한 번 해주면 적용됨.
log_line_prefix = '%t [%p] %u %d %h '
log_statement = 'all'  -- or 'mod'
log_min_duration_statement = -1

원하면

  • “운영 서비스에 최소 영향만 주는 추천값”
  • “CloudWatch에서 해당 유저만 필터링해서 보는 법” 은 추가 필요.

    [변경방법]
    1. 데이터베이스 선택 후 아래->구성 탭에서 확인하고

    2. 데이터베이스 수정을 눌러 아래 그림에서 파라미터 변경하면 됨.

    3. 그 후에 rds instance REBOOT: (1~2분 서비스 중단) 이 필요함.

Posted by yongary
,

centos7에 설치/운영되고 있는 mongoDB 5를 mongoDB 7로 업그레이드 하기 위해서는
2번의 업그레이가 필요. 
(mongoDB 7.0.26 으로 설치해도 7.0.26-1 이 설치됨: 이버전으로 업그레이드 목표)


0. 공통 사전 준비

버전/구성 파악
FCV가 "version" : "5.0" 인지 확인 (5.0.x에서 6.0으로 올릴 때 전제조건)MongoDB


mongosh --eval 'db.version()' mongosh --eval 'rs.status()' mongosh --eval 'db.adminCommand({getParameter:1, featureCompatibilityVersion:1})'


백업 필수

각 노드에서 mongodump 로 전체 백업 (tools는 CentOS 7 지원)MongoDB

mongodump --host <primary-host>:27017 --authenticationDatabase admin \ -u <user> -p<password> --out /backup/mongo_$(date +%F)


드라이버/애플리케이션 호환성 확인
사용하는 드라이버가 MongoDB 6.0, 7.0을 지원하는지 체크 (공식 드라이버 호환 테이블 참고)MongoDB
MongoDB 공식 yum repo 확인
현재 /etc/yum.repos.d/mongodb-org-5.0.repo (혹은 비슷한 이름) 있을 것.
6.0 / 7.0 업그레이드 시 이 repo 파일을 버전별로 교체하거나, baseurl의 5.0 부분을 6.0 / 7.0 으로 바꾸는 식으로 진행.
예시(6.0용 repo, CentOS 7):

[mongodb-org-6.0] name=MongoDB Repository baseurl=https://repo.mongodb.org/yum/redhat/7/mongodb-org/6.0/x86_64/ gpgcheck=1 enabled=1 gpgkey=https://www.mongodb.org/static/pgp/server-6.0.asc

 

1단계: 5 -> 6

1단계: MongoDB 5.0.23 → 6.0.x (Replica Set 기준)

MongoDB 공식 문서에서도 5.0 → 6.0 업그레이드는 replica set의 각 멤버를 5.0에서 6.0으로 순차 업그레이드하는 방식으로 안내합니다.MongoDB+1

1-1. 세컨더리 업그레이드 (각각 순서대로)

각 세컨더리에서 아래를 한 대씩 수행합니다.

  1. 해당 노드 상태 확인
    mongosh --host <secondary-host> --eval 'rs.isMaster()' # "secondary" 인지 확인
  2. 서비스 중지
    sudo systemctl stop mongod

  3. yum repo를 6.0용으로 변경
  4. $sudo yum clean all
    $sudo yum makecache
    이게 안되면..
    (1.centOs 를 기본으로 바꾸고)
    sudo sed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-*.repo
    sudo sed -i 's|#baseurl=http://mirror.centos.org/centos/$releasever|baseurl=http://vault.centos.org/7.9.2009|g' /etc/yum.repos.d/CentOS-*.repo


    (2.ius 업데이트 차단)
    sudo sed -i 's/enabled *= *1/enabled=0/g' /etc/yum.repos.d/ius*.repo  후에

    sudo yum --disablerepo="*" --enablerepo="mongodb-org-6.0" makecache

    MongoDB 6.0으로 패키지 업그레이드

    $ sudo yum update -y mongodb-org-6.0.24 mongodb-org-server-6.0.24 mongodb-org-shell-6.0.24 mongodb-org-mongos-6.0.24 mongodb-org-tools-6.0.24

    안되면 

    //$sudo yum install -y mongodb-org
    # 또는
    //$ sudo yum update -y mongodb-org mongodb-org-server mongodb-org-shell mongodb-org-mongos mongodb-org-tools

    (혹시 gpg key= 패키지 키 설치에러면)
    $ sudo rpm --import https://pgp.mongodb.com/server-6.0.asc
    후에
    $ sudo rpm -qi gpg-pubkey-64c3c388-* 로 확인

    sudo yum --disablerepo="*" --enablerepo="mongodb-org-6.0" makecache 

    ==> 있으면 다시 yum update 
    sudo yum --disablerepo="*" --enablerepo="mongodb-org-6.0" update -y mongodb-org-6.0.24 mongodb-org-server-6.0.24 mongodb-org-shell-6.0.24 mongodb-org-mongos-6.0.24 mongodb-org-tools-6.0.24

  5.  서비스 재시작 & 상태 확인
    (mongod --version 으로 확인해보고)

    sudo systemctl start mongod sudo systemctl status mongod
  6. Replica Set에서 정상 합류 확인
     
    mongosh --host <primary-host> --eval 'rs.status()'
    • 해당 노드가 SECONDARY 상태로 돌아오면 다음 세컨더리로 넘어갑니다.
    • 세컨더리 하나 업그레이드가 완전히 끝난 뒤 다음 세컨더리 진행 (동시에 하지 말 것).
  7. Primary에서:
    mongosh --host <primary-host> --eval 'rs.status()'
    해당 노드가 SECONDARY 상태로 돌아오면 다음 세컨더리로 넘어갑니다.

    세컨더리 하나 업그레이드가 완전히 끝난 뒤 다음 세컨더리 진행 (동시에 하지 말 것).
sudo sed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-*.repo
sudo sed -i 's|#baseurl=http://mirror.centos.org/centos/$releasever|baseurl=http://vault.centos.org/7.9.2009|g' /etc/yum.repos.d/CentOS-*.repo

1-2. Primary 업그레이드

  1. Primary를 강제로 Step Down
    • 다른 노드가 자동으로 Primary가 됩니다.
       
      mongosh --host <primary-host> rs.stepDown(60) // 60초 동안 재선출 허용
  2. 원래 Primary 노드에서 mongod 중지
    sudo systemctl stop mongod
  3. 해당 노드도 repo를 6.0용으로 변경 & 패키지 업그레이드
     
    sudo yum clean all sudo yum makecache sudo yum install -y mongodb-org
  4. 서비스 시작
    sudo systemctl start mongod
  5. Replica Set 전체 버전 확인
     
    Primary(새로 선출된 노드)에서:
mongosh
rs.status()
db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })
  • 모든 멤버의 mongod --version이 6.0.x 인지 확인.


1-3. FCV를 6.0으로 올리기

MongoDB는 메이저 업그레이드 시 바이너리 버전 → FCV 순서로 올리라고 합니다.MongoDB+1

  1. Primary에서 실행:
    db.adminCommand({ setFeatureCompatibilityVersion: "6.0" })
  2. 다시 확인:
    db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 }) # "version" : "6.0" 이어야 함
  3. 애플리케이션 기능 테스트, 로그 에러 여부 확인.

 

 

2단계: MongoDB 6.0.x → 7.0.x 업그레이드

MongoDB 7.0 업그레이드 문서도 동일한 패턴(세컨더리 → Primary, FCV 설정)을 안내합니다.MongoDB+1

2-1. 사전 확인

  1. 7.0 호환성 변경 사항 검토
    • 인덱스, 제거된 옵션, 암호화 컬렉션, FCV 다운그레이드 제한 등.MongoDB+1
  2. 드라이버가 7.0 지원하는지 재확인
  3. FCV가 현재 6.0인지 확인
    db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })

2-2. 세컨더리부터 7.0으로 업그레이드

각 세컨더리에서 다시 한 대씩 아래 수행:

  1. 상태 확인 (SECONDARY인지)
  2. mongod 중지
    sudo systemctl stop mongod
  3. yum repo를 7.0용으로 변경
     
    [mongodb-org-7.0]
    name=MongoDB Repository
    baseurl=https://repo.mongodb.org/yum/redhat/7/mongodb-org/7.0/x86_64/
    gpgcheck=1
    enabled=1
    gpgkey=https://www.mongodb.org/static/pgp/server-7.0.asc
  4. 패키지 업그레이드
     
    sudo yum clean all sudo yum makecache sudo yum install -y mongodb-org
  5. 서비스 시작 & 상태 확인 (rs.status()로 SECONDARY 복귀 확인)


  6. secondary 한대 추가될때마다 application 에 IP 앞쪽에 추가/뒤쪽에 삭제 해서 배포
    (단, 다른글 - add/remove글 참조)

2-3. Primary 업그레이드

  1. Primary에서 step down
     
    mongosh --host <primary-host> rs.stepDown(60)
  2. 해당 노드에서 mongod 중지 → repo 7.0으로 변경 → 패키지 업그레이드 → mongod 시작
  3. 전체 상태 점검
    rs.status() # 각 노드의 mongo --version 도 7.0.x 인지 확인

2-4. FCV를 7.0으로 올리기 (주의)

MongoDB 7.0에서는 FCV를 올린 뒤에는 다운그레이드가 훨씬 까다롭다고 명시합니다.MongoDB+1

  1. 충분히 테스트한 뒤, Primary에서:
     
    db.adminCommand({ setFeatureCompatibilityVersion: "7.0" })
  2. 확인:
     
    db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 }) # "version" : "7.0"
  3. 모든 애플리케이션 기능/쿼리 테스트, 로그 모니터링.

3. CentOS 7에서의 현실적인 권장 사항

  • MongoDB는 RHEL/CentOS 7/8/9를 지원 리스트에 올리고 있지만MongoDB
  • CentOS 7 자체가 2024-06-30 EOL이라 보안 패치가 더 이상 안 나옵니다.
  • 운영 환경이면,
    • **새 서버(RHEL 8/9, Rocky 8/9, Alma 8/9, Amazon Linux 2023 등)**에 MongoDB 7.0 새로 설치
    • 기존 5.0.23 클러스터에서 mongodump → 새 클러스터로 mongorestore 또는
    • Replica Set을 새 서버로 확장(add member) 후, 구 서버 멤버를 하나씩 제거하는 방식으로 롤링 마이그레이션
      같은 OS 마이그레이션 + 버전 업그레이드 계획을 같이 가져가는 걸 개인적으로 가장 추천합니다.
Posted by yongary
,

1. rs.status() 로 state=2 (secondary), health=1  이면 sync가 완료된 것임.

2. 한대 추가 후, 삭제 하기

rs.add("NEW_IP:27017")

js
코드 복사
rs.status()
🔎 state: 2 = Secondary 가 될 때까지 기다리기 (sync 시간 필요)


rs.remove("OLD_IP:27017")


2-1: 중요 - application 배포 하기. (바뀐 IP 적용해서 배포하기)
위 스텝 2번 반복.

 

중요: 마지막 primary 변경전에는 primary를 다른 2번서버로 옮기기.

cfg = rs.conf()
cfg.members[0].priority = 0    // 기존 Primary
cfg.members[1].priority = 2    // 새로 Primary가 되길 원하는 노드
rs.reconfig(cfg)



3. primary는 secondary처럼  conf change하고시기고
     application 배포 하기. (바뀐 IP 적용해서 배포하기)


그 후 primary 변경 후
마지막 application 배포 하기.

Posted by yongary
,

부제: mongo-keyfile 을 설정해서 mongoDB를 restart해도 auth를 활성화 하지 않은 상태로 유지하기


0. 아래 1번부터 하면 좋지만, keyfile이 없는 서버와 있는서버간 통신이 안되는 문제가 있으므로
- security:
  keyFile: /etc/mongo/mongo-keyfile
  transitionToAuth: true
  authorization: disabled

로 설정해서 3,2,1 순서로 restart하고

다되면 , mongoDB user추가 및 application 소스에 반영한 후에

# transitionToAuth: true 로 코멘트 하고, 3,2,1 다시 restart


1. keyFile = “노드끼리 비밀 합의용 패스워드”

keyFile은 원래 replica set / sharded cluster 노드끼리 서로 인증하는 용도입니다.

  • security.keyFile: /etc/mongo-keyfile 만 설정하고
  • security.authorization: disabled 로 두면:

👉 결과

  • 서버끼리(replica set 멤버 간):
    • keyFile을 사용해서 internal authentication을 합니다.
  • 클라이언트(SPRING, mongo shell 등):
    • 여전히 user/password 없이 접속 가능
    • 기존처럼 mongosh --host ... 하면 그냥 접속

즉, keyFile만 설정했다고 해서 바로 “유저 인증 필수” 모드로 바뀌진 않습니다.
유저/비밀번호 강제를 하려면 authorization: enabled 가 결정적 스위치입니다.


2. 그래서 rollout 순서는 이렇게 가면 돼요

당신이 계획한 것처럼:

  1. 지금:
    • authorization: disabled
    • keyFile 없음
    • 아무나 접속 가능
  2. 1단계 – keyFile만 먼저 적용 (auth 여전히 꺼둔 상태)
    • 모든 노드에 /etc/mongo-keyfile 배포
    • mongod.conf에:
    •  
      security: authorization: disabled keyFile: /etc/mongo-keyfile
    • replica set이면 롤링 재시작
    • 이때도 여전히 애플리케이션은 비번 없이 접속 가능
  3. 2단계 – 유저 생성 + 스프링부트에서 계정/비번 사용하도록 변경
    • 아직 authorization: disabled라
      • 유저를 만들어도
      • 비번을 보내도 / 안 보내도
        전부 통과됩니다.
    • 이 상태에서 Spring Boot 설정을 미리:
    •  
      spring.data.mongodb.uri=mongodb://appUser:강력비번@host1,host2,host3/yourDb?replicaSet=rs0&authSource=admin
  4. 3단계 – 마지막에 auth ON
     
    security: authorization: enabled keyFile: /etc/mongo-keyfile
    로 바꾸고, 다시 replica set 롤링 재시작.
    • 이 시점부터는
      • 계정/비번 없이 접속하는 옛날 클라이언트 → 전부 접속 실패
      • 이미 계정/비번 쓰도록 바꾼 Spring Boot → 정상 동작
  5. 이제 정말로:

3. 한 줄로 요약

keyFile 설정 + 재시작만으로는 클라이언트 인증이 강제되지 않습니다.

그래서 원하는 대로
“keyFile 먼저 → 나중에 authorization: enabled”
순서로 점진적 전환(거의 무중단) 진행이 가능합니다.

 

Posted by yongary
,

🎯 요약

단계내용
1 (sudo -iu root 후에)
openssl rand -base64 756 > mongo-keyfile
2 chmod 600 & chown mongod
3 모든 노드 /etc/mongo-keyfile로 SCP
4 mongod.conf에 authorization + keyFile 설정
5 replica set 롤링 재시작
6 rs.status() 확인

3서버에 완전히 동일한 mongo-keyfile이 복사되어야 함

상세 설명:
MongoDB Replica Set에서 auth를 켜려면 노드 간 인증용 keyFile을 동일하게 만들어서 모든 서버에 배포해야 합니다.
AWS EC2(리눅스) 기준으로 MongoDB 5.0.23에서 사용할 수 있는 안전한 방법을 설명할게요.


🔐 Mongo KeyFile 생성 & 배포 절차 (EC2 / Linux)

📌 1) EC2 한 대에서 keyFile 생성

아래 명령을 실행 (지금 로그인한 서버 아무 곳이나 상관없음):

 
openssl rand -base64 756 > mongo-keyfile
  • 파일 길이는 at least 32 bytes 필요하지만
    MongoDB 권장값인 756 bytes 사용 (5.x에서도 OK)
  • 파일 이름은 아무거나 가능 (mongo-keyfile, rs.key, 등)

📌 2) 권한 설정 (반드시 600)

MongoDB는 keyFile 권한이 600 아니면 실행 실패합니다.

 
chmod 600 mongo-keyfile

(소유자는 MongoDB 사용자면 더 안전함)

 
chown mongod:mongod mongo-keyfile # 시스템마다 mongod 또는 mongodb

만약 MongoDB 프로세스가 mongod 유저가 아닌 root로 실행되고 있다면
chown 하지 않아도 됨. (권장 X → 되도록 mongod 유저로)


📌 3) keyFile을 Replica Set 모든 서버에 복사

각 EC2 인스턴스에 SSH로 복사:

 
scp mongo-keyfile ec2-user@<SERVER1_IP>:/etc/ scp mongo-keyfile ec2-user@<SERVER2_IP>:/etc/ scp mongo-keyfile ec2-user@<SERVER3_IP>:/etc/

그리고 각 서버에서 다시 권한 재확인:

 
chmod 600 /etc/mongo-keyfile chown mongod:mongod /etc/mongo-keyfile

3개 노드가 완전히 동일한 keyFile을 사용해야 합니다.
절대 각각 생성하면 안 됩니다.


📌 4) mongod.conf 수정 (각 서버 공통)

/etc/mongod.conf 파일 편집:

 
security: authorization: enabled keyFile: /etc/mongo-keyfile

또는 기존에 security: 블록이 있다면 아래처럼 통합:

 
security: authorization: enabled keyFile: /etc/mongo-keyfile

keyFile 설정 + authorization을 같이 써야 합니다.
keyFile만 넣고 auth 안 넣으면 replica set 인증만 되고 클라이언트는 무조건 실패합니다.


📌 5) 노드별 롤링 재시작

각 서버에서 한 대씩 순서대로:

 
sudo systemctl restart mongod

🚨 주의

Primary를 마지막에 재시작해야 합니다.
Secondary 2대 → Primary 순서.

Primary 먼저 끄면 다운타임 발생.


📌 6) 상태 확인

Primary에 접속한 후:

 
rs.status()

정상 replica set이면 OK.


🧠 추가 Tip (AWS EC2 운영 시 추천)

✔ KeyFile 보안을 위해 권장

 
chmod 400 /etc/mongo-keyfile

MongoDB는 600 또는 400 모두 허용합니다.

✔ EC2 UserData로 자동 배포하는 경우

  • Cloud-init에서 keyFile copy 후 chmod + chown 반드시 적용

✔ KeyFile Backup

  • keyFile은 분실하면 replica set이 절대로 부활 못합니다.
  • 안전한 S3/KMS 또는 Parameter Store에 백업 권장
Posted by yongary
,

1. 개념부터 정리

MongoDB는 크게 두 상태 중 하나입니다.

  1. authorization 꺼짐 (–-auth 없음, security.authorization: disabled)
    • 비번 여부 상관 없이 모든 연결이 풀 권한
    • 클라이언트가 username/password를 보내도 그냥 무시하고 동작
  2. authorization 켜짐 (–-auth, security.authorization: enabled)
    • 모든 요청은 인증 필수
    • 유저/롤 기반 권한 체크 수행

즉, “비번 있어도 되고 없어도 되고” 라는 중간 모드는 없어요.
하지만 auth 끄고 있는 동안에도 미리 계정을 만들어 놓고,
앱 쪽은 비번을 먼저 사용하게 바꾸는 건 가능
합니다.


2. 전체 전략 요약 (거의 무중단 버전)

  1. 현재: auth OFF 상태
  2. 관리용/앱용 사용자 미리 생성 (auth OFF 상태에서)
  3. Spring Boot 설정을 수정해서 username/password로 접속하도록 배포
  4. MongoDB 설정에 auth 켜고, replica set이면 노드별 롤링 재시작

이 순서로 하면:

  • 앱 입장:
    • 1~3단계: 비번 없어도 잘 되고, 비번 보내도 잘 됨 (auth OFF라 그냥 통과)
    • 4단계: auth ON 이후에도 이미 비번을 보내고 있으므로 정상 동작
  • 기존 “비번 안 쓰는 옛날 설정” 앱은
    • 4단계 이후부터는 접속 실패 → 이 때 이미 전부 새 설정으로 바꿔 둔 상태여야 함

3. 단계별로 조금 더 구체적으로

✅ 1단계: 관리자/앱 계정 만들기 (auth OFF 상태에서)

mongosh(또는 mongo)로 접속:

 
mongosh --host <primary-host>:27017

admin DB에서 계정 생성:

 
use admin; // 관리용 계정 (DBA용) db.createUser({ user: "adminUser", pwd: "강력한_비번", roles: [ { role: "userAdminAnyDatabase", db: "admin" }, { role: "dbAdminAnyDatabase", db: "admin" }, { role: "readWriteAnyDatabase", db: "admin" }, ] }); // 애플리케이션용 계정 (권한 최소화) db.createUser({ user: "appUser", pwd: "또_다른_강력한_비번", roles: [ { role: "readWrite", db: "yourAppDb" } // 실제 사용하는 DB명 ] });

주의: 지금은 auth OFF라 사실상 아무 제약 없이 createUser가 되지만,
나중에 auth ON 하면 이 계정들이 진짜로 쓰이게 됩니다.


✅ 2단계: Spring Boot 쪽에서 먼저 비번 사용하도록 변경

application.yml 예시 (mongoTemplate 쓰는 경우도 똑같이 설정):

 
spring: data: mongodb: uri: mongodb://appUser:또_다른_강력한_비번@host1:27017,host2:27017,host3:27017/yourAppDb?replicaSet=rs0&authSource=admin

혹은 application.properties:

 
spring.data.mongodb.uri=mongodb://appUser:또_다른_강력한_비번@host1:27017,host2:27017,host3:27017/yourAppDb?replicaSet=rs0&authSource=admin

그리고 이 설정으로 애플리케이션 배포/롤링 배포를 먼저 합니다.

  • 이 시점에는 Mongo가 auth OFF →
    비번이 틀려도 연결은 그냥 성공합니다.
  • 하지만 나중에 auth ON 하면 이 계정 정보가 실제로 사용되죠.

✅ 3단계: MongoDB에 auth 활성화 (security.authorization: enabled)

3-1. mongod.conf 수정

각 노드의 /etc/mongod.conf (또는 사용 중인 conf)에서:

 
security: authorization: enabled # replica set이면 keyFile도 같이 설정해야 함 (이미 쓰고 있을 수도 있음) # keyFile: /etc/mongo-keyfile

replica set인데 아직 keyFile 안 쓰고 있다면,
별도로 keyfile 생성해서 세 노드에 같은 파일/권한으로 배포해야 합니다.
(이건 내부 노드 간 인증용)

3-2. replica set이라면 롤링 재시작

예: 노드 3대 (primary 1 + secondary 2)라고 가정.

  1. secondary 1
    • mongod stop
    • conf 수정 (authorization: enabled, keyFile 설정)
    • mongod start
    • rs.status()로 정상 동기 확인
  2. secondary 2
    • 위와 동일하게
  3. primary
    • primary를 마지막에 재시작 → 이때 잠깐 primary 선거가 일어날 수 있지만
      Driver가 잘 잡아주면 애플리케이션은 거의 문제없이 넘어감

이 과정을 통해 전체 replica set이 auth ON 상태로 전환됩니다.


4. 진짜 “무중단”이 가능한가?

  • 싱글 인스턴스 MongoDB라면:
    • auth 켜려면 어쨌든 한 번은 mongod 재시작이 필요 → 짧은 다운은 피하기 어려움
  • 3대 replica set이라면:
    • 각 노드를 하나씩 재시작하는 롤링 방식으로
      사용자 입장에서는 거의 무중단에 가깝게 전환 가능
    • 순간적인 primary 선거 동안 짧은 hiccup 정도는 있을 수 있음
    • spring-data-mongodb와 드라이버가 replica set + 재시도를 잘 처리해주면 체감 거의 없음

5. 질문하신 포인트에 대한 답 정리

비번이 없어도 동작하고 비번이 있어도 동작하도록 했다가,
나중에 비번 없이는 안 돌게 바꾸는 게 되나요?

  • Yes, 이 순서로 가능합니다:
    1. auth OFF 상태에서 사용자 계정들 미리 생성
    2. Spring Boot를 먼저 username/password 쓰게 배포
      → 이때는 “비번 있어도/없어도” 모두 동작하는 상태 (auth OFF니까)
    3. MongoDB에 authorization: enabled 설정 후 롤링 재시작
      → 이제는 비번 없이는 안 되는 상태로 전환
Posted by yongary
,

🔥 결론: 하나의 Java 프로세스 안에 있는 thread들을 전부 분리해서 확인할 수 있다

방법은 바로 이것:


✅ 1. Linux에서 Java 프로세스의 thread를 분리해서 보는 방법

명령어

 
top -H -p <java_pid>

예:

 
top -H -p 12345

그럼 하나의 Java 프로세스(PID=12345) 안에 있는 모든 thread(TID) 가 각각 CPU를 얼마나 쓰는지 나옵니다.

예:

 
PID %CPU COMMAND 12345 10.0 java <-- 전체 12347 280.0 java <-- 문제 쓰레드(스레드 ID) 12349 12.3 java 12351 5.6 java
  • 12347: 하나의 TID(thread ID)
  • 이게 CPU 280% 먹고 있음 → 문제 쓰레드

즉, Java 한 개 프로세스 안의 각 thread CPU 사용량을 분리해서 알 수 있음.


✅ 2. 문제 thread가 “Java 내부에서 어떤 함수”를 돌고 있는지도 확인 가능

이건 Java thread dump(jstack)를 사용합니다.


Step A. thread ID(TID)를 16진수로 변환

Java thread dump는 hex 값으로 thread를 표시함.

 
printf "%x\n" 12347

출력 예:

 
303b

Step B. jstack 실행

 
jstack 12345 > /tmp/stack.txt

Step C. thread ID(hex)로 검색

 
grep -n "0x303b" /tmp/stack.txt

그럼 이렇게 나옵니다:

 
"pool-1-thread-3" #32 prio=5 tid=0x00007fda900303b nid=0x303b runnable at com.myapp.service.OrderService.process(OrderService.java:182) at com.myapp.service.OrderService.runLoop(OrderService.java:155) at java.lang.Thread.run(Thread.java:748)

여기서:

  • nid=0x303b ← 이게 바로 문제 thread
  • process() 함수에서 CPU를 많이 먹고 있음
  • runLoop() 안에서 무한 loop가 돌고 있을 수 있음

이렇게 정확한 함수 이름 + 라인번호까지 알 수 있습니다.


🔥 추가: thread 이름까지 알 수 있음

Java에서는 thread를 만들 때 이름을 붙이면

 
new Thread(() -> {...}, "Worker-1").start();

jstack에서 이렇게 보입니다:

 
"Worker-1" #25 prio=5 ...

즉,

  • thread 이름
  • thread 상태
  • thread call stack
  • CPU 점유율
  • lock(JVM monitor) 보유 여부

전부 확인 가능.


🔥 추가: thread 목록 전체 보기

 
jcmd <pid> Thread.print

또는 자동으로 thread hierarchy 확인 가능.


🔥 결론 요약

하나의 Java 프로그램이라도 내부 thread들은 개별 TID를 가지므로
CPU 사용량, 함수 위치, thread 이름 등을 전부 분리해서 확인 가능하다.

✔ top -H -p PID → thread CPU 식별
✔ printf "%x" TID → hex 변환
✔ jstack PID → thread dump
✔ grep hex → problem thread 찾기
✔ 어떤 함수인지 100% 확인 가능


원하면 지금 사용 중인 Java 서버에서
문제가 되는 thread를 실시간으로 찾는 쉘 스크립트도 만들어줄게!

나의 말:

thread ID는 찾아도 어떤 코드인지 알수는 없는건가요?

ChatGPT의 말:

알 수 있습니다.
단순히 “thread ID 숫자만 보인다 → 코드 위치는 모른다”가 아니라,
thread ID(TID) → jstack → Java 함수명 + 파일명 + 라인번호까지 정확히 찾을 수 있습니다.

즉,

thread ID만 알면 “어떤 코드(어떤 함수)가 문제인지 100% 추적 가능합니다.”

아래를 보면 왜 가능한지 이해됩니다.


🔥 핵심 개념:

리눅스에서 보이는 thread ID(TID)는 JVM 내부 스레드의 nid(native thread id)와 1:1로 매핑됩니다.

예를 들어 top에서:

 
TID = 12347

라고 CPU를 많이 먹는 스레드가 보이면,

  • 이걸 HEX로 바꾸고
  • jstack에서 해당 TID를 검색하면
  • 그 스레드가 어떤 Java 코드(메소드) 를 실행 중인지 정확히 알 수 있습니다.

실제로 이렇게 나옵니다:


🚀 실전 예제 (이게 실제로 나오는 화면)

Step1 — CPU 많이 먹는 thread 찾기

 
top -H -p <java_pid>

예:

 
12347 280% java ← 문제 스레드

Step2 — 10진수를 16진수(hex)로 변환

 
printf "%x\n" 12347

출력:

 
303b

Step3 — jstack 덤프 생성

 
jstack <java_pid> > dump.txt

Step4 — 문제 스레드 검색

 
grep -n "0x303b" dump.txt

🔥 실제로 보이는 결과

 
"pool-4-thread-1" #47 prio=5 os_prio=0 tid=0x00007fda900303b nid=0x303b runnable at com.company.payment.PaymentService.calculateFee(PaymentService.java:183) at com.company.payment.PaymentWorker.run(PaymentWorker.java:92) at java.lang.Thread.run(Thread.java:748)

즉, thread ID 하나만으로

  • 어떤 클래스인지
  • 어떤 메소드인지
  • 몇 번째 줄(line number)인지
    정확히 나온다.

여기서는:

  • calculateFee() 함수가 CPU를 폭주시키고 있음
  • 파일: PaymentService.java
  • 라인: 183

이 수준까지 바로 알 수 있습니다.


💡 정리하면

❗ thread ID 자체로는 코드 이름이 안 보이지만

❗ thread ID → jstack 검색을 하면

👉 스레드가 실행 중인 메소드
👉 메소드가 있는 클래스
👉 Java 파일 이름
👉 라인 번호
👉 Lock held 여부
👉 JVM 상태(runnable / waiting / blocked)

까지 전부 알 수 있습니다.


🎯 결론

thread ID만 알면 어떤 코드가 문제인지 100% 찾아낼 수 있습니다.
단순한 추론이 아니라, 실제 Java stack trace에 메소드/라인번호가 정확히 찍힙니다.

Posted by yongary
,