본문 바로가기
3.1 Java_Backend/SpringBoot

[springboot] @Value와 싱글톤 공유 문제로부터의 교훈

by Dohi._. 2025. 6. 2.
728x90

문제 상황

개발 중 GPT 추천 기능에서 @Value("${openai.prompt}")로 주입된 프롬프트 템플릿 문자열을 기반으로 사용자의 MBTI, 선택지, 목표 등을 바꿔서 요청하는 로직을 만들었다.
처음에는 문제없이 잘 동작했다고 생각했지만, 서버 배포 후 이전 사용자의 값이 그대로 반영되어 응답되는 버그가 발생했다.

 

프론트엔드 : 어 .. 전 분명 토마토와 각종 과일중에 먹을걸 추천해달라했는데 산과 바다중에 고민해요..?!

 

 

문제의 코드

//... 생략
@Service
public class RecommendService {

    @Value("${openai.prompt}")
    private String promptTemplate;
//... 생략
    
        private String putValuesInPrompt(String retry, String choices, String setting, String goals, Mbti mbti) {
        Map<String, String> values = new HashMap<>();
//... 생략
        values.put("choices", choices);
        values.put("setting", setting);
//... 생략
        for (Map.Entry<String, String> entry : values.entrySet()) {
            promptTemplate = promptTemplate.replace("{{ " + entry.getKey() + " }}", entry.getValue());
        }
        return promptTemplate;
    }
  }

 

문제 분석

RecommendService는 스프링의 @Service를 붙인 싱글톤 객체

@Value로 주입된 String promptTemplate 필드는 클래스의 공유 상태로 존재

초기에 promptTemplate.replace(...)로 직접 치환 로직을 처리했는데, 이를 반복하면서 결국 공유 필드가 오염되었다.

로컬 디버깅에서는 한번 요청이라 넘어갔었다, 서버는 동시에 여러 요청을 처리하기 때문에 이전 사용자의 값이 남는 현상발생

 

수정후

String prompt = new Stirng(promptTemplate); // 원본은 그대로 깊은복사
//String prompt = promptTemplate; // 원본은 그대로? (얕은 복사)
for (Map.Entry<String, String> entry : values.entrySet()) {
    prompt = prompt.replace("{{ " + entry.getKey() + " }}", entry.getValue());
}
return prompt;

 

replace로 인해서 얕은 복사를 해도 상관이 없다.
String은 불변객체이기 때문에 .replace(),.substring()등의 메서드는 원래 문자열을 바꾸지 않고,새로운 문자열 객체를 반환
-> 앞에서 얕은 복사를 하여도 뒤에서 깊은복사 발생 

 

교훈

 

  • 멀티스레드 환경에서 공유 상태는 절대 건드리지 말자. 상태가 필요하다면 로컬 변수로만 처리하자.
  • 로컬에서는 한사용자로 테스트를 진행하면서 안 보이던 문제들이 서버에서 동시 요청을 통해 드러날 수 있다는 점도 새삼 실감했다.

명심할점

 

  • 상태가 공유될 수 있는 모든 필드는 항상 신중하게 다룰 것
  • 멀티스레드 환경에서의 동작을 염두에 두고 설계, 테스트, 코드 리뷰를 진행할 것

 

 

 

 

728x90

댓글