728x90
반응형

 

환경 : OpenAI / Claude 등 LLM API 공통

목표 : 같은 모델에서 답 품질을 끌어올리는 실전 프롬프트 패턴 (AI 입문 시리즈 2/4)






1. 역할 부여

프롬프트 맨 앞에 "너는 누구다"를 박아주면 답의 톤과 깊이가 달라짐. 같은 질문도 부여한 역할에 따라 전혀 다른 수준으로 나옴.

 

// 모호한 프롬프트
이 코드 리뷰해줘

// 역할 + 기준 부여
너는 10년차 백엔드 개발자다.
아래 Kotlin 코드를 보안과 성능 관점에서만 리뷰.
지적마다 수정 코드도 함께 제시.

 

역할이 먹히는 이유는 모델의 답이 모이는 방향이 바뀌기 때문임. 막연한 질문엔 학습 데이터의 평균치로 두루뭉술하게 답하다가, 역할이 주어지면 그 분야 전문가가 쓸 법한 어휘와 판단 기준 쪽으로 답의 분포가 좁혀짐. 즉 역할은 없던 지식을 더해주는 게 아니라, 이미 가진 지식 중 어디를 꺼낼지 방향을 잡아주는 장치임.

 

그래서 한계도 분명함. 역할을 줘도 모르는 사실을 지어내는 환각은 그대로 남음 — 역할은 관점과 깊이를 바꿀 뿐 정확성까지 보장하진 않음. 한편 역할은 system 메시지에 넣으면 대화 내내 유지되므로, 매 질문마다 반복할 필요가 없음.






2. 구체적으로 지시

모호한 단어를 빼고 측정 가능한 조건으로 바꿈. "잘", "간단히", "적당히" 같은 말은 모델이 제멋대로 해석함.

 

모호한 단어를 모델은 학습 분포의 평균값으로 해석함. "간단히"가 사람마다 다르듯 모델도 매번 다른 길이로 잡음. 길이·대상·형식을 숫자로 못 박으면 해석의 폭이 좁아져 결과가 일정해짐.

 

// 모호함 — 길이도 형식도 안 정해짐
요약해줘

// 구체적 — 길이, 대상, 제약을 명시
아래 글을 3문장으로 요약.
비전공자도 이해할 수 있게, 전문 용어는 쉽게 풀어서.

 

제약을 거는 게 핵심인데, "하지 마라"보다 "이렇게 해라"가 더 잘 먹힘. 부정 지시는 무엇을 하지 말지만 알려줄 뿐 그럼 뭘 해야 하는지는 안 알려줘서, 모델이 그 빈칸을 또 제멋대로 채우기 때문.

 

// 약함
너무 길게 쓰지 마

// 강함
각 항목은 한 문장, 최대 5개 항목으로 작성






3. 출력 형식 고정

응답을 코드로 받아 처리할 거면 형식을 못 박아야 함. 형식을 안 정하면 매번 다른 모양으로 와서 파싱이 깨짐.

 

// 형식 지정 없음 → 줄글로 옴, 파싱 불가

// 형식 고정 → 항상 같은 구조
아래 리뷰를 분석.
다음 JSON 형식으로만 답하고 다른 말은 붙이지 마라.
{"sentiment": "긍정|부정|중립", "score": 1~5, "keywords": []}

 

※ "다른 말은 붙이지 마라"가 중요함. 안 그러면 앞에 "네, 분석해드리겠습니다" 같은 군더더기가 붙어서 JSON 파싱이 깨짐.

 

형식을 더 확실히 지키게 하려면 프롬프트로만 부탁하지 말고 두 가지를 같이 씀. 1편에서 다룬 temperature를 낮춰 무작위성을 줄이고, 모델이 지원하면 JSON 모드나 structured output(스키마 강제) 기능을 켜는 것. 프롬프트 문장 하나로 형식을 비는 것보다 훨씬 안정적임.






4. 예시 제공 (Few-shot)

설명보다 예시 한두 개가 훨씬 강력함. 원하는 입출력 쌍을 보여주면 모델이 그 패턴을 그대로 따라함.

 

// 입력 → 출력 예시를 먼저 보여줌 (Few-shot)
다음 형식으로 회사명을 정규화해라.

입력: "삼성전자(주)" → 출력: 삼성전자
입력: "(주)카카오" → 출력: 카카오
입력: "네이버 주식회사" → 출력: ?

 

예시가 0개면 zero-shot, 1개 이상이면 few-shot임. 모델은 예시의 입력→출력 쌍에서 형식과 규칙을 거꾸로 추론해 흉내 내므로, 예시의 형태가 일관될수록 정확도가 올라감. 분류·포맷 변환·추출처럼 정답 모양이 정해진 작업에서 특히 효과가 큼.

 

다만 예시 나열만으로 안 되는 영역이 있음. 여러 단계를 거쳐야 하는 추론·계산 문제는 예시를 줘도 자주 틀리는데, 이때 필요한 게 다음 패턴인 단계적 사고 유도임.






5. 단계적 사고 유도

복잡한 추론은 "바로 답하지 말고 단계적으로 생각하라"를 넣으면 정답률이 올라감. 모델이 중간 과정을 거치며 실수를 줄이기 때문.

 

// 바로 답 요구 → 계산 실수 잦음
이 주문들 총 할인액은?

// 단계 유도 → 과정을 거쳐 정확해짐
각 주문의 할인액을 하나씩 계산한 뒤, 마지막에 합계를 내라.

 

효과의 이유는 단순함. 답을 한 번에 뱉으면 중간 계산을 건너뛰다 틀리는데, 과정을 글로 쓰게 하면 각 단계가 다음 단계의 근거가 되어 오류가 누적되기 전에 드러남.

 

참고로 o1 같은 추론 특화 모델은 이 과정을 내부적으로 이미 수행하므로, 이 기법은 일반 모델에서 특히 효과적임.

 

또한 입력 데이터와 지시를 섞지 말고 구분자로 분리하면 어디까지가 데이터인지 명확해짐.

 

아래 ``` 안의 텍스트만 번역해라. 지시문은 번역하지 마라.
```
{사용자 입력}
```






모델을 바꾸기 전에 프롬프트부터 손보는 게 먼저. 같은 모델에서도 위 5가지로 체감 품질이 크게 달라짐.

 

728x90
반응형
728x90
반응형

[Spring] @Value 주입이 null인 경우

환경 : Spring Boot 3.x, Kotlin 1.9



1. 원인

  • static 필드에 직접 사용 — Spring이 주입 불가
  • new 키워드로 생성한 객체 — Spring 빈이 아님
  • 생성자 호출 시점에 다른 빈에서 해당 필드 참조 — 초기화 순서 문제
  • property 키 오타 또는 application.yml 누락
  • SpEL 표현식 오류


2. 해결 방법

2.1) static 필드 금지 — 생성자 주입 사용

Spring은 인스턴스 필드에만 의존성 주입 가능. static 필드는 클래스 레벨이라 주입 안 됨

// 동작 안 함
@Component
class AppConfig {
    companion object {
        @Value("\${app.name}")
        lateinit var appName: String // null
    }
}

// 정상
@Component
class AppConfig(
    @Value("\${app.name}") val appName: String
)

어쩔 수 없이 static처럼 써야 하면 setter trick

@Component
class AppConfig {
    companion object {
        lateinit var appName: String
            private set
    }

    @Value("\${app.name}")
    fun setAppNameStatic(value: String) {
        appName = value
    }
}

하지만 위 방식보다는 @ConfigurationProperties로 객체로 관리하는 게 더 깔끔


2.2) new 키워드 금지 — 빈으로 등록

new로 만든 객체는 Spring 컨테이너 밖이라 @Value 동작 안 함

// 동작 안 함
class EmailService {
    @Value("\${mail.from}")
    lateinit var from: String
}

@Service
class NotificationService {
    private val emailService = EmailService() // new와 동일 → @Value 무시
}
// 정상 — 빈으로 등록
@Component
class EmailService(
    @Value("\${mail.from}") val from: String
)

@Service
class NotificationService(
    private val emailService: EmailService // DI
)

2.3) 초기화 순서 문제 — @PostConstruct에서 사용

생성자 시점에 다른 필드를 참조하면 주입 전 상태일 수 있음

// 동작 안 함
@Component
class AppConfig {
    @Value("\${app.name}")
    lateinit var appName: String

    val fullName = "App: $appName" // 생성자 시점 → null
}

// 정상 — @PostConstruct 사용
@Component
class AppConfig {
    @Value("\${app.name}")
    lateinit var appName: String

    lateinit var fullName: String

    @PostConstruct
    fun init() {
        fullName = "App: $appName" // 주입 완료 후 실행
    }
}

2.4) property 키 확인 — default 값 설정

오타나 키 누락 시 기본값으로 방어

// 키 오타나 누락 시 예외 발생
@Value("\${app.nmae}") // 오타
lateinit var appName: String

// default 값 사용
@Value("\${app.name:DefaultApp}")
lateinit var appName: String

숫자나 boolean도 동일

@Value("\${app.timeout:30}")
var timeout: Int = 0

@Value("\${app.enabled:true}")
var enabled: Boolean = false

2.5) SpEL 표현식 오류

#{...}는 SpEL, ${...}는 property placeholder. 헷갈리지 않기

// property 값
@Value("\${app.name}")
lateinit var appName: String

// SpEL 표현식
@Value("#{T(java.lang.Math).random() * 100}")
var randomValue: Double = 0.0

// property를 SpEL에서 사용
@Value("#{\${app.timeout} * 1000}")
var timeoutMs: Long = 0

SpEL 문법 오류 시 앱 시작 자체가 실패하므로 쉽게 발견됨


2.6) @ConfigurationProperties 권장

여러 property를 다룰 때는 @Value보다 @ConfigurationProperties가 안전

// application.yml
app:
  name: MyApp
  timeout: 30
  mail:
    from: no-reply@example.com
    host: smtp.example.com
@ConfigurationProperties(prefix = "app")
@ConstructorBinding
data class AppProperties(
    val name: String,
    val timeout: Int,
    val mail: MailProperties
)

data class MailProperties(
    val from: String,
    val host: String
)

@EnableConfigurationProperties(AppProperties::class)
@SpringBootApplication
class Application
@Service
class NotificationService(
    private val appProperties: AppProperties
) {
    fun send() {
        println("앱: ${appProperties.name}, 발신: ${appProperties.mail.from}")
    }
}

타입 안전성 + IDE 자동완성 + 검증 통합 가능



3. 디버깅

3.1) Environment에서 직접 확인

property가 실제로 로드됐는지 확인

@Component
class DebugConfig(private val env: Environment) {
    @PostConstruct
    fun check() {
        val appName = env.getProperty("app.name")
        println("app.name = $appName") // null이면 yml 파일 확인
    }
}

3.2) default 값으로 누락 확인

@Value("\${app.name:NOT_FOUND}")
lateinit var appName: String

@PostConstruct
fun check() {
    if (appName == "NOT_FOUND") {
        println("app.name property 누락")
    }
}

3.3) 주입 시점 로그

@Component
class AppConfig(
    @Value("\${app.name}") val appName: String
) {
    init {
        println("생성자 시점 appName: $appName")
    }

    @PostConstruct
    fun postConstruct() {
        println("@PostConstruct 시점 appName: $appName")
    }
}

init@PostConstruct 모두에서 찍히면 정상. init에서만 찍히고 값이 이상하면 주입 실패


※ 생성자 주입 + @ConfigurationProperties가 가장 안전. @Value는 간단한 값 하나만 쓸 때 사용


728x90
반응형
728x90
반응형

[Spring] @Scheduled 멀티 인스턴스 중복 실행

환경 : Spring Boot 3.x, Kotlin 1.9, Kubernetes



1. 원인

  • 여러 pod/replica가 동일한 스케줄 코드 보유 — 각자 독립적으로 실행
  • 배치 작업이 멱등하지 않으면 데이터 중복 생성/처리
  • 분산 락 없이 여러 인스턴스가 동시 작업


2. 해결 방법

2.1) ShedLock — 분산 락 기반 스케줄 중복 방지

가장 간단한 방법. Redis/MongoDB/JDBC 등의 공유 저장소에 락을 기록해서 한 번에 한 인스턴스만 실행

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
    implementation("net.javacrumbs.shedlock:shedlock-spring:5.9.1")
    implementation("net.javacrumbs.shedlock:shedlock-provider-mongo:5.9.1")
}
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class ScheduleConfig

@Bean
fun lockProvider(mongoClient: MongoClient): LockProvider {
    return MongoLockProvider(mongoClient.getDatabase("mydb"))
}
@Component
class ScheduledJob {
    @Scheduled(cron = "0 0 * * * *") // 매시 정각
    @SchedulerLock(
        name = "dailyReport",
        lockAtMostFor = "5m", // 최대 5분 후 락 자동 해제
        lockAtLeastFor = "1m"  // 최소 1분간 락 유지
    )
    fun generateDailyReport() {
        println("[${LocalDateTime.now()}] 리포트 생성 시작 (pod: ${System.getenv("HOSTNAME")})")
        // 실제 배치 로직
    }
}

lockAtMostFor — 작업이 비정상 종료되도 락이 영원히 안 풀리지 않도록 최대 시간 설정

lockAtLeastFor — 작업이 너무 빨리 끝나도 일정 시간은 락 유지 (다른 인스턴스가 바로 또 실행하지 않도록)


2.2) ShedLock with JDBC

MongoDB 대신 DB 사용하는 경우

// build.gradle.kts
dependencies {
    implementation("net.javacrumbs.shedlock:shedlock-spring:5.9.1")
    implementation("net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.9.1")
}
@Bean
fun lockProvider(dataSource: DataSource): LockProvider {
    return JdbcTemplateLockProvider(
        JdbcTemplateLockProvider.Configuration.builder()
            .withJdbcTemplate(JdbcTemplate(dataSource))
            .usingDbTime()
            .build()
    )
}

DB에 shedlock 테이블 생성 필요

-- PostgreSQL 예시
CREATE TABLE shedlock (
    name VARCHAR(64) NOT NULL,
    lock_until TIMESTAMP NOT NULL,
    locked_at TIMESTAMP NOT NULL,
    locked_by VARCHAR(255) NOT NULL,
    PRIMARY KEY (name)
);

2.3) ShedLock with Redis

// build.gradle.kts
dependencies {
    implementation("net.javacrumbs.shedlock:shedlock-spring:5.9.1")
    implementation("net.javacrumbs.shedlock:shedlock-provider-redis-spring:5.9.1")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
}
@Bean
fun lockProvider(connectionFactory: RedisConnectionFactory): LockProvider {
    return RedisLockProvider(connectionFactory)
}

2.4) Quartz Cluster 모드

고급 스케줄링 기능(동적 스케줄 등록/삭제, 실패 재시도)이 필요하면 Quartz 사용

// build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-quartz")
# application.yml
spring:
  quartz:
    job-store-type: jdbc
    properties:
      org.quartz.jobStore.isClustered: true
      org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
      org.quartz.scheduler.instanceId: AUTO

Quartz 테이블 스키마는 공식 문서 참고. ShedLock보다 무겁지만 분산 환경에서 안정적


2.5) Leader Election 패턴

Kubernetes 환경에서는 한 pod만 leader로 선출해서 스케줄 실행

// build.gradle.kts
implementation("org.springframework.integration:spring-integration-zookeeper")
@Configuration
class LeaderConfig {
    @Bean
    fun leaderInitiator(curatorFramework: CuratorFramework): LeaderInitiator {
        return LeaderInitiator(curatorFramework, Candidate("schedulerLeader", Runnable {
            println("리더 선출됨")
        }))
    }
}

@Component
class ScheduledJob(private val leaderInitiator: LeaderInitiator) {
    @Scheduled(fixedRate = 60000)
    fun task() {
        if (leaderInitiator.context.isLeader) {
            println("리더만 실행")
        }
    }
}

Zookeeper/etcd 같은 별도 인프라 필요. 대부분 환경에서는 ShedLock이 더 간단


2.6) Profile 격리 — 단일 인스턴스 전용

스케줄 작업을 특정 profile에서만 활성화

@Component
@Profile("scheduler")
class ScheduledJob {
    @Scheduled(cron = "0 0 * * * *")
    fun generateReport() { ... }
}

배포 시 한 pod만 SPRING_PROFILES_ACTIVE=scheduler 환경변수 주입. 나머지 pod는 스케줄 코드 자체가 빈 등록 안 됨

간단하지만 해당 pod가 죽으면 스케줄 중단됨 — 고가용성 낮음



3. 디버깅

3.1) 로그에 hostname/pod 이름 출력

여러 인스턴스 중 어디서 실행됐는지 확인

@Component
class ScheduledJob {
    @Scheduled(cron = "0 * * * * *")
    @SchedulerLock(name = "test", lockAtMostFor = "1m")
    fun task() {
        val hostname = System.getenv("HOSTNAME") ?: "local"
        println("[${LocalDateTime.now()}] 실행됨 (pod: $hostname)")
    }
}

ShedLock 적용 전에는 pod 개수만큼 로그 찍힘. 적용 후에는 하나만 찍혀야 정상


3.2) ShedLock 락 상태 확인

MongoDB 예시

db.shedLock.find()

JDBC 예시

SELECT * FROM shedlock;

lock_until 시간이 현재보다 미래면 락 유지 중


※ ShedLock이 가장 범용적. 간단한 배치는 ShedLock, 복잡한 스케줄 관리는 Quartz Cluster, 인프라 통제 가능하면 Leader Election


728x90
반응형

+ Recent posts